@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.
@@ -0,0 +1,27 @@
1
+ import { DbExecutor, ExplainEstimate } from "../index.cjs";
2
+ import { config } from "mssql";
3
+ //#region src/mssql/index.d.ts
4
+ /** Connection settings — any `mssql` config object, or a raw connection string. */
5
+ type MssqlConfig = config | {
6
+ connectionString: string;
7
+ };
8
+ /**
9
+ * Turn the mssql dialect's output into something SHOWPLAN can actually cost.
10
+ *
11
+ * It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does
12
+ * NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement
13
+ * must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared
14
+ * outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost
15
+ * estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.
16
+ */
17
+ declare function toExplainableBatch(sql: string): string;
18
+ /** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */
19
+ declare function parsePlanXml(xml: string): ExplainEstimate;
20
+ /**
21
+ * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained
22
+ * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).
23
+ */
24
+ declare function createMssqlExecutor(config: MssqlConfig): DbExecutor;
25
+ //#endregion
26
+ export { MssqlConfig, createMssqlExecutor, parsePlanXml, toExplainableBatch };
27
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,27 @@
1
+ import { DbExecutor, ExplainEstimate } from "../index.mjs";
2
+ import { config } from "mssql";
3
+ //#region src/mssql/index.d.ts
4
+ /** Connection settings — any `mssql` config object, or a raw connection string. */
5
+ type MssqlConfig = config | {
6
+ connectionString: string;
7
+ };
8
+ /**
9
+ * Turn the mssql dialect's output into something SHOWPLAN can actually cost.
10
+ *
11
+ * It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does
12
+ * NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement
13
+ * must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared
14
+ * outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost
15
+ * estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.
16
+ */
17
+ declare function toExplainableBatch(sql: string): string;
18
+ /** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */
19
+ declare function parsePlanXml(xml: string): ExplainEstimate;
20
+ /**
21
+ * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained
22
+ * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).
23
+ */
24
+ declare function createMssqlExecutor(config: MssqlConfig): DbExecutor;
25
+ //#endregion
26
+ export { MssqlConfig, createMssqlExecutor, parsePlanXml, toExplainableBatch };
27
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,129 @@
1
+ import mssql from "mssql";
2
+ //#region src/mssql/index.ts
3
+ const { ConnectionPool, Transaction, Request } = mssql;
4
+ /** Read the T-SQL string literal starting at `from` (just past the opening quote), honouring `''`
5
+ * escapes. Returns the unescaped text and the index just past the closing quote. */
6
+ function readLiteral(sql, from) {
7
+ let out = "";
8
+ for (let i = from; i < sql.length; i++) {
9
+ if (sql[i] !== "'") {
10
+ out += sql[i];
11
+ continue;
12
+ }
13
+ if (sql[i + 1] === "'") {
14
+ out += "'";
15
+ i++;
16
+ continue;
17
+ }
18
+ return {
19
+ text: out,
20
+ end: i + 1
21
+ };
22
+ }
23
+ }
24
+ /**
25
+ * Turn the mssql dialect's output into something SHOWPLAN can actually cost.
26
+ *
27
+ * It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does
28
+ * NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement
29
+ * must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared
30
+ * outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost
31
+ * estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.
32
+ */
33
+ function toExplainableBatch(sql) {
34
+ const m = /exec\s+sp_executesql\s+N'/i.exec(sql);
35
+ if (!m) return sql;
36
+ const inner = readLiteral(sql, m.index + m[0].length);
37
+ if (!inner) return sql;
38
+ const decl = /^\s*,\s*N'/.exec(sql.slice(inner.end));
39
+ const decls = decl ? readLiteral(sql, inner.end + decl[0].length)?.text.trim() : "";
40
+ return decls ? `DECLARE ${decls};\n${inner.text}` : inner.text;
41
+ }
42
+ /** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */
43
+ function parsePlanXml(xml) {
44
+ let best;
45
+ for (const [tag] of xml.matchAll(/<StmtSimple\b[^>]*>/g)) {
46
+ const cost = Number(/StatementSubTreeCost="([\d.eE+-]+)"/.exec(tag)?.[1]);
47
+ if (!Number.isFinite(cost) || best && cost <= best.cost) continue;
48
+ const rows = Number(/StatementEstRows="([\d.eE+-]+)"/.exec(tag)?.[1]);
49
+ best = {
50
+ cost,
51
+ rows: Number.isFinite(rows) ? rows : void 0
52
+ };
53
+ }
54
+ return {
55
+ cost: best?.cost,
56
+ rows: best?.rows,
57
+ fullScan: /PhysicalOp="(?:Table Scan|Clustered Index Scan|Index Scan)"/.test(xml),
58
+ plan: xml.slice(0, 500)
59
+ };
60
+ }
61
+ const toResult = (result) => ({
62
+ rows: result.recordset ?? [],
63
+ rowCount: result.recordset ? result.recordset.length : result.rowsAffected?.[0] ?? 0
64
+ });
65
+ const bindParams = (request, params) => {
66
+ (params ?? []).forEach((value, i) => request.input(`p${i}`, value));
67
+ return request;
68
+ };
69
+ /**
70
+ * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained
71
+ * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).
72
+ */
73
+ function createMssqlExecutor(config) {
74
+ const makePool = () => new ConnectionPool("connectionString" in config ? config.connectionString : config);
75
+ let pool = makePool();
76
+ let ready;
77
+ const ensureReady = () => ready ??= pool.connect().catch((e) => {
78
+ ready = void 0;
79
+ const dead = pool;
80
+ pool = makePool();
81
+ dead.close().catch(() => {});
82
+ throw e;
83
+ });
84
+ return {
85
+ async run(prepared) {
86
+ await ensureReady();
87
+ const request = bindParams(pool.request(), prepared.params);
88
+ return toResult(await request.query(prepared.sql));
89
+ },
90
+ async transaction(statements) {
91
+ await ensureReady();
92
+ const tx = new Transaction(pool);
93
+ await tx.begin();
94
+ try {
95
+ const results = [];
96
+ for (const s of statements) {
97
+ const request = bindParams(new Request(tx), s.params);
98
+ results.push(toResult(await request.query(s.sql)));
99
+ }
100
+ await tx.commit();
101
+ return results;
102
+ } catch (err) {
103
+ await tx.rollback().catch(() => {});
104
+ throw err;
105
+ }
106
+ },
107
+ async explain(prepared) {
108
+ await ensureReady();
109
+ const tx = new Transaction(pool);
110
+ await tx.begin();
111
+ try {
112
+ await new Request(tx).batch("SET SHOWPLAN_XML ON");
113
+ const first = (await new Request(tx).batch(toExplainableBatch(prepared.sql))).recordset?.[0];
114
+ return parsePlanXml(String(first ? Object.values(first)[0] : ""));
115
+ } finally {
116
+ await new Request(tx).batch("SET SHOWPLAN_XML OFF").catch(() => {});
117
+ await tx.rollback().catch(() => {});
118
+ }
119
+ },
120
+ async close() {
121
+ await (ready?.catch(() => {}) ?? Promise.resolve());
122
+ await pool.close();
123
+ }
124
+ };
125
+ }
126
+ //#endregion
127
+ export { createMssqlExecutor, parsePlanXml, toExplainableBatch };
128
+
129
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/mssql/index.ts"],"sourcesContent":["// `mssql` is CommonJS and Node's ESM loader can't see its named exports — import the default and\n// destructure. (pg/mysql2/@libsql expose named exports fine.)\nimport mssql from 'mssql';\nimport type { config as MssqlDriverConfig, IResult } from 'mssql';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\nconst { ConnectionPool, Transaction, Request } = mssql;\n\n/** Connection settings — any `mssql` config object, or a raw connection string. */\nexport type MssqlConfig = MssqlDriverConfig | { connectionString: string };\n\n/** Read the T-SQL string literal starting at `from` (just past the opening quote), honouring `''`\n * escapes. Returns the unescaped text and the index just past the closing quote. */\nfunction readLiteral(sql: string, from: number): { text: string; end: number } | undefined {\n let out = '';\n for (let i = from; i < sql.length; i++) {\n if (sql[i] !== \"'\") {\n out += sql[i];\n continue;\n }\n if (sql[i + 1] === \"'\") {\n out += \"'\";\n i++;\n continue;\n }\n return { text: out, end: i + 1 };\n }\n return undefined; // unterminated — caller falls back to the raw batch\n}\n\n/**\n * Turn the mssql dialect's output into something SHOWPLAN can actually cost.\n *\n * It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does\n * NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement\n * must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared\n * outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost\n * estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.\n */\nexport function toExplainableBatch(sql: string): string {\n const m = /exec\\s+sp_executesql\\s+N'/i.exec(sql);\n if (!m) return sql;\n const inner = readLiteral(sql, m.index + m[0].length);\n if (!inner) return sql;\n const decl = /^\\s*,\\s*N'/.exec(sql.slice(inner.end));\n const decls = decl ? readLiteral(sql, inner.end + decl[0].length)?.text.trim() : '';\n return decls ? `DECLARE ${decls};\\n${inner.text}` : inner.text;\n}\n\n/** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */\nexport function parsePlanXml(xml: string): ExplainEstimate {\n // A batch holds one <StmtSimple> per statement (the injected DECLARE, SET NOCOUNT ON, the SELECT).\n // Take the most expensive — never blindly the first, which is usually a costless preamble.\n let best: { cost: number; rows?: number } | undefined;\n for (const [tag] of xml.matchAll(/<StmtSimple\\b[^>]*>/g)) {\n const cost = Number(/StatementSubTreeCost=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n if (!Number.isFinite(cost) || (best && cost <= best.cost)) continue;\n const rows = Number(/StatementEstRows=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n best = { cost, rows: Number.isFinite(rows) ? rows : undefined };\n }\n return {\n cost: best?.cost,\n rows: best?.rows,\n fullScan: /PhysicalOp=\"(?:Table Scan|Clustered Index Scan|Index Scan)\"/.test(xml),\n plan: xml.slice(0, 500),\n };\n}\n\nconst toResult = <T>(result: IResult<unknown>): QueryResult<T> => ({\n rows: (result.recordset ?? []) as unknown as T[],\n rowCount: result.recordset ? result.recordset.length : (result.rowsAffected?.[0] ?? 0),\n});\n\n// Bind params (if any) as @p0..@pN. SQLEasy's mssql dialect inlines its values into the\n// sp_executesql batch and passes `params: []`, so nothing binds on that path; a caller passing\n// bound values must reference @p0.. in their SQL (mssql has no positional `?`). No `?`→`@p`\n// rewriting — that scan corrupts a `?` inside a string literal.\ntype BindableRequest = { input(name: string, value: unknown): unknown };\nconst bindParams = <R extends BindableRequest>(request: R, params?: readonly unknown[]): R => {\n (params ?? []).forEach((value, i) => request.input(`p${i}`, value));\n return request;\n};\n\n/**\n * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained\n * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).\n */\nexport function createMssqlExecutor(config: MssqlConfig): DbExecutor {\n const makePool = () =>\n new ConnectionPool('connectionString' in config ? config.connectionString : config);\n let pool = makePool();\n\n // Single-flight connect that RECOVERS. Caching a rejected `pool.connect()` promise would brick the\n // pool forever (a DB restart / blip): every later query awaits the same settled rejection. So reset\n // the gate and rebuild the pool on failure, and the next call retries. (pg/mysql self-heal\n // per-acquire; mssql caches connect, so it alone needs this.)\n let ready: Promise<unknown> | undefined;\n const ensureReady = () =>\n (ready ??= pool.connect().catch((e: unknown) => {\n ready = undefined;\n const dead = pool;\n pool = makePool();\n void dead.close().catch(() => {});\n throw e;\n }));\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n await ensureReady();\n const request = bindParams(pool.request(), prepared.params);\n return toResult<T>(await request.query(prepared.sql));\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n await ensureReady();\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n const results: QueryResult[] = [];\n for (const s of statements) {\n const request = bindParams(new Request(tx), s.params);\n results.push(toResult(await request.query(s.sql)));\n }\n await tx.commit();\n return results;\n } catch (err) {\n await tx.rollback().catch(() => {});\n throw err;\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n await ensureReady();\n // SQL Server has no EXPLAIN. The estimated plan comes from SET SHOWPLAN_XML, which (a) must be\n // the ONLY statement in its batch and (b) is SESSION state — so the SET and the query must run\n // on the SAME connection. A transaction pins one connection for both; the finally block always\n // clears the flag and releases it, so SHOWPLAN never leaks onto a connection serving reads.\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n await new Request(tx).batch('SET SHOWPLAN_XML ON');\n const res = await new Request(tx).batch(toExplainableBatch(prepared.sql));\n const first = res.recordset?.[0] as Row | undefined;\n return parsePlanXml(String(first ? Object.values(first)[0] : ''));\n } finally {\n await new Request(tx).batch('SET SHOWPLAN_XML OFF').catch(() => {});\n await tx.rollback().catch(() => {});\n }\n },\n\n async close(): Promise<void> {\n await (ready?.catch(() => {}) ?? Promise.resolve());\n await pool.close();\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let mysql2_promise = require("mysql2/promise");
3
+ //#region src/mysql/index.ts
4
+ /** Every `table` node in the plan, however deeply the operation wrappers nest it. */
5
+ function tablesOf(block) {
6
+ if (!block) return [];
7
+ return [
8
+ ...block.table ? [block.table] : [],
9
+ ...(block.nested_loop ?? []).flatMap(tablesOf),
10
+ ...tablesOf(block.ordering_operation),
11
+ ...tablesOf(block.grouping_operation),
12
+ ...tablesOf(block.duplicates_removal)
13
+ ];
14
+ }
15
+ /** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */
16
+ function parseMysqlPlan(raw) {
17
+ let plan = {};
18
+ try {
19
+ plan = JSON.parse(raw || "{}");
20
+ } catch {}
21
+ const cost = Number(plan.query_block?.cost_info?.query_cost);
22
+ const tables = tablesOf(plan.query_block);
23
+ return {
24
+ cost: Number.isFinite(cost) ? cost : void 0,
25
+ rows: tables[0]?.rows_examined_per_scan,
26
+ fullScan: tables.some((t) => t.access_type === "ALL"),
27
+ plan: (raw ?? "").slice(0, 500)
28
+ };
29
+ }
30
+ const argsOf = (prepared) => prepared.params ?? [];
31
+ const toResult = (result) => {
32
+ if (Array.isArray(result)) return {
33
+ rows: result,
34
+ rowCount: result.length
35
+ };
36
+ return {
37
+ rows: [],
38
+ rowCount: result.affectedRows ?? 0
39
+ };
40
+ };
41
+ /**
42
+ * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
43
+ * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
44
+ */
45
+ function createMysqlExecutorFromPool(pool) {
46
+ return {
47
+ async run(prepared) {
48
+ const [result] = await pool.query(prepared.sql, argsOf(prepared));
49
+ return toResult(result);
50
+ },
51
+ async transaction(statements) {
52
+ const conn = await pool.getConnection();
53
+ try {
54
+ await conn.beginTransaction();
55
+ const results = [];
56
+ for (const s of statements) {
57
+ const [result] = await conn.query(s.sql, argsOf(s));
58
+ results.push(toResult(result));
59
+ }
60
+ await conn.commit();
61
+ return results;
62
+ } catch (err) {
63
+ await conn.rollback().catch(() => {});
64
+ throw err;
65
+ } finally {
66
+ conn.release();
67
+ }
68
+ },
69
+ async explain(prepared) {
70
+ const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));
71
+ return parseMysqlPlan((Array.isArray(rows) ? rows[0]?.EXPLAIN : "") ?? "");
72
+ },
73
+ async close() {
74
+ await pool.end();
75
+ }
76
+ };
77
+ }
78
+ /**
79
+ * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
80
+ * `{ sql, params }` a `MysqlQuery` builder emits.
81
+ */
82
+ function createMysqlExecutor(config) {
83
+ return createMysqlExecutorFromPool((0, mysql2_promise.createPool)(config));
84
+ }
85
+ //#endregion
86
+ exports.createMysqlExecutor = createMysqlExecutor;
87
+ exports.createMysqlExecutorFromPool = createMysqlExecutorFromPool;
88
+ exports.parseMysqlPlan = parseMysqlPlan;
89
+
90
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import { createPool, type Pool, type PoolOptions, type ResultSetHeader } from 'mysql2/promise';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `mysql2` `PoolOptions`. */\nexport type MysqlConfig = PoolOptions;\n\n/** `EXPLAIN FORMAT=JSON` shape — only the bits we read. A `table` node is not always a direct child\n * of `query_block`: a JOIN nests it under `nested_loop[]`, ORDER BY under `ordering_operation`, etc. */\ntype MysqlTable = { access_type?: string; rows_examined_per_scan?: number };\ntype MysqlBlock = {\n cost_info?: { query_cost?: string };\n table?: MysqlTable;\n nested_loop?: MysqlBlock[];\n ordering_operation?: MysqlBlock;\n grouping_operation?: MysqlBlock;\n duplicates_removal?: MysqlBlock;\n};\ntype MysqlPlan = { query_block?: MysqlBlock };\n\n/** Every `table` node in the plan, however deeply the operation wrappers nest it. */\nfunction tablesOf(block: MysqlBlock | undefined): MysqlTable[] {\n if (!block) return [];\n return [\n ...(block.table ? [block.table] : []),\n ...(block.nested_loop ?? []).flatMap(tablesOf),\n ...tablesOf(block.ordering_operation),\n ...tablesOf(block.grouping_operation),\n ...tablesOf(block.duplicates_removal),\n ];\n}\n\n/** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */\nexport function parseMysqlPlan(raw: string): ExplainEstimate {\n let plan: MysqlPlan = {};\n try {\n plan = JSON.parse(raw || '{}') as MysqlPlan;\n } catch {\n // Unparseable plan — an empty estimate beats throwing.\n }\n const cost = Number(plan.query_block?.cost_info?.query_cost);\n const tables = tablesOf(plan.query_block);\n return {\n cost: Number.isFinite(cost) ? cost : undefined,\n // The driving (first) table's scanned rows.\n rows: tables[0]?.rows_examined_per_scan,\n // `ALL` is MySQL's full-table-scan access type — a scan ANYWHERE in the plan counts.\n fullScan: tables.some((t) => t.access_type === 'ALL'),\n plan: (raw ?? '').slice(0, 500),\n };\n}\n\nconst argsOf = (prepared: PreparedSql): unknown[] => (prepared.params ?? []) as unknown[];\n\n// mysql2's query() resolves to `[rows | ResultSetHeader, fields]`. SELECT → an array of rows; a\n// write → a ResultSetHeader carrying affectedRows.\nconst toResult = <T>(result: unknown): QueryResult<T> => {\n if (Array.isArray(result)) return { rows: result as T[], rowCount: result.length };\n return { rows: [], rowCount: (result as ResultSetHeader).affectedRows ?? 0 };\n};\n\n/**\n * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across\n * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.\n */\nexport function createMysqlExecutorFromPool(pool: Pool): DbExecutor {\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const [result] = await pool.query(prepared.sql, argsOf(prepared));\n return toResult<T>(result);\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // Driver-level transaction on a single pinned connection — beginTransaction/commit/rollback\n // speak the protocol correctly (no `multipleStatements`, no concatenation). Each statement\n // runs on its own; ROLLBACK on any error; release the connection no matter what.\n const conn = await pool.getConnection();\n try {\n await conn.beginTransaction();\n const results: QueryResult[] = [];\n for (const s of statements) {\n const [result] = await conn.query(s.sql, argsOf(s));\n results.push(toResult(result));\n }\n await conn.commit();\n return results;\n } catch (err) {\n await conn.rollback().catch(() => {});\n throw err;\n } finally {\n conn.release();\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // EXPLAIN never executes the statement; FORMAT=JSON is the only form carrying a cost estimate.\n const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));\n const raw = Array.isArray(rows) ? (rows[0] as { EXPLAIN?: string } | undefined)?.EXPLAIN : '';\n return parseMysqlPlan(raw ?? '');\n },\n\n async close(): Promise<void> {\n await pool.end();\n },\n };\n}\n\n/**\n * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the\n * `{ sql, params }` a `MysqlQuery` builder emits.\n */\nexport function createMysqlExecutor(config: MysqlConfig): DbExecutor {\n return createMysqlExecutorFromPool(createPool(config));\n}\n"],"mappings":";;;;AAoBA,SAAS,SAAS,OAA6C;CAC7D,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,OAAO;EACL,GAAI,MAAM,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC;EACnC,IAAI,MAAM,eAAe,CAAC,EAAA,CAAG,QAAQ,QAAQ;EAC7C,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;CACtC;AACF;;AAGA,SAAgB,eAAe,KAA8B;CAC3D,IAAI,OAAkB,CAAC;CACvB,IAAI;EACF,OAAO,KAAK,MAAM,OAAO,IAAI;CAC/B,QAAQ,CAER;CACA,MAAM,OAAO,OAAO,KAAK,aAAa,WAAW,UAAU;CAC3D,MAAM,SAAS,SAAS,KAAK,WAAW;CACxC,OAAO;EACL,MAAM,OAAO,SAAS,IAAI,IAAI,OAAO,KAAA;EAErC,MAAM,OAAO,EAAE,EAAE;EAEjB,UAAU,OAAO,MAAM,MAAM,EAAE,gBAAgB,KAAK;EACpD,OAAO,OAAO,GAAA,CAAI,MAAM,GAAG,GAAG;CAChC;AACF;AAEA,MAAM,UAAU,aAAsC,SAAS,UAAU,CAAC;AAI1E,MAAM,YAAe,WAAoC;CACvD,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO;EAAE,MAAM;EAAe,UAAU,OAAO;CAAO;CACjF,OAAO;EAAE,MAAM,CAAC;EAAG,UAAW,OAA2B,gBAAgB;CAAE;AAC7E;;;;;AAMA,SAAgB,4BAA4B,MAAwB;CAClE,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;GAChE,OAAO,SAAY,MAAM;EAC3B;EAEA,MAAM,YAAY,YAA4D;GAI5E,MAAM,OAAO,MAAM,KAAK,cAAc;GACtC,IAAI;IACF,MAAM,KAAK,iBAAiB;IAC5B,MAAM,UAAyB,CAAC;IAChC,KAAK,MAAM,KAAK,YAAY;KAC1B,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;KAClD,QAAQ,KAAK,SAAS,MAAM,CAAC;IAC/B;IACA,MAAM,KAAK,OAAO;IAClB,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,KAAK,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM;GACR,UAAU;IACR,KAAK,QAAQ;GACf;EACF;EAEA,MAAM,QAAQ,UAAiD;GAE7D,MAAM,CAAC,QAAQ,MAAM,KAAK,MAAM,uBAAuB,SAAS,OAAO,OAAO,QAAQ,CAAC;GAEvF,OAAO,gBADK,MAAM,QAAQ,IAAI,IAAK,KAAK,EAAE,EAAuC,UAAU,OAC9D,EAAE;EACjC;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;AAMA,SAAgB,oBAAoB,QAAiC;CACnE,OAAO,6BAAA,GAAA,eAAA,WAAA,CAAuC,MAAM,CAAC;AACvD"}
@@ -0,0 +1,20 @@
1
+ import { DbExecutor, ExplainEstimate } from "../index.cjs";
2
+ import { Pool, PoolOptions } from "mysql2/promise";
3
+ //#region src/mysql/index.d.ts
4
+ /** Connection settings — any `mysql2` `PoolOptions`. */
5
+ type MysqlConfig = PoolOptions;
6
+ /** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */
7
+ declare function parseMysqlPlan(raw: string): ExplainEstimate;
8
+ /**
9
+ * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
10
+ * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
11
+ */
12
+ declare function createMysqlExecutorFromPool(pool: Pool): DbExecutor;
13
+ /**
14
+ * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
15
+ * `{ sql, params }` a `MysqlQuery` builder emits.
16
+ */
17
+ declare function createMysqlExecutor(config: MysqlConfig): DbExecutor;
18
+ //#endregion
19
+ export { MysqlConfig, createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
20
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,20 @@
1
+ import { DbExecutor, ExplainEstimate } from "../index.mjs";
2
+ import { Pool, PoolOptions } from "mysql2/promise";
3
+ //#region src/mysql/index.d.ts
4
+ /** Connection settings — any `mysql2` `PoolOptions`. */
5
+ type MysqlConfig = PoolOptions;
6
+ /** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */
7
+ declare function parseMysqlPlan(raw: string): ExplainEstimate;
8
+ /**
9
+ * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
10
+ * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
11
+ */
12
+ declare function createMysqlExecutorFromPool(pool: Pool): DbExecutor;
13
+ /**
14
+ * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
15
+ * `{ sql, params }` a `MysqlQuery` builder emits.
16
+ */
17
+ declare function createMysqlExecutor(config: MysqlConfig): DbExecutor;
18
+ //#endregion
19
+ export { MysqlConfig, createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
20
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,87 @@
1
+ import { createPool } from "mysql2/promise";
2
+ //#region src/mysql/index.ts
3
+ /** Every `table` node in the plan, however deeply the operation wrappers nest it. */
4
+ function tablesOf(block) {
5
+ if (!block) return [];
6
+ return [
7
+ ...block.table ? [block.table] : [],
8
+ ...(block.nested_loop ?? []).flatMap(tablesOf),
9
+ ...tablesOf(block.ordering_operation),
10
+ ...tablesOf(block.grouping_operation),
11
+ ...tablesOf(block.duplicates_removal)
12
+ ];
13
+ }
14
+ /** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */
15
+ function parseMysqlPlan(raw) {
16
+ let plan = {};
17
+ try {
18
+ plan = JSON.parse(raw || "{}");
19
+ } catch {}
20
+ const cost = Number(plan.query_block?.cost_info?.query_cost);
21
+ const tables = tablesOf(plan.query_block);
22
+ return {
23
+ cost: Number.isFinite(cost) ? cost : void 0,
24
+ rows: tables[0]?.rows_examined_per_scan,
25
+ fullScan: tables.some((t) => t.access_type === "ALL"),
26
+ plan: (raw ?? "").slice(0, 500)
27
+ };
28
+ }
29
+ const argsOf = (prepared) => prepared.params ?? [];
30
+ const toResult = (result) => {
31
+ if (Array.isArray(result)) return {
32
+ rows: result,
33
+ rowCount: result.length
34
+ };
35
+ return {
36
+ rows: [],
37
+ rowCount: result.affectedRows ?? 0
38
+ };
39
+ };
40
+ /**
41
+ * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
42
+ * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
43
+ */
44
+ function createMysqlExecutorFromPool(pool) {
45
+ return {
46
+ async run(prepared) {
47
+ const [result] = await pool.query(prepared.sql, argsOf(prepared));
48
+ return toResult(result);
49
+ },
50
+ async transaction(statements) {
51
+ const conn = await pool.getConnection();
52
+ try {
53
+ await conn.beginTransaction();
54
+ const results = [];
55
+ for (const s of statements) {
56
+ const [result] = await conn.query(s.sql, argsOf(s));
57
+ results.push(toResult(result));
58
+ }
59
+ await conn.commit();
60
+ return results;
61
+ } catch (err) {
62
+ await conn.rollback().catch(() => {});
63
+ throw err;
64
+ } finally {
65
+ conn.release();
66
+ }
67
+ },
68
+ async explain(prepared) {
69
+ const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));
70
+ return parseMysqlPlan((Array.isArray(rows) ? rows[0]?.EXPLAIN : "") ?? "");
71
+ },
72
+ async close() {
73
+ await pool.end();
74
+ }
75
+ };
76
+ }
77
+ /**
78
+ * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
79
+ * `{ sql, params }` a `MysqlQuery` builder emits.
80
+ */
81
+ function createMysqlExecutor(config) {
82
+ return createMysqlExecutorFromPool(createPool(config));
83
+ }
84
+ //#endregion
85
+ export { createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
86
+
87
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import { createPool, type Pool, type PoolOptions, type ResultSetHeader } from 'mysql2/promise';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `mysql2` `PoolOptions`. */\nexport type MysqlConfig = PoolOptions;\n\n/** `EXPLAIN FORMAT=JSON` shape — only the bits we read. A `table` node is not always a direct child\n * of `query_block`: a JOIN nests it under `nested_loop[]`, ORDER BY under `ordering_operation`, etc. */\ntype MysqlTable = { access_type?: string; rows_examined_per_scan?: number };\ntype MysqlBlock = {\n cost_info?: { query_cost?: string };\n table?: MysqlTable;\n nested_loop?: MysqlBlock[];\n ordering_operation?: MysqlBlock;\n grouping_operation?: MysqlBlock;\n duplicates_removal?: MysqlBlock;\n};\ntype MysqlPlan = { query_block?: MysqlBlock };\n\n/** Every `table` node in the plan, however deeply the operation wrappers nest it. */\nfunction tablesOf(block: MysqlBlock | undefined): MysqlTable[] {\n if (!block) return [];\n return [\n ...(block.table ? [block.table] : []),\n ...(block.nested_loop ?? []).flatMap(tablesOf),\n ...tablesOf(block.ordering_operation),\n ...tablesOf(block.grouping_operation),\n ...tablesOf(block.duplicates_removal),\n ];\n}\n\n/** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */\nexport function parseMysqlPlan(raw: string): ExplainEstimate {\n let plan: MysqlPlan = {};\n try {\n plan = JSON.parse(raw || '{}') as MysqlPlan;\n } catch {\n // Unparseable plan — an empty estimate beats throwing.\n }\n const cost = Number(plan.query_block?.cost_info?.query_cost);\n const tables = tablesOf(plan.query_block);\n return {\n cost: Number.isFinite(cost) ? cost : undefined,\n // The driving (first) table's scanned rows.\n rows: tables[0]?.rows_examined_per_scan,\n // `ALL` is MySQL's full-table-scan access type — a scan ANYWHERE in the plan counts.\n fullScan: tables.some((t) => t.access_type === 'ALL'),\n plan: (raw ?? '').slice(0, 500),\n };\n}\n\nconst argsOf = (prepared: PreparedSql): unknown[] => (prepared.params ?? []) as unknown[];\n\n// mysql2's query() resolves to `[rows | ResultSetHeader, fields]`. SELECT → an array of rows; a\n// write → a ResultSetHeader carrying affectedRows.\nconst toResult = <T>(result: unknown): QueryResult<T> => {\n if (Array.isArray(result)) return { rows: result as T[], rowCount: result.length };\n return { rows: [], rowCount: (result as ResultSetHeader).affectedRows ?? 0 };\n};\n\n/**\n * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across\n * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.\n */\nexport function createMysqlExecutorFromPool(pool: Pool): DbExecutor {\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const [result] = await pool.query(prepared.sql, argsOf(prepared));\n return toResult<T>(result);\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // Driver-level transaction on a single pinned connection — beginTransaction/commit/rollback\n // speak the protocol correctly (no `multipleStatements`, no concatenation). Each statement\n // runs on its own; ROLLBACK on any error; release the connection no matter what.\n const conn = await pool.getConnection();\n try {\n await conn.beginTransaction();\n const results: QueryResult[] = [];\n for (const s of statements) {\n const [result] = await conn.query(s.sql, argsOf(s));\n results.push(toResult(result));\n }\n await conn.commit();\n return results;\n } catch (err) {\n await conn.rollback().catch(() => {});\n throw err;\n } finally {\n conn.release();\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // EXPLAIN never executes the statement; FORMAT=JSON is the only form carrying a cost estimate.\n const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));\n const raw = Array.isArray(rows) ? (rows[0] as { EXPLAIN?: string } | undefined)?.EXPLAIN : '';\n return parseMysqlPlan(raw ?? '');\n },\n\n async close(): Promise<void> {\n await pool.end();\n },\n };\n}\n\n/**\n * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the\n * `{ sql, params }` a `MysqlQuery` builder emits.\n */\nexport function createMysqlExecutor(config: MysqlConfig): DbExecutor {\n return createMysqlExecutorFromPool(createPool(config));\n}\n"],"mappings":";;;AAoBA,SAAS,SAAS,OAA6C;CAC7D,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,OAAO;EACL,GAAI,MAAM,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC;EACnC,IAAI,MAAM,eAAe,CAAC,EAAA,CAAG,QAAQ,QAAQ;EAC7C,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;CACtC;AACF;;AAGA,SAAgB,eAAe,KAA8B;CAC3D,IAAI,OAAkB,CAAC;CACvB,IAAI;EACF,OAAO,KAAK,MAAM,OAAO,IAAI;CAC/B,QAAQ,CAER;CACA,MAAM,OAAO,OAAO,KAAK,aAAa,WAAW,UAAU;CAC3D,MAAM,SAAS,SAAS,KAAK,WAAW;CACxC,OAAO;EACL,MAAM,OAAO,SAAS,IAAI,IAAI,OAAO,KAAA;EAErC,MAAM,OAAO,EAAE,EAAE;EAEjB,UAAU,OAAO,MAAM,MAAM,EAAE,gBAAgB,KAAK;EACpD,OAAO,OAAO,GAAA,CAAI,MAAM,GAAG,GAAG;CAChC;AACF;AAEA,MAAM,UAAU,aAAsC,SAAS,UAAU,CAAC;AAI1E,MAAM,YAAe,WAAoC;CACvD,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO;EAAE,MAAM;EAAe,UAAU,OAAO;CAAO;CACjF,OAAO;EAAE,MAAM,CAAC;EAAG,UAAW,OAA2B,gBAAgB;CAAE;AAC7E;;;;;AAMA,SAAgB,4BAA4B,MAAwB;CAClE,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;GAChE,OAAO,SAAY,MAAM;EAC3B;EAEA,MAAM,YAAY,YAA4D;GAI5E,MAAM,OAAO,MAAM,KAAK,cAAc;GACtC,IAAI;IACF,MAAM,KAAK,iBAAiB;IAC5B,MAAM,UAAyB,CAAC;IAChC,KAAK,MAAM,KAAK,YAAY;KAC1B,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;KAClD,QAAQ,KAAK,SAAS,MAAM,CAAC;IAC/B;IACA,MAAM,KAAK,OAAO;IAClB,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,KAAK,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM;GACR,UAAU;IACR,KAAK,QAAQ;GACf;EACF;EAEA,MAAM,QAAQ,UAAiD;GAE7D,MAAM,CAAC,QAAQ,MAAM,KAAK,MAAM,uBAAuB,SAAS,OAAO,OAAO,QAAQ,CAAC;GAEvF,OAAO,gBADK,MAAM,QAAQ,IAAI,IAAK,KAAK,EAAE,EAAuC,UAAU,OAC9D,EAAE;EACjC;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;AAMA,SAAgB,oBAAoB,QAAiC;CACnE,OAAO,4BAA4B,WAAW,MAAM,CAAC;AACvD"}
@@ -0,0 +1,64 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let pg = require("pg");
3
+ //#region src/postgres/index.ts
4
+ /** Parse `EXPLAIN (FORMAT JSON)` result rows into the normalized estimate. Exported for unit tests. */
5
+ function parsePgPlan(rows) {
6
+ const root = rows[0]?.["QUERY PLAN"]?.[0]?.Plan;
7
+ const seqScan = (n) => !!n && (n["Node Type"] === "Seq Scan" || (n.Plans ?? []).some(seqScan));
8
+ return {
9
+ cost: root?.["Total Cost"],
10
+ rows: root?.["Plan Rows"],
11
+ fullScan: seqScan(root),
12
+ plan: JSON.stringify(root ?? {}).slice(0, 500)
13
+ };
14
+ }
15
+ const argsOf = (prepared) => prepared.params ?? [];
16
+ const toResult = (res) => ({
17
+ rows: res.rows,
18
+ rowCount: res.rowCount ?? res.rows.length
19
+ });
20
+ /**
21
+ * Build a Postgres executor over an EXISTING pool — bring your own `pg` Pool to share one pool
22
+ * across your app (or hand in a test double). {@link createPostgresExecutor} is the usual entry.
23
+ */
24
+ function createPostgresExecutorFromPool(pool) {
25
+ return {
26
+ async run(prepared) {
27
+ return toResult(await pool.query(prepared.sql, argsOf(prepared)));
28
+ },
29
+ async transaction(statements) {
30
+ const client = await pool.connect();
31
+ try {
32
+ await client.query("BEGIN");
33
+ const results = [];
34
+ for (const s of statements) results.push(toResult(await client.query(s.sql, argsOf(s))));
35
+ await client.query("COMMIT");
36
+ return results;
37
+ } catch (err) {
38
+ await client.query("ROLLBACK").catch(() => {});
39
+ throw err;
40
+ } finally {
41
+ client.release();
42
+ }
43
+ },
44
+ async explain(prepared) {
45
+ return parsePgPlan((await pool.query(`EXPLAIN (FORMAT JSON) ${prepared.sql}`, argsOf(prepared))).rows);
46
+ },
47
+ async close() {
48
+ await pool.end();
49
+ }
50
+ };
51
+ }
52
+ /**
53
+ * A Postgres executor backed by a `pg` connection pool (placeholders: `$1`, `$2`, …). Feed it the
54
+ * `{ sql, params }` a `PostgresQuery` builder emits.
55
+ */
56
+ function createPostgresExecutor(config) {
57
+ return createPostgresExecutorFromPool(new pg.Pool(config));
58
+ }
59
+ //#endregion
60
+ exports.createPostgresExecutor = createPostgresExecutor;
61
+ exports.createPostgresExecutorFromPool = createPostgresExecutorFromPool;
62
+ exports.parsePgPlan = parsePgPlan;
63
+
64
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["Pool"],"sources":["../../src/postgres/index.ts"],"sourcesContent":["import { Pool, type PoolConfig } from 'pg';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `pg` `PoolConfig`. Set `statement_timeout` here if you want a\n * per-connection ceiling; the engine imposes none of its own. */\nexport type PostgresConfig = PoolConfig;\n\n/** The root of `EXPLAIN (FORMAT JSON)`: cost/rows sit on the top plan; a `Seq Scan` anywhere in the\n * tree is the full-scan signal. */\ntype PgPlanNode = {\n 'Node Type'?: string;\n 'Total Cost'?: number;\n 'Plan Rows'?: number;\n Plans?: PgPlanNode[];\n};\n\n/** Parse `EXPLAIN (FORMAT JSON)` result rows into the normalized estimate. Exported for unit tests. */\nexport function parsePgPlan(rows: readonly unknown[]): ExplainEstimate {\n const root = (rows[0] as { 'QUERY PLAN'?: { Plan?: PgPlanNode }[] } | undefined)?.[\n 'QUERY PLAN'\n ]?.[0]?.Plan;\n const seqScan = (n: PgPlanNode | undefined): boolean =>\n !!n && (n['Node Type'] === 'Seq Scan' || (n.Plans ?? []).some(seqScan));\n return {\n cost: root?.['Total Cost'],\n rows: root?.['Plan Rows'],\n fullScan: seqScan(root),\n plan: JSON.stringify(root ?? {}).slice(0, 500),\n };\n}\n\n// The slice of `pg`'s result the executor reads. `pg`'s QueryResult is structurally assignable.\ntype PgResultLike = { rows: unknown[]; rowCount: number | null };\n\nconst argsOf = (prepared: PreparedSql): unknown[] => (prepared.params ?? []) as unknown[];\n\nconst toResult = <T>(res: PgResultLike): QueryResult<T> => ({\n rows: res.rows as T[],\n rowCount: res.rowCount ?? res.rows.length,\n});\n\n/**\n * Build a Postgres executor over an EXISTING pool — bring your own `pg` Pool to share one pool\n * across your app (or hand in a test double). {@link createPostgresExecutor} is the usual entry.\n */\nexport function createPostgresExecutorFromPool(pool: Pool): DbExecutor {\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n return toResult<T>(await pool.query(prepared.sql, argsOf(prepared)));\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // Postgres's extended protocol runs exactly ONE statement per parameterized query() — so a\n // batch is NEVER concatenated into one string (its placeholders restart per statement and\n // would misbind). Each statement runs on its own, inside a single checked-out connection so\n // BEGIN/COMMIT actually wrap them. ROLLBACK on any error; release the client no matter what.\n const client = await pool.connect();\n try {\n await client.query('BEGIN');\n const results: QueryResult[] = [];\n for (const s of statements) {\n results.push(toResult(await client.query(s.sql, argsOf(s))));\n }\n await client.query('COMMIT');\n return results;\n } catch (err) {\n await client.query('ROLLBACK').catch(() => {});\n throw err;\n } finally {\n client.release();\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // Plain EXPLAIN never executes the statement (only EXPLAIN ANALYZE does).\n const res = await pool.query(`EXPLAIN (FORMAT JSON) ${prepared.sql}`, argsOf(prepared));\n return parsePgPlan(res.rows);\n },\n\n async close(): Promise<void> {\n await pool.end();\n },\n };\n}\n\n/**\n * A Postgres executor backed by a `pg` connection pool (placeholders: `$1`, `$2`, …). Feed it the\n * `{ sql, params }` a `PostgresQuery` builder emits.\n */\nexport function createPostgresExecutor(config: PostgresConfig): DbExecutor {\n return createPostgresExecutorFromPool(new Pool(config));\n}\n"],"mappings":";;;;AAiBA,SAAgB,YAAY,MAA2C;CACrE,MAAM,OAAQ,KAAK,EAAE,GACnB,aACD,GAAG,EAAE,EAAE;CACR,MAAM,WAAW,MACf,CAAC,CAAC,MAAM,EAAE,iBAAiB,eAAe,EAAE,SAAS,CAAC,EAAA,CAAG,KAAK,OAAO;CACvE,OAAO;EACL,MAAM,OAAO;EACb,MAAM,OAAO;EACb,UAAU,QAAQ,IAAI;EACtB,MAAM,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,GAAG;CAC/C;AACF;AAKA,MAAM,UAAU,aAAsC,SAAS,UAAU,CAAC;AAE1E,MAAM,YAAe,SAAuC;CAC1D,MAAM,IAAI;CACV,UAAU,IAAI,YAAY,IAAI,KAAK;AACrC;;;;;AAMA,SAAgB,+BAA+B,MAAwB;CACrE,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,OAAO,SAAY,MAAM,KAAK,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC,CAAC;EACrE;EAEA,MAAM,YAAY,YAA4D;GAK5E,MAAM,SAAS,MAAM,KAAK,QAAQ;GAClC,IAAI;IACF,MAAM,OAAO,MAAM,OAAO;IAC1B,MAAM,UAAyB,CAAC;IAChC,KAAK,MAAM,KAAK,YACd,QAAQ,KAAK,SAAS,MAAM,OAAO,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAE7D,MAAM,OAAO,MAAM,QAAQ;IAC3B,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,OAAO,MAAM,UAAU,CAAC,CAAC,YAAY,CAAC,CAAC;IAC7C,MAAM;GACR,UAAU;IACR,OAAO,QAAQ;GACjB;EACF;EAEA,MAAM,QAAQ,UAAiD;GAG7D,OAAO,aAAY,MADD,KAAK,MAAM,yBAAyB,SAAS,OAAO,OAAO,QAAQ,CAAC,EAAA,CAC/D,IAAI;EAC7B;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;AAMA,SAAgB,uBAAuB,QAAoC;CACzE,OAAO,+BAA+B,IAAIA,GAAAA,KAAK,MAAM,CAAC;AACxD"}
@@ -0,0 +1,21 @@
1
+ import { DbExecutor, ExplainEstimate } from "../index.cjs";
2
+ import { Pool, PoolConfig } from "pg";
3
+ //#region src/postgres/index.d.ts
4
+ /** Connection settings — any `pg` `PoolConfig`. Set `statement_timeout` here if you want a
5
+ * per-connection ceiling; the engine imposes none of its own. */
6
+ type PostgresConfig = PoolConfig;
7
+ /** Parse `EXPLAIN (FORMAT JSON)` result rows into the normalized estimate. Exported for unit tests. */
8
+ declare function parsePgPlan(rows: readonly unknown[]): ExplainEstimate;
9
+ /**
10
+ * Build a Postgres executor over an EXISTING pool — bring your own `pg` Pool to share one pool
11
+ * across your app (or hand in a test double). {@link createPostgresExecutor} is the usual entry.
12
+ */
13
+ declare function createPostgresExecutorFromPool(pool: Pool): DbExecutor;
14
+ /**
15
+ * A Postgres executor backed by a `pg` connection pool (placeholders: `$1`, `$2`, …). Feed it the
16
+ * `{ sql, params }` a `PostgresQuery` builder emits.
17
+ */
18
+ declare function createPostgresExecutor(config: PostgresConfig): DbExecutor;
19
+ //#endregion
20
+ export { PostgresConfig, createPostgresExecutor, createPostgresExecutorFromPool, parsePgPlan };
21
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,21 @@
1
+ import { DbExecutor, ExplainEstimate } from "../index.mjs";
2
+ import { Pool, PoolConfig } from "pg";
3
+ //#region src/postgres/index.d.ts
4
+ /** Connection settings — any `pg` `PoolConfig`. Set `statement_timeout` here if you want a
5
+ * per-connection ceiling; the engine imposes none of its own. */
6
+ type PostgresConfig = PoolConfig;
7
+ /** Parse `EXPLAIN (FORMAT JSON)` result rows into the normalized estimate. Exported for unit tests. */
8
+ declare function parsePgPlan(rows: readonly unknown[]): ExplainEstimate;
9
+ /**
10
+ * Build a Postgres executor over an EXISTING pool — bring your own `pg` Pool to share one pool
11
+ * across your app (or hand in a test double). {@link createPostgresExecutor} is the usual entry.
12
+ */
13
+ declare function createPostgresExecutorFromPool(pool: Pool): DbExecutor;
14
+ /**
15
+ * A Postgres executor backed by a `pg` connection pool (placeholders: `$1`, `$2`, …). Feed it the
16
+ * `{ sql, params }` a `PostgresQuery` builder emits.
17
+ */
18
+ declare function createPostgresExecutor(config: PostgresConfig): DbExecutor;
19
+ //#endregion
20
+ export { PostgresConfig, createPostgresExecutor, createPostgresExecutorFromPool, parsePgPlan };
21
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,61 @@
1
+ import { Pool } from "pg";
2
+ //#region src/postgres/index.ts
3
+ /** Parse `EXPLAIN (FORMAT JSON)` result rows into the normalized estimate. Exported for unit tests. */
4
+ function parsePgPlan(rows) {
5
+ const root = rows[0]?.["QUERY PLAN"]?.[0]?.Plan;
6
+ const seqScan = (n) => !!n && (n["Node Type"] === "Seq Scan" || (n.Plans ?? []).some(seqScan));
7
+ return {
8
+ cost: root?.["Total Cost"],
9
+ rows: root?.["Plan Rows"],
10
+ fullScan: seqScan(root),
11
+ plan: JSON.stringify(root ?? {}).slice(0, 500)
12
+ };
13
+ }
14
+ const argsOf = (prepared) => prepared.params ?? [];
15
+ const toResult = (res) => ({
16
+ rows: res.rows,
17
+ rowCount: res.rowCount ?? res.rows.length
18
+ });
19
+ /**
20
+ * Build a Postgres executor over an EXISTING pool — bring your own `pg` Pool to share one pool
21
+ * across your app (or hand in a test double). {@link createPostgresExecutor} is the usual entry.
22
+ */
23
+ function createPostgresExecutorFromPool(pool) {
24
+ return {
25
+ async run(prepared) {
26
+ return toResult(await pool.query(prepared.sql, argsOf(prepared)));
27
+ },
28
+ async transaction(statements) {
29
+ const client = await pool.connect();
30
+ try {
31
+ await client.query("BEGIN");
32
+ const results = [];
33
+ for (const s of statements) results.push(toResult(await client.query(s.sql, argsOf(s))));
34
+ await client.query("COMMIT");
35
+ return results;
36
+ } catch (err) {
37
+ await client.query("ROLLBACK").catch(() => {});
38
+ throw err;
39
+ } finally {
40
+ client.release();
41
+ }
42
+ },
43
+ async explain(prepared) {
44
+ return parsePgPlan((await pool.query(`EXPLAIN (FORMAT JSON) ${prepared.sql}`, argsOf(prepared))).rows);
45
+ },
46
+ async close() {
47
+ await pool.end();
48
+ }
49
+ };
50
+ }
51
+ /**
52
+ * A Postgres executor backed by a `pg` connection pool (placeholders: `$1`, `$2`, …). Feed it the
53
+ * `{ sql, params }` a `PostgresQuery` builder emits.
54
+ */
55
+ function createPostgresExecutor(config) {
56
+ return createPostgresExecutorFromPool(new Pool(config));
57
+ }
58
+ //#endregion
59
+ export { createPostgresExecutor, createPostgresExecutorFromPool, parsePgPlan };
60
+
61
+ //# sourceMappingURL=index.mjs.map