@deebeetech/sqleasy-engine 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/index.cjs +0 -0
- package/dist/index.d.cts +70 -0
- package/dist/index.d.mts +70 -0
- package/dist/index.mjs +1 -0
- package/dist/introspection/index.cjs +419 -0
- package/dist/introspection/index.cjs.map +1 -0
- package/dist/introspection/index.d.cts +106 -0
- package/dist/introspection/index.d.mts +106 -0
- package/dist/introspection/index.mjs +413 -0
- package/dist/introspection/index.mjs.map +1 -0
- package/dist/mssql/index.cjs +155 -0
- package/dist/mssql/index.cjs.map +1 -0
- package/dist/mssql/index.d.cts +27 -0
- package/dist/mssql/index.d.mts +27 -0
- package/dist/mssql/index.mjs +129 -0
- package/dist/mssql/index.mjs.map +1 -0
- package/dist/mysql/index.cjs +90 -0
- package/dist/mysql/index.cjs.map +1 -0
- package/dist/mysql/index.d.cts +20 -0
- package/dist/mysql/index.d.mts +20 -0
- package/dist/mysql/index.mjs +87 -0
- package/dist/mysql/index.mjs.map +1 -0
- package/dist/postgres/index.cjs +64 -0
- package/dist/postgres/index.cjs.map +1 -0
- package/dist/postgres/index.d.cts +21 -0
- package/dist/postgres/index.d.mts +21 -0
- package/dist/postgres/index.mjs +61 -0
- package/dist/postgres/index.mjs.map +1 -0
- package/dist/sqlite/index.cjs +81 -0
- package/dist/sqlite/index.cjs.map +1 -0
- package/dist/sqlite/index.d.cts +18 -0
- package/dist/sqlite/index.d.mts +18 -0
- package/dist/sqlite/index.mjs +80 -0
- package/dist/sqlite/index.mjs.map +1 -0
- package/package.json +125 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
//#region src/introspection/build-schema.ts
|
|
2
|
+
/** Stitch flat catalog rows into per-table column/FK/index lists. Shared by every dialect reader. */
|
|
3
|
+
function buildSchema(tables, columns, fks, indexColumns = [], rowCounts = []) {
|
|
4
|
+
const colMap = /* @__PURE__ */ new Map();
|
|
5
|
+
for (const c of columns) {
|
|
6
|
+
const key = `${c.schema}.${c.table}`;
|
|
7
|
+
const list = colMap.get(key) ?? [];
|
|
8
|
+
list.push({
|
|
9
|
+
name: c.name,
|
|
10
|
+
dataType: c.dataType,
|
|
11
|
+
nullable: c.nullable,
|
|
12
|
+
isPrimaryKey: c.isPrimaryKey,
|
|
13
|
+
defaultValue: c.defaultValue
|
|
14
|
+
});
|
|
15
|
+
colMap.set(key, list);
|
|
16
|
+
}
|
|
17
|
+
const fkMap = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const fk of fks) {
|
|
19
|
+
const key = `${fk.schema}.${fk.table}`;
|
|
20
|
+
const list = fkMap.get(key) ?? [];
|
|
21
|
+
list.push({
|
|
22
|
+
columnName: fk.columnName,
|
|
23
|
+
referencedTable: fk.referencedTable,
|
|
24
|
+
referencedColumn: fk.referencedColumn,
|
|
25
|
+
referencedSchema: fk.referencedSchema
|
|
26
|
+
});
|
|
27
|
+
fkMap.set(key, list);
|
|
28
|
+
}
|
|
29
|
+
const idxMap = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const r of indexColumns) {
|
|
31
|
+
const key = `${r.schema}.${r.table}`;
|
|
32
|
+
const byName = idxMap.get(key) ?? /* @__PURE__ */ new Map();
|
|
33
|
+
const idx = byName.get(r.indexName) ?? {
|
|
34
|
+
unique: r.unique,
|
|
35
|
+
cols: []
|
|
36
|
+
};
|
|
37
|
+
idx.cols.push({
|
|
38
|
+
name: r.columnName,
|
|
39
|
+
ordinal: r.ordinal
|
|
40
|
+
});
|
|
41
|
+
byName.set(r.indexName, idx);
|
|
42
|
+
idxMap.set(key, byName);
|
|
43
|
+
}
|
|
44
|
+
const indexesFor = (key) => [...idxMap.get(key)?.entries() ?? []].map(([name, i]) => ({
|
|
45
|
+
name,
|
|
46
|
+
unique: i.unique,
|
|
47
|
+
columns: i.cols.sort((a, b) => a.ordinal - b.ordinal).map((c) => c.name)
|
|
48
|
+
}));
|
|
49
|
+
const rowCountMap = new Map(rowCounts.map((r) => [`${r.schema}.${r.table}`, r.count]));
|
|
50
|
+
return { tables: tables.map((t) => {
|
|
51
|
+
const key = `${t.schema}.${t.name}`;
|
|
52
|
+
return {
|
|
53
|
+
schema: t.schema,
|
|
54
|
+
name: t.name,
|
|
55
|
+
type: t.isView ? "view" : "table",
|
|
56
|
+
columns: colMap.get(key) ?? [],
|
|
57
|
+
foreignKeys: fkMap.get(key) ?? [],
|
|
58
|
+
indexes: indexesFor(key),
|
|
59
|
+
approxRowCount: rowCountMap.get(key)
|
|
60
|
+
};
|
|
61
|
+
}) };
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/introspection/mssql.ts
|
|
65
|
+
/** SQL Server catalog reader (INFORMATION_SCHEMA + sys catalog). Pass an executor connected to a
|
|
66
|
+
* SQL Server database (`createMssqlExecutor`). */
|
|
67
|
+
async function introspectMssql(executor, schema) {
|
|
68
|
+
const target = schema || "dbo";
|
|
69
|
+
const tables = await executor.run({
|
|
70
|
+
sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type
|
|
71
|
+
FROM INFORMATION_SCHEMA.TABLES
|
|
72
|
+
WHERE TABLE_SCHEMA = @p0 AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
|
|
73
|
+
ORDER BY TABLE_TYPE, TABLE_NAME`,
|
|
74
|
+
params: [target]
|
|
75
|
+
});
|
|
76
|
+
const columns = await executor.run({
|
|
77
|
+
sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,
|
|
78
|
+
IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default
|
|
79
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
80
|
+
WHERE TABLE_SCHEMA = @p0
|
|
81
|
+
ORDER BY TABLE_NAME, ORDINAL_POSITION`,
|
|
82
|
+
params: [target]
|
|
83
|
+
});
|
|
84
|
+
const pks = await executor.run({
|
|
85
|
+
sql: `SELECT kcu.TABLE_NAME AS table_name, kcu.COLUMN_NAME AS column_name
|
|
86
|
+
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
|
87
|
+
JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
|
88
|
+
ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA
|
|
89
|
+
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = @p0`,
|
|
90
|
+
params: [target]
|
|
91
|
+
});
|
|
92
|
+
const fks = await executor.run({
|
|
93
|
+
sql: `SELECT t1.name AS table_name, c1.name AS column_name,
|
|
94
|
+
s2.name AS referenced_schema, t2.name AS referenced_table, c2.name AS referenced_column
|
|
95
|
+
FROM sys.foreign_key_columns fkc
|
|
96
|
+
JOIN sys.objects t1 ON fkc.parent_object_id = t1.object_id
|
|
97
|
+
JOIN sys.columns c1 ON fkc.parent_object_id = c1.object_id AND fkc.parent_column_id = c1.column_id
|
|
98
|
+
JOIN sys.objects t2 ON fkc.referenced_object_id = t2.object_id
|
|
99
|
+
JOIN sys.columns c2 ON fkc.referenced_object_id = c2.object_id AND fkc.referenced_column_id = c2.column_id
|
|
100
|
+
JOIN sys.schemas s1 ON t1.schema_id = s1.schema_id
|
|
101
|
+
JOIN sys.schemas s2 ON t2.schema_id = s2.schema_id
|
|
102
|
+
WHERE s1.name = @p0`,
|
|
103
|
+
params: [target]
|
|
104
|
+
});
|
|
105
|
+
const indexes = await executor.run({
|
|
106
|
+
sql: `SELECT t.name AS table_name, i.name AS index_name, i.is_unique AS is_unique,
|
|
107
|
+
c.name AS column_name, ic.key_ordinal AS ordinal
|
|
108
|
+
FROM sys.indexes i
|
|
109
|
+
JOIN sys.tables t ON t.object_id = i.object_id
|
|
110
|
+
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
|
111
|
+
JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
|
|
112
|
+
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
|
|
113
|
+
WHERE s.name = @p0 AND i.type > 0 AND ic.is_included_column = 0
|
|
114
|
+
ORDER BY t.name, i.name, ic.key_ordinal`,
|
|
115
|
+
params: [target]
|
|
116
|
+
});
|
|
117
|
+
const rowCounts = await executor.run({
|
|
118
|
+
sql: `SELECT t.name AS table_name, SUM(p.rows) AS n
|
|
119
|
+
FROM sys.tables t
|
|
120
|
+
JOIN sys.schemas s ON s.schema_id = t.schema_id
|
|
121
|
+
JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)
|
|
122
|
+
WHERE s.name = @p0
|
|
123
|
+
GROUP BY t.name`,
|
|
124
|
+
params: [target]
|
|
125
|
+
});
|
|
126
|
+
const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));
|
|
127
|
+
return buildSchema(tables.rows.map((t) => ({
|
|
128
|
+
schema: target,
|
|
129
|
+
name: t.table_name,
|
|
130
|
+
isView: t.table_type === "VIEW"
|
|
131
|
+
})), columns.rows.map((c) => ({
|
|
132
|
+
schema: target,
|
|
133
|
+
table: c.table_name,
|
|
134
|
+
name: c.column_name,
|
|
135
|
+
dataType: c.data_type,
|
|
136
|
+
nullable: c.is_nullable === "YES",
|
|
137
|
+
isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),
|
|
138
|
+
defaultValue: c.column_default ?? void 0
|
|
139
|
+
})), fks.rows.map((fk) => ({
|
|
140
|
+
schema: target,
|
|
141
|
+
table: fk.table_name,
|
|
142
|
+
columnName: fk.column_name,
|
|
143
|
+
referencedTable: fk.referenced_table,
|
|
144
|
+
referencedColumn: fk.referenced_column,
|
|
145
|
+
referencedSchema: fk.referenced_schema
|
|
146
|
+
})), indexes.rows.map((r) => ({
|
|
147
|
+
schema: target,
|
|
148
|
+
table: r.table_name,
|
|
149
|
+
indexName: r.index_name,
|
|
150
|
+
unique: Boolean(r.is_unique),
|
|
151
|
+
columnName: r.column_name,
|
|
152
|
+
ordinal: Number(r.ordinal)
|
|
153
|
+
})), rowCounts.rows.map((r) => ({
|
|
154
|
+
schema: target,
|
|
155
|
+
table: r.table_name,
|
|
156
|
+
count: Number(r.n)
|
|
157
|
+
})));
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/introspection/mysql.ts
|
|
161
|
+
/** MySQL / MariaDB catalog reader (information_schema). Pass an executor connected to a MySQL
|
|
162
|
+
* database (`createMysqlExecutor`). */
|
|
163
|
+
async function introspectMysql(executor, schema) {
|
|
164
|
+
let target = schema;
|
|
165
|
+
if (!target) target = (await executor.run({ sql: "SELECT DATABASE() AS db" })).rows[0]?.db ?? "";
|
|
166
|
+
const tables = await executor.run({
|
|
167
|
+
sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type
|
|
168
|
+
FROM information_schema.tables
|
|
169
|
+
WHERE table_schema = ? AND table_type IN ('BASE TABLE', 'VIEW')
|
|
170
|
+
ORDER BY table_type, table_name`,
|
|
171
|
+
params: [target]
|
|
172
|
+
});
|
|
173
|
+
const columns = await executor.run({
|
|
174
|
+
sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,
|
|
175
|
+
IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default, COLUMN_KEY AS column_key
|
|
176
|
+
FROM information_schema.columns
|
|
177
|
+
WHERE table_schema = ?
|
|
178
|
+
ORDER BY table_name, ordinal_position`,
|
|
179
|
+
params: [target]
|
|
180
|
+
});
|
|
181
|
+
const fks = await executor.run({
|
|
182
|
+
sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name,
|
|
183
|
+
REFERENCED_TABLE_SCHEMA AS referenced_table_schema,
|
|
184
|
+
REFERENCED_TABLE_NAME AS referenced_table_name,
|
|
185
|
+
REFERENCED_COLUMN_NAME AS referenced_column_name
|
|
186
|
+
FROM information_schema.key_column_usage
|
|
187
|
+
WHERE table_schema = ? AND referenced_table_name IS NOT NULL`,
|
|
188
|
+
params: [target]
|
|
189
|
+
});
|
|
190
|
+
const indexes = await executor.run({
|
|
191
|
+
sql: `SELECT TABLE_NAME AS table_name, INDEX_NAME AS index_name, NON_UNIQUE AS non_unique,
|
|
192
|
+
COLUMN_NAME AS column_name, SEQ_IN_INDEX AS seq
|
|
193
|
+
FROM information_schema.statistics
|
|
194
|
+
WHERE table_schema = ?
|
|
195
|
+
ORDER BY table_name, index_name, seq`,
|
|
196
|
+
params: [target]
|
|
197
|
+
});
|
|
198
|
+
const rowCounts = await executor.run({
|
|
199
|
+
sql: `SELECT TABLE_NAME AS table_name, TABLE_ROWS AS n
|
|
200
|
+
FROM information_schema.tables
|
|
201
|
+
WHERE table_schema = ? AND table_type = 'BASE TABLE'`,
|
|
202
|
+
params: [target]
|
|
203
|
+
});
|
|
204
|
+
return buildSchema(tables.rows.map((t) => ({
|
|
205
|
+
schema: target,
|
|
206
|
+
name: t.table_name,
|
|
207
|
+
isView: t.table_type === "VIEW"
|
|
208
|
+
})), columns.rows.map((c) => ({
|
|
209
|
+
schema: target,
|
|
210
|
+
table: c.table_name,
|
|
211
|
+
name: c.column_name,
|
|
212
|
+
dataType: c.data_type,
|
|
213
|
+
nullable: c.is_nullable === "YES",
|
|
214
|
+
isPrimaryKey: c.column_key === "PRI",
|
|
215
|
+
defaultValue: c.column_default ?? void 0
|
|
216
|
+
})), fks.rows.map((fk) => ({
|
|
217
|
+
schema: target,
|
|
218
|
+
table: fk.table_name,
|
|
219
|
+
columnName: fk.column_name,
|
|
220
|
+
referencedTable: fk.referenced_table_name,
|
|
221
|
+
referencedColumn: fk.referenced_column_name,
|
|
222
|
+
referencedSchema: fk.referenced_table_schema
|
|
223
|
+
})), indexes.rows.filter((r) => r.column_name != null).map((r) => ({
|
|
224
|
+
schema: target,
|
|
225
|
+
table: r.table_name,
|
|
226
|
+
indexName: r.index_name,
|
|
227
|
+
unique: r.non_unique === 0,
|
|
228
|
+
columnName: r.column_name,
|
|
229
|
+
ordinal: Number(r.seq)
|
|
230
|
+
})), rowCounts.rows.map((r) => ({
|
|
231
|
+
schema: target,
|
|
232
|
+
table: r.table_name,
|
|
233
|
+
count: Number(r.n ?? 0)
|
|
234
|
+
})));
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/introspection/postgres.ts
|
|
238
|
+
/** Postgres catalog reader (information_schema + pg_catalog). Pass an executor connected to a
|
|
239
|
+
* Postgres database (`createPostgresExecutor`). */
|
|
240
|
+
async function introspectPostgres(executor, schema) {
|
|
241
|
+
const target = schema || "public";
|
|
242
|
+
const tables = await executor.run({
|
|
243
|
+
sql: `SELECT table_name, table_type
|
|
244
|
+
FROM information_schema.tables
|
|
245
|
+
WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')
|
|
246
|
+
ORDER BY table_type, table_name`,
|
|
247
|
+
params: [target]
|
|
248
|
+
});
|
|
249
|
+
const columns = await executor.run({
|
|
250
|
+
sql: `SELECT table_name, column_name, data_type, is_nullable, column_default
|
|
251
|
+
FROM information_schema.columns
|
|
252
|
+
WHERE table_schema = $1
|
|
253
|
+
ORDER BY table_name, ordinal_position`,
|
|
254
|
+
params: [target]
|
|
255
|
+
});
|
|
256
|
+
const pks = await executor.run({
|
|
257
|
+
sql: `SELECT kcu.table_name, kcu.column_name
|
|
258
|
+
FROM information_schema.table_constraints tc
|
|
259
|
+
JOIN information_schema.key_column_usage kcu
|
|
260
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
261
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1`,
|
|
262
|
+
params: [target]
|
|
263
|
+
});
|
|
264
|
+
const fks = await executor.run({
|
|
265
|
+
sql: `SELECT kcu.table_name, kcu.column_name,
|
|
266
|
+
ccu.table_schema AS referenced_table_schema,
|
|
267
|
+
ccu.table_name AS referenced_table_name,
|
|
268
|
+
ccu.column_name AS referenced_column_name
|
|
269
|
+
FROM information_schema.table_constraints tc
|
|
270
|
+
JOIN information_schema.key_column_usage kcu
|
|
271
|
+
ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
|
|
272
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
273
|
+
ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
|
|
274
|
+
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`,
|
|
275
|
+
params: [target]
|
|
276
|
+
});
|
|
277
|
+
const indexes = await executor.run({
|
|
278
|
+
sql: `SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique,
|
|
279
|
+
a.attname AS column_name, k.ord AS ordinal
|
|
280
|
+
FROM pg_index ix
|
|
281
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
282
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
283
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
284
|
+
JOIN LATERAL unnest(string_to_array(ix.indkey::text, ' ')::int[]) WITH ORDINALITY AS k(attnum, ord)
|
|
285
|
+
ON true
|
|
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
|
|
288
|
+
ORDER BY table_name, index_name, ordinal`,
|
|
289
|
+
params: [target]
|
|
290
|
+
});
|
|
291
|
+
const rowCounts = await executor.run({
|
|
292
|
+
sql: `SELECT c.relname AS table_name, c.reltuples::bigint AS n
|
|
293
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
294
|
+
WHERE n.nspname = $1 AND c.relkind = 'r'`,
|
|
295
|
+
params: [target]
|
|
296
|
+
});
|
|
297
|
+
const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));
|
|
298
|
+
return buildSchema(tables.rows.map((t) => ({
|
|
299
|
+
schema: target,
|
|
300
|
+
name: t.table_name,
|
|
301
|
+
isView: t.table_type === "VIEW"
|
|
302
|
+
})), columns.rows.map((c) => ({
|
|
303
|
+
schema: target,
|
|
304
|
+
table: c.table_name,
|
|
305
|
+
name: c.column_name,
|
|
306
|
+
dataType: c.data_type,
|
|
307
|
+
nullable: c.is_nullable === "YES",
|
|
308
|
+
isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),
|
|
309
|
+
defaultValue: c.column_default ?? void 0
|
|
310
|
+
})), fks.rows.map((fk) => ({
|
|
311
|
+
schema: target,
|
|
312
|
+
table: fk.table_name,
|
|
313
|
+
columnName: fk.column_name,
|
|
314
|
+
referencedTable: fk.referenced_table_name,
|
|
315
|
+
referencedColumn: fk.referenced_column_name,
|
|
316
|
+
referencedSchema: fk.referenced_table_schema
|
|
317
|
+
})), indexes.rows.map((r) => ({
|
|
318
|
+
schema: target,
|
|
319
|
+
table: r.table_name,
|
|
320
|
+
indexName: r.index_name,
|
|
321
|
+
unique: r.is_unique,
|
|
322
|
+
columnName: r.column_name,
|
|
323
|
+
ordinal: Number(r.ordinal)
|
|
324
|
+
})), rowCounts.rows.map((r) => ({
|
|
325
|
+
schema: target,
|
|
326
|
+
table: r.table_name,
|
|
327
|
+
count: Number(r.n)
|
|
328
|
+
})).filter((r) => r.count >= 0));
|
|
329
|
+
}
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/introspection/sqlite.ts
|
|
332
|
+
/** SQLite catalog reader (sqlite_master + pragma table-valued functions). Also serves libsql/turso —
|
|
333
|
+
* they are all SQLite engines with the same catalog. Pass an executor from `createSqliteExecutor`. */
|
|
334
|
+
async function introspectSqlite(executor) {
|
|
335
|
+
const tables = await executor.run({ sql: `SELECT name, type FROM sqlite_master
|
|
336
|
+
WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'
|
|
337
|
+
ORDER BY type, name` });
|
|
338
|
+
const columns = [];
|
|
339
|
+
const fks = [];
|
|
340
|
+
const indexColumns = [];
|
|
341
|
+
for (const t of tables.rows) {
|
|
342
|
+
const cols = await executor.run({
|
|
343
|
+
sql: "SELECT name, type, \"notnull\", dflt_value, pk FROM pragma_table_info(?)",
|
|
344
|
+
params: [t.name]
|
|
345
|
+
});
|
|
346
|
+
for (const c of cols.rows) columns.push({
|
|
347
|
+
schema: "main",
|
|
348
|
+
table: t.name,
|
|
349
|
+
name: c.name,
|
|
350
|
+
dataType: c.type || "TEXT",
|
|
351
|
+
nullable: c.notnull === 0 && c.pk === 0,
|
|
352
|
+
isPrimaryKey: c.pk > 0,
|
|
353
|
+
defaultValue: c.dflt_value ?? void 0
|
|
354
|
+
});
|
|
355
|
+
const fkRows = await executor.run({
|
|
356
|
+
sql: "SELECT \"from\", \"table\", \"to\" FROM pragma_foreign_key_list(?)",
|
|
357
|
+
params: [t.name]
|
|
358
|
+
});
|
|
359
|
+
for (const fk of fkRows.rows) fks.push({
|
|
360
|
+
schema: "main",
|
|
361
|
+
table: t.name,
|
|
362
|
+
columnName: fk.from,
|
|
363
|
+
referencedTable: fk.table,
|
|
364
|
+
referencedColumn: fk.to
|
|
365
|
+
});
|
|
366
|
+
const idxList = await executor.run({
|
|
367
|
+
sql: "SELECT name, \"unique\" FROM pragma_index_list(?)",
|
|
368
|
+
params: [t.name]
|
|
369
|
+
});
|
|
370
|
+
for (const idx of idxList.rows) {
|
|
371
|
+
const idxCols = await executor.run({
|
|
372
|
+
sql: "SELECT seqno, name FROM pragma_index_info(?)",
|
|
373
|
+
params: [idx.name]
|
|
374
|
+
});
|
|
375
|
+
for (const ic of idxCols.rows) {
|
|
376
|
+
if (ic.name == null) continue;
|
|
377
|
+
indexColumns.push({
|
|
378
|
+
schema: "main",
|
|
379
|
+
table: t.name,
|
|
380
|
+
indexName: idx.name,
|
|
381
|
+
unique: idx.unique === 1,
|
|
382
|
+
columnName: ic.name,
|
|
383
|
+
ordinal: ic.seqno
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return buildSchema(tables.rows.map((t) => ({
|
|
389
|
+
schema: "main",
|
|
390
|
+
name: t.name,
|
|
391
|
+
isView: t.type === "view"
|
|
392
|
+
})), columns, fks, indexColumns);
|
|
393
|
+
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/introspection/index.ts
|
|
396
|
+
/**
|
|
397
|
+
* Read a database's catalog as a {@link SchemaData}, through a supplied {@link DbExecutor}. Choose
|
|
398
|
+
* the `dialect` matching the executor you built — the reader runs that dialect's catalog queries.
|
|
399
|
+
* `schema` scopes the namespace, defaulting per dialect (postgres `public`, mysql the current
|
|
400
|
+
* database, mssql `dbo`, sqlite `main` — sqlite ignores it).
|
|
401
|
+
*/
|
|
402
|
+
function introspectSchema(executor, dialect, schema) {
|
|
403
|
+
switch (dialect) {
|
|
404
|
+
case "postgres": return introspectPostgres(executor, schema);
|
|
405
|
+
case "mysql": return introspectMysql(executor, schema);
|
|
406
|
+
case "mssql": return introspectMssql(executor, schema);
|
|
407
|
+
case "sqlite": return introspectSqlite(executor);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
export { buildSchema, introspectMssql, introspectMysql, introspectPostgres, introspectSchema, introspectSqlite };
|
|
412
|
+
|
|
413
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let mssql = require("mssql");
|
|
25
|
+
mssql = __toESM(mssql, 1);
|
|
26
|
+
//#region src/mssql/index.ts
|
|
27
|
+
const { ConnectionPool, Transaction, Request } = mssql.default;
|
|
28
|
+
/** Read the T-SQL string literal starting at `from` (just past the opening quote), honouring `''`
|
|
29
|
+
* escapes. Returns the unescaped text and the index just past the closing quote. */
|
|
30
|
+
function readLiteral(sql, from) {
|
|
31
|
+
let out = "";
|
|
32
|
+
for (let i = from; i < sql.length; i++) {
|
|
33
|
+
if (sql[i] !== "'") {
|
|
34
|
+
out += sql[i];
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (sql[i + 1] === "'") {
|
|
38
|
+
out += "'";
|
|
39
|
+
i++;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
text: out,
|
|
44
|
+
end: i + 1
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Turn the mssql dialect's output into something SHOWPLAN can actually cost.
|
|
50
|
+
*
|
|
51
|
+
* It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does
|
|
52
|
+
* NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement
|
|
53
|
+
* must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared
|
|
54
|
+
* outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost
|
|
55
|
+
* estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.
|
|
56
|
+
*/
|
|
57
|
+
function toExplainableBatch(sql) {
|
|
58
|
+
const m = /exec\s+sp_executesql\s+N'/i.exec(sql);
|
|
59
|
+
if (!m) return sql;
|
|
60
|
+
const inner = readLiteral(sql, m.index + m[0].length);
|
|
61
|
+
if (!inner) return sql;
|
|
62
|
+
const decl = /^\s*,\s*N'/.exec(sql.slice(inner.end));
|
|
63
|
+
const decls = decl ? readLiteral(sql, inner.end + decl[0].length)?.text.trim() : "";
|
|
64
|
+
return decls ? `DECLARE ${decls};\n${inner.text}` : inner.text;
|
|
65
|
+
}
|
|
66
|
+
/** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */
|
|
67
|
+
function parsePlanXml(xml) {
|
|
68
|
+
let best;
|
|
69
|
+
for (const [tag] of xml.matchAll(/<StmtSimple\b[^>]*>/g)) {
|
|
70
|
+
const cost = Number(/StatementSubTreeCost="([\d.eE+-]+)"/.exec(tag)?.[1]);
|
|
71
|
+
if (!Number.isFinite(cost) || best && cost <= best.cost) continue;
|
|
72
|
+
const rows = Number(/StatementEstRows="([\d.eE+-]+)"/.exec(tag)?.[1]);
|
|
73
|
+
best = {
|
|
74
|
+
cost,
|
|
75
|
+
rows: Number.isFinite(rows) ? rows : void 0
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
cost: best?.cost,
|
|
80
|
+
rows: best?.rows,
|
|
81
|
+
fullScan: /PhysicalOp="(?:Table Scan|Clustered Index Scan|Index Scan)"/.test(xml),
|
|
82
|
+
plan: xml.slice(0, 500)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const toResult = (result) => ({
|
|
86
|
+
rows: result.recordset ?? [],
|
|
87
|
+
rowCount: result.recordset ? result.recordset.length : result.rowsAffected?.[0] ?? 0
|
|
88
|
+
});
|
|
89
|
+
const bindParams = (request, params) => {
|
|
90
|
+
(params ?? []).forEach((value, i) => request.input(`p${i}`, value));
|
|
91
|
+
return request;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained
|
|
95
|
+
* `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).
|
|
96
|
+
*/
|
|
97
|
+
function createMssqlExecutor(config) {
|
|
98
|
+
const makePool = () => new ConnectionPool("connectionString" in config ? config.connectionString : config);
|
|
99
|
+
let pool = makePool();
|
|
100
|
+
let ready;
|
|
101
|
+
const ensureReady = () => ready ??= pool.connect().catch((e) => {
|
|
102
|
+
ready = void 0;
|
|
103
|
+
const dead = pool;
|
|
104
|
+
pool = makePool();
|
|
105
|
+
dead.close().catch(() => {});
|
|
106
|
+
throw e;
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
async run(prepared) {
|
|
110
|
+
await ensureReady();
|
|
111
|
+
const request = bindParams(pool.request(), prepared.params);
|
|
112
|
+
return toResult(await request.query(prepared.sql));
|
|
113
|
+
},
|
|
114
|
+
async transaction(statements) {
|
|
115
|
+
await ensureReady();
|
|
116
|
+
const tx = new Transaction(pool);
|
|
117
|
+
await tx.begin();
|
|
118
|
+
try {
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const s of statements) {
|
|
121
|
+
const request = bindParams(new Request(tx), s.params);
|
|
122
|
+
results.push(toResult(await request.query(s.sql)));
|
|
123
|
+
}
|
|
124
|
+
await tx.commit();
|
|
125
|
+
return results;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
await tx.rollback().catch(() => {});
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
async explain(prepared) {
|
|
132
|
+
await ensureReady();
|
|
133
|
+
const tx = new Transaction(pool);
|
|
134
|
+
await tx.begin();
|
|
135
|
+
try {
|
|
136
|
+
await new Request(tx).batch("SET SHOWPLAN_XML ON");
|
|
137
|
+
const first = (await new Request(tx).batch(toExplainableBatch(prepared.sql))).recordset?.[0];
|
|
138
|
+
return parsePlanXml(String(first ? Object.values(first)[0] : ""));
|
|
139
|
+
} finally {
|
|
140
|
+
await new Request(tx).batch("SET SHOWPLAN_XML OFF").catch(() => {});
|
|
141
|
+
await tx.rollback().catch(() => {});
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
async close() {
|
|
145
|
+
await (ready?.catch(() => {}) ?? Promise.resolve());
|
|
146
|
+
await pool.close();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
exports.createMssqlExecutor = createMssqlExecutor;
|
|
152
|
+
exports.parsePlanXml = parsePlanXml;
|
|
153
|
+
exports.toExplainableBatch = toExplainableBatch;
|
|
154
|
+
|
|
155
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +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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"}
|