Skip to content

Commit f73c545

Browse files
committed
refactor(driver-sql): move introspectSchema's body out of sql-driver.ts into sql-driver-introspection.ts (#25521 cut 4)
Move-only. The body of SqlDriver#introspectSchema moves unedited into cut 1's module and reaches the class through cut 1's IntrospectionHost, which gains the five members the body reads that it did not list yet (dialectName as a getter; introspectColumns, introspectForeignKeys, introspectPrimaryKeys and introspectUniqueConstraints as late-bound closures). The method keeps its name, signature and position. postgresTableNameCollisionError, read only by this body, stays in sql-driver.ts and gains a leading export. listPhysicalColumns stays in the class: its body is shorter than the host literal a stub would need. sql-driver.ts goes from 21,678 to 21,595 lines. Claude-Session: https://claude.ai/code/session_01JVTd3PprStGa26aez5omLB Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6021b5e commit f73c545

2 files changed

Lines changed: 165 additions & 113 deletions

File tree

‎packages/drivers/driver-sql/src/sql-driver-introspection.ts‎

Lines changed: 141 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
* catalog reads `introspectSchema` and the schema-sync drift checks are built
88
* on — moved out of `sql-driver.ts` whole.
99
*
10+
* [#25521 cut 4] `introspectSchema`, the public whole-schema read those
11+
* helpers serve, moved here too: it lists the tables per dialect and reads each
12+
* one through four of them. `listPhysicalColumns`, the other member of the
13+
* class's Schema Introspection section, stayed in the class: its body is
14+
* shorter than the host literal a stub would need, so moving it would make
15+
* `sql-driver.ts` longer, not shorter.
16+
*
1017
* A MOVE, not a redesign. Each function below is the body of the `SqlDriver`
1118
* method of the same name as it stood, re-indented one level and otherwise
1219
* unedited: the same catalog statements, the same dialect arms, the same
@@ -27,22 +34,39 @@
2734
*
2835
* `introspectColumns` reads `introspectColumnOrder` through a closure that
2936
* calls it on the driver at call time, so a subclass override or a test spy on
30-
* the instance is still the one the body reaches.
37+
* the instance is still the one the body reaches. `introspectSchema` reads the
38+
* four helpers it calls the same way, so `TursoDriver`'s override of
39+
* `introspectColumns` (and a test spy on the instance) is still the one it
40+
* reaches. `TursoDriver` also overrides `introspectSchema` itself and calls
41+
* `super`, which reaches the class method, which hands this body its host.
3142
*
32-
* The field `knex` and the three dialect getters go as GETTERS, so each read
33-
* happens when the body makes it, as it did on the class.
43+
* The field `knex`, the three dialect getters and the getter `dialectName` go
44+
* as GETTERS, so each read happens when the body makes it, as it did on the
45+
* class.
3446
*
3547
* The two module-level declarations the bodies read, `UniqueConstraintMember`
3648
* and `singleColumnUniqueColumns`, stay in `sql-driver.ts`, which exports them
3749
* already (a test imports `singleColumnUniqueColumns` from there), and are
3850
* imported from there: the module cycle is evaluation-safe because the one
3951
* value import is read only inside a function body, never while the two
40-
* modules are first evaluated. `index.ts` re-exports nothing from this module,
52+
* modules are first evaluated. `introspectSchema` also reads the types
53+
* `IntrospectedSchema` and `IntrospectedTable`, which `sql-driver.ts` exports
54+
* already, and the function `postgresTableNameCollisionError`, which only
55+
* this body reads: it stays in `sql-driver.ts` and gained a leading `export`
56+
* there (the shape cut 2 applied), so it is imported, not copied, and is read
57+
* only inside the body too. `index.ts` re-exports nothing from this module,
4158
* so the published surface does not move.
4259
*/
4360

44-
import type { IntrospectedColumn, IntrospectedForeignKey, SqlDriver, UniqueConstraintMember } from './sql-driver.js';
45-
import { singleColumnUniqueColumns } from './sql-driver.js';
61+
import type {
62+
IntrospectedColumn,
63+
IntrospectedForeignKey,
64+
IntrospectedSchema,
65+
IntrospectedTable,
66+
SqlDriver,
67+
UniqueConstraintMember,
68+
} from './sql-driver.js';
69+
import { postgresTableNameCollisionError, singleColumnUniqueColumns } from './sql-driver.js';
4670

4771
/**
4872
* The `SqlDriver` members the functions below read — every one of them, and
@@ -53,7 +77,12 @@ import { singleColumnUniqueColumns } from './sql-driver.js';
5377
* for which members are getters.
5478
*/
5579
export interface IntrospectionHost {
80+
readonly dialectName: SqlDriver['dialectName'];
5681
introspectColumnOrder: SqlDriver['introspectColumnOrder'];
82+
introspectColumns: SqlDriver['introspectColumns'];
83+
introspectForeignKeys: SqlDriver['introspectForeignKeys'];
84+
introspectPrimaryKeys: SqlDriver['introspectPrimaryKeys'];
85+
introspectUniqueConstraints: SqlDriver['introspectUniqueConstraints'];
5786
readonly isMysql: SqlDriver['isMysql'];
5887
readonly isPostgres: SqlDriver['isPostgres'];
5988
readonly isSqlite: SqlDriver['isSqlite'];
@@ -663,3 +692,109 @@ export async function introspectUniqueConstraints(
663692

664693
return singleColumnUniqueColumns(members);
665694
}
695+
696+
/** The body of `SqlDriver#introspectSchema`; the method's signature there is its contract. */
697+
export async function introspectSchema(this: Pick<IntrospectionHost, 'dialectName' | 'introspectColumns' | 'introspectForeignKeys' | 'introspectPrimaryKeys' | 'introspectUniqueConstraints' | 'isMysql' | 'isPostgres' | 'isSqlite' | 'knex'>): Promise<IntrospectedSchema> {
698+
const tables: Record<string, IntrospectedTable> = {};
699+
// Stamped BEFORE the reads, not after: a consumer asking "has the remote
700+
// changed since this snapshot?" must not be told the snapshot covers a
701+
// moment later than the first table it actually read.
702+
const introspectedAt = new Date().toISOString();
703+
let tableNames: string[] = [];
704+
// PostgreSQL only (#24707): the schema each listed table lives in, so its
705+
// columns are read THERE. Every other dialect leaves it empty.
706+
const schemaByTable = new Map<string, string>();
707+
708+
if (this.isPostgres) {
709+
// Every schema the session can actually reach unqualified, rather than
710+
// the literal `'public'` — same answer for a default deployment, where
711+
// `current_schemas(false)` is exactly `{public}`, and the right answer for
712+
// a session pointed at another schema, where the literal listed nothing.
713+
//
714+
// `table_schema` and its `search_path` position are selected too
715+
// (#24707). The result is keyed by BARE name, so two schemas holding the
716+
// same name used to collapse into one key with the later table silently
717+
// dropped — refused below instead. And the schema is what the column read
718+
// needs: knex `columnInfo()` on a bare name reads `current_schema()`, the
719+
// FIRST schema on the path only, so a table in a later schema reported
720+
// zero columns. No `ORDER BY` is added: the key order of a single-schema
721+
// answer stays exactly what it was.
722+
const result = await this.knex.raw(`
723+
SELECT table_schema, table_name,
724+
array_position(current_schemas(false), table_schema::name) AS path_position
725+
FROM information_schema.tables
726+
WHERE table_schema = ANY (current_schemas(false))
727+
AND table_type = 'BASE TABLE'
728+
`);
729+
const rows = result.rows as { table_schema: string; table_name: string; path_position: number }[];
730+
const schemasByTable = new Map<string, { schema: string; position: number }[]>();
731+
for (const row of rows) {
732+
const seen = schemasByTable.get(row.table_name);
733+
const entry = { schema: row.table_schema, position: Number(row.path_position) };
734+
if (seen) seen.push(entry);
735+
else schemasByTable.set(row.table_name, [entry]);
736+
}
737+
const collisions = [...schemasByTable]
738+
.filter(([, schemas]) => schemas.length > 1)
739+
.map(([table, schemas]) => ({
740+
table,
741+
schemas: schemas.sort((a, b) => a.position - b.position).map((s) => s.schema),
742+
}))
743+
.sort((a, b) => (a.table < b.table ? -1 : a.table > b.table ? 1 : 0));
744+
if (collisions.length > 0) throw postgresTableNameCollisionError(collisions);
745+
746+
tableNames = rows.map((row) => row.table_name);
747+
for (const row of rows) schemaByTable.set(row.table_name, row.table_schema);
748+
} else if (this.isMysql) {
749+
const result = await this.knex.raw(`
750+
SELECT table_name
751+
FROM information_schema.tables
752+
WHERE table_schema = DATABASE()
753+
AND table_type = 'BASE TABLE'
754+
`);
755+
tableNames = result[0].map((row: any) => row.TABLE_NAME);
756+
} else if (this.isSqlite) {
757+
const result = await this.knex.raw(`
758+
SELECT name as table_name
759+
FROM sqlite_master
760+
WHERE type='table'
761+
AND name NOT LIKE 'sqlite_%'
762+
`);
763+
tableNames = result.map((row: any) => row.table_name);
764+
}
765+
766+
for (const tableName of tableNames) {
767+
// All four reads take the #7332 default (`onFailure: 'throw'`): this
768+
// caller cannot correct a short read, and `introspectColumns` has never
769+
// swallowed — so a partially-readable database fails the whole-schema
770+
// introspection loudly instead of emitting tables whose keys silently
771+
// read as absent (#11161). Every in-tree caller of `introspectSchema`
772+
// already handles a throw (the datasource health check reports
773+
// `{ ok: false }`, the REST/CLI seams surface the error).
774+
// On PostgreSQL the columns are read in the table's OWN schema (#24707);
775+
// the three key reads below already scope to `current_schemas(false)`
776+
// or resolve along `search_path`, which the refusal above makes
777+
// unambiguous among base tables.
778+
const columns = await this.introspectColumns(tableName, schemaByTable.get(tableName));
779+
const foreignKeys = await this.introspectForeignKeys(tableName);
780+
const primaryKeys = await this.introspectPrimaryKeys(tableName);
781+
const uniqueConstraints = await this.introspectUniqueConstraints(tableName);
782+
783+
for (const col of columns) {
784+
if (primaryKeys.includes(col.name)) col.primaryKey = true;
785+
if (uniqueConstraints.includes(col.name)) col.isUnique = true;
786+
}
787+
788+
tables[tableName] = { name: tableName, columns, foreignKeys, primaryKeys };
789+
}
790+
791+
// `dialectName` — not the raw Knex client, and not the spec's
792+
// `SQLDialectSchema` enum. The only in-tree consumer of this key is
793+
// `suggestFieldTypeForSqlType(col.type, schema.dialect as SqlDialect)`,
794+
// whose `SqlDialect` vocabulary (`packages/spec/src/data/type-compat.ts`)
795+
// spells PostgreSQL `postgres`, exactly as `dialectName` does. Emitting
796+
// the enum's `postgresql` instead would put the key in `Object.keys()`
797+
// while leaving every per-dialect type alias unreachable — the omission
798+
// this repairs, wearing a fix's clothes.
799+
return { tables, dialect: this.dialectName, introspectedAt };
800+
}

‎packages/drivers/driver-sql/src/sql-driver.ts‎

Lines changed: 24 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,15 @@ import {
160160
} from './builtin-column-collision.js';
161161
import { recoverUnencodedJsonText } from './unencoded-json-text.js';
162162
// [#25521 cut 1] The catalog-introspection helpers (`introspectColumnOrder`,
163-
// `introspectColumns` and the foreign-key, primary-key and unique-constraint reads);
164-
// the methods of the same names below hand each body the members it reads
165-
// (`IntrospectionHost`).
163+
// `introspectColumns` and the foreign-key, primary-key and unique-constraint reads),
164+
// and [cut 4] `introspectSchema`, the whole-schema read built on them; the methods
165+
// of the same names below hand each body the members it reads (`IntrospectionHost`).
166166
import {
167167
introspectColumnOrder,
168168
introspectColumns,
169169
introspectForeignKeys,
170170
introspectPrimaryKeys,
171+
introspectSchema,
171172
introspectUniqueConstraints,
172173
} from './sql-driver-introspection.js';
173174
// [#25521 cut 2] The raw-execution path (`execute`, its refusal terminal
@@ -4960,7 +4961,7 @@ function classifyFilterKey(
49604961
*
49614962
* `collisions` is non-empty; each entry's `schemas` has two or more members.
49624963
*/
4963-
function postgresTableNameCollisionError(collisions: ReadonlyArray<{ table: string; schemas: string[] }>): Error {
4964+
export function postgresTableNameCollisionError(collisions: ReadonlyArray<{ table: string; schemas: string[] }>): Error {
49644965
const list = (names: string[]) => names.map((s) => `'${s}'`).join(', ');
49654966
const [first] = collisions;
49664967
const err = new Error(
@@ -15822,109 +15823,25 @@ export class SqlDriver implements IDataDriver {
1582215823
return Object.keys(info ?? {});
1582315824
}
1582415825

15825-
async introspectSchema(): Promise<IntrospectedSchema> {
15826-
const tables: Record<string, IntrospectedTable> = {};
15827-
// Stamped BEFORE the reads, not after: a consumer asking "has the remote
15828-
// changed since this snapshot?" must not be told the snapshot covers a
15829-
// moment later than the first table it actually read.
15830-
const introspectedAt = new Date().toISOString();
15831-
let tableNames: string[] = [];
15832-
// PostgreSQL only (#24707): the schema each listed table lives in, so its
15833-
// columns are read THERE. Every other dialect leaves it empty.
15834-
const schemaByTable = new Map<string, string>();
15835-
15836-
if (this.isPostgres) {
15837-
// Every schema the session can actually reach unqualified, rather than
15838-
// the literal `'public'` — same answer for a default deployment, where
15839-
// `current_schemas(false)` is exactly `{public}`, and the right answer for
15840-
// a session pointed at another schema, where the literal listed nothing.
15841-
//
15842-
// `table_schema` and its `search_path` position are selected too
15843-
// (#24707). The result is keyed by BARE name, so two schemas holding the
15844-
// same name used to collapse into one key with the later table silently
15845-
// dropped — refused below instead. And the schema is what the column read
15846-
// needs: knex `columnInfo()` on a bare name reads `current_schema()`, the
15847-
// FIRST schema on the path only, so a table in a later schema reported
15848-
// zero columns. No `ORDER BY` is added: the key order of a single-schema
15849-
// answer stays exactly what it was.
15850-
const result = await this.knex.raw(`
15851-
SELECT table_schema, table_name,
15852-
array_position(current_schemas(false), table_schema::name) AS path_position
15853-
FROM information_schema.tables
15854-
WHERE table_schema = ANY (current_schemas(false))
15855-
AND table_type = 'BASE TABLE'
15856-
`);
15857-
const rows = result.rows as { table_schema: string; table_name: string; path_position: number }[];
15858-
const schemasByTable = new Map<string, { schema: string; position: number }[]>();
15859-
for (const row of rows) {
15860-
const seen = schemasByTable.get(row.table_name);
15861-
const entry = { schema: row.table_schema, position: Number(row.path_position) };
15862-
if (seen) seen.push(entry);
15863-
else schemasByTable.set(row.table_name, [entry]);
15864-
}
15865-
const collisions = [...schemasByTable]
15866-
.filter(([, schemas]) => schemas.length > 1)
15867-
.map(([table, schemas]) => ({
15868-
table,
15869-
schemas: schemas.sort((a, b) => a.position - b.position).map((s) => s.schema),
15870-
}))
15871-
.sort((a, b) => (a.table < b.table ? -1 : a.table > b.table ? 1 : 0));
15872-
if (collisions.length > 0) throw postgresTableNameCollisionError(collisions);
15873-
15874-
tableNames = rows.map((row) => row.table_name);
15875-
for (const row of rows) schemaByTable.set(row.table_name, row.table_schema);
15876-
} else if (this.isMysql) {
15877-
const result = await this.knex.raw(`
15878-
SELECT table_name
15879-
FROM information_schema.tables
15880-
WHERE table_schema = DATABASE()
15881-
AND table_type = 'BASE TABLE'
15882-
`);
15883-
tableNames = result[0].map((row: any) => row.TABLE_NAME);
15884-
} else if (this.isSqlite) {
15885-
const result = await this.knex.raw(`
15886-
SELECT name as table_name
15887-
FROM sqlite_master
15888-
WHERE type='table'
15889-
AND name NOT LIKE 'sqlite_%'
15890-
`);
15891-
tableNames = result.map((row: any) => row.table_name);
15892-
}
15893-
15894-
for (const tableName of tableNames) {
15895-
// All four reads take the #7332 default (`onFailure: 'throw'`): this
15896-
// caller cannot correct a short read, and `introspectColumns` has never
15897-
// swallowed — so a partially-readable database fails the whole-schema
15898-
// introspection loudly instead of emitting tables whose keys silently
15899-
// read as absent (#11161). Every in-tree caller of `introspectSchema`
15900-
// already handles a throw (the datasource health check reports
15901-
// `{ ok: false }`, the REST/CLI seams surface the error).
15902-
// On PostgreSQL the columns are read in the table's OWN schema (#24707);
15903-
// the three key reads below already scope to `current_schemas(false)`
15904-
// or resolve along `search_path`, which the refusal above makes
15905-
// unambiguous among base tables.
15906-
const columns = await this.introspectColumns(tableName, schemaByTable.get(tableName));
15907-
const foreignKeys = await this.introspectForeignKeys(tableName);
15908-
const primaryKeys = await this.introspectPrimaryKeys(tableName);
15909-
const uniqueConstraints = await this.introspectUniqueConstraints(tableName);
15910-
15911-
for (const col of columns) {
15912-
if (primaryKeys.includes(col.name)) col.primaryKey = true;
15913-
if (uniqueConstraints.includes(col.name)) col.isUnique = true;
15914-
}
15915-
15916-
tables[tableName] = { name: tableName, columns, foreignKeys, primaryKeys };
15917-
}
15918-
15919-
// `dialectName` — not the raw Knex client, and not the spec's
15920-
// `SQLDialectSchema` enum. The only in-tree consumer of this key is
15921-
// `suggestFieldTypeForSqlType(col.type, schema.dialect as SqlDialect)`,
15922-
// whose `SqlDialect` vocabulary (`packages/spec/src/data/type-compat.ts`)
15923-
// spells PostgreSQL `postgres`, exactly as `dialectName` does. Emitting
15924-
// the enum's `postgresql` instead would put the key in `Object.keys()`
15925-
// while leaving every per-dialect type alias unreachable — the omission
15926-
// this repairs, wearing a fix's clothes.
15927-
return { tables, dialect: this.dialectName, introspectedAt };
15826+
introspectSchema(): Promise<IntrospectedSchema> {
15827+
// [#25521 cut 4] The body moved to `sql-driver-introspection.ts` (see `IntrospectionHost`).
15828+
const readDialectName = () => this.dialectName;
15829+
const readIsMysql = () => this.isMysql;
15830+
const readIsPostgres = () => this.isPostgres;
15831+
const readIsSqlite = () => this.isSqlite;
15832+
const readKnex = () => this.knex;
15833+
const host: ThisParameterType<typeof introspectSchema> = {
15834+
get dialectName() { return readDialectName(); },
15835+
introspectColumns: (...a) => this.introspectColumns(...a),
15836+
introspectForeignKeys: (...a) => this.introspectForeignKeys(...a),
15837+
introspectPrimaryKeys: (...a) => this.introspectPrimaryKeys(...a),
15838+
introspectUniqueConstraints: (...a) => this.introspectUniqueConstraints(...a),
15839+
get isMysql() { return readIsMysql(); },
15840+
get isPostgres() { return readIsPostgres(); },
15841+
get isSqlite() { return readIsSqlite(); },
15842+
get knex() { return readKnex(); },
15843+
};
15844+
return introspectSchema.call(host);
1592815845
}
1592915846

1593015847
// ===================================

0 commit comments

Comments
 (0)