@deebeetech/sqleasy-engine 1.0.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.
@@ -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 = 'r' AND k.attnum > 0
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
- WHERE n.nspname = $1 AND c.relkind = 'r'`,
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":";;;AA0BA,SAAgB,YACd,QACA,SACA,KACA,eAAiC,CAAC,GAClC,YAAgE,CAAC,GACrD;CACZ,MAAM,yBAAS,IAAI,IAA4B;CAC/C,KAAK,MAAM,KAAK,SAAS;EACvB,MAAM,MAAM,GAAG,EAAE,OAAO,GAAG,EAAE;EAC7B,MAAM,OAAO,OAAO,IAAI,GAAG,KAAK,CAAC;EACjC,KAAK,KAAK;GACR,MAAM,EAAE;GACR,UAAU,EAAE;GACZ,UAAU,EAAE;GACZ,cAAc,EAAE;GAChB,cAAc,EAAE;EAClB,CAAC;EACD,OAAO,IAAI,KAAK,IAAI;CACtB;CAEA,MAAM,wBAAQ,IAAI,IAAgC;CAClD,KAAK,MAAM,MAAM,KAAK;EACpB,MAAM,MAAM,GAAG,GAAG,OAAO,GAAG,GAAG;EAC/B,MAAM,OAAO,MAAM,IAAI,GAAG,KAAK,CAAC;EAChC,KAAK,KAAK;GACR,YAAY,GAAG;GACf,iBAAiB,GAAG;GACpB,kBAAkB,GAAG;GACrB,kBAAkB,GAAG;EACvB,CAAC;EACD,MAAM,IAAI,KAAK,IAAI;CACrB;CAGA,MAAM,yBAAS,IAAI,IAGjB;CACF,KAAK,MAAM,KAAK,cAAc;EAC5B,MAAM,MAAM,GAAG,EAAE,OAAO,GAAG,EAAE;EAC7B,MAAM,SAAS,OAAO,IAAI,GAAG,qBAAK,IAAI,IAAI;EAC1C,MAAM,MAAM,OAAO,IAAI,EAAE,SAAS,KAAK;GAAE,QAAQ,EAAE;GAAQ,MAAM,CAAC;EAAE;EACpE,IAAI,KAAK,KAAK;GAAE,MAAM,EAAE;GAAY,SAAS,EAAE;EAAQ,CAAC;EACxD,OAAO,IAAI,EAAE,WAAW,GAAG;EAC3B,OAAO,IAAI,KAAK,MAAM;CACxB;CACA,MAAM,cAAc,QAClB,CAAC,GAAI,OAAO,IAAI,GAAG,CAAC,EAAE,QAAQ,KAAK,CAAC,CAAE,CAAC,CAAC,KAAK,CAAC,MAAM,QAAQ;EAC1D;EACA,QAAQ,EAAE;EACV,SAAS,EAAE,KAAK,MAAM,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC,KAAK,MAAM,EAAE,IAAI;CACzE,EAAE;CAEJ,MAAM,cAAc,IAAI,IAAI,UAAU,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;CAerF,OAAO,EAAE,QAbqB,OAAO,KAAK,MAAM;EAC9C,MAAM,MAAM,GAAG,EAAE,OAAO,GAAG,EAAE;EAC7B,OAAO;GACL,QAAQ,EAAE;GACV,MAAM,EAAE;GACR,MAAM,EAAE,SAAS,SAAS;GAC1B,SAAS,OAAO,IAAI,GAAG,KAAK,CAAC;GAC7B,aAAa,MAAM,IAAI,GAAG,KAAK,CAAC;GAChC,SAAS,WAAW,GAAG;GACvB,gBAAgB,YAAY,IAAI,GAAG;EACrC;CACF,CAEsB,EAAE;AAC1B;;;;;AC1FA,eAAsB,gBAAgB,UAAsB,QAAsC;CAChG,MAAM,SAAS,UAAU;CAEzB,MAAM,SAAS,MAAM,SAAS,IAAgD;EAC5E,KAAK;;;;EAIL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,UAAU,MAAM,SAAS,IAM5B;EACD,KAAK;;;;;EAKL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,MAAM,MAAM,SAAS,IAAiD;EAC1E,KAAK;;;;;EAKL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,MAAM,MAAM,SAAS,IAMxB;EACD,KAAK;;;;;;;;;;EAUL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAID,MAAM,UAAU,MAAM,SAAS,IAM5B;EACD,KAAK;;;;;;;;;EASL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,YAAY,MAAM,SAAS,IAAuC;EACtE,KAAK;;;;;;EAML,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,QAAQ,IAAI,IAAI,IAAI,KAAK,KAAK,MAAM,GAAG,EAAE,WAAW,GAAG,EAAE,aAAa,CAAC;CAE7E,OAAO,YACL,OAAO,KAAK,KAAK,OAAO;EACtB,QAAQ;EACR,MAAM,EAAE;EACR,QAAQ,EAAE,eAAe;CAC3B,EAAE,GACF,QAAQ,KAAK,KAAK,OAAO;EACvB,QAAQ;EACR,OAAO,EAAE;EACT,MAAM,EAAE;EACR,UAAU,EAAE;EACZ,UAAU,EAAE,gBAAgB;EAC5B,cAAc,MAAM,IAAI,GAAG,EAAE,WAAW,GAAG,EAAE,aAAa;EAC1D,cAAc,EAAE,kBAAkB,KAAA;CACpC,EAAE,GACF,IAAI,KAAK,KAAK,QAAQ;EACpB,QAAQ;EACR,OAAO,GAAG;EACV,YAAY,GAAG;EACf,iBAAiB,GAAG;EACpB,kBAAkB,GAAG;EACrB,kBAAkB,GAAG;CACvB,EAAE,GACF,QAAQ,KAAK,KAAK,OAAO;EACvB,QAAQ;EACR,OAAO,EAAE;EACT,WAAW,EAAE;EACb,QAAQ,QAAQ,EAAE,SAAS;EAC3B,YAAY,EAAE;EACd,SAAS,OAAO,EAAE,OAAO;CAC3B,EAAE,GACF,UAAU,KAAK,KAAK,OAAO;EAAE,QAAQ;EAAQ,OAAO,EAAE;EAAY,OAAO,OAAO,EAAE,CAAC;CAAE,EAAE,CACzF;AACF;;;;;ACzHA,eAAsB,gBAAgB,UAAsB,QAAsC;CAChG,IAAI,SAAS;CACb,IAAI,CAAC,QAEH,UAAS,MADO,SAAS,IAAoB,EAAE,KAAK,0BAA0B,CAAC,EAAA,CACpE,KAAK,EAAE,EAAE,MAAM;CAK5B,MAAM,SAAS,MAAM,SAAS,IAAgD;EAC5E,KAAK;;;;EAIL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,UAAU,MAAM,SAAS,IAO5B;EACD,KAAK;;;;;EAKL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,MAAM,MAAM,SAAS,IAMxB;EACD,KAAK;;;;;;EAML,QAAQ,CAAC,MAAM;CACjB,CAAC;CAGD,MAAM,UAAU,MAAM,SAAS,IAM5B;EACD,KAAK;;;;;EAKL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,YAAY,MAAM,SAAS,IAA8C;EAC7E,KAAK;;;EAGL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,OAAO,YACL,OAAO,KAAK,KAAK,OAAO;EACtB,QAAQ;EACR,MAAM,EAAE;EACR,QAAQ,EAAE,eAAe;CAC3B,EAAE,GACF,QAAQ,KAAK,KAAK,OAAO;EACvB,QAAQ;EACR,OAAO,EAAE;EACT,MAAM,EAAE;EACR,UAAU,EAAE;EACZ,UAAU,EAAE,gBAAgB;EAC5B,cAAc,EAAE,eAAe;EAC/B,cAAc,EAAE,kBAAkB,KAAA;CACpC,EAAE,GACF,IAAI,KAAK,KAAK,QAAQ;EACpB,QAAQ;EACR,OAAO,GAAG;EACV,YAAY,GAAG;EACf,iBAAiB,GAAG;EACpB,kBAAkB,GAAG;EACrB,kBAAkB,GAAG;CACvB,EAAE,GACF,QAAQ,KACL,QAAQ,MAAM,EAAE,eAAe,IAAI,CAAC,CACpC,KAAK,OAAO;EACX,QAAQ;EACR,OAAO,EAAE;EACT,WAAW,EAAE;EACb,QAAQ,EAAE,eAAe;EACzB,YAAY,EAAE;EACd,SAAS,OAAO,EAAE,GAAG;CACvB,EAAE,GACJ,UAAU,KAAK,KAAK,OAAO;EAAE,QAAQ;EAAQ,OAAO,EAAE;EAAY,OAAO,OAAO,EAAE,KAAK,CAAC;CAAE,EAAE,CAC9F;AACF;;;;;AC3GA,eAAsB,mBACpB,UACA,QACqB;CACrB,MAAM,SAAS,UAAU;CAEzB,MAAM,SAAS,MAAM,SAAS,IAAgD;EAC5E,KAAK;;;;EAIL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,UAAU,MAAM,SAAS,IAM5B;EACD,KAAK;;;;EAIL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,MAAM,MAAM,SAAS,IAAiD;EAC1E,KAAK;;;;;EAKL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,MAAM,MAAM,SAAS,IAMxB;EACD,KAAK;;;;;;;;;;EAUL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAQD,MAAM,UAAU,MAAM,SAAS,IAM5B;EACD,KAAK;;;;;;;;;;;EAWL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,YAAY,MAAM,SAAS,IAAuC;EACtE,KAAK;;;;;EAKL,QAAQ,CAAC,MAAM;CACjB,CAAC;CAED,MAAM,QAAQ,IAAI,IAAI,IAAI,KAAK,KAAK,MAAM,GAAG,EAAE,WAAW,GAAG,EAAE,aAAa,CAAC;CAE7E,OAAO,YACL,OAAO,KAAK,KAAK,OAAO;EACtB,QAAQ;EACR,MAAM,EAAE;EACR,QAAQ,EAAE,eAAe;CAC3B,EAAE,GACF,QAAQ,KAAK,KAAK,OAAO;EACvB,QAAQ;EACR,OAAO,EAAE;EACT,MAAM,EAAE;EACR,UAAU,EAAE;EACZ,UAAU,EAAE,gBAAgB;EAC5B,cAAc,MAAM,IAAI,GAAG,EAAE,WAAW,GAAG,EAAE,aAAa;EAC1D,cAAc,EAAE,kBAAkB,KAAA;CACpC,EAAE,GACF,IAAI,KAAK,KAAK,QAAQ;EACpB,QAAQ;EACR,OAAO,GAAG;EACV,YAAY,GAAG;EACf,iBAAiB,GAAG;EACpB,kBAAkB,GAAG;EACrB,kBAAkB,GAAG;CACvB,EAAE,GACF,QAAQ,KAAK,KAAK,OAAO;EACvB,QAAQ;EACR,OAAO,EAAE;EACT,WAAW,EAAE;EACb,QAAQ,EAAE;EACV,YAAY,EAAE;EACd,SAAS,OAAO,EAAE,OAAO;CAC3B,EAAE,GACF,UAAU,KACP,KAAK,OAAO;EAAE,QAAQ;EAAQ,OAAO,EAAE;EAAY,OAAO,OAAO,EAAE,CAAC;CAAE,EAAE,CAAC,CACzE,QAAQ,MAAM,EAAE,SAAS,CAAC,CAC/B;AACF;;;;;AClIA,eAAsB,iBAAiB,UAA2C;CAChF,MAAM,SAAS,MAAM,SAAS,IAAoC,EAChE,KAAK;;4BAGP,CAAC;CAED,MAAM,UAAgE,CAAC;CACvE,MAAM,MAAgE,CAAC;CACvE,MAAM,eAAiC,CAAC;CAExC,KAAK,MAAM,KAAK,OAAO,MAAM;EAG3B,MAAM,OAAO,MAAM,SAAS,IAMzB;GACD,KAAK;GACL,QAAQ,CAAC,EAAE,IAAI;EACjB,CAAC;EAED,KAAK,MAAM,KAAK,KAAK,MACnB,QAAQ,KAAK;GACX,QAAQ;GACR,OAAO,EAAE;GACT,MAAM,EAAE;GACR,UAAU,EAAE,QAAQ;GACpB,UAAU,EAAE,YAAY,KAAK,EAAE,OAAO;GACtC,cAAc,EAAE,KAAK;GACrB,cAAc,EAAE,cAAc,KAAA;EAChC,CAAC;EAGH,MAAM,SAAS,MAAM,SAAS,IAAiD;GAC7E,KAAK;GACL,QAAQ,CAAC,EAAE,IAAI;EACjB,CAAC;EACD,KAAK,MAAM,MAAM,OAAO,MACtB,IAAI,KAAK;GACP,QAAQ;GACR,OAAO,EAAE;GACT,YAAY,GAAG;GACf,iBAAiB,GAAG;GACpB,kBAAkB,GAAG;EACvB,CAAC;EAIH,MAAM,UAAU,MAAM,SAAS,IAAsC;GACnE,KAAK;GACL,QAAQ,CAAC,EAAE,IAAI;EACjB,CAAC;EACD,KAAK,MAAM,OAAO,QAAQ,MAAM;GAC9B,MAAM,UAAU,MAAM,SAAS,IAA4C;IACzE,KAAK;IACL,QAAQ,CAAC,IAAI,IAAI;GACnB,CAAC;GACD,KAAK,MAAM,MAAM,QAAQ,MAAM;IAC7B,IAAI,GAAG,QAAQ,MAAM;IACrB,aAAa,KAAK;KAChB,QAAQ;KACR,OAAO,EAAE;KACT,WAAW,IAAI;KACf,QAAQ,IAAI,WAAW;KACvB,YAAY,GAAG;KACf,SAAS,GAAG;IACd,CAAC;GACH;EACF;CACF;CAEA,OAAO,YACL,OAAO,KAAK,KAAK,OAAO;EAAE,QAAQ;EAAQ,MAAM,EAAE;EAAM,QAAQ,EAAE,SAAS;CAAO,EAAE,GACpF,SACA,KACA,YACF;AACF;;;;;;;;;AClDA,SAAgB,iBACd,UACA,SACA,QACqB;CACrB,QAAQ,SAAR;EACE,KAAK,YACH,OAAO,mBAAmB,UAAU,MAAM;EAC5C,KAAK,SACH,OAAO,gBAAgB,UAAU,MAAM;EACzC,KAAK,SACH,OAAO,gBAAgB,UAAU,MAAM;EACzC,KAAK,UACH,OAAO,iBAAiB,QAAQ;CACpC;AACF"}
@@ -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 = 'r' AND k.attnum > 0
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
- WHERE n.nspname = $1 AND c.relkind = 'r'`,
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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"}
@@ -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
@@ -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\n// bound values must reference @p0.. in their SQL (mssql has no positional `?`). No `?`→`@p`\n// rewriting — that scan corrupts a `?` inside a string literal.\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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"}
@@ -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
@@ -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
@@ -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
@@ -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\n// bound values must reference @p0.. in their SQL (mssql has no positional `?`). No `?`→`@p`\n// rewriting — that scan corrupts a `?` inside a string literal.\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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"}
@@ -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
- /** Every `table` node in the plan, however deeply the operation wrappers nest it. */
5
- function tablesOf(block) {
6
- if (!block) return [];
7
- return [
8
- ...block.table ? [block.table] : [],
9
- ...(block.nested_loop ?? []).flatMap(tablesOf),
10
- ...tablesOf(block.ordering_operation),
11
- ...tablesOf(block.grouping_operation),
12
- ...tablesOf(block.duplicates_removal)
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) {
@@ -42,10 +52,15 @@ const toResult = (result) => {
42
52
  * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
43
53
  * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
44
54
  */
45
- function createMysqlExecutorFromPool(pool) {
55
+ function createMysqlExecutorFromPool(pool, opts = {}) {
56
+ const { statementTimeoutMs } = opts;
57
+ const query = (target, sql, args) => statementTimeoutMs != null ? target.query({
58
+ sql,
59
+ timeout: statementTimeoutMs
60
+ }, args) : target.query(sql, args);
46
61
  return {
47
62
  async run(prepared) {
48
- const [result] = await pool.query(prepared.sql, argsOf(prepared));
63
+ const [result] = await query(pool, prepared.sql, argsOf(prepared));
49
64
  return toResult(result);
50
65
  },
51
66
  async transaction(statements) {
@@ -54,7 +69,7 @@ function createMysqlExecutorFromPool(pool) {
54
69
  await conn.beginTransaction();
55
70
  const results = [];
56
71
  for (const s of statements) {
57
- const [result] = await conn.query(s.sql, argsOf(s));
72
+ const [result] = await query(conn, s.sql, argsOf(s));
58
73
  results.push(toResult(result));
59
74
  }
60
75
  await conn.commit();
@@ -67,7 +82,7 @@ function createMysqlExecutorFromPool(pool) {
67
82
  }
68
83
  },
69
84
  async explain(prepared) {
70
- const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));
85
+ const [rows] = await query(pool, `EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));
71
86
  return parseMysqlPlan((Array.isArray(rows) ? rows[0]?.EXPLAIN : "") ?? "");
72
87
  },
73
88
  async close() {
@@ -77,10 +92,11 @@ function createMysqlExecutorFromPool(pool) {
77
92
  }
78
93
  /**
79
94
  * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
80
- * `{ sql, params }` a `MysqlQuery` builder emits.
95
+ * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement
96
+ * ceiling (MySQL has no pool-level knob for it).
81
97
  */
82
- function createMysqlExecutor(config) {
83
- return createMysqlExecutorFromPool((0, mysql2_promise.createPool)(config));
98
+ function createMysqlExecutor(config, opts = {}) {
99
+ return createMysqlExecutorFromPool((0, mysql2_promise.createPool)(config), opts);
84
100
  }
85
101
  //#endregion
86
102
  exports.createMysqlExecutor = createMysqlExecutor;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import { createPool, type Pool, type PoolOptions, type ResultSetHeader } 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 MysqlBlock = {\n cost_info?: { query_cost?: string };\n table?: MysqlTable;\n nested_loop?: MysqlBlock[];\n ordering_operation?: MysqlBlock;\n grouping_operation?: MysqlBlock;\n duplicates_removal?: MysqlBlock;\n};\ntype MysqlPlan = { query_block?: MysqlBlock };\n\n/** Every `table` node in the plan, however deeply the operation wrappers nest it. */\nfunction tablesOf(block: MysqlBlock | undefined): MysqlTable[] {\n if (!block) return [];\n return [\n ...(block.table ? [block.table] : []),\n ...(block.nested_loop ?? []).flatMap(tablesOf),\n ...tablesOf(block.ordering_operation),\n ...tablesOf(block.grouping_operation),\n ...tablesOf(block.duplicates_removal),\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/**\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(pool: Pool): DbExecutor {\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const [result] = await pool.query(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 conn.query(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 pool.query(`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.\n */\nexport function createMysqlExecutor(config: MysqlConfig): DbExecutor {\n return createMysqlExecutorFromPool(createPool(config));\n}\n"],"mappings":";;;;AAoBA,SAAS,SAAS,OAA6C;CAC7D,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,OAAO;EACL,GAAI,MAAM,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC;EACnC,IAAI,MAAM,eAAe,CAAC,EAAA,CAAG,QAAQ,QAAQ;EAC7C,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;CACtC;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;;;;;AAMA,SAAgB,4BAA4B,MAAwB;CAClE,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;GAChE,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,KAAK,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;KAClD,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,KAAK,MAAM,uBAAuB,SAAS,OAAO,OAAO,QAAQ,CAAC;GAEvF,OAAO,gBADK,MAAM,QAAQ,IAAI,IAAK,KAAK,EAAE,EAAuC,UAAU,OAC9D,EAAE;EACjC;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;AAMA,SAAgB,oBAAoB,QAAiC;CACnE,OAAO,6BAAA,GAAA,eAAA,WAAA,CAAuC,MAAM,CAAC;AACvD"}
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"}
@@ -5,16 +5,26 @@ import { Pool, PoolOptions } from "mysql2/promise";
5
5
  type MysqlConfig = PoolOptions;
6
6
  /** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */
7
7
  declare function parseMysqlPlan(raw: string): ExplainEstimate;
8
+ /** Options for a MySQL executor. */
9
+ type MysqlExecutorOptions = {
10
+ /**
11
+ * Per-statement client-side timeout in milliseconds. On expiry `mysql2` errors and destroys the
12
+ * connection, which makes the server kill the running statement. MySQL has no pool-level
13
+ * query-timeout config, so this is the knob for a statement ceiling. Omit for no timeout.
14
+ */
15
+ statementTimeoutMs?: number;
16
+ };
8
17
  /**
9
18
  * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
10
19
  * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
11
20
  */
12
- declare function createMysqlExecutorFromPool(pool: Pool): DbExecutor;
21
+ declare function createMysqlExecutorFromPool(pool: Pool, opts?: MysqlExecutorOptions): DbExecutor;
13
22
  /**
14
23
  * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
15
- * `{ sql, params }` a `MysqlQuery` builder emits.
24
+ * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement
25
+ * ceiling (MySQL has no pool-level knob for it).
16
26
  */
17
- declare function createMysqlExecutor(config: MysqlConfig): DbExecutor;
27
+ declare function createMysqlExecutor(config: MysqlConfig, opts?: MysqlExecutorOptions): DbExecutor;
18
28
  //#endregion
19
- export { MysqlConfig, createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
29
+ export { MysqlConfig, MysqlExecutorOptions, createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
20
30
  //# sourceMappingURL=index.d.cts.map
@@ -5,16 +5,26 @@ import { Pool, PoolOptions } from "mysql2/promise";
5
5
  type MysqlConfig = PoolOptions;
6
6
  /** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */
7
7
  declare function parseMysqlPlan(raw: string): ExplainEstimate;
8
+ /** Options for a MySQL executor. */
9
+ type MysqlExecutorOptions = {
10
+ /**
11
+ * Per-statement client-side timeout in milliseconds. On expiry `mysql2` errors and destroys the
12
+ * connection, which makes the server kill the running statement. MySQL has no pool-level
13
+ * query-timeout config, so this is the knob for a statement ceiling. Omit for no timeout.
14
+ */
15
+ statementTimeoutMs?: number;
16
+ };
8
17
  /**
9
18
  * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
10
19
  * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
11
20
  */
12
- declare function createMysqlExecutorFromPool(pool: Pool): DbExecutor;
21
+ declare function createMysqlExecutorFromPool(pool: Pool, opts?: MysqlExecutorOptions): DbExecutor;
13
22
  /**
14
23
  * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
15
- * `{ sql, params }` a `MysqlQuery` builder emits.
24
+ * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement
25
+ * ceiling (MySQL has no pool-level knob for it).
16
26
  */
17
- declare function createMysqlExecutor(config: MysqlConfig): DbExecutor;
27
+ declare function createMysqlExecutor(config: MysqlConfig, opts?: MysqlExecutorOptions): DbExecutor;
18
28
  //#endregion
19
- export { MysqlConfig, createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
29
+ export { MysqlConfig, MysqlExecutorOptions, createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
20
30
  //# sourceMappingURL=index.d.mts.map
@@ -1,15 +1,25 @@
1
1
  import { createPool } from "mysql2/promise";
2
2
  //#region src/mysql/index.ts
3
- /** Every `table` node in the plan, however deeply the operation wrappers nest it. */
4
- function tablesOf(block) {
5
- if (!block) return [];
6
- return [
7
- ...block.table ? [block.table] : [],
8
- ...(block.nested_loop ?? []).flatMap(tablesOf),
9
- ...tablesOf(block.ordering_operation),
10
- ...tablesOf(block.grouping_operation),
11
- ...tablesOf(block.duplicates_removal)
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) {
@@ -41,10 +51,15 @@ const toResult = (result) => {
41
51
  * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
42
52
  * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
43
53
  */
44
- function createMysqlExecutorFromPool(pool) {
54
+ function createMysqlExecutorFromPool(pool, opts = {}) {
55
+ const { statementTimeoutMs } = opts;
56
+ const query = (target, sql, args) => statementTimeoutMs != null ? target.query({
57
+ sql,
58
+ timeout: statementTimeoutMs
59
+ }, args) : target.query(sql, args);
45
60
  return {
46
61
  async run(prepared) {
47
- const [result] = await pool.query(prepared.sql, argsOf(prepared));
62
+ const [result] = await query(pool, prepared.sql, argsOf(prepared));
48
63
  return toResult(result);
49
64
  },
50
65
  async transaction(statements) {
@@ -53,7 +68,7 @@ function createMysqlExecutorFromPool(pool) {
53
68
  await conn.beginTransaction();
54
69
  const results = [];
55
70
  for (const s of statements) {
56
- const [result] = await conn.query(s.sql, argsOf(s));
71
+ const [result] = await query(conn, s.sql, argsOf(s));
57
72
  results.push(toResult(result));
58
73
  }
59
74
  await conn.commit();
@@ -66,7 +81,7 @@ function createMysqlExecutorFromPool(pool) {
66
81
  }
67
82
  },
68
83
  async explain(prepared) {
69
- const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));
84
+ const [rows] = await query(pool, `EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));
70
85
  return parseMysqlPlan((Array.isArray(rows) ? rows[0]?.EXPLAIN : "") ?? "");
71
86
  },
72
87
  async close() {
@@ -76,10 +91,11 @@ function createMysqlExecutorFromPool(pool) {
76
91
  }
77
92
  /**
78
93
  * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
79
- * `{ sql, params }` a `MysqlQuery` builder emits.
94
+ * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement
95
+ * ceiling (MySQL has no pool-level knob for it).
80
96
  */
81
- function createMysqlExecutor(config) {
82
- return createMysqlExecutorFromPool(createPool(config));
97
+ function createMysqlExecutor(config, opts = {}) {
98
+ return createMysqlExecutorFromPool(createPool(config), opts);
83
99
  }
84
100
  //#endregion
85
101
  export { createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import { createPool, type Pool, type PoolOptions, type ResultSetHeader } 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 MysqlBlock = {\n cost_info?: { query_cost?: string };\n table?: MysqlTable;\n nested_loop?: MysqlBlock[];\n ordering_operation?: MysqlBlock;\n grouping_operation?: MysqlBlock;\n duplicates_removal?: MysqlBlock;\n};\ntype MysqlPlan = { query_block?: MysqlBlock };\n\n/** Every `table` node in the plan, however deeply the operation wrappers nest it. */\nfunction tablesOf(block: MysqlBlock | undefined): MysqlTable[] {\n if (!block) return [];\n return [\n ...(block.table ? [block.table] : []),\n ...(block.nested_loop ?? []).flatMap(tablesOf),\n ...tablesOf(block.ordering_operation),\n ...tablesOf(block.grouping_operation),\n ...tablesOf(block.duplicates_removal),\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/**\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(pool: Pool): DbExecutor {\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const [result] = await pool.query(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 conn.query(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 pool.query(`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.\n */\nexport function createMysqlExecutor(config: MysqlConfig): DbExecutor {\n return createMysqlExecutorFromPool(createPool(config));\n}\n"],"mappings":";;;AAoBA,SAAS,SAAS,OAA6C;CAC7D,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,OAAO;EACL,GAAI,MAAM,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC;EACnC,IAAI,MAAM,eAAe,CAAC,EAAA,CAAG,QAAQ,QAAQ;EAC7C,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;CACtC;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;;;;;AAMA,SAAgB,4BAA4B,MAAwB;CAClE,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;GAChE,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,KAAK,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;KAClD,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,KAAK,MAAM,uBAAuB,SAAS,OAAO,OAAO,QAAQ,CAAC;GAEvF,OAAO,gBADK,MAAM,QAAQ,IAAI,IAAK,KAAK,EAAE,EAAuC,UAAU,OAC9D,EAAE;EACjC;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;AAMA,SAAgB,oBAAoB,QAAiC;CACnE,OAAO,4BAA4B,WAAW,MAAM,CAAC;AACvD"}
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.0.0",
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.mjs",
31
+ "main": "dist/index.cjs",
32
32
  "types": "dist/index.d.mts",
33
33
  "exports": {
34
34
  ".": {
35
- "types": "./dist/index.d.mts",
36
- "import": "./dist/index.mjs",
37
- "require": "./dist/index.cjs"
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
- "types": "./dist/sqlite/index.d.mts",
41
- "import": "./dist/sqlite/index.mjs",
42
- "require": "./dist/sqlite/index.cjs"
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
- "types": "./dist/postgres/index.d.mts",
46
- "import": "./dist/postgres/index.mjs",
47
- "require": "./dist/postgres/index.cjs"
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
- "types": "./dist/mysql/index.d.mts",
51
- "import": "./dist/mysql/index.mjs",
52
- "require": "./dist/mysql/index.cjs"
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
- "types": "./dist/mssql/index.d.mts",
56
- "import": "./dist/mssql/index.mjs",
57
- "require": "./dist/mssql/index.cjs"
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
- "types": "./dist/introspection/index.d.mts",
61
- "import": "./dist/introspection/index.mjs",
62
- "require": "./dist/introspection/index.cjs"
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": [
@@ -100,6 +130,7 @@
100
130
  "devDependencies": {
101
131
  "@eslint/js": "^10.0.1",
102
132
  "@libsql/client": "^0.17.4",
133
+ "@sebbo2002/semantic-release-jsr": "^4.0.2",
103
134
  "@semantic-release/changelog": "^6.0.3",
104
135
  "@semantic-release/commit-analyzer": "^13.0.1",
105
136
  "@semantic-release/git": "^10.0.1",
@@ -117,6 +148,7 @@
117
148
  "pg": "^8.22.0",
118
149
  "prettier": "^3.9.5",
119
150
  "semantic-release": "^25.0.7",
151
+ "semantic-release-replace-plugin": "^1.2.7",
120
152
  "tsdown": "^0.22.7",
121
153
  "typescript": "^5.9.3",
122
154
  "typescript-eslint": "^8.64.0",