turbine-orm 0.77.1 → 0.78.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 +13 -9
- package/dist/cjs/cli/config.d.ts +7 -1
- package/dist/cjs/cli/config.js +11 -2
- package/dist/cjs/cli/destructive.d.ts +1 -1
- package/dist/cjs/cli/destructive.js +307 -9
- package/dist/cjs/cli/index.js +252 -42
- package/dist/cjs/cli/mcp.d.ts +23 -0
- package/dist/cjs/cli/mcp.js +190 -152
- package/dist/cjs/cli/migrate.d.ts +243 -3
- package/dist/cjs/cli/migrate.js +432 -43
- package/dist/cjs/cli/sql-statements.js +27 -0
- package/dist/cjs/cli/studio.d.ts +0 -1
- package/dist/cjs/cli/studio.js +9 -7
- package/dist/cjs/client.d.ts +8 -1
- package/dist/cjs/client.js +7 -0
- package/dist/cjs/connection-url.d.ts +82 -0
- package/dist/cjs/connection-url.js +187 -1
- package/dist/cjs/errors.d.ts +112 -12
- package/dist/cjs/errors.js +558 -114
- package/dist/cjs/generate.js +47 -15
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/introspect.d.ts +33 -0
- package/dist/cjs/introspect.js +54 -1
- package/dist/cjs/mssql.js +21 -1
- package/dist/cjs/nested-write.js +85 -14
- package/dist/cjs/pipeline-submittable.d.ts +2 -0
- package/dist/cjs/pipeline-submittable.js +88 -3
- package/dist/cjs/pipeline.js +13 -1
- package/dist/cjs/powdb-introspect.d.ts +5 -1
- package/dist/cjs/powdb-introspect.js +5 -1
- package/dist/cjs/powql.d.ts +14 -0
- package/dist/cjs/powql.js +44 -4
- package/dist/cjs/prisma-compat.js +95 -8
- package/dist/cjs/query/aggregates.js +56 -6
- package/dist/cjs/query/builder.d.ts +76 -13
- package/dist/cjs/query/builder.js +188 -58
- package/dist/cjs/query/compound-unique.d.ts +76 -0
- package/dist/cjs/query/compound-unique.js +129 -0
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/types.d.ts +65 -11
- package/dist/cjs/query/where.d.ts +85 -19
- package/dist/cjs/query/where.js +262 -47
- package/dist/cjs/query/writes.d.ts +11 -2
- package/dist/cjs/query/writes.js +116 -21
- package/dist/cjs/seed.d.ts +16 -0
- package/dist/cjs/seed.js +16 -0
- package/dist/cli/config.d.ts +7 -1
- package/dist/cli/config.js +11 -2
- package/dist/cli/destructive.d.ts +1 -1
- package/dist/cli/destructive.js +307 -9
- package/dist/cli/index.js +254 -44
- package/dist/cli/mcp.d.ts +23 -0
- package/dist/cli/mcp.js +187 -150
- package/dist/cli/migrate.d.ts +243 -3
- package/dist/cli/migrate.js +423 -45
- package/dist/cli/sql-statements.js +27 -0
- package/dist/cli/studio.d.ts +0 -1
- package/dist/cli/studio.js +10 -7
- package/dist/client.d.ts +8 -1
- package/dist/client.js +7 -0
- package/dist/connection-url.d.ts +82 -0
- package/dist/connection-url.js +183 -0
- package/dist/errors.d.ts +112 -12
- package/dist/errors.js +558 -114
- package/dist/generate.js +47 -15
- package/dist/index.d.ts +1 -1
- package/dist/introspect.d.ts +33 -0
- package/dist/introspect.js +53 -1
- package/dist/mssql.js +21 -1
- package/dist/nested-write.js +85 -14
- package/dist/pipeline-submittable.d.ts +2 -0
- package/dist/pipeline-submittable.js +87 -3
- package/dist/pipeline.js +14 -2
- package/dist/powdb-introspect.d.ts +5 -1
- package/dist/powdb-introspect.js +5 -1
- package/dist/powql.d.ts +14 -0
- package/dist/powql.js +45 -5
- package/dist/prisma-compat.js +96 -9
- package/dist/query/aggregates.js +56 -6
- package/dist/query/builder.d.ts +76 -13
- package/dist/query/builder.js +188 -58
- package/dist/query/compound-unique.d.ts +76 -0
- package/dist/query/compound-unique.js +126 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +65 -11
- package/dist/query/where.d.ts +85 -19
- package/dist/query/where.js +260 -47
- package/dist/query/writes.d.ts +11 -2
- package/dist/query/writes.js +117 -22
- package/dist/seed.d.ts +16 -0
- package/dist/seed.js +16 -0
- package/package.json +3 -3
- package/skills/turbine-orm/SKILL.md +37 -10
package/dist/cjs/query/where.js
CHANGED
|
@@ -40,6 +40,8 @@ exports.emptyRelationsHost = emptyRelationsHost;
|
|
|
40
40
|
exports.scopedWhereHost = scopedWhereHost;
|
|
41
41
|
exports.relationWhereScope = relationWhereScope;
|
|
42
42
|
exports.aliasWhereScope = aliasWhereScope;
|
|
43
|
+
exports.renderedRefWhereScope = renderedRefWhereScope;
|
|
44
|
+
exports.buildRenderedRefWhere = buildRenderedRefWhere;
|
|
43
45
|
exports.buildScopedWhere = buildScopedWhere;
|
|
44
46
|
exports.buildScopedScalarClause = buildScopedScalarClause;
|
|
45
47
|
exports.collectScopedWhereParams = collectScopedWhereParams;
|
|
@@ -298,7 +300,8 @@ function collectRelationFilterParams(qi, relDef, filterObj, params, depth = 0) {
|
|
|
298
300
|
if (filterObj.every !== undefined && filterObj.every !== null) {
|
|
299
301
|
// gf is only emitted (build) when the `every` sub-where compiles to a
|
|
300
302
|
// filter, otherwise `every` is trivially true and no subquery is built.
|
|
301
|
-
if (buildSubWhereForRelation(qi, target, filterObj.every, [], depth + 1) !==
|
|
303
|
+
if (buildSubWhereForRelation(qi, target, qi.q(target), filterObj.every, [], depth + 1) !==
|
|
304
|
+
null) {
|
|
302
305
|
collectRelFilterParams(qi, target, filterObj.every, params, depth + 1);
|
|
303
306
|
collectTargetGlobalFilterExists(qi, target, params);
|
|
304
307
|
}
|
|
@@ -318,7 +321,9 @@ function collectRelFilterParams(qi, targetTable, subWhere, params, depth = 0) {
|
|
|
318
321
|
const meta = qi.schema.tables[targetTable];
|
|
319
322
|
if (!meta)
|
|
320
323
|
return;
|
|
321
|
-
|
|
324
|
+
// The FROM-item reference only shapes SQL text, never params, so the bare
|
|
325
|
+
// name serves the collect mirror whether or not the build path aliased it.
|
|
326
|
+
collectScopedWhereParams(qi, relationWhereScope(qi, targetTable, meta, qi.q(targetTable)), subWhere, params, depth);
|
|
322
327
|
}
|
|
323
328
|
/**
|
|
324
329
|
* Collect params from operator clauses. Mirrors buildOperatorClauses:
|
|
@@ -336,8 +341,11 @@ function collectOperatorParams(qi, column, op, params, refCtx) {
|
|
|
336
341
|
};
|
|
337
342
|
// Mirrors buildOperatorClauses' temporal bind rewrite exactly.
|
|
338
343
|
const cv = (v) => (refCtx ? coerceWhereOperand(qi, refCtx.meta, refCtx.rawColumn, v) : v);
|
|
344
|
+
const insensitive = op.mode === 'insensitive';
|
|
339
345
|
if (op.equals !== undefined && op.equals !== null && !skipRef(op.equals)) {
|
|
340
346
|
(0, filters_js_1.assertBindableEqualsOperand)(op.equals, `"${column}"`);
|
|
347
|
+
if (insensitive)
|
|
348
|
+
assertInsensitiveOperand(op.equals, 'equals', column);
|
|
341
349
|
params.push(cv(op.equals));
|
|
342
350
|
}
|
|
343
351
|
if (op.gt !== undefined && !skipRef(op.gt))
|
|
@@ -348,12 +356,25 @@ function collectOperatorParams(qi, column, op, params, refCtx) {
|
|
|
348
356
|
params.push(cv(op.lt));
|
|
349
357
|
if (op.lte !== undefined && !skipRef(op.lte))
|
|
350
358
|
params.push(cv(op.lte));
|
|
351
|
-
if (op.not !== undefined && op.not !== null && !skipRef(op.not))
|
|
359
|
+
if (op.not !== undefined && op.not !== null && !skipRef(op.not)) {
|
|
360
|
+
if (insensitive)
|
|
361
|
+
assertInsensitiveOperand(op.not, 'not', column);
|
|
352
362
|
params.push(cv(op.not));
|
|
353
|
-
|
|
363
|
+
}
|
|
364
|
+
if (op.in !== undefined) {
|
|
365
|
+
if (insensitive) {
|
|
366
|
+
assertInsensitiveOperand(op.in, 'in', column);
|
|
367
|
+
requireInsensitiveList(qi, column, 'in');
|
|
368
|
+
}
|
|
354
369
|
params.push(qi.inParam(cv(op.in)));
|
|
355
|
-
|
|
370
|
+
}
|
|
371
|
+
if (op.notIn !== undefined) {
|
|
372
|
+
if (insensitive) {
|
|
373
|
+
assertInsensitiveOperand(op.notIn, 'notIn', column);
|
|
374
|
+
requireInsensitiveList(qi, column, 'notIn');
|
|
375
|
+
}
|
|
356
376
|
params.push(qi.inParam(cv(op.notIn)));
|
|
377
|
+
}
|
|
357
378
|
if (op.contains !== undefined)
|
|
358
379
|
params.push(`%${likeOperand(qi, op.contains)}%`);
|
|
359
380
|
if (op.startsWith !== undefined)
|
|
@@ -519,16 +540,16 @@ function collectTargetGlobalFilterAlias(qi, targetTable, params) {
|
|
|
519
540
|
collectAliasWhereParams(qi, targetTable, meta, gf, params);
|
|
520
541
|
}
|
|
521
542
|
/**
|
|
522
|
-
* SQL clause for `targetTable`'s global filter rendered against
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
* {@link collectTargetGlobalFilterExists}.
|
|
543
|
+
* SQL clause for `targetTable`'s global filter rendered against `targetRef`,
|
|
544
|
+
* the relation-filter `EXISTS` subquery's FROM item (the bare table name, or
|
|
545
|
+
* its alias when {@link buildRelationFilter} had to alias it). Pushes its
|
|
546
|
+
* params; `''` when none. Mirror: {@link collectTargetGlobalFilterExists}.
|
|
526
547
|
*/
|
|
527
|
-
function targetGlobalFilterExists(qi, targetTable, params) {
|
|
548
|
+
function targetGlobalFilterExists(qi, targetTable, targetRef, params) {
|
|
528
549
|
const gf = resolveGlobalFilter(qi, targetTable);
|
|
529
550
|
if (!gf)
|
|
530
551
|
return '';
|
|
531
|
-
return buildSubWhereForRelation(qi, targetTable, gf, params) ?? '';
|
|
552
|
+
return buildSubWhereForRelation(qi, targetTable, targetRef, gf, params) ?? '';
|
|
532
553
|
}
|
|
533
554
|
/** Param-collect mirror of {@link targetGlobalFilterExists}. */
|
|
534
555
|
function collectTargetGlobalFilterExists(qi, targetTable, params) {
|
|
@@ -749,29 +770,81 @@ function scopedWhereHost(qi, meta) {
|
|
|
749
770
|
}
|
|
750
771
|
return host;
|
|
751
772
|
}
|
|
752
|
-
/**
|
|
753
|
-
|
|
773
|
+
/**
|
|
774
|
+
* Build the scope for a relation-filter EXISTS sub-where. `targetRef` is the
|
|
775
|
+
* subquery's FROM item as the SQL refers to it: the quoted target table, or the
|
|
776
|
+
* alias {@link buildRelationFilter} gave it when the bare name would have been
|
|
777
|
+
* captured by the correlation. Every column the sub-where names, and every
|
|
778
|
+
* nested relation filter's correlation parent, is qualified by that same
|
|
779
|
+
* reference, so the two can never point at different rows.
|
|
780
|
+
*/
|
|
781
|
+
function relationWhereScope(qi, targetTable, meta, targetRef) {
|
|
754
782
|
return {
|
|
755
783
|
meta,
|
|
756
784
|
table: targetTable,
|
|
757
|
-
qualifier: `${
|
|
758
|
-
relationParent:
|
|
785
|
+
qualifier: `${targetRef}.`,
|
|
786
|
+
relationParent: targetRef,
|
|
759
787
|
host: scopedWhereHost(qi, meta),
|
|
760
788
|
unknownColumn: (field) => new errors_js_1.ValidationError(`Unknown field "${field}" in relation filter for table "${targetTable}". ` +
|
|
761
789
|
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`),
|
|
762
790
|
};
|
|
763
791
|
}
|
|
764
|
-
/**
|
|
792
|
+
/**
|
|
793
|
+
* Build the scope for a relation `with`-clause `where` compiled against
|
|
794
|
+
* `alias`. The alias is BARE (`t0`), never a rendered reference: this scope
|
|
795
|
+
* quotes it itself for the nested-relation correlation parent, so a caller
|
|
796
|
+
* that hands over an already-quoted name gets `"""posts"""` inside an EXISTS
|
|
797
|
+
* body and a 42P01 that names the table three times. That happened twice, on
|
|
798
|
+
* two independent paths, from the same one-parameter-two-meanings ambiguity,
|
|
799
|
+
* so the ambiguity is refused here rather than corrected at each call site.
|
|
800
|
+
* A caller that genuinely holds a rendered reference wants
|
|
801
|
+
* {@link renderedRefWhereScope}.
|
|
802
|
+
*/
|
|
765
803
|
function aliasWhereScope(qi, targetTable, meta, alias) {
|
|
804
|
+
if (alias.includes('"')) {
|
|
805
|
+
throw new errors_js_1.ValidationError(`Internal: aliasWhereScope was given the rendered reference ${alias} for table "${targetTable}", ` +
|
|
806
|
+
'but it takes a bare alias and quotes it itself. Use renderedRefWhereScope for an already-quoted reference.');
|
|
807
|
+
}
|
|
766
808
|
return {
|
|
767
809
|
meta,
|
|
768
810
|
table: targetTable,
|
|
769
811
|
qualifier: `${alias}.`,
|
|
770
|
-
|
|
812
|
+
// Rendered through the quoter (`"t0"`), which is what a nested relation
|
|
813
|
+
// filter always emitted for this parent; PostgreSQL folds the unquoted
|
|
814
|
+
// alias to the same identifier. The empty alias the collect mirror passes
|
|
815
|
+
// never reaches SQL text.
|
|
816
|
+
relationParent: alias === '' ? alias : qi.q(alias),
|
|
771
817
|
host: scopedWhereHost(qi, meta),
|
|
772
818
|
unknownColumn: (field) => new errors_js_1.ValidationError(`Unknown column "${field}" in where for table "${targetTable}"`),
|
|
773
819
|
};
|
|
774
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* The scope for a `where` compiled against an ALREADY-RENDERED FROM-item
|
|
823
|
+
* reference: `"users"` inside `ON CONFLICT ... DO UPDATE`, or the quoted child
|
|
824
|
+
* table a batched follow-up selects from. One parameter with one meaning, used
|
|
825
|
+
* verbatim for the column qualifier AND for a nested relation filter's
|
|
826
|
+
* correlation parent, which is exactly the pair {@link aliasWhereScope} gets
|
|
827
|
+
* wrong when it is handed a rendered reference.
|
|
828
|
+
*
|
|
829
|
+
* It is {@link relationWhereScope} with the unknown-column wording of a plain
|
|
830
|
+
* `where`, because the filters compiled through it are `globalFilters` entries
|
|
831
|
+
* and a misconfigured one should read the same here as it does on the `update`
|
|
832
|
+
* path.
|
|
833
|
+
*/
|
|
834
|
+
function renderedRefWhereScope(qi, targetTable, meta, targetRef) {
|
|
835
|
+
return {
|
|
836
|
+
...relationWhereScope(qi, targetTable, meta, targetRef),
|
|
837
|
+
unknownColumn: (field) => new errors_js_1.ValidationError(`Unknown column "${field}" in where for table "${targetTable}"`),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Compile `where` against an already-rendered FROM-item reference. The seam
|
|
842
|
+
* both global-filter callers that hold one go through, so neither can reach
|
|
843
|
+
* {@link aliasWhereScope} by mistake again.
|
|
844
|
+
*/
|
|
845
|
+
function buildRenderedRefWhere(qi, targetTable, targetMeta, targetRef, where, params) {
|
|
846
|
+
return buildScopedWhere(qi, renderedRefWhereScope(qi, targetTable, targetMeta, targetRef), where, params);
|
|
847
|
+
}
|
|
775
848
|
/**
|
|
776
849
|
* Compile a scoped sub-where to SQL. Serves BOTH the relation-filter EXISTS
|
|
777
850
|
* body ({@link buildSubWhereForRelation}) and the relation `with`-clause
|
|
@@ -966,11 +1039,45 @@ function fingerprintScopedWhere(qi, host, where, depth = 0) {
|
|
|
966
1039
|
}
|
|
967
1040
|
return parts.join('&');
|
|
968
1041
|
}
|
|
1042
|
+
/**
|
|
1043
|
+
* The alias a relation-filter subquery's FROM item takes when the bare table
|
|
1044
|
+
* name would be captured by the correlation (see {@link buildRelationFilter}).
|
|
1045
|
+
* Derived from the WHERE walk depth rather than from a counter: the depth is a
|
|
1046
|
+
* pure function of the where SHAPE, which the cache fingerprint already
|
|
1047
|
+
* encodes, so the same fingerprint always yields the same SQL text, and two
|
|
1048
|
+
* nested levels are one depth apart and therefore never share a name.
|
|
1049
|
+
*/
|
|
1050
|
+
function relationFilterAlias(depth) {
|
|
1051
|
+
return `rf${depth}`;
|
|
1052
|
+
}
|
|
969
1053
|
/**
|
|
970
1054
|
* Build relation filter SQL: WHERE EXISTS / NOT EXISTS subquery
|
|
971
1055
|
* Supports: some (EXISTS), every (NOT EXISTS ... NOT), none (NOT EXISTS)
|
|
1056
|
+
*
|
|
1057
|
+
* NAME CAPTURE, and the aliasing rule that closes it. The subquery correlates
|
|
1058
|
+
* its FROM item to the parent row (`target.fk = parent.pk`). When the target
|
|
1059
|
+
* is named bare and the parent reference is that same bare name, a
|
|
1060
|
+
* self-referencing relation (`comments.parent_id -> comments.id`) compiles
|
|
1061
|
+
* `"comments"."parent_id" = "comments"."id"`, and SQL resolves BOTH sides to
|
|
1062
|
+
* the inner row: `some: {}` matched nothing, `none: {}` matched everything,
|
|
1063
|
+
* `is: null` matched every row, and the defect propagated through nested
|
|
1064
|
+
* filters and into the batched loader's follow-up query (which is a top-level
|
|
1065
|
+
* findMany on the child table). So the FROM item is aliased EXACTLY when its
|
|
1066
|
+
* quoted name equals `parentRef`, and the alias is then used for the
|
|
1067
|
+
* correlation, the sub-where qualifier, the global-filter fragment and as the
|
|
1068
|
+
* parent reference of nested filters. Every other relation filter keeps the
|
|
1069
|
+
* bare-table template it always emitted, byte for byte. The rule is complete
|
|
1070
|
+
* rather than a heuristic: the only reference the inner FROM item can capture
|
|
1071
|
+
* is one that spells its own name, and a nested level whose parent is already
|
|
1072
|
+
* an alias (`rf0`, `t0`, `ord0`) has no such reference to capture. The
|
|
1073
|
+
* junction of a manyToMany branch gets the same treatment for the same reason.
|
|
1074
|
+
*
|
|
1075
|
+
* `parentRef` is the RENDERED reference of the row being correlated against:
|
|
1076
|
+
* the quoted table at the top level (`undefined` here), an alias inside a
|
|
1077
|
+
* relation `with` where (`"t0"`), or the enclosing filter's FROM reference for
|
|
1078
|
+
* a nested relation filter.
|
|
972
1079
|
*/
|
|
973
|
-
function buildRelationFilter(qi, _relName, relDef, filterObj, params,
|
|
1080
|
+
function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentRef,
|
|
974
1081
|
/**
|
|
975
1082
|
* Nesting depth of the WHERE walk that reached this relation filter. Each
|
|
976
1083
|
* relation descent is a level too: `{ posts: { some: { comments: { some:
|
|
@@ -982,8 +1089,12 @@ depth = 0) {
|
|
|
982
1089
|
const targetMeta = qi.schema.tables[targetTable];
|
|
983
1090
|
if (!targetMeta)
|
|
984
1091
|
return null;
|
|
985
|
-
const
|
|
986
|
-
const
|
|
1092
|
+
const qSelf = parentRef ?? qi.q(qi.table);
|
|
1093
|
+
const qTargetName = qi.q(targetTable);
|
|
1094
|
+
// `qt` is how the subquery REFERS to its FROM item (bare name or alias);
|
|
1095
|
+
// `fromTarget` is the FROM item itself (`"t"` or `"t" rf0`).
|
|
1096
|
+
const qt = qTargetName === qSelf ? relationFilterAlias(depth) : qTargetName;
|
|
1097
|
+
const fromTarget = qt === qTargetName ? qTargetName : `${qTargetName} ${qt}`;
|
|
987
1098
|
const clauses = [];
|
|
988
1099
|
// Correlation: link child table to parent table (supports composite FKs)
|
|
989
1100
|
let correlation;
|
|
@@ -993,13 +1104,16 @@ depth = 0) {
|
|
|
993
1104
|
// parent.pk and silently match nothing, so route through the junction:
|
|
994
1105
|
// EXISTS (SELECT 1 FROM junction
|
|
995
1106
|
// WHERE junction.targetKey = target.pk AND junction.sourceKey = parent.ref)
|
|
996
|
-
//
|
|
997
|
-
//
|
|
998
|
-
// fragment binds no params, so
|
|
1107
|
+
// The target reference is `qt` (bare or aliased, see above), and the
|
|
1108
|
+
// junction is aliased by the same capture rule should its name coincide
|
|
1109
|
+
// with either side. The fragment binds no params, so
|
|
1110
|
+
// collectRelationFilterParams needs no mirror.
|
|
999
1111
|
if (!relDef.through) {
|
|
1000
1112
|
throw new errors_js_1.ValidationError(`manyToMany relation "${relDef.name}" is missing a \`through\` junction descriptor.`);
|
|
1001
1113
|
}
|
|
1002
|
-
const
|
|
1114
|
+
const qJunctionName = qi.q(relDef.through.table);
|
|
1115
|
+
const qJunction = qJunctionName === qSelf || qJunctionName === qt ? `rj${depth}` : qJunctionName;
|
|
1116
|
+
const fromJunction = qJunction === qJunctionName ? qJunctionName : `${qJunctionName} ${qJunction}`;
|
|
1003
1117
|
const targetKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.targetKey);
|
|
1004
1118
|
const targetPk = targetMeta.primaryKey;
|
|
1005
1119
|
if (targetPk.length === 0) {
|
|
@@ -1022,7 +1136,7 @@ depth = 0) {
|
|
|
1022
1136
|
const parentLink = sourceKeys
|
|
1023
1137
|
.map((jcol, i) => `${qJunction}.${qi.q(jcol)} = ${qSelf}.${qi.q(refKeys[i])}`)
|
|
1024
1138
|
.join(' AND ');
|
|
1025
|
-
correlation = `EXISTS (SELECT 1 FROM ${
|
|
1139
|
+
correlation = `EXISTS (SELECT 1 FROM ${fromJunction} WHERE ${targetLink} AND ${parentLink})`;
|
|
1026
1140
|
}
|
|
1027
1141
|
else if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
|
|
1028
1142
|
// parent.pk = child.fk
|
|
@@ -1037,10 +1151,10 @@ depth = 0) {
|
|
|
1037
1151
|
// ignore filtered-out rows, and `every` quantifies over only the surviving
|
|
1038
1152
|
// rows ("every NON-deleted related row matches P"). It is ANDed into the
|
|
1039
1153
|
// correlation and its params pushed AFTER the per-branch filter, mirrored
|
|
1040
|
-
// exactly in collectWhereParams' relation-filter branch.
|
|
1041
|
-
//
|
|
1154
|
+
// exactly in collectWhereParams' relation-filter branch. Rendered against
|
|
1155
|
+
// `qt`, the same reference the FROM item and the sub-where use.
|
|
1042
1156
|
const gfAnd = () => {
|
|
1043
|
-
const gf = targetGlobalFilterExists(qi, targetTable, params);
|
|
1157
|
+
const gf = targetGlobalFilterExists(qi, targetTable, qt, params);
|
|
1044
1158
|
return gf ? ` AND ${gf}` : '';
|
|
1045
1159
|
};
|
|
1046
1160
|
// "some": EXISTS (SELECT 1 FROM target WHERE correlation AND filter AND gf)
|
|
@@ -1049,26 +1163,26 @@ depth = 0) {
|
|
|
1049
1163
|
// which also skips null. Unreachable via normalization today, guarded anyway.
|
|
1050
1164
|
if (filterObj.some !== undefined && filterObj.some !== null) {
|
|
1051
1165
|
const subWhere = filterObj.some;
|
|
1052
|
-
const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
|
|
1166
|
+
const filterClause = buildSubWhereForRelation(qi, targetTable, qt, subWhere, params, depth + 1);
|
|
1053
1167
|
const filterAnd = filterClause ? ` AND ${filterClause}` : '';
|
|
1054
|
-
clauses.push(`EXISTS (SELECT 1 FROM ${
|
|
1168
|
+
clauses.push(`EXISTS (SELECT 1 FROM ${fromTarget} WHERE ${correlation}${filterAnd}${gfAnd()})`);
|
|
1055
1169
|
}
|
|
1056
1170
|
// "none": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND filter AND gf)
|
|
1057
1171
|
if (filterObj.none !== undefined && filterObj.none !== null) {
|
|
1058
1172
|
const subWhere = filterObj.none;
|
|
1059
|
-
const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
|
|
1173
|
+
const filterClause = buildSubWhereForRelation(qi, targetTable, qt, subWhere, params, depth + 1);
|
|
1060
1174
|
const filterAnd = filterClause ? ` AND ${filterClause}` : '';
|
|
1061
|
-
clauses.push(`NOT EXISTS (SELECT 1 FROM ${
|
|
1175
|
+
clauses.push(`NOT EXISTS (SELECT 1 FROM ${fromTarget} WHERE ${correlation}${filterAnd}${gfAnd()})`);
|
|
1062
1176
|
}
|
|
1063
1177
|
// "every": NOT EXISTS (SELECT 1 FROM target WHERE correlation AND gf AND NOT (filter))
|
|
1064
1178
|
if (filterObj.every !== undefined && filterObj.every !== null) {
|
|
1065
1179
|
const subWhere = filterObj.every;
|
|
1066
|
-
const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
|
|
1180
|
+
const filterClause = buildSubWhereForRelation(qi, targetTable, qt, subWhere, params, depth + 1);
|
|
1067
1181
|
if (filterClause) {
|
|
1068
1182
|
// gf params pushed AFTER filter params (collect mirrors this order), but
|
|
1069
1183
|
// placed textually inside the domain so it restricts which rows count.
|
|
1070
1184
|
const gf = gfAnd();
|
|
1071
|
-
clauses.push(`NOT EXISTS (SELECT 1 FROM ${
|
|
1185
|
+
clauses.push(`NOT EXISTS (SELECT 1 FROM ${fromTarget} WHERE ${correlation}${gf} AND NOT (${filterClause}))`);
|
|
1072
1186
|
}
|
|
1073
1187
|
else {
|
|
1074
1188
|
// "every" with empty filter = true (all match trivially), gf irrelevant.
|
|
@@ -1078,26 +1192,26 @@ depth = 0) {
|
|
|
1078
1192
|
// `is: null` = "no related row" (Prisma semantics) → NOT EXISTS.
|
|
1079
1193
|
if (filterObj.is !== undefined) {
|
|
1080
1194
|
if (filterObj.is === null) {
|
|
1081
|
-
clauses.push(`NOT EXISTS (SELECT 1 FROM ${
|
|
1195
|
+
clauses.push(`NOT EXISTS (SELECT 1 FROM ${fromTarget} WHERE ${correlation}${gfAnd()})`);
|
|
1082
1196
|
}
|
|
1083
1197
|
else {
|
|
1084
1198
|
const subWhere = filterObj.is;
|
|
1085
|
-
const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
|
|
1199
|
+
const filterClause = buildSubWhereForRelation(qi, targetTable, qt, subWhere, params, depth + 1);
|
|
1086
1200
|
const filterAnd = filterClause ? ` AND ${filterClause}` : '';
|
|
1087
|
-
clauses.push(`EXISTS (SELECT 1 FROM ${
|
|
1201
|
+
clauses.push(`EXISTS (SELECT 1 FROM ${fromTarget} WHERE ${correlation}${filterAnd}${gfAnd()})`);
|
|
1088
1202
|
}
|
|
1089
1203
|
}
|
|
1090
1204
|
// "isNot": NOT EXISTS, for to-one relations (same SQL as "none").
|
|
1091
1205
|
// `isNot: null` = "a related row exists" → EXISTS.
|
|
1092
1206
|
if (filterObj.isNot !== undefined) {
|
|
1093
1207
|
if (filterObj.isNot === null) {
|
|
1094
|
-
clauses.push(`EXISTS (SELECT 1 FROM ${
|
|
1208
|
+
clauses.push(`EXISTS (SELECT 1 FROM ${fromTarget} WHERE ${correlation}${gfAnd()})`);
|
|
1095
1209
|
}
|
|
1096
1210
|
else {
|
|
1097
1211
|
const subWhere = filterObj.isNot;
|
|
1098
|
-
const filterClause = buildSubWhereForRelation(qi, targetTable, subWhere, params, depth + 1);
|
|
1212
|
+
const filterClause = buildSubWhereForRelation(qi, targetTable, qt, subWhere, params, depth + 1);
|
|
1099
1213
|
const filterAnd = filterClause ? ` AND ${filterClause}` : '';
|
|
1100
|
-
clauses.push(`NOT EXISTS (SELECT 1 FROM ${
|
|
1214
|
+
clauses.push(`NOT EXISTS (SELECT 1 FROM ${fromTarget} WHERE ${correlation}${filterAnd}${gfAnd()})`);
|
|
1101
1215
|
}
|
|
1102
1216
|
}
|
|
1103
1217
|
return clauses.length > 0 ? clauses.join(' AND ') : null;
|
|
@@ -1106,11 +1220,11 @@ depth = 0) {
|
|
|
1106
1220
|
* Build WHERE clause conditions for a relation filter subquery.
|
|
1107
1221
|
* Uses the target table's column mapping to resolve field names.
|
|
1108
1222
|
*/
|
|
1109
|
-
function buildSubWhereForRelation(qi, targetTable, subWhere, params, depth = 0) {
|
|
1223
|
+
function buildSubWhereForRelation(qi, targetTable, targetRef, subWhere, params, depth = 0) {
|
|
1110
1224
|
const meta = qi.schema.tables[targetTable];
|
|
1111
1225
|
if (!meta)
|
|
1112
1226
|
return null;
|
|
1113
|
-
return buildScopedWhere(qi, relationWhereScope(qi, targetTable, meta), subWhere, params, depth);
|
|
1227
|
+
return buildScopedWhere(qi, relationWhereScope(qi, targetTable, meta, targetRef), subWhere, params, depth);
|
|
1114
1228
|
}
|
|
1115
1229
|
/**
|
|
1116
1230
|
* Resolve a column's Postgres type from an arbitrary table's metadata
|
|
@@ -1283,6 +1397,7 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1283
1397
|
// Temporal bind rewrite, identical to `collectOperatorParams`. Value-only, so
|
|
1284
1398
|
// the emitted SQL (and therefore the template cache) is untouched.
|
|
1285
1399
|
const cv = (v) => (refCtx ? coerceWhereOperand(qi, refCtx.meta, refCtx.rawColumn, v) : v);
|
|
1400
|
+
const insensitive = op.mode === 'insensitive';
|
|
1286
1401
|
if (op.equals !== undefined) {
|
|
1287
1402
|
if (op.equals === null) {
|
|
1288
1403
|
clauses.push(`${column} IS NULL`);
|
|
@@ -1292,8 +1407,10 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1292
1407
|
}
|
|
1293
1408
|
else {
|
|
1294
1409
|
(0, filters_js_1.assertBindableEqualsOperand)(op.equals, column);
|
|
1410
|
+
if (insensitive)
|
|
1411
|
+
assertInsensitiveOperand(op.equals, 'equals', column);
|
|
1295
1412
|
params.push(cv(op.equals));
|
|
1296
|
-
clauses.push(
|
|
1413
|
+
clauses.push(insensitiveEquality(column, '=', qi.p(params.length), insensitive));
|
|
1297
1414
|
}
|
|
1298
1415
|
}
|
|
1299
1416
|
if (op.gt !== undefined) {
|
|
@@ -1340,19 +1457,32 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1340
1457
|
clauses.push(`${column} != ${columnRefSql(qi, op.not, refCtx, op.mode)}`);
|
|
1341
1458
|
}
|
|
1342
1459
|
else {
|
|
1460
|
+
if (insensitive)
|
|
1461
|
+
assertInsensitiveOperand(op.not, 'not', column);
|
|
1343
1462
|
params.push(cv(op.not));
|
|
1344
|
-
clauses.push(
|
|
1463
|
+
clauses.push(insensitiveEquality(column, '!=', qi.p(params.length), insensitive));
|
|
1345
1464
|
}
|
|
1346
1465
|
}
|
|
1347
1466
|
if (op.in !== undefined) {
|
|
1467
|
+
if (insensitive) {
|
|
1468
|
+
assertInsensitiveOperand(op.in, 'in', column);
|
|
1469
|
+
requireInsensitiveList(qi, column, 'in');
|
|
1470
|
+
}
|
|
1348
1471
|
params.push(qi.inParam(cv(op.in)));
|
|
1349
|
-
clauses.push(
|
|
1472
|
+
clauses.push(insensitive
|
|
1473
|
+
? insensitiveInClause(qi, column, qi.p(params.length), false, 'in')
|
|
1474
|
+
: qi.inClause(column, qi.p(params.length), false));
|
|
1350
1475
|
}
|
|
1351
1476
|
if (op.notIn !== undefined) {
|
|
1477
|
+
if (insensitive) {
|
|
1478
|
+
assertInsensitiveOperand(op.notIn, 'notIn', column);
|
|
1479
|
+
requireInsensitiveList(qi, column, 'notIn');
|
|
1480
|
+
}
|
|
1352
1481
|
params.push(qi.inParam(cv(op.notIn)));
|
|
1353
|
-
clauses.push(
|
|
1482
|
+
clauses.push(insensitive
|
|
1483
|
+
? insensitiveInClause(qi, column, qi.p(params.length), true, 'notIn')
|
|
1484
|
+
: qi.inClause(column, qi.p(params.length), true));
|
|
1354
1485
|
}
|
|
1355
|
-
const insensitive = op.mode === 'insensitive';
|
|
1356
1486
|
if (op.contains !== undefined) {
|
|
1357
1487
|
params.push(`%${likeOperand(qi, op.contains)}%`);
|
|
1358
1488
|
clauses.push(buildLikeClause(qi, column, qi.p(params.length), insensitive));
|
|
@@ -1547,6 +1677,91 @@ function buildLikeClause(qi, column, paramRef, insensitive) {
|
|
|
1547
1677
|
const base = insensitive ? qi.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
|
|
1548
1678
|
return `${base} ESCAPE '\\'`;
|
|
1549
1679
|
}
|
|
1680
|
+
/**
|
|
1681
|
+
* One equality-family comparison (`=` / `!=`) honoring `mode: 'insensitive'`.
|
|
1682
|
+
*
|
|
1683
|
+
* `mode` is a sibling key of `equals` / `not` / `in` / `notIn` on
|
|
1684
|
+
* {@link WhereOperator}, and it used to be read by the three LIKE operators
|
|
1685
|
+
* ONLY, so `{ equals: 'dup name', mode: 'insensitive' }` compiled to a
|
|
1686
|
+
* case-sensitive `"name" = $1` and returned zero rows where `ILIKE` found the
|
|
1687
|
+
* variants, with no error. The fold is `LOWER(col) = LOWER($n)`: exact,
|
|
1688
|
+
* portable to every engine, and computed by the DATABASE on both sides, because
|
|
1689
|
+
* the JavaScript `toLowerCase()` and PostgreSQL's `LOWER` disagree on `İ` and
|
|
1690
|
+
* `ß`, so folding the operand client-side would compare two different
|
|
1691
|
+
* alphabets. Without the mode the emitted text is byte-identical to before.
|
|
1692
|
+
*/
|
|
1693
|
+
function insensitiveEquality(column, operator, paramRef, insensitive) {
|
|
1694
|
+
return insensitive ? `LOWER(${column}) ${operator} LOWER(${paramRef})` : `${column} ${operator} ${paramRef}`;
|
|
1695
|
+
}
|
|
1696
|
+
/**
|
|
1697
|
+
* The `in` / `notIn` comparison under `mode: 'insensitive'`.
|
|
1698
|
+
*
|
|
1699
|
+
* The fold is the engine's on BOTH sides: the column is lowered by `LOWER`,
|
|
1700
|
+
* and so is every list element, inside `unnest`. The list stays ONE bound
|
|
1701
|
+
* `text[]`, so the statement text (and therefore its cache key) is independent
|
|
1702
|
+
* of the list length. `NOT IN` keeps SQL's NULL semantics, matching the plain
|
|
1703
|
+
* `!= ALL`.
|
|
1704
|
+
*
|
|
1705
|
+
* PostgreSQL only; {@link requireInsensitiveList} is the gate, and it runs on
|
|
1706
|
+
* the param path as well so a warm template cannot slip past it.
|
|
1707
|
+
*/
|
|
1708
|
+
function insensitiveInClause(qi, column, paramRef, negated, operator) {
|
|
1709
|
+
requireInsensitiveList(qi, column, operator);
|
|
1710
|
+
return `LOWER(${column}) ${negated ? 'NOT IN' : 'IN'} (SELECT LOWER(v) FROM unnest(${paramRef}::text[]) AS v(v))`;
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Gate `mode: 'insensitive'` on `in` / `notIn` to PostgreSQL.
|
|
1714
|
+
*
|
|
1715
|
+
* The property the operator has to hold is that the COLUMN and the LIST
|
|
1716
|
+
* ELEMENTS are folded by the SAME function. Break that and `equals` and `in`
|
|
1717
|
+
* answer the same operand with DIFFERENT rows on the same engine, which is a
|
|
1718
|
+
* wrong row and not an error.
|
|
1719
|
+
*
|
|
1720
|
+
* The first cut broke it: the column went through the engine's `LOWER` while
|
|
1721
|
+
* the elements went through JavaScript's `toLowerCase()`. Those are two
|
|
1722
|
+
* alphabets. SQLite's `LOWER` is ASCII-only, so over the rows `CAFÉ` and
|
|
1723
|
+
* `café` the operand `'CAFÉ'` matched `CAFÉ` through `equals` and `café`
|
|
1724
|
+
* through `in` - not an exotic-codepoint edge case but every non-ASCII letter
|
|
1725
|
+
* on that engine.
|
|
1726
|
+
*
|
|
1727
|
+
* Folding the elements in SQL instead is not reachable from here for the other
|
|
1728
|
+
* dialects: each unpacks a bound IN list in a subquery of its OWN shape
|
|
1729
|
+
* (`json_each` / `JSON_TABLE` / `OPENJSON`) whose projection is written by the
|
|
1730
|
+
* dialect, not by this module, and the length-independence the SQL-template
|
|
1731
|
+
* cache depends on rules out the other portable form, one placeholder per
|
|
1732
|
+
* element. Restoring the operator on those engines means a dialect hook that
|
|
1733
|
+
* projects the folded element, one correct implementation per engine. Until
|
|
1734
|
+
* then a typed refusal is the honest outcome. `equals`, `not` and the LIKE
|
|
1735
|
+
* operators are unaffected: they fold a single operand the engine can reach.
|
|
1736
|
+
*
|
|
1737
|
+
* Called from BOTH the build and the cache-hit param-collect path, mirroring
|
|
1738
|
+
* every other gate in this module.
|
|
1739
|
+
*/
|
|
1740
|
+
function requireInsensitiveList(qi, column, operator) {
|
|
1741
|
+
if (qi.dialect.name === 'postgresql')
|
|
1742
|
+
return;
|
|
1743
|
+
throw new errors_js_1.UnsupportedFeatureError(`mode: 'insensitive' on \`${operator}\` (${column})`, qi.dialect.name, "Case-folding a bound list needs the engine to lower every element in SQL, which this dialect's " +
|
|
1744
|
+
'IN-list form cannot express. Lowering them in JavaScript instead would fold the list and the ' +
|
|
1745
|
+
`column by two different functions, so \`equals\` and \`${operator}\` would return different rows ` +
|
|
1746
|
+
'for the same operand. Write the list as branches: ' +
|
|
1747
|
+
"`{ OR: [{ field: { equals: a, mode: 'insensitive' } }, ...] }`.");
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Refuse `mode: 'insensitive'` beside a non-string equality operand (E003).
|
|
1751
|
+
* `LOWER(5)` is a type error the engine would raise in its own words on the
|
|
1752
|
+
* build path and, worse, a warm template would bind the value without ever
|
|
1753
|
+
* reaching the engine's check for the SQL it was compiled against, so the
|
|
1754
|
+
* refusal runs on both the build and the collect side. The value itself stays
|
|
1755
|
+
* out of the message (safe error mode renders keys, never values).
|
|
1756
|
+
*/
|
|
1757
|
+
function assertInsensitiveOperand(value, operator, column) {
|
|
1758
|
+
const ok = Array.isArray(value) ? value.every((v) => typeof v === 'string') : typeof value === 'string';
|
|
1759
|
+
if (ok)
|
|
1760
|
+
return;
|
|
1761
|
+
throw new errors_js_1.ValidationError(`mode: 'insensitive' on ${column} requires a string operand for '${operator}' ` +
|
|
1762
|
+
`(received ${Array.isArray(value) ? 'a list with a non-string element' : typeof value}). ` +
|
|
1763
|
+
`Case folding applies to text; drop the mode or compare a string.`);
|
|
1764
|
+
}
|
|
1550
1765
|
/**
|
|
1551
1766
|
* Refuse a {@link JsonFilter} carrying a key that is not a JSON operator, and
|
|
1552
1767
|
* refuse a filter that selects a `path` but never compares it.
|
|
@@ -164,8 +164,17 @@ export declare function assertNoGeneratedColumns(qi: BuilderCtx, data: Record<st
|
|
|
164
164
|
* Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
|
|
165
165
|
* pushes any required params onto the shared params array so that WHERE
|
|
166
166
|
* clause numbering continues correctly afterward.
|
|
167
|
-
|
|
168
|
-
|
|
167
|
+
*
|
|
168
|
+
* `refQualifier` prefixes the column REFERENCE an arithmetic operator reads
|
|
169
|
+
* from (`"view_count" = <qualifier>"view_count" + $n`). A plain `UPDATE` has
|
|
170
|
+
* one table in scope and passes nothing; `upsert` passes the table name,
|
|
171
|
+
* because inside `ON CONFLICT ... DO UPDATE SET` the target table and
|
|
172
|
+
* `excluded` are BOTH in scope and a bare reference is 42702 on PostgreSQL.
|
|
173
|
+
* The assignment target on the left stays bare on every engine (it can only
|
|
174
|
+
* ever be the target table's column), and `set` / literal values reference no
|
|
175
|
+
* column at all, so their SQL is byte-identical with or without a qualifier.
|
|
176
|
+
*/
|
|
177
|
+
export declare function buildSetClause(qi: BuilderCtx, key: string, value: unknown, params: unknown[], refQualifier?: string): string;
|
|
169
178
|
/**
|
|
170
179
|
* Reject a `data` value that reached {@link buildSetClause}'s plain-value
|
|
171
180
|
* fallthrough as a plain object with exactly ONE key that is not a known
|