@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
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
|
package/dist/index.d.cts
ADDED
|
@@ -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
|
package/dist/index.d.mts
ADDED
|
@@ -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
|