turbine-orm 0.28.3 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cjs/cli/index.js +5 -0
- package/dist/cjs/cli/mcp.js +22 -92
- package/dist/cjs/client.js +69 -5
- package/dist/cjs/generate.js +71 -25
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +350 -120
- package/dist/cjs/mssql.js +18 -133
- package/dist/cjs/mysql.js +16 -129
- package/dist/cjs/optional-peer-import.cjs +122 -0
- package/dist/cjs/powdb.js +440 -81
- package/dist/cjs/powql.js +49 -25
- package/dist/cjs/query/builder.js +290 -23
- package/dist/cjs/query/filters.js +32 -1
- package/dist/cjs/schema-metadata.js +316 -0
- package/dist/cjs/sqlite.js +8 -89
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +5 -0
- package/dist/cli/mcp.d.ts +18 -0
- package/dist/cli/mcp.js +22 -93
- package/dist/client.d.ts +44 -6
- package/dist/client.js +69 -5
- package/dist/generate.d.ts +16 -4
- package/dist/generate.js +71 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +94 -1
- package/dist/introspect.js +345 -120
- package/dist/mssql.js +16 -101
- package/dist/mysql.js +14 -97
- package/dist/optional-peer-import.cjs +89 -0
- package/dist/optional-peer-import.d.cts +53 -0
- package/dist/powdb.d.ts +94 -26
- package/dist/powdb.js +435 -80
- package/dist/powql.d.ts +6 -0
- package/dist/powql.js +51 -27
- package/dist/query/builder.d.ts +60 -3
- package/dist/query/builder.js +291 -24
- package/dist/query/deferred.d.ts +7 -2
- package/dist/query/filters.d.ts +18 -0
- package/dist/query/filters.js +30 -0
- package/dist/query/types.d.ts +19 -0
- package/dist/schema-metadata.d.ts +77 -0
- package/dist/schema-metadata.js +313 -0
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +9 -90
- package/package.json +3 -3
package/dist/mysql.js
CHANGED
|
@@ -65,7 +65,9 @@
|
|
|
65
65
|
import { TurbineClient } from './client.js';
|
|
66
66
|
import { postgresDialect, } from './dialect.js';
|
|
67
67
|
import { ConnectionError, UnsupportedFeatureError } from './errors.js';
|
|
68
|
-
import {
|
|
68
|
+
import { deriveEngineRelations } from './introspect.js';
|
|
69
|
+
import importOptionalPeer from './optional-peer-import.cjs';
|
|
70
|
+
import { isDateType, snakeToCamel, } from './schema.js';
|
|
69
71
|
// ---------------------------------------------------------------------------
|
|
70
72
|
// Value coercion (params in)
|
|
71
73
|
// ---------------------------------------------------------------------------
|
|
@@ -555,103 +557,15 @@ export const mysqlDialect = {
|
|
|
555
557
|
};
|
|
556
558
|
const num = (v) => (typeof v === 'string' ? Number(v) : (v ?? 0));
|
|
557
559
|
/**
|
|
558
|
-
* Derive
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
560
|
+
* Derive relations from the FK list via the SHARED introspection pipeline
|
|
561
|
+
* (`deriveEngineRelations` → `buildRelationsFromForeignKeys` +
|
|
562
|
+
* `addAutoManyToManyRelations` in introspect.ts), so this engine derives
|
|
563
|
+
* IDENTICAL relation names to `turbine generate` against Postgres for the
|
|
564
|
+
* same logical schema — legacy-first naming, per-column disambiguation, and
|
|
565
|
+
* collision resolution against scalar column fields included.
|
|
562
566
|
*/
|
|
563
567
|
function buildRelationsFromForeignKeys(tableNames, foreignKeys, pkByTable, columnsByTable) {
|
|
564
|
-
|
|
565
|
-
const fkCounts = new Map();
|
|
566
|
-
for (const fk of foreignKeys) {
|
|
567
|
-
const key = `${fk.sourceTable}->${fk.targetTable}`;
|
|
568
|
-
fkCounts.set(key, (fkCounts.get(key) ?? 0) + 1);
|
|
569
|
-
}
|
|
570
|
-
const relationsByTable = new Map();
|
|
571
|
-
for (const fk of foreignKeys) {
|
|
572
|
-
if (!tableSet.has(fk.targetTable))
|
|
573
|
-
continue;
|
|
574
|
-
const needsDisambiguation = (fkCounts.get(`${fk.sourceTable}->${fk.targetTable}`) ?? 0) > 1;
|
|
575
|
-
const foreignKey = fk.sourceColumns.length === 1 ? fk.sourceColumns[0] : fk.sourceColumns;
|
|
576
|
-
const referenceKey = fk.targetColumns.length === 1 ? fk.targetColumns[0] : fk.targetColumns;
|
|
577
|
-
const belongsToName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
578
|
-
? snakeToCamel(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
579
|
-
: singularize(snakeToCamel(fk.targetTable));
|
|
580
|
-
if (!relationsByTable.has(fk.sourceTable))
|
|
581
|
-
relationsByTable.set(fk.sourceTable, {});
|
|
582
|
-
relationsByTable.get(fk.sourceTable)[belongsToName] = {
|
|
583
|
-
type: 'belongsTo',
|
|
584
|
-
name: belongsToName,
|
|
585
|
-
from: fk.sourceTable,
|
|
586
|
-
to: fk.targetTable,
|
|
587
|
-
foreignKey,
|
|
588
|
-
referenceKey,
|
|
589
|
-
};
|
|
590
|
-
const hasManyName = needsDisambiguation && fk.sourceColumns.length === 1
|
|
591
|
-
? snakeToCamel(`${fk.sourceTable}_by_${fk.sourceColumns[0].replace(/_id$/, '')}`)
|
|
592
|
-
: snakeToCamel(fk.sourceTable);
|
|
593
|
-
if (!relationsByTable.has(fk.targetTable))
|
|
594
|
-
relationsByTable.set(fk.targetTable, {});
|
|
595
|
-
relationsByTable.get(fk.targetTable)[hasManyName] = {
|
|
596
|
-
type: 'hasMany',
|
|
597
|
-
name: hasManyName,
|
|
598
|
-
from: fk.targetTable,
|
|
599
|
-
to: fk.sourceTable,
|
|
600
|
-
foreignKey,
|
|
601
|
-
referenceKey,
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
// Conservative many-to-many auto-detection (additive): a table J is a pure
|
|
605
|
-
// junction iff PK is exactly two columns, exactly two single-column FKs whose
|
|
606
|
-
// source columns ARE the PK, two distinct target tables, and no payload columns.
|
|
607
|
-
for (const tableName of tableNames) {
|
|
608
|
-
const pk = pkByTable.get(tableName) ?? [];
|
|
609
|
-
if (pk.length !== 2)
|
|
610
|
-
continue;
|
|
611
|
-
const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
|
|
612
|
-
if (tableFks.length !== 2)
|
|
613
|
-
continue;
|
|
614
|
-
if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
|
|
615
|
-
continue;
|
|
616
|
-
const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
|
|
617
|
-
const pkSet = new Set(pk);
|
|
618
|
-
if (!fkCols.every((c) => pkSet.has(c)))
|
|
619
|
-
continue;
|
|
620
|
-
if (new Set(fkCols).size !== 2)
|
|
621
|
-
continue;
|
|
622
|
-
const [fkA, fkB] = tableFks;
|
|
623
|
-
if (fkA.targetTable === fkB.targetTable)
|
|
624
|
-
continue;
|
|
625
|
-
const jCols = (columnsByTable.get(tableName) ?? []).map((c) => c.name);
|
|
626
|
-
if (jCols.length !== 2)
|
|
627
|
-
continue;
|
|
628
|
-
const addM2M = (self, other) => {
|
|
629
|
-
const sourceTbl = self.targetTable;
|
|
630
|
-
const targetTbl = other.targetTable;
|
|
631
|
-
const relName = snakeToCamel(targetTbl);
|
|
632
|
-
if (!relationsByTable.has(sourceTbl))
|
|
633
|
-
relationsByTable.set(sourceTbl, {});
|
|
634
|
-
const existing = relationsByTable.get(sourceTbl);
|
|
635
|
-
if (existing[relName])
|
|
636
|
-
return;
|
|
637
|
-
existing[relName] = {
|
|
638
|
-
type: 'manyToMany',
|
|
639
|
-
name: relName,
|
|
640
|
-
from: sourceTbl,
|
|
641
|
-
to: targetTbl,
|
|
642
|
-
referenceKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
643
|
-
foreignKey: self.targetColumns.length === 1 ? self.targetColumns[0] : self.targetColumns,
|
|
644
|
-
through: {
|
|
645
|
-
table: tableName,
|
|
646
|
-
sourceKey: self.sourceColumns[0],
|
|
647
|
-
targetKey: other.sourceColumns[0],
|
|
648
|
-
},
|
|
649
|
-
};
|
|
650
|
-
};
|
|
651
|
-
addM2M(fkA, fkB);
|
|
652
|
-
addM2M(fkB, fkA);
|
|
653
|
-
}
|
|
654
|
-
return relationsByTable;
|
|
568
|
+
return deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable);
|
|
655
569
|
}
|
|
656
570
|
/**
|
|
657
571
|
* Introspect a MySQL database into the same {@link SchemaMetadata} shape the
|
|
@@ -898,7 +812,10 @@ function parseMysqlConfig(connectionString) {
|
|
|
898
812
|
async function loadCreatePool() {
|
|
899
813
|
let mod;
|
|
900
814
|
try {
|
|
901
|
-
|
|
815
|
+
// Via the .cts helper so the CJS build keeps a path to a REAL dynamic
|
|
816
|
+
// import() even if a future mysql2 major goes ESM-only (the CommonJS pass
|
|
817
|
+
// transpiles a plain `import()` here into `require()`).
|
|
818
|
+
mod = (await importOptionalPeer('mysql2/promise'));
|
|
902
819
|
}
|
|
903
820
|
catch (err) {
|
|
904
821
|
throw new ConnectionError("[turbine] turbine-orm/mysql requires the optional peer dependency 'mysql2'. Install it: npm i mysql2. " +
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* True dynamic `import()` for the optional peer dependencies (`mysql2`,
|
|
4
|
+
* `mssql`, `@zvndev/powdb-client`, `@zvndev/powdb-embedded`) — safe in BOTH
|
|
5
|
+
* build outputs, including for peers that are ESM-only.
|
|
6
|
+
*
|
|
7
|
+
* THE PROBLEM THIS FILE SOLVES (the `@zvndev/powdb-client` ≥ 0.9 CJS break):
|
|
8
|
+
* the engine subpaths load their optional peers with dynamic `import()` so the
|
|
9
|
+
* peers stay out of the static graph. The ESM build (`tsconfig.json`, module
|
|
10
|
+
* NodeNext) emits that `import()` verbatim. The CJS build (`tsconfig.cjs.json`,
|
|
11
|
+
* module CommonJS) however TRANSPILES `import()` into
|
|
12
|
+
* `Promise.resolve().then(() => require(...))` — and `require()` of an
|
|
13
|
+
* ESM-only package (no `require` export condition, e.g. powdb-client ≥ 0.9)
|
|
14
|
+
* throws `ERR_PACKAGE_PATH_NOT_EXPORTED`, breaking every CJS consumer.
|
|
15
|
+
*
|
|
16
|
+
* TypeScript offers no way to preserve `import()` under `module: CommonJS`,
|
|
17
|
+
* and the CJS pass cannot switch to `module: NodeNext` (the root package.json
|
|
18
|
+
* says `"type": "module"`, so NodeNext would classify every `.ts` source as
|
|
19
|
+
* ESM and emit ESM into dist/cjs). A `.cts` file is the escape hatch: it is
|
|
20
|
+
* CommonJS-format by extension regardless of package `type`, so under the ESM
|
|
21
|
+
* pass (NodeNext) it compiles to `dist/optional-peer-import.cjs` — a CommonJS
|
|
22
|
+
* file whose `import()` SURVIVES transpilation (NodeNext preserves dynamic
|
|
23
|
+
* import in CJS files precisely because it is the only way CJS can load ESM).
|
|
24
|
+
*
|
|
25
|
+
* That gives the published package two copies of this module:
|
|
26
|
+
* - `dist/optional-peer-import.cjs` (ESM pass, NodeNext) — real `import()`
|
|
27
|
+
* - `dist/cjs/optional-peer-import.cjs` (CJS pass, CommonJS) — lowered to `require()`
|
|
28
|
+
*
|
|
29
|
+
* The lowered copy works fine for CJS-loadable peers (`mysql2`, `mssql`, older
|
|
30
|
+
* powdb peers). When it hits an ESM-only peer, the `require()` fails with a
|
|
31
|
+
* recognizable code and this function falls back to delegating the load to the
|
|
32
|
+
* sibling NodeNext copy one directory up (`../optional-peer-import.cjs`) —
|
|
33
|
+
* which is a plain CommonJS file (loadable by `require()` on every supported
|
|
34
|
+
* Node) whose real `import()` then loads the ESM peer. The ESM-pass copy has
|
|
35
|
+
* no such sibling; its lazy `require` throws and the original error surfaces,
|
|
36
|
+
* so the fallback can never recurse.
|
|
37
|
+
*
|
|
38
|
+
* Keep this module dependency-free and side-effect-free: it must be loadable
|
|
39
|
+
* from both module systems on every supported Node (≥ 20) without pulling in
|
|
40
|
+
* anything else.
|
|
41
|
+
*/
|
|
42
|
+
/**
|
|
43
|
+
* Does this error mean "the module exists but cannot be loaded via
|
|
44
|
+
* `require()` because it is ESM-only"? These are the only failures worth
|
|
45
|
+
* retrying through a real `import()`; anything else (not installed, throw on
|
|
46
|
+
* init, …) is rethrown untouched so callers keep the original diagnostics.
|
|
47
|
+
*/
|
|
48
|
+
function isEsmOnlyLoadError(err) {
|
|
49
|
+
const code = err?.code;
|
|
50
|
+
return (code === 'ERR_PACKAGE_PATH_NOT_EXPORTED' || // exports map has no `require` condition
|
|
51
|
+
code === 'ERR_REQUIRE_ESM' || // require() of an ES module (pre-require(esm) Node)
|
|
52
|
+
code === 'ERR_REQUIRE_ASYNC_MODULE' // require(esm) of a module with top-level await
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Dynamically import an optional peer dependency. In the ESM build this is a
|
|
57
|
+
* plain `import()`. In the CJS build the first attempt is a transpiled
|
|
58
|
+
* `require()`; if the peer turns out to be ESM-only, the load is retried
|
|
59
|
+
* through the ESM-build sibling copy of this file, whose `import()` survived
|
|
60
|
+
* transpilation (see the module doc comment).
|
|
61
|
+
*
|
|
62
|
+
* @param specifier bare package specifier (e.g. `'@zvndev/powdb-client'`).
|
|
63
|
+
* @param allowEsmFallback internal recursion guard — the delegated call passes
|
|
64
|
+
* `false` so a failure in the sibling copy can never bounce back.
|
|
65
|
+
*/
|
|
66
|
+
async function importOptionalPeer(specifier, allowEsmFallback = true) {
|
|
67
|
+
try {
|
|
68
|
+
return await import(specifier);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
if (!allowEsmFallback || !isEsmOnlyLoadError(err))
|
|
72
|
+
throw err;
|
|
73
|
+
let esmCapableCopy;
|
|
74
|
+
try {
|
|
75
|
+
// Only resolvable from dist/cjs/, where it lands on the NodeNext-built
|
|
76
|
+
// dist/optional-peer-import.cjs. From anywhere else (the ESM copy
|
|
77
|
+
// itself, or running the TypeScript source directly) the file does not
|
|
78
|
+
// exist and the original error is rethrown below.
|
|
79
|
+
esmCapableCopy = require('../optional-peer-import.cjs');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
esmCapableCopy = undefined;
|
|
83
|
+
}
|
|
84
|
+
if (typeof esmCapableCopy !== 'function')
|
|
85
|
+
throw err;
|
|
86
|
+
return esmCapableCopy(specifier, false);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
module.exports = importOptionalPeer;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True dynamic `import()` for the optional peer dependencies (`mysql2`,
|
|
3
|
+
* `mssql`, `@zvndev/powdb-client`, `@zvndev/powdb-embedded`) — safe in BOTH
|
|
4
|
+
* build outputs, including for peers that are ESM-only.
|
|
5
|
+
*
|
|
6
|
+
* THE PROBLEM THIS FILE SOLVES (the `@zvndev/powdb-client` ≥ 0.9 CJS break):
|
|
7
|
+
* the engine subpaths load their optional peers with dynamic `import()` so the
|
|
8
|
+
* peers stay out of the static graph. The ESM build (`tsconfig.json`, module
|
|
9
|
+
* NodeNext) emits that `import()` verbatim. The CJS build (`tsconfig.cjs.json`,
|
|
10
|
+
* module CommonJS) however TRANSPILES `import()` into
|
|
11
|
+
* `Promise.resolve().then(() => require(...))` — and `require()` of an
|
|
12
|
+
* ESM-only package (no `require` export condition, e.g. powdb-client ≥ 0.9)
|
|
13
|
+
* throws `ERR_PACKAGE_PATH_NOT_EXPORTED`, breaking every CJS consumer.
|
|
14
|
+
*
|
|
15
|
+
* TypeScript offers no way to preserve `import()` under `module: CommonJS`,
|
|
16
|
+
* and the CJS pass cannot switch to `module: NodeNext` (the root package.json
|
|
17
|
+
* says `"type": "module"`, so NodeNext would classify every `.ts` source as
|
|
18
|
+
* ESM and emit ESM into dist/cjs). A `.cts` file is the escape hatch: it is
|
|
19
|
+
* CommonJS-format by extension regardless of package `type`, so under the ESM
|
|
20
|
+
* pass (NodeNext) it compiles to `dist/optional-peer-import.cjs` — a CommonJS
|
|
21
|
+
* file whose `import()` SURVIVES transpilation (NodeNext preserves dynamic
|
|
22
|
+
* import in CJS files precisely because it is the only way CJS can load ESM).
|
|
23
|
+
*
|
|
24
|
+
* That gives the published package two copies of this module:
|
|
25
|
+
* - `dist/optional-peer-import.cjs` (ESM pass, NodeNext) — real `import()`
|
|
26
|
+
* - `dist/cjs/optional-peer-import.cjs` (CJS pass, CommonJS) — lowered to `require()`
|
|
27
|
+
*
|
|
28
|
+
* The lowered copy works fine for CJS-loadable peers (`mysql2`, `mssql`, older
|
|
29
|
+
* powdb peers). When it hits an ESM-only peer, the `require()` fails with a
|
|
30
|
+
* recognizable code and this function falls back to delegating the load to the
|
|
31
|
+
* sibling NodeNext copy one directory up (`../optional-peer-import.cjs`) —
|
|
32
|
+
* which is a plain CommonJS file (loadable by `require()` on every supported
|
|
33
|
+
* Node) whose real `import()` then loads the ESM peer. The ESM-pass copy has
|
|
34
|
+
* no such sibling; its lazy `require` throws and the original error surfaces,
|
|
35
|
+
* so the fallback can never recurse.
|
|
36
|
+
*
|
|
37
|
+
* Keep this module dependency-free and side-effect-free: it must be loadable
|
|
38
|
+
* from both module systems on every supported Node (≥ 20) without pulling in
|
|
39
|
+
* anything else.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Dynamically import an optional peer dependency. In the ESM build this is a
|
|
43
|
+
* plain `import()`. In the CJS build the first attempt is a transpiled
|
|
44
|
+
* `require()`; if the peer turns out to be ESM-only, the load is retried
|
|
45
|
+
* through the ESM-build sibling copy of this file, whose `import()` survived
|
|
46
|
+
* transpilation (see the module doc comment).
|
|
47
|
+
*
|
|
48
|
+
* @param specifier bare package specifier (e.g. `'@zvndev/powdb-client'`).
|
|
49
|
+
* @param allowEsmFallback internal recursion guard — the delegated call passes
|
|
50
|
+
* `false` so a failure in the sibling copy can never bounce back.
|
|
51
|
+
*/
|
|
52
|
+
declare function importOptionalPeer(specifier: string, allowEsmFallback?: boolean): Promise<unknown>;
|
|
53
|
+
export = importOptionalPeer;
|
package/dist/powdb.d.ts
CHANGED
|
@@ -22,8 +22,19 @@
|
|
|
22
22
|
* `string` PKs hold UUID strings.
|
|
23
23
|
* - **No JSON aggregation / link navigation** — single-query nested `with` is
|
|
24
24
|
* impossible → it degrades to batched N+1 loaders (Phase B).
|
|
25
|
-
* - **Single global write lock; no savepoints/isolation
|
|
25
|
+
* - **Single global write lock; no savepoints/isolation** — nested
|
|
26
26
|
* transactions / isolation / vector / LISTEN-NOTIFY / RLS throw.
|
|
27
|
+
* Independent concurrent `db.$transaction` calls do NOT throw: they queue
|
|
28
|
+
* FIFO on a pool-level gate and run one at a time (see {@link PowdbTxGate}).
|
|
29
|
+
* Only a *re-entrant* transaction — a `db.$transaction` opened from inside
|
|
30
|
+
* an active transaction callback's async context, which queueing would
|
|
31
|
+
* deadlock — fails fast with E017.
|
|
32
|
+
* - **The wire protocol pipelines** — `@zvndev/powdb-client` writes each
|
|
33
|
+
* request frame immediately and matches replies FIFO, so multiple queries
|
|
34
|
+
* may be in flight on one connection. {@link PowdbPool}'s checked-out
|
|
35
|
+
* clients advertise `supportsPipelining`, which lets the batch
|
|
36
|
+
* `$transaction([...])` overload dispatch all statements in one write
|
|
37
|
+
* burst (~1 round trip) instead of one round trip per statement.
|
|
27
38
|
*
|
|
28
39
|
* `@zvndev/powdb-client` is an **optional peer dependency** loaded by dynamic
|
|
29
40
|
* import; `npm i turbine-orm` still pulls only `pg`.
|
|
@@ -59,9 +70,11 @@ import type { ColumnMetadata, SchemaMetadata, TableMetadata } from './schema.js'
|
|
|
59
70
|
* {@link UnsupportedFeatureError} (E017): a nested `tx.$transaction` emits a
|
|
60
71
|
* savepoint synchronously (before any DB call) and so fails fast with a
|
|
61
72
|
* clear typed error instead of leaking PowDB's cryptic `Parse(... 'sp_1')`.
|
|
62
|
-
* The pool-level
|
|
63
|
-
* other
|
|
64
|
-
*
|
|
73
|
+
* The pool-level transaction gate (see {@link PowdbTxGate}) handles the
|
|
74
|
+
* other shapes: a fresh top-level `db.$transaction` opened inside an
|
|
75
|
+
* already-open one throws E017 before it can deadlock on the write lock,
|
|
76
|
+
* while INDEPENDENT concurrent `db.$transaction` calls queue FIFO and run
|
|
77
|
+
* one at a time instead of failing.
|
|
65
78
|
* Isolation levels remain Phase B.
|
|
66
79
|
*/
|
|
67
80
|
export declare const powdbDialect: Dialect;
|
|
@@ -155,6 +168,24 @@ export declare function powqlColumnType(col: ColumnMetadata): PowqlType;
|
|
|
155
168
|
* `auto` modifier, so PowDB assigns a monotonic id on insert and Turbine stops
|
|
156
169
|
* synthesizing a client-side value for it.
|
|
157
170
|
*/
|
|
171
|
+
/**
|
|
172
|
+
* PowQL reserved words — the v0.10 lexer keyword table from POWQL.md's
|
|
173
|
+
* "Reserved Words and Quoting" section, including the v0.10 additions
|
|
174
|
+
* `schema` and `describe`. Keyword matching is case-sensitive in the lexer,
|
|
175
|
+
* so only the exact lowercase form collides.
|
|
176
|
+
*/
|
|
177
|
+
export declare const POWQL_KEYWORDS: ReadonlySet<string>;
|
|
178
|
+
/**
|
|
179
|
+
* Backtick-quote an identifier when PowQL would otherwise lex it as a keyword
|
|
180
|
+
* (or when it contains characters outside the bare-identifier grammar).
|
|
181
|
+
* Applied only in bare-identifier positions — DDL type/field names, index DDL,
|
|
182
|
+
* and `insert`/`update`/`upsert` assignment targets. Dotted references
|
|
183
|
+
* (`.col` in filters/projections/ordering) bypass keyword lookup on every
|
|
184
|
+
* engine version and deliberately stay bare for ≤0.9 compatibility. Backticks
|
|
185
|
+
* parse on PowDB ≥ 0.10; on older engines these names were already parse
|
|
186
|
+
* errors when emitted bare, so quoting is strictly an improvement.
|
|
187
|
+
*/
|
|
188
|
+
export declare function quotePowqlIdent(name: string): string;
|
|
158
189
|
export declare function powqlSchemaDDL(schema: SchemaMetadata): string[];
|
|
159
190
|
/**
|
|
160
191
|
* Coerce a single PowDB wire string into the JS value its column type implies.
|
|
@@ -188,6 +219,27 @@ type QueryArg = string | {
|
|
|
188
219
|
text: string;
|
|
189
220
|
values?: unknown[];
|
|
190
221
|
};
|
|
222
|
+
/**
|
|
223
|
+
* Default cap (ms) on how long a `begin` may wait in the FIFO queue for
|
|
224
|
+
* PowDB's single global write lock before failing with a typed
|
|
225
|
+
* {@link TimeoutError} (E002). Prevents silent starvation behind a wedged
|
|
226
|
+
* transaction. Override via `transactionQueueTimeoutMs`
|
|
227
|
+
* ({@link TurbinePowdbOptions} / {@link PowdbPoolOptions}); `0` or `Infinity`
|
|
228
|
+
* waits without limit.
|
|
229
|
+
*/
|
|
230
|
+
export declare const DEFAULT_TX_QUEUE_TIMEOUT_MS = 30000;
|
|
231
|
+
/** Tuning options shared by {@link PowdbPool} and {@link PowdbEmbeddedPool}. */
|
|
232
|
+
export interface PowdbPoolOptions {
|
|
233
|
+
/**
|
|
234
|
+
* Max time (ms) a concurrent transaction's `begin` may wait in the FIFO
|
|
235
|
+
* queue for the single-writer lock before failing with a
|
|
236
|
+
* {@link TimeoutError}. Default {@link DEFAULT_TX_QUEUE_TIMEOUT_MS};
|
|
237
|
+
* `0` / `Infinity` = wait without limit. Note this is a separate surface
|
|
238
|
+
* from `$transaction`'s `timeout` option, which only covers the callback
|
|
239
|
+
* *after* the transaction has begun.
|
|
240
|
+
*/
|
|
241
|
+
transactionQueueTimeoutMs?: number;
|
|
242
|
+
}
|
|
191
243
|
/**
|
|
192
244
|
* A {@link PgCompatPool} backed by a `@zvndev/powdb-client` `Pool`. The query
|
|
193
245
|
* `text` is **PowQL**, not SQL — {@link PowqlInterface} generates it. Rows come
|
|
@@ -199,20 +251,16 @@ export declare class PowdbPool implements PgCompatPool {
|
|
|
199
251
|
private readonly toParam;
|
|
200
252
|
private closed;
|
|
201
253
|
/**
|
|
202
|
-
* Pool-level single-writer
|
|
203
|
-
* most one transaction may be open across the whole pool.
|
|
204
|
-
*
|
|
205
|
-
* connection and
|
|
206
|
-
|
|
207
|
-
private activeTransaction;
|
|
208
|
-
constructor(pool: PowdbClientPool, toParam?: (v: unknown, i: number) => PowdbParam);
|
|
209
|
-
/**
|
|
210
|
-
* Enforce the single-writer model on a transaction-control statement. Throws
|
|
211
|
-
* (before any query runs) if a `begin` arrives while a transaction is open;
|
|
212
|
-
* otherwise flips the pool-level flag. Returns the control kind so the caller
|
|
213
|
-
* can decide whether it even needs to hit the engine.
|
|
254
|
+
* Pool-level single-writer gate. PowDB holds one global write lock, so at
|
|
255
|
+
* most one transaction may be open across the whole pool. Concurrent
|
|
256
|
+
* `begin`s queue FIFO on the gate (instead of checking out a second
|
|
257
|
+
* connection and blocking on the lock forever — the networked hang);
|
|
258
|
+
* re-entrant `begin`s throw E017 (see {@link PowdbTxGate}).
|
|
214
259
|
*/
|
|
215
|
-
private
|
|
260
|
+
private readonly txGate;
|
|
261
|
+
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
262
|
+
private poolHold;
|
|
263
|
+
constructor(pool: PowdbClientPool, toParam?: (v: unknown, i: number) => PowdbParam, options?: PowdbPoolOptions);
|
|
216
264
|
query(text: QueryArg, values?: unknown[]): Promise<any>;
|
|
217
265
|
connect(): Promise<PgCompatPoolClient>;
|
|
218
266
|
end(): Promise<void>;
|
|
@@ -272,17 +320,27 @@ export declare class PowdbEmbeddedPool implements PgCompatPool {
|
|
|
272
320
|
private readonly db;
|
|
273
321
|
private closed;
|
|
274
322
|
/**
|
|
275
|
-
* Single-writer
|
|
323
|
+
* Single-writer gate. The embedded engine is one handle with one global
|
|
276
324
|
* write lock — only one transaction may be open at a time. A re-entrant
|
|
277
|
-
* `begin` (a fresh top-level `db.$transaction` opened inside an open one
|
|
278
|
-
* would otherwise hit PowDB's raw "already in a transaction"
|
|
279
|
-
*
|
|
280
|
-
*
|
|
325
|
+
* `begin` (a fresh top-level `db.$transaction` opened inside an open one's
|
|
326
|
+
* callback) would otherwise hit PowDB's raw "already in a transaction"
|
|
327
|
+
* parse error; the gate surfaces a typed E017 instead, while INDEPENDENT
|
|
328
|
+
* concurrent transactions queue FIFO and run one at a time. (Nested
|
|
329
|
+
* `tx.$transaction` is caught earlier still, by the savepoint override in
|
|
330
|
+
* {@link powdbDialect}.)
|
|
331
|
+
*/
|
|
332
|
+
private readonly txGate;
|
|
333
|
+
/** Hold taken by a `begin` issued via `query()` directly (no checked-out client). */
|
|
334
|
+
private readonly poolHoldRef;
|
|
335
|
+
constructor(db: EmbeddedDatabase, options?: PowdbPoolOptions);
|
|
336
|
+
/** Materialize `$N` params and hand the PowQL to the in-process engine. */
|
|
337
|
+
private exec;
|
|
338
|
+
/**
|
|
339
|
+
* Run one statement, gating transaction control. `holdRef` scopes the gate
|
|
340
|
+
* hold to whoever issued the `begin` (the pool itself or one checked-out
|
|
341
|
+
* client), so finishing a transaction can never release a slot a different
|
|
342
|
+
* transaction is holding.
|
|
281
343
|
*/
|
|
282
|
-
private activeTransaction;
|
|
283
|
-
constructor(db: EmbeddedDatabase);
|
|
284
|
-
/** Enforce the single-writer model on a transaction-control statement. */
|
|
285
|
-
private guardTxControl;
|
|
286
344
|
private run;
|
|
287
345
|
query(text: QueryArg, values?: unknown[]): Promise<any>;
|
|
288
346
|
connect(): Promise<PgCompatPoolClient>;
|
|
@@ -293,6 +351,16 @@ export { PowqlInterface } from './powql.js';
|
|
|
293
351
|
export interface TurbinePowdbOptions extends Pick<TurbineConfig, 'logging' | 'defaultLimit' | 'warnOnUnlimited'> {
|
|
294
352
|
/** Max pooled connections (default 10). Networked transport only. */
|
|
295
353
|
connectionLimit?: number;
|
|
354
|
+
/**
|
|
355
|
+
* Max time (ms) a concurrent `$transaction` waits in the FIFO queue for
|
|
356
|
+
* PowDB's single global write lock before failing with a typed
|
|
357
|
+
* `TimeoutError` (default {@link DEFAULT_TX_QUEUE_TIMEOUT_MS} = 30 000;
|
|
358
|
+
* `0` / `Infinity` = wait without limit). Independent concurrent
|
|
359
|
+
* transactions queue and run one at a time; only a re-entrant
|
|
360
|
+
* `db.$transaction` (opened inside an active transaction callback) throws
|
|
361
|
+
* E017 — queueing that shape would deadlock.
|
|
362
|
+
*/
|
|
363
|
+
transactionQueueTimeoutMs?: number;
|
|
296
364
|
}
|
|
297
365
|
/**
|
|
298
366
|
* Selects the **embedded** transport — an in-process `@zvndev/powdb-embedded`
|