@deebeetech/sqleasy-engine 1.0.0 → 1.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/mssql/index.ts"],"sourcesContent":["// `mssql` is CommonJS and Node's ESM loader can't see its named exports — import the default and\n// destructure. (pg/mysql2/@libsql expose named exports fine.)\nimport mssql from 'mssql';\nimport type { config as MssqlDriverConfig, IResult } from 'mssql';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\nconst { ConnectionPool, Transaction, Request } = mssql;\n\n/** Connection settings — any `mssql` config object, or a raw connection string. */\nexport type MssqlConfig = MssqlDriverConfig | { connectionString: string };\n\n/** Read the T-SQL string literal starting at `from` (just past the opening quote), honouring `''`\n * escapes. Returns the unescaped text and the index just past the closing quote. */\nfunction readLiteral(sql: string, from: number): { text: string; end: number } | undefined {\n let out = '';\n for (let i = from; i < sql.length; i++) {\n if (sql[i] !== \"'\") {\n out += sql[i];\n continue;\n }\n if (sql[i + 1] === \"'\") {\n out += \"'\";\n i++;\n continue;\n }\n return { text: out, end: i + 1 };\n }\n return undefined; // unterminated — caller falls back to the raw batch\n}\n\n/**\n * Turn the mssql dialect's output into something SHOWPLAN can actually cost.\n *\n * It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does\n * NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement\n * must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared\n * outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost\n * estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.\n */\nexport function toExplainableBatch(sql: string): string {\n const m = /exec\\s+sp_executesql\\s+N'/i.exec(sql);\n if (!m) return sql;\n const inner = readLiteral(sql, m.index + m[0].length);\n if (!inner) return sql;\n const decl = /^\\s*,\\s*N'/.exec(sql.slice(inner.end));\n const decls = decl ? readLiteral(sql, inner.end + decl[0].length)?.text.trim() : '';\n return decls ? `DECLARE ${decls};\\n${inner.text}` : inner.text;\n}\n\n/** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */\nexport function parsePlanXml(xml: string): ExplainEstimate {\n // A batch holds one <StmtSimple> per statement (the injected DECLARE, SET NOCOUNT ON, the SELECT).\n // Take the most expensive — never blindly the first, which is usually a costless preamble.\n let best: { cost: number; rows?: number } | undefined;\n for (const [tag] of xml.matchAll(/<StmtSimple\\b[^>]*>/g)) {\n const cost = Number(/StatementSubTreeCost=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n if (!Number.isFinite(cost) || (best && cost <= best.cost)) continue;\n const rows = Number(/StatementEstRows=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n best = { cost, rows: Number.isFinite(rows) ? rows : undefined };\n }\n return {\n cost: best?.cost,\n rows: best?.rows,\n fullScan: /PhysicalOp=\"(?:Table Scan|Clustered Index Scan|Index Scan)\"/.test(xml),\n plan: xml.slice(0, 500),\n };\n}\n\nconst toResult = <T>(result: IResult<unknown>): QueryResult<T> => ({\n rows: (result.recordset ?? []) as unknown as T[],\n rowCount: result.recordset ? result.recordset.length : (result.rowsAffected?.[0] ?? 0),\n});\n\n// Bind params (if any) as @p0..@pN. SQLEasy's mssql dialect inlines its values into the\n// sp_executesql batch and passes `params: []`, so nothing binds on that path; a caller passing\n// bound values must reference @p0.. in their SQL (mssql has no positional `?`). No `?`→`@p`\n// rewriting — that scan corrupts a `?` inside a string literal.\ntype BindableRequest = { input(name: string, value: unknown): unknown };\nconst bindParams = <R extends BindableRequest>(request: R, params?: readonly unknown[]): R => {\n (params ?? []).forEach((value, i) => request.input(`p${i}`, value));\n return request;\n};\n\n/**\n * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained\n * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).\n */\nexport function createMssqlExecutor(config: MssqlConfig): DbExecutor {\n const makePool = () =>\n new ConnectionPool('connectionString' in config ? config.connectionString : config);\n let pool = makePool();\n\n // Single-flight connect that RECOVERS. Caching a rejected `pool.connect()` promise would brick the\n // pool forever (a DB restart / blip): every later query awaits the same settled rejection. So reset\n // the gate and rebuild the pool on failure, and the next call retries. (pg/mysql self-heal\n // per-acquire; mssql caches connect, so it alone needs this.)\n let ready: Promise<unknown> | undefined;\n const ensureReady = () =>\n (ready ??= pool.connect().catch((e: unknown) => {\n ready = undefined;\n const dead = pool;\n pool = makePool();\n void dead.close().catch(() => {});\n throw e;\n }));\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n await ensureReady();\n const request = bindParams(pool.request(), prepared.params);\n return toResult<T>(await request.query(prepared.sql));\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n await ensureReady();\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n const results: QueryResult[] = [];\n for (const s of statements) {\n const request = bindParams(new Request(tx), s.params);\n results.push(toResult(await request.query(s.sql)));\n }\n await tx.commit();\n return results;\n } catch (err) {\n await tx.rollback().catch(() => {});\n throw err;\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n await ensureReady();\n // SQL Server has no EXPLAIN. The estimated plan comes from SET SHOWPLAN_XML, which (a) must be\n // the ONLY statement in its batch and (b) is SESSION state — so the SET and the query must run\n // on the SAME connection. A transaction pins one connection for both; the finally block always\n // clears the flag and releases it, so SHOWPLAN never leaks onto a connection serving reads.\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n await new Request(tx).batch('SET SHOWPLAN_XML ON');\n const res = await new Request(tx).batch(toExplainableBatch(prepared.sql));\n const first = res.recordset?.[0] as Row | undefined;\n return parsePlanXml(String(first ? Object.values(first)[0] : ''));\n } finally {\n await new Request(tx).batch('SET SHOWPLAN_XML OFF').catch(() => {});\n await tx.rollback().catch(() => {});\n }\n },\n\n async close(): Promise<void> {\n await (ready?.catch(() => {}) ?? Promise.resolve());\n await pool.close();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/mssql/index.ts"],"sourcesContent":["// `mssql` is CommonJS and Node's ESM loader can't see its named exports — import the default and\n// destructure. (pg/mysql2/@libsql expose named exports fine.)\nimport mssql from 'mssql';\nimport type { config as MssqlDriverConfig, IResult } from 'mssql';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\nconst { ConnectionPool, Transaction, Request } = mssql;\n\n/** Connection settings — any `mssql` config object, or a raw connection string. */\nexport type MssqlConfig = MssqlDriverConfig | { connectionString: string };\n\n/** Read the T-SQL string literal starting at `from` (just past the opening quote), honouring `''`\n * escapes. Returns the unescaped text and the index just past the closing quote. */\nfunction readLiteral(sql: string, from: number): { text: string; end: number } | undefined {\n let out = '';\n for (let i = from; i < sql.length; i++) {\n if (sql[i] !== \"'\") {\n out += sql[i];\n continue;\n }\n if (sql[i + 1] === \"'\") {\n out += \"'\";\n i++;\n continue;\n }\n return { text: out, end: i + 1 };\n }\n return undefined; // unterminated — caller falls back to the raw batch\n}\n\n/**\n * Turn the mssql dialect's output into something SHOWPLAN can actually cost.\n *\n * It emits `SET NOCOUNT ON; exec sp_executesql N'<select>', N'<decls>'[, @p0 = …];`. SHOWPLAN does\n * NOT compile dynamic SQL, so explaining the EXEC yields a plan with no cost — the inner statement\n * must be lifted out. When it has parameters, that inner statement references `@p0`, undeclared\n * outside sp_executesql, so re-declare them. The assigned VALUES are dropped on purpose: a cost\n * estimate doesn't need them. Not a wrapped statement ⇒ returned unchanged.\n */\nexport function toExplainableBatch(sql: string): string {\n const m = /exec\\s+sp_executesql\\s+N'/i.exec(sql);\n if (!m) return sql;\n const inner = readLiteral(sql, m.index + m[0].length);\n if (!inner) return sql;\n const decl = /^\\s*,\\s*N'/.exec(sql.slice(inner.end));\n const decls = decl ? readLiteral(sql, inner.end + decl[0].length)?.text.trim() : '';\n return decls ? `DECLARE ${decls};\\n${inner.text}` : inner.text;\n}\n\n/** Parse a SHOWPLAN_XML document into the normalized estimate. Exported for unit tests. */\nexport function parsePlanXml(xml: string): ExplainEstimate {\n // A batch holds one <StmtSimple> per statement (the injected DECLARE, SET NOCOUNT ON, the SELECT).\n // Take the most expensive — never blindly the first, which is usually a costless preamble.\n let best: { cost: number; rows?: number } | undefined;\n for (const [tag] of xml.matchAll(/<StmtSimple\\b[^>]*>/g)) {\n const cost = Number(/StatementSubTreeCost=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n if (!Number.isFinite(cost) || (best && cost <= best.cost)) continue;\n const rows = Number(/StatementEstRows=\"([\\d.eE+-]+)\"/.exec(tag)?.[1]);\n best = { cost, rows: Number.isFinite(rows) ? rows : undefined };\n }\n return {\n cost: best?.cost,\n rows: best?.rows,\n fullScan: /PhysicalOp=\"(?:Table Scan|Clustered Index Scan|Index Scan)\"/.test(xml),\n plan: xml.slice(0, 500),\n };\n}\n\nconst toResult = <T>(result: IResult<unknown>): QueryResult<T> => ({\n rows: (result.recordset ?? []) as unknown as T[],\n rowCount: result.recordset ? result.recordset.length : (result.rowsAffected?.[0] ?? 0),\n});\n\n// Bind params (if any) as @p0..@pN. SQLEasy's mssql dialect inlines its values into the\n// sp_executesql batch and passes `params: []`, so nothing binds on that path; a caller passing bound\n// values must reference @p0.. in their SQL (mssql has no positional `?`). No `?`→`@p` rewriting —\n// that scan corrupts a `?` inside a string literal. query() re-wraps its argument in sp_executesql,\n// which is harmless for both a plain statement and a pre-formed batch (verified against real SQL\n// Server: two sp_executesql inserts in a transaction commit both).\ntype BindableRequest = { input(name: string, value: unknown): unknown };\nconst bindParams = <R extends BindableRequest>(request: R, params?: readonly unknown[]): R => {\n (params ?? []).forEach((value, i) => request.input(`p${i}`, value));\n return request;\n};\n\n/**\n * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained\n * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).\n */\nexport function createMssqlExecutor(config: MssqlConfig): DbExecutor {\n const makePool = () =>\n new ConnectionPool('connectionString' in config ? config.connectionString : config);\n let pool = makePool();\n\n // Single-flight connect that RECOVERS. Caching a rejected `pool.connect()` promise would brick the\n // pool forever (a DB restart / blip): every later query awaits the same settled rejection. So reset\n // the gate and rebuild the pool on failure, and the next call retries. (pg/mysql self-heal\n // per-acquire; mssql caches connect, so it alone needs this.)\n let ready: Promise<unknown> | undefined;\n const ensureReady = () =>\n (ready ??= pool.connect().catch((e: unknown) => {\n ready = undefined;\n const dead = pool;\n pool = makePool();\n void dead.close().catch(() => {});\n throw e;\n }));\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n await ensureReady();\n const request = bindParams(pool.request(), prepared.params);\n return toResult<T>(await request.query(prepared.sql));\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n await ensureReady();\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n const results: QueryResult[] = [];\n for (const s of statements) {\n const request = bindParams(new Request(tx), s.params);\n results.push(toResult(await request.query(s.sql)));\n }\n await tx.commit();\n return results;\n } catch (err) {\n await tx.rollback().catch(() => {});\n throw err;\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n await ensureReady();\n // SQL Server has no EXPLAIN. The estimated plan comes from SET SHOWPLAN_XML, which (a) must be\n // the ONLY statement in its batch and (b) is SESSION state — so the SET and the query must run\n // on the SAME connection. A transaction pins one connection for both; the finally block always\n // clears the flag and releases it, so SHOWPLAN never leaks onto a connection serving reads.\n const tx = new Transaction(pool);\n await tx.begin();\n try {\n await new Request(tx).batch('SET SHOWPLAN_XML ON');\n const res = await new Request(tx).batch(toExplainableBatch(prepared.sql));\n const first = res.recordset?.[0] as Row | undefined;\n return parsePlanXml(String(first ? Object.values(first)[0] : ''));\n } finally {\n await new Request(tx).batch('SET SHOWPLAN_XML OFF').catch(() => {});\n await tx.rollback().catch(() => {});\n }\n },\n\n async close(): Promise<void> {\n await (ready?.catch(() => {}) ?? Promise.resolve());\n await pool.close();\n },\n };\n}\n"],"mappings":"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"}
@@ -1 +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":";;AAMA,MAAM,EAAE,gBAAgB,aAAa,YAAY;;;AAOjD,SAAS,YAAY,KAAa,MAAyD;CACzF,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,MAAM,IAAI,IAAI,QAAQ,KAAK;EACtC,IAAI,IAAI,OAAO,KAAK;GAClB,OAAO,IAAI;GACX;EACF;EACA,IAAI,IAAI,IAAI,OAAO,KAAK;GACtB,OAAO;GACP;GACA;EACF;EACA,OAAO;GAAE,MAAM;GAAK,KAAK,IAAI;EAAE;CACjC;AAEF;;;;;;;;;;AAWA,SAAgB,mBAAmB,KAAqB;CACtD,MAAM,IAAI,6BAA6B,KAAK,GAAG;CAC/C,IAAI,CAAC,GAAG,OAAO;CACf,MAAM,QAAQ,YAAY,KAAK,EAAE,QAAQ,EAAE,EAAE,CAAC,MAAM;CACpD,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,OAAO,aAAa,KAAK,IAAI,MAAM,MAAM,GAAG,CAAC;CACnD,MAAM,QAAQ,OAAO,YAAY,KAAK,MAAM,MAAM,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,KAAK,IAAI;CACjF,OAAO,QAAQ,WAAW,MAAM,KAAK,MAAM,SAAS,MAAM;AAC5D;;AAGA,SAAgB,aAAa,KAA8B;CAGzD,IAAI;CACJ,KAAK,MAAM,CAAC,QAAQ,IAAI,SAAS,sBAAsB,GAAG;EACxD,MAAM,OAAO,OAAO,sCAAsC,KAAK,GAAG,CAAC,GAAG,EAAE;EACxE,IAAI,CAAC,OAAO,SAAS,IAAI,KAAM,QAAQ,QAAQ,KAAK,MAAO;EAC3D,MAAM,OAAO,OAAO,kCAAkC,KAAK,GAAG,CAAC,GAAG,EAAE;EACpE,OAAO;GAAE;GAAM,MAAM,OAAO,SAAS,IAAI,IAAI,OAAO,KAAA;EAAU;CAChE;CACA,OAAO;EACL,MAAM,MAAM;EACZ,MAAM,MAAM;EACZ,UAAU,8DAA8D,KAAK,GAAG;EAChF,MAAM,IAAI,MAAM,GAAG,GAAG;CACxB;AACF;AAEA,MAAM,YAAe,YAA8C;CACjE,MAAO,OAAO,aAAa,CAAC;CAC5B,UAAU,OAAO,YAAY,OAAO,UAAU,SAAU,OAAO,eAAe,MAAM;AACtF;AAOA,MAAM,cAAyC,SAAY,WAAmC;CAC5F,CAAC,UAAU,CAAC,EAAA,CAAG,SAAS,OAAO,MAAM,QAAQ,MAAM,IAAI,KAAK,KAAK,CAAC;CAClE,OAAO;AACT;;;;;AAMA,SAAgB,oBAAoB,QAAiC;CACnE,MAAM,iBACJ,IAAI,eAAe,sBAAsB,SAAS,OAAO,mBAAmB,MAAM;CACpF,IAAI,OAAO,SAAS;CAMpB,IAAI;CACJ,MAAM,oBACH,UAAU,KAAK,QAAQ,CAAC,CAAC,OAAO,MAAe;EAC9C,QAAQ,KAAA;EACR,MAAM,OAAO;EACb,OAAO,SAAS;EAChB,KAAU,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;EAChC,MAAM;CACR,CAAC;CAEH,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,YAAY;GAClB,MAAM,UAAU,WAAW,KAAK,QAAQ,GAAG,SAAS,MAAM;GAC1D,OAAO,SAAY,MAAM,QAAQ,MAAM,SAAS,GAAG,CAAC;EACtD;EAEA,MAAM,YAAY,YAA4D;GAC5E,MAAM,YAAY;GAClB,MAAM,KAAK,IAAI,YAAY,IAAI;GAC/B,MAAM,GAAG,MAAM;GACf,IAAI;IACF,MAAM,UAAyB,CAAC;IAChC,KAAK,MAAM,KAAK,YAAY;KAC1B,MAAM,UAAU,WAAW,IAAI,QAAQ,EAAE,GAAG,EAAE,MAAM;KACpD,QAAQ,KAAK,SAAS,MAAM,QAAQ,MAAM,EAAE,GAAG,CAAC,CAAC;IACnD;IACA,MAAM,GAAG,OAAO;IAChB,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,GAAG,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;IAClC,MAAM;GACR;EACF;EAEA,MAAM,QAAQ,UAAiD;GAC7D,MAAM,YAAY;GAKlB,MAAM,KAAK,IAAI,YAAY,IAAI;GAC/B,MAAM,GAAG,MAAM;GACf,IAAI;IACF,MAAM,IAAI,QAAQ,EAAE,CAAC,CAAC,MAAM,qBAAqB;IAEjD,MAAM,SAAQ,MADI,IAAI,QAAQ,EAAE,CAAC,CAAC,MAAM,mBAAmB,SAAS,GAAG,CAAC,EAAA,CACtD,YAAY;IAC9B,OAAO,aAAa,OAAO,QAAQ,OAAO,OAAO,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC;GAClE,UAAU;IACR,MAAM,IAAI,QAAQ,EAAE,CAAC,CAAC,MAAM,sBAAsB,CAAC,CAAC,YAAY,CAAC,CAAC;IAClE,MAAM,GAAG,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;GACpC;EACF;EAEA,MAAM,QAAuB;GAC3B,OAAO,OAAO,YAAY,CAAC,CAAC,KAAK,QAAQ,QAAQ;GACjD,MAAM,KAAK,MAAM;EACnB;CACF;AACF"}
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 bound\n// values must reference @p0.. in their SQL (mssql has no positional `?`). No `?`→`@p` rewriting —\n// that scan corrupts a `?` inside a string literal. query() re-wraps its argument in sp_executesql,\n// which is harmless for both a plain statement and a pre-formed batch (verified against real SQL\n// Server: two sp_executesql inserts in a transaction commit both).\ntype BindableRequest = { input(name: string, value: unknown): unknown };\nconst bindParams = <R extends BindableRequest>(request: R, params?: readonly unknown[]): R => {\n (params ?? []).forEach((value, i) => request.input(`p${i}`, value));\n return request;\n};\n\n/**\n * A SQL Server executor backed by an `mssql` connection pool. Feed it the self-contained\n * `sp_executesql` batch a `MssqlQuery` builder emits (its `params` is always `[]`).\n */\nexport function createMssqlExecutor(config: MssqlConfig): DbExecutor {\n const makePool = () =>\n new ConnectionPool('connectionString' in config ? config.connectionString : config);\n let pool = makePool();\n\n // Single-flight connect that RECOVERS. Caching a rejected `pool.connect()` promise would brick the\n // pool forever (a DB restart / blip): every later query awaits the same settled rejection. So reset\n // the gate and rebuild the pool on failure, and the next call retries. (pg/mysql self-heal\n // per-acquire; mssql caches connect, so it alone needs this.)\n let ready: Promise<unknown> | undefined;\n const ensureReady = () =>\n (ready ??= pool.connect().catch((e: unknown) => {\n ready = undefined;\n const dead = pool;\n pool = makePool();\n void dead.close().catch(() => {});\n throw e;\n }));\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n await ensureReady();\n const request = bindParams(pool.request(), prepared.params);\n return toResult<T>(await request.query(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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"}
@@ -42,10 +42,15 @@ const toResult = (result) => {
42
42
  * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
43
43
  * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
44
44
  */
45
- function createMysqlExecutorFromPool(pool) {
45
+ function createMysqlExecutorFromPool(pool, opts = {}) {
46
+ const { statementTimeoutMs } = opts;
47
+ const query = (target, sql, args) => statementTimeoutMs != null ? target.query({
48
+ sql,
49
+ timeout: statementTimeoutMs
50
+ }, args) : target.query(sql, args);
46
51
  return {
47
52
  async run(prepared) {
48
- const [result] = await pool.query(prepared.sql, argsOf(prepared));
53
+ const [result] = await query(pool, prepared.sql, argsOf(prepared));
49
54
  return toResult(result);
50
55
  },
51
56
  async transaction(statements) {
@@ -54,7 +59,7 @@ function createMysqlExecutorFromPool(pool) {
54
59
  await conn.beginTransaction();
55
60
  const results = [];
56
61
  for (const s of statements) {
57
- const [result] = await conn.query(s.sql, argsOf(s));
62
+ const [result] = await query(conn, s.sql, argsOf(s));
58
63
  results.push(toResult(result));
59
64
  }
60
65
  await conn.commit();
@@ -67,7 +72,7 @@ function createMysqlExecutorFromPool(pool) {
67
72
  }
68
73
  },
69
74
  async explain(prepared) {
70
- const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));
75
+ const [rows] = await query(pool, `EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));
71
76
  return parseMysqlPlan((Array.isArray(rows) ? rows[0]?.EXPLAIN : "") ?? "");
72
77
  },
73
78
  async close() {
@@ -77,10 +82,11 @@ function createMysqlExecutorFromPool(pool) {
77
82
  }
78
83
  /**
79
84
  * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
80
- * `{ sql, params }` a `MysqlQuery` builder emits.
85
+ * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement
86
+ * ceiling (MySQL has no pool-level knob for it).
81
87
  */
82
- function createMysqlExecutor(config) {
83
- return createMysqlExecutorFromPool((0, mysql2_promise.createPool)(config));
88
+ function createMysqlExecutor(config, opts = {}) {
89
+ return createMysqlExecutorFromPool((0, mysql2_promise.createPool)(config), opts);
84
90
  }
85
91
  //#endregion
86
92
  exports.createMysqlExecutor = createMysqlExecutor;
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import { createPool, type Pool, type PoolOptions, type ResultSetHeader } from 'mysql2/promise';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `mysql2` `PoolOptions`. */\nexport type MysqlConfig = PoolOptions;\n\n/** `EXPLAIN FORMAT=JSON` shape — only the bits we read. A `table` node is not always a direct child\n * of `query_block`: a JOIN nests it under `nested_loop[]`, ORDER BY under `ordering_operation`, etc. */\ntype MysqlTable = { access_type?: string; rows_examined_per_scan?: number };\ntype MysqlBlock = {\n cost_info?: { query_cost?: string };\n table?: MysqlTable;\n nested_loop?: MysqlBlock[];\n ordering_operation?: MysqlBlock;\n grouping_operation?: MysqlBlock;\n duplicates_removal?: MysqlBlock;\n};\ntype MysqlPlan = { query_block?: MysqlBlock };\n\n/** Every `table` node in the plan, however deeply the operation wrappers nest it. */\nfunction tablesOf(block: MysqlBlock | undefined): MysqlTable[] {\n if (!block) return [];\n return [\n ...(block.table ? [block.table] : []),\n ...(block.nested_loop ?? []).flatMap(tablesOf),\n ...tablesOf(block.ordering_operation),\n ...tablesOf(block.grouping_operation),\n ...tablesOf(block.duplicates_removal),\n ];\n}\n\n/** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */\nexport function parseMysqlPlan(raw: string): ExplainEstimate {\n let plan: MysqlPlan = {};\n try {\n plan = JSON.parse(raw || '{}') as MysqlPlan;\n } catch {\n // Unparseable plan — an empty estimate beats throwing.\n }\n const cost = Number(plan.query_block?.cost_info?.query_cost);\n const tables = tablesOf(plan.query_block);\n return {\n cost: Number.isFinite(cost) ? cost : undefined,\n // The driving (first) table's scanned rows.\n rows: tables[0]?.rows_examined_per_scan,\n // `ALL` is MySQL's full-table-scan access type — a scan ANYWHERE in the plan counts.\n fullScan: tables.some((t) => t.access_type === 'ALL'),\n plan: (raw ?? '').slice(0, 500),\n };\n}\n\nconst argsOf = (prepared: PreparedSql): unknown[] => (prepared.params ?? []) as unknown[];\n\n// mysql2's query() resolves to `[rows | ResultSetHeader, fields]`. SELECT → an array of rows; a\n// write → a ResultSetHeader carrying affectedRows.\nconst toResult = <T>(result: unknown): QueryResult<T> => {\n if (Array.isArray(result)) return { rows: result as T[], rowCount: result.length };\n return { rows: [], rowCount: (result as ResultSetHeader).affectedRows ?? 0 };\n};\n\n/**\n * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across\n * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.\n */\nexport function createMysqlExecutorFromPool(pool: Pool): DbExecutor {\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const [result] = await pool.query(prepared.sql, argsOf(prepared));\n return toResult<T>(result);\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // Driver-level transaction on a single pinned connection — beginTransaction/commit/rollback\n // speak the protocol correctly (no `multipleStatements`, no concatenation). Each statement\n // runs on its own; ROLLBACK on any error; release the connection no matter what.\n const conn = await pool.getConnection();\n try {\n await conn.beginTransaction();\n const results: QueryResult[] = [];\n for (const s of statements) {\n const [result] = await conn.query(s.sql, argsOf(s));\n results.push(toResult(result));\n }\n await conn.commit();\n return results;\n } catch (err) {\n await conn.rollback().catch(() => {});\n throw err;\n } finally {\n conn.release();\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // EXPLAIN never executes the statement; FORMAT=JSON is the only form carrying a cost estimate.\n const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));\n const raw = Array.isArray(rows) ? (rows[0] as { EXPLAIN?: string } | undefined)?.EXPLAIN : '';\n return parseMysqlPlan(raw ?? '');\n },\n\n async close(): Promise<void> {\n await pool.end();\n },\n };\n}\n\n/**\n * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the\n * `{ sql, params }` a `MysqlQuery` builder emits.\n */\nexport function createMysqlExecutor(config: MysqlConfig): DbExecutor {\n return createMysqlExecutorFromPool(createPool(config));\n}\n"],"mappings":";;;;AAoBA,SAAS,SAAS,OAA6C;CAC7D,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,OAAO;EACL,GAAI,MAAM,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC;EACnC,IAAI,MAAM,eAAe,CAAC,EAAA,CAAG,QAAQ,QAAQ;EAC7C,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;CACtC;AACF;;AAGA,SAAgB,eAAe,KAA8B;CAC3D,IAAI,OAAkB,CAAC;CACvB,IAAI;EACF,OAAO,KAAK,MAAM,OAAO,IAAI;CAC/B,QAAQ,CAER;CACA,MAAM,OAAO,OAAO,KAAK,aAAa,WAAW,UAAU;CAC3D,MAAM,SAAS,SAAS,KAAK,WAAW;CACxC,OAAO;EACL,MAAM,OAAO,SAAS,IAAI,IAAI,OAAO,KAAA;EAErC,MAAM,OAAO,EAAE,EAAE;EAEjB,UAAU,OAAO,MAAM,MAAM,EAAE,gBAAgB,KAAK;EACpD,OAAO,OAAO,GAAA,CAAI,MAAM,GAAG,GAAG;CAChC;AACF;AAEA,MAAM,UAAU,aAAsC,SAAS,UAAU,CAAC;AAI1E,MAAM,YAAe,WAAoC;CACvD,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO;EAAE,MAAM;EAAe,UAAU,OAAO;CAAO;CACjF,OAAO;EAAE,MAAM,CAAC;EAAG,UAAW,OAA2B,gBAAgB;CAAE;AAC7E;;;;;AAMA,SAAgB,4BAA4B,MAAwB;CAClE,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;GAChE,OAAO,SAAY,MAAM;EAC3B;EAEA,MAAM,YAAY,YAA4D;GAI5E,MAAM,OAAO,MAAM,KAAK,cAAc;GACtC,IAAI;IACF,MAAM,KAAK,iBAAiB;IAC5B,MAAM,UAAyB,CAAC;IAChC,KAAK,MAAM,KAAK,YAAY;KAC1B,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;KAClD,QAAQ,KAAK,SAAS,MAAM,CAAC;IAC/B;IACA,MAAM,KAAK,OAAO;IAClB,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,KAAK,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM;GACR,UAAU;IACR,KAAK,QAAQ;GACf;EACF;EAEA,MAAM,QAAQ,UAAiD;GAE7D,MAAM,CAAC,QAAQ,MAAM,KAAK,MAAM,uBAAuB,SAAS,OAAO,OAAO,QAAQ,CAAC;GAEvF,OAAO,gBADK,MAAM,QAAQ,IAAI,IAAK,KAAK,EAAE,EAAuC,UAAU,OAC9D,EAAE;EACjC;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;AAMA,SAAgB,oBAAoB,QAAiC;CACnE,OAAO,6BAAA,GAAA,eAAA,WAAA,CAAuC,MAAM,CAAC;AACvD"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import {\n createPool,\n type Pool,\n type PoolConnection,\n type PoolOptions,\n type ResultSetHeader,\n} from 'mysql2/promise';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `mysql2` `PoolOptions`. */\nexport type MysqlConfig = PoolOptions;\n\n/** `EXPLAIN FORMAT=JSON` shape — only the bits we read. A `table` node is not always a direct child\n * of `query_block`: a JOIN nests it under `nested_loop[]`, ORDER BY under `ordering_operation`, etc. */\ntype MysqlTable = { access_type?: string; rows_examined_per_scan?: number };\ntype 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/** Options for a MySQL executor. */\nexport type MysqlExecutorOptions = {\n /**\n * Per-statement client-side timeout in milliseconds. On expiry `mysql2` errors and destroys the\n * connection, which makes the server kill the running statement. MySQL has no pool-level\n * query-timeout config, so this is the knob for a statement ceiling. Omit for no timeout.\n */\n statementTimeoutMs?: number;\n};\n\n/**\n * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across\n * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.\n */\nexport function createMysqlExecutorFromPool(\n pool: Pool,\n opts: MysqlExecutorOptions = {},\n): DbExecutor {\n const { statementTimeoutMs } = opts;\n // mysql2 has no pool-level query timeout — apply it per statement via the object form, on both the\n // pool (run/explain) and a checked-out connection (transaction).\n const query = (target: Pool | PoolConnection, sql: string, args: unknown[]) =>\n statementTimeoutMs != null\n ? target.query({ sql, timeout: statementTimeoutMs }, args)\n : target.query(sql, args);\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const [result] = await query(pool, prepared.sql, argsOf(prepared));\n return toResult<T>(result);\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // Driver-level transaction on a single pinned connection — beginTransaction/commit/rollback\n // speak the protocol correctly (no `multipleStatements`, no concatenation). Each statement\n // runs on its own; ROLLBACK on any error; release the connection no matter what.\n const conn = await pool.getConnection();\n try {\n await conn.beginTransaction();\n const results: QueryResult[] = [];\n for (const s of statements) {\n const [result] = await query(conn, s.sql, argsOf(s));\n results.push(toResult(result));\n }\n await conn.commit();\n return results;\n } catch (err) {\n await conn.rollback().catch(() => {});\n throw err;\n } finally {\n conn.release();\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // EXPLAIN never executes the statement; FORMAT=JSON is the only form carrying a cost estimate.\n const [rows] = await query(pool, `EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));\n const raw = Array.isArray(rows) ? (rows[0] as { EXPLAIN?: string } | undefined)?.EXPLAIN : '';\n return parseMysqlPlan(raw ?? '');\n },\n\n async close(): Promise<void> {\n await pool.end();\n },\n };\n}\n\n/**\n * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the\n * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement\n * ceiling (MySQL has no pool-level knob for it).\n */\nexport function createMysqlExecutor(\n config: MysqlConfig,\n opts: MysqlExecutorOptions = {},\n): DbExecutor {\n return createMysqlExecutorFromPool(createPool(config), opts);\n}\n"],"mappings":";;;;AA0BA,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;;;;;AAgBA,SAAgB,4BACd,MACA,OAA6B,CAAC,GAClB;CACZ,MAAM,EAAE,uBAAuB;CAG/B,MAAM,SAAS,QAA+B,KAAa,SACzD,sBAAsB,OAClB,OAAO,MAAM;EAAE;EAAK,SAAS;CAAmB,GAAG,IAAI,IACvD,OAAO,MAAM,KAAK,IAAI;CAE5B,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,CAAC,UAAU,MAAM,MAAM,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;GACjE,OAAO,SAAY,MAAM;EAC3B;EAEA,MAAM,YAAY,YAA4D;GAI5E,MAAM,OAAO,MAAM,KAAK,cAAc;GACtC,IAAI;IACF,MAAM,KAAK,iBAAiB;IAC5B,MAAM,UAAyB,CAAC;IAChC,KAAK,MAAM,KAAK,YAAY;KAC1B,MAAM,CAAC,UAAU,MAAM,MAAM,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;KACnD,QAAQ,KAAK,SAAS,MAAM,CAAC;IAC/B;IACA,MAAM,KAAK,OAAO;IAClB,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,KAAK,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM;GACR,UAAU;IACR,KAAK,QAAQ;GACf;EACF;EAEA,MAAM,QAAQ,UAAiD;GAE7D,MAAM,CAAC,QAAQ,MAAM,MAAM,MAAM,uBAAuB,SAAS,OAAO,OAAO,QAAQ,CAAC;GAExF,OAAO,gBADK,MAAM,QAAQ,IAAI,IAAK,KAAK,EAAE,EAAuC,UAAU,OAC9D,EAAE;EACjC;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;;AAOA,SAAgB,oBACd,QACA,OAA6B,CAAC,GAClB;CACZ,OAAO,6BAAA,GAAA,eAAA,WAAA,CAAuC,MAAM,GAAG,IAAI;AAC7D"}
@@ -5,16 +5,26 @@ import { Pool, PoolOptions } from "mysql2/promise";
5
5
  type MysqlConfig = PoolOptions;
6
6
  /** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */
7
7
  declare function parseMysqlPlan(raw: string): ExplainEstimate;
8
+ /** Options for a MySQL executor. */
9
+ type MysqlExecutorOptions = {
10
+ /**
11
+ * Per-statement client-side timeout in milliseconds. On expiry `mysql2` errors and destroys the
12
+ * connection, which makes the server kill the running statement. MySQL has no pool-level
13
+ * query-timeout config, so this is the knob for a statement ceiling. Omit for no timeout.
14
+ */
15
+ statementTimeoutMs?: number;
16
+ };
8
17
  /**
9
18
  * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
10
19
  * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
11
20
  */
12
- declare function createMysqlExecutorFromPool(pool: Pool): DbExecutor;
21
+ declare function createMysqlExecutorFromPool(pool: Pool, opts?: MysqlExecutorOptions): DbExecutor;
13
22
  /**
14
23
  * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
15
- * `{ sql, params }` a `MysqlQuery` builder emits.
24
+ * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement
25
+ * ceiling (MySQL has no pool-level knob for it).
16
26
  */
17
- declare function createMysqlExecutor(config: MysqlConfig): DbExecutor;
27
+ declare function createMysqlExecutor(config: MysqlConfig, opts?: MysqlExecutorOptions): DbExecutor;
18
28
  //#endregion
19
- export { MysqlConfig, createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
29
+ export { MysqlConfig, MysqlExecutorOptions, createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
20
30
  //# sourceMappingURL=index.d.cts.map
@@ -5,16 +5,26 @@ import { Pool, PoolOptions } from "mysql2/promise";
5
5
  type MysqlConfig = PoolOptions;
6
6
  /** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */
7
7
  declare function parseMysqlPlan(raw: string): ExplainEstimate;
8
+ /** Options for a MySQL executor. */
9
+ type MysqlExecutorOptions = {
10
+ /**
11
+ * Per-statement client-side timeout in milliseconds. On expiry `mysql2` errors and destroys the
12
+ * connection, which makes the server kill the running statement. MySQL has no pool-level
13
+ * query-timeout config, so this is the knob for a statement ceiling. Omit for no timeout.
14
+ */
15
+ statementTimeoutMs?: number;
16
+ };
8
17
  /**
9
18
  * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
10
19
  * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
11
20
  */
12
- declare function createMysqlExecutorFromPool(pool: Pool): DbExecutor;
21
+ declare function createMysqlExecutorFromPool(pool: Pool, opts?: MysqlExecutorOptions): DbExecutor;
13
22
  /**
14
23
  * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
15
- * `{ sql, params }` a `MysqlQuery` builder emits.
24
+ * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement
25
+ * ceiling (MySQL has no pool-level knob for it).
16
26
  */
17
- declare function createMysqlExecutor(config: MysqlConfig): DbExecutor;
27
+ declare function createMysqlExecutor(config: MysqlConfig, opts?: MysqlExecutorOptions): DbExecutor;
18
28
  //#endregion
19
- export { MysqlConfig, createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
29
+ export { MysqlConfig, MysqlExecutorOptions, createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
20
30
  //# sourceMappingURL=index.d.mts.map
@@ -41,10 +41,15 @@ const toResult = (result) => {
41
41
  * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across
42
42
  * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.
43
43
  */
44
- function createMysqlExecutorFromPool(pool) {
44
+ function createMysqlExecutorFromPool(pool, opts = {}) {
45
+ const { statementTimeoutMs } = opts;
46
+ const query = (target, sql, args) => statementTimeoutMs != null ? target.query({
47
+ sql,
48
+ timeout: statementTimeoutMs
49
+ }, args) : target.query(sql, args);
45
50
  return {
46
51
  async run(prepared) {
47
- const [result] = await pool.query(prepared.sql, argsOf(prepared));
52
+ const [result] = await query(pool, prepared.sql, argsOf(prepared));
48
53
  return toResult(result);
49
54
  },
50
55
  async transaction(statements) {
@@ -53,7 +58,7 @@ function createMysqlExecutorFromPool(pool) {
53
58
  await conn.beginTransaction();
54
59
  const results = [];
55
60
  for (const s of statements) {
56
- const [result] = await conn.query(s.sql, argsOf(s));
61
+ const [result] = await query(conn, s.sql, argsOf(s));
57
62
  results.push(toResult(result));
58
63
  }
59
64
  await conn.commit();
@@ -66,7 +71,7 @@ function createMysqlExecutorFromPool(pool) {
66
71
  }
67
72
  },
68
73
  async explain(prepared) {
69
- const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));
74
+ const [rows] = await query(pool, `EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));
70
75
  return parseMysqlPlan((Array.isArray(rows) ? rows[0]?.EXPLAIN : "") ?? "");
71
76
  },
72
77
  async close() {
@@ -76,10 +81,11 @@ function createMysqlExecutorFromPool(pool) {
76
81
  }
77
82
  /**
78
83
  * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the
79
- * `{ sql, params }` a `MysqlQuery` builder emits.
84
+ * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement
85
+ * ceiling (MySQL has no pool-level knob for it).
80
86
  */
81
- function createMysqlExecutor(config) {
82
- return createMysqlExecutorFromPool(createPool(config));
87
+ function createMysqlExecutor(config, opts = {}) {
88
+ return createMysqlExecutorFromPool(createPool(config), opts);
83
89
  }
84
90
  //#endregion
85
91
  export { createMysqlExecutor, createMysqlExecutorFromPool, parseMysqlPlan };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import { createPool, type Pool, type PoolOptions, type ResultSetHeader } from 'mysql2/promise';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `mysql2` `PoolOptions`. */\nexport type MysqlConfig = PoolOptions;\n\n/** `EXPLAIN FORMAT=JSON` shape — only the bits we read. A `table` node is not always a direct child\n * of `query_block`: a JOIN nests it under `nested_loop[]`, ORDER BY under `ordering_operation`, etc. */\ntype MysqlTable = { access_type?: string; rows_examined_per_scan?: number };\ntype MysqlBlock = {\n cost_info?: { query_cost?: string };\n table?: MysqlTable;\n nested_loop?: MysqlBlock[];\n ordering_operation?: MysqlBlock;\n grouping_operation?: MysqlBlock;\n duplicates_removal?: MysqlBlock;\n};\ntype MysqlPlan = { query_block?: MysqlBlock };\n\n/** Every `table` node in the plan, however deeply the operation wrappers nest it. */\nfunction tablesOf(block: MysqlBlock | undefined): MysqlTable[] {\n if (!block) return [];\n return [\n ...(block.table ? [block.table] : []),\n ...(block.nested_loop ?? []).flatMap(tablesOf),\n ...tablesOf(block.ordering_operation),\n ...tablesOf(block.grouping_operation),\n ...tablesOf(block.duplicates_removal),\n ];\n}\n\n/** Parse `EXPLAIN FORMAT=JSON` output into the normalized estimate. Exported for unit tests. */\nexport function parseMysqlPlan(raw: string): ExplainEstimate {\n let plan: MysqlPlan = {};\n try {\n plan = JSON.parse(raw || '{}') as MysqlPlan;\n } catch {\n // Unparseable plan — an empty estimate beats throwing.\n }\n const cost = Number(plan.query_block?.cost_info?.query_cost);\n const tables = tablesOf(plan.query_block);\n return {\n cost: Number.isFinite(cost) ? cost : undefined,\n // The driving (first) table's scanned rows.\n rows: tables[0]?.rows_examined_per_scan,\n // `ALL` is MySQL's full-table-scan access type — a scan ANYWHERE in the plan counts.\n fullScan: tables.some((t) => t.access_type === 'ALL'),\n plan: (raw ?? '').slice(0, 500),\n };\n}\n\nconst argsOf = (prepared: PreparedSql): unknown[] => (prepared.params ?? []) as unknown[];\n\n// mysql2's query() resolves to `[rows | ResultSetHeader, fields]`. SELECT → an array of rows; a\n// write → a ResultSetHeader carrying affectedRows.\nconst toResult = <T>(result: unknown): QueryResult<T> => {\n if (Array.isArray(result)) return { rows: result as T[], rowCount: result.length };\n return { rows: [], rowCount: (result as ResultSetHeader).affectedRows ?? 0 };\n};\n\n/**\n * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across\n * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.\n */\nexport function createMysqlExecutorFromPool(pool: Pool): DbExecutor {\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const [result] = await pool.query(prepared.sql, argsOf(prepared));\n return toResult<T>(result);\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // Driver-level transaction on a single pinned connection — beginTransaction/commit/rollback\n // speak the protocol correctly (no `multipleStatements`, no concatenation). Each statement\n // runs on its own; ROLLBACK on any error; release the connection no matter what.\n const conn = await pool.getConnection();\n try {\n await conn.beginTransaction();\n const results: QueryResult[] = [];\n for (const s of statements) {\n const [result] = await conn.query(s.sql, argsOf(s));\n results.push(toResult(result));\n }\n await conn.commit();\n return results;\n } catch (err) {\n await conn.rollback().catch(() => {});\n throw err;\n } finally {\n conn.release();\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // EXPLAIN never executes the statement; FORMAT=JSON is the only form carrying a cost estimate.\n const [rows] = await pool.query(`EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));\n const raw = Array.isArray(rows) ? (rows[0] as { EXPLAIN?: string } | undefined)?.EXPLAIN : '';\n return parseMysqlPlan(raw ?? '');\n },\n\n async close(): Promise<void> {\n await pool.end();\n },\n };\n}\n\n/**\n * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the\n * `{ sql, params }` a `MysqlQuery` builder emits.\n */\nexport function createMysqlExecutor(config: MysqlConfig): DbExecutor {\n return createMysqlExecutorFromPool(createPool(config));\n}\n"],"mappings":";;;AAoBA,SAAS,SAAS,OAA6C;CAC7D,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,OAAO;EACL,GAAI,MAAM,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC;EACnC,IAAI,MAAM,eAAe,CAAC,EAAA,CAAG,QAAQ,QAAQ;EAC7C,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;EACpC,GAAG,SAAS,MAAM,kBAAkB;CACtC;AACF;;AAGA,SAAgB,eAAe,KAA8B;CAC3D,IAAI,OAAkB,CAAC;CACvB,IAAI;EACF,OAAO,KAAK,MAAM,OAAO,IAAI;CAC/B,QAAQ,CAER;CACA,MAAM,OAAO,OAAO,KAAK,aAAa,WAAW,UAAU;CAC3D,MAAM,SAAS,SAAS,KAAK,WAAW;CACxC,OAAO;EACL,MAAM,OAAO,SAAS,IAAI,IAAI,OAAO,KAAA;EAErC,MAAM,OAAO,EAAE,EAAE;EAEjB,UAAU,OAAO,MAAM,MAAM,EAAE,gBAAgB,KAAK;EACpD,OAAO,OAAO,GAAA,CAAI,MAAM,GAAG,GAAG;CAChC;AACF;AAEA,MAAM,UAAU,aAAsC,SAAS,UAAU,CAAC;AAI1E,MAAM,YAAe,WAAoC;CACvD,IAAI,MAAM,QAAQ,MAAM,GAAG,OAAO;EAAE,MAAM;EAAe,UAAU,OAAO;CAAO;CACjF,OAAO;EAAE,MAAM,CAAC;EAAG,UAAW,OAA2B,gBAAgB;CAAE;AAC7E;;;;;AAMA,SAAgB,4BAA4B,MAAwB;CAClE,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;GAChE,OAAO,SAAY,MAAM;EAC3B;EAEA,MAAM,YAAY,YAA4D;GAI5E,MAAM,OAAO,MAAM,KAAK,cAAc;GACtC,IAAI;IACF,MAAM,KAAK,iBAAiB;IAC5B,MAAM,UAAyB,CAAC;IAChC,KAAK,MAAM,KAAK,YAAY;KAC1B,MAAM,CAAC,UAAU,MAAM,KAAK,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;KAClD,QAAQ,KAAK,SAAS,MAAM,CAAC;IAC/B;IACA,MAAM,KAAK,OAAO;IAClB,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,KAAK,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM;GACR,UAAU;IACR,KAAK,QAAQ;GACf;EACF;EAEA,MAAM,QAAQ,UAAiD;GAE7D,MAAM,CAAC,QAAQ,MAAM,KAAK,MAAM,uBAAuB,SAAS,OAAO,OAAO,QAAQ,CAAC;GAEvF,OAAO,gBADK,MAAM,QAAQ,IAAI,IAAK,KAAK,EAAE,EAAuC,UAAU,OAC9D,EAAE;EACjC;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;AAMA,SAAgB,oBAAoB,QAAiC;CACnE,OAAO,4BAA4B,WAAW,MAAM,CAAC;AACvD"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/mysql/index.ts"],"sourcesContent":["import {\n createPool,\n type Pool,\n type PoolConnection,\n type PoolOptions,\n type ResultSetHeader,\n} from 'mysql2/promise';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/** Connection settings — any `mysql2` `PoolOptions`. */\nexport type MysqlConfig = PoolOptions;\n\n/** `EXPLAIN FORMAT=JSON` shape — only the bits we read. A `table` node is not always a direct child\n * of `query_block`: a JOIN nests it under `nested_loop[]`, ORDER BY under `ordering_operation`, etc. */\ntype MysqlTable = { access_type?: string; rows_examined_per_scan?: number };\ntype 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/** Options for a MySQL executor. */\nexport type MysqlExecutorOptions = {\n /**\n * Per-statement client-side timeout in milliseconds. On expiry `mysql2` errors and destroys the\n * connection, which makes the server kill the running statement. MySQL has no pool-level\n * query-timeout config, so this is the knob for a statement ceiling. Omit for no timeout.\n */\n statementTimeoutMs?: number;\n};\n\n/**\n * Build a MySQL executor over an EXISTING pool — bring your own `mysql2` Pool to share one across\n * your app (or hand in a test double). {@link createMysqlExecutor} is the usual entry.\n */\nexport function createMysqlExecutorFromPool(\n pool: Pool,\n opts: MysqlExecutorOptions = {},\n): DbExecutor {\n const { statementTimeoutMs } = opts;\n // mysql2 has no pool-level query timeout — apply it per statement via the object form, on both the\n // pool (run/explain) and a checked-out connection (transaction).\n const query = (target: Pool | PoolConnection, sql: string, args: unknown[]) =>\n statementTimeoutMs != null\n ? target.query({ sql, timeout: statementTimeoutMs }, args)\n : target.query(sql, args);\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const [result] = await query(pool, prepared.sql, argsOf(prepared));\n return toResult<T>(result);\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // Driver-level transaction on a single pinned connection — beginTransaction/commit/rollback\n // speak the protocol correctly (no `multipleStatements`, no concatenation). Each statement\n // runs on its own; ROLLBACK on any error; release the connection no matter what.\n const conn = await pool.getConnection();\n try {\n await conn.beginTransaction();\n const results: QueryResult[] = [];\n for (const s of statements) {\n const [result] = await query(conn, s.sql, argsOf(s));\n results.push(toResult(result));\n }\n await conn.commit();\n return results;\n } catch (err) {\n await conn.rollback().catch(() => {});\n throw err;\n } finally {\n conn.release();\n }\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // EXPLAIN never executes the statement; FORMAT=JSON is the only form carrying a cost estimate.\n const [rows] = await query(pool, `EXPLAIN FORMAT=JSON ${prepared.sql}`, argsOf(prepared));\n const raw = Array.isArray(rows) ? (rows[0] as { EXPLAIN?: string } | undefined)?.EXPLAIN : '';\n return parseMysqlPlan(raw ?? '');\n },\n\n async close(): Promise<void> {\n await pool.end();\n },\n };\n}\n\n/**\n * A MySQL / MariaDB executor backed by a `mysql2` connection pool (placeholders: `?`). Feed it the\n * `{ sql, params }` a `MysqlQuery` builder emits. Pass `{ statementTimeoutMs }` for a per-statement\n * ceiling (MySQL has no pool-level knob for it).\n */\nexport function createMysqlExecutor(\n config: MysqlConfig,\n opts: MysqlExecutorOptions = {},\n): DbExecutor {\n return createMysqlExecutorFromPool(createPool(config), opts);\n}\n"],"mappings":";;;AA0BA,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;;;;;AAgBA,SAAgB,4BACd,MACA,OAA6B,CAAC,GAClB;CACZ,MAAM,EAAE,uBAAuB;CAG/B,MAAM,SAAS,QAA+B,KAAa,SACzD,sBAAsB,OAClB,OAAO,MAAM;EAAE;EAAK,SAAS;CAAmB,GAAG,IAAI,IACvD,OAAO,MAAM,KAAK,IAAI;CAE5B,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,CAAC,UAAU,MAAM,MAAM,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;GACjE,OAAO,SAAY,MAAM;EAC3B;EAEA,MAAM,YAAY,YAA4D;GAI5E,MAAM,OAAO,MAAM,KAAK,cAAc;GACtC,IAAI;IACF,MAAM,KAAK,iBAAiB;IAC5B,MAAM,UAAyB,CAAC;IAChC,KAAK,MAAM,KAAK,YAAY;KAC1B,MAAM,CAAC,UAAU,MAAM,MAAM,MAAM,EAAE,KAAK,OAAO,CAAC,CAAC;KACnD,QAAQ,KAAK,SAAS,MAAM,CAAC;IAC/B;IACA,MAAM,KAAK,OAAO;IAClB,OAAO;GACT,SAAS,KAAK;IACZ,MAAM,KAAK,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;IACpC,MAAM;GACR,UAAU;IACR,KAAK,QAAQ;GACf;EACF;EAEA,MAAM,QAAQ,UAAiD;GAE7D,MAAM,CAAC,QAAQ,MAAM,MAAM,MAAM,uBAAuB,SAAS,OAAO,OAAO,QAAQ,CAAC;GAExF,OAAO,gBADK,MAAM,QAAQ,IAAI,IAAK,KAAK,EAAE,EAAuC,UAAU,OAC9D,EAAE;EACjC;EAEA,MAAM,QAAuB;GAC3B,MAAM,KAAK,IAAI;EACjB;CACF;AACF;;;;;;AAOA,SAAgB,oBACd,QACA,OAA6B,CAAC,GAClB;CACZ,OAAO,4BAA4B,WAAW,MAAM,GAAG,IAAI;AAC7D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deebeetech/sqleasy-engine",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "A thin, opt-in executor for @deebeetech/sqleasy: runs its prepared SQL and transactions on Postgres, MySQL, SQL Server, and SQLite — loading only the driver you import.",
5
5
  "keywords": [
6
6
  "sql",
@@ -100,6 +100,7 @@
100
100
  "devDependencies": {
101
101
  "@eslint/js": "^10.0.1",
102
102
  "@libsql/client": "^0.17.4",
103
+ "@sebbo2002/semantic-release-jsr": "^4.0.2",
103
104
  "@semantic-release/changelog": "^6.0.3",
104
105
  "@semantic-release/commit-analyzer": "^13.0.1",
105
106
  "@semantic-release/git": "^10.0.1",
@@ -117,6 +118,7 @@
117
118
  "pg": "^8.22.0",
118
119
  "prettier": "^3.9.5",
119
120
  "semantic-release": "^25.0.7",
121
+ "semantic-release-replace-plugin": "^1.2.7",
120
122
  "tsdown": "^0.22.7",
121
123
  "typescript": "^5.9.3",
122
124
  "typescript-eslint": "^8.64.0",