@deebeetech/sqleasy-engine 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/introspection/index.cjs +4 -2
- package/dist/introspection/index.cjs.map +1 -1
- package/dist/introspection/index.mjs +4 -2
- package/dist/introspection/index.mjs.map +1 -1
- package/dist/mssql/index.cjs +19 -2
- package/dist/mssql/index.cjs.map +1 -1
- package/dist/mssql/index.d.cts +17 -1
- package/dist/mssql/index.d.mts +17 -1
- package/dist/mssql/index.mjs +19 -3
- package/dist/mssql/index.mjs.map +1 -1
- package/dist/mysql/index.cjs +20 -10
- package/dist/mysql/index.cjs.map +1 -1
- package/dist/mysql/index.mjs +20 -10
- package/dist/mysql/index.mjs.map +1 -1
- package/package.json +50 -20
|
@@ -285,14 +285,16 @@ async function introspectPostgres(executor, schema) {
|
|
|
285
285
|
JOIN LATERAL unnest(string_to_array(ix.indkey::text, ' ')::int[]) WITH ORDINALITY AS k(attnum, ord)
|
|
286
286
|
ON true
|
|
287
287
|
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
288
|
-
WHERE n.nspname = $1 AND t.relkind
|
|
288
|
+
WHERE n.nspname = $1 AND t.relkind IN ('r', 'p') AND k.attnum > 0
|
|
289
289
|
ORDER BY table_name, index_name, ordinal`,
|
|
290
290
|
params: [target]
|
|
291
291
|
});
|
|
292
292
|
const rowCounts = await executor.run({
|
|
293
293
|
sql: `SELECT c.relname AS table_name, c.reltuples::bigint AS n
|
|
294
294
|
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
295
|
-
|
|
295
|
+
-- reltuples on a partitioned parent is -1 until it is ANALYZEd; the >= 0 filter below
|
|
296
|
+
-- drops that rather than reporting a table with minus one row.
|
|
297
|
+
WHERE n.nspname = $1 AND c.relkind IN ('r', 'p')`,
|
|
296
298
|
params: [target]
|
|
297
299
|
});
|
|
298
300
|
const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/introspection/build-schema.ts","../../src/introspection/mssql.ts","../../src/introspection/mysql.ts","../../src/introspection/postgres.ts","../../src/introspection/sqlite.ts","../../src/introspection/index.ts"],"sourcesContent":["import type {\n SchemaColumn,\n SchemaData,\n SchemaForeignKey,\n SchemaIndex,\n SchemaTable,\n} from './schema';\n\n/** A table row as read from the catalog, before assembly. */\nexport type RawTable = {\n schema: string;\n name: string;\n isView: boolean;\n};\n\n/** One (index, column) pair as read from the catalog, before grouping into per-table indexes. */\nexport type IndexColumnRow = {\n schema: string;\n table: string;\n indexName: string;\n unique: boolean;\n columnName: string;\n ordinal: number;\n};\n\n/** Stitch flat catalog rows into per-table column/FK/index lists. Shared by every dialect reader. */\nexport function buildSchema(\n tables: RawTable[],\n columns: (SchemaColumn & { schema: string; table: string })[],\n fks: (SchemaForeignKey & { schema: string; table: string })[],\n indexColumns: IndexColumnRow[] = [],\n rowCounts: { schema: string; table: string; count: number }[] = [],\n): SchemaData {\n const colMap = new Map<string, SchemaColumn[]>();\n for (const c of columns) {\n const key = `${c.schema}.${c.table}`;\n const list = colMap.get(key) ?? [];\n list.push({\n name: c.name,\n dataType: c.dataType,\n nullable: c.nullable,\n isPrimaryKey: c.isPrimaryKey,\n defaultValue: c.defaultValue,\n });\n colMap.set(key, list);\n }\n\n const fkMap = new Map<string, SchemaForeignKey[]>();\n for (const fk of fks) {\n const key = `${fk.schema}.${fk.table}`;\n const list = fkMap.get(key) ?? [];\n list.push({\n columnName: fk.columnName,\n referencedTable: fk.referencedTable,\n referencedColumn: fk.referencedColumn,\n referencedSchema: fk.referencedSchema,\n });\n fkMap.set(key, list);\n }\n\n // Group flat (index, column) rows into per-table indexes, columns ordered within each index.\n const idxMap = new Map<\n string,\n Map<string, { unique: boolean; cols: { name: string; ordinal: number }[] }>\n >();\n for (const r of indexColumns) {\n const key = `${r.schema}.${r.table}`;\n const byName = idxMap.get(key) ?? new Map();\n const idx = byName.get(r.indexName) ?? { unique: r.unique, cols: [] };\n idx.cols.push({ name: r.columnName, ordinal: r.ordinal });\n byName.set(r.indexName, idx);\n idxMap.set(key, byName);\n }\n const indexesFor = (key: string): SchemaIndex[] =>\n [...(idxMap.get(key)?.entries() ?? [])].map(([name, i]) => ({\n name,\n unique: i.unique,\n columns: i.cols.sort((a, b) => a.ordinal - b.ordinal).map((c) => c.name),\n }));\n\n const rowCountMap = new Map(rowCounts.map((r) => [`${r.schema}.${r.table}`, r.count]));\n\n const result: SchemaTable[] = tables.map((t) => {\n const key = `${t.schema}.${t.name}`;\n return {\n schema: t.schema,\n name: t.name,\n type: t.isView ? 'view' : 'table',\n columns: colMap.get(key) ?? [],\n foreignKeys: fkMap.get(key) ?? [],\n indexes: indexesFor(key),\n approxRowCount: rowCountMap.get(key),\n };\n });\n\n return { tables: result };\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** SQL Server catalog reader (INFORMATION_SCHEMA + sys catalog). Pass an executor connected to a\n * SQL Server database (`createMssqlExecutor`). */\nexport async function introspectMssql(executor: DbExecutor, schema?: string): Promise<SchemaData> {\n const target = schema || 'dbo';\n\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type\n FROM INFORMATION_SCHEMA.TABLES\n WHERE TABLE_SCHEMA = @p0 AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')\n ORDER BY TABLE_TYPE, TABLE_NAME`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,\n IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default\n FROM INFORMATION_SCHEMA.COLUMNS\n WHERE TABLE_SCHEMA = @p0\n ORDER BY TABLE_NAME, ORDINAL_POSITION`,\n params: [target],\n });\n\n const pks = await executor.run<{ table_name: string; column_name: string }>({\n sql: `SELECT kcu.TABLE_NAME AS table_name, kcu.COLUMN_NAME AS column_name\n FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc\n JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu\n ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA\n WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = @p0`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_schema: string;\n referenced_table: string;\n referenced_column: string;\n }>({\n sql: `SELECT t1.name AS table_name, c1.name AS column_name,\n s2.name AS referenced_schema, t2.name AS referenced_table, c2.name AS referenced_column\n FROM sys.foreign_key_columns fkc\n JOIN sys.objects t1 ON fkc.parent_object_id = t1.object_id\n JOIN sys.columns c1 ON fkc.parent_object_id = c1.object_id AND fkc.parent_column_id = c1.column_id\n JOIN sys.objects t2 ON fkc.referenced_object_id = t2.object_id\n JOIN sys.columns c2 ON fkc.referenced_object_id = c2.object_id AND fkc.referenced_column_id = c2.column_id\n JOIN sys.schemas s1 ON t1.schema_id = s1.schema_id\n JOIN sys.schemas s2 ON t2.schema_id = s2.schema_id\n WHERE s1.name = @p0`,\n params: [target],\n });\n\n // Secondary indexes (sys.index_columns, key columns only in key_ordinal order; `i.type > 0` skips\n // heaps) + an approximate row count from partition stats.\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n is_unique: boolean | number;\n column_name: string;\n ordinal: number;\n }>({\n sql: `SELECT t.name AS table_name, i.name AS index_name, i.is_unique AS is_unique,\n c.name AS column_name, ic.key_ordinal AS ordinal\n FROM sys.indexes i\n JOIN sys.tables t ON t.object_id = i.object_id\n JOIN sys.schemas s ON s.schema_id = t.schema_id\n JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id\n JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id\n WHERE s.name = @p0 AND i.type > 0 AND ic.is_included_column = 0\n ORDER BY t.name, i.name, ic.key_ordinal`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: number }>({\n sql: `SELECT t.name AS table_name, SUM(p.rows) AS n\n FROM sys.tables t\n JOIN sys.schemas s ON s.schema_id = t.schema_id\n JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)\n WHERE s.name = @p0\n GROUP BY t.name`,\n params: [target],\n });\n\n const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table,\n referencedColumn: fk.referenced_column,\n referencedSchema: fk.referenced_schema,\n })),\n indexes.rows.map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: Boolean(r.is_unique),\n columnName: r.column_name,\n ordinal: Number(r.ordinal),\n })),\n rowCounts.rows.map((r) => ({ schema: target, table: r.table_name, count: Number(r.n) })),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** MySQL / MariaDB catalog reader (information_schema). Pass an executor connected to a MySQL\n * database (`createMysqlExecutor`). */\nexport async function introspectMysql(executor: DbExecutor, schema?: string): Promise<SchemaData> {\n let target = schema;\n if (!target) {\n const r = await executor.run<{ db: string }>({ sql: 'SELECT DATABASE() AS db' });\n target = r.rows[0]?.db ?? '';\n }\n\n // Explicit lowercase aliases: MySQL 8's information_schema returns UPPERCASE column headers unless\n // aliased (5.7/MariaDB return them as written) — without them every row property reads undefined.\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type\n FROM information_schema.tables\n WHERE table_schema = ? AND table_type IN ('BASE TABLE', 'VIEW')\n ORDER BY table_type, table_name`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n column_key: string;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,\n IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default, COLUMN_KEY AS column_key\n FROM information_schema.columns\n WHERE table_schema = ?\n ORDER BY table_name, ordinal_position`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_table_schema: string;\n referenced_table_name: string;\n referenced_column_name: string;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name,\n REFERENCED_TABLE_SCHEMA AS referenced_table_schema,\n REFERENCED_TABLE_NAME AS referenced_table_name,\n REFERENCED_COLUMN_NAME AS referenced_column_name\n FROM information_schema.key_column_usage\n WHERE table_schema = ? AND referenced_table_name IS NOT NULL`,\n params: [target],\n });\n\n // Secondary indexes (one row per index column, ordered by SEQ_IN_INDEX) + approx row count.\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n non_unique: number;\n column_name: string | null;\n seq: number;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, INDEX_NAME AS index_name, NON_UNIQUE AS non_unique,\n COLUMN_NAME AS column_name, SEQ_IN_INDEX AS seq\n FROM information_schema.statistics\n WHERE table_schema = ?\n ORDER BY table_name, index_name, seq`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: number | null }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_ROWS AS n\n FROM information_schema.tables\n WHERE table_schema = ? AND table_type = 'BASE TABLE'`,\n params: [target],\n });\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: c.column_key === 'PRI',\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table_name,\n referencedColumn: fk.referenced_column_name,\n referencedSchema: fk.referenced_table_schema,\n })),\n indexes.rows\n .filter((r) => r.column_name != null)\n .map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: r.non_unique === 0,\n columnName: r.column_name!,\n ordinal: Number(r.seq),\n })),\n rowCounts.rows.map((r) => ({ schema: target, table: r.table_name, count: Number(r.n ?? 0) })),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** Postgres catalog reader (information_schema + pg_catalog). Pass an executor connected to a\n * Postgres database (`createPostgresExecutor`). */\nexport async function introspectPostgres(\n executor: DbExecutor,\n schema?: string,\n): Promise<SchemaData> {\n const target = schema || 'public';\n\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT table_name, table_type\n FROM information_schema.tables\n WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')\n ORDER BY table_type, table_name`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n }>({\n sql: `SELECT table_name, column_name, data_type, is_nullable, column_default\n FROM information_schema.columns\n WHERE table_schema = $1\n ORDER BY table_name, ordinal_position`,\n params: [target],\n });\n\n const pks = await executor.run<{ table_name: string; column_name: string }>({\n sql: `SELECT kcu.table_name, kcu.column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema\n WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_table_schema: string;\n referenced_table_name: string;\n referenced_column_name: string;\n }>({\n sql: `SELECT kcu.table_name, kcu.column_name,\n ccu.table_schema AS referenced_table_schema,\n ccu.table_name AS referenced_table_name,\n ccu.column_name AS referenced_column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema\n JOIN information_schema.constraint_column_usage ccu\n ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema\n WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`,\n params: [target],\n });\n\n // Secondary indexes: columns unnested in key order (WITH ORDINALITY), `attnum > 0` drops\n // expression-index entries. Plus an approximate row count from planner stats (reltuples).\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n is_unique: boolean;\n column_name: string;\n ordinal: number;\n }>({\n sql: `SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique,\n a.attname AS column_name, k.ord AS ordinal\n FROM pg_index ix\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_class t ON t.oid = ix.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n JOIN LATERAL unnest(string_to_array(ix.indkey::text, ' ')::int[]) WITH ORDINALITY AS k(attnum, ord)\n ON true\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum\n WHERE n.nspname = $1 AND t.relkind = 'r' AND k.attnum > 0\n ORDER BY table_name, index_name, ordinal`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: string }>({\n sql: `SELECT c.relname AS table_name, c.reltuples::bigint AS n\n FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = $1 AND c.relkind = 'r'`,\n params: [target],\n });\n\n const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table_name,\n referencedColumn: fk.referenced_column_name,\n referencedSchema: fk.referenced_table_schema,\n })),\n indexes.rows.map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: r.is_unique,\n columnName: r.column_name,\n ordinal: Number(r.ordinal),\n })),\n rowCounts.rows\n .map((r) => ({ schema: target, table: r.table_name, count: Number(r.n) }))\n .filter((r) => r.count >= 0),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema, type IndexColumnRow } from './build-schema';\nimport type { SchemaColumn, SchemaData, SchemaForeignKey } from './schema';\n\n/** SQLite catalog reader (sqlite_master + pragma table-valued functions). Also serves libsql/turso —\n * they are all SQLite engines with the same catalog. Pass an executor from `createSqliteExecutor`. */\nexport async function introspectSqlite(executor: DbExecutor): Promise<SchemaData> {\n const tables = await executor.run<{ name: string; type: string }>({\n sql: `SELECT name, type FROM sqlite_master\n WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'\n ORDER BY type, name`,\n });\n\n const columns: (SchemaColumn & { schema: string; table: string })[] = [];\n const fks: (SchemaForeignKey & { schema: string; table: string })[] = [];\n const indexColumns: IndexColumnRow[] = [];\n\n for (const t of tables.rows) {\n // Table-valued pragma functions accept the table name as a bound parameter, so no identifier\n // interpolation is needed.\n const cols = await executor.run<{\n name: string;\n type: string;\n notnull: number;\n dflt_value: string | null;\n pk: number;\n }>({\n sql: 'SELECT name, type, \"notnull\", dflt_value, pk FROM pragma_table_info(?)',\n params: [t.name],\n });\n\n for (const c of cols.rows) {\n columns.push({\n schema: 'main',\n table: t.name,\n name: c.name,\n dataType: c.type || 'TEXT',\n nullable: c.notnull === 0 && c.pk === 0,\n isPrimaryKey: c.pk > 0,\n defaultValue: c.dflt_value ?? undefined,\n });\n }\n\n const fkRows = await executor.run<{ from: string; table: string; to: string }>({\n sql: 'SELECT \"from\", \"table\", \"to\" FROM pragma_foreign_key_list(?)',\n params: [t.name],\n });\n for (const fk of fkRows.rows) {\n fks.push({\n schema: 'main',\n table: t.name,\n columnName: fk.from,\n referencedTable: fk.table,\n referencedColumn: fk.to,\n });\n }\n\n // Indexes: list them, then read each index's columns (pragma_index_info, ordered by seqno).\n const idxList = await executor.run<{ name: string; unique: number }>({\n sql: 'SELECT name, \"unique\" FROM pragma_index_list(?)',\n params: [t.name],\n });\n for (const idx of idxList.rows) {\n const idxCols = await executor.run<{ seqno: number; name: string | null }>({\n sql: 'SELECT seqno, name FROM pragma_index_info(?)',\n params: [idx.name],\n });\n for (const ic of idxCols.rows) {\n if (ic.name == null) continue; // expression-index columns have no name\n indexColumns.push({\n schema: 'main',\n table: t.name,\n indexName: idx.name,\n unique: idx.unique === 1,\n columnName: ic.name,\n ordinal: ic.seqno,\n });\n }\n }\n }\n\n return buildSchema(\n tables.rows.map((t) => ({ schema: 'main', name: t.name, isView: t.type === 'view' })),\n columns,\n fks,\n indexColumns,\n );\n}\n","/**\n * Schema introspection: read a database's catalog (tables, views, columns, primary keys, foreign\n * keys, indexes, approximate row counts) into a normalized {@link SchemaData}.\n *\n * This entry point pulls in NO driver — it runs entirely through a {@link DbExecutor} you supply, so\n * it reuses that executor's connection, credentials, and placeholder convention rather than opening\n * its own. Build the executor from the matching dialect subpath and pass it in.\n */\nimport type { DbExecutor } from '../index';\nimport { introspectMssql } from './mssql';\nimport { introspectMysql } from './mysql';\nimport { introspectPostgres } from './postgres';\nimport type { SchemaData } from './schema';\nimport { introspectSqlite } from './sqlite';\n\nexport type {\n SchemaColumn,\n SchemaForeignKey,\n SchemaIndex,\n SchemaTable,\n SchemaData,\n} from './schema';\nexport { buildSchema, type RawTable, type IndexColumnRow } from './build-schema';\nexport { introspectPostgres } from './postgres';\nexport { introspectMysql } from './mysql';\nexport { introspectMssql } from './mssql';\nexport { introspectSqlite } from './sqlite';\n\n/** The dialects whose catalog this package can read. libsql and turso use `'sqlite'`. */\nexport type IntrospectDialect = 'postgres' | 'mysql' | 'mssql' | 'sqlite';\n\n/**\n * Read a database's catalog as a {@link SchemaData}, through a supplied {@link DbExecutor}. Choose\n * the `dialect` matching the executor you built — the reader runs that dialect's catalog queries.\n * `schema` scopes the namespace, defaulting per dialect (postgres `public`, mysql the current\n * database, mssql `dbo`, sqlite `main` — sqlite ignores it).\n */\nexport function introspectSchema(\n executor: DbExecutor,\n dialect: IntrospectDialect,\n schema?: string,\n): Promise<SchemaData> {\n switch (dialect) {\n case 'postgres':\n return introspectPostgres(executor, schema);\n case 'mysql':\n return introspectMysql(executor, schema);\n case 'mssql':\n return introspectMssql(executor, schema);\n case 'sqlite':\n return introspectSqlite(executor);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/introspection/build-schema.ts","../../src/introspection/mssql.ts","../../src/introspection/mysql.ts","../../src/introspection/postgres.ts","../../src/introspection/sqlite.ts","../../src/introspection/index.ts"],"sourcesContent":["import type {\n SchemaColumn,\n SchemaData,\n SchemaForeignKey,\n SchemaIndex,\n SchemaTable,\n} from './schema';\n\n/** A table row as read from the catalog, before assembly. */\nexport type RawTable = {\n schema: string;\n name: string;\n isView: boolean;\n};\n\n/** One (index, column) pair as read from the catalog, before grouping into per-table indexes. */\nexport type IndexColumnRow = {\n schema: string;\n table: string;\n indexName: string;\n unique: boolean;\n columnName: string;\n ordinal: number;\n};\n\n/** Stitch flat catalog rows into per-table column/FK/index lists. Shared by every dialect reader. */\nexport function buildSchema(\n tables: RawTable[],\n columns: (SchemaColumn & { schema: string; table: string })[],\n fks: (SchemaForeignKey & { schema: string; table: string })[],\n indexColumns: IndexColumnRow[] = [],\n rowCounts: { schema: string; table: string; count: number }[] = [],\n): SchemaData {\n const colMap = new Map<string, SchemaColumn[]>();\n for (const c of columns) {\n const key = `${c.schema}.${c.table}`;\n const list = colMap.get(key) ?? [];\n list.push({\n name: c.name,\n dataType: c.dataType,\n nullable: c.nullable,\n isPrimaryKey: c.isPrimaryKey,\n defaultValue: c.defaultValue,\n });\n colMap.set(key, list);\n }\n\n const fkMap = new Map<string, SchemaForeignKey[]>();\n for (const fk of fks) {\n const key = `${fk.schema}.${fk.table}`;\n const list = fkMap.get(key) ?? [];\n list.push({\n columnName: fk.columnName,\n referencedTable: fk.referencedTable,\n referencedColumn: fk.referencedColumn,\n referencedSchema: fk.referencedSchema,\n });\n fkMap.set(key, list);\n }\n\n // Group flat (index, column) rows into per-table indexes, columns ordered within each index.\n const idxMap = new Map<\n string,\n Map<string, { unique: boolean; cols: { name: string; ordinal: number }[] }>\n >();\n for (const r of indexColumns) {\n const key = `${r.schema}.${r.table}`;\n const byName = idxMap.get(key) ?? new Map();\n const idx = byName.get(r.indexName) ?? { unique: r.unique, cols: [] };\n idx.cols.push({ name: r.columnName, ordinal: r.ordinal });\n byName.set(r.indexName, idx);\n idxMap.set(key, byName);\n }\n const indexesFor = (key: string): SchemaIndex[] =>\n [...(idxMap.get(key)?.entries() ?? [])].map(([name, i]) => ({\n name,\n unique: i.unique,\n columns: i.cols.sort((a, b) => a.ordinal - b.ordinal).map((c) => c.name),\n }));\n\n const rowCountMap = new Map(rowCounts.map((r) => [`${r.schema}.${r.table}`, r.count]));\n\n const result: SchemaTable[] = tables.map((t) => {\n const key = `${t.schema}.${t.name}`;\n return {\n schema: t.schema,\n name: t.name,\n type: t.isView ? 'view' : 'table',\n columns: colMap.get(key) ?? [],\n foreignKeys: fkMap.get(key) ?? [],\n indexes: indexesFor(key),\n approxRowCount: rowCountMap.get(key),\n };\n });\n\n return { tables: result };\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** SQL Server catalog reader (INFORMATION_SCHEMA + sys catalog). Pass an executor connected to a\n * SQL Server database (`createMssqlExecutor`). */\nexport async function introspectMssql(executor: DbExecutor, schema?: string): Promise<SchemaData> {\n const target = schema || 'dbo';\n\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type\n FROM INFORMATION_SCHEMA.TABLES\n WHERE TABLE_SCHEMA = @p0 AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')\n ORDER BY TABLE_TYPE, TABLE_NAME`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,\n IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default\n FROM INFORMATION_SCHEMA.COLUMNS\n WHERE TABLE_SCHEMA = @p0\n ORDER BY TABLE_NAME, ORDINAL_POSITION`,\n params: [target],\n });\n\n const pks = await executor.run<{ table_name: string; column_name: string }>({\n sql: `SELECT kcu.TABLE_NAME AS table_name, kcu.COLUMN_NAME AS column_name\n FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc\n JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu\n ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA\n WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = @p0`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_schema: string;\n referenced_table: string;\n referenced_column: string;\n }>({\n sql: `SELECT t1.name AS table_name, c1.name AS column_name,\n s2.name AS referenced_schema, t2.name AS referenced_table, c2.name AS referenced_column\n FROM sys.foreign_key_columns fkc\n JOIN sys.objects t1 ON fkc.parent_object_id = t1.object_id\n JOIN sys.columns c1 ON fkc.parent_object_id = c1.object_id AND fkc.parent_column_id = c1.column_id\n JOIN sys.objects t2 ON fkc.referenced_object_id = t2.object_id\n JOIN sys.columns c2 ON fkc.referenced_object_id = c2.object_id AND fkc.referenced_column_id = c2.column_id\n JOIN sys.schemas s1 ON t1.schema_id = s1.schema_id\n JOIN sys.schemas s2 ON t2.schema_id = s2.schema_id\n WHERE s1.name = @p0`,\n params: [target],\n });\n\n // Secondary indexes (sys.index_columns, key columns only in key_ordinal order; `i.type > 0` skips\n // heaps) + an approximate row count from partition stats.\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n is_unique: boolean | number;\n column_name: string;\n ordinal: number;\n }>({\n sql: `SELECT t.name AS table_name, i.name AS index_name, i.is_unique AS is_unique,\n c.name AS column_name, ic.key_ordinal AS ordinal\n FROM sys.indexes i\n JOIN sys.tables t ON t.object_id = i.object_id\n JOIN sys.schemas s ON s.schema_id = t.schema_id\n JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id\n JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id\n WHERE s.name = @p0 AND i.type > 0 AND ic.is_included_column = 0\n ORDER BY t.name, i.name, ic.key_ordinal`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: number }>({\n sql: `SELECT t.name AS table_name, SUM(p.rows) AS n\n FROM sys.tables t\n JOIN sys.schemas s ON s.schema_id = t.schema_id\n JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)\n WHERE s.name = @p0\n GROUP BY t.name`,\n params: [target],\n });\n\n const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table,\n referencedColumn: fk.referenced_column,\n referencedSchema: fk.referenced_schema,\n })),\n indexes.rows.map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: Boolean(r.is_unique),\n columnName: r.column_name,\n ordinal: Number(r.ordinal),\n })),\n rowCounts.rows.map((r) => ({ schema: target, table: r.table_name, count: Number(r.n) })),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** MySQL / MariaDB catalog reader (information_schema). Pass an executor connected to a MySQL\n * database (`createMysqlExecutor`). */\nexport async function introspectMysql(executor: DbExecutor, schema?: string): Promise<SchemaData> {\n let target = schema;\n if (!target) {\n const r = await executor.run<{ db: string }>({ sql: 'SELECT DATABASE() AS db' });\n target = r.rows[0]?.db ?? '';\n }\n\n // Explicit lowercase aliases: MySQL 8's information_schema returns UPPERCASE column headers unless\n // aliased (5.7/MariaDB return them as written) — without them every row property reads undefined.\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type\n FROM information_schema.tables\n WHERE table_schema = ? AND table_type IN ('BASE TABLE', 'VIEW')\n ORDER BY table_type, table_name`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n column_key: string;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,\n IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default, COLUMN_KEY AS column_key\n FROM information_schema.columns\n WHERE table_schema = ?\n ORDER BY table_name, ordinal_position`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_table_schema: string;\n referenced_table_name: string;\n referenced_column_name: string;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name,\n REFERENCED_TABLE_SCHEMA AS referenced_table_schema,\n REFERENCED_TABLE_NAME AS referenced_table_name,\n REFERENCED_COLUMN_NAME AS referenced_column_name\n FROM information_schema.key_column_usage\n WHERE table_schema = ? AND referenced_table_name IS NOT NULL`,\n params: [target],\n });\n\n // Secondary indexes (one row per index column, ordered by SEQ_IN_INDEX) + approx row count.\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n non_unique: number;\n column_name: string | null;\n seq: number;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, INDEX_NAME AS index_name, NON_UNIQUE AS non_unique,\n COLUMN_NAME AS column_name, SEQ_IN_INDEX AS seq\n FROM information_schema.statistics\n WHERE table_schema = ?\n ORDER BY table_name, index_name, seq`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: number | null }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_ROWS AS n\n FROM information_schema.tables\n WHERE table_schema = ? AND table_type = 'BASE TABLE'`,\n params: [target],\n });\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: c.column_key === 'PRI',\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table_name,\n referencedColumn: fk.referenced_column_name,\n referencedSchema: fk.referenced_table_schema,\n })),\n indexes.rows\n .filter((r) => r.column_name != null)\n .map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: r.non_unique === 0,\n columnName: r.column_name!,\n ordinal: Number(r.seq),\n })),\n rowCounts.rows.map((r) => ({ schema: target, table: r.table_name, count: Number(r.n ?? 0) })),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** Postgres catalog reader (information_schema + pg_catalog). Pass an executor connected to a\n * Postgres database (`createPostgresExecutor`). */\nexport async function introspectPostgres(\n executor: DbExecutor,\n schema?: string,\n): Promise<SchemaData> {\n const target = schema || 'public';\n\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT table_name, table_type\n FROM information_schema.tables\n WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')\n ORDER BY table_type, table_name`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n }>({\n sql: `SELECT table_name, column_name, data_type, is_nullable, column_default\n FROM information_schema.columns\n WHERE table_schema = $1\n ORDER BY table_name, ordinal_position`,\n params: [target],\n });\n\n const pks = await executor.run<{ table_name: string; column_name: string }>({\n sql: `SELECT kcu.table_name, kcu.column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema\n WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_table_schema: string;\n referenced_table_name: string;\n referenced_column_name: string;\n }>({\n sql: `SELECT kcu.table_name, kcu.column_name,\n ccu.table_schema AS referenced_table_schema,\n ccu.table_name AS referenced_table_name,\n ccu.column_name AS referenced_column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema\n JOIN information_schema.constraint_column_usage ccu\n ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema\n WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`,\n params: [target],\n });\n\n // Secondary indexes: columns unnested in key order (WITH ORDINALITY), `attnum > 0` drops\n // expression-index entries. Plus an approximate row count from planner stats (reltuples).\n //\n // relkind IN ('r','p'): a PARTITIONED table's parent is 'p', not 'r'. Filtering on 'r' alone\n // reported every partitioned table as having no indexes and no row count — silently, and worst on\n // exactly the biggest tables in a database, which are the ones most likely to be partitioned.\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n is_unique: boolean;\n column_name: string;\n ordinal: number;\n }>({\n sql: `SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique,\n a.attname AS column_name, k.ord AS ordinal\n FROM pg_index ix\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_class t ON t.oid = ix.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n JOIN LATERAL unnest(string_to_array(ix.indkey::text, ' ')::int[]) WITH ORDINALITY AS k(attnum, ord)\n ON true\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum\n WHERE n.nspname = $1 AND t.relkind IN ('r', 'p') AND k.attnum > 0\n ORDER BY table_name, index_name, ordinal`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: string }>({\n sql: `SELECT c.relname AS table_name, c.reltuples::bigint AS n\n FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace\n -- reltuples on a partitioned parent is -1 until it is ANALYZEd; the >= 0 filter below\n -- drops that rather than reporting a table with minus one row.\n WHERE n.nspname = $1 AND c.relkind IN ('r', 'p')`,\n params: [target],\n });\n\n const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table_name,\n referencedColumn: fk.referenced_column_name,\n referencedSchema: fk.referenced_table_schema,\n })),\n indexes.rows.map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: r.is_unique,\n columnName: r.column_name,\n ordinal: Number(r.ordinal),\n })),\n rowCounts.rows\n .map((r) => ({ schema: target, table: r.table_name, count: Number(r.n) }))\n .filter((r) => r.count >= 0),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema, type IndexColumnRow } from './build-schema';\nimport type { SchemaColumn, SchemaData, SchemaForeignKey } from './schema';\n\n/** SQLite catalog reader (sqlite_master + pragma table-valued functions). Also serves libsql/turso —\n * they are all SQLite engines with the same catalog. Pass an executor from `createSqliteExecutor`. */\nexport async function introspectSqlite(executor: DbExecutor): Promise<SchemaData> {\n const tables = await executor.run<{ name: string; type: string }>({\n sql: `SELECT name, type FROM sqlite_master\n WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'\n ORDER BY type, name`,\n });\n\n const columns: (SchemaColumn & { schema: string; table: string })[] = [];\n const fks: (SchemaForeignKey & { schema: string; table: string })[] = [];\n const indexColumns: IndexColumnRow[] = [];\n\n for (const t of tables.rows) {\n // Table-valued pragma functions accept the table name as a bound parameter, so no identifier\n // interpolation is needed.\n const cols = await executor.run<{\n name: string;\n type: string;\n notnull: number;\n dflt_value: string | null;\n pk: number;\n }>({\n sql: 'SELECT name, type, \"notnull\", dflt_value, pk FROM pragma_table_info(?)',\n params: [t.name],\n });\n\n for (const c of cols.rows) {\n columns.push({\n schema: 'main',\n table: t.name,\n name: c.name,\n dataType: c.type || 'TEXT',\n nullable: c.notnull === 0 && c.pk === 0,\n isPrimaryKey: c.pk > 0,\n defaultValue: c.dflt_value ?? undefined,\n });\n }\n\n const fkRows = await executor.run<{ from: string; table: string; to: string }>({\n sql: 'SELECT \"from\", \"table\", \"to\" FROM pragma_foreign_key_list(?)',\n params: [t.name],\n });\n for (const fk of fkRows.rows) {\n fks.push({\n schema: 'main',\n table: t.name,\n columnName: fk.from,\n referencedTable: fk.table,\n referencedColumn: fk.to,\n });\n }\n\n // Indexes: list them, then read each index's columns (pragma_index_info, ordered by seqno).\n const idxList = await executor.run<{ name: string; unique: number }>({\n sql: 'SELECT name, \"unique\" FROM pragma_index_list(?)',\n params: [t.name],\n });\n for (const idx of idxList.rows) {\n const idxCols = await executor.run<{ seqno: number; name: string | null }>({\n sql: 'SELECT seqno, name FROM pragma_index_info(?)',\n params: [idx.name],\n });\n for (const ic of idxCols.rows) {\n if (ic.name == null) continue; // expression-index columns have no name\n indexColumns.push({\n schema: 'main',\n table: t.name,\n indexName: idx.name,\n unique: idx.unique === 1,\n columnName: ic.name,\n ordinal: ic.seqno,\n });\n }\n }\n }\n\n return buildSchema(\n tables.rows.map((t) => ({ schema: 'main', name: t.name, isView: t.type === 'view' })),\n columns,\n fks,\n indexColumns,\n );\n}\n","/**\n * Schema introspection: read a database's catalog (tables, views, columns, primary keys, foreign\n * keys, indexes, approximate row counts) into a normalized {@link SchemaData}.\n *\n * This entry point pulls in NO driver — it runs entirely through a {@link DbExecutor} you supply, so\n * it reuses that executor's connection, credentials, and placeholder convention rather than opening\n * its own. Build the executor from the matching dialect subpath and pass it in.\n */\nimport type { DbExecutor } from '../index';\nimport { introspectMssql } from './mssql';\nimport { introspectMysql } from './mysql';\nimport { introspectPostgres } from './postgres';\nimport type { SchemaData } from './schema';\nimport { introspectSqlite } from './sqlite';\n\nexport type {\n SchemaColumn,\n SchemaForeignKey,\n SchemaIndex,\n SchemaTable,\n SchemaData,\n} from './schema';\nexport { buildSchema, type RawTable, type IndexColumnRow } from './build-schema';\nexport { introspectPostgres } from './postgres';\nexport { introspectMysql } from './mysql';\nexport { introspectMssql } from './mssql';\nexport { introspectSqlite } from './sqlite';\n\n/** The dialects whose catalog this package can read. libsql and turso use `'sqlite'`. */\nexport type IntrospectDialect = 'postgres' | 'mysql' | 'mssql' | 'sqlite';\n\n/**\n * Read a database's catalog as a {@link SchemaData}, through a supplied {@link DbExecutor}. Choose\n * the `dialect` matching the executor you built — the reader runs that dialect's catalog queries.\n * `schema` scopes the namespace, defaulting per dialect (postgres `public`, mysql the current\n * database, mssql `dbo`, sqlite `main` — sqlite ignores it).\n */\nexport function introspectSchema(\n executor: DbExecutor,\n dialect: IntrospectDialect,\n schema?: string,\n): Promise<SchemaData> {\n switch (dialect) {\n case 'postgres':\n return introspectPostgres(executor, schema);\n case 'mysql':\n return introspectMysql(executor, schema);\n case 'mssql':\n return introspectMssql(executor, schema);\n case 'sqlite':\n return introspectSqlite(executor);\n }\n}\n"],"mappings":"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"}
|
|
@@ -284,14 +284,16 @@ async function introspectPostgres(executor, schema) {
|
|
|
284
284
|
JOIN LATERAL unnest(string_to_array(ix.indkey::text, ' ')::int[]) WITH ORDINALITY AS k(attnum, ord)
|
|
285
285
|
ON true
|
|
286
286
|
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
|
|
287
|
-
WHERE n.nspname = $1 AND t.relkind
|
|
287
|
+
WHERE n.nspname = $1 AND t.relkind IN ('r', 'p') AND k.attnum > 0
|
|
288
288
|
ORDER BY table_name, index_name, ordinal`,
|
|
289
289
|
params: [target]
|
|
290
290
|
});
|
|
291
291
|
const rowCounts = await executor.run({
|
|
292
292
|
sql: `SELECT c.relname AS table_name, c.reltuples::bigint AS n
|
|
293
293
|
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
294
|
-
|
|
294
|
+
-- reltuples on a partitioned parent is -1 until it is ANALYZEd; the >= 0 filter below
|
|
295
|
+
-- drops that rather than reporting a table with minus one row.
|
|
296
|
+
WHERE n.nspname = $1 AND c.relkind IN ('r', 'p')`,
|
|
295
297
|
params: [target]
|
|
296
298
|
});
|
|
297
299
|
const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/introspection/build-schema.ts","../../src/introspection/mssql.ts","../../src/introspection/mysql.ts","../../src/introspection/postgres.ts","../../src/introspection/sqlite.ts","../../src/introspection/index.ts"],"sourcesContent":["import type {\n SchemaColumn,\n SchemaData,\n SchemaForeignKey,\n SchemaIndex,\n SchemaTable,\n} from './schema';\n\n/** A table row as read from the catalog, before assembly. */\nexport type RawTable = {\n schema: string;\n name: string;\n isView: boolean;\n};\n\n/** One (index, column) pair as read from the catalog, before grouping into per-table indexes. */\nexport type IndexColumnRow = {\n schema: string;\n table: string;\n indexName: string;\n unique: boolean;\n columnName: string;\n ordinal: number;\n};\n\n/** Stitch flat catalog rows into per-table column/FK/index lists. Shared by every dialect reader. */\nexport function buildSchema(\n tables: RawTable[],\n columns: (SchemaColumn & { schema: string; table: string })[],\n fks: (SchemaForeignKey & { schema: string; table: string })[],\n indexColumns: IndexColumnRow[] = [],\n rowCounts: { schema: string; table: string; count: number }[] = [],\n): SchemaData {\n const colMap = new Map<string, SchemaColumn[]>();\n for (const c of columns) {\n const key = `${c.schema}.${c.table}`;\n const list = colMap.get(key) ?? [];\n list.push({\n name: c.name,\n dataType: c.dataType,\n nullable: c.nullable,\n isPrimaryKey: c.isPrimaryKey,\n defaultValue: c.defaultValue,\n });\n colMap.set(key, list);\n }\n\n const fkMap = new Map<string, SchemaForeignKey[]>();\n for (const fk of fks) {\n const key = `${fk.schema}.${fk.table}`;\n const list = fkMap.get(key) ?? [];\n list.push({\n columnName: fk.columnName,\n referencedTable: fk.referencedTable,\n referencedColumn: fk.referencedColumn,\n referencedSchema: fk.referencedSchema,\n });\n fkMap.set(key, list);\n }\n\n // Group flat (index, column) rows into per-table indexes, columns ordered within each index.\n const idxMap = new Map<\n string,\n Map<string, { unique: boolean; cols: { name: string; ordinal: number }[] }>\n >();\n for (const r of indexColumns) {\n const key = `${r.schema}.${r.table}`;\n const byName = idxMap.get(key) ?? new Map();\n const idx = byName.get(r.indexName) ?? { unique: r.unique, cols: [] };\n idx.cols.push({ name: r.columnName, ordinal: r.ordinal });\n byName.set(r.indexName, idx);\n idxMap.set(key, byName);\n }\n const indexesFor = (key: string): SchemaIndex[] =>\n [...(idxMap.get(key)?.entries() ?? [])].map(([name, i]) => ({\n name,\n unique: i.unique,\n columns: i.cols.sort((a, b) => a.ordinal - b.ordinal).map((c) => c.name),\n }));\n\n const rowCountMap = new Map(rowCounts.map((r) => [`${r.schema}.${r.table}`, r.count]));\n\n const result: SchemaTable[] = tables.map((t) => {\n const key = `${t.schema}.${t.name}`;\n return {\n schema: t.schema,\n name: t.name,\n type: t.isView ? 'view' : 'table',\n columns: colMap.get(key) ?? [],\n foreignKeys: fkMap.get(key) ?? [],\n indexes: indexesFor(key),\n approxRowCount: rowCountMap.get(key),\n };\n });\n\n return { tables: result };\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** SQL Server catalog reader (INFORMATION_SCHEMA + sys catalog). Pass an executor connected to a\n * SQL Server database (`createMssqlExecutor`). */\nexport async function introspectMssql(executor: DbExecutor, schema?: string): Promise<SchemaData> {\n const target = schema || 'dbo';\n\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type\n FROM INFORMATION_SCHEMA.TABLES\n WHERE TABLE_SCHEMA = @p0 AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')\n ORDER BY TABLE_TYPE, TABLE_NAME`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,\n IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default\n FROM INFORMATION_SCHEMA.COLUMNS\n WHERE TABLE_SCHEMA = @p0\n ORDER BY TABLE_NAME, ORDINAL_POSITION`,\n params: [target],\n });\n\n const pks = await executor.run<{ table_name: string; column_name: string }>({\n sql: `SELECT kcu.TABLE_NAME AS table_name, kcu.COLUMN_NAME AS column_name\n FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc\n JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu\n ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA\n WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = @p0`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_schema: string;\n referenced_table: string;\n referenced_column: string;\n }>({\n sql: `SELECT t1.name AS table_name, c1.name AS column_name,\n s2.name AS referenced_schema, t2.name AS referenced_table, c2.name AS referenced_column\n FROM sys.foreign_key_columns fkc\n JOIN sys.objects t1 ON fkc.parent_object_id = t1.object_id\n JOIN sys.columns c1 ON fkc.parent_object_id = c1.object_id AND fkc.parent_column_id = c1.column_id\n JOIN sys.objects t2 ON fkc.referenced_object_id = t2.object_id\n JOIN sys.columns c2 ON fkc.referenced_object_id = c2.object_id AND fkc.referenced_column_id = c2.column_id\n JOIN sys.schemas s1 ON t1.schema_id = s1.schema_id\n JOIN sys.schemas s2 ON t2.schema_id = s2.schema_id\n WHERE s1.name = @p0`,\n params: [target],\n });\n\n // Secondary indexes (sys.index_columns, key columns only in key_ordinal order; `i.type > 0` skips\n // heaps) + an approximate row count from partition stats.\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n is_unique: boolean | number;\n column_name: string;\n ordinal: number;\n }>({\n sql: `SELECT t.name AS table_name, i.name AS index_name, i.is_unique AS is_unique,\n c.name AS column_name, ic.key_ordinal AS ordinal\n FROM sys.indexes i\n JOIN sys.tables t ON t.object_id = i.object_id\n JOIN sys.schemas s ON s.schema_id = t.schema_id\n JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id\n JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id\n WHERE s.name = @p0 AND i.type > 0 AND ic.is_included_column = 0\n ORDER BY t.name, i.name, ic.key_ordinal`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: number }>({\n sql: `SELECT t.name AS table_name, SUM(p.rows) AS n\n FROM sys.tables t\n JOIN sys.schemas s ON s.schema_id = t.schema_id\n JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)\n WHERE s.name = @p0\n GROUP BY t.name`,\n params: [target],\n });\n\n const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table,\n referencedColumn: fk.referenced_column,\n referencedSchema: fk.referenced_schema,\n })),\n indexes.rows.map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: Boolean(r.is_unique),\n columnName: r.column_name,\n ordinal: Number(r.ordinal),\n })),\n rowCounts.rows.map((r) => ({ schema: target, table: r.table_name, count: Number(r.n) })),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** MySQL / MariaDB catalog reader (information_schema). Pass an executor connected to a MySQL\n * database (`createMysqlExecutor`). */\nexport async function introspectMysql(executor: DbExecutor, schema?: string): Promise<SchemaData> {\n let target = schema;\n if (!target) {\n const r = await executor.run<{ db: string }>({ sql: 'SELECT DATABASE() AS db' });\n target = r.rows[0]?.db ?? '';\n }\n\n // Explicit lowercase aliases: MySQL 8's information_schema returns UPPERCASE column headers unless\n // aliased (5.7/MariaDB return them as written) — without them every row property reads undefined.\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type\n FROM information_schema.tables\n WHERE table_schema = ? AND table_type IN ('BASE TABLE', 'VIEW')\n ORDER BY table_type, table_name`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n column_key: string;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,\n IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default, COLUMN_KEY AS column_key\n FROM information_schema.columns\n WHERE table_schema = ?\n ORDER BY table_name, ordinal_position`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_table_schema: string;\n referenced_table_name: string;\n referenced_column_name: string;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name,\n REFERENCED_TABLE_SCHEMA AS referenced_table_schema,\n REFERENCED_TABLE_NAME AS referenced_table_name,\n REFERENCED_COLUMN_NAME AS referenced_column_name\n FROM information_schema.key_column_usage\n WHERE table_schema = ? AND referenced_table_name IS NOT NULL`,\n params: [target],\n });\n\n // Secondary indexes (one row per index column, ordered by SEQ_IN_INDEX) + approx row count.\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n non_unique: number;\n column_name: string | null;\n seq: number;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, INDEX_NAME AS index_name, NON_UNIQUE AS non_unique,\n COLUMN_NAME AS column_name, SEQ_IN_INDEX AS seq\n FROM information_schema.statistics\n WHERE table_schema = ?\n ORDER BY table_name, index_name, seq`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: number | null }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_ROWS AS n\n FROM information_schema.tables\n WHERE table_schema = ? AND table_type = 'BASE TABLE'`,\n params: [target],\n });\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: c.column_key === 'PRI',\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table_name,\n referencedColumn: fk.referenced_column_name,\n referencedSchema: fk.referenced_table_schema,\n })),\n indexes.rows\n .filter((r) => r.column_name != null)\n .map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: r.non_unique === 0,\n columnName: r.column_name!,\n ordinal: Number(r.seq),\n })),\n rowCounts.rows.map((r) => ({ schema: target, table: r.table_name, count: Number(r.n ?? 0) })),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** Postgres catalog reader (information_schema + pg_catalog). Pass an executor connected to a\n * Postgres database (`createPostgresExecutor`). */\nexport async function introspectPostgres(\n executor: DbExecutor,\n schema?: string,\n): Promise<SchemaData> {\n const target = schema || 'public';\n\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT table_name, table_type\n FROM information_schema.tables\n WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')\n ORDER BY table_type, table_name`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n }>({\n sql: `SELECT table_name, column_name, data_type, is_nullable, column_default\n FROM information_schema.columns\n WHERE table_schema = $1\n ORDER BY table_name, ordinal_position`,\n params: [target],\n });\n\n const pks = await executor.run<{ table_name: string; column_name: string }>({\n sql: `SELECT kcu.table_name, kcu.column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema\n WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_table_schema: string;\n referenced_table_name: string;\n referenced_column_name: string;\n }>({\n sql: `SELECT kcu.table_name, kcu.column_name,\n ccu.table_schema AS referenced_table_schema,\n ccu.table_name AS referenced_table_name,\n ccu.column_name AS referenced_column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema\n JOIN information_schema.constraint_column_usage ccu\n ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema\n WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`,\n params: [target],\n });\n\n // Secondary indexes: columns unnested in key order (WITH ORDINALITY), `attnum > 0` drops\n // expression-index entries. Plus an approximate row count from planner stats (reltuples).\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n is_unique: boolean;\n column_name: string;\n ordinal: number;\n }>({\n sql: `SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique,\n a.attname AS column_name, k.ord AS ordinal\n FROM pg_index ix\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_class t ON t.oid = ix.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n JOIN LATERAL unnest(string_to_array(ix.indkey::text, ' ')::int[]) WITH ORDINALITY AS k(attnum, ord)\n ON true\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum\n WHERE n.nspname = $1 AND t.relkind = 'r' AND k.attnum > 0\n ORDER BY table_name, index_name, ordinal`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: string }>({\n sql: `SELECT c.relname AS table_name, c.reltuples::bigint AS n\n FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace\n WHERE n.nspname = $1 AND c.relkind = 'r'`,\n params: [target],\n });\n\n const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table_name,\n referencedColumn: fk.referenced_column_name,\n referencedSchema: fk.referenced_table_schema,\n })),\n indexes.rows.map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: r.is_unique,\n columnName: r.column_name,\n ordinal: Number(r.ordinal),\n })),\n rowCounts.rows\n .map((r) => ({ schema: target, table: r.table_name, count: Number(r.n) }))\n .filter((r) => r.count >= 0),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema, type IndexColumnRow } from './build-schema';\nimport type { SchemaColumn, SchemaData, SchemaForeignKey } from './schema';\n\n/** SQLite catalog reader (sqlite_master + pragma table-valued functions). Also serves libsql/turso —\n * they are all SQLite engines with the same catalog. Pass an executor from `createSqliteExecutor`. */\nexport async function introspectSqlite(executor: DbExecutor): Promise<SchemaData> {\n const tables = await executor.run<{ name: string; type: string }>({\n sql: `SELECT name, type FROM sqlite_master\n WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'\n ORDER BY type, name`,\n });\n\n const columns: (SchemaColumn & { schema: string; table: string })[] = [];\n const fks: (SchemaForeignKey & { schema: string; table: string })[] = [];\n const indexColumns: IndexColumnRow[] = [];\n\n for (const t of tables.rows) {\n // Table-valued pragma functions accept the table name as a bound parameter, so no identifier\n // interpolation is needed.\n const cols = await executor.run<{\n name: string;\n type: string;\n notnull: number;\n dflt_value: string | null;\n pk: number;\n }>({\n sql: 'SELECT name, type, \"notnull\", dflt_value, pk FROM pragma_table_info(?)',\n params: [t.name],\n });\n\n for (const c of cols.rows) {\n columns.push({\n schema: 'main',\n table: t.name,\n name: c.name,\n dataType: c.type || 'TEXT',\n nullable: c.notnull === 0 && c.pk === 0,\n isPrimaryKey: c.pk > 0,\n defaultValue: c.dflt_value ?? undefined,\n });\n }\n\n const fkRows = await executor.run<{ from: string; table: string; to: string }>({\n sql: 'SELECT \"from\", \"table\", \"to\" FROM pragma_foreign_key_list(?)',\n params: [t.name],\n });\n for (const fk of fkRows.rows) {\n fks.push({\n schema: 'main',\n table: t.name,\n columnName: fk.from,\n referencedTable: fk.table,\n referencedColumn: fk.to,\n });\n }\n\n // Indexes: list them, then read each index's columns (pragma_index_info, ordered by seqno).\n const idxList = await executor.run<{ name: string; unique: number }>({\n sql: 'SELECT name, \"unique\" FROM pragma_index_list(?)',\n params: [t.name],\n });\n for (const idx of idxList.rows) {\n const idxCols = await executor.run<{ seqno: number; name: string | null }>({\n sql: 'SELECT seqno, name FROM pragma_index_info(?)',\n params: [idx.name],\n });\n for (const ic of idxCols.rows) {\n if (ic.name == null) continue; // expression-index columns have no name\n indexColumns.push({\n schema: 'main',\n table: t.name,\n indexName: idx.name,\n unique: idx.unique === 1,\n columnName: ic.name,\n ordinal: ic.seqno,\n });\n }\n }\n }\n\n return buildSchema(\n tables.rows.map((t) => ({ schema: 'main', name: t.name, isView: t.type === 'view' })),\n columns,\n fks,\n indexColumns,\n );\n}\n","/**\n * Schema introspection: read a database's catalog (tables, views, columns, primary keys, foreign\n * keys, indexes, approximate row counts) into a normalized {@link SchemaData}.\n *\n * This entry point pulls in NO driver — it runs entirely through a {@link DbExecutor} you supply, so\n * it reuses that executor's connection, credentials, and placeholder convention rather than opening\n * its own. Build the executor from the matching dialect subpath and pass it in.\n */\nimport type { DbExecutor } from '../index';\nimport { introspectMssql } from './mssql';\nimport { introspectMysql } from './mysql';\nimport { introspectPostgres } from './postgres';\nimport type { SchemaData } from './schema';\nimport { introspectSqlite } from './sqlite';\n\nexport type {\n SchemaColumn,\n SchemaForeignKey,\n SchemaIndex,\n SchemaTable,\n SchemaData,\n} from './schema';\nexport { buildSchema, type RawTable, type IndexColumnRow } from './build-schema';\nexport { introspectPostgres } from './postgres';\nexport { introspectMysql } from './mysql';\nexport { introspectMssql } from './mssql';\nexport { introspectSqlite } from './sqlite';\n\n/** The dialects whose catalog this package can read. libsql and turso use `'sqlite'`. */\nexport type IntrospectDialect = 'postgres' | 'mysql' | 'mssql' | 'sqlite';\n\n/**\n * Read a database's catalog as a {@link SchemaData}, through a supplied {@link DbExecutor}. Choose\n * the `dialect` matching the executor you built — the reader runs that dialect's catalog queries.\n * `schema` scopes the namespace, defaulting per dialect (postgres `public`, mysql the current\n * database, mssql `dbo`, sqlite `main` — sqlite ignores it).\n */\nexport function introspectSchema(\n executor: DbExecutor,\n dialect: IntrospectDialect,\n schema?: string,\n): Promise<SchemaData> {\n switch (dialect) {\n case 'postgres':\n return introspectPostgres(executor, schema);\n case 'mysql':\n return introspectMysql(executor, schema);\n case 'mssql':\n return introspectMssql(executor, schema);\n case 'sqlite':\n return introspectSqlite(executor);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/introspection/build-schema.ts","../../src/introspection/mssql.ts","../../src/introspection/mysql.ts","../../src/introspection/postgres.ts","../../src/introspection/sqlite.ts","../../src/introspection/index.ts"],"sourcesContent":["import type {\n SchemaColumn,\n SchemaData,\n SchemaForeignKey,\n SchemaIndex,\n SchemaTable,\n} from './schema';\n\n/** A table row as read from the catalog, before assembly. */\nexport type RawTable = {\n schema: string;\n name: string;\n isView: boolean;\n};\n\n/** One (index, column) pair as read from the catalog, before grouping into per-table indexes. */\nexport type IndexColumnRow = {\n schema: string;\n table: string;\n indexName: string;\n unique: boolean;\n columnName: string;\n ordinal: number;\n};\n\n/** Stitch flat catalog rows into per-table column/FK/index lists. Shared by every dialect reader. */\nexport function buildSchema(\n tables: RawTable[],\n columns: (SchemaColumn & { schema: string; table: string })[],\n fks: (SchemaForeignKey & { schema: string; table: string })[],\n indexColumns: IndexColumnRow[] = [],\n rowCounts: { schema: string; table: string; count: number }[] = [],\n): SchemaData {\n const colMap = new Map<string, SchemaColumn[]>();\n for (const c of columns) {\n const key = `${c.schema}.${c.table}`;\n const list = colMap.get(key) ?? [];\n list.push({\n name: c.name,\n dataType: c.dataType,\n nullable: c.nullable,\n isPrimaryKey: c.isPrimaryKey,\n defaultValue: c.defaultValue,\n });\n colMap.set(key, list);\n }\n\n const fkMap = new Map<string, SchemaForeignKey[]>();\n for (const fk of fks) {\n const key = `${fk.schema}.${fk.table}`;\n const list = fkMap.get(key) ?? [];\n list.push({\n columnName: fk.columnName,\n referencedTable: fk.referencedTable,\n referencedColumn: fk.referencedColumn,\n referencedSchema: fk.referencedSchema,\n });\n fkMap.set(key, list);\n }\n\n // Group flat (index, column) rows into per-table indexes, columns ordered within each index.\n const idxMap = new Map<\n string,\n Map<string, { unique: boolean; cols: { name: string; ordinal: number }[] }>\n >();\n for (const r of indexColumns) {\n const key = `${r.schema}.${r.table}`;\n const byName = idxMap.get(key) ?? new Map();\n const idx = byName.get(r.indexName) ?? { unique: r.unique, cols: [] };\n idx.cols.push({ name: r.columnName, ordinal: r.ordinal });\n byName.set(r.indexName, idx);\n idxMap.set(key, byName);\n }\n const indexesFor = (key: string): SchemaIndex[] =>\n [...(idxMap.get(key)?.entries() ?? [])].map(([name, i]) => ({\n name,\n unique: i.unique,\n columns: i.cols.sort((a, b) => a.ordinal - b.ordinal).map((c) => c.name),\n }));\n\n const rowCountMap = new Map(rowCounts.map((r) => [`${r.schema}.${r.table}`, r.count]));\n\n const result: SchemaTable[] = tables.map((t) => {\n const key = `${t.schema}.${t.name}`;\n return {\n schema: t.schema,\n name: t.name,\n type: t.isView ? 'view' : 'table',\n columns: colMap.get(key) ?? [],\n foreignKeys: fkMap.get(key) ?? [],\n indexes: indexesFor(key),\n approxRowCount: rowCountMap.get(key),\n };\n });\n\n return { tables: result };\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** SQL Server catalog reader (INFORMATION_SCHEMA + sys catalog). Pass an executor connected to a\n * SQL Server database (`createMssqlExecutor`). */\nexport async function introspectMssql(executor: DbExecutor, schema?: string): Promise<SchemaData> {\n const target = schema || 'dbo';\n\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type\n FROM INFORMATION_SCHEMA.TABLES\n WHERE TABLE_SCHEMA = @p0 AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')\n ORDER BY TABLE_TYPE, TABLE_NAME`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,\n IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default\n FROM INFORMATION_SCHEMA.COLUMNS\n WHERE TABLE_SCHEMA = @p0\n ORDER BY TABLE_NAME, ORDINAL_POSITION`,\n params: [target],\n });\n\n const pks = await executor.run<{ table_name: string; column_name: string }>({\n sql: `SELECT kcu.TABLE_NAME AS table_name, kcu.COLUMN_NAME AS column_name\n FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc\n JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu\n ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA\n WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = @p0`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_schema: string;\n referenced_table: string;\n referenced_column: string;\n }>({\n sql: `SELECT t1.name AS table_name, c1.name AS column_name,\n s2.name AS referenced_schema, t2.name AS referenced_table, c2.name AS referenced_column\n FROM sys.foreign_key_columns fkc\n JOIN sys.objects t1 ON fkc.parent_object_id = t1.object_id\n JOIN sys.columns c1 ON fkc.parent_object_id = c1.object_id AND fkc.parent_column_id = c1.column_id\n JOIN sys.objects t2 ON fkc.referenced_object_id = t2.object_id\n JOIN sys.columns c2 ON fkc.referenced_object_id = c2.object_id AND fkc.referenced_column_id = c2.column_id\n JOIN sys.schemas s1 ON t1.schema_id = s1.schema_id\n JOIN sys.schemas s2 ON t2.schema_id = s2.schema_id\n WHERE s1.name = @p0`,\n params: [target],\n });\n\n // Secondary indexes (sys.index_columns, key columns only in key_ordinal order; `i.type > 0` skips\n // heaps) + an approximate row count from partition stats.\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n is_unique: boolean | number;\n column_name: string;\n ordinal: number;\n }>({\n sql: `SELECT t.name AS table_name, i.name AS index_name, i.is_unique AS is_unique,\n c.name AS column_name, ic.key_ordinal AS ordinal\n FROM sys.indexes i\n JOIN sys.tables t ON t.object_id = i.object_id\n JOIN sys.schemas s ON s.schema_id = t.schema_id\n JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id\n JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id\n WHERE s.name = @p0 AND i.type > 0 AND ic.is_included_column = 0\n ORDER BY t.name, i.name, ic.key_ordinal`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: number }>({\n sql: `SELECT t.name AS table_name, SUM(p.rows) AS n\n FROM sys.tables t\n JOIN sys.schemas s ON s.schema_id = t.schema_id\n JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)\n WHERE s.name = @p0\n GROUP BY t.name`,\n params: [target],\n });\n\n const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table,\n referencedColumn: fk.referenced_column,\n referencedSchema: fk.referenced_schema,\n })),\n indexes.rows.map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: Boolean(r.is_unique),\n columnName: r.column_name,\n ordinal: Number(r.ordinal),\n })),\n rowCounts.rows.map((r) => ({ schema: target, table: r.table_name, count: Number(r.n) })),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** MySQL / MariaDB catalog reader (information_schema). Pass an executor connected to a MySQL\n * database (`createMysqlExecutor`). */\nexport async function introspectMysql(executor: DbExecutor, schema?: string): Promise<SchemaData> {\n let target = schema;\n if (!target) {\n const r = await executor.run<{ db: string }>({ sql: 'SELECT DATABASE() AS db' });\n target = r.rows[0]?.db ?? '';\n }\n\n // Explicit lowercase aliases: MySQL 8's information_schema returns UPPERCASE column headers unless\n // aliased (5.7/MariaDB return them as written) — without them every row property reads undefined.\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type\n FROM information_schema.tables\n WHERE table_schema = ? AND table_type IN ('BASE TABLE', 'VIEW')\n ORDER BY table_type, table_name`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n column_key: string;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,\n IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default, COLUMN_KEY AS column_key\n FROM information_schema.columns\n WHERE table_schema = ?\n ORDER BY table_name, ordinal_position`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_table_schema: string;\n referenced_table_name: string;\n referenced_column_name: string;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name,\n REFERENCED_TABLE_SCHEMA AS referenced_table_schema,\n REFERENCED_TABLE_NAME AS referenced_table_name,\n REFERENCED_COLUMN_NAME AS referenced_column_name\n FROM information_schema.key_column_usage\n WHERE table_schema = ? AND referenced_table_name IS NOT NULL`,\n params: [target],\n });\n\n // Secondary indexes (one row per index column, ordered by SEQ_IN_INDEX) + approx row count.\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n non_unique: number;\n column_name: string | null;\n seq: number;\n }>({\n sql: `SELECT TABLE_NAME AS table_name, INDEX_NAME AS index_name, NON_UNIQUE AS non_unique,\n COLUMN_NAME AS column_name, SEQ_IN_INDEX AS seq\n FROM information_schema.statistics\n WHERE table_schema = ?\n ORDER BY table_name, index_name, seq`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: number | null }>({\n sql: `SELECT TABLE_NAME AS table_name, TABLE_ROWS AS n\n FROM information_schema.tables\n WHERE table_schema = ? AND table_type = 'BASE TABLE'`,\n params: [target],\n });\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: c.column_key === 'PRI',\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table_name,\n referencedColumn: fk.referenced_column_name,\n referencedSchema: fk.referenced_table_schema,\n })),\n indexes.rows\n .filter((r) => r.column_name != null)\n .map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: r.non_unique === 0,\n columnName: r.column_name!,\n ordinal: Number(r.seq),\n })),\n rowCounts.rows.map((r) => ({ schema: target, table: r.table_name, count: Number(r.n ?? 0) })),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema } from './build-schema';\nimport type { SchemaData } from './schema';\n\n/** Postgres catalog reader (information_schema + pg_catalog). Pass an executor connected to a\n * Postgres database (`createPostgresExecutor`). */\nexport async function introspectPostgres(\n executor: DbExecutor,\n schema?: string,\n): Promise<SchemaData> {\n const target = schema || 'public';\n\n const tables = await executor.run<{ table_name: string; table_type: string }>({\n sql: `SELECT table_name, table_type\n FROM information_schema.tables\n WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')\n ORDER BY table_type, table_name`,\n params: [target],\n });\n\n const columns = await executor.run<{\n table_name: string;\n column_name: string;\n data_type: string;\n is_nullable: string;\n column_default: string | null;\n }>({\n sql: `SELECT table_name, column_name, data_type, is_nullable, column_default\n FROM information_schema.columns\n WHERE table_schema = $1\n ORDER BY table_name, ordinal_position`,\n params: [target],\n });\n\n const pks = await executor.run<{ table_name: string; column_name: string }>({\n sql: `SELECT kcu.table_name, kcu.column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema\n WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1`,\n params: [target],\n });\n\n const fks = await executor.run<{\n table_name: string;\n column_name: string;\n referenced_table_schema: string;\n referenced_table_name: string;\n referenced_column_name: string;\n }>({\n sql: `SELECT kcu.table_name, kcu.column_name,\n ccu.table_schema AS referenced_table_schema,\n ccu.table_name AS referenced_table_name,\n ccu.column_name AS referenced_column_name\n FROM information_schema.table_constraints tc\n JOIN information_schema.key_column_usage kcu\n ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema\n JOIN information_schema.constraint_column_usage ccu\n ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema\n WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`,\n params: [target],\n });\n\n // Secondary indexes: columns unnested in key order (WITH ORDINALITY), `attnum > 0` drops\n // expression-index entries. Plus an approximate row count from planner stats (reltuples).\n //\n // relkind IN ('r','p'): a PARTITIONED table's parent is 'p', not 'r'. Filtering on 'r' alone\n // reported every partitioned table as having no indexes and no row count — silently, and worst on\n // exactly the biggest tables in a database, which are the ones most likely to be partitioned.\n const indexes = await executor.run<{\n table_name: string;\n index_name: string;\n is_unique: boolean;\n column_name: string;\n ordinal: number;\n }>({\n sql: `SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique,\n a.attname AS column_name, k.ord AS ordinal\n FROM pg_index ix\n JOIN pg_class i ON i.oid = ix.indexrelid\n JOIN pg_class t ON t.oid = ix.indrelid\n JOIN pg_namespace n ON n.oid = t.relnamespace\n JOIN LATERAL unnest(string_to_array(ix.indkey::text, ' ')::int[]) WITH ORDINALITY AS k(attnum, ord)\n ON true\n JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum\n WHERE n.nspname = $1 AND t.relkind IN ('r', 'p') AND k.attnum > 0\n ORDER BY table_name, index_name, ordinal`,\n params: [target],\n });\n\n const rowCounts = await executor.run<{ table_name: string; n: string }>({\n sql: `SELECT c.relname AS table_name, c.reltuples::bigint AS n\n FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace\n -- reltuples on a partitioned parent is -1 until it is ANALYZEd; the >= 0 filter below\n -- drops that rather than reporting a table with minus one row.\n WHERE n.nspname = $1 AND c.relkind IN ('r', 'p')`,\n params: [target],\n });\n\n const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));\n\n return buildSchema(\n tables.rows.map((t) => ({\n schema: target,\n name: t.table_name,\n isView: t.table_type === 'VIEW',\n })),\n columns.rows.map((c) => ({\n schema: target,\n table: c.table_name,\n name: c.column_name,\n dataType: c.data_type,\n nullable: c.is_nullable === 'YES',\n isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),\n defaultValue: c.column_default ?? undefined,\n })),\n fks.rows.map((fk) => ({\n schema: target,\n table: fk.table_name,\n columnName: fk.column_name,\n referencedTable: fk.referenced_table_name,\n referencedColumn: fk.referenced_column_name,\n referencedSchema: fk.referenced_table_schema,\n })),\n indexes.rows.map((r) => ({\n schema: target,\n table: r.table_name,\n indexName: r.index_name,\n unique: r.is_unique,\n columnName: r.column_name,\n ordinal: Number(r.ordinal),\n })),\n rowCounts.rows\n .map((r) => ({ schema: target, table: r.table_name, count: Number(r.n) }))\n .filter((r) => r.count >= 0),\n );\n}\n","import type { DbExecutor } from '../index';\nimport { buildSchema, type IndexColumnRow } from './build-schema';\nimport type { SchemaColumn, SchemaData, SchemaForeignKey } from './schema';\n\n/** SQLite catalog reader (sqlite_master + pragma table-valued functions). Also serves libsql/turso —\n * they are all SQLite engines with the same catalog. Pass an executor from `createSqliteExecutor`. */\nexport async function introspectSqlite(executor: DbExecutor): Promise<SchemaData> {\n const tables = await executor.run<{ name: string; type: string }>({\n sql: `SELECT name, type FROM sqlite_master\n WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'\n ORDER BY type, name`,\n });\n\n const columns: (SchemaColumn & { schema: string; table: string })[] = [];\n const fks: (SchemaForeignKey & { schema: string; table: string })[] = [];\n const indexColumns: IndexColumnRow[] = [];\n\n for (const t of tables.rows) {\n // Table-valued pragma functions accept the table name as a bound parameter, so no identifier\n // interpolation is needed.\n const cols = await executor.run<{\n name: string;\n type: string;\n notnull: number;\n dflt_value: string | null;\n pk: number;\n }>({\n sql: 'SELECT name, type, \"notnull\", dflt_value, pk FROM pragma_table_info(?)',\n params: [t.name],\n });\n\n for (const c of cols.rows) {\n columns.push({\n schema: 'main',\n table: t.name,\n name: c.name,\n dataType: c.type || 'TEXT',\n nullable: c.notnull === 0 && c.pk === 0,\n isPrimaryKey: c.pk > 0,\n defaultValue: c.dflt_value ?? undefined,\n });\n }\n\n const fkRows = await executor.run<{ from: string; table: string; to: string }>({\n sql: 'SELECT \"from\", \"table\", \"to\" FROM pragma_foreign_key_list(?)',\n params: [t.name],\n });\n for (const fk of fkRows.rows) {\n fks.push({\n schema: 'main',\n table: t.name,\n columnName: fk.from,\n referencedTable: fk.table,\n referencedColumn: fk.to,\n });\n }\n\n // Indexes: list them, then read each index's columns (pragma_index_info, ordered by seqno).\n const idxList = await executor.run<{ name: string; unique: number }>({\n sql: 'SELECT name, \"unique\" FROM pragma_index_list(?)',\n params: [t.name],\n });\n for (const idx of idxList.rows) {\n const idxCols = await executor.run<{ seqno: number; name: string | null }>({\n sql: 'SELECT seqno, name FROM pragma_index_info(?)',\n params: [idx.name],\n });\n for (const ic of idxCols.rows) {\n if (ic.name == null) continue; // expression-index columns have no name\n indexColumns.push({\n schema: 'main',\n table: t.name,\n indexName: idx.name,\n unique: idx.unique === 1,\n columnName: ic.name,\n ordinal: ic.seqno,\n });\n }\n }\n }\n\n return buildSchema(\n tables.rows.map((t) => ({ schema: 'main', name: t.name, isView: t.type === 'view' })),\n columns,\n fks,\n indexColumns,\n );\n}\n","/**\n * Schema introspection: read a database's catalog (tables, views, columns, primary keys, foreign\n * keys, indexes, approximate row counts) into a normalized {@link SchemaData}.\n *\n * This entry point pulls in NO driver — it runs entirely through a {@link DbExecutor} you supply, so\n * it reuses that executor's connection, credentials, and placeholder convention rather than opening\n * its own. Build the executor from the matching dialect subpath and pass it in.\n */\nimport type { DbExecutor } from '../index';\nimport { introspectMssql } from './mssql';\nimport { introspectMysql } from './mysql';\nimport { introspectPostgres } from './postgres';\nimport type { SchemaData } from './schema';\nimport { introspectSqlite } from './sqlite';\n\nexport type {\n SchemaColumn,\n SchemaForeignKey,\n SchemaIndex,\n SchemaTable,\n SchemaData,\n} from './schema';\nexport { buildSchema, type RawTable, type IndexColumnRow } from './build-schema';\nexport { introspectPostgres } from './postgres';\nexport { introspectMysql } from './mysql';\nexport { introspectMssql } from './mssql';\nexport { introspectSqlite } from './sqlite';\n\n/** The dialects whose catalog this package can read. libsql and turso use `'sqlite'`. */\nexport type IntrospectDialect = 'postgres' | 'mysql' | 'mssql' | 'sqlite';\n\n/**\n * Read a database's catalog as a {@link SchemaData}, through a supplied {@link DbExecutor}. Choose\n * the `dialect` matching the executor you built — the reader runs that dialect's catalog queries.\n * `schema` scopes the namespace, defaulting per dialect (postgres `public`, mysql the current\n * database, mssql `dbo`, sqlite `main` — sqlite ignores it).\n */\nexport function introspectSchema(\n executor: DbExecutor,\n dialect: IntrospectDialect,\n schema?: string,\n): Promise<SchemaData> {\n switch (dialect) {\n case 'postgres':\n return introspectPostgres(executor, schema);\n case 'mysql':\n return introspectMysql(executor, schema);\n case 'mssql':\n return introspectMssql(executor, schema);\n case 'sqlite':\n return introspectSqlite(executor);\n }\n}\n"],"mappings":"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"}
|
package/dist/mssql/index.cjs
CHANGED
|
@@ -86,6 +86,22 @@ const toResult = (result) => ({
|
|
|
86
86
|
rows: result.recordset ?? [],
|
|
87
87
|
rowCount: result.recordset ? result.recordset.length : result.rowsAffected?.[0] ?? 0
|
|
88
88
|
});
|
|
89
|
+
/**
|
|
90
|
+
* SQLEasy's mssql dialect unconditionally prefixes `SET NOCOUNT ON; ` to every statement it emits.
|
|
91
|
+
* NOCOUNT suppresses the DONE row counts that tedious reads, so `rowsAffected` came back `[]` and
|
|
92
|
+
* every INSERT/UPDATE/DELETE routed through here reported `rowCount: 0` — a write that plainly
|
|
93
|
+
* succeeded looked like it had touched nothing.
|
|
94
|
+
*
|
|
95
|
+
* Rewritten rather than stripped: forcing OFF also corrects a session that already had NOCOUNT ON,
|
|
96
|
+
* whereas removing the prefix would just inherit whatever the connection was left in. The match is
|
|
97
|
+
* anchored at the start of the batch, so it cannot fire on the same text inside a string literal —
|
|
98
|
+
* the reason this file refuses to rewrite `?` placeholders by scanning.
|
|
99
|
+
*
|
|
100
|
+
* The real fix belongs upstream in SQLEasy, which has no reason to emit this at all; the prefix is
|
|
101
|
+
* frozen into its cross-language golden corpus, so it cannot move without the Dart port moving in
|
|
102
|
+
* lockstep.
|
|
103
|
+
*/
|
|
104
|
+
const withRowCounts = (sql) => sql.replace(/^\s*SET\s+NOCOUNT\s+ON\s*;/i, "SET NOCOUNT OFF;");
|
|
89
105
|
const bindParams = (request, params) => {
|
|
90
106
|
(params ?? []).forEach((value, i) => request.input(`p${i}`, value));
|
|
91
107
|
return request;
|
|
@@ -109,7 +125,7 @@ function createMssqlExecutor(config) {
|
|
|
109
125
|
async run(prepared) {
|
|
110
126
|
await ensureReady();
|
|
111
127
|
const request = bindParams(pool.request(), prepared.params);
|
|
112
|
-
return toResult(await request.query(prepared.sql));
|
|
128
|
+
return toResult(await request.query(withRowCounts(prepared.sql)));
|
|
113
129
|
},
|
|
114
130
|
async transaction(statements) {
|
|
115
131
|
await ensureReady();
|
|
@@ -119,7 +135,7 @@ function createMssqlExecutor(config) {
|
|
|
119
135
|
const results = [];
|
|
120
136
|
for (const s of statements) {
|
|
121
137
|
const request = bindParams(new Request(tx), s.params);
|
|
122
|
-
results.push(toResult(await request.query(s.sql)));
|
|
138
|
+
results.push(toResult(await request.query(withRowCounts(s.sql))));
|
|
123
139
|
}
|
|
124
140
|
await tx.commit();
|
|
125
141
|
return results;
|
|
@@ -151,5 +167,6 @@ function createMssqlExecutor(config) {
|
|
|
151
167
|
exports.createMssqlExecutor = createMssqlExecutor;
|
|
152
168
|
exports.parsePlanXml = parsePlanXml;
|
|
153
169
|
exports.toExplainableBatch = toExplainableBatch;
|
|
170
|
+
exports.withRowCounts = withRowCounts;
|
|
154
171
|
|
|
155
172
|
//# sourceMappingURL=index.cjs.map
|
package/dist/mssql/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/mssql/index.ts"],"sourcesContent":["// `mssql` is CommonJS and Node's ESM loader can't see its named exports — import the default and\n// destructure. (pg/mysql2/@libsql expose named exports fine.)\nimport mssql from 'mssql';\nimport type { config as MssqlDriverConfig, IResult } from 'mssql';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\nconst { ConnectionPool, Transaction, Request } = mssql;\n\n/** Connection settings — any `mssql` config object, or a raw connection string. */\nexport type MssqlConfig = MssqlDriverConfig | { connectionString: string };\n\n/** Read the T-SQL string literal starting at `from` (just past the opening quote), honouring `''`\n * escapes. Returns the unescaped text and the index just past the closing quote. */\nfunction readLiteral(sql: string, from: number): { text: string; end: number } | undefined {\n let out = '';\n for (let i = from; i < sql.length; i++) {\n if (sql[i] !== \"'\") {\n out += sql[i];\n continue;\n }\n if (sql[i + 1] === \"'\") {\n out += \"'\";\n i++;\n continue;\n }\n return { text: out, end: i + 1 };\n }\n return undefined; // unterminated — caller falls back to the raw batch\n}\n\n/**\n * Turn the mssql dialect's output into something SHOWPLAN can actually cost.\n *\n * It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does\n * NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement\n * must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared\n * outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost\n * estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.\n */\nexport function toExplainableBatch(sql: string): string {\n const m = /exec\\s+sp_executesql\\s+N'/i.exec(sql);\n if (!m) return sql;\n const inner = readLiteral(sql, m.index + m[0].length);\n if (!inner) return sql;\n const decl = /^\\s*,\\s*N'/.exec(sql.slice(inner.end));\n const decls = decl ? readLiteral(sql, inner.end + decl[0].length)?.text.trim() : '';\n return decls ? `DECLARE ${decls};\\n${inner.text}` : inner.text;\n}\n\n/** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */\nexport function parsePlanXml(xml: string): ExplainEstimate {\n // A batch holds one <StmtSimple> per statement (the injected DECLARE, SET NOCOUNT ON, the SELECT).\n // Take the most expensive — never blindly the first, which is usually a costless preamble.\n let best: { cost: number; rows?: number } | undefined;\n for (const [tag] of xml.matchAll(/<StmtSimple\\b[^>]*>/g)) {\n const cost = Number(/StatementSubTreeCost=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n if (!Number.isFinite(cost) || (best && cost <= best.cost)) continue;\n const rows = Number(/StatementEstRows=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n best = { cost, rows: Number.isFinite(rows) ? rows : undefined };\n }\n return {\n cost: best?.cost,\n rows: best?.rows,\n fullScan: /PhysicalOp=\"(?:Table Scan|Clustered Index Scan|Index Scan)\"/.test(xml),\n plan: xml.slice(0, 500),\n };\n}\n\nconst toResult = <T>(result: IResult<unknown>): QueryResult<T> => ({\n rows: (result.recordset ?? []) as unknown as T[],\n rowCount: result.recordset ? result.recordset.length : (result.rowsAffected?.[0] ?? 0),\n});\n\n// Bind params (if any) as @p0..@pN. SQLEasy's mssql dialect inlines its values into the\n// sp_executesql batch and passes `params: []`, so nothing binds on that path; a caller passing bound\n// values must reference @p0.. in their SQL (mssql has no positional `?`). No `?`→`@p` rewriting —\n// that scan corrupts a `?` inside a string literal. query() re-wraps its argument in sp_executesql,\n// which is harmless for both a plain statement and a pre-formed batch (verified against real SQL\n// Server: two sp_executesql inserts in a transaction commit both).\ntype BindableRequest = { input(name: string, value: unknown): unknown };\nconst bindParams = <R extends BindableRequest>(request: R, params?: readonly unknown[]): R => {\n (params ?? []).forEach((value, i) => request.input(`p${i}`, value));\n return request;\n};\n\n/**\n * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained\n * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).\n */\nexport function createMssqlExecutor(config: MssqlConfig): DbExecutor {\n const makePool = () =>\n new ConnectionPool('connectionString' in config ? config.connectionString : config);\n let pool = makePool();\n\n // Single-flight connect that RECOVERS. Caching a rejected `pool.connect()` promise would brick the\n // pool forever (a DB restart / blip): every later query awaits the same settled rejection. So reset\n // the gate and rebuild the pool on failure, and the next call retries. (pg/mysql self-heal\n // per-acquire; mssql caches connect, so it alone needs this.)\n let ready: Promise<unknown> | undefined;\n const ensureReady = () =>\n (ready ??= pool.connect().catch((e: unknown) => {\n ready = undefined;\n const dead = pool;\n pool = makePool();\n void dead.close().catch(() => {});\n throw e;\n }));\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n await ensureReady();\n const request = bindParams(pool.request(), prepared.params);\n return toResult<T>(await request.query(prepared.sql));\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n await ensureReady();\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n const results: QueryResult[] = [];\n for (const s of statements) {\n const request = bindParams(new Request(tx), s.params);\n results.push(toResult(await request.query(s.sql)));\n }\n await tx.commit();\n return results;\n } catch (err) {\n await tx.rollback().catch(() => {});\n throw err;\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n await ensureReady();\n // SQL Server has no EXPLAIN. The estimated plan comes from SET SHOWPLAN_XML, which (a) must be\n // the ONLY statement in its batch and (b) is SESSION state — so the SET and the query must run\n // on the SAME connection. A transaction pins one connection for both; the finally block always\n // clears the flag and releases it, so SHOWPLAN never leaks onto a connection serving reads.\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n await new Request(tx).batch('SET SHOWPLAN_XML ON');\n const res = await new Request(tx).batch(toExplainableBatch(prepared.sql));\n const first = res.recordset?.[0] as Row | undefined;\n return parsePlanXml(String(first ? Object.values(first)[0] : ''));\n } finally {\n await new Request(tx).batch('SET SHOWPLAN_XML OFF').catch(() => {});\n await tx.rollback().catch(() => {});\n }\n },\n\n async close(): Promise<void> {\n await (ready?.catch(() => {}) ?? Promise.resolve());\n await pool.close();\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/mssql/index.ts"],"sourcesContent":["// `mssql` is CommonJS and Node's ESM loader can't see its named exports — import the default and\n// destructure. (pg/mysql2/@libsql expose named exports fine.)\nimport mssql from 'mssql';\nimport type { config as MssqlDriverConfig, IResult } from 'mssql';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\nconst { ConnectionPool, Transaction, Request } = mssql;\n\n/** Connection settings — any `mssql` config object, or a raw connection string. */\nexport type MssqlConfig = MssqlDriverConfig | { connectionString: string };\n\n/** Read the T-SQL string literal starting at `from` (just past the opening quote), honouring `''`\n * escapes. Returns the unescaped text and the index just past the closing quote. */\nfunction readLiteral(sql: string, from: number): { text: string; end: number } | undefined {\n let out = '';\n for (let i = from; i < sql.length; i++) {\n if (sql[i] !== \"'\") {\n out += sql[i];\n continue;\n }\n if (sql[i + 1] === \"'\") {\n out += \"'\";\n i++;\n continue;\n }\n return { text: out, end: i + 1 };\n }\n return undefined; // unterminated — caller falls back to the raw batch\n}\n\n/**\n * Turn the mssql dialect's output into something SHOWPLAN can actually cost.\n *\n * It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does\n * NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement\n * must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared\n * outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost\n * estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.\n */\nexport function toExplainableBatch(sql: string): string {\n const m = /exec\\s+sp_executesql\\s+N'/i.exec(sql);\n if (!m) return sql;\n const inner = readLiteral(sql, m.index + m[0].length);\n if (!inner) return sql;\n const decl = /^\\s*,\\s*N'/.exec(sql.slice(inner.end));\n const decls = decl ? readLiteral(sql, inner.end + decl[0].length)?.text.trim() : '';\n return decls ? `DECLARE ${decls};\\n${inner.text}` : inner.text;\n}\n\n/** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */\nexport function parsePlanXml(xml: string): ExplainEstimate {\n // A batch holds one <StmtSimple> per statement (the injected DECLARE, SET NOCOUNT ON, the SELECT).\n // Take the most expensive — never blindly the first, which is usually a costless preamble.\n let best: { cost: number; rows?: number } | undefined;\n for (const [tag] of xml.matchAll(/<StmtSimple\\b[^>]*>/g)) {\n const cost = Number(/StatementSubTreeCost=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n if (!Number.isFinite(cost) || (best && cost <= best.cost)) continue;\n const rows = Number(/StatementEstRows=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n best = { cost, rows: Number.isFinite(rows) ? rows : undefined };\n }\n return {\n cost: best?.cost,\n rows: best?.rows,\n fullScan: /PhysicalOp=\"(?:Table Scan|Clustered Index Scan|Index Scan)\"/.test(xml),\n plan: xml.slice(0, 500),\n };\n}\n\nconst toResult = <T>(result: IResult<unknown>): QueryResult<T> => ({\n rows: (result.recordset ?? []) as unknown as T[],\n rowCount: result.recordset ? result.recordset.length : (result.rowsAffected?.[0] ?? 0),\n});\n\n/**\n * SQLEasy's mssql dialect unconditionally prefixes `SET NOCOUNT ON; ` to every statement it emits.\n * NOCOUNT suppresses the DONE row counts that tedious reads, so `rowsAffected` came back `[]` and\n * every INSERT/UPDATE/DELETE routed through here reported `rowCount: 0` — a write that plainly\n * succeeded looked like it had touched nothing.\n *\n * Rewritten rather than stripped: forcing OFF also corrects a session that already had NOCOUNT ON,\n * whereas removing the prefix would just inherit whatever the connection was left in. The match is\n * anchored at the start of the batch, so it cannot fire on the same text inside a string literal —\n * the reason this file refuses to rewrite `?` placeholders by scanning.\n *\n * The real fix belongs upstream in SQLEasy, which has no reason to emit this at all; the prefix is\n * frozen into its cross-language golden corpus, so it cannot move without the Dart port moving in\n * lockstep.\n */\nexport const withRowCounts = (sql: string): string =>\n sql.replace(/^\\s*SET\\s+NOCOUNT\\s+ON\\s*;/i, 'SET NOCOUNT OFF;');\n\n// Bind params (if any) as @p0..@pN. SQLEasy's mssql dialect inlines its values into the\n// sp_executesql batch and passes `params: []`, so nothing binds on that path; a caller passing bound\n// values must reference @p0.. in their SQL (mssql has no positional `?`). No `?`→`@p` rewriting —\n// that scan corrupts a `?` inside a string literal. query() re-wraps its argument in sp_executesql,\n// which is harmless for both a plain statement and a pre-formed batch (verified against real SQL\n// Server: two sp_executesql inserts in a transaction commit both).\ntype BindableRequest = { input(name: string, value: unknown): unknown };\nconst bindParams = <R extends BindableRequest>(request: R, params?: readonly unknown[]): R => {\n (params ?? []).forEach((value, i) => request.input(`p${i}`, value));\n return request;\n};\n\n/**\n * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained\n * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).\n */\nexport function createMssqlExecutor(config: MssqlConfig): DbExecutor {\n const makePool = () =>\n new ConnectionPool('connectionString' in config ? config.connectionString : config);\n let pool = makePool();\n\n // Single-flight connect that RECOVERS. Caching a rejected `pool.connect()` promise would brick the\n // pool forever (a DB restart / blip): every later query awaits the same settled rejection. So reset\n // the gate and rebuild the pool on failure, and the next call retries. (pg/mysql self-heal\n // per-acquire; mssql caches connect, so it alone needs this.)\n let ready: Promise<unknown> | undefined;\n const ensureReady = () =>\n (ready ??= pool.connect().catch((e: unknown) => {\n ready = undefined;\n const dead = pool;\n pool = makePool();\n void dead.close().catch(() => {});\n throw e;\n }));\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n await ensureReady();\n const request = bindParams(pool.request(), prepared.params);\n return toResult<T>(await request.query(withRowCounts(prepared.sql)));\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n await ensureReady();\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n const results: QueryResult[] = [];\n for (const s of statements) {\n const request = bindParams(new Request(tx), s.params);\n results.push(toResult(await request.query(withRowCounts(s.sql))));\n }\n await tx.commit();\n return results;\n } catch (err) {\n await tx.rollback().catch(() => {});\n throw err;\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n await ensureReady();\n // SQL Server has no EXPLAIN. The estimated plan comes from SET SHOWPLAN_XML, which (a) must be\n // the ONLY statement in its batch and (b) is SESSION state — so the SET and the query must run\n // on the SAME connection. A transaction pins one connection for both; the finally block always\n // clears the flag and releases it, so SHOWPLAN never leaks onto a connection serving reads.\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n await new Request(tx).batch('SET SHOWPLAN_XML ON');\n const res = await new Request(tx).batch(toExplainableBatch(prepared.sql));\n const first = res.recordset?.[0] as Row | undefined;\n return parsePlanXml(String(first ? Object.values(first)[0] : ''));\n } finally {\n await new Request(tx).batch('SET SHOWPLAN_XML OFF').catch(() => {});\n await tx.rollback().catch(() => {});\n }\n },\n\n async close(): Promise<void> {\n await (ready?.catch(() => {}) ?? Promise.resolve());\n await pool.close();\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/mssql/index.d.cts
CHANGED
|
@@ -17,11 +17,27 @@ type MssqlConfig = config | {
|
|
|
17
17
|
declare function toExplainableBatch(sql: string): string;
|
|
18
18
|
/** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */
|
|
19
19
|
declare function parsePlanXml(xml: string): ExplainEstimate;
|
|
20
|
+
/**
|
|
21
|
+
* SQLEasy's mssql dialect unconditionally prefixes `SET NOCOUNT ON; ` to every statement it emits.
|
|
22
|
+
* NOCOUNT suppresses the DONE row counts that tedious reads, so `rowsAffected` came back `[]` and
|
|
23
|
+
* every INSERT/UPDATE/DELETE routed through here reported `rowCount: 0` — a write that plainly
|
|
24
|
+
* succeeded looked like it had touched nothing.
|
|
25
|
+
*
|
|
26
|
+
* Rewritten rather than stripped: forcing OFF also corrects a session that already had NOCOUNT ON,
|
|
27
|
+
* whereas removing the prefix would just inherit whatever the connection was left in. The match is
|
|
28
|
+
* anchored at the start of the batch, so it cannot fire on the same text inside a string literal —
|
|
29
|
+
* the reason this file refuses to rewrite `?` placeholders by scanning.
|
|
30
|
+
*
|
|
31
|
+
* The real fix belongs upstream in SQLEasy, which has no reason to emit this at all; the prefix is
|
|
32
|
+
* frozen into its cross-language golden corpus, so it cannot move without the Dart port moving in
|
|
33
|
+
* lockstep.
|
|
34
|
+
*/
|
|
35
|
+
declare const withRowCounts: (sql: string) => string;
|
|
20
36
|
/**
|
|
21
37
|
* A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained
|
|
22
38
|
* `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).
|
|
23
39
|
*/
|
|
24
40
|
declare function createMssqlExecutor(config: MssqlConfig): DbExecutor;
|
|
25
41
|
//#endregion
|
|
26
|
-
export { MssqlConfig, createMssqlExecutor, parsePlanXml, toExplainableBatch };
|
|
42
|
+
export { MssqlConfig, createMssqlExecutor, parsePlanXml, toExplainableBatch, withRowCounts };
|
|
27
43
|
//# sourceMappingURL=index.d.cts.map
|
package/dist/mssql/index.d.mts
CHANGED
|
@@ -17,11 +17,27 @@ type MssqlConfig = config | {
|
|
|
17
17
|
declare function toExplainableBatch(sql: string): string;
|
|
18
18
|
/** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */
|
|
19
19
|
declare function parsePlanXml(xml: string): ExplainEstimate;
|
|
20
|
+
/**
|
|
21
|
+
* SQLEasy's mssql dialect unconditionally prefixes `SET NOCOUNT ON; ` to every statement it emits.
|
|
22
|
+
* NOCOUNT suppresses the DONE row counts that tedious reads, so `rowsAffected` came back `[]` and
|
|
23
|
+
* every INSERT/UPDATE/DELETE routed through here reported `rowCount: 0` — a write that plainly
|
|
24
|
+
* succeeded looked like it had touched nothing.
|
|
25
|
+
*
|
|
26
|
+
* Rewritten rather than stripped: forcing OFF also corrects a session that already had NOCOUNT ON,
|
|
27
|
+
* whereas removing the prefix would just inherit whatever the connection was left in. The match is
|
|
28
|
+
* anchored at the start of the batch, so it cannot fire on the same text inside a string literal —
|
|
29
|
+
* the reason this file refuses to rewrite `?` placeholders by scanning.
|
|
30
|
+
*
|
|
31
|
+
* The real fix belongs upstream in SQLEasy, which has no reason to emit this at all; the prefix is
|
|
32
|
+
* frozen into its cross-language golden corpus, so it cannot move without the Dart port moving in
|
|
33
|
+
* lockstep.
|
|
34
|
+
*/
|
|
35
|
+
declare const withRowCounts: (sql: string) => string;
|
|
20
36
|
/**
|
|
21
37
|
* A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained
|
|
22
38
|
* `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).
|
|
23
39
|
*/
|
|
24
40
|
declare function createMssqlExecutor(config: MssqlConfig): DbExecutor;
|
|
25
41
|
//#endregion
|
|
26
|
-
export { MssqlConfig, createMssqlExecutor, parsePlanXml, toExplainableBatch };
|
|
42
|
+
export { MssqlConfig, createMssqlExecutor, parsePlanXml, toExplainableBatch, withRowCounts };
|
|
27
43
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/mssql/index.mjs
CHANGED
|
@@ -62,6 +62,22 @@ const toResult = (result) => ({
|
|
|
62
62
|
rows: result.recordset ?? [],
|
|
63
63
|
rowCount: result.recordset ? result.recordset.length : result.rowsAffected?.[0] ?? 0
|
|
64
64
|
});
|
|
65
|
+
/**
|
|
66
|
+
* SQLEasy's mssql dialect unconditionally prefixes `SET NOCOUNT ON; ` to every statement it emits.
|
|
67
|
+
* NOCOUNT suppresses the DONE row counts that tedious reads, so `rowsAffected` came back `[]` and
|
|
68
|
+
* every INSERT/UPDATE/DELETE routed through here reported `rowCount: 0` — a write that plainly
|
|
69
|
+
* succeeded looked like it had touched nothing.
|
|
70
|
+
*
|
|
71
|
+
* Rewritten rather than stripped: forcing OFF also corrects a session that already had NOCOUNT ON,
|
|
72
|
+
* whereas removing the prefix would just inherit whatever the connection was left in. The match is
|
|
73
|
+
* anchored at the start of the batch, so it cannot fire on the same text inside a string literal —
|
|
74
|
+
* the reason this file refuses to rewrite `?` placeholders by scanning.
|
|
75
|
+
*
|
|
76
|
+
* The real fix belongs upstream in SQLEasy, which has no reason to emit this at all; the prefix is
|
|
77
|
+
* frozen into its cross-language golden corpus, so it cannot move without the Dart port moving in
|
|
78
|
+
* lockstep.
|
|
79
|
+
*/
|
|
80
|
+
const withRowCounts = (sql) => sql.replace(/^\s*SET\s+NOCOUNT\s+ON\s*;/i, "SET NOCOUNT OFF;");
|
|
65
81
|
const bindParams = (request, params) => {
|
|
66
82
|
(params ?? []).forEach((value, i) => request.input(`p${i}`, value));
|
|
67
83
|
return request;
|
|
@@ -85,7 +101,7 @@ function createMssqlExecutor(config) {
|
|
|
85
101
|
async run(prepared) {
|
|
86
102
|
await ensureReady();
|
|
87
103
|
const request = bindParams(pool.request(), prepared.params);
|
|
88
|
-
return toResult(await request.query(prepared.sql));
|
|
104
|
+
return toResult(await request.query(withRowCounts(prepared.sql)));
|
|
89
105
|
},
|
|
90
106
|
async transaction(statements) {
|
|
91
107
|
await ensureReady();
|
|
@@ -95,7 +111,7 @@ function createMssqlExecutor(config) {
|
|
|
95
111
|
const results = [];
|
|
96
112
|
for (const s of statements) {
|
|
97
113
|
const request = bindParams(new Request(tx), s.params);
|
|
98
|
-
results.push(toResult(await request.query(s.sql)));
|
|
114
|
+
results.push(toResult(await request.query(withRowCounts(s.sql))));
|
|
99
115
|
}
|
|
100
116
|
await tx.commit();
|
|
101
117
|
return results;
|
|
@@ -124,6 +140,6 @@ function createMssqlExecutor(config) {
|
|
|
124
140
|
};
|
|
125
141
|
}
|
|
126
142
|
//#endregion
|
|
127
|
-
export { createMssqlExecutor, parsePlanXml, toExplainableBatch };
|
|
143
|
+
export { createMssqlExecutor, parsePlanXml, toExplainableBatch, withRowCounts };
|
|
128
144
|
|
|
129
145
|
//# sourceMappingURL=index.mjs.map
|
package/dist/mssql/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/mssql/index.ts"],"sourcesContent":["// `mssql` is CommonJS and Node's ESM loader can't see its named exports — import the default and\n// destructure. (pg/mysql2/@libsql expose named exports fine.)\nimport mssql from 'mssql';\nimport type { config as MssqlDriverConfig, IResult } from 'mssql';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\nconst { ConnectionPool, Transaction, Request } = mssql;\n\n/** Connection settings — any `mssql` config object, or a raw connection string. */\nexport type MssqlConfig = MssqlDriverConfig | { connectionString: string };\n\n/** Read the T-SQL string literal starting at `from` (just past the opening quote), honouring `''`\n * escapes. Returns the unescaped text and the index just past the closing quote. */\nfunction readLiteral(sql: string, from: number): { text: string; end: number } | undefined {\n let out = '';\n for (let i = from; i < sql.length; i++) {\n if (sql[i] !== \"'\") {\n out += sql[i];\n continue;\n }\n if (sql[i + 1] === \"'\") {\n out += \"'\";\n i++;\n continue;\n }\n return { text: out, end: i + 1 };\n }\n return undefined; // unterminated — caller falls back to the raw batch\n}\n\n/**\n * Turn the mssql dialect's output into something SHOWPLAN can actually cost.\n *\n * It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does\n * NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement\n * must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared\n * outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost\n * estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.\n */\nexport function toExplainableBatch(sql: string): string {\n const m = /exec\\s+sp_executesql\\s+N'/i.exec(sql);\n if (!m) return sql;\n const inner = readLiteral(sql, m.index + m[0].length);\n if (!inner) return sql;\n const decl = /^\\s*,\\s*N'/.exec(sql.slice(inner.end));\n const decls = decl ? readLiteral(sql, inner.end + decl[0].length)?.text.trim() : '';\n return decls ? `DECLARE ${decls};\\n${inner.text}` : inner.text;\n}\n\n/** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */\nexport function parsePlanXml(xml: string): ExplainEstimate {\n // A batch holds one <StmtSimple> per statement (the injected DECLARE, SET NOCOUNT ON, the SELECT).\n // Take the most expensive — never blindly the first, which is usually a costless preamble.\n let best: { cost: number; rows?: number } | undefined;\n for (const [tag] of xml.matchAll(/<StmtSimple\\b[^>]*>/g)) {\n const cost = Number(/StatementSubTreeCost=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n if (!Number.isFinite(cost) || (best && cost <= best.cost)) continue;\n const rows = Number(/StatementEstRows=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n best = { cost, rows: Number.isFinite(rows) ? rows : undefined };\n }\n return {\n cost: best?.cost,\n rows: best?.rows,\n fullScan: /PhysicalOp=\"(?:Table Scan|Clustered Index Scan|Index Scan)\"/.test(xml),\n plan: xml.slice(0, 500),\n };\n}\n\nconst toResult = <T>(result: IResult<unknown>): QueryResult<T> => ({\n rows: (result.recordset ?? []) as unknown as T[],\n rowCount: result.recordset ? result.recordset.length : (result.rowsAffected?.[0] ?? 0),\n});\n\n// Bind params (if any) as @p0..@pN. SQLEasy's mssql dialect inlines its values into the\n// sp_executesql batch and passes `params: []`, so nothing binds on that path; a caller passing bound\n// values must reference @p0.. in their SQL (mssql has no positional `?`). No `?`→`@p` rewriting —\n// that scan corrupts a `?` inside a string literal. query() re-wraps its argument in sp_executesql,\n// which is harmless for both a plain statement and a pre-formed batch (verified against real SQL\n// Server: two sp_executesql inserts in a transaction commit both).\ntype BindableRequest = { input(name: string, value: unknown): unknown };\nconst bindParams = <R extends BindableRequest>(request: R, params?: readonly unknown[]): R => {\n (params ?? []).forEach((value, i) => request.input(`p${i}`, value));\n return request;\n};\n\n/**\n * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained\n * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).\n */\nexport function createMssqlExecutor(config: MssqlConfig): DbExecutor {\n const makePool = () =>\n new ConnectionPool('connectionString' in config ? config.connectionString : config);\n let pool = makePool();\n\n // Single-flight connect that RECOVERS. Caching a rejected `pool.connect()` promise would brick the\n // pool forever (a DB restart / blip): every later query awaits the same settled rejection. So reset\n // the gate and rebuild the pool on failure, and the next call retries. (pg/mysql self-heal\n // per-acquire; mssql caches connect, so it alone needs this.)\n let ready: Promise<unknown> | undefined;\n const ensureReady = () =>\n (ready ??= pool.connect().catch((e: unknown) => {\n ready = undefined;\n const dead = pool;\n pool = makePool();\n void dead.close().catch(() => {});\n throw e;\n }));\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n await ensureReady();\n const request = bindParams(pool.request(), prepared.params);\n return toResult<T>(await request.query(prepared.sql));\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n await ensureReady();\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n const results: QueryResult[] = [];\n for (const s of statements) {\n const request = bindParams(new Request(tx), s.params);\n results.push(toResult(await request.query(s.sql)));\n }\n await tx.commit();\n return results;\n } catch (err) {\n await tx.rollback().catch(() => {});\n throw err;\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n await ensureReady();\n // SQL Server has no EXPLAIN. The estimated plan comes from SET SHOWPLAN_XML, which (a) must be\n // the ONLY statement in its batch and (b) is SESSION state — so the SET and the query must run\n // on the SAME connection. A transaction pins one connection for both; the finally block always\n // clears the flag and releases it, so SHOWPLAN never leaks onto a connection serving reads.\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n await new Request(tx).batch('SET SHOWPLAN_XML ON');\n const res = await new Request(tx).batch(toExplainableBatch(prepared.sql));\n const first = res.recordset?.[0] as Row | undefined;\n return parsePlanXml(String(first ? Object.values(first)[0] : ''));\n } finally {\n await new Request(tx).batch('SET SHOWPLAN_XML OFF').catch(() => {});\n await tx.rollback().catch(() => {});\n }\n },\n\n async close(): Promise<void> {\n await (ready?.catch(() => {}) ?? Promise.resolve());\n await pool.close();\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/mssql/index.ts"],"sourcesContent":["// `mssql` is CommonJS and Node's ESM loader can't see its named exports — import the default and\n// destructure. (pg/mysql2/@libsql expose named exports fine.)\nimport mssql from 'mssql';\nimport type { config as MssqlDriverConfig, IResult } from 'mssql';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\nconst { ConnectionPool, Transaction, Request } = mssql;\n\n/** Connection settings — any `mssql` config object, or a raw connection string. */\nexport type MssqlConfig = MssqlDriverConfig | { connectionString: string };\n\n/** Read the T-SQL string literal starting at `from` (just past the opening quote), honouring `''`\n * escapes. Returns the unescaped text and the index just past the closing quote. */\nfunction readLiteral(sql: string, from: number): { text: string; end: number } | undefined {\n let out = '';\n for (let i = from; i < sql.length; i++) {\n if (sql[i] !== \"'\") {\n out += sql[i];\n continue;\n }\n if (sql[i + 1] === \"'\") {\n out += \"'\";\n i++;\n continue;\n }\n return { text: out, end: i + 1 };\n }\n return undefined; // unterminated — caller falls back to the raw batch\n}\n\n/**\n * Turn the mssql dialect's output into something SHOWPLAN can actually cost.\n *\n * It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does\n * NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement\n * must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared\n * outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost\n * estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.\n */\nexport function toExplainableBatch(sql: string): string {\n const m = /exec\\s+sp_executesql\\s+N'/i.exec(sql);\n if (!m) return sql;\n const inner = readLiteral(sql, m.index + m[0].length);\n if (!inner) return sql;\n const decl = /^\\s*,\\s*N'/.exec(sql.slice(inner.end));\n const decls = decl ? readLiteral(sql, inner.end + decl[0].length)?.text.trim() : '';\n return decls ? `DECLARE ${decls};\\n${inner.text}` : inner.text;\n}\n\n/** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */\nexport function parsePlanXml(xml: string): ExplainEstimate {\n // A batch holds one <StmtSimple> per statement (the injected DECLARE, SET NOCOUNT ON, the SELECT).\n // Take the most expensive — never blindly the first, which is usually a costless preamble.\n let best: { cost: number; rows?: number } | undefined;\n for (const [tag] of xml.matchAll(/<StmtSimple\\b[^>]*>/g)) {\n const cost = Number(/StatementSubTreeCost=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n if (!Number.isFinite(cost) || (best && cost <= best.cost)) continue;\n const rows = Number(/StatementEstRows=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n best = { cost, rows: Number.isFinite(rows) ? rows : undefined };\n }\n return {\n cost: best?.cost,\n rows: best?.rows,\n fullScan: /PhysicalOp=\"(?:Table Scan|Clustered Index Scan|Index Scan)\"/.test(xml),\n plan: xml.slice(0, 500),\n };\n}\n\nconst toResult = <T>(result: IResult<unknown>): QueryResult<T> => ({\n rows: (result.recordset ?? []) as unknown as T[],\n rowCount: result.recordset ? result.recordset.length : (result.rowsAffected?.[0] ?? 0),\n});\n\n/**\n * SQLEasy's mssql dialect unconditionally prefixes `SET NOCOUNT ON; ` to every statement it emits.\n * NOCOUNT suppresses the DONE row counts that tedious reads, so `rowsAffected` came back `[]` and\n * every INSERT/UPDATE/DELETE routed through here reported `rowCount: 0` — a write that plainly\n * succeeded looked like it had touched nothing.\n *\n * Rewritten rather than stripped: forcing OFF also corrects a session that already had NOCOUNT ON,\n * whereas removing the prefix would just inherit whatever the connection was left in. The match is\n * anchored at the start of the batch, so it cannot fire on the same text inside a string literal —\n * the reason this file refuses to rewrite `?` placeholders by scanning.\n *\n * The real fix belongs upstream in SQLEasy, which has no reason to emit this at all; the prefix is\n * frozen into its cross-language golden corpus, so it cannot move without the Dart port moving in\n * lockstep.\n */\nexport const withRowCounts = (sql: string): string =>\n sql.replace(/^\\s*SET\\s+NOCOUNT\\s+ON\\s*;/i, 'SET NOCOUNT OFF;');\n\n// Bind params (if any) as @p0..@pN. SQLEasy's mssql dialect inlines its values into the\n// sp_executesql batch and passes `params: []`, so nothing binds on that path; a caller passing bound\n// values must reference @p0.. in their SQL (mssql has no positional `?`). No `?`→`@p` rewriting —\n// that scan corrupts a `?` inside a string literal. query() re-wraps its argument in sp_executesql,\n// which is harmless for both a plain statement and a pre-formed batch (verified against real SQL\n// Server: two sp_executesql inserts in a transaction commit both).\ntype BindableRequest = { input(name: string, value: unknown): unknown };\nconst bindParams = <R extends BindableRequest>(request: R, params?: readonly unknown[]): R => {\n (params ?? []).forEach((value, i) => request.input(`p${i}`, value));\n return request;\n};\n\n/**\n * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained\n * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).\n */\nexport function createMssqlExecutor(config: MssqlConfig): DbExecutor {\n const makePool = () =>\n new ConnectionPool('connectionString' in config ? config.connectionString : config);\n let pool = makePool();\n\n // Single-flight connect that RECOVERS. Caching a rejected `pool.connect()` promise would brick the\n // pool forever (a DB restart / blip): every later query awaits the same settled rejection. So reset\n // the gate and rebuild the pool on failure, and the next call retries. (pg/mysql self-heal\n // per-acquire; mssql caches connect, so it alone needs this.)\n let ready: Promise<unknown> | undefined;\n const ensureReady = () =>\n (ready ??= pool.connect().catch((e: unknown) => {\n ready = undefined;\n const dead = pool;\n pool = makePool();\n void dead.close().catch(() => {});\n throw e;\n }));\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n await ensureReady();\n const request = bindParams(pool.request(), prepared.params);\n return toResult<T>(await request.query(withRowCounts(prepared.sql)));\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n await ensureReady();\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n const results: QueryResult[] = [];\n for (const s of statements) {\n const request = bindParams(new Request(tx), s.params);\n results.push(toResult(await request.query(withRowCounts(s.sql))));\n }\n await tx.commit();\n return results;\n } catch (err) {\n await tx.rollback().catch(() => {});\n throw err;\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n await ensureReady();\n // SQL Server has no EXPLAIN. The estimated plan comes from SET SHOWPLAN_XML, which (a) must be\n // the ONLY statement in its batch and (b) is SESSION state — so the SET and the query must run\n // on the SAME connection. A transaction pins one connection for both; the finally block always\n // clears the flag and releases it, so SHOWPLAN never leaks onto a connection serving reads.\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n await new Request(tx).batch('SET SHOWPLAN_XML ON');\n const res = await new Request(tx).batch(toExplainableBatch(prepared.sql));\n const first = res.recordset?.[0] as Row | undefined;\n return parsePlanXml(String(first ? Object.values(first)[0] : ''));\n } finally {\n await new Request(tx).batch('SET SHOWPLAN_XML OFF').catch(() => {});\n await tx.rollback().catch(() => {});\n }\n },\n\n async close(): Promise<void> {\n await (ready?.catch(() => {}) ?? Promise.resolve());\n await pool.close();\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/mysql/index.cjs
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let mysql2_promise = require("mysql2/promise");
|
|
3
3
|
//#region src/mysql/index.ts
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Every `table` node in the plan, however deeply it is nested.
|
|
6
|
+
*
|
|
7
|
+
* This walks generically instead of naming the wrappers it knows about. Enumerating them is what
|
|
8
|
+
* broke it before: it recursed `nested_loop`/`ordering_operation`/`grouping_operation`/
|
|
9
|
+
* `duplicates_removal` and stopped there, so a UNION — whose `query_block` holds nothing but
|
|
10
|
+
* `union_result.query_specifications[]` — yielded zero tables and reported `fullScan: false` while
|
|
11
|
+
* scanning both branches. Verified against real MySQL 8.4, the wrappers that can hide a table are at
|
|
12
|
+
* least: union_result.query_specifications[], materialized_from_subquery, attached_subqueries[],
|
|
13
|
+
* select_list_subqueries[], optimized_away_subqueries[], having_subqueries[] and
|
|
14
|
+
* order_by_subqueries[]. Naming those would just move the cliff to the next node MySQL adds.
|
|
15
|
+
*
|
|
16
|
+
* Collection stays keyed on the `table` PROPERTY, which is the one thing that reliably marks a table
|
|
17
|
+
* node. That deliberately excludes `union_result` itself: it carries `access_type: "ALL"` for the
|
|
18
|
+
* temporary table it reads back, which is not a base-table scan and must not be reported as one.
|
|
19
|
+
*/
|
|
20
|
+
function tablesOf(node) {
|
|
21
|
+
if (Array.isArray(node)) return node.flatMap(tablesOf);
|
|
22
|
+
if (node === null || typeof node !== "object") return [];
|
|
23
|
+
return Object.entries(node).flatMap(([key, value]) => key === "table" && value !== null && typeof value === "object" ? [value, ...tablesOf(value)] : tablesOf(value));
|
|
14
24
|
}
|
|
15
25
|
/** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */
|
|
16
26
|
function parseMysqlPlan(raw) {
|
package/dist/mysql/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import {\n createPool,\n type Pool,\n type PoolConnection,\n type PoolOptions,\n type ResultSetHeader,\n} from 'mysql2/promise';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `mysql2` `PoolOptions`. */\nexport type MysqlConfig = PoolOptions;\n\n/** `EXPLAIN FORMAT=JSON` shape — only the bits we read. A `table` node is not always a direct child\n * of `query_block`: a JOIN nests it under `nested_loop[]`, ORDER BY under `ordering_operation`, etc. */\ntype MysqlTable = { access_type?: string; rows_examined_per_scan?: number };\ntype
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import {\n createPool,\n type Pool,\n type PoolConnection,\n type PoolOptions,\n type ResultSetHeader,\n} from 'mysql2/promise';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `mysql2` `PoolOptions`. */\nexport type MysqlConfig = PoolOptions;\n\n/** `EXPLAIN FORMAT=JSON` shape — only the bits we read. A `table` node is not always a direct child\n * of `query_block`: a JOIN nests it under `nested_loop[]`, ORDER BY under `ordering_operation`, etc. */\ntype MysqlTable = { access_type?: string; rows_examined_per_scan?: number };\ntype MysqlPlan = { query_block?: { cost_info?: { query_cost?: string } } };\n\n/**\n * Every `table` node in the plan, however deeply it is nested.\n *\n * This walks generically instead of naming the wrappers it knows about. Enumerating them is what\n * broke it before: it recursed `nested_loop`/`ordering_operation`/`grouping_operation`/\n * `duplicates_removal` and stopped there, so a UNION — whose `query_block` holds nothing but\n * `union_result.query_specifications[]` — yielded zero tables and reported `fullScan: false` while\n * scanning both branches. Verified against real MySQL 8.4, the wrappers that can hide a table are at\n * least: union_result.query_specifications[], materialized_from_subquery, attached_subqueries[],\n * select_list_subqueries[], optimized_away_subqueries[], having_subqueries[] and\n * order_by_subqueries[]. Naming those would just move the cliff to the next node MySQL adds.\n *\n * Collection stays keyed on the `table` PROPERTY, which is the one thing that reliably marks a table\n * node. That deliberately excludes `union_result` itself: it carries `access_type: \"ALL\"` for the\n * temporary table it reads back, which is not a base-table scan and must not be reported as one.\n */\nfunction tablesOf(node: unknown): MysqlTable[] {\n if (Array.isArray(node)) return node.flatMap(tablesOf);\n if (node === null || typeof node !== 'object') return [];\n return Object.entries(node).flatMap(([key, value]) =>\n // Recurse into the table too — a materialized derived table hangs its inner plan off it.\n key === 'table' && value !== null && typeof value === 'object'\n ? [value as MysqlTable, ...tablesOf(value)]\n : tablesOf(value),\n );\n}\n\n/** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */\nexport function parseMysqlPlan(raw: string): ExplainEstimate {\n let plan: MysqlPlan = {};\n try {\n plan = JSON.parse(raw || '{}') as MysqlPlan;\n } catch {\n // Unparseable plan — an empty estimate beats throwing.\n }\n const cost = Number(plan.query_block?.cost_info?.query_cost);\n const tables = tablesOf(plan.query_block);\n return {\n cost: Number.isFinite(cost) ? cost : undefined,\n // The driving (first) table's scanned rows.\n rows: tables[0]?.rows_examined_per_scan,\n // `ALL` is MySQL's full-table-scan access type — a scan ANYWHERE in the plan counts.\n fullScan: tables.some((t) => t.access_type === 'ALL'),\n plan: (raw ?? '').slice(0, 500),\n };\n}\n\nconst argsOf = (prepared: PreparedSql): unknown[] => (prepared.params ?? []) as unknown[];\n\n// mysql2's query() resolves to `[rows | ResultSetHeader, fields]`. SELECT → an array of rows; a\n// write → a ResultSetHeader carrying affectedRows.\nconst toResult = <T>(result: unknown): QueryResult<T> => {\n if (Array.isArray(result)) return { rows: result as T[], rowCount: result.length };\n return { rows: [], rowCount: (result as ResultSetHeader).affectedRows ?? 0 };\n};\n\n/** Options for a MySQL executor. */\nexport type MysqlExecutorOptions = {\n /**\n * Per-statement client-side timeout in milliseconds. On expiry `mysql2` errors and destroys the\n * connection, which makes the server kill the running statement. MySQL has no pool-level\n * query-timeout config, so this is the knob for a statement ceiling. Omit for no timeout.\n */\n statementTimeoutMs?: number;\n};\n\n/**\n * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across\n * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.\n */\nexport function createMysqlExecutorFromPool(\n pool: Pool,\n opts: MysqlExecutorOptions = {},\n): DbExecutor {\n const { statementTimeoutMs } = opts;\n // mysql2 has no pool-level query timeout — apply it per statement via the object form, on both the\n // pool (run/explain) and a checked-out connection (transaction).\n const query = (target: Pool | PoolConnection, sql: string, args: unknown[]) =>\n statementTimeoutMs != null\n ? target.query({ sql, timeout: statementTimeoutMs }, args)\n : target.query(sql, args);\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const [result] = await query(pool, prepared.sql, argsOf(prepared));\n return toResult<T>(result);\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // Driver-level transaction on a single pinned connection — beginTransaction/commit/rollback\n // speak the protocol correctly (no `multipleStatements`, no concatenation). Each statement\n // runs on its own; ROLLBACK on any error; release the connection no matter what.\n const conn = await pool.getConnection();\n try {\n await conn.beginTransaction();\n const results: QueryResult[] = [];\n for (const s of statements) {\n const [result] = await query(conn, s.sql, argsOf(s));\n results.push(toResult(result));\n }\n await conn.commit();\n return results;\n } catch (err) {\n await conn.rollback().catch(() => {});\n throw err;\n } finally {\n conn.release();\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // EXPLAIN never executes the statement; FORMAT=JSON is the only form carrying a cost estimate.\n const [rows] = await query(pool, `EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));\n const raw = Array.isArray(rows) ? (rows[0] as { EXPLAIN?: string } | undefined)?.EXPLAIN : '';\n return parseMysqlPlan(raw ?? '');\n },\n\n async close(): Promise<void> {\n await pool.end();\n },\n };\n}\n\n/**\n * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the\n * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement\n * ceiling (MySQL has no pool-level knob for it).\n */\nexport function createMysqlExecutor(\n config: MysqlConfig,\n opts: MysqlExecutorOptions = {},\n): DbExecutor {\n return createMysqlExecutorFromPool(createPool(config), opts);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiCA,SAAS,SAAS,MAA6B;CAC7C,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAK,QAAQ,QAAQ;CACrD,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO,CAAC;CACvD,OAAO,OAAO,QAAQ,IAAI,CAAC,CAAC,SAAS,CAAC,KAAK,WAEzC,QAAQ,WAAW,UAAU,QAAQ,OAAO,UAAU,WAClD,CAAC,OAAqB,GAAG,SAAS,KAAK,CAAC,IACxC,SAAS,KAAK,CACpB;AACF;;AAGA,SAAgB,eAAe,KAA8B;CAC3D,IAAI,OAAkB,CAAC;CACvB,IAAI;EACF,OAAO,KAAK,MAAM,OAAO,IAAI;CAC/B,QAAQ,CAER;CACA,MAAM,OAAO,OAAO,KAAK,aAAa,WAAW,UAAU;CAC3D,MAAM,SAAS,SAAS,KAAK,WAAW;CACxC,OAAO;EACL,MAAM,OAAO,SAAS,IAAI,IAAI,OAAO,KAAA;EAErC,MAAM,OAAO,EAAE,EAAE;EAEjB,UAAU,OAAO,MAAM,MAAM,EAAE,gBAAgB,KAAK;EACpD,OAAO,OAAO,GAAA,CAAI,MAAM,GAAG,GAAG;CAChC;AACF;AAEA,MAAM,UAAU,aAAsC,SAAS,UAAU,CAAC;AAI1E,MAAM,YAAe,WAAoC;CACvD,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO;EAAE,MAAM;EAAe,UAAU,OAAO;CAAO;CACjF,OAAO;EAAE,MAAM,CAAC;EAAG,UAAW,OAA2B,gBAAgB;CAAE;AAC7E;;;;;AAgBA,SAAgB,4BACd,MACA,OAA6B,CAAC,GAClB;CACZ,MAAM,EAAE,uBAAuB;CAG/B,MAAM,SAAS,QAA+B,KAAa,SACzD,sBAAsB,OAClB,OAAO,MAAM;EAAE;EAAK,SAAS;CAAmB,GAAG,IAAI,IACvD,OAAO,MAAM,KAAK,IAAI;CAE5B,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,CAAC,UAAU,MAAM,MAAM,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;GACjE,OAAO,SAAY,MAAM;EAC3B;EAEA,MAAM,YAAY,YAA4D;GAI5E,MAAM,OAAO,MAAM,KAAK,cAAc;GACtC,IAAI;IACF,MAAM,KAAK,iBAAiB;IAC5B,MAAM,UAAyB,CAAC;IAChC,KAAK,MAAM,KAAK,YAAY;KAC1B,MAAM,CAAC,UAAU,MAAM,MAAM,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;KACnD,QAAQ,KAAK,SAAS,MAAM,CAAC;IAC/B;IACA,MAAM,KAAK,OAAO;IAClB,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,KAAK,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM;GACR,UAAU;IACR,KAAK,QAAQ;GACf;EACF;EAEA,MAAM,QAAQ,UAAiD;GAE7D,MAAM,CAAC,QAAQ,MAAM,MAAM,MAAM,uBAAuB,SAAS,OAAO,OAAO,QAAQ,CAAC;GAExF,OAAO,gBADK,MAAM,QAAQ,IAAI,IAAK,KAAK,EAAE,EAAuC,UAAU,OAC9D,EAAE;EACjC;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;;AAOA,SAAgB,oBACd,QACA,OAA6B,CAAC,GAClB;CACZ,OAAO,6BAAA,GAAA,eAAA,WAAA,CAAuC,MAAM,GAAG,IAAI;AAC7D"}
|
package/dist/mysql/index.mjs
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { createPool } from "mysql2/promise";
|
|
2
2
|
//#region src/mysql/index.ts
|
|
3
|
-
/**
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Every `table` node in the plan, however deeply it is nested.
|
|
5
|
+
*
|
|
6
|
+
* This walks generically instead of naming the wrappers it knows about. Enumerating them is what
|
|
7
|
+
* broke it before: it recursed `nested_loop`/`ordering_operation`/`grouping_operation`/
|
|
8
|
+
* `duplicates_removal` and stopped there, so a UNION — whose `query_block` holds nothing but
|
|
9
|
+
* `union_result.query_specifications[]` — yielded zero tables and reported `fullScan: false` while
|
|
10
|
+
* scanning both branches. Verified against real MySQL 8.4, the wrappers that can hide a table are at
|
|
11
|
+
* least: union_result.query_specifications[], materialized_from_subquery, attached_subqueries[],
|
|
12
|
+
* select_list_subqueries[], optimized_away_subqueries[], having_subqueries[] and
|
|
13
|
+
* order_by_subqueries[]. Naming those would just move the cliff to the next node MySQL adds.
|
|
14
|
+
*
|
|
15
|
+
* Collection stays keyed on the `table` PROPERTY, which is the one thing that reliably marks a table
|
|
16
|
+
* node. That deliberately excludes `union_result` itself: it carries `access_type: "ALL"` for the
|
|
17
|
+
* temporary table it reads back, which is not a base-table scan and must not be reported as one.
|
|
18
|
+
*/
|
|
19
|
+
function tablesOf(node) {
|
|
20
|
+
if (Array.isArray(node)) return node.flatMap(tablesOf);
|
|
21
|
+
if (node === null || typeof node !== "object") return [];
|
|
22
|
+
return Object.entries(node).flatMap(([key, value]) => key === "table" && value !== null && typeof value === "object" ? [value, ...tablesOf(value)] : tablesOf(value));
|
|
13
23
|
}
|
|
14
24
|
/** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */
|
|
15
25
|
function parseMysqlPlan(raw) {
|
package/dist/mysql/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import {\n createPool,\n type Pool,\n type PoolConnection,\n type PoolOptions,\n type ResultSetHeader,\n} from 'mysql2/promise';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `mysql2` `PoolOptions`. */\nexport type MysqlConfig = PoolOptions;\n\n/** `EXPLAIN FORMAT=JSON` shape — only the bits we read. A `table` node is not always a direct child\n * of `query_block`: a JOIN nests it under `nested_loop[]`, ORDER BY under `ordering_operation`, etc. */\ntype MysqlTable = { access_type?: string; rows_examined_per_scan?: number };\ntype
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import {\n createPool,\n type Pool,\n type PoolConnection,\n type PoolOptions,\n type ResultSetHeader,\n} from 'mysql2/promise';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `mysql2` `PoolOptions`. */\nexport type MysqlConfig = PoolOptions;\n\n/** `EXPLAIN FORMAT=JSON` shape — only the bits we read. A `table` node is not always a direct child\n * of `query_block`: a JOIN nests it under `nested_loop[]`, ORDER BY under `ordering_operation`, etc. */\ntype MysqlTable = { access_type?: string; rows_examined_per_scan?: number };\ntype MysqlPlan = { query_block?: { cost_info?: { query_cost?: string } } };\n\n/**\n * Every `table` node in the plan, however deeply it is nested.\n *\n * This walks generically instead of naming the wrappers it knows about. Enumerating them is what\n * broke it before: it recursed `nested_loop`/`ordering_operation`/`grouping_operation`/\n * `duplicates_removal` and stopped there, so a UNION — whose `query_block` holds nothing but\n * `union_result.query_specifications[]` — yielded zero tables and reported `fullScan: false` while\n * scanning both branches. Verified against real MySQL 8.4, the wrappers that can hide a table are at\n * least: union_result.query_specifications[], materialized_from_subquery, attached_subqueries[],\n * select_list_subqueries[], optimized_away_subqueries[], having_subqueries[] and\n * order_by_subqueries[]. Naming those would just move the cliff to the next node MySQL adds.\n *\n * Collection stays keyed on the `table` PROPERTY, which is the one thing that reliably marks a table\n * node. That deliberately excludes `union_result` itself: it carries `access_type: \"ALL\"` for the\n * temporary table it reads back, which is not a base-table scan and must not be reported as one.\n */\nfunction tablesOf(node: unknown): MysqlTable[] {\n if (Array.isArray(node)) return node.flatMap(tablesOf);\n if (node === null || typeof node !== 'object') return [];\n return Object.entries(node).flatMap(([key, value]) =>\n // Recurse into the table too — a materialized derived table hangs its inner plan off it.\n key === 'table' && value !== null && typeof value === 'object'\n ? [value as MysqlTable, ...tablesOf(value)]\n : tablesOf(value),\n );\n}\n\n/** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */\nexport function parseMysqlPlan(raw: string): ExplainEstimate {\n let plan: MysqlPlan = {};\n try {\n plan = JSON.parse(raw || '{}') as MysqlPlan;\n } catch {\n // Unparseable plan — an empty estimate beats throwing.\n }\n const cost = Number(plan.query_block?.cost_info?.query_cost);\n const tables = tablesOf(plan.query_block);\n return {\n cost: Number.isFinite(cost) ? cost : undefined,\n // The driving (first) table's scanned rows.\n rows: tables[0]?.rows_examined_per_scan,\n // `ALL` is MySQL's full-table-scan access type — a scan ANYWHERE in the plan counts.\n fullScan: tables.some((t) => t.access_type === 'ALL'),\n plan: (raw ?? '').slice(0, 500),\n };\n}\n\nconst argsOf = (prepared: PreparedSql): unknown[] => (prepared.params ?? []) as unknown[];\n\n// mysql2's query() resolves to `[rows | ResultSetHeader, fields]`. SELECT → an array of rows; a\n// write → a ResultSetHeader carrying affectedRows.\nconst toResult = <T>(result: unknown): QueryResult<T> => {\n if (Array.isArray(result)) return { rows: result as T[], rowCount: result.length };\n return { rows: [], rowCount: (result as ResultSetHeader).affectedRows ?? 0 };\n};\n\n/** Options for a MySQL executor. */\nexport type MysqlExecutorOptions = {\n /**\n * Per-statement client-side timeout in milliseconds. On expiry `mysql2` errors and destroys the\n * connection, which makes the server kill the running statement. MySQL has no pool-level\n * query-timeout config, so this is the knob for a statement ceiling. Omit for no timeout.\n */\n statementTimeoutMs?: number;\n};\n\n/**\n * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across\n * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.\n */\nexport function createMysqlExecutorFromPool(\n pool: Pool,\n opts: MysqlExecutorOptions = {},\n): DbExecutor {\n const { statementTimeoutMs } = opts;\n // mysql2 has no pool-level query timeout — apply it per statement via the object form, on both the\n // pool (run/explain) and a checked-out connection (transaction).\n const query = (target: Pool | PoolConnection, sql: string, args: unknown[]) =>\n statementTimeoutMs != null\n ? target.query({ sql, timeout: statementTimeoutMs }, args)\n : target.query(sql, args);\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const [result] = await query(pool, prepared.sql, argsOf(prepared));\n return toResult<T>(result);\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // Driver-level transaction on a single pinned connection — beginTransaction/commit/rollback\n // speak the protocol correctly (no `multipleStatements`, no concatenation). Each statement\n // runs on its own; ROLLBACK on any error; release the connection no matter what.\n const conn = await pool.getConnection();\n try {\n await conn.beginTransaction();\n const results: QueryResult[] = [];\n for (const s of statements) {\n const [result] = await query(conn, s.sql, argsOf(s));\n results.push(toResult(result));\n }\n await conn.commit();\n return results;\n } catch (err) {\n await conn.rollback().catch(() => {});\n throw err;\n } finally {\n conn.release();\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // EXPLAIN never executes the statement; FORMAT=JSON is the only form carrying a cost estimate.\n const [rows] = await query(pool, `EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));\n const raw = Array.isArray(rows) ? (rows[0] as { EXPLAIN?: string } | undefined)?.EXPLAIN : '';\n return parseMysqlPlan(raw ?? '');\n },\n\n async close(): Promise<void> {\n await pool.end();\n },\n };\n}\n\n/**\n * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the\n * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement\n * ceiling (MySQL has no pool-level knob for it).\n */\nexport function createMysqlExecutor(\n config: MysqlConfig,\n opts: MysqlExecutorOptions = {},\n): DbExecutor {\n return createMysqlExecutorFromPool(createPool(config), opts);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiCA,SAAS,SAAS,MAA6B;CAC7C,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAK,QAAQ,QAAQ;CACrD,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO,CAAC;CACvD,OAAO,OAAO,QAAQ,IAAI,CAAC,CAAC,SAAS,CAAC,KAAK,WAEzC,QAAQ,WAAW,UAAU,QAAQ,OAAO,UAAU,WAClD,CAAC,OAAqB,GAAG,SAAS,KAAK,CAAC,IACxC,SAAS,KAAK,CACpB;AACF;;AAGA,SAAgB,eAAe,KAA8B;CAC3D,IAAI,OAAkB,CAAC;CACvB,IAAI;EACF,OAAO,KAAK,MAAM,OAAO,IAAI;CAC/B,QAAQ,CAER;CACA,MAAM,OAAO,OAAO,KAAK,aAAa,WAAW,UAAU;CAC3D,MAAM,SAAS,SAAS,KAAK,WAAW;CACxC,OAAO;EACL,MAAM,OAAO,SAAS,IAAI,IAAI,OAAO,KAAA;EAErC,MAAM,OAAO,EAAE,EAAE;EAEjB,UAAU,OAAO,MAAM,MAAM,EAAE,gBAAgB,KAAK;EACpD,OAAO,OAAO,GAAA,CAAI,MAAM,GAAG,GAAG;CAChC;AACF;AAEA,MAAM,UAAU,aAAsC,SAAS,UAAU,CAAC;AAI1E,MAAM,YAAe,WAAoC;CACvD,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO;EAAE,MAAM;EAAe,UAAU,OAAO;CAAO;CACjF,OAAO;EAAE,MAAM,CAAC;EAAG,UAAW,OAA2B,gBAAgB;CAAE;AAC7E;;;;;AAgBA,SAAgB,4BACd,MACA,OAA6B,CAAC,GAClB;CACZ,MAAM,EAAE,uBAAuB;CAG/B,MAAM,SAAS,QAA+B,KAAa,SACzD,sBAAsB,OAClB,OAAO,MAAM;EAAE;EAAK,SAAS;CAAmB,GAAG,IAAI,IACvD,OAAO,MAAM,KAAK,IAAI;CAE5B,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,CAAC,UAAU,MAAM,MAAM,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;GACjE,OAAO,SAAY,MAAM;EAC3B;EAEA,MAAM,YAAY,YAA4D;GAI5E,MAAM,OAAO,MAAM,KAAK,cAAc;GACtC,IAAI;IACF,MAAM,KAAK,iBAAiB;IAC5B,MAAM,UAAyB,CAAC;IAChC,KAAK,MAAM,KAAK,YAAY;KAC1B,MAAM,CAAC,UAAU,MAAM,MAAM,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;KACnD,QAAQ,KAAK,SAAS,MAAM,CAAC;IAC/B;IACA,MAAM,KAAK,OAAO;IAClB,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,KAAK,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM;GACR,UAAU;IACR,KAAK,QAAQ;GACf;EACF;EAEA,MAAM,QAAQ,UAAiD;GAE7D,MAAM,CAAC,QAAQ,MAAM,MAAM,MAAM,uBAAuB,SAAS,OAAO,OAAO,QAAQ,CAAC;GAExF,OAAO,gBADK,MAAM,QAAQ,IAAI,IAAK,KAAK,EAAE,EAAuC,UAAU,OAC9D,EAAE;EACjC;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;;AAOA,SAAgB,oBACd,QACA,OAA6B,CAAC,GAClB;CACZ,OAAO,4BAA4B,WAAW,MAAM,GAAG,IAAI;AAC7D"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deebeetech/sqleasy-engine",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "A thin, opt-in executor for @deebeetech/sqleasy: runs its prepared SQL and transactions on Postgres, MySQL, SQL Server, and SQLite — loading only the driver you import.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sql",
|
|
@@ -28,38 +28,68 @@
|
|
|
28
28
|
"url": "https://github.com/deebee-tech/sqleasy-engine"
|
|
29
29
|
},
|
|
30
30
|
"type": "module",
|
|
31
|
-
"main": "dist/index.
|
|
31
|
+
"main": "dist/index.cjs",
|
|
32
32
|
"types": "dist/index.d.mts",
|
|
33
33
|
"exports": {
|
|
34
34
|
".": {
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
"import": {
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"default": "./dist/index.mjs"
|
|
38
|
+
},
|
|
39
|
+
"require": {
|
|
40
|
+
"types": "./dist/index.d.cts",
|
|
41
|
+
"default": "./dist/index.cjs"
|
|
42
|
+
}
|
|
38
43
|
},
|
|
39
44
|
"./sqlite": {
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
"import": {
|
|
46
|
+
"types": "./dist/sqlite/index.d.mts",
|
|
47
|
+
"default": "./dist/sqlite/index.mjs"
|
|
48
|
+
},
|
|
49
|
+
"require": {
|
|
50
|
+
"types": "./dist/sqlite/index.d.cts",
|
|
51
|
+
"default": "./dist/sqlite/index.cjs"
|
|
52
|
+
}
|
|
43
53
|
},
|
|
44
54
|
"./postgres": {
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
55
|
+
"import": {
|
|
56
|
+
"types": "./dist/postgres/index.d.mts",
|
|
57
|
+
"default": "./dist/postgres/index.mjs"
|
|
58
|
+
},
|
|
59
|
+
"require": {
|
|
60
|
+
"types": "./dist/postgres/index.d.cts",
|
|
61
|
+
"default": "./dist/postgres/index.cjs"
|
|
62
|
+
}
|
|
48
63
|
},
|
|
49
64
|
"./mysql": {
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
"import": {
|
|
66
|
+
"types": "./dist/mysql/index.d.mts",
|
|
67
|
+
"default": "./dist/mysql/index.mjs"
|
|
68
|
+
},
|
|
69
|
+
"require": {
|
|
70
|
+
"types": "./dist/mysql/index.d.cts",
|
|
71
|
+
"default": "./dist/mysql/index.cjs"
|
|
72
|
+
}
|
|
53
73
|
},
|
|
54
74
|
"./mssql": {
|
|
55
|
-
"
|
|
56
|
-
|
|
57
|
-
|
|
75
|
+
"import": {
|
|
76
|
+
"types": "./dist/mssql/index.d.mts",
|
|
77
|
+
"default": "./dist/mssql/index.mjs"
|
|
78
|
+
},
|
|
79
|
+
"require": {
|
|
80
|
+
"types": "./dist/mssql/index.d.cts",
|
|
81
|
+
"default": "./dist/mssql/index.cjs"
|
|
82
|
+
}
|
|
58
83
|
},
|
|
59
84
|
"./introspection": {
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
"import": {
|
|
86
|
+
"types": "./dist/introspection/index.d.mts",
|
|
87
|
+
"default": "./dist/introspection/index.mjs"
|
|
88
|
+
},
|
|
89
|
+
"require": {
|
|
90
|
+
"types": "./dist/introspection/index.d.cts",
|
|
91
|
+
"default": "./dist/introspection/index.cjs"
|
|
92
|
+
}
|
|
63
93
|
}
|
|
64
94
|
},
|
|
65
95
|
"files": [
|