@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeeBee Tech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/deebee-tech/sqleasy-engine/main/assets/sqleasy-engine-lockup-dark.svg">
4
+ <img alt="SQLEasy Engine" src="https://raw.githubusercontent.com/deebee-tech/sqleasy-engine/main/assets/sqleasy-engine-lockup-light.svg" width="482">
5
+ </picture>
6
+ </p>
7
+
8
+ **A thin, opt-in executor for [`@deebeetech/sqleasy`](https://github.com/deebee-tech/sqleasy).**
9
+
10
+ SQLEasy builds dialect-correct SQL and hands you `{ sql, params }`. It deliberately does not run
11
+ anything. This package runs it: one small `run` / `transaction` / `explain` / `close` surface per
12
+ dialect, and — crucially — **you load only the driver you import.**
13
+
14
+ ```bash
15
+ npm install @deebeetech/sqleasy-engine @libsql/client # only the driver(s) you use
16
+ ```
17
+
18
+ Each dialect lives at its own subpath, so importing one pulls in only its driver:
19
+
20
+ | Import | Driver (optional peer) | Factory |
21
+ | ------------------------------------- | ---------------------- | ------------------------ |
22
+ | `@deebeetech/sqleasy-engine/sqlite` | `@libsql/client` | `createSqliteExecutor` |
23
+ | `@deebeetech/sqleasy-engine/postgres` | `pg` | `createPostgresExecutor` |
24
+ | `@deebeetech/sqleasy-engine/mysql` | `mysql2` | `createMysqlExecutor` |
25
+ | `@deebeetech/sqleasy-engine/mssql` | `mssql` | `createMssqlExecutor` |
26
+
27
+ ```typescript
28
+ import { createSqliteExecutor } from '@deebeetech/sqleasy-engine/sqlite';
29
+
30
+ const db = createSqliteExecutor({ file: './app.db' });
31
+
32
+ // One statement — hand it a builder's parsePrepared() output.
33
+ const { rows } = await db.run({ sql: 'SELECT * FROM "users" WHERE "id" = ?;', params: [1] });
34
+
35
+ // A whole batch as one atomic transaction — hand it a MultiBuilder's preparedStatements().
36
+ // It commits on success and rolls back on any error; each statement runs as its own prepared
37
+ // statement, in order.
38
+ await db.transaction(multi.preparedStatements());
39
+
40
+ await db.close();
41
+ ```
42
+
43
+ The drivers are **optional peer dependencies** — install `pg`, `mysql2`, `mssql`, or `@libsql/client`
44
+ yourself, only for the dialects you actually use. Nothing is loaded for a dialect you never import.
45
+
46
+ ## Why a transaction runs statement by statement
47
+
48
+ SQLEasy's `MultiBuilder.parse()` renders a batch as one string for display — but placeholder
49
+ numbering restarts per statement, so that string is not a runnable parameterized call on
50
+ Postgres/MySQL/SQLite. `preparedStatements()` returns each statement's own `{ sql, params }`; this
51
+ engine opens a real driver-level transaction and runs them in order. That is the correct way to
52
+ execute a SQLEasy batch, and this package does it for you.
53
+
54
+ ## Status
55
+
56
+ All four dialect executors — **SQLite/libSQL, Postgres, MySQL, SQL Server** — implement the full
57
+ `run` / `transaction` / `explain` / `close` surface. The Postgres/MySQL/MSSQL executors also expose a
58
+ `…FromPool` variant so you can share a pool you already manage.
59
+
60
+ Tested: SQLite runs against real in-memory libSQL (params, atomic commit, rollback, explain). The
61
+ others verify their transaction orchestration (BEGIN → per-statement → COMMIT/ROLLBACK + connection
62
+ release) against a recording fake driver, their EXPLAIN parsers against real plan output, and carry
63
+ real-database integration blocks gated on a connection env var (`DATABASE_URL`, `MYSQL_URL`,
64
+ `MSSQL_CONNECTION_STRING`) for CI. Optional schema introspection is next.
65
+
66
+ Part of the [DeeBee](https://github.com/deebee-tech) ecosystem.
67
+
68
+ ## License
69
+
70
+ MIT © DeeBee Tech
package/dist/index.cjs ADDED
File without changes
@@ -0,0 +1,70 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * Core, driver-agnostic types for the SQLEasy engine.
4
+ *
5
+ * This entry point pulls in NO database driver — importing it is free. Pick a dialect executor from
6
+ * its own subpath (`@deebeetech/sqleasy-engine/sqlite`, `/postgres`, …); each one imports only its
7
+ * own driver, so you install and load just the drivers you use.
8
+ */
9
+ /** A single result-set row, keyed by column name. */
10
+ type Row = Record<string, unknown>;
11
+ /**
12
+ * A prepared statement and its ordered bound parameters — exactly the shape SQLEasy's
13
+ * `parsePrepared()` and `MultiBuilder.preparedStatements()` return.
14
+ *
15
+ * Structural on purpose: the engine accepts any `{ sql, params }`, so it never depends on the
16
+ * builder. `params` is optional/empty for SQL that carries its own values (e.g. SQLEasy's MSSQL
17
+ * `sp_executesql`, whose `params` is always `[]`).
18
+ */
19
+ type PreparedSql = {
20
+ sql: string;
21
+ params?: readonly unknown[];
22
+ };
23
+ /** The outcome of executing one statement. */
24
+ type QueryResult<T = Row> = {
25
+ rows: T[];
26
+ /** Rows returned (SELECT) or affected (INSERT/UPDATE/DELETE). */
27
+ rowCount: number;
28
+ };
29
+ /**
30
+ * The planner's estimate for a statement, obtained WITHOUT executing it. `cost` is in the dialect's
31
+ * own units and is NOT comparable across dialects — gate on `rows`/`fullScan` when you need one rule
32
+ * for all four. Best-effort: a backend supplies only what its planner exposes (SQLite has no
33
+ * numbers at all, only the plan shape).
34
+ */
35
+ type ExplainEstimate = {
36
+ /** Planner cost in the dialect's own units. Absent when the backend reports none (SQLite). */
37
+ cost?: number;
38
+ /** Estimated rows the plan produces. Absent when the backend reports none (SQLite). */
39
+ rows?: number;
40
+ /** The plan reads a whole table instead of seeking an index — the portable "this will hurt" signal. */
41
+ fullScan: boolean;
42
+ /** A short raw-plan excerpt, for display and debugging. */
43
+ plan: string;
44
+ };
45
+ /**
46
+ * Executes prepared SQL against one database.
47
+ *
48
+ * Obtain one from a dialect subpath (`createSqliteExecutor`, `createPostgresExecutor`, …). Pick the
49
+ * executor whose dialect matches the SQL you built, so placeholders and quoting line up — the engine
50
+ * runs what it is given verbatim and does not rewrite dialects.
51
+ */
52
+ type DbExecutor = {
53
+ /** Run one prepared statement and return its rows. */
54
+ run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>>;
55
+ /**
56
+ * Run several prepared statements as ONE atomic transaction: commit on success, roll back on any
57
+ * error. This is how you execute a SQLEasy `MultiBuilder` — pass `multi.preparedStatements()`.
58
+ * Statements run in order, each as its own prepared statement (never concatenated into one string,
59
+ * which would misbind: placeholder numbering restarts per statement), and each statement's result
60
+ * is returned in the same order.
61
+ */
62
+ transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]>;
63
+ /** Ask the planner what a statement would cost WITHOUT running it. Best-effort per backend. */
64
+ explain(prepared: PreparedSql): Promise<ExplainEstimate>;
65
+ /** Close the underlying connection/pool. */
66
+ close(): Promise<void>;
67
+ };
68
+ //#endregion
69
+ export { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row };
70
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,70 @@
1
+ //#region src/index.d.ts
2
+ /**
3
+ * Core, driver-agnostic types for the SQLEasy engine.
4
+ *
5
+ * This entry point pulls in NO database driver — importing it is free. Pick a dialect executor from
6
+ * its own subpath (`@deebeetech/sqleasy-engine/sqlite`, `/postgres`, …); each one imports only its
7
+ * own driver, so you install and load just the drivers you use.
8
+ */
9
+ /** A single result-set row, keyed by column name. */
10
+ type Row = Record<string, unknown>;
11
+ /**
12
+ * A prepared statement and its ordered bound parameters — exactly the shape SQLEasy's
13
+ * `parsePrepared()` and `MultiBuilder.preparedStatements()` return.
14
+ *
15
+ * Structural on purpose: the engine accepts any `{ sql, params }`, so it never depends on the
16
+ * builder. `params` is optional/empty for SQL that carries its own values (e.g. SQLEasy's MSSQL
17
+ * `sp_executesql`, whose `params` is always `[]`).
18
+ */
19
+ type PreparedSql = {
20
+ sql: string;
21
+ params?: readonly unknown[];
22
+ };
23
+ /** The outcome of executing one statement. */
24
+ type QueryResult<T = Row> = {
25
+ rows: T[];
26
+ /** Rows returned (SELECT) or affected (INSERT/UPDATE/DELETE). */
27
+ rowCount: number;
28
+ };
29
+ /**
30
+ * The planner's estimate for a statement, obtained WITHOUT executing it. `cost` is in the dialect's
31
+ * own units and is NOT comparable across dialects — gate on `rows`/`fullScan` when you need one rule
32
+ * for all four. Best-effort: a backend supplies only what its planner exposes (SQLite has no
33
+ * numbers at all, only the plan shape).
34
+ */
35
+ type ExplainEstimate = {
36
+ /** Planner cost in the dialect's own units. Absent when the backend reports none (SQLite). */
37
+ cost?: number;
38
+ /** Estimated rows the plan produces. Absent when the backend reports none (SQLite). */
39
+ rows?: number;
40
+ /** The plan reads a whole table instead of seeking an index — the portable "this will hurt" signal. */
41
+ fullScan: boolean;
42
+ /** A short raw-plan excerpt, for display and debugging. */
43
+ plan: string;
44
+ };
45
+ /**
46
+ * Executes prepared SQL against one database.
47
+ *
48
+ * Obtain one from a dialect subpath (`createSqliteExecutor`, `createPostgresExecutor`, …). Pick the
49
+ * executor whose dialect matches the SQL you built, so placeholders and quoting line up — the engine
50
+ * runs what it is given verbatim and does not rewrite dialects.
51
+ */
52
+ type DbExecutor = {
53
+ /** Run one prepared statement and return its rows. */
54
+ run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>>;
55
+ /**
56
+ * Run several prepared statements as ONE atomic transaction: commit on success, roll back on any
57
+ * error. This is how you execute a SQLEasy `MultiBuilder` — pass `multi.preparedStatements()`.
58
+ * Statements run in order, each as its own prepared statement (never concatenated into one string,
59
+ * which would misbind: placeholder numbering restarts per statement), and each statement's result
60
+ * is returned in the same order.
61
+ */
62
+ transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]>;
63
+ /** Ask the planner what a statement would cost WITHOUT running it. Best-effort per backend. */
64
+ explain(prepared: PreparedSql): Promise<ExplainEstimate>;
65
+ /** Close the underlying connection/pool. */
66
+ close(): Promise<void>;
67
+ };
68
+ //#endregion
69
+ export { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row };
70
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,419 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/introspection/build-schema.ts
3
+ /** Stitch flat catalog rows into per-table column/FK/index lists. Shared by every dialect reader. */
4
+ function buildSchema(tables, columns, fks, indexColumns = [], rowCounts = []) {
5
+ const colMap = /* @__PURE__ */ new Map();
6
+ for (const c of columns) {
7
+ const key = `${c.schema}.${c.table}`;
8
+ const list = colMap.get(key) ?? [];
9
+ list.push({
10
+ name: c.name,
11
+ dataType: c.dataType,
12
+ nullable: c.nullable,
13
+ isPrimaryKey: c.isPrimaryKey,
14
+ defaultValue: c.defaultValue
15
+ });
16
+ colMap.set(key, list);
17
+ }
18
+ const fkMap = /* @__PURE__ */ new Map();
19
+ for (const fk of fks) {
20
+ const key = `${fk.schema}.${fk.table}`;
21
+ const list = fkMap.get(key) ?? [];
22
+ list.push({
23
+ columnName: fk.columnName,
24
+ referencedTable: fk.referencedTable,
25
+ referencedColumn: fk.referencedColumn,
26
+ referencedSchema: fk.referencedSchema
27
+ });
28
+ fkMap.set(key, list);
29
+ }
30
+ const idxMap = /* @__PURE__ */ new Map();
31
+ for (const r of indexColumns) {
32
+ const key = `${r.schema}.${r.table}`;
33
+ const byName = idxMap.get(key) ?? /* @__PURE__ */ new Map();
34
+ const idx = byName.get(r.indexName) ?? {
35
+ unique: r.unique,
36
+ cols: []
37
+ };
38
+ idx.cols.push({
39
+ name: r.columnName,
40
+ ordinal: r.ordinal
41
+ });
42
+ byName.set(r.indexName, idx);
43
+ idxMap.set(key, byName);
44
+ }
45
+ const indexesFor = (key) => [...idxMap.get(key)?.entries() ?? []].map(([name, i]) => ({
46
+ name,
47
+ unique: i.unique,
48
+ columns: i.cols.sort((a, b) => a.ordinal - b.ordinal).map((c) => c.name)
49
+ }));
50
+ const rowCountMap = new Map(rowCounts.map((r) => [`${r.schema}.${r.table}`, r.count]));
51
+ return { tables: tables.map((t) => {
52
+ const key = `${t.schema}.${t.name}`;
53
+ return {
54
+ schema: t.schema,
55
+ name: t.name,
56
+ type: t.isView ? "view" : "table",
57
+ columns: colMap.get(key) ?? [],
58
+ foreignKeys: fkMap.get(key) ?? [],
59
+ indexes: indexesFor(key),
60
+ approxRowCount: rowCountMap.get(key)
61
+ };
62
+ }) };
63
+ }
64
+ //#endregion
65
+ //#region src/introspection/mssql.ts
66
+ /** SQL Server catalog reader (INFORMATION_SCHEMA + sys catalog). Pass an executor connected to a
67
+ * SQL Server database (`createMssqlExecutor`). */
68
+ async function introspectMssql(executor, schema) {
69
+ const target = schema || "dbo";
70
+ const tables = await executor.run({
71
+ sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type
72
+ FROM INFORMATION_SCHEMA.TABLES
73
+ WHERE TABLE_SCHEMA = @p0 AND TABLE_TYPE IN ('BASE TABLE', 'VIEW')
74
+ ORDER BY TABLE_TYPE, TABLE_NAME`,
75
+ params: [target]
76
+ });
77
+ const columns = await executor.run({
78
+ sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,
79
+ IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default
80
+ FROM INFORMATION_SCHEMA.COLUMNS
81
+ WHERE TABLE_SCHEMA = @p0
82
+ ORDER BY TABLE_NAME, ORDINAL_POSITION`,
83
+ params: [target]
84
+ });
85
+ const pks = await executor.run({
86
+ sql: `SELECT kcu.TABLE_NAME AS table_name, kcu.COLUMN_NAME AS column_name
87
+ FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
88
+ JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
89
+ ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME AND tc.TABLE_SCHEMA = kcu.TABLE_SCHEMA
90
+ WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY' AND tc.TABLE_SCHEMA = @p0`,
91
+ params: [target]
92
+ });
93
+ const fks = await executor.run({
94
+ sql: `SELECT t1.name AS table_name, c1.name AS column_name,
95
+ s2.name AS referenced_schema, t2.name AS referenced_table, c2.name AS referenced_column
96
+ FROM sys.foreign_key_columns fkc
97
+ JOIN sys.objects t1 ON fkc.parent_object_id = t1.object_id
98
+ JOIN sys.columns c1 ON fkc.parent_object_id = c1.object_id AND fkc.parent_column_id = c1.column_id
99
+ JOIN sys.objects t2 ON fkc.referenced_object_id = t2.object_id
100
+ JOIN sys.columns c2 ON fkc.referenced_object_id = c2.object_id AND fkc.referenced_column_id = c2.column_id
101
+ JOIN sys.schemas s1 ON t1.schema_id = s1.schema_id
102
+ JOIN sys.schemas s2 ON t2.schema_id = s2.schema_id
103
+ WHERE s1.name = @p0`,
104
+ params: [target]
105
+ });
106
+ const indexes = await executor.run({
107
+ sql: `SELECT t.name AS table_name, i.name AS index_name, i.is_unique AS is_unique,
108
+ c.name AS column_name, ic.key_ordinal AS ordinal
109
+ FROM sys.indexes i
110
+ JOIN sys.tables t ON t.object_id = i.object_id
111
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
112
+ JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id
113
+ JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id
114
+ WHERE s.name = @p0 AND i.type > 0 AND ic.is_included_column = 0
115
+ ORDER BY t.name, i.name, ic.key_ordinal`,
116
+ params: [target]
117
+ });
118
+ const rowCounts = await executor.run({
119
+ sql: `SELECT t.name AS table_name, SUM(p.rows) AS n
120
+ FROM sys.tables t
121
+ JOIN sys.schemas s ON s.schema_id = t.schema_id
122
+ JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0, 1)
123
+ WHERE s.name = @p0
124
+ GROUP BY t.name`,
125
+ params: [target]
126
+ });
127
+ const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));
128
+ return buildSchema(tables.rows.map((t) => ({
129
+ schema: target,
130
+ name: t.table_name,
131
+ isView: t.table_type === "VIEW"
132
+ })), columns.rows.map((c) => ({
133
+ schema: target,
134
+ table: c.table_name,
135
+ name: c.column_name,
136
+ dataType: c.data_type,
137
+ nullable: c.is_nullable === "YES",
138
+ isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),
139
+ defaultValue: c.column_default ?? void 0
140
+ })), fks.rows.map((fk) => ({
141
+ schema: target,
142
+ table: fk.table_name,
143
+ columnName: fk.column_name,
144
+ referencedTable: fk.referenced_table,
145
+ referencedColumn: fk.referenced_column,
146
+ referencedSchema: fk.referenced_schema
147
+ })), indexes.rows.map((r) => ({
148
+ schema: target,
149
+ table: r.table_name,
150
+ indexName: r.index_name,
151
+ unique: Boolean(r.is_unique),
152
+ columnName: r.column_name,
153
+ ordinal: Number(r.ordinal)
154
+ })), rowCounts.rows.map((r) => ({
155
+ schema: target,
156
+ table: r.table_name,
157
+ count: Number(r.n)
158
+ })));
159
+ }
160
+ //#endregion
161
+ //#region src/introspection/mysql.ts
162
+ /** MySQL / MariaDB catalog reader (information_schema). Pass an executor connected to a MySQL
163
+ * database (`createMysqlExecutor`). */
164
+ async function introspectMysql(executor, schema) {
165
+ let target = schema;
166
+ if (!target) target = (await executor.run({ sql: "SELECT DATABASE() AS db" })).rows[0]?.db ?? "";
167
+ const tables = await executor.run({
168
+ sql: `SELECT TABLE_NAME AS table_name, TABLE_TYPE AS table_type
169
+ FROM information_schema.tables
170
+ WHERE table_schema = ? AND table_type IN ('BASE TABLE', 'VIEW')
171
+ ORDER BY table_type, table_name`,
172
+ params: [target]
173
+ });
174
+ const columns = await executor.run({
175
+ sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name, DATA_TYPE AS data_type,
176
+ IS_NULLABLE AS is_nullable, COLUMN_DEFAULT AS column_default, COLUMN_KEY AS column_key
177
+ FROM information_schema.columns
178
+ WHERE table_schema = ?
179
+ ORDER BY table_name, ordinal_position`,
180
+ params: [target]
181
+ });
182
+ const fks = await executor.run({
183
+ sql: `SELECT TABLE_NAME AS table_name, COLUMN_NAME AS column_name,
184
+ REFERENCED_TABLE_SCHEMA AS referenced_table_schema,
185
+ REFERENCED_TABLE_NAME AS referenced_table_name,
186
+ REFERENCED_COLUMN_NAME AS referenced_column_name
187
+ FROM information_schema.key_column_usage
188
+ WHERE table_schema = ? AND referenced_table_name IS NOT NULL`,
189
+ params: [target]
190
+ });
191
+ const indexes = await executor.run({
192
+ sql: `SELECT TABLE_NAME AS table_name, INDEX_NAME AS index_name, NON_UNIQUE AS non_unique,
193
+ COLUMN_NAME AS column_name, SEQ_IN_INDEX AS seq
194
+ FROM information_schema.statistics
195
+ WHERE table_schema = ?
196
+ ORDER BY table_name, index_name, seq`,
197
+ params: [target]
198
+ });
199
+ const rowCounts = await executor.run({
200
+ sql: `SELECT TABLE_NAME AS table_name, TABLE_ROWS AS n
201
+ FROM information_schema.tables
202
+ WHERE table_schema = ? AND table_type = 'BASE TABLE'`,
203
+ params: [target]
204
+ });
205
+ return buildSchema(tables.rows.map((t) => ({
206
+ schema: target,
207
+ name: t.table_name,
208
+ isView: t.table_type === "VIEW"
209
+ })), columns.rows.map((c) => ({
210
+ schema: target,
211
+ table: c.table_name,
212
+ name: c.column_name,
213
+ dataType: c.data_type,
214
+ nullable: c.is_nullable === "YES",
215
+ isPrimaryKey: c.column_key === "PRI",
216
+ defaultValue: c.column_default ?? void 0
217
+ })), fks.rows.map((fk) => ({
218
+ schema: target,
219
+ table: fk.table_name,
220
+ columnName: fk.column_name,
221
+ referencedTable: fk.referenced_table_name,
222
+ referencedColumn: fk.referenced_column_name,
223
+ referencedSchema: fk.referenced_table_schema
224
+ })), indexes.rows.filter((r) => r.column_name != null).map((r) => ({
225
+ schema: target,
226
+ table: r.table_name,
227
+ indexName: r.index_name,
228
+ unique: r.non_unique === 0,
229
+ columnName: r.column_name,
230
+ ordinal: Number(r.seq)
231
+ })), rowCounts.rows.map((r) => ({
232
+ schema: target,
233
+ table: r.table_name,
234
+ count: Number(r.n ?? 0)
235
+ })));
236
+ }
237
+ //#endregion
238
+ //#region src/introspection/postgres.ts
239
+ /** Postgres catalog reader (information_schema + pg_catalog). Pass an executor connected to a
240
+ * Postgres database (`createPostgresExecutor`). */
241
+ async function introspectPostgres(executor, schema) {
242
+ const target = schema || "public";
243
+ const tables = await executor.run({
244
+ sql: `SELECT table_name, table_type
245
+ FROM information_schema.tables
246
+ WHERE table_schema = $1 AND table_type IN ('BASE TABLE', 'VIEW')
247
+ ORDER BY table_type, table_name`,
248
+ params: [target]
249
+ });
250
+ const columns = await executor.run({
251
+ sql: `SELECT table_name, column_name, data_type, is_nullable, column_default
252
+ FROM information_schema.columns
253
+ WHERE table_schema = $1
254
+ ORDER BY table_name, ordinal_position`,
255
+ params: [target]
256
+ });
257
+ const pks = await executor.run({
258
+ sql: `SELECT kcu.table_name, kcu.column_name
259
+ FROM information_schema.table_constraints tc
260
+ JOIN information_schema.key_column_usage kcu
261
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
262
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = $1`,
263
+ params: [target]
264
+ });
265
+ const fks = await executor.run({
266
+ sql: `SELECT kcu.table_name, kcu.column_name,
267
+ ccu.table_schema AS referenced_table_schema,
268
+ ccu.table_name AS referenced_table_name,
269
+ ccu.column_name AS referenced_column_name
270
+ FROM information_schema.table_constraints tc
271
+ JOIN information_schema.key_column_usage kcu
272
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
273
+ JOIN information_schema.constraint_column_usage ccu
274
+ ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
275
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1`,
276
+ params: [target]
277
+ });
278
+ const indexes = await executor.run({
279
+ sql: `SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique,
280
+ a.attname AS column_name, k.ord AS ordinal
281
+ FROM pg_index ix
282
+ JOIN pg_class i ON i.oid = ix.indexrelid
283
+ JOIN pg_class t ON t.oid = ix.indrelid
284
+ JOIN pg_namespace n ON n.oid = t.relnamespace
285
+ JOIN LATERAL unnest(string_to_array(ix.indkey::text, ' ')::int[]) WITH ORDINALITY AS k(attnum, ord)
286
+ ON true
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
289
+ ORDER BY table_name, index_name, ordinal`,
290
+ params: [target]
291
+ });
292
+ const rowCounts = await executor.run({
293
+ sql: `SELECT c.relname AS table_name, c.reltuples::bigint AS n
294
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
295
+ WHERE n.nspname = $1 AND c.relkind = 'r'`,
296
+ params: [target]
297
+ });
298
+ const pkSet = new Set(pks.rows.map((p) => `${p.table_name}.${p.column_name}`));
299
+ return buildSchema(tables.rows.map((t) => ({
300
+ schema: target,
301
+ name: t.table_name,
302
+ isView: t.table_type === "VIEW"
303
+ })), columns.rows.map((c) => ({
304
+ schema: target,
305
+ table: c.table_name,
306
+ name: c.column_name,
307
+ dataType: c.data_type,
308
+ nullable: c.is_nullable === "YES",
309
+ isPrimaryKey: pkSet.has(`${c.table_name}.${c.column_name}`),
310
+ defaultValue: c.column_default ?? void 0
311
+ })), fks.rows.map((fk) => ({
312
+ schema: target,
313
+ table: fk.table_name,
314
+ columnName: fk.column_name,
315
+ referencedTable: fk.referenced_table_name,
316
+ referencedColumn: fk.referenced_column_name,
317
+ referencedSchema: fk.referenced_table_schema
318
+ })), indexes.rows.map((r) => ({
319
+ schema: target,
320
+ table: r.table_name,
321
+ indexName: r.index_name,
322
+ unique: r.is_unique,
323
+ columnName: r.column_name,
324
+ ordinal: Number(r.ordinal)
325
+ })), rowCounts.rows.map((r) => ({
326
+ schema: target,
327
+ table: r.table_name,
328
+ count: Number(r.n)
329
+ })).filter((r) => r.count >= 0));
330
+ }
331
+ //#endregion
332
+ //#region src/introspection/sqlite.ts
333
+ /** SQLite catalog reader (sqlite_master + pragma table-valued functions). Also serves libsql/turso —
334
+ * they are all SQLite engines with the same catalog. Pass an executor from `createSqliteExecutor`. */
335
+ async function introspectSqlite(executor) {
336
+ const tables = await executor.run({ sql: `SELECT name, type FROM sqlite_master
337
+ WHERE type IN ('table', 'view') AND name NOT LIKE 'sqlite_%'
338
+ ORDER BY type, name` });
339
+ const columns = [];
340
+ const fks = [];
341
+ const indexColumns = [];
342
+ for (const t of tables.rows) {
343
+ const cols = await executor.run({
344
+ sql: "SELECT name, type, \"notnull\", dflt_value, pk FROM pragma_table_info(?)",
345
+ params: [t.name]
346
+ });
347
+ for (const c of cols.rows) columns.push({
348
+ schema: "main",
349
+ table: t.name,
350
+ name: c.name,
351
+ dataType: c.type || "TEXT",
352
+ nullable: c.notnull === 0 && c.pk === 0,
353
+ isPrimaryKey: c.pk > 0,
354
+ defaultValue: c.dflt_value ?? void 0
355
+ });
356
+ const fkRows = await executor.run({
357
+ sql: "SELECT \"from\", \"table\", \"to\" FROM pragma_foreign_key_list(?)",
358
+ params: [t.name]
359
+ });
360
+ for (const fk of fkRows.rows) fks.push({
361
+ schema: "main",
362
+ table: t.name,
363
+ columnName: fk.from,
364
+ referencedTable: fk.table,
365
+ referencedColumn: fk.to
366
+ });
367
+ const idxList = await executor.run({
368
+ sql: "SELECT name, \"unique\" FROM pragma_index_list(?)",
369
+ params: [t.name]
370
+ });
371
+ for (const idx of idxList.rows) {
372
+ const idxCols = await executor.run({
373
+ sql: "SELECT seqno, name FROM pragma_index_info(?)",
374
+ params: [idx.name]
375
+ });
376
+ for (const ic of idxCols.rows) {
377
+ if (ic.name == null) continue;
378
+ indexColumns.push({
379
+ schema: "main",
380
+ table: t.name,
381
+ indexName: idx.name,
382
+ unique: idx.unique === 1,
383
+ columnName: ic.name,
384
+ ordinal: ic.seqno
385
+ });
386
+ }
387
+ }
388
+ }
389
+ return buildSchema(tables.rows.map((t) => ({
390
+ schema: "main",
391
+ name: t.name,
392
+ isView: t.type === "view"
393
+ })), columns, fks, indexColumns);
394
+ }
395
+ //#endregion
396
+ //#region src/introspection/index.ts
397
+ /**
398
+ * Read a database's catalog as a {@link SchemaData}, through a supplied {@link DbExecutor}. Choose
399
+ * the `dialect` matching the executor you built — the reader runs that dialect's catalog queries.
400
+ * `schema` scopes the namespace, defaulting per dialect (postgres `public`, mysql the current
401
+ * database, mssql `dbo`, sqlite `main` — sqlite ignores it).
402
+ */
403
+ function introspectSchema(executor, dialect, schema) {
404
+ switch (dialect) {
405
+ case "postgres": return introspectPostgres(executor, schema);
406
+ case "mysql": return introspectMysql(executor, schema);
407
+ case "mssql": return introspectMssql(executor, schema);
408
+ case "sqlite": return introspectSqlite(executor);
409
+ }
410
+ }
411
+ //#endregion
412
+ exports.buildSchema = buildSchema;
413
+ exports.introspectMssql = introspectMssql;
414
+ exports.introspectMysql = introspectMysql;
415
+ exports.introspectPostgres = introspectPostgres;
416
+ exports.introspectSchema = introspectSchema;
417
+ exports.introspectSqlite = introspectSqlite;
418
+
419
+ //# sourceMappingURL=index.cjs.map