@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 @@
1
+ {"version":3,"file":"index.mjs","names":[],"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,IAAI,KAAK,MAAM,CAAC;AACxD"}
@@ -0,0 +1,81 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _libsql_client = require("@libsql/client");
3
+ //#region src/sqlite/index.ts
4
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
5
+ /** SQLite's "another connection holds the lock" signal — a statement that never ran, safe to retry. */
6
+ const isBusy = (err) => {
7
+ const msg = err instanceof Error ? err.message : String(err);
8
+ return msg.includes("SQLITE_BUSY") || msg.includes("database is locked");
9
+ };
10
+ const BUSY_RETRY_DELAYS_MS = [
11
+ 100,
12
+ 200,
13
+ 400,
14
+ 800,
15
+ 1600,
16
+ 3200
17
+ ];
18
+ const argsOf = (prepared) => prepared.params ?? [];
19
+ const toResult = (rs) => {
20
+ const rows = rs.rows;
21
+ return {
22
+ rows,
23
+ rowCount: rows.length > 0 ? rows.length : rs.rowsAffected
24
+ };
25
+ };
26
+ /**
27
+ * A SQLite / libSQL / Turso executor backed by `@libsql/client` (placeholders: `?`). Feed it the
28
+ * `{ sql, params }` a `SqliteQuery` builder emits.
29
+ */
30
+ function createSqliteExecutor(config) {
31
+ const clientConfig = "file" in config ? { url: `file:${config.file}` } : config;
32
+ const client = (0, _libsql_client.createClient)(clientConfig);
33
+ const isLocalFile = typeof clientConfig.url === "string" && clientConfig.url.startsWith("file:");
34
+ let busyTimeout;
35
+ const ensureBusyTimeout = () => {
36
+ if (!isLocalFile) return Promise.resolve();
37
+ busyTimeout ??= client.execute("PRAGMA busy_timeout = 5000").then(() => {}, () => {});
38
+ return busyTimeout;
39
+ };
40
+ const withBusyRetry = async (op) => {
41
+ await ensureBusyTimeout();
42
+ for (let attempt = 0;; attempt++) try {
43
+ return await op();
44
+ } catch (err) {
45
+ if (!isLocalFile || !isBusy(err) || attempt >= BUSY_RETRY_DELAYS_MS.length) throw err;
46
+ await sleep(BUSY_RETRY_DELAYS_MS[attempt]);
47
+ }
48
+ };
49
+ return {
50
+ async run(prepared) {
51
+ const rs = await withBusyRetry(() => client.execute({
52
+ sql: prepared.sql,
53
+ args: argsOf(prepared)
54
+ }));
55
+ return toResult(rs);
56
+ },
57
+ async transaction(statements) {
58
+ return (await withBusyRetry(() => client.batch(statements.map((s) => ({
59
+ sql: s.sql,
60
+ args: argsOf(s)
61
+ })), "write"))).map((rs) => toResult(rs));
62
+ },
63
+ async explain(prepared) {
64
+ const details = (await withBusyRetry(() => client.execute({
65
+ sql: `EXPLAIN QUERY PLAN ${prepared.sql}`,
66
+ args: argsOf(prepared)
67
+ }))).rows.map((r) => String(r.detail ?? ""));
68
+ return {
69
+ fullScan: details.some((d) => d.startsWith("SCAN")),
70
+ plan: details.join("; ").slice(0, 500)
71
+ };
72
+ },
73
+ async close() {
74
+ client.close();
75
+ }
76
+ };
77
+ }
78
+ //#endregion
79
+ exports.createSqliteExecutor = createSqliteExecutor;
80
+
81
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/sqlite/index.ts"],"sourcesContent":["import { createClient, type Config, type InArgs, type ResultSet } from '@libsql/client';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/**\n * How to reach the database. Either a full `@libsql/client` {@link Config} (a Turso/libSQL `url` +\n * `authToken`, an in-memory `':memory:'`, …) or the shorthand `{ file }` for a local SQLite file.\n */\nexport type SqliteConfig = Config | { file: string };\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/** SQLite's \"another connection holds the lock\" signal — a statement that never ran, safe to retry. */\nconst isBusy = (err: unknown): boolean => {\n const msg = err instanceof Error ? err.message : String(err);\n return msg.includes('SQLITE_BUSY') || msg.includes('database is locked');\n};\n\n// Up to ~6.3s of extra waiting beyond the busy timeout, then give up and surface the error.\nconst BUSY_RETRY_DELAYS_MS = [100, 200, 400, 800, 1600, 3200];\n\nconst argsOf = (prepared: PreparedSql): InArgs => (prepared.params ?? []) as InArgs;\n\nconst toResult = <T>(rs: ResultSet): QueryResult<T> => {\n const rows = rs.rows as unknown as T[];\n // SELECT → row count; write → affected count (rows is empty).\n return { rows, rowCount: rows.length > 0 ? rows.length : rs.rowsAffected };\n};\n\n/**\n * A SQLite / libSQL / Turso executor backed by `@libsql/client` (placeholders: `?`). Feed it the\n * `{ sql, params }` a `SqliteQuery` builder emits.\n */\nexport function createSqliteExecutor(config: SqliteConfig): DbExecutor {\n const clientConfig: Config = 'file' in config ? { url: `file:${config.file}` } : config;\n const client = createClient(clientConfig);\n\n // A local SQLite FILE can be open in more than one connection at once (SQLite allows one writer),\n // so two defenses, only for file URLs — remote libSQL/Turso has no such lock:\n // 1. a busy timeout, so a briefly-held lock is waited out instead of throwing on the spot;\n // 2. a bounded retry, because under sustained reader pressure the timeout can still lapse. A\n // SQLITE_BUSY means the statement did not run, so retrying — read OR write — repeats nothing;\n // a transaction batch rolls back fully before it surfaces, so retrying it is equally safe.\n const isLocalFile = typeof clientConfig.url === 'string' && clientConfig.url.startsWith('file:');\n\n let busyTimeout: Promise<void> | undefined;\n const ensureBusyTimeout = (): Promise<void> => {\n if (!isLocalFile) return Promise.resolve();\n // Cached and best-effort: a driver that rejects the pragma must not break the executor.\n busyTimeout ??= client.execute('PRAGMA busy_timeout = 5000').then(\n () => {},\n () => {},\n );\n return busyTimeout;\n };\n\n const withBusyRetry = async <R>(op: () => Promise<R>): Promise<R> => {\n await ensureBusyTimeout();\n for (let attempt = 0; ; attempt++) {\n try {\n return await op();\n } catch (err) {\n if (!isLocalFile || !isBusy(err) || attempt >= BUSY_RETRY_DELAYS_MS.length) throw err;\n await sleep(BUSY_RETRY_DELAYS_MS[attempt]!);\n }\n }\n };\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const rs = await withBusyRetry(() =>\n client.execute({ sql: prepared.sql, args: argsOf(prepared) }),\n );\n return toResult<T>(rs);\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // `batch` runs every statement in ONE transaction and rolls back on any error — exactly the\n // multi-builder contract, and each statement stays its own prepared statement.\n const results = await withBusyRetry(() =>\n client.batch(\n statements.map((s) => ({ sql: s.sql, args: argsOf(s) })),\n 'write',\n ),\n );\n return results.map((rs) => toResult(rs));\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // SQLite's planner exposes no cost or row estimate — only the plan SHAPE. `SCAN` means a full\n // table scan, `SEARCH` an index seek, so `fullScan` is the only signal this dialect gives.\n const rs = await withBusyRetry(() =>\n client.execute({ sql: `EXPLAIN QUERY PLAN ${prepared.sql}`, args: argsOf(prepared) }),\n );\n const details = rs.rows.map((r) =>\n String((r as unknown as { detail?: string }).detail ?? ''),\n );\n return {\n fullScan: details.some((d) => d.startsWith('SCAN')),\n plan: details.join('; ').slice(0, 500),\n };\n },\n\n async close(): Promise<void> {\n client.close();\n },\n };\n}\n"],"mappings":";;;AASA,MAAM,SAAS,OAA8B,IAAI,SAAS,MAAM,WAAW,GAAG,EAAE,CAAC;;AAGjF,MAAM,UAAU,QAA0B;CACxC,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC3D,OAAO,IAAI,SAAS,aAAa,KAAK,IAAI,SAAS,oBAAoB;AACzE;AAGA,MAAM,uBAAuB;CAAC;CAAK;CAAK;CAAK;CAAK;CAAM;AAAI;AAE5D,MAAM,UAAU,aAAmC,SAAS,UAAU,CAAC;AAEvE,MAAM,YAAe,OAAkC;CACrD,MAAM,OAAO,GAAG;CAEhB,OAAO;EAAE;EAAM,UAAU,KAAK,SAAS,IAAI,KAAK,SAAS,GAAG;CAAa;AAC3E;;;;;AAMA,SAAgB,qBAAqB,QAAkC;CACrE,MAAM,eAAuB,UAAU,SAAS,EAAE,KAAK,QAAQ,OAAO,OAAO,IAAI;CACjF,MAAM,UAAA,GAAA,eAAA,aAAA,CAAsB,YAAY;CAQxC,MAAM,cAAc,OAAO,aAAa,QAAQ,YAAY,aAAa,IAAI,WAAW,OAAO;CAE/F,IAAI;CACJ,MAAM,0BAAyC;EAC7C,IAAI,CAAC,aAAa,OAAO,QAAQ,QAAQ;EAEzC,gBAAgB,OAAO,QAAQ,4BAA4B,CAAC,CAAC,WACrD,CAAC,SACD,CAAC,CACT;EACA,OAAO;CACT;CAEA,MAAM,gBAAgB,OAAU,OAAqC;EACnE,MAAM,kBAAkB;EACxB,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;GACF,OAAO,MAAM,GAAG;EAClB,SAAS,KAAK;GACZ,IAAI,CAAC,eAAe,CAAC,OAAO,GAAG,KAAK,WAAW,qBAAqB,QAAQ,MAAM;GAClF,MAAM,MAAM,qBAAqB,QAAS;EAC5C;CAEJ;CAEA,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,KAAK,MAAM,oBACf,OAAO,QAAQ;IAAE,KAAK,SAAS;IAAK,MAAM,OAAO,QAAQ;GAAE,CAAC,CAC9D;GACA,OAAO,SAAY,EAAE;EACvB;EAEA,MAAM,YAAY,YAA4D;GAS5E,QAAO,MANe,oBACpB,OAAO,MACL,WAAW,KAAK,OAAO;IAAE,KAAK,EAAE;IAAK,MAAM,OAAO,CAAC;GAAE,EAAE,GACvD,OACF,CACF,EAAA,CACe,KAAK,OAAO,SAAS,EAAE,CAAC;EACzC;EAEA,MAAM,QAAQ,UAAiD;GAM7D,MAAM,WAAU,MAHC,oBACf,OAAO,QAAQ;IAAE,KAAK,sBAAsB,SAAS;IAAO,MAAM,OAAO,QAAQ;GAAE,CAAC,CACtF,EAAA,CACmB,KAAK,KAAK,MAC3B,OAAQ,EAAqC,UAAU,EAAE,CAC3D;GACA,OAAO;IACL,UAAU,QAAQ,MAAM,MAAM,EAAE,WAAW,MAAM,CAAC;IAClD,MAAM,QAAQ,KAAK,IAAI,CAAC,CAAC,MAAM,GAAG,GAAG;GACvC;EACF;EAEA,MAAM,QAAuB;GAC3B,OAAO,MAAM;EACf;CACF;AACF"}
@@ -0,0 +1,18 @@
1
+ import { DbExecutor } from "../index.cjs";
2
+ import { Config } from "@libsql/client";
3
+ //#region src/sqlite/index.d.ts
4
+ /**
5
+ * How to reach the database. Either a full `@libsql/client` {@link Config} (a Turso/libSQL `url` +
6
+ * `authToken`, an in-memory `':memory:'`, …) or the shorthand `{ file }` for a local SQLite file.
7
+ */
8
+ type SqliteConfig = Config | {
9
+ file: string;
10
+ };
11
+ /**
12
+ * A SQLite / libSQL / Turso executor backed by `@libsql/client` (placeholders: `?`). Feed it the
13
+ * `{ sql, params }` a `SqliteQuery` builder emits.
14
+ */
15
+ declare function createSqliteExecutor(config: SqliteConfig): DbExecutor;
16
+ //#endregion
17
+ export { SqliteConfig, createSqliteExecutor };
18
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1,18 @@
1
+ import { DbExecutor } from "../index.mjs";
2
+ import { Config } from "@libsql/client";
3
+ //#region src/sqlite/index.d.ts
4
+ /**
5
+ * How to reach the database. Either a full `@libsql/client` {@link Config} (a Turso/libSQL `url` +
6
+ * `authToken`, an in-memory `':memory:'`, …) or the shorthand `{ file }` for a local SQLite file.
7
+ */
8
+ type SqliteConfig = Config | {
9
+ file: string;
10
+ };
11
+ /**
12
+ * A SQLite / libSQL / Turso executor backed by `@libsql/client` (placeholders: `?`). Feed it the
13
+ * `{ sql, params }` a `SqliteQuery` builder emits.
14
+ */
15
+ declare function createSqliteExecutor(config: SqliteConfig): DbExecutor;
16
+ //#endregion
17
+ export { SqliteConfig, createSqliteExecutor };
18
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,80 @@
1
+ import { createClient } from "@libsql/client";
2
+ //#region src/sqlite/index.ts
3
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
4
+ /** SQLite's "another connection holds the lock" signal — a statement that never ran, safe to retry. */
5
+ const isBusy = (err) => {
6
+ const msg = err instanceof Error ? err.message : String(err);
7
+ return msg.includes("SQLITE_BUSY") || msg.includes("database is locked");
8
+ };
9
+ const BUSY_RETRY_DELAYS_MS = [
10
+ 100,
11
+ 200,
12
+ 400,
13
+ 800,
14
+ 1600,
15
+ 3200
16
+ ];
17
+ const argsOf = (prepared) => prepared.params ?? [];
18
+ const toResult = (rs) => {
19
+ const rows = rs.rows;
20
+ return {
21
+ rows,
22
+ rowCount: rows.length > 0 ? rows.length : rs.rowsAffected
23
+ };
24
+ };
25
+ /**
26
+ * A SQLite / libSQL / Turso executor backed by `@libsql/client` (placeholders: `?`). Feed it the
27
+ * `{ sql, params }` a `SqliteQuery` builder emits.
28
+ */
29
+ function createSqliteExecutor(config) {
30
+ const clientConfig = "file" in config ? { url: `file:${config.file}` } : config;
31
+ const client = createClient(clientConfig);
32
+ const isLocalFile = typeof clientConfig.url === "string" && clientConfig.url.startsWith("file:");
33
+ let busyTimeout;
34
+ const ensureBusyTimeout = () => {
35
+ if (!isLocalFile) return Promise.resolve();
36
+ busyTimeout ??= client.execute("PRAGMA busy_timeout = 5000").then(() => {}, () => {});
37
+ return busyTimeout;
38
+ };
39
+ const withBusyRetry = async (op) => {
40
+ await ensureBusyTimeout();
41
+ for (let attempt = 0;; attempt++) try {
42
+ return await op();
43
+ } catch (err) {
44
+ if (!isLocalFile || !isBusy(err) || attempt >= BUSY_RETRY_DELAYS_MS.length) throw err;
45
+ await sleep(BUSY_RETRY_DELAYS_MS[attempt]);
46
+ }
47
+ };
48
+ return {
49
+ async run(prepared) {
50
+ const rs = await withBusyRetry(() => client.execute({
51
+ sql: prepared.sql,
52
+ args: argsOf(prepared)
53
+ }));
54
+ return toResult(rs);
55
+ },
56
+ async transaction(statements) {
57
+ return (await withBusyRetry(() => client.batch(statements.map((s) => ({
58
+ sql: s.sql,
59
+ args: argsOf(s)
60
+ })), "write"))).map((rs) => toResult(rs));
61
+ },
62
+ async explain(prepared) {
63
+ const details = (await withBusyRetry(() => client.execute({
64
+ sql: `EXPLAIN QUERY PLAN ${prepared.sql}`,
65
+ args: argsOf(prepared)
66
+ }))).rows.map((r) => String(r.detail ?? ""));
67
+ return {
68
+ fullScan: details.some((d) => d.startsWith("SCAN")),
69
+ plan: details.join("; ").slice(0, 500)
70
+ };
71
+ },
72
+ async close() {
73
+ client.close();
74
+ }
75
+ };
76
+ }
77
+ //#endregion
78
+ export { createSqliteExecutor };
79
+
80
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/sqlite/index.ts"],"sourcesContent":["import { createClient, type Config, type InArgs, type ResultSet } from '@libsql/client';\nimport type { DbExecutor, ExplainEstimate, PreparedSql, QueryResult, Row } from '../index';\n\n/**\n * How to reach the database. Either a full `@libsql/client` {@link Config} (a Turso/libSQL `url` +\n * `authToken`, an in-memory `':memory:'`, …) or the shorthand `{ file }` for a local SQLite file.\n */\nexport type SqliteConfig = Config | { file: string };\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/** SQLite's \"another connection holds the lock\" signal — a statement that never ran, safe to retry. */\nconst isBusy = (err: unknown): boolean => {\n const msg = err instanceof Error ? err.message : String(err);\n return msg.includes('SQLITE_BUSY') || msg.includes('database is locked');\n};\n\n// Up to ~6.3s of extra waiting beyond the busy timeout, then give up and surface the error.\nconst BUSY_RETRY_DELAYS_MS = [100, 200, 400, 800, 1600, 3200];\n\nconst argsOf = (prepared: PreparedSql): InArgs => (prepared.params ?? []) as InArgs;\n\nconst toResult = <T>(rs: ResultSet): QueryResult<T> => {\n const rows = rs.rows as unknown as T[];\n // SELECT → row count; write → affected count (rows is empty).\n return { rows, rowCount: rows.length > 0 ? rows.length : rs.rowsAffected };\n};\n\n/**\n * A SQLite / libSQL / Turso executor backed by `@libsql/client` (placeholders: `?`). Feed it the\n * `{ sql, params }` a `SqliteQuery` builder emits.\n */\nexport function createSqliteExecutor(config: SqliteConfig): DbExecutor {\n const clientConfig: Config = 'file' in config ? { url: `file:${config.file}` } : config;\n const client = createClient(clientConfig);\n\n // A local SQLite FILE can be open in more than one connection at once (SQLite allows one writer),\n // so two defenses, only for file URLs — remote libSQL/Turso has no such lock:\n // 1. a busy timeout, so a briefly-held lock is waited out instead of throwing on the spot;\n // 2. a bounded retry, because under sustained reader pressure the timeout can still lapse. A\n // SQLITE_BUSY means the statement did not run, so retrying — read OR write — repeats nothing;\n // a transaction batch rolls back fully before it surfaces, so retrying it is equally safe.\n const isLocalFile = typeof clientConfig.url === 'string' && clientConfig.url.startsWith('file:');\n\n let busyTimeout: Promise<void> | undefined;\n const ensureBusyTimeout = (): Promise<void> => {\n if (!isLocalFile) return Promise.resolve();\n // Cached and best-effort: a driver that rejects the pragma must not break the executor.\n busyTimeout ??= client.execute('PRAGMA busy_timeout = 5000').then(\n () => {},\n () => {},\n );\n return busyTimeout;\n };\n\n const withBusyRetry = async <R>(op: () => Promise<R>): Promise<R> => {\n await ensureBusyTimeout();\n for (let attempt = 0; ; attempt++) {\n try {\n return await op();\n } catch (err) {\n if (!isLocalFile || !isBusy(err) || attempt >= BUSY_RETRY_DELAYS_MS.length) throw err;\n await sleep(BUSY_RETRY_DELAYS_MS[attempt]!);\n }\n }\n };\n\n return {\n async run<T = Row>(prepared: PreparedSql): Promise<QueryResult<T>> {\n const rs = await withBusyRetry(() =>\n client.execute({ sql: prepared.sql, args: argsOf(prepared) }),\n );\n return toResult<T>(rs);\n },\n\n async transaction(statements: readonly PreparedSql[]): Promise<QueryResult[]> {\n // `batch` runs every statement in ONE transaction and rolls back on any error — exactly the\n // multi-builder contract, and each statement stays its own prepared statement.\n const results = await withBusyRetry(() =>\n client.batch(\n statements.map((s) => ({ sql: s.sql, args: argsOf(s) })),\n 'write',\n ),\n );\n return results.map((rs) => toResult(rs));\n },\n\n async explain(prepared: PreparedSql): Promise<ExplainEstimate> {\n // SQLite's planner exposes no cost or row estimate — only the plan SHAPE. `SCAN` means a full\n // table scan, `SEARCH` an index seek, so `fullScan` is the only signal this dialect gives.\n const rs = await withBusyRetry(() =>\n client.execute({ sql: `EXPLAIN QUERY PLAN ${prepared.sql}`, args: argsOf(prepared) }),\n );\n const details = rs.rows.map((r) =>\n String((r as unknown as { detail?: string }).detail ?? ''),\n );\n return {\n fullScan: details.some((d) => d.startsWith('SCAN')),\n plan: details.join('; ').slice(0, 500),\n };\n },\n\n async close(): Promise<void> {\n client.close();\n },\n };\n}\n"],"mappings":";;AASA,MAAM,SAAS,OAA8B,IAAI,SAAS,MAAM,WAAW,GAAG,EAAE,CAAC;;AAGjF,MAAM,UAAU,QAA0B;CACxC,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;CAC3D,OAAO,IAAI,SAAS,aAAa,KAAK,IAAI,SAAS,oBAAoB;AACzE;AAGA,MAAM,uBAAuB;CAAC;CAAK;CAAK;CAAK;CAAK;CAAM;AAAI;AAE5D,MAAM,UAAU,aAAmC,SAAS,UAAU,CAAC;AAEvE,MAAM,YAAe,OAAkC;CACrD,MAAM,OAAO,GAAG;CAEhB,OAAO;EAAE;EAAM,UAAU,KAAK,SAAS,IAAI,KAAK,SAAS,GAAG;CAAa;AAC3E;;;;;AAMA,SAAgB,qBAAqB,QAAkC;CACrE,MAAM,eAAuB,UAAU,SAAS,EAAE,KAAK,QAAQ,OAAO,OAAO,IAAI;CACjF,MAAM,SAAS,aAAa,YAAY;CAQxC,MAAM,cAAc,OAAO,aAAa,QAAQ,YAAY,aAAa,IAAI,WAAW,OAAO;CAE/F,IAAI;CACJ,MAAM,0BAAyC;EAC7C,IAAI,CAAC,aAAa,OAAO,QAAQ,QAAQ;EAEzC,gBAAgB,OAAO,QAAQ,4BAA4B,CAAC,CAAC,WACrD,CAAC,SACD,CAAC,CACT;EACA,OAAO;CACT;CAEA,MAAM,gBAAgB,OAAU,OAAqC;EACnE,MAAM,kBAAkB;EACxB,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;GACF,OAAO,MAAM,GAAG;EAClB,SAAS,KAAK;GACZ,IAAI,CAAC,eAAe,CAAC,OAAO,GAAG,KAAK,WAAW,qBAAqB,QAAQ,MAAM;GAClF,MAAM,MAAM,qBAAqB,QAAS;EAC5C;CAEJ;CAEA,OAAO;EACL,MAAM,IAAa,UAAgD;GACjE,MAAM,KAAK,MAAM,oBACf,OAAO,QAAQ;IAAE,KAAK,SAAS;IAAK,MAAM,OAAO,QAAQ;GAAE,CAAC,CAC9D;GACA,OAAO,SAAY,EAAE;EACvB;EAEA,MAAM,YAAY,YAA4D;GAS5E,QAAO,MANe,oBACpB,OAAO,MACL,WAAW,KAAK,OAAO;IAAE,KAAK,EAAE;IAAK,MAAM,OAAO,CAAC;GAAE,EAAE,GACvD,OACF,CACF,EAAA,CACe,KAAK,OAAO,SAAS,EAAE,CAAC;EACzC;EAEA,MAAM,QAAQ,UAAiD;GAM7D,MAAM,WAAU,MAHC,oBACf,OAAO,QAAQ;IAAE,KAAK,sBAAsB,SAAS;IAAO,MAAM,OAAO,QAAQ;GAAE,CAAC,CACtF,EAAA,CACmB,KAAK,KAAK,MAC3B,OAAQ,EAAqC,UAAU,EAAE,CAC3D;GACA,OAAO;IACL,UAAU,QAAQ,MAAM,MAAM,EAAE,WAAW,MAAM,CAAC;IAClD,MAAM,QAAQ,KAAK,IAAI,CAAC,CAAC,MAAM,GAAG,GAAG;GACvC;EACF;EAEA,MAAM,QAAuB;GAC3B,OAAO,MAAM;EACf;CACF;AACF"}
package/package.json ADDED
@@ -0,0 +1,125 @@
1
+ {
2
+ "name": "@deebeetech/sqleasy-engine",
3
+ "version": "0.0.0",
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
+ "keywords": [
6
+ "sql",
7
+ "sql-executor",
8
+ "query-runner",
9
+ "postgres",
10
+ "mysql",
11
+ "mssql",
12
+ "sqlite",
13
+ "libsql",
14
+ "transactions",
15
+ "sqleasy",
16
+ "typescript",
17
+ "database"
18
+ ],
19
+ "homepage": "https://github.com/deebee-tech/sqleasy-engine",
20
+ "license": "MIT",
21
+ "private": false,
22
+ "author": {
23
+ "name": "Sandy Weatherby",
24
+ "email": "unicornbeachdiaries@gmail.com"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/deebee-tech/sqleasy-engine"
29
+ },
30
+ "type": "module",
31
+ "main": "dist/index.mjs",
32
+ "types": "dist/index.d.mts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.mts",
36
+ "import": "./dist/index.mjs",
37
+ "require": "./dist/index.cjs"
38
+ },
39
+ "./sqlite": {
40
+ "types": "./dist/sqlite/index.d.mts",
41
+ "import": "./dist/sqlite/index.mjs",
42
+ "require": "./dist/sqlite/index.cjs"
43
+ },
44
+ "./postgres": {
45
+ "types": "./dist/postgres/index.d.mts",
46
+ "import": "./dist/postgres/index.mjs",
47
+ "require": "./dist/postgres/index.cjs"
48
+ },
49
+ "./mysql": {
50
+ "types": "./dist/mysql/index.d.mts",
51
+ "import": "./dist/mysql/index.mjs",
52
+ "require": "./dist/mysql/index.cjs"
53
+ },
54
+ "./mssql": {
55
+ "types": "./dist/mssql/index.d.mts",
56
+ "import": "./dist/mssql/index.mjs",
57
+ "require": "./dist/mssql/index.cjs"
58
+ },
59
+ "./introspection": {
60
+ "types": "./dist/introspection/index.d.mts",
61
+ "import": "./dist/introspection/index.mjs",
62
+ "require": "./dist/introspection/index.cjs"
63
+ }
64
+ },
65
+ "files": [
66
+ "dist"
67
+ ],
68
+ "publishConfig": {
69
+ "access": "public"
70
+ },
71
+ "scripts": {
72
+ "build": "tsdown",
73
+ "format": "prettier --write .",
74
+ "format:check": "prettier --check .",
75
+ "lint": "eslint .",
76
+ "test": "vitest run",
77
+ "typecheck": "tsc --noEmit"
78
+ },
79
+ "packageManager": "pnpm@11.13.0",
80
+ "peerDependencies": {
81
+ "@libsql/client": "*",
82
+ "mssql": "*",
83
+ "mysql2": "*",
84
+ "pg": "*"
85
+ },
86
+ "peerDependenciesMeta": {
87
+ "@libsql/client": {
88
+ "optional": true
89
+ },
90
+ "mssql": {
91
+ "optional": true
92
+ },
93
+ "pg": {
94
+ "optional": true
95
+ },
96
+ "mysql2": {
97
+ "optional": true
98
+ }
99
+ },
100
+ "devDependencies": {
101
+ "@eslint/js": "^10.0.1",
102
+ "@libsql/client": "^0.17.4",
103
+ "@semantic-release/changelog": "^6.0.3",
104
+ "@semantic-release/commit-analyzer": "^13.0.1",
105
+ "@semantic-release/git": "^10.0.1",
106
+ "@semantic-release/github": "^12.0.9",
107
+ "@semantic-release/npm": "^13.1.5",
108
+ "@semantic-release/release-notes-generator": "^14.1.1",
109
+ "@types/mssql": "^12.3.0",
110
+ "@types/node": "^26.1.1",
111
+ "@types/pg": "^8.20.0",
112
+ "conventional-changelog-conventionalcommits": "^10.2.1",
113
+ "eslint": "^10.7.0",
114
+ "eslint-config-prettier": "^10.1.8",
115
+ "mssql": "^12.7.0",
116
+ "mysql2": "^3.22.6",
117
+ "pg": "^8.22.0",
118
+ "prettier": "^3.9.5",
119
+ "semantic-release": "^25.0.7",
120
+ "tsdown": "^0.22.7",
121
+ "typescript": "^5.9.3",
122
+ "typescript-eslint": "^8.64.0",
123
+ "vitest": "^4.1.10"
124
+ }
125
+ }