prisma-ltree 0.6.0 → 0.7.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 +33 -0
- package/dist/{codec-types-Ba5cQ3bc.d.mts → codec-types-B0DtEnU6.d.mts} +2 -2
- package/dist/{codec-types-Ba5cQ3bc.d.mts.map → codec-types-B0DtEnU6.d.mts.map} +1 -1
- package/dist/codec-types.d.mts +1 -1
- package/dist/control.mjs +1 -1
- package/dist/{descriptor-meta-Cc6tmP54.mjs → descriptor-meta-DwbFei9y.mjs} +274 -1
- package/dist/descriptor-meta-DwbFei9y.mjs.map +1 -0
- package/dist/{operation-types-8su4lbK-.d.mts → operation-types-DD-KyBms.d.mts} +46 -1
- package/dist/{operation-types-8su4lbK-.d.mts.map → operation-types-DD-KyBms.d.mts.map} +1 -1
- package/dist/operation-types.d.mts +1 -1
- package/dist/pack.d.mts +1 -1
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/runtime.d.mts +35 -1
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +34 -1
- package/dist/runtime.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/descriptor-meta.ts +64 -4
- package/src/core/hierarchy-collection-methods.ts +111 -0
- package/src/core/pattern-validation.ts +313 -0
- package/src/exports/runtime.ts +3 -0
- package/src/types/operation-types.ts +37 -0
- package/dist/descriptor-meta-Cc6tmP54.mjs.map +0 -1
package/dist/runtime.d.mts
CHANGED
|
@@ -1,11 +1,45 @@
|
|
|
1
|
+
import { i as LTREE_CODEC_ID, n as Ltree } from "./codec-types-B0DtEnU6.mjs";
|
|
1
2
|
import { SqlRuntimeExtensionDescriptor } from "@prisma/orm-family-sql/runtime";
|
|
2
3
|
import { CodecDescriptorRegistry } from "@prisma/orm-family-sql/relational-core/query-lane-context";
|
|
4
|
+
import { CollectionMethodContributionType, CollectionMethodModelType } from "@prisma/orm-family-sql/relational-core/collection-methods";
|
|
3
5
|
|
|
4
6
|
//#region src/core/registry.d.ts
|
|
5
7
|
declare const ltreeCodecRegistry: CodecDescriptorRegistry;
|
|
6
8
|
//#endregion
|
|
9
|
+
//#region src/core/hierarchy-collection-methods.d.ts
|
|
10
|
+
type LtreeFieldNames<Fields> = { [Name in keyof Fields]: Fields[Name] extends {
|
|
11
|
+
readonly many: object;
|
|
12
|
+
} ? never : Fields[Name] extends {
|
|
13
|
+
readonly type: {
|
|
14
|
+
readonly codecId: typeof LTREE_CODEC_ID;
|
|
15
|
+
};
|
|
16
|
+
} ? Name : never }[keyof Fields];
|
|
17
|
+
type HierarchyMethodTable<Model extends CollectionMethodModelType> = { readonly [Name in LtreeFieldNames<Model["fields"]> & string as `${Name}_descendants`]: {
|
|
18
|
+
readonly parameters: readonly [root: Ltree];
|
|
19
|
+
readonly filters: true;
|
|
20
|
+
readonly orders: false;
|
|
21
|
+
} } & { readonly [Name in LtreeFieldNames<Model["fields"]> & string as `${Name}_ancestors`]: {
|
|
22
|
+
readonly parameters: readonly [leaf: Ltree];
|
|
23
|
+
readonly filters: true;
|
|
24
|
+
readonly orders: false;
|
|
25
|
+
} };
|
|
26
|
+
/**
|
|
27
|
+
* One `{field}_descendants` / `{field}_ancestors` method per scalar `ltree`
|
|
28
|
+
* field. Descendants map to `isDescendantOf` (`<@`); ancestors map to
|
|
29
|
+
* `isAncestorOf` (`@>`). Both are inclusive of equality, matching PostgreSQL.
|
|
30
|
+
*/
|
|
31
|
+
interface LtreeHierarchyMethods extends CollectionMethodContributionType {
|
|
32
|
+
readonly methods: HierarchyMethodTable<this["model"]>;
|
|
33
|
+
}
|
|
34
|
+
declare module "@prisma/orm-family-sql/relational-core/collection-methods" {
|
|
35
|
+
interface CollectionMethodRegistry {
|
|
36
|
+
readonly "ltree/hierarchy": LtreeHierarchyMethods;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** The run-time half of `LtreeHierarchyMethods`. */
|
|
40
|
+
//#endregion
|
|
7
41
|
//#region src/exports/runtime.d.ts
|
|
8
42
|
declare const ltreeRuntimeDescriptor: SqlRuntimeExtensionDescriptor<"postgres">;
|
|
9
43
|
//#endregion
|
|
10
|
-
export { ltreeRuntimeDescriptor as default, ltreeCodecRegistry };
|
|
44
|
+
export { type LtreeHierarchyMethods, ltreeRuntimeDescriptor as default, ltreeCodecRegistry };
|
|
11
45
|
//# sourceMappingURL=runtime.d.mts.map
|
package/dist/runtime.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.mts","names":[],"sources":["../src/core/registry.ts","../src/exports/runtime.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"runtime.d.mts","names":[],"sources":["../src/core/registry.ts","../src/core/hierarchy-collection-methods.ts","../src/exports/runtime.ts"],"mappings":";;;;;;cAIa,kBAAA,EAAoB,uBACe;;;KCO3C,eAAA,4BACY,MAAA,GAAS,MAAA,CAAO,IAAA;EAAA,SAAyB,IAAA;AAAA,YAEpD,MAAA,CAAO,IAAA;EAAA,SACM,IAAA;IAAA,SAAiB,OAAA,SAAgB,cAAA;EAAA;AAAA,IAE5C,IAAA,iBAEA,MAAA;AAAA,KAEH,oBAAA,eAAmC,yBAAA,wBACpB,eAAA,CAAgB,KAAA,2BAAgC,IAAA;EAAA,SACvD,UAAA,YAAsB,IAAA,EAAM,KAAA;EAAA,SAC5B,OAAA;EAAA,SACA,MAAA;AAAA,0BAGO,eAAA,CAAgB,KAAA,2BAAgC,IAAA;EAAA,SACvD,UAAA,YAAsB,IAAA,EAAM,KAAA;EAAA,SAC5B,OAAA;EAAA,SACA,MAAA;AAAA;;;;;;UASI,qBAAA,SAA8B,gCAAgC;EAAA,SACpE,OAAA,EAAS,oBAAA;AAAA;AAAA;EAAA,UAIR,wBAAA;IAAA,SACC,iBAAA,EAAmB,qBAAqB;EAAA;AAAA;;;;cC1C/C,sBAAA,EAAwB,6BAA6B"}
|
package/dist/runtime.mjs
CHANGED
|
@@ -1,4 +1,36 @@
|
|
|
1
|
-
import
|
|
1
|
+
import "./constants-Dfknw-h5.mjs";
|
|
2
|
+
import { n as ltreeQueryOperations, r as ltreeCodecRegistry, t as ltreePackMeta } from "./descriptor-meta-DwbFei9y.mjs";
|
|
3
|
+
import { blindCast } from "@prisma/orm-framework/utils/casts";
|
|
4
|
+
import { InternalError } from "@prisma/orm-framework/utils/internal-error";
|
|
5
|
+
//#region src/core/hierarchy-collection-methods.ts
|
|
6
|
+
function fieldOf(model, field) {
|
|
7
|
+
const accessor = blindCast(model)[field];
|
|
8
|
+
if (accessor === void 0) throw new InternalError(`The model accessor has no ltree field "${field}".`);
|
|
9
|
+
return accessor;
|
|
10
|
+
}
|
|
11
|
+
function descendantsMethod(field) {
|
|
12
|
+
return (collection, ...args) => {
|
|
13
|
+
const [root] = blindCast(args);
|
|
14
|
+
return collection.where((model) => fieldOf(model, field).isDescendantOf(root));
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function ancestorsMethod(field) {
|
|
18
|
+
return (collection, ...args) => {
|
|
19
|
+
const [leaf] = blindCast(args);
|
|
20
|
+
return collection.where((model) => fieldOf(model, field).isAncestorOf(leaf));
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function isScalarLtreeField(field) {
|
|
24
|
+
if (typeof field !== "object" || field === null) return false;
|
|
25
|
+
const record = field;
|
|
26
|
+
return record.many === void 0 && record.type?.kind === "scalar" && record.type.codecId === "pg/ltree@1";
|
|
27
|
+
}
|
|
28
|
+
/** The run-time half of `LtreeHierarchyMethods`. */
|
|
29
|
+
const ltreeHierarchyMethods = {
|
|
30
|
+
id: "ltree/hierarchy",
|
|
31
|
+
methodsFor: (model) => Object.fromEntries(Object.entries(model.fields).flatMap(([name, field]) => isScalarLtreeField(field) ? [[`${name}_descendants`, descendantsMethod(name)], [`${name}_ancestors`, ancestorsMethod(name)]] : []))
|
|
32
|
+
};
|
|
33
|
+
//#endregion
|
|
2
34
|
//#region src/exports/runtime.ts
|
|
3
35
|
const ltreeRuntimeDescriptor = {
|
|
4
36
|
kind: "extension",
|
|
@@ -10,6 +42,7 @@ const ltreeRuntimeDescriptor = {
|
|
|
10
42
|
types: { codecTypes: { codecDescriptors: Array.from(ltreeCodecRegistry.values()) } },
|
|
11
43
|
codecs: () => Array.from(ltreeCodecRegistry.values()),
|
|
12
44
|
queryOperations: () => ltreeQueryOperations(),
|
|
45
|
+
collectionMethods: () => [ltreeHierarchyMethods],
|
|
13
46
|
create() {
|
|
14
47
|
return {
|
|
15
48
|
familyId: "sql",
|
package/dist/runtime.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.mjs","names":[],"sources":["../src/exports/runtime.ts"],"sourcesContent":["import type { SqlRuntimeExtensionDescriptor } from \"@prisma/orm-family-sql/runtime\";\nimport { ltreePackMeta, ltreeQueryOperations } from \"../core/descriptor-meta\";\nimport { ltreeCodecRegistry } from \"../core/registry\";\n\nconst ltreeRuntimeDescriptor: SqlRuntimeExtensionDescriptor<\"postgres\"> = {\n kind: \"extension\" as const,\n id: ltreePackMeta.id,\n version: ltreePackMeta.version,\n dataTypes: ltreePackMeta.dataTypes,\n familyId: \"sql\" as const,\n targetId: \"postgres\" as const,\n types: {\n codecTypes: {\n codecDescriptors: Array.from(ltreeCodecRegistry.values()),\n },\n },\n codecs: () => Array.from(ltreeCodecRegistry.values()),\n queryOperations: () => ltreeQueryOperations(),\n create() {\n return {\n familyId: \"sql\" as const,\n targetId: \"postgres\" as const,\n };\n },\n};\n\nexport { ltreeCodecRegistry };\nexport default ltreeRuntimeDescriptor;\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"runtime.mjs","names":[],"sources":["../src/core/hierarchy-collection-methods.ts","../src/exports/runtime.ts"],"sourcesContent":["import type {\n CollectionMethodBody,\n CollectionMethodContribution,\n CollectionMethodContributionId,\n CollectionMethodContributionType,\n CollectionMethodModelType,\n} from \"@prisma/orm-family-sql/relational-core/collection-methods\";\nimport { blindCast } from \"@prisma/orm-framework/utils/casts\";\nimport { InternalError } from \"@prisma/orm-framework/utils/internal-error\";\nimport type { Ltree } from \"../types/codec-types\";\nimport { LTREE_CODEC_ID } from \"./constants\";\n\ntype LtreeFieldNames<Fields> = {\n [Name in keyof Fields]: Fields[Name] extends { readonly many: object }\n ? never\n : Fields[Name] extends {\n readonly type: { readonly codecId: typeof LTREE_CODEC_ID };\n }\n ? Name\n : never;\n}[keyof Fields];\n\ntype HierarchyMethodTable<Model extends CollectionMethodModelType> = {\n readonly [Name in LtreeFieldNames<Model[\"fields\"]> & string as `${Name}_descendants`]: {\n readonly parameters: readonly [root: Ltree];\n readonly filters: true;\n readonly orders: false;\n };\n} & {\n readonly [Name in LtreeFieldNames<Model[\"fields\"]> & string as `${Name}_ancestors`]: {\n readonly parameters: readonly [leaf: Ltree];\n readonly filters: true;\n readonly orders: false;\n };\n};\n\n/**\n * One `{field}_descendants` / `{field}_ancestors` method per scalar `ltree`\n * field. Descendants map to `isDescendantOf` (`<@`); ancestors map to\n * `isAncestorOf` (`@>`). Both are inclusive of equality, matching PostgreSQL.\n */\nexport interface LtreeHierarchyMethods extends CollectionMethodContributionType {\n readonly methods: HierarchyMethodTable<this[\"model\"]>;\n}\n\ndeclare module \"@prisma/orm-family-sql/relational-core/collection-methods\" {\n interface CollectionMethodRegistry {\n readonly \"ltree/hierarchy\": LtreeHierarchyMethods;\n }\n}\n\ninterface HierarchyField {\n isDescendantOf(root: Ltree): unknown;\n isAncestorOf(leaf: Ltree): unknown;\n}\n\ntype LtreeFields = Readonly<Record<string, HierarchyField>>;\n\nfunction fieldOf(model: unknown, field: string): HierarchyField {\n const accessor = blindCast<\n LtreeFields,\n \"an ltree field of the ORM model accessor has hierarchy operators\"\n >(model)[field];\n if (accessor === undefined) {\n throw new InternalError(`The model accessor has no ltree field \"${field}\".`);\n }\n return accessor;\n}\n\nfunction descendantsMethod(field: string): CollectionMethodBody {\n return (collection, ...args) => {\n const [root] = blindCast<readonly [Ltree], \"the type half declares a root path\">(args);\n return collection.where((model) => fieldOf(model, field).isDescendantOf(root));\n };\n}\n\nfunction ancestorsMethod(field: string): CollectionMethodBody {\n return (collection, ...args) => {\n const [leaf] = blindCast<readonly [Ltree], \"the type half declares a leaf path\">(args);\n return collection.where((model) => fieldOf(model, field).isAncestorOf(leaf));\n };\n}\n\nfunction isScalarLtreeField(field: unknown): boolean {\n if (typeof field !== \"object\" || field === null) return false;\n const record = field as {\n readonly many?: unknown;\n readonly type?: { readonly kind?: string; readonly codecId?: string };\n };\n return (\n record.many === undefined &&\n record.type?.kind === \"scalar\" &&\n record.type.codecId === LTREE_CODEC_ID\n );\n}\n\n/** The run-time half of `LtreeHierarchyMethods`. */\nexport const ltreeHierarchyMethods: CollectionMethodContribution = {\n id: \"ltree/hierarchy\" satisfies CollectionMethodContributionId,\n methodsFor: (model) =>\n Object.fromEntries(\n Object.entries(model.fields).flatMap(([name, field]) =>\n isScalarLtreeField(field)\n ? [\n [`${name}_descendants`, descendantsMethod(name)],\n [`${name}_ancestors`, ancestorsMethod(name)],\n ]\n : [],\n ),\n ),\n};\n","import type { SqlRuntimeExtensionDescriptor } from \"@prisma/orm-family-sql/runtime\";\nimport { ltreePackMeta, ltreeQueryOperations } from \"../core/descriptor-meta\";\nimport { ltreeHierarchyMethods } from \"../core/hierarchy-collection-methods\";\nimport { ltreeCodecRegistry } from \"../core/registry\";\n\nconst ltreeRuntimeDescriptor: SqlRuntimeExtensionDescriptor<\"postgres\"> = {\n kind: \"extension\" as const,\n id: ltreePackMeta.id,\n version: ltreePackMeta.version,\n dataTypes: ltreePackMeta.dataTypes,\n familyId: \"sql\" as const,\n targetId: \"postgres\" as const,\n types: {\n codecTypes: {\n codecDescriptors: Array.from(ltreeCodecRegistry.values()),\n },\n },\n codecs: () => Array.from(ltreeCodecRegistry.values()),\n queryOperations: () => ltreeQueryOperations(),\n collectionMethods: () => [ltreeHierarchyMethods],\n create() {\n return {\n familyId: \"sql\" as const,\n targetId: \"postgres\" as const,\n };\n },\n};\n\nexport type { LtreeHierarchyMethods } from \"../core/hierarchy-collection-methods\";\nexport { ltreeCodecRegistry };\nexport default ltreeRuntimeDescriptor;\n"],"mappings":";;;;;AA0DA,SAAS,QAAQ,OAAgB,OAA+B;CAC9D,MAAM,WAAW,UAGf,KAAK,EAAE;CACT,IAAI,aAAa,KAAA,GACf,MAAM,IAAI,cAAc,0CAA0C,MAAM,GAAG;CAE7E,OAAO;AACT;AAEA,SAAS,kBAAkB,OAAqC;CAC9D,QAAQ,YAAY,GAAG,SAAS;EAC9B,MAAM,CAAC,QAAQ,UAAkE,IAAI;EACrF,OAAO,WAAW,OAAO,UAAU,QAAQ,OAAO,KAAK,EAAE,eAAe,IAAI,CAAC;CAC/E;AACF;AAEA,SAAS,gBAAgB,OAAqC;CAC5D,QAAQ,YAAY,GAAG,SAAS;EAC9B,MAAM,CAAC,QAAQ,UAAkE,IAAI;EACrF,OAAO,WAAW,OAAO,UAAU,QAAQ,OAAO,KAAK,EAAE,aAAa,IAAI,CAAC;CAC7E;AACF;AAEA,SAAS,mBAAmB,OAAyB;CACnD,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,SAAS;CAIf,OACE,OAAO,SAAS,KAAA,KAChB,OAAO,MAAM,SAAS,YACtB,OAAO,KAAK,YAAA;AAEhB;;AAGA,MAAa,wBAAsD;CACjE,IAAI;CACJ,aAAa,UACX,OAAO,YACL,OAAO,QAAQ,MAAM,MAAM,EAAE,SAAS,CAAC,MAAM,WAC3C,mBAAmB,KAAK,IACpB,CACE,CAAC,GAAG,KAAK,eAAe,kBAAkB,IAAI,CAAC,GAC/C,CAAC,GAAG,KAAK,aAAa,gBAAgB,IAAI,CAAC,CAC7C,IACA,CAAC,CACP,CACF;AACJ;;;ACzGA,MAAM,yBAAoE;CACxE,MAAM;CACN,IAAI,cAAc;CAClB,SAAS,cAAc;CACvB,WAAW,cAAc;CACzB,UAAU;CACV,UAAU;CACV,OAAO,EACL,YAAY,EACV,kBAAkB,MAAM,KAAK,mBAAmB,OAAO,CAAC,EAC1D,EACF;CACA,cAAc,MAAM,KAAK,mBAAmB,OAAO,CAAC;CACpD,uBAAuB,qBAAqB;CAC5C,yBAAyB,CAAC,qBAAqB;CAC/C,SAAS;EACP,OAAO;GACL,UAAU;GACV,UAAU;EACZ;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import type { QueryOperationTypes } from "../types/operation-types";
|
|
|
11
11
|
import { ltreeAuthoringTypes } from "./authoring";
|
|
12
12
|
import { LTREE_ARRAY_CODEC_ID, LTREE_CODEC_ID } from "./constants";
|
|
13
13
|
import { ltreeDataTypes } from "./data-types";
|
|
14
|
+
import { assertPatternArg } from "./pattern-validation";
|
|
14
15
|
import { ltreeCodecRegistry } from "./registry";
|
|
15
16
|
|
|
16
17
|
type CodecTypesBase = Record<string, { readonly input: unknown; readonly output: unknown }>;
|
|
@@ -57,8 +58,10 @@ export function ltreeQueryOperations<CT extends CodecTypesBase>(): QueryOperatio
|
|
|
57
58
|
// Hierarchy operators compare two `ltree` values; the user argument shares the
|
|
58
59
|
// column's `pg/ltree@1` codec. Pattern-match operators take a query/pattern
|
|
59
60
|
// string (or `string[]`) that is NOT an ltree — it is bound as text and cast
|
|
60
|
-
// in-template to `lquery` / `lquery[]` / `ltxtquery
|
|
61
|
-
//
|
|
61
|
+
// in-template to `lquery` / `lquery[]` / `ltxtquery`. Literal string args get a
|
|
62
|
+
// lightweight structural check first (see `pattern-validation.ts`) so empty /
|
|
63
|
+
// obviously illegal patterns fail with a clear pack error instead of an opaque
|
|
64
|
+
// Postgres cast failure; Expression / ParamRef binds still defer to Postgres.
|
|
62
65
|
const hierarchyOp = (
|
|
63
66
|
method: string,
|
|
64
67
|
operator: string,
|
|
@@ -80,11 +83,12 @@ export function ltreeQueryOperations<CT extends CodecTypesBase>(): QueryOperatio
|
|
|
80
83
|
const patternOp = (
|
|
81
84
|
method: string,
|
|
82
85
|
operator: string,
|
|
83
|
-
castType:
|
|
86
|
+
castType: "lquery" | "ltxtquery" | "lquery[]",
|
|
84
87
|
self: CodecExpression<typeof LTREE_CODEC_ID, boolean, CT>,
|
|
85
88
|
arg: unknown,
|
|
86
89
|
many = false,
|
|
87
90
|
): BoolReturn => {
|
|
91
|
+
assertPatternArg(castType, arg);
|
|
88
92
|
const selfCodec = codecOf(self);
|
|
89
93
|
return buildOperation({
|
|
90
94
|
method,
|
|
@@ -118,6 +122,34 @@ export function ltreeQueryOperations<CT extends CodecTypesBase>(): QueryOperatio
|
|
|
118
122
|
lowering: { targetFamily: "sql", template },
|
|
119
123
|
});
|
|
120
124
|
|
|
125
|
+
// Boolean operators on an `ltree[]` receiver (ADR-007). Same bind/cast rules as
|
|
126
|
+
// `firstMatchOp`, but the result is `boolean`. These are the operators
|
|
127
|
+
// `gist__ltree_ops` indexes (`<@`, `~`, `@`, `?`), plus `@>` which PostgreSQL
|
|
128
|
+
// ships and the opclass does not index.
|
|
129
|
+
const arrayBoolOp = (
|
|
130
|
+
method: string,
|
|
131
|
+
operator: string,
|
|
132
|
+
self: CodecExpression<typeof LTREE_ARRAY_CODEC_ID, boolean, CT>,
|
|
133
|
+
arg: unknown,
|
|
134
|
+
argCodecId: typeof LTREE_CODEC_ID | typeof TEXT_CODEC_ID,
|
|
135
|
+
castType?: "lquery" | "ltxtquery" | "lquery[]",
|
|
136
|
+
many = false,
|
|
137
|
+
): BoolReturn => {
|
|
138
|
+
if (castType) assertPatternArg(castType, arg);
|
|
139
|
+
const template = castType
|
|
140
|
+
? `{{self}} ${operator} ({{arg0}})::${castType}`
|
|
141
|
+
: `{{self}} ${operator} {{arg0}}`;
|
|
142
|
+
return buildOperation({
|
|
143
|
+
method,
|
|
144
|
+
args: [
|
|
145
|
+
toExpr(self, codecOf(self)),
|
|
146
|
+
toExpr(arg, many ? { codecId: argCodecId, many: true } : { codecId: argCodecId }),
|
|
147
|
+
],
|
|
148
|
+
returns: BOOL_RETURN,
|
|
149
|
+
lowering: { targetFamily: "sql", template },
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
121
153
|
// Tier 3 — first-match operators on an `ltree[]` receiver (ADR-003).
|
|
122
154
|
const firstMatchOp = (
|
|
123
155
|
method: string,
|
|
@@ -125,8 +157,9 @@ export function ltreeQueryOperations<CT extends CodecTypesBase>(): QueryOperatio
|
|
|
125
157
|
self: CodecExpression<typeof LTREE_ARRAY_CODEC_ID, boolean, CT>,
|
|
126
158
|
arg: unknown,
|
|
127
159
|
argCodecId: typeof LTREE_CODEC_ID | typeof TEXT_CODEC_ID,
|
|
128
|
-
castType?:
|
|
160
|
+
castType?: "lquery" | "ltxtquery",
|
|
129
161
|
): LtreeReturn => {
|
|
162
|
+
if (castType) assertPatternArg(castType, arg);
|
|
130
163
|
const selfCodec = codecOf(self);
|
|
131
164
|
const template = castType
|
|
132
165
|
? `{{self}} ${operator} ({{arg0}})::${castType}`
|
|
@@ -281,6 +314,33 @@ export function ltreeQueryOperations<CT extends CodecTypesBase>(): QueryOperatio
|
|
|
281
314
|
impl: (self, query) =>
|
|
282
315
|
firstMatchOp("firstMatchLtxtquery", "?@", self, query, TEXT_CODEC_ID, "ltxtquery"),
|
|
283
316
|
},
|
|
317
|
+
// Boolean array operators (ADR-007). Distinct names from scalar methods because
|
|
318
|
+
// the operation registry keys by name only. `containsDescendantOf`,
|
|
319
|
+
// `matchesAnyLquery`, `matchesAnyLtxtquery`, and `matchesAnyLqueryArray` are
|
|
320
|
+
// in `gist__ltree_ops`; `containsAncestorOf` is not.
|
|
321
|
+
containsAncestorOf: {
|
|
322
|
+
self: { codecId: LTREE_ARRAY_CODEC_ID },
|
|
323
|
+
impl: (self, other) => arrayBoolOp("containsAncestorOf", "@>", self, other, LTREE_CODEC_ID),
|
|
324
|
+
},
|
|
325
|
+
containsDescendantOf: {
|
|
326
|
+
self: { codecId: LTREE_ARRAY_CODEC_ID },
|
|
327
|
+
impl: (self, other) => arrayBoolOp("containsDescendantOf", "<@", self, other, LTREE_CODEC_ID),
|
|
328
|
+
},
|
|
329
|
+
matchesAnyLquery: {
|
|
330
|
+
self: { codecId: LTREE_ARRAY_CODEC_ID },
|
|
331
|
+
impl: (self, pattern) =>
|
|
332
|
+
arrayBoolOp("matchesAnyLquery", "~", self, pattern, TEXT_CODEC_ID, "lquery"),
|
|
333
|
+
},
|
|
334
|
+
matchesAnyLqueryArray: {
|
|
335
|
+
self: { codecId: LTREE_ARRAY_CODEC_ID },
|
|
336
|
+
impl: (self, patterns) =>
|
|
337
|
+
arrayBoolOp("matchesAnyLqueryArray", "?", self, patterns, TEXT_CODEC_ID, "lquery[]", true),
|
|
338
|
+
},
|
|
339
|
+
matchesAnyLtxtquery: {
|
|
340
|
+
self: { codecId: LTREE_ARRAY_CODEC_ID },
|
|
341
|
+
impl: (self, query) =>
|
|
342
|
+
arrayBoolOp("matchesAnyLtxtquery", "@", self, query, TEXT_CODEC_ID, "ltxtquery"),
|
|
343
|
+
},
|
|
284
344
|
// `lca(ltree[])` — array-receiver form (ADR-001, ADR-005). Named `lcaAll`
|
|
285
345
|
// because prisma-next keys operations by name only (`createOperationRegistry`
|
|
286
346
|
// throws on a duplicate); `lca` is already the variadic scalar method on
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CollectionMethodBody,
|
|
3
|
+
CollectionMethodContribution,
|
|
4
|
+
CollectionMethodContributionId,
|
|
5
|
+
CollectionMethodContributionType,
|
|
6
|
+
CollectionMethodModelType,
|
|
7
|
+
} from "@prisma/orm-family-sql/relational-core/collection-methods";
|
|
8
|
+
import { blindCast } from "@prisma/orm-framework/utils/casts";
|
|
9
|
+
import { InternalError } from "@prisma/orm-framework/utils/internal-error";
|
|
10
|
+
import type { Ltree } from "../types/codec-types";
|
|
11
|
+
import { LTREE_CODEC_ID } from "./constants";
|
|
12
|
+
|
|
13
|
+
type LtreeFieldNames<Fields> = {
|
|
14
|
+
[Name in keyof Fields]: Fields[Name] extends { readonly many: object }
|
|
15
|
+
? never
|
|
16
|
+
: Fields[Name] extends {
|
|
17
|
+
readonly type: { readonly codecId: typeof LTREE_CODEC_ID };
|
|
18
|
+
}
|
|
19
|
+
? Name
|
|
20
|
+
: never;
|
|
21
|
+
}[keyof Fields];
|
|
22
|
+
|
|
23
|
+
type HierarchyMethodTable<Model extends CollectionMethodModelType> = {
|
|
24
|
+
readonly [Name in LtreeFieldNames<Model["fields"]> & string as `${Name}_descendants`]: {
|
|
25
|
+
readonly parameters: readonly [root: Ltree];
|
|
26
|
+
readonly filters: true;
|
|
27
|
+
readonly orders: false;
|
|
28
|
+
};
|
|
29
|
+
} & {
|
|
30
|
+
readonly [Name in LtreeFieldNames<Model["fields"]> & string as `${Name}_ancestors`]: {
|
|
31
|
+
readonly parameters: readonly [leaf: Ltree];
|
|
32
|
+
readonly filters: true;
|
|
33
|
+
readonly orders: false;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One `{field}_descendants` / `{field}_ancestors` method per scalar `ltree`
|
|
39
|
+
* field. Descendants map to `isDescendantOf` (`<@`); ancestors map to
|
|
40
|
+
* `isAncestorOf` (`@>`). Both are inclusive of equality, matching PostgreSQL.
|
|
41
|
+
*/
|
|
42
|
+
export interface LtreeHierarchyMethods extends CollectionMethodContributionType {
|
|
43
|
+
readonly methods: HierarchyMethodTable<this["model"]>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare module "@prisma/orm-family-sql/relational-core/collection-methods" {
|
|
47
|
+
interface CollectionMethodRegistry {
|
|
48
|
+
readonly "ltree/hierarchy": LtreeHierarchyMethods;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface HierarchyField {
|
|
53
|
+
isDescendantOf(root: Ltree): unknown;
|
|
54
|
+
isAncestorOf(leaf: Ltree): unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type LtreeFields = Readonly<Record<string, HierarchyField>>;
|
|
58
|
+
|
|
59
|
+
function fieldOf(model: unknown, field: string): HierarchyField {
|
|
60
|
+
const accessor = blindCast<
|
|
61
|
+
LtreeFields,
|
|
62
|
+
"an ltree field of the ORM model accessor has hierarchy operators"
|
|
63
|
+
>(model)[field];
|
|
64
|
+
if (accessor === undefined) {
|
|
65
|
+
throw new InternalError(`The model accessor has no ltree field "${field}".`);
|
|
66
|
+
}
|
|
67
|
+
return accessor;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function descendantsMethod(field: string): CollectionMethodBody {
|
|
71
|
+
return (collection, ...args) => {
|
|
72
|
+
const [root] = blindCast<readonly [Ltree], "the type half declares a root path">(args);
|
|
73
|
+
return collection.where((model) => fieldOf(model, field).isDescendantOf(root));
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ancestorsMethod(field: string): CollectionMethodBody {
|
|
78
|
+
return (collection, ...args) => {
|
|
79
|
+
const [leaf] = blindCast<readonly [Ltree], "the type half declares a leaf path">(args);
|
|
80
|
+
return collection.where((model) => fieldOf(model, field).isAncestorOf(leaf));
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isScalarLtreeField(field: unknown): boolean {
|
|
85
|
+
if (typeof field !== "object" || field === null) return false;
|
|
86
|
+
const record = field as {
|
|
87
|
+
readonly many?: unknown;
|
|
88
|
+
readonly type?: { readonly kind?: string; readonly codecId?: string };
|
|
89
|
+
};
|
|
90
|
+
return (
|
|
91
|
+
record.many === undefined &&
|
|
92
|
+
record.type?.kind === "scalar" &&
|
|
93
|
+
record.type.codecId === LTREE_CODEC_ID
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The run-time half of `LtreeHierarchyMethods`. */
|
|
98
|
+
export const ltreeHierarchyMethods: CollectionMethodContribution = {
|
|
99
|
+
id: "ltree/hierarchy" satisfies CollectionMethodContributionId,
|
|
100
|
+
methodsFor: (model) =>
|
|
101
|
+
Object.fromEntries(
|
|
102
|
+
Object.entries(model.fields).flatMap(([name, field]) =>
|
|
103
|
+
isScalarLtreeField(field)
|
|
104
|
+
? [
|
|
105
|
+
[`${name}_descendants`, descendantsMethod(name)],
|
|
106
|
+
[`${name}_ancestors`, ancestorsMethod(name)],
|
|
107
|
+
]
|
|
108
|
+
: [],
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight structural checks for PostgreSQL `lquery` / `ltxtquery` pattern
|
|
3
|
+
* arguments. These catch empty values, empty path segments, unbalanced
|
|
4
|
+
* operators/parentheses, and obviously illegal syntax before the value is cast
|
|
5
|
+
* in SQL — without inventing framework PN-* error codes.
|
|
6
|
+
*
|
|
7
|
+
* Label bodies intentionally allow Unicode letters/numbers (`\p{L}` / `\p{N}`)
|
|
8
|
+
* plus `_` / `-` so locale-dependent labels that Postgres accepts are not
|
|
9
|
+
* rejected here. Full Postgres parsing still happens at the database.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Word body chars: locale letters/digits, underscore, hyphen. */
|
|
13
|
+
const WORD_CHAR = /[\p{L}\p{N}_-]/u;
|
|
14
|
+
|
|
15
|
+
function isWordChar(ch: string): boolean {
|
|
16
|
+
return WORD_CHAR.test(ch);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isDigit(ch: string): boolean {
|
|
20
|
+
return ch >= "0" && ch <= "9";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function fail(kind: "lquery" | "ltxtquery", message: string, value: string): never {
|
|
24
|
+
throw new Error(`${kind} ${message}: "${value}"`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse an optional `{n}`, `{n,}`, `{,m}`, `{n,m}`, or `{,}` quantifier starting
|
|
29
|
+
* at `i`. Returns the index after the closing `}`, or `i` when no quantifier
|
|
30
|
+
* begins here. Throws on a malformed `{...}`.
|
|
31
|
+
*/
|
|
32
|
+
function consumeQuantifier(
|
|
33
|
+
kind: "lquery" | "ltxtquery",
|
|
34
|
+
value: string,
|
|
35
|
+
i: number,
|
|
36
|
+
allow: boolean,
|
|
37
|
+
): number {
|
|
38
|
+
if (value[i] !== "{") return i;
|
|
39
|
+
if (!allow) {
|
|
40
|
+
fail(kind, `syntax error at character ${i + 1}`, value);
|
|
41
|
+
}
|
|
42
|
+
let j = i + 1;
|
|
43
|
+
let low: number | undefined;
|
|
44
|
+
let high: number | undefined;
|
|
45
|
+
let sawComma = false;
|
|
46
|
+
|
|
47
|
+
const readNumber = (): number | undefined => {
|
|
48
|
+
if (j >= value.length || !isDigit(value[j]!)) return undefined;
|
|
49
|
+
const start = j;
|
|
50
|
+
while (j < value.length && isDigit(value[j]!)) j += 1;
|
|
51
|
+
return Number(value.slice(start, j));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
low = readNumber();
|
|
55
|
+
if (j < value.length && value[j] === ",") {
|
|
56
|
+
sawComma = true;
|
|
57
|
+
j += 1;
|
|
58
|
+
high = readNumber();
|
|
59
|
+
}
|
|
60
|
+
if (j >= value.length || value[j] !== "}") {
|
|
61
|
+
fail(kind, `syntax error at character ${i + 1}`, value);
|
|
62
|
+
}
|
|
63
|
+
j += 1;
|
|
64
|
+
|
|
65
|
+
// `{}` is invalid; `{,}` (both ends open) is valid in Postgres.
|
|
66
|
+
if (low === undefined && high === undefined && !sawComma) {
|
|
67
|
+
fail(kind, `syntax error at character ${i + 1}`, value);
|
|
68
|
+
}
|
|
69
|
+
if (low !== undefined && high !== undefined && low > high) {
|
|
70
|
+
fail(kind, "syntax error (quantifier low > high)", value);
|
|
71
|
+
}
|
|
72
|
+
return j;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Consume `@` / `*` / `%` modifiers that follow a non-star word. */
|
|
76
|
+
function consumeModifiers(value: string, i: number): number {
|
|
77
|
+
while (i < value.length) {
|
|
78
|
+
const ch = value[i]!;
|
|
79
|
+
if (ch === "@" || ch === "*" || ch === "%") {
|
|
80
|
+
i += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
return i;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function consumeWord(kind: "lquery" | "ltxtquery", value: string, i: number): number {
|
|
89
|
+
if (i >= value.length || !isWordChar(value[i]!)) {
|
|
90
|
+
fail(kind, `syntax error at character ${i + 1}`, value);
|
|
91
|
+
}
|
|
92
|
+
let j = i;
|
|
93
|
+
while (j < value.length && isWordChar(value[j]!)) j += 1;
|
|
94
|
+
return j;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* One lquery level: either `*` + optional quantifier, or an optional `!`
|
|
99
|
+
* followed by `word mods? (| word mods?)*` and an optional trailing quantifier.
|
|
100
|
+
*/
|
|
101
|
+
function consumeLqueryLevel(value: string, i: number): number {
|
|
102
|
+
if (value[i] === "*") {
|
|
103
|
+
// Star wildcards cannot take `@`/`%` modifiers; only an optional quantifier.
|
|
104
|
+
return consumeQuantifier("lquery", value, i + 1, true);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let j = i;
|
|
108
|
+
if (value[j] === "!") {
|
|
109
|
+
j += 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
j = consumeWord("lquery", value, j);
|
|
113
|
+
j = consumeModifiers(value, j);
|
|
114
|
+
|
|
115
|
+
while (j < value.length && value[j] === "|") {
|
|
116
|
+
j += 1;
|
|
117
|
+
j = consumeWord("lquery", value, j);
|
|
118
|
+
j = consumeModifiers(value, j);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return consumeQuantifier("lquery", value, j, true);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function assertValidLquery(value: unknown): asserts value is string {
|
|
125
|
+
if (typeof value !== "string") {
|
|
126
|
+
throw new Error("lquery pattern must be a string");
|
|
127
|
+
}
|
|
128
|
+
if (value.length === 0) {
|
|
129
|
+
throw new Error('lquery pattern must not be empty: ""');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let i = 0;
|
|
133
|
+
i = consumeLqueryLevel(value, i);
|
|
134
|
+
while (i < value.length) {
|
|
135
|
+
if (value[i] !== ".") {
|
|
136
|
+
fail("lquery", `syntax error at character ${i + 1}`, value);
|
|
137
|
+
}
|
|
138
|
+
i += 1;
|
|
139
|
+
if (i >= value.length) {
|
|
140
|
+
fail("lquery", "syntax error (trailing '.' / empty label)", value);
|
|
141
|
+
}
|
|
142
|
+
i = consumeLqueryLevel(value, i);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function assertValidLqueryArray(value: unknown): asserts value is readonly string[] {
|
|
147
|
+
if (!Array.isArray(value)) {
|
|
148
|
+
throw new Error("lquery[] patterns must be a string array");
|
|
149
|
+
}
|
|
150
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
151
|
+
const entry = value[index];
|
|
152
|
+
try {
|
|
153
|
+
assertValidLquery(entry);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
156
|
+
throw new Error(`lquery[] patterns[${index}]: ${message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
type LtxtToken =
|
|
162
|
+
| { readonly kind: "word"; readonly text: string }
|
|
163
|
+
| { readonly kind: "and" }
|
|
164
|
+
| { readonly kind: "or" }
|
|
165
|
+
| { readonly kind: "not" }
|
|
166
|
+
| { readonly kind: "lparen" }
|
|
167
|
+
| { readonly kind: "rparen" };
|
|
168
|
+
|
|
169
|
+
function tokenizeLtxtquery(value: string): LtxtToken[] {
|
|
170
|
+
const tokens: LtxtToken[] = [];
|
|
171
|
+
let i = 0;
|
|
172
|
+
while (i < value.length) {
|
|
173
|
+
const ch = value[i]!;
|
|
174
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
|
|
175
|
+
i += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (ch === "&") {
|
|
179
|
+
tokens.push({ kind: "and" });
|
|
180
|
+
i += 1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (ch === "|") {
|
|
184
|
+
tokens.push({ kind: "or" });
|
|
185
|
+
i += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (ch === "!") {
|
|
189
|
+
tokens.push({ kind: "not" });
|
|
190
|
+
i += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (ch === "(") {
|
|
194
|
+
tokens.push({ kind: "lparen" });
|
|
195
|
+
i += 1;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (ch === ")") {
|
|
199
|
+
tokens.push({ kind: "rparen" });
|
|
200
|
+
i += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (isWordChar(ch)) {
|
|
204
|
+
const start = i;
|
|
205
|
+
i = consumeWord("ltxtquery", value, i);
|
|
206
|
+
i = consumeModifiers(value, i);
|
|
207
|
+
tokens.push({ kind: "word", text: value.slice(start, i) });
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
fail("ltxtquery", `syntax error at character ${i + 1}`, value);
|
|
211
|
+
}
|
|
212
|
+
return tokens;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Recursive-descent check mirroring Postgres ltxtquery operator precedence:
|
|
217
|
+
* OR < AND < NOT < primary (word / parenthesized expr).
|
|
218
|
+
*/
|
|
219
|
+
function parseLtxtExpr(tokens: readonly LtxtToken[], value: string): void {
|
|
220
|
+
let pos = 0;
|
|
221
|
+
|
|
222
|
+
const peek = (): LtxtToken | undefined => tokens[pos];
|
|
223
|
+
const take = (): LtxtToken => {
|
|
224
|
+
const tok = tokens[pos];
|
|
225
|
+
if (!tok) fail("ltxtquery", "syntax error (unexpected end)", value);
|
|
226
|
+
pos += 1;
|
|
227
|
+
return tok;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const parsePrimary = (): void => {
|
|
231
|
+
const tok = peek();
|
|
232
|
+
if (!tok) fail("ltxtquery", "syntax error (missing operand)", value);
|
|
233
|
+
if (tok.kind === "word") {
|
|
234
|
+
take();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (tok.kind === "lparen") {
|
|
238
|
+
take();
|
|
239
|
+
parseOr();
|
|
240
|
+
const close = take();
|
|
241
|
+
if (close.kind !== "rparen") {
|
|
242
|
+
fail("ltxtquery", "syntax error (expected ')')", value);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
fail("ltxtquery", "syntax error (missing operand)", value);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const parseFactor = (): void => {
|
|
250
|
+
while (peek()?.kind === "not") take();
|
|
251
|
+
parsePrimary();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const parseAnd = (): void => {
|
|
255
|
+
parseFactor();
|
|
256
|
+
while (peek()?.kind === "and") {
|
|
257
|
+
take();
|
|
258
|
+
parseFactor();
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const parseOr = (): void => {
|
|
263
|
+
parseAnd();
|
|
264
|
+
while (peek()?.kind === "or") {
|
|
265
|
+
take();
|
|
266
|
+
parseAnd();
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
parseOr();
|
|
271
|
+
if (pos !== tokens.length) {
|
|
272
|
+
fail("ltxtquery", "syntax error (unexpected token)", value);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function assertValidLtxtquery(value: unknown): asserts value is string {
|
|
277
|
+
if (typeof value !== "string") {
|
|
278
|
+
throw new Error("ltxtquery query must be a string");
|
|
279
|
+
}
|
|
280
|
+
if (value.length === 0) {
|
|
281
|
+
throw new Error('ltxtquery query must not be empty: ""');
|
|
282
|
+
}
|
|
283
|
+
const tokens = tokenizeLtxtquery(value);
|
|
284
|
+
if (tokens.length === 0) {
|
|
285
|
+
fail("ltxtquery", "syntax error (missing operand)", value);
|
|
286
|
+
}
|
|
287
|
+
parseLtxtExpr(tokens, value);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Validate a pattern argument when the caller passed a raw JS value. Expression
|
|
292
|
+
* / ParamRef objects are left alone so dynamic binds still round-trip to
|
|
293
|
+
* Postgres (which remains the authority for those).
|
|
294
|
+
*/
|
|
295
|
+
export function assertPatternArg(
|
|
296
|
+
castType: "lquery" | "ltxtquery" | "lquery[]",
|
|
297
|
+
arg: unknown,
|
|
298
|
+
): void {
|
|
299
|
+
if (arg !== null && typeof arg === "object" && !Array.isArray(arg)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
switch (castType) {
|
|
303
|
+
case "lquery":
|
|
304
|
+
assertValidLquery(arg);
|
|
305
|
+
return;
|
|
306
|
+
case "ltxtquery":
|
|
307
|
+
assertValidLtxtquery(arg);
|
|
308
|
+
return;
|
|
309
|
+
case "lquery[]":
|
|
310
|
+
assertValidLqueryArray(arg);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|