turbine-orm 0.33.0 → 0.35.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 +2 -2
- package/dist/cjs/client.js +26 -4
- package/dist/cjs/dialect.js +1 -0
- package/dist/cjs/errors.js +41 -1
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/mssql.js +5 -0
- package/dist/cjs/mysql.js +4 -0
- package/dist/cjs/optional-peer-import.cjs +28 -0
- package/dist/cjs/powdb-introspect.js +222 -0
- package/dist/cjs/powdb.js +592 -72
- package/dist/cjs/powql.js +998 -134
- package/dist/cjs/query/builder.js +72 -1
- package/dist/cjs/schema-builder.js +16 -0
- package/dist/cjs/schema-metadata.js +81 -10
- package/dist/cjs/sqlite.js +3 -0
- package/dist/client.d.ts +32 -5
- package/dist/client.js +26 -4
- package/dist/dialect.d.ts +13 -0
- package/dist/dialect.js +1 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +39 -0
- package/dist/index-advisor.d.ts +15 -1
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mssql.js +5 -0
- package/dist/mysql.js +4 -0
- package/dist/optional-peer-import.cjs +28 -0
- package/dist/optional-peer-import.d.cts +19 -0
- package/dist/powdb-introspect.d.ts +84 -0
- package/dist/powdb-introspect.js +219 -0
- package/dist/powdb.d.ts +361 -19
- package/dist/powdb.js +585 -72
- package/dist/powql.d.ts +245 -8
- package/dist/powql.js +1001 -137
- package/dist/query/builder.d.ts +36 -1
- package/dist/query/builder.js +72 -1
- package/dist/query/deferred.d.ts +6 -2
- package/dist/query/types.d.ts +49 -12
- package/dist/schema-builder.d.ts +46 -1
- package/dist/schema-builder.js +15 -0
- package/dist/schema-metadata.d.ts +13 -7
- package/dist/schema-metadata.js +82 -11
- package/dist/schema.d.ts +25 -0
- package/dist/sqlite.js +3 -0
- package/package.json +3 -3
|
@@ -177,6 +177,11 @@ class QueryInterface {
|
|
|
177
177
|
warnOnUnlimited;
|
|
178
178
|
utcTimestamps;
|
|
179
179
|
preparedStatementsEnabled;
|
|
180
|
+
/**
|
|
181
|
+
* Whether the SQL template cache is active. Set once in the constructor.
|
|
182
|
+
* Mutable (not `readonly`) only so {@link withSqlCacheDisabled} can flip it
|
|
183
|
+
* off around a single synchronous compile (see {@link explain}).
|
|
184
|
+
*/
|
|
180
185
|
sqlCacheEnabled;
|
|
181
186
|
dialect;
|
|
182
187
|
/** Client-level default relation-loading strategy ('join' unless configured). */
|
|
@@ -870,6 +875,61 @@ class QueryInterface {
|
|
|
870
875
|
return deferred.transform(result);
|
|
871
876
|
});
|
|
872
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Return the engine's query plan for a {@link findMany}-shaped query as plain
|
|
880
|
+
* text lines: a diagnostic surface for inspecting how the database will run a
|
|
881
|
+
* query (index usage, join strategy, scan type).
|
|
882
|
+
*
|
|
883
|
+
* The compiled SELECT is prefixed with the dialect's explain syntax
|
|
884
|
+
* (Postgres `EXPLAIN`, SQLite `EXPLAIN QUERY PLAN`, MySQL `EXPLAIN
|
|
885
|
+
* FORMAT=TREE`) and run as a read. The findMany args are compiled with the
|
|
886
|
+
* SQL template cache disabled, so an explain never reads or writes the shared
|
|
887
|
+
* cache. Middleware is NOT applied: the returned rows are plan text, not
|
|
888
|
+
* entity rows. Each result row is flattened to one line by joining its column
|
|
889
|
+
* values with a single space (Postgres returns one `QUERY PLAN` text column,
|
|
890
|
+
* SQLite's `EXPLAIN QUERY PLAN` returns four, MySQL's tree format one).
|
|
891
|
+
*
|
|
892
|
+
* Only `findMany` shapes are supported (where / orderBy / with / limit /
|
|
893
|
+
* pagination). Engines whose plan cannot be requested in-band from a compiled
|
|
894
|
+
* query (SQL Server, whose SHOWPLAN is a session toggle) throw
|
|
895
|
+
* {@link UnsupportedFeatureError} (E017).
|
|
896
|
+
*
|
|
897
|
+
* The plan text itself is engine-owned and NOT covered by semver: its content
|
|
898
|
+
* and formatting can change with the underlying database version.
|
|
899
|
+
*/
|
|
900
|
+
async explain(args) {
|
|
901
|
+
const explainSyntax = this.dialect.explainQuery;
|
|
902
|
+
if (!explainSyntax) {
|
|
903
|
+
throw new errors_js_1.UnsupportedFeatureError('explain()', this.dialect.name, 'This engine cannot explain a compiled query in-band.');
|
|
904
|
+
}
|
|
905
|
+
// Compile the findMany SQL with the cache disabled: the prefixed EXPLAIN
|
|
906
|
+
// statement is a one-off diagnostic and must never read or write the shared
|
|
907
|
+
// query-template cache.
|
|
908
|
+
const deferred = this.withSqlCacheDisabled(() => this.buildFindMany(args));
|
|
909
|
+
const sql = `${explainSyntax.prefix} ${deferred.sql}`;
|
|
910
|
+
this.currentAction = 'explain';
|
|
911
|
+
// No preparedName: keep the diagnostic statement out of the prepared path.
|
|
912
|
+
const result = await this.queryWithTimeout(sql, deferred.params, args?.timeout);
|
|
913
|
+
return result.rows.map((row) => Object.values(row)
|
|
914
|
+
.map((value) => (typeof value === 'string' ? value : String(value)))
|
|
915
|
+
.join(' '));
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Run `fn` with the SQL template cache forced off, restoring the prior state
|
|
919
|
+
* afterward. Used by {@link explain}, whose one-off prefixed statement must
|
|
920
|
+
* neither read nor write the shared cache. `fn` is synchronous, so no query
|
|
921
|
+
* interleaves between the toggle and its restore.
|
|
922
|
+
*/
|
|
923
|
+
withSqlCacheDisabled(fn) {
|
|
924
|
+
const prev = this.sqlCacheEnabled;
|
|
925
|
+
this.sqlCacheEnabled = false;
|
|
926
|
+
try {
|
|
927
|
+
return fn();
|
|
928
|
+
}
|
|
929
|
+
finally {
|
|
930
|
+
this.sqlCacheEnabled = prev;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
873
933
|
/**
|
|
874
934
|
* Emit a one-time `console.warn` when {@link findMany} is called without an
|
|
875
935
|
* explicit `limit`/`take` and `warnOnUnlimited` has not been disabled.
|
|
@@ -4527,7 +4587,18 @@ class QueryInterface {
|
|
|
4527
4587
|
const extract = this.dialect.buildJsonPathExtract(`${prefix}${this.q(col)}`, this.p(params.length));
|
|
4528
4588
|
const lhs = spec.type === 'numeric' ? this.castJsonNumeric(extract) : extract;
|
|
4529
4589
|
const dir = spec.direction?.toLowerCase() === 'desc' ? 'DESC' : 'ASC';
|
|
4530
|
-
|
|
4590
|
+
// Rows whose document lacks the path extract to NULL. Without a nulls
|
|
4591
|
+
// clause, Postgres DESC defaults to NULLS FIRST, which both diverges from
|
|
4592
|
+
// pick-row ordering (NULLS LAST both directions since 0.33) and from
|
|
4593
|
+
// engines whose path ordering is nulls-last in both directions. Default to
|
|
4594
|
+
// NULLS LAST in BOTH directions unless the caller set `nulls` explicitly;
|
|
4595
|
+
// the grammar gate matches nullsSuffix.
|
|
4596
|
+
const nullsSql = spec.nulls
|
|
4597
|
+
? this.nullsSuffix(spec.nulls)
|
|
4598
|
+
: this.dialect.name === 'postgresql' || this.dialect.name === 'sqlite'
|
|
4599
|
+
? ' NULLS LAST'
|
|
4600
|
+
: '';
|
|
4601
|
+
return `${lhs} ${dir}${nullsSql}`;
|
|
4531
4602
|
}
|
|
4532
4603
|
/**
|
|
4533
4604
|
* Compile a relation ordering term. For a to-many relation the only allowed
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
27
|
exports.camelToSnake = exports.column = exports.ColumnBuilder = void 0;
|
|
28
|
+
exports.isDocFieldIndexDef = isDocFieldIndexDef;
|
|
28
29
|
exports.defineSchema = defineSchema;
|
|
29
30
|
exports.table = table;
|
|
30
31
|
exports.applyManyToManyRelations = applyManyToManyRelations;
|
|
@@ -102,6 +103,10 @@ function resolveColumn(def) {
|
|
|
102
103
|
check: def.check ?? null,
|
|
103
104
|
};
|
|
104
105
|
}
|
|
106
|
+
/** Type guard: is this index declaration a doc-field expression index? */
|
|
107
|
+
function isDocFieldIndexDef(idx) {
|
|
108
|
+
return 'docField' in idx && typeof idx.docField === 'string';
|
|
109
|
+
}
|
|
105
110
|
/** Check if a value is a TableDef (from legacy table() builder) */
|
|
106
111
|
function isTableDef(v) {
|
|
107
112
|
return typeof v === 'object' && v !== null && 'columns' in v && 'name' in v;
|
|
@@ -145,7 +150,17 @@ function defineSchema(input, options) {
|
|
|
145
150
|
let pk;
|
|
146
151
|
let m2m;
|
|
147
152
|
let checks;
|
|
153
|
+
let indexes;
|
|
148
154
|
for (const [fieldName, def] of Object.entries(raw)) {
|
|
155
|
+
if (fieldName === 'indexes') {
|
|
156
|
+
if (def !== undefined) {
|
|
157
|
+
if (!Array.isArray(def)) {
|
|
158
|
+
throw new Error(`Table "${accessor}": "indexes" must be an array of index declarations`);
|
|
159
|
+
}
|
|
160
|
+
indexes = def;
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
149
164
|
if (fieldName === 'manyToMany') {
|
|
150
165
|
if (def !== undefined) {
|
|
151
166
|
if (!Array.isArray(def)) {
|
|
@@ -205,6 +220,7 @@ function defineSchema(input, options) {
|
|
|
205
220
|
...(pk && pk.length > 0 ? { primaryKey: pk } : {}),
|
|
206
221
|
...(m2m && m2m.length > 0 ? { manyToMany: m2m } : {}),
|
|
207
222
|
...(checks && checks.length > 0 ? { checks } : {}),
|
|
223
|
+
...(indexes && indexes.length > 0 ? { indexes } : {}),
|
|
208
224
|
};
|
|
209
225
|
}
|
|
210
226
|
}
|
|
@@ -25,10 +25,12 @@
|
|
|
25
25
|
* the same conservative auto-`manyToMany` treatment as introspection.
|
|
26
26
|
* - Explicit `manyToMany` declarations on the SchemaDef are merged via
|
|
27
27
|
* {@link applyManyToManyRelations} (additive, never clobbering).
|
|
28
|
-
* - `indexes`
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* - `indexes` carries any declared `TableDef.indexes` (plain column and/or
|
|
29
|
+
* PowDB doc-field expression indexes); a table with none declared gets
|
|
30
|
+
* `[]`, which keeps `schemaHasIndexInfo()` false so the index advisor and
|
|
31
|
+
* the dev-mode missing-index warning stay silent instead of producing
|
|
32
|
+
* blanket false positives. Doc-field (docPath) indexes are ignored by the
|
|
33
|
+
* advisor, so a doc-only index set never flips `schemaHasIndexInfo()`.
|
|
32
34
|
*
|
|
33
35
|
* @example
|
|
34
36
|
* ```ts
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
*/
|
|
46
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
49
|
exports.schemaDefToMetadata = schemaDefToMetadata;
|
|
50
|
+
const errors_js_1 = require("./errors.js");
|
|
48
51
|
const introspect_js_1 = require("./introspect.js");
|
|
49
52
|
const schema_js_1 = require("./schema.js");
|
|
50
53
|
const schema_builder_js_1 = require("./schema-builder.js");
|
|
@@ -97,6 +100,67 @@ function resolveColumnName(raw, target) {
|
|
|
97
100
|
return (0, schema_js_1.camelToSnake)(raw);
|
|
98
101
|
}
|
|
99
102
|
// ---------------------------------------------------------------------------
|
|
103
|
+
// Index declarations → IndexMetadata
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
/**
|
|
106
|
+
* Render a doc-field JSON path into an illustrative PowQL fragment for the
|
|
107
|
+
* `IndexMetadata.definition` field (debuggability only; the authoritative,
|
|
108
|
+
* lexer-exact emission lives in `powqlSchemaDDL`). String segments are shown
|
|
109
|
+
* double-quoted, integer array indexes bare.
|
|
110
|
+
*/
|
|
111
|
+
function docPathFragment(column, path) {
|
|
112
|
+
const segs = path.map((s) => (typeof s === 'number' ? `->${s}` : `->"${s}"`)).join('');
|
|
113
|
+
return `(.${column}${segs})`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Convert a table's {@link SchemaIndexDef} list into {@link IndexMetadata}.
|
|
117
|
+
* A doc-field index carries `docPath` and `columns: [<json column>]`; a plain
|
|
118
|
+
* column index carries its snake_case column list and no `docPath`. Names are
|
|
119
|
+
* auto-derived (`<table>_<cols>_idx`) when not supplied.
|
|
120
|
+
*/
|
|
121
|
+
function mapIndexes(tableDef, declared) {
|
|
122
|
+
if (!declared || declared.length === 0)
|
|
123
|
+
return [];
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const idx of declared) {
|
|
126
|
+
if ((0, schema_builder_js_1.isDocFieldIndexDef)(idx)) {
|
|
127
|
+
const column = (0, schema_js_1.camelToSnake)(idx.docField);
|
|
128
|
+
const segPart = idx.path.map((s) => (typeof s === 'number' ? String(s) : s)).join('_');
|
|
129
|
+
const name = idx.name ?? `${tableDef.name}_${column}_${segPart}_idx`;
|
|
130
|
+
// Validate numeric (array-index) segments up front: PowDB rejects a
|
|
131
|
+
// negative / fractional / NaN JSON-path index at migration time with an
|
|
132
|
+
// opaque parse error, so fail here with a typed ValidationError naming the
|
|
133
|
+
// index instead of emitting malformed PowQL later.
|
|
134
|
+
for (const seg of idx.path) {
|
|
135
|
+
if (typeof seg === 'number' && (!Number.isInteger(seg) || seg < 0)) {
|
|
136
|
+
throw new errors_js_1.ValidationError(`[turbine] Doc-field index "${name}" on "${tableDef.name}": array-index path segment ${seg} must be a ` +
|
|
137
|
+
'non-negative integer (a JSON array index). Use a string for an object key.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
out.push({
|
|
141
|
+
name,
|
|
142
|
+
columns: [column],
|
|
143
|
+
unique: idx.unique ?? false,
|
|
144
|
+
definition: `${idx.unique ? 'unique ' : 'index '}${docPathFragment(column, idx.path)}`,
|
|
145
|
+
docPath: [...idx.path],
|
|
146
|
+
declared: true,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
const columns = idx.columns.map(schema_js_1.camelToSnake);
|
|
151
|
+
const name = idx.name ?? `${tableDef.name}_${columns.join('_')}_idx`;
|
|
152
|
+
out.push({
|
|
153
|
+
name,
|
|
154
|
+
columns,
|
|
155
|
+
unique: idx.unique ?? false,
|
|
156
|
+
definition: `${idx.unique ? 'unique ' : 'index '}(${columns.join(', ')})`,
|
|
157
|
+
declared: true,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
100
164
|
// The converter
|
|
101
165
|
// ---------------------------------------------------------------------------
|
|
102
166
|
/**
|
|
@@ -122,10 +186,14 @@ function resolveColumnName(raw, target) {
|
|
|
122
186
|
* - Explicit `manyToMany` declarations → merged additively.
|
|
123
187
|
* - Schema-level `enums`.
|
|
124
188
|
*
|
|
189
|
+
* What maps (continued):
|
|
190
|
+
* - `indexes` → declared `TableDef.indexes` become `IndexMetadata` (plain
|
|
191
|
+
* column indexes and PowDB doc-field expression indexes, the latter
|
|
192
|
+
* carrying `docPath`). A table with no declared indexes gets `[]`, keeping
|
|
193
|
+
* `schemaHasIndexInfo()` false so index-advisor consumers produce no false
|
|
194
|
+
* positives on index-less code-first metadata.
|
|
195
|
+
*
|
|
125
196
|
* What SchemaDef cannot express (and how it degrades):
|
|
126
|
-
* - Indexes → every table gets `indexes: []`, which keeps
|
|
127
|
-
* `schemaHasIndexInfo()` false so index-advisor consumers produce no
|
|
128
|
-
* false positives on code-first metadata.
|
|
129
197
|
* - Views → never marked (`isView` is introspection-only).
|
|
130
198
|
* - Composite foreign keys → `references:` is single-column by design.
|
|
131
199
|
*/
|
|
@@ -302,9 +370,12 @@ function schemaDefToMetadata(def) {
|
|
|
302
370
|
primaryKey: pk,
|
|
303
371
|
uniqueColumns,
|
|
304
372
|
relations: relationsByTable.get(tableDef.name) ?? {},
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
indexes
|
|
373
|
+
// Declared `indexes` (plain column + doc-field expression) carry through;
|
|
374
|
+
// an undeclared table gets `[]`, which keeps schemaHasIndexInfo() false so
|
|
375
|
+
// the index advisor stays silent. Doc-field (docPath) indexes are ignored
|
|
376
|
+
// by the advisor entirely (see index-advisor.ts), so a doc-only index set
|
|
377
|
+
// never flips schemaHasIndexInfo() and never produces FK false positives.
|
|
378
|
+
indexes: mapIndexes(tableDef, tableDef.indexes),
|
|
308
379
|
};
|
|
309
380
|
}
|
|
310
381
|
const enums = {};
|
package/dist/cjs/sqlite.js
CHANGED
|
@@ -380,6 +380,9 @@ exports.sqliteDialect = {
|
|
|
380
380
|
supportsAdvisoryLock: false,
|
|
381
381
|
// No FROM-clause LATERAL: the opt-in lateral pick plan is Postgres-only.
|
|
382
382
|
supportsLateralJoin: false,
|
|
383
|
+
// SQLite explains a compiled query with `EXPLAIN QUERY PLAN` (four columns:
|
|
384
|
+
// id, parent, notused, detail), overriding the inherited Postgres `EXPLAIN`.
|
|
385
|
+
explainQuery: { prefix: 'EXPLAIN QUERY PLAN' },
|
|
383
386
|
// json_group_array / json_object have no inline ORDER BY argument, so every
|
|
384
387
|
// ordered to-many relation is forced through the inner-subquery rewrite.
|
|
385
388
|
aggSupportsInlineOrderBy: false,
|
package/dist/client.d.ts
CHANGED
|
@@ -181,13 +181,17 @@ export interface TurbineConfig {
|
|
|
181
181
|
* Default strategy for resolving `with`-clause relations, applied to every
|
|
182
182
|
* `findMany`/`findUnique`/`findFirst` unless overridden per query.
|
|
183
183
|
*
|
|
184
|
-
* - `'join'
|
|
185
|
-
* `json_agg(json_build_object(...))` subqueries.
|
|
186
|
-
*
|
|
184
|
+
* - `'join'`: one SQL statement using correlated
|
|
185
|
+
* `json_agg(json_build_object(...))` subqueries. On PowDB, `'join'` opts
|
|
186
|
+
* into native server-side joins where eligible instead.
|
|
187
|
+
* - `'batched'`: run the base query, then one flat follow-up query per
|
|
187
188
|
* relation (`WHERE fk = ANY($1)`), stitching children client-side. Wins
|
|
188
189
|
* when child FK columns are unindexed or result sets are large.
|
|
189
190
|
*
|
|
190
|
-
* Precedence: per-query `relationLoadStrategy` arg > this config >
|
|
191
|
+
* Precedence: per-query `relationLoadStrategy` arg > this config > the engine
|
|
192
|
+
* default. On SQL engines the default is `'join'`; on PowDB the default is the
|
|
193
|
+
* batched loaders (an ineligible relation falls back to them silently even
|
|
194
|
+
* under `'join'`).
|
|
191
195
|
*/
|
|
192
196
|
relationLoadStrategy?: RelationLoadStrategy;
|
|
193
197
|
/**
|
|
@@ -341,11 +345,34 @@ export declare class TransactionClient {
|
|
|
341
345
|
readonly schema: SchemaMetadata;
|
|
342
346
|
private readonly middlewares;
|
|
343
347
|
private readonly queryOptions?;
|
|
348
|
+
/**
|
|
349
|
+
* The parent pool this transaction runs on. Only its `readonly` and
|
|
350
|
+
* `capabilities` are read (both PowDB-only flags), so the transaction-scoped
|
|
351
|
+
* proxy pool built by {@link createTxPool} carries them through: without this
|
|
352
|
+
* a read-only client's `$transaction` writes bypass the E018 guard, and an
|
|
353
|
+
* older-engine client falls back to ALL_POWDB_CAPABILITIES inside the tx
|
|
354
|
+
* (emitting join PowQL a pre-0.13 engine rejects). Undefined / absent flags
|
|
355
|
+
* for a plain pg pool leave the proxy unchanged.
|
|
356
|
+
*/
|
|
357
|
+
private readonly sourcePool?;
|
|
344
358
|
private readonly tableCache;
|
|
345
359
|
private savepointCounter;
|
|
346
360
|
/** Active SQL dialect — owns savepoint keywords and raw-SQL placeholders. */
|
|
347
361
|
private readonly dialect;
|
|
348
|
-
constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[], queryOptions?: QueryInterfaceOptions | undefined
|
|
362
|
+
constructor(client: pg.PoolClient, schema: SchemaMetadata, middlewares: Middleware[], queryOptions?: QueryInterfaceOptions | undefined,
|
|
363
|
+
/**
|
|
364
|
+
* The parent pool this transaction runs on. Only its `readonly` and
|
|
365
|
+
* `capabilities` are read (both PowDB-only flags), so the transaction-scoped
|
|
366
|
+
* proxy pool built by {@link createTxPool} carries them through: without this
|
|
367
|
+
* a read-only client's `$transaction` writes bypass the E018 guard, and an
|
|
368
|
+
* older-engine client falls back to ALL_POWDB_CAPABILITIES inside the tx
|
|
369
|
+
* (emitting join PowQL a pre-0.13 engine rejects). Undefined / absent flags
|
|
370
|
+
* for a plain pg pool leave the proxy unchanged.
|
|
371
|
+
*/
|
|
372
|
+
sourcePool?: {
|
|
373
|
+
readonly readonly?: boolean;
|
|
374
|
+
readonly capabilities?: unknown;
|
|
375
|
+
} | undefined);
|
|
349
376
|
/**
|
|
350
377
|
* Get a QueryInterface for a table within this transaction.
|
|
351
378
|
* Uses the dedicated transaction connection instead of the pool.
|
package/dist/client.js
CHANGED
|
@@ -104,15 +104,27 @@ export class TransactionClient {
|
|
|
104
104
|
schema;
|
|
105
105
|
middlewares;
|
|
106
106
|
queryOptions;
|
|
107
|
+
sourcePool;
|
|
107
108
|
tableCache = new Map();
|
|
108
109
|
savepointCounter = 0;
|
|
109
110
|
/** Active SQL dialect — owns savepoint keywords and raw-SQL placeholders. */
|
|
110
111
|
dialect;
|
|
111
|
-
constructor(client, schema, middlewares, queryOptions
|
|
112
|
+
constructor(client, schema, middlewares, queryOptions,
|
|
113
|
+
/**
|
|
114
|
+
* The parent pool this transaction runs on. Only its `readonly` and
|
|
115
|
+
* `capabilities` are read (both PowDB-only flags), so the transaction-scoped
|
|
116
|
+
* proxy pool built by {@link createTxPool} carries them through: without this
|
|
117
|
+
* a read-only client's `$transaction` writes bypass the E018 guard, and an
|
|
118
|
+
* older-engine client falls back to ALL_POWDB_CAPABILITIES inside the tx
|
|
119
|
+
* (emitting join PowQL a pre-0.13 engine rejects). Undefined / absent flags
|
|
120
|
+
* for a plain pg pool leave the proxy unchanged.
|
|
121
|
+
*/
|
|
122
|
+
sourcePool) {
|
|
112
123
|
this.client = client;
|
|
113
124
|
this.schema = schema;
|
|
114
125
|
this.middlewares = middlewares;
|
|
115
126
|
this.queryOptions = queryOptions;
|
|
127
|
+
this.sourcePool = sourcePool;
|
|
116
128
|
this.dialect = queryOptions?.dialect ?? postgresDialect;
|
|
117
129
|
// Auto-create typed table accessors for all tables in the schema
|
|
118
130
|
for (const tableName of Object.keys(schema.tables)) {
|
|
@@ -192,7 +204,7 @@ export class TransactionClient {
|
|
|
192
204
|
const client = this.client;
|
|
193
205
|
// Return a minimal pool-compatible object that routes queries
|
|
194
206
|
// through the transaction client
|
|
195
|
-
|
|
207
|
+
const txPool = {
|
|
196
208
|
query: async (textOrConfig, values) => {
|
|
197
209
|
try {
|
|
198
210
|
if (typeof textOrConfig === 'string') {
|
|
@@ -209,6 +221,14 @@ export class TransactionClient {
|
|
|
209
221
|
},
|
|
210
222
|
connect: () => Promise.resolve(client),
|
|
211
223
|
};
|
|
224
|
+
// Carry the parent pool's PowDB-only flags through so a transaction-scoped
|
|
225
|
+
// PowqlInterface reads the same read-only guard and capabilities it would
|
|
226
|
+
// outside the transaction (a plain pg pool has neither, so nothing changes).
|
|
227
|
+
if (this.sourcePool?.readonly !== undefined)
|
|
228
|
+
txPool.readonly = this.sourcePool.readonly;
|
|
229
|
+
if (this.sourcePool?.capabilities !== undefined)
|
|
230
|
+
txPool.capabilities = this.sourcePool.capabilities;
|
|
231
|
+
return txPool;
|
|
212
232
|
}
|
|
213
233
|
}
|
|
214
234
|
// ---------------------------------------------------------------------------
|
|
@@ -876,8 +896,10 @@ export class TurbineClient {
|
|
|
876
896
|
await client.query(cfg.sql, cfg.params);
|
|
877
897
|
}
|
|
878
898
|
}
|
|
879
|
-
// Create the transaction client with typed table accessors
|
|
880
|
-
|
|
899
|
+
// Create the transaction client with typed table accessors. Pass the
|
|
900
|
+
// parent pool so its read-only guard + PowDB capabilities flow into the
|
|
901
|
+
// transaction-scoped proxy pool (see TransactionClient.createTxPool).
|
|
902
|
+
const tx = new TransactionClient(client, this.schema, this.middlewares, this.queryOptions, this.pool);
|
|
881
903
|
// Dynamically attach table accessors to tx
|
|
882
904
|
for (const tableName of Object.keys(this.schema.tables)) {
|
|
883
905
|
const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
package/dist/dialect.d.ts
CHANGED
|
@@ -302,6 +302,19 @@ export interface Dialect {
|
|
|
302
302
|
* lateral plan (else E017). PostgreSQL only in this release.
|
|
303
303
|
*/
|
|
304
304
|
readonly supportsLateralJoin?: boolean;
|
|
305
|
+
/**
|
|
306
|
+
* How this dialect surfaces a query plan for a compiled SELECT. When present,
|
|
307
|
+
* `QueryInterface.explain()` prepends `prefix` (plus a single space) to the
|
|
308
|
+
* compiled findMany SQL and runs it as a read, returning the plan text lines.
|
|
309
|
+
* PostgreSQL / CockroachDB / YugabyteDB and MySQL use `EXPLAIN`, SQLite
|
|
310
|
+
* `EXPLAIN QUERY PLAN`. Absent means the engine
|
|
311
|
+
* cannot explain a compiled query in-band (SQL Server, whose SHOWPLAN needs a
|
|
312
|
+
* separate session toggle), so `QueryInterface.explain()` throws E017.
|
|
313
|
+
* Optional: dialects that predate this hook keep throwing E017.
|
|
314
|
+
*/
|
|
315
|
+
readonly explainQuery?: {
|
|
316
|
+
prefix: string;
|
|
317
|
+
};
|
|
305
318
|
/** Build a dialect-specific RETURNING clause. Return an empty string when unsupported. */
|
|
306
319
|
buildReturningClause(selection?: string): string;
|
|
307
320
|
/** Build a single-row INSERT statement. Inputs are SQL-ready quoted fragments. */
|
package/dist/dialect.js
CHANGED
package/dist/errors.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export declare const TurbineErrorCode: {
|
|
|
23
23
|
readonly OPTIMISTIC_LOCK: "TURBINE_E015";
|
|
24
24
|
readonly EXCLUSION_VIOLATION: "TURBINE_E016";
|
|
25
25
|
readonly UNSUPPORTED_FEATURE: "TURBINE_E017";
|
|
26
|
+
readonly READ_ONLY: "TURBINE_E018";
|
|
26
27
|
};
|
|
27
28
|
export type TurbineErrorCode = (typeof TurbineErrorCode)[keyof typeof TurbineErrorCode];
|
|
28
29
|
/** Base error class for all Turbine errors */
|
|
@@ -303,6 +304,41 @@ export declare class UnsupportedFeatureError extends TurbineError {
|
|
|
303
304
|
readonly dialect: string;
|
|
304
305
|
constructor(feature: string, dialect: string, hint?: string);
|
|
305
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Thrown when a write or DDL statement is refused because the target is
|
|
309
|
+
* read-only. Two shapes reach here, both on PowDB:
|
|
310
|
+
* - an embedded database opened read-only for snapshot serving refuses a write
|
|
311
|
+
* with `readonly mode: statement requires a writer …`;
|
|
312
|
+
* - a networked read-only role refuses a write with `permission denied: role
|
|
313
|
+
* '<role>' cannot execute write statements` (translated by `wrapPowdbError`).
|
|
314
|
+
* It is also raised locally, before the wire, when a write is issued on a pool
|
|
315
|
+
* the caller marked read-only (fail-fast). The message carries the engine text
|
|
316
|
+
* plus a hint to route writes to a writable primary.
|
|
317
|
+
*
|
|
318
|
+
* NOT retryable: the same write against the same read-only target fails
|
|
319
|
+
* identically; route it to a writable primary instead.
|
|
320
|
+
*/
|
|
321
|
+
export declare class ReadOnlyError extends TurbineError {
|
|
322
|
+
/**
|
|
323
|
+
* Why the write was refused. `'snapshot'`: the database itself is read-only
|
|
324
|
+
* (snapshot serving, an embedded `readonly: true` open, or the client-level
|
|
325
|
+
* fail-fast flag), so NOTHING can write here and writes must route to the
|
|
326
|
+
* primary. `'rbac'`: the database is writable but THIS connection's role may
|
|
327
|
+
* not write (per-connection permission), so re-authenticating may suffice.
|
|
328
|
+
*/
|
|
329
|
+
readonly reason: 'snapshot' | 'rbac';
|
|
330
|
+
/**
|
|
331
|
+
* @param detail human-readable description of the refused write (the engine
|
|
332
|
+
* message, or a local fail-fast description). A "route writes to a writable
|
|
333
|
+
* primary" hint is always appended.
|
|
334
|
+
* @param options optional driver `cause` to preserve when wrapping a refusal,
|
|
335
|
+
* and the refusal `reason` (default `'snapshot'`).
|
|
336
|
+
*/
|
|
337
|
+
constructor(detail: string, options?: {
|
|
338
|
+
cause?: unknown;
|
|
339
|
+
reason?: 'snapshot' | 'rbac';
|
|
340
|
+
});
|
|
341
|
+
}
|
|
306
342
|
/**
|
|
307
343
|
* Translate a pg driver error into a typed Turbine error.
|
|
308
344
|
* If the error doesn't match a known constraint code, returns it unchanged.
|
package/dist/errors.js
CHANGED
|
@@ -23,6 +23,7 @@ export const TurbineErrorCode = {
|
|
|
23
23
|
OPTIMISTIC_LOCK: 'TURBINE_E015',
|
|
24
24
|
EXCLUSION_VIOLATION: 'TURBINE_E016',
|
|
25
25
|
UNSUPPORTED_FEATURE: 'TURBINE_E017',
|
|
26
|
+
READ_ONLY: 'TURBINE_E018',
|
|
26
27
|
};
|
|
27
28
|
/**
|
|
28
29
|
* Prefix a human message with its stable error code so logs are greppable
|
|
@@ -479,6 +480,44 @@ export class UnsupportedFeatureError extends TurbineError {
|
|
|
479
480
|
this.dialect = dialect;
|
|
480
481
|
}
|
|
481
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Thrown when a write or DDL statement is refused because the target is
|
|
485
|
+
* read-only. Two shapes reach here, both on PowDB:
|
|
486
|
+
* - an embedded database opened read-only for snapshot serving refuses a write
|
|
487
|
+
* with `readonly mode: statement requires a writer …`;
|
|
488
|
+
* - a networked read-only role refuses a write with `permission denied: role
|
|
489
|
+
* '<role>' cannot execute write statements` (translated by `wrapPowdbError`).
|
|
490
|
+
* It is also raised locally, before the wire, when a write is issued on a pool
|
|
491
|
+
* the caller marked read-only (fail-fast). The message carries the engine text
|
|
492
|
+
* plus a hint to route writes to a writable primary.
|
|
493
|
+
*
|
|
494
|
+
* NOT retryable: the same write against the same read-only target fails
|
|
495
|
+
* identically; route it to a writable primary instead.
|
|
496
|
+
*/
|
|
497
|
+
export class ReadOnlyError extends TurbineError {
|
|
498
|
+
/**
|
|
499
|
+
* Why the write was refused. `'snapshot'`: the database itself is read-only
|
|
500
|
+
* (snapshot serving, an embedded `readonly: true` open, or the client-level
|
|
501
|
+
* fail-fast flag), so NOTHING can write here and writes must route to the
|
|
502
|
+
* primary. `'rbac'`: the database is writable but THIS connection's role may
|
|
503
|
+
* not write (per-connection permission), so re-authenticating may suffice.
|
|
504
|
+
*/
|
|
505
|
+
reason;
|
|
506
|
+
/**
|
|
507
|
+
* @param detail human-readable description of the refused write (the engine
|
|
508
|
+
* message, or a local fail-fast description). A "route writes to a writable
|
|
509
|
+
* primary" hint is always appended.
|
|
510
|
+
* @param options optional driver `cause` to preserve when wrapping a refusal,
|
|
511
|
+
* and the refusal `reason` (default `'snapshot'`).
|
|
512
|
+
*/
|
|
513
|
+
constructor(detail, options) {
|
|
514
|
+
super(TurbineErrorCode.READ_ONLY, `[turbine] ${detail} Route writes to a writable primary.`, {
|
|
515
|
+
cause: options?.cause,
|
|
516
|
+
});
|
|
517
|
+
this.name = 'ReadOnlyError';
|
|
518
|
+
this.reason = options?.reason ?? 'snapshot';
|
|
519
|
+
}
|
|
520
|
+
}
|
|
482
521
|
/**
|
|
483
522
|
* Parse column names out of a pg `detail` string like:
|
|
484
523
|
* "Key (email)=(foo@bar) already exists."
|
package/dist/index-advisor.d.ts
CHANGED
|
@@ -58,7 +58,21 @@ export declare function isProbeIndexed(meta: TableMetadata, columns: string[]):
|
|
|
58
58
|
* should gate on {@link schemaHasIndexInfo} to avoid blanket false positives.
|
|
59
59
|
*/
|
|
60
60
|
export declare function findMissingRelationIndexes(schema: SchemaMetadata): MissingRelationIndex[];
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* True when at least one table in the schema carries real, DB-backed index
|
|
63
|
+
* metadata (i.e. from introspection).
|
|
64
|
+
*
|
|
65
|
+
* Excluded (so they never flip the flag, keeping the schema "index-info
|
|
66
|
+
* unknown"):
|
|
67
|
+
* - doc-field expression indexes (`docPath`): they carry no FK-coverage info;
|
|
68
|
+
* - code-first DECLARED indexes (`declared`): the SQL DDL generators do NOT
|
|
69
|
+
* emit `TableDef.indexes` yet, so a declared index does not reflect a real
|
|
70
|
+
* index on the SQL engines. Counting one would arm blanket FK false
|
|
71
|
+
* positives (the FK auto-index the push path DID create is then reported
|
|
72
|
+
* "missing") and, inversely, suppress warnings for indexes never created.
|
|
73
|
+
* A pure code-first schema therefore stays silent exactly as it did before
|
|
74
|
+
* `TableDef.indexes` existed.
|
|
75
|
+
*/
|
|
62
76
|
export declare function schemaHasIndexInfo(schema: SchemaMetadata): boolean;
|
|
63
77
|
/**
|
|
64
78
|
* Single-relation check for the dev-mode runtime warning: the (table, columns)
|
package/dist/index-advisor.js
CHANGED
|
Binary file
|
package/dist/index.d.ts
CHANGED
|
@@ -37,17 +37,17 @@ export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapt
|
|
|
37
37
|
export { type Middleware, type MiddlewareNext, type MiddlewareParams, type PgCompatPool, type PgCompatPoolClient, type PgCompatQueryResult, type RetryOptions, TransactionClient, type TransactionOptions, TurbineClient, type TurbineConfig, type TurbineDriver, withRetry, } from './client.js';
|
|
38
38
|
export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, DialectIntrospector, DialectMigrator, DialectName, InsertStatementInput, IntrospectOptions as DialectIntrospectOptions, ResultStrategy, StreamableConnection, UpsertStatementInput, } from './dialect.js';
|
|
39
39
|
export { postgresDialect } from './dialect.js';
|
|
40
|
-
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, type PipelineResultSlot, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
40
|
+
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, type ErrorMessageMode, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, type PipelineResultSlot, ReadOnlyError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
41
41
|
export { type GenerateOptions, generate } from './generate.js';
|
|
42
42
|
export { type IntrospectOptions, introspect } from './introspect.js';
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export type { ObserveConfig, ObserveHandle } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
46
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
47
47
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
48
48
|
export type { CheckMetadata, ColumnMetadata, IndexMetadata, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
49
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
50
|
-
export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, defineSchema, type ManyToManyDef, type ReferenceDef, type SchemaDef, type TableDef, table, } from './schema-builder.js';
|
|
50
|
+
export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnIndexDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, type DocFieldIndexDef, defineSchema, isDocFieldIndexDef, type ManyToManyDef, type ReferenceDef, type SchemaDef, type SchemaIndexDef, type TableDef, table, } from './schema-builder.js';
|
|
51
51
|
export { schemaDefToMetadata } from './schema-metadata.js';
|
|
52
52
|
export { type AlterColumnDef, type AlterDef, type DiffResult, type PushResult, type SchemaSqlOptions, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
53
53
|
export { type DefinedSeed, defineSeed, type SeedFunction } from './seed.js';
|
package/dist/index.js
CHANGED
|
@@ -37,7 +37,7 @@ export { alloydb, cockroachdb, postgresql, timescale, yugabytedb } from './adapt
|
|
|
37
37
|
export { TransactionClient, TurbineClient, withRetry, } from './client.js';
|
|
38
38
|
export { postgresDialect } from './dialect.js';
|
|
39
39
|
// Error types
|
|
40
|
-
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
40
|
+
export { CheckConstraintError, CircularRelationError, ConnectionError, DeadlockError, ExclusionConstraintError, ForeignKeyError, getErrorMessageMode, MigrationError, NotFoundError, NotNullViolationError, OptimisticLockError, PipelineError, ReadOnlyError, RelationError, SerializationFailureError, setErrorMessageMode, TimeoutError, TurbineError, TurbineErrorCode, UniqueConstraintError, UnsupportedFeatureError, ValidationError, wrapPgError, } from './errors.js';
|
|
41
41
|
// Code generation
|
|
42
42
|
export { generate } from './generate.js';
|
|
43
43
|
// Introspection
|
|
@@ -53,7 +53,7 @@ export { validateChannel } from './realtime.js';
|
|
|
53
53
|
// Schema utilities
|
|
54
54
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
55
55
|
// Schema builder — define schemas in TypeScript
|
|
56
|
-
export { applyManyToManyRelations, ColumnBuilder, column, defineSchema,
|
|
56
|
+
export { applyManyToManyRelations, ColumnBuilder, column, defineSchema, isDocFieldIndexDef,
|
|
57
57
|
// Legacy compat (deprecated — use object format with defineSchema)
|
|
58
58
|
table, } from './schema-builder.js';
|
|
59
59
|
// Schema metadata bridge — defineSchema() → SchemaMetadata without a live DB
|
package/dist/mssql.js
CHANGED
|
@@ -472,6 +472,11 @@ export const mssqlDialect = {
|
|
|
472
472
|
supportsLateralJoin: false,
|
|
473
473
|
// sp_getapplock / sp_releaseapplock exist (used by a future migrate adapter).
|
|
474
474
|
supportsAdvisoryLock: true,
|
|
475
|
+
// No in-band EXPLAIN: SQL Server's SHOWPLAN is a session toggle
|
|
476
|
+
// (SET SHOWPLAN_ALL ON), not a statement prefix, so a compiled query cannot
|
|
477
|
+
// be explained in one round-trip. Override the inherited Postgres `EXPLAIN`
|
|
478
|
+
// to absent → QueryInterface.explain() throws E017.
|
|
479
|
+
explainQuery: undefined,
|
|
475
480
|
// FOR JSON over zero rows is NULL → coalesced in the relation override.
|
|
476
481
|
aggSupportsInlineOrderBy: false,
|
|
477
482
|
jsonPathSupport: 'limited',
|
package/dist/mysql.js
CHANGED
|
@@ -373,6 +373,10 @@ export const mysqlDialect = {
|
|
|
373
373
|
supportsLateralJoin: false,
|
|
374
374
|
// GET_LOCK / RELEASE_LOCK exist (used by a future migrate adapter).
|
|
375
375
|
supportsAdvisoryLock: true,
|
|
376
|
+
// Plain `EXPLAIN` (one row of tabular plan columns) works on every supported
|
|
377
|
+
// MySQL 8.0.x; the readable `FORMAT=TREE` variant only exists from 8.0.16 and
|
|
378
|
+
// the engine floor here is 8.0.0. Plan text is a diagnostic, not a contract.
|
|
379
|
+
explainQuery: { prefix: 'EXPLAIN' },
|
|
376
380
|
// JSON_ARRAYAGG has no inline ORDER BY argument → force the inner-subquery
|
|
377
381
|
// rewrite for every ordered to-many relation.
|
|
378
382
|
aggSupportsInlineOrderBy: false,
|