turbine-orm 0.47.0 → 0.48.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/dist/cjs/powdb-introspect.js +121 -2
- package/dist/cjs/powdb.js +188 -5
- package/dist/cjs/powql.js +207 -8
- package/dist/cjs/query/warn-registry.js +2 -0
- package/dist/powdb-introspect.js +122 -3
- package/dist/powdb.d.ts +100 -3
- package/dist/powdb.js +185 -5
- package/dist/powql.d.ts +51 -0
- package/dist/powql.js +207 -8
- package/dist/query/warn-registry.d.ts +2 -0
- package/dist/query/warn-registry.js +2 -0
- package/package.json +3 -3
|
@@ -128,7 +128,16 @@ async function introspectPowdbDatabase(exec, options = {}) {
|
|
|
128
128
|
// `describe` needs the table name in bare-identifier position → quote it so
|
|
129
129
|
// a reserved-word / non-bare table name (`order`) does not become a parse
|
|
130
130
|
// error.
|
|
131
|
-
const describeRows = (await exec(`describe ${(0, powdb_js_1.quotePowqlIdent)(tableName)}`)).rows
|
|
131
|
+
const describeRows = (await exec(`describe ${(0, powdb_js_1.quotePowqlIdent)(tableName)}`)).rows
|
|
132
|
+
// PowDB >= 0.19.1 appends entity-LINK rows after the 4 column rows in
|
|
133
|
+
// `describe <T>` (a `type` cell of literally `"link"`, an Empty nullable
|
|
134
|
+
// slot, and an arrow description in the index slot). They are NOT columns,
|
|
135
|
+
// so drop them before column parsing, unconditionally (no capability check):
|
|
136
|
+
// the rows simply never appear on a pre-0.19.1 engine, and a linked database
|
|
137
|
+
// introspected without this filter would produce garbage `link`-typed
|
|
138
|
+
// columns. Declared links are read separately via `schema links` below.
|
|
139
|
+
.filter((r) => asString(r.type) !== 'link')
|
|
140
|
+
.map((r) => ({
|
|
132
141
|
column: asString(r.column),
|
|
133
142
|
type: asString(r.type),
|
|
134
143
|
nullable: asBool(r.nullable),
|
|
@@ -208,10 +217,120 @@ async function introspectPowdbDatabase(exec, options = {}) {
|
|
|
208
217
|
allColumns,
|
|
209
218
|
primaryKey,
|
|
210
219
|
uniqueColumns,
|
|
211
|
-
//
|
|
220
|
+
// Populated below from `schema links` when the engine supports link
|
|
221
|
+
// introspection (>= 0.19.1); stays `{}` otherwise (PowDB has no declared
|
|
222
|
+
// foreign keys, so pre-link introspection reports no relations).
|
|
212
223
|
relations: {},
|
|
213
224
|
indexes,
|
|
214
225
|
};
|
|
215
226
|
}
|
|
227
|
+
// Link introspection (>= 0.19.1): populate relations from declared entity links.
|
|
228
|
+
// Behaves exactly as before (relations stay `{}`) when the capability is absent
|
|
229
|
+
// or not supplied.
|
|
230
|
+
if (options.capabilities?.linkIntrospection) {
|
|
231
|
+
await introspectPowdbLinks(exec, tables);
|
|
232
|
+
}
|
|
216
233
|
return { tables, enums: {} };
|
|
217
234
|
}
|
|
235
|
+
/**
|
|
236
|
+
* Read `schema links` and populate {@link TableMetadata.relations}: the first
|
|
237
|
+
* time PowDB introspection can report relations. Each declared link becomes a
|
|
238
|
+
* relation on its OWNER, and its natural REVERSE is synthesized on the target
|
|
239
|
+
* (mirroring the SQL introspector, which always emits both sides of a FK):
|
|
240
|
+
*
|
|
241
|
+
* - `"to-one"` link `Order.user -> User`: owner gets a `belongsTo` (localKey =
|
|
242
|
+
* the FK on the owner, targetKey = the referenced key on target); the target
|
|
243
|
+
* gets a reverse `hasMany` named for the pluralized owner table.
|
|
244
|
+
* - `"to-many"` link `User.orders -> Order`: owner gets a `hasMany` (localKey =
|
|
245
|
+
* the referenced key on the owner, targetKey = the FK on the child); the
|
|
246
|
+
* target gets a reverse `belongsTo` named for the singularized owner table.
|
|
247
|
+
*
|
|
248
|
+
* Reverse synthesis is best-effort and collision-guarded: a synthesized name that
|
|
249
|
+
* would shadow a column field or an already-present relation on the target is
|
|
250
|
+
* skipped (never a hard error), so introspection can only ADD relations, never
|
|
251
|
+
* clobber. m2m junctions cannot be inferred from links and stay undetected;
|
|
252
|
+
* `defineSchema` remains the relation-complete path. Column names arrive as
|
|
253
|
+
* PowDB's emitted (snake) names and stay snake in `foreignKey`/`referenceKey`
|
|
254
|
+
* (the SQL introspector's convention); the relation NAME is camelCased to match
|
|
255
|
+
* the query field surface. Naming edge: `schema links` is the contextual listing
|
|
256
|
+
* keyword: a table literally named `links` is still read via `describe links`.
|
|
257
|
+
*/
|
|
258
|
+
async function introspectPowdbLinks(exec, tables) {
|
|
259
|
+
const linkRows = (await exec('schema links')).rows.map((r) => ({
|
|
260
|
+
owner: asString(r.owner),
|
|
261
|
+
name: asString(r.name),
|
|
262
|
+
target: asString(r.target),
|
|
263
|
+
localKey: asString(r.local_key),
|
|
264
|
+
targetKey: asString(r.target_key),
|
|
265
|
+
cardinality: asString(r.cardinality),
|
|
266
|
+
}));
|
|
267
|
+
for (const link of linkRows) {
|
|
268
|
+
const owner = tables[link.owner];
|
|
269
|
+
const target = tables[link.target];
|
|
270
|
+
// A link referencing a filtered-out (include/exclude) table is skipped: we
|
|
271
|
+
// cannot describe the target, so the relation would be unusable.
|
|
272
|
+
if (!owner || !target)
|
|
273
|
+
continue;
|
|
274
|
+
const toOne = link.cardinality === 'to-one';
|
|
275
|
+
const relName = (0, schema_js_1.snakeToCamel)(link.name);
|
|
276
|
+
// Forward relation on the owner (the declared direction).
|
|
277
|
+
if (toOne) {
|
|
278
|
+
addRelation(owner, {
|
|
279
|
+
type: 'belongsTo',
|
|
280
|
+
name: relName,
|
|
281
|
+
from: link.owner,
|
|
282
|
+
to: link.target,
|
|
283
|
+
foreignKey: link.localKey,
|
|
284
|
+
referenceKey: link.targetKey,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
addRelation(owner, {
|
|
289
|
+
type: 'hasMany',
|
|
290
|
+
name: relName,
|
|
291
|
+
from: link.owner,
|
|
292
|
+
to: link.target,
|
|
293
|
+
foreignKey: link.targetKey, // FK on the child (target) side
|
|
294
|
+
referenceKey: link.localKey, // referenced key on the owner (parent) side
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// Reverse relation on the target (synthesized, collision-guarded).
|
|
298
|
+
const reverseName = toOne ? pluralize((0, schema_js_1.snakeToCamel)(link.owner)) : (0, schema_js_1.singularize)((0, schema_js_1.snakeToCamel)(link.owner));
|
|
299
|
+
if (!relationNameTaken(target, reverseName)) {
|
|
300
|
+
addRelation(target, {
|
|
301
|
+
type: toOne ? 'hasMany' : 'belongsTo',
|
|
302
|
+
name: reverseName,
|
|
303
|
+
from: link.target,
|
|
304
|
+
to: link.owner,
|
|
305
|
+
// Same two columns, roles swapped for the opposite direction.
|
|
306
|
+
foreignKey: toOne ? link.localKey : link.targetKey,
|
|
307
|
+
referenceKey: toOne ? link.targetKey : link.localKey,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/** True when `name` already names a relation OR a column field/name on `meta`. */
|
|
313
|
+
function relationNameTaken(meta, name) {
|
|
314
|
+
if (meta.relations[name])
|
|
315
|
+
return true;
|
|
316
|
+
return meta.columns.some((c) => c.field === name || c.name === name);
|
|
317
|
+
}
|
|
318
|
+
/** Add a relation to a table's relations map (keyed by its camelCase name). */
|
|
319
|
+
function addRelation(meta, rel) {
|
|
320
|
+
if (meta.relations[rel.name])
|
|
321
|
+
return; // never clobber an existing relation
|
|
322
|
+
meta.relations[rel.name] = rel;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Minimal English pluralizer for reverse-relation names (`user` → `users`,
|
|
326
|
+
* `company` → `companies`, `box` → `boxes`). Only ever produces a candidate name
|
|
327
|
+
* that {@link relationNameTaken} then vets, so an imperfect plural can at worst be
|
|
328
|
+
* skipped, never break a query.
|
|
329
|
+
*/
|
|
330
|
+
function pluralize(word) {
|
|
331
|
+
if (/[^aeiou]y$/i.test(word))
|
|
332
|
+
return `${word.slice(0, -1)}ies`;
|
|
333
|
+
if (/(s|x|z|ch|sh)$/i.test(word))
|
|
334
|
+
return `${word}es`;
|
|
335
|
+
return `${word}s`;
|
|
336
|
+
}
|
package/dist/cjs/powdb.js
CHANGED
|
@@ -98,7 +98,10 @@ exports.requireCapability = requireCapability;
|
|
|
98
98
|
exports.isJsonColumn = isJsonColumn;
|
|
99
99
|
exports.powqlColumnType = powqlColumnType;
|
|
100
100
|
exports.quotePowqlIdent = quotePowqlIdent;
|
|
101
|
+
exports.deriveDesiredLinks = deriveDesiredLinks;
|
|
102
|
+
exports.powdbLinkStatement = powdbLinkStatement;
|
|
101
103
|
exports.powqlSchemaDDL = powqlSchemaDDL;
|
|
104
|
+
exports.applyPowdbLinks = applyPowdbLinks;
|
|
102
105
|
exports.coerceValue = coerceValue;
|
|
103
106
|
exports.coerceNativeValue = coerceNativeValue;
|
|
104
107
|
exports.rowToEntity = rowToEntity;
|
|
@@ -112,6 +115,8 @@ const client_js_1 = require("./client.js");
|
|
|
112
115
|
const dialect_js_1 = require("./dialect.js");
|
|
113
116
|
const errors_js_1 = require("./errors.js");
|
|
114
117
|
const optional_peer_import_cjs_1 = __importDefault(require("./optional-peer-import.cjs"));
|
|
118
|
+
const warn_registry_js_1 = require("./query/warn-registry.js");
|
|
119
|
+
const schema_js_1 = require("./schema.js");
|
|
115
120
|
/**
|
|
116
121
|
* Capability descriptor for PowDB. PowQL generation is owned by
|
|
117
122
|
* {@link PowqlInterface} (not the SQL `Dialect`), so this dialect exists only to
|
|
@@ -247,7 +252,14 @@ function assertSupportedPowdbVersion(version) {
|
|
|
247
252
|
throw new errors_js_1.ConnectionError(`[turbine] turbine-orm/powdb requires PowDB >= ${exports.MIN_POWDB_VERSION}; the server reports "${version}". ` +
|
|
248
253
|
'Upgrade the PowDB server (0.7.0 added the `returning` keyword and the int->float coercion fix Turbine relies on).');
|
|
249
254
|
}
|
|
250
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* Minimum engine version each gated feature needs, for the E017 hint text.
|
|
257
|
+
* Most gates carry a `major.minor` floor (patch-insensitive); the two link
|
|
258
|
+
* lanes carry a `major.minor.patch` floor (`0.19.1`) because the listing
|
|
259
|
+
* statement and the safe traversal semantics landed in the PATCH release, not
|
|
260
|
+
* in 0.19.0. {@link atLeastVersion} compares all three components, so a
|
|
261
|
+
* `major.minor` floor still matches every patch of that minor.
|
|
262
|
+
*/
|
|
251
263
|
const POWDB_FEATURE_MIN_VERSION = {
|
|
252
264
|
introspection: '0.10',
|
|
253
265
|
jsonDocs: '0.12',
|
|
@@ -255,6 +267,8 @@ const POWDB_FEATURE_MIN_VERSION = {
|
|
|
255
267
|
serverJoins: '0.13',
|
|
256
268
|
nestedProjections: '0.18',
|
|
257
269
|
entityLinks: '0.19',
|
|
270
|
+
linkIntrospection: '0.19.1',
|
|
271
|
+
linkPaths: '0.19.1',
|
|
258
272
|
};
|
|
259
273
|
/**
|
|
260
274
|
* Trusted-caller default: every FEATURE gate on, engine version unknown. Used
|
|
@@ -268,6 +282,11 @@ const POWDB_FEATURE_MIN_VERSION = {
|
|
|
268
282
|
* `entityLinks` stays OFF for a stronger reason still: declaring a link
|
|
269
283
|
* one-way-upgrades the on-disk catalog to v7 and locks out pre-0.19 binaries,
|
|
270
284
|
* so it must only ever light up behind a real version probe.
|
|
285
|
+
* `linkIntrospection` / `linkPaths` stay OFF for the same probe-only discipline:
|
|
286
|
+
* `linkPaths` flips real query generation (a to-one `with` compiling to link
|
|
287
|
+
* projections), and `linkIntrospection` is only meaningful once genuinely
|
|
288
|
+
* probed, so both must come from a real version resolution, never a bare
|
|
289
|
+
* construction.
|
|
271
290
|
*/
|
|
272
291
|
exports.ALL_POWDB_CAPABILITIES = {
|
|
273
292
|
engineVersion: null,
|
|
@@ -277,6 +296,8 @@ exports.ALL_POWDB_CAPABILITIES = {
|
|
|
277
296
|
serverJoins: true,
|
|
278
297
|
nestedProjections: false,
|
|
279
298
|
entityLinks: false,
|
|
299
|
+
linkIntrospection: false,
|
|
300
|
+
linkPaths: false,
|
|
280
301
|
nativeRaw: false,
|
|
281
302
|
};
|
|
282
303
|
/** Parse a PowDB semver prefix (`0.13.0`, `0.13`, `1.2.3-rc`) into components, or `null`. */
|
|
@@ -286,9 +307,20 @@ function parsePowdbSemver(version) {
|
|
|
286
307
|
return null;
|
|
287
308
|
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3] ?? 0) };
|
|
288
309
|
}
|
|
289
|
-
/**
|
|
290
|
-
|
|
291
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Is `sem` at least `major.minor.patch`? PATCH-AWARE: `patch` defaults to `0`,
|
|
312
|
+
* so a two-component floor (`atLeastVersion(sem, 0, 19)`) behaves exactly as the
|
|
313
|
+
* old major/minor comparison did (matches every patch of 0.19), while a
|
|
314
|
+
* three-component floor (`atLeastVersion(sem, 0, 19, 1)`) additionally requires
|
|
315
|
+
* the patch, the distinction the link lanes need (0.19.1, never 0.19.0). Every
|
|
316
|
+
* existing two-argument call keeps its prior semantics unchanged.
|
|
317
|
+
*/
|
|
318
|
+
function atLeastVersion(sem, major, minor, patch = 0) {
|
|
319
|
+
if (sem.major !== major)
|
|
320
|
+
return sem.major > major;
|
|
321
|
+
if (sem.minor !== minor)
|
|
322
|
+
return sem.minor > minor;
|
|
323
|
+
return sem.patch >= patch;
|
|
292
324
|
}
|
|
293
325
|
/**
|
|
294
326
|
* Derive {@link PowdbCapabilities} from an engine version string. A non-semver /
|
|
@@ -307,6 +339,8 @@ function capabilitiesFromVersion(version, opts = {}) {
|
|
|
307
339
|
serverJoins: false,
|
|
308
340
|
nestedProjections: false,
|
|
309
341
|
entityLinks: false,
|
|
342
|
+
linkIntrospection: false,
|
|
343
|
+
linkPaths: false,
|
|
310
344
|
nativeRaw: false,
|
|
311
345
|
};
|
|
312
346
|
}
|
|
@@ -318,6 +352,11 @@ function capabilitiesFromVersion(version, opts = {}) {
|
|
|
318
352
|
serverJoins: atLeastVersion(sem, 0, 13),
|
|
319
353
|
nestedProjections: atLeastVersion(sem, 0, 18),
|
|
320
354
|
entityLinks: atLeastVersion(sem, 0, 19),
|
|
355
|
+
// PATCH-floored at 0.19.1: the `schema links` listing + `describe` link rows
|
|
356
|
+
// and the safe (hard-erroring) scalar-path traversal both landed in 0.19.1,
|
|
357
|
+
// never 0.19.0.
|
|
358
|
+
linkIntrospection: atLeastVersion(sem, 0, 19, 1),
|
|
359
|
+
linkPaths: atLeastVersion(sem, 0, 19, 1),
|
|
321
360
|
nativeRaw: Boolean(opts.hasNativeRaw) && atLeastVersion(sem, 0, 13),
|
|
322
361
|
};
|
|
323
362
|
}
|
|
@@ -526,6 +565,48 @@ function quotePowqlIdent(name) {
|
|
|
526
565
|
}
|
|
527
566
|
return exports.POWQL_KEYWORDS.has(name) || !POWQL_BARE_IDENT.test(name) ? `\`${name}\`` : name;
|
|
528
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Derive the entity links Turbine would declare from a schema's relations. One
|
|
570
|
+
* link per single-column hasMany / hasOne / belongsTo relation, owned by the
|
|
571
|
+
* relation's `from` table:
|
|
572
|
+
* - belongsTo Order->User: `Order.user -> User on user_id = id`
|
|
573
|
+
* (localKey = the FK on the owner, targetKey = the referenced key on target).
|
|
574
|
+
* - hasMany User->Order: `User.orders -> Order on id = user_id`
|
|
575
|
+
* (localKey = the referenced key on the owner, targetKey = the FK on the child).
|
|
576
|
+
* Composite-key relations and m2m junctions are skipped (links are single-column;
|
|
577
|
+
* a junction cannot be inferred from links). A relation whose name collides with
|
|
578
|
+
* a column on the owner is skipped and reported through `onCollision` (the caller
|
|
579
|
+
* decides whether to warn). Pure: no capability gate, no side effects.
|
|
580
|
+
*/
|
|
581
|
+
function deriveDesiredLinks(schema, onCollision) {
|
|
582
|
+
const links = [];
|
|
583
|
+
for (const meta of Object.values(schema.tables)) {
|
|
584
|
+
for (const rel of Object.values(meta.relations)) {
|
|
585
|
+
if (rel.type === 'manyToMany')
|
|
586
|
+
continue;
|
|
587
|
+
const fk = (0, schema_js_1.normalizeKeyColumns)(rel.foreignKey);
|
|
588
|
+
const rk = (0, schema_js_1.normalizeKeyColumns)(rel.referenceKey);
|
|
589
|
+
if (fk.length !== 1 || rk.length !== 1)
|
|
590
|
+
continue; // links are single-column
|
|
591
|
+
// A link name that collides with a column on the owner is a hard engine
|
|
592
|
+
// error at declare time, so skip it (and let the caller warn).
|
|
593
|
+
const collides = meta.columns.some((c) => c.name === rel.name || c.field === rel.name);
|
|
594
|
+
if (collides) {
|
|
595
|
+
onCollision?.(meta.name, rel.name);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
const localKey = rel.type === 'belongsTo' ? fk[0] : rk[0];
|
|
599
|
+
const targetKey = rel.type === 'belongsTo' ? rk[0] : fk[0];
|
|
600
|
+
links.push({ owner: meta.name, name: rel.name, target: rel.to, localKey, targetKey });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return links;
|
|
604
|
+
}
|
|
605
|
+
/** Render one {@link PowdbDesiredLink} as its create-only `link ...` DDL statement. */
|
|
606
|
+
function powdbLinkStatement(link) {
|
|
607
|
+
return (`link ${quotePowqlIdent(link.owner)}.${quotePowqlIdent(link.name)} -> ` +
|
|
608
|
+
`${quotePowqlIdent(link.target)} on ${quotePowqlIdent(link.localKey)} = ${quotePowqlIdent(link.targetKey)}`);
|
|
609
|
+
}
|
|
529
610
|
function powqlSchemaDDL(schema, opts = {}) {
|
|
530
611
|
const caps = opts.capabilities;
|
|
531
612
|
const stmts = [];
|
|
@@ -606,8 +687,94 @@ function powqlSchemaDDL(schema, opts = {}) {
|
|
|
606
687
|
}
|
|
607
688
|
}
|
|
608
689
|
}
|
|
690
|
+
// Entity-link DDL (opt-in). Appended after every type so the referenced types
|
|
691
|
+
// already exist when the links declare. Gated behind the entityLinks capability
|
|
692
|
+
// when a caller supplied one (an old engine has no `link` statement).
|
|
693
|
+
if (opts.emitLinks) {
|
|
694
|
+
if (caps)
|
|
695
|
+
requireCapability(caps, 'entityLinks', 'entity link DDL (`emitLinks`)');
|
|
696
|
+
const links = deriveDesiredLinks(schema, (owner, name) => {
|
|
697
|
+
if ((0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.powdbLinks, `collide:${owner}.${name}`)) {
|
|
698
|
+
console.warn(`[turbine] powqlSchemaDDL(emitLinks): relation "${name}" on "${owner}" collides with a column of the ` +
|
|
699
|
+
'same name; skipping its link declaration (PowDB hard-errors on a link that shadows a column).');
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
for (const link of links)
|
|
703
|
+
stmts.push(powdbLinkStatement(link));
|
|
704
|
+
}
|
|
609
705
|
return stmts;
|
|
610
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Existence-checked apply of entity-link DDL against a LIVE PowDB database.
|
|
709
|
+
* Because link DDL is create-only (no `if not exists`, redeclaring is an error,
|
|
710
|
+
* and there is no drop spelling), an apply layer must diff against the live
|
|
711
|
+
* catalog first: this reads the `schema links` listing, then executes only the
|
|
712
|
+
* links that are genuinely missing.
|
|
713
|
+
*
|
|
714
|
+
* - a desired link already declared with the SAME endpoints → skipped (idempotent);
|
|
715
|
+
* - a link declared with the same owner + name but DIFFERENT endpoints → skipped
|
|
716
|
+
* with a one-time warning (never dropped/replaced: there is no drop DDL, and a
|
|
717
|
+
* silent replace would be a destructive schema change);
|
|
718
|
+
* - a name/column collision on the owner → skipped (deriveDesiredLinks warns).
|
|
719
|
+
*
|
|
720
|
+
* Requires the `entityLinks` + `linkIntrospection` capabilities when `capabilities`
|
|
721
|
+
* is supplied (the listing statement is 0.19.1+). Returns the statements executed.
|
|
722
|
+
* `exec` runs one PowQL statement (embedded `db.raw` / a networked query shim); it
|
|
723
|
+
* must return rows keyed by column name for the `schema links` read.
|
|
724
|
+
*/
|
|
725
|
+
async function applyPowdbLinks(exec, schema, options = {}) {
|
|
726
|
+
const caps = options.capabilities;
|
|
727
|
+
if (caps) {
|
|
728
|
+
requireCapability(caps, 'entityLinks', 'entity link DDL (`applyPowdbLinks`)');
|
|
729
|
+
requireCapability(caps, 'linkIntrospection', 'PowDB `schema links` introspection');
|
|
730
|
+
}
|
|
731
|
+
const existing = await listPowdbLinks(exec);
|
|
732
|
+
const byOwnerName = new Map(existing.map((l) => [`${l.owner}${l.name}`, l]));
|
|
733
|
+
const desired = deriveDesiredLinks(schema, (owner, name) => {
|
|
734
|
+
if ((0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.powdbLinks, `collide:${owner}.${name}`)) {
|
|
735
|
+
console.warn(`[turbine] applyPowdbLinks: relation "${name}" on "${owner}" collides with a column of the same name; ` +
|
|
736
|
+
'skipping its link (PowDB hard-errors on a link that shadows a column).');
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
const executed = [];
|
|
740
|
+
for (const link of desired) {
|
|
741
|
+
const found = byOwnerName.get(`${link.owner}${link.name}`);
|
|
742
|
+
if (found) {
|
|
743
|
+
const same = found.target === link.target && found.localKey === link.localKey && found.targetKey === link.targetKey;
|
|
744
|
+
if (!same && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.powdbLinks, `drift:${link.owner}.${link.name}`)) {
|
|
745
|
+
console.warn(`[turbine] applyPowdbLinks: link "${link.name}" on "${link.owner}" is already declared with different ` +
|
|
746
|
+
`endpoints (have ${found.target} on ${found.localKey} = ${found.targetKey}, want ${link.target} on ` +
|
|
747
|
+
`${link.localKey} = ${link.targetKey}); leaving it untouched (PowDB has no drop-link DDL).`);
|
|
748
|
+
}
|
|
749
|
+
continue; // identical → idempotent skip; drift → warned skip
|
|
750
|
+
}
|
|
751
|
+
const stmt = powdbLinkStatement(link);
|
|
752
|
+
await exec(stmt);
|
|
753
|
+
executed.push(stmt);
|
|
754
|
+
}
|
|
755
|
+
return executed;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Read the live `schema links` listing into {@link PowdbDesiredLink} rows (owner,
|
|
759
|
+
* name, target, localKey, targetKey; cardinality is dropped — it is derived, not
|
|
760
|
+
* a DDL input). An empty catalog returns `[]`, never an error. Naming edge: a
|
|
761
|
+
* table literally named `links` is described with `describe links`, but the
|
|
762
|
+
* link LISTING is the contextual keyword form `schema links`.
|
|
763
|
+
*/
|
|
764
|
+
async function listPowdbLinks(exec) {
|
|
765
|
+
const { rows } = await exec('schema links');
|
|
766
|
+
return rows.map((r) => ({
|
|
767
|
+
owner: powdbCell(r.owner),
|
|
768
|
+
name: powdbCell(r.name),
|
|
769
|
+
target: powdbCell(r.target),
|
|
770
|
+
localKey: powdbCell(r.local_key),
|
|
771
|
+
targetKey: powdbCell(r.target_key),
|
|
772
|
+
}));
|
|
773
|
+
}
|
|
774
|
+
/** Coerce a wire cell (string on the legacy wire, typed on the native wire) to string. */
|
|
775
|
+
function powdbCell(v) {
|
|
776
|
+
return v === null || v === undefined ? '' : String(v);
|
|
777
|
+
}
|
|
611
778
|
/** Coerce a JS value into a PowDB positional param (the write side). */
|
|
612
779
|
function toPowdbParam(value, col) {
|
|
613
780
|
if (value instanceof PowdbFloatParam)
|
|
@@ -847,6 +1014,18 @@ function wrapPowdbError(err) {
|
|
|
847
1014
|
if (/nested-loop join would evaluate|join result exceeds row limit/i.test(msg)) {
|
|
848
1015
|
return new errors_js_1.ValidationError(`[turbine] PowDB join rejected: ${msg}`);
|
|
849
1016
|
}
|
|
1017
|
+
// Entity-link misuse hard errors (0.19.1 turned the two silent-wrong-results
|
|
1018
|
+
// 0.19.0 behaviors into hard errors) → ValidationError (E003), a query defect
|
|
1019
|
+
// to fix. Matched explicitly on the pinned message text so both transports
|
|
1020
|
+
// classify identically regardless of how the wire wraps them (embedded tags
|
|
1021
|
+
// every error `GenericFailure`; networked carries a wire class): a bare dotted
|
|
1022
|
+
// projection path that was not alias-qualified, and an aggregate over a nested
|
|
1023
|
+
// or link projection. Turbine's own generator never emits either shape (it
|
|
1024
|
+
// always aliases and never aggregates over a link), so these fire only for a
|
|
1025
|
+
// raw user PowQL string; mapping them keeps that path typed.
|
|
1026
|
+
if (/is ambiguous in a projection|aggregates over a nested or link projection/i.test(msg)) {
|
|
1027
|
+
return new errors_js_1.ValidationError(`[turbine] PowDB query rejected: ${msg}`);
|
|
1028
|
+
}
|
|
850
1029
|
// Typed wire error class (networked, server >= 0.17): the client surfaces the
|
|
851
1030
|
// stable one-byte class from the error frame as `.wireErrorClass`. Classify by
|
|
852
1031
|
// it BEFORE the generic message regexes: the server sanitizes non-allowlisted
|
|
@@ -1518,7 +1697,11 @@ function encodePowqlLiteral(value) {
|
|
|
1518
1697
|
* between v0.18.0 and v0.19.0 (empty git diff), and `link` was already a lexer
|
|
1519
1698
|
* keyword at 0.18.0. The 0.19 entity-links surface adds new STATEMENTS built from
|
|
1520
1699
|
* pre-existing tokens, so the tokenization / escape surface this ceiling guards is
|
|
1521
|
-
* unmoved.
|
|
1700
|
+
* unmoved. The 0.19.1 link-introspection / link-path round is likewise lexer-neutral:
|
|
1701
|
+
* `git diff v0.19.0 v0.19.1 -- crates/query/src/lexer.rs` is empty (the bare-dotted-path
|
|
1702
|
+
* hard error is parser-level, not tokenization), so this ceiling stays `'0.19'`. The
|
|
1703
|
+
* guard in {@link PowdbEmbeddedPool.exec} compares major.minor only, so `'0.19'`
|
|
1704
|
+
* already covers every 0.19.x patch — no bump is needed for 0.19.1.
|
|
1522
1705
|
*/
|
|
1523
1706
|
exports.POWQL_LEXER_TESTED_CEILING = '0.19';
|
|
1524
1707
|
/** Escape a string into a PowQL `"…"` literal, matching the engine lexer's escape rules. */
|