@nicia-ai/typegraph 0.20.0 → 0.22.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/{ast-Bh2NDeaK.d.cts → ast-CSLbPIOC.d.ts} +56 -3
- package/dist/{ast-COMyNeMn.d.ts → ast-DogwbBqA.d.cts} +56 -3
- package/dist/backend/postgres/index.cjs +123 -45
- package/dist/backend/postgres/index.cjs.map +1 -1
- package/dist/backend/postgres/index.d.cts +15 -7
- package/dist/backend/postgres/index.d.ts +15 -7
- package/dist/backend/postgres/index.js +101 -23
- package/dist/backend/postgres/index.js.map +1 -1
- package/dist/backend/sqlite/index.cjs +15 -15
- package/dist/backend/sqlite/index.d.cts +22 -7
- package/dist/backend/sqlite/index.d.ts +22 -7
- package/dist/backend/sqlite/index.js +5 -5
- package/dist/backend/sqlite/libsql.cjs +8 -8
- package/dist/backend/sqlite/libsql.d.cts +4 -5
- package/dist/backend/sqlite/libsql.d.ts +4 -5
- package/dist/backend/sqlite/libsql.js +5 -5
- package/dist/backend/sqlite/local.cjs +27 -10
- package/dist/backend/sqlite/local.cjs.map +1 -1
- package/dist/backend/sqlite/local.d.cts +4 -5
- package/dist/backend/sqlite/local.d.ts +4 -5
- package/dist/backend/sqlite/local.js +22 -6
- package/dist/backend/sqlite/local.js.map +1 -1
- package/dist/{chunk-MME3H4ZF.cjs → chunk-3JK66E3I.cjs} +387 -9
- package/dist/chunk-3JK66E3I.cjs.map +1 -0
- package/dist/{chunk-6GWJH6AR.js → chunk-3PPEHLGK.js} +4 -4
- package/dist/{chunk-6GWJH6AR.js.map → chunk-3PPEHLGK.js.map} +1 -1
- package/dist/{chunk-QFZQGEQ3.js → chunk-5ZMMHUWR.js} +199 -38
- package/dist/chunk-5ZMMHUWR.js.map +1 -0
- package/dist/{chunk-GNIYZKBI.js → chunk-753F33CV.js} +378 -8
- package/dist/chunk-753F33CV.js.map +1 -0
- package/dist/{chunk-K7SQ3SWP.js → chunk-EYELZHKY.js} +120 -12
- package/dist/chunk-EYELZHKY.js.map +1 -0
- package/dist/{chunk-HXJXTOXR.cjs → chunk-GUYI6VAS.cjs} +101 -21
- package/dist/chunk-GUYI6VAS.cjs.map +1 -0
- package/dist/{chunk-X33WV2UF.cjs → chunk-HRNQ3DVJ.cjs} +24 -21
- package/dist/chunk-HRNQ3DVJ.cjs.map +1 -0
- package/dist/{chunk-W6M7XWKX.js → chunk-JYUQA77I.js} +21 -18
- package/dist/chunk-JYUQA77I.js.map +1 -0
- package/dist/{chunk-LLHFS53Q.js → chunk-NSLTDXVH.js} +91 -11
- package/dist/chunk-NSLTDXVH.js.map +1 -0
- package/dist/{chunk-LELLOHJK.cjs → chunk-PEE4EWXZ.cjs} +35 -35
- package/dist/{chunk-LELLOHJK.cjs.map → chunk-PEE4EWXZ.cjs.map} +1 -1
- package/dist/{chunk-G5CYWKRF.js → chunk-RS5OLGEW.js} +2 -2
- package/dist/{chunk-G5CYWKRF.js.map → chunk-RS5OLGEW.js.map} +1 -1
- package/dist/{chunk-IYWWXOVK.cjs → chunk-SCXOGKLD.cjs} +252 -91
- package/dist/chunk-SCXOGKLD.cjs.map +1 -0
- package/dist/{chunk-P5CNM325.cjs → chunk-UVZHTBZ5.cjs} +123 -11
- package/dist/chunk-UVZHTBZ5.cjs.map +1 -0
- package/dist/{chunk-5GZMPJOP.cjs → chunk-WRWVWKQL.cjs} +2 -2
- package/dist/{chunk-5GZMPJOP.cjs.map → chunk-WRWVWKQL.cjs.map} +1 -1
- package/dist/{ddl-BBo2EcFc.d.cts → ddl-BqkTShcR.d.ts} +28 -6
- package/dist/{ddl-D4jlcOH_.d.ts → ddl-Dk1NDN3M.d.cts} +28 -6
- package/dist/index.cjs +1809 -513
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -11
- package/dist/index.d.ts +53 -11
- package/dist/index.js +1668 -398
- package/dist/index.js.map +1 -1
- package/dist/indexes/index.cjs +17 -17
- package/dist/indexes/index.d.cts +5 -5
- package/dist/indexes/index.d.ts +5 -5
- package/dist/indexes/index.js +3 -3
- package/dist/interchange/index.cjs +6 -6
- package/dist/interchange/index.d.cts +4 -5
- package/dist/interchange/index.d.ts +4 -5
- package/dist/interchange/index.js +1 -1
- package/dist/{manager-Chhrq1vl.d.ts → manager-0jVZtiE8.d.ts} +1 -2
- package/dist/{manager-Bj1UEnhE.d.cts → manager-BPK-2PUI.d.cts} +1 -2
- package/dist/profiler/index.cjs +15 -8
- package/dist/profiler/index.cjs.map +1 -1
- package/dist/profiler/index.d.cts +7 -8
- package/dist/profiler/index.d.ts +7 -8
- package/dist/profiler/index.js +8 -1
- package/dist/profiler/index.js.map +1 -1
- package/dist/schema/index.cjs +21 -21
- package/dist/schema/index.d.cts +4 -5
- package/dist/schema/index.d.ts +4 -5
- package/dist/schema/index.js +3 -3
- package/dist/{sqlite-DE-6NWtC.d.cts → sqlite-D1rrwcir.d.cts} +14 -1
- package/dist/{sqlite-Blq-AhmK.d.ts → sqlite-W3rmJMlc.d.ts} +14 -1
- package/dist/{store-BMrRi_mY.d.ts → store-2QW2SIRk.d.cts} +404 -5
- package/dist/{store-DRRvALu8.d.cts → store-C0q13mtJ.d.ts} +404 -5
- package/dist/{types-B3mmOMJV.d.cts → types-CQF88McG.d.cts} +475 -2
- package/dist/{types-ThB4cFLp.d.ts → types-CQF88McG.d.ts} +475 -2
- package/dist/{types-COPePE8_.d.ts → types-CglvhPKz.d.ts} +1 -1
- package/dist/{types-CVtGFpB9.d.cts → types-D23slCOA.d.cts} +1 -1
- package/dist/{types-Ceb01czq.d.ts → types-IMQCuser.d.ts} +2 -2
- package/dist/{types-CZd2PEOc.d.cts → types-vgjar7Ch.d.cts} +2 -2
- package/package.json +9 -9
- package/dist/chunk-GNIYZKBI.js.map +0 -1
- package/dist/chunk-HXJXTOXR.cjs.map +0 -1
- package/dist/chunk-IYWWXOVK.cjs.map +0 -1
- package/dist/chunk-K7SQ3SWP.js.map +0 -1
- package/dist/chunk-LLHFS53Q.js.map +0 -1
- package/dist/chunk-MME3H4ZF.cjs.map +0 -1
- package/dist/chunk-P5CNM325.cjs.map +0 -1
- package/dist/chunk-QFZQGEQ3.js.map +0 -1
- package/dist/chunk-W6M7XWKX.js.map +0 -1
- package/dist/chunk-X33WV2UF.cjs.map +0 -1
- package/dist/types-DMzKq0d5.d.cts +0 -181
- package/dist/types-DMzKq0d5.d.ts +0 -181
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { META_EDGE_BRAND, META_EDGE_SUB_CLASS_OF, META_EDGE_NARROWER, META_EDGE_BROADER, META_EDGE_RELATED_TO, META_EDGE_EQUIVALENT_TO, META_EDGE_SAME_AS, META_EDGE_DIFFERENT_FROM, META_EDGE_DISJOINT_WITH, META_EDGE_HAS_PART, META_EDGE_PART_OF, META_EDGE_INVERSE_OF, META_EDGE_IMPLIES, ensureSchema, computeClosuresFromOntology, createEmptyClosures, KindRegistry, validateNodeProps, validateEdgeProps } from './chunk-
|
|
2
|
-
export { isMetaEdge } from './chunk-
|
|
3
|
-
import { NODE_TYPE_BRAND, EDGE_TYPE_BRAND, nowIso, validateOptionalIsoDate, getNodeKinds, getEdgeKinds } from './chunk-
|
|
4
|
-
export { defineGraph, getEdgeKinds, getNodeKinds, isEdgeType, isEdgeTypeWithEndpoints, isGraphDef, isNodeType } from './chunk-
|
|
1
|
+
import { META_EDGE_BRAND, META_EDGE_SUB_CLASS_OF, META_EDGE_NARROWER, META_EDGE_BROADER, META_EDGE_RELATED_TO, META_EDGE_EQUIVALENT_TO, META_EDGE_SAME_AS, META_EDGE_DIFFERENT_FROM, META_EDGE_DISJOINT_WITH, META_EDGE_HAS_PART, META_EDGE_PART_OF, META_EDGE_INVERSE_OF, META_EDGE_IMPLIES, ensureSchema, computeClosuresFromOntology, createEmptyClosures, KindRegistry, validateNodeProps, validateEdgeProps } from './chunk-JYUQA77I.js';
|
|
2
|
+
export { isMetaEdge } from './chunk-JYUQA77I.js';
|
|
3
|
+
import { NODE_TYPE_BRAND, EDGE_TYPE_BRAND, nowIso, validateOptionalIsoDate, getNodeKinds, getEdgeKinds } from './chunk-RS5OLGEW.js';
|
|
4
|
+
export { defineGraph, getEdgeKinds, getNodeKinds, isEdgeType, isEdgeTypeWithEndpoints, isGraphDef, isNodeType } from './chunk-RS5OLGEW.js';
|
|
5
5
|
import { ConfigurationError, UnsupportedPredicateError, ValidationError, CompilerInvariantError, KindNotFoundError, DatabaseOperationError, EdgeNotFoundError, NodeConstraintNotFoundError, RestrictedDeleteError, NodeNotFoundError, EndpointNotFoundError, UniquenessError, CardinalityError, EndpointError, DisjointError } from './chunk-WFMTAEEN.js';
|
|
6
6
|
export { BackendDisposedError, CardinalityError, CompilerInvariantError, ConfigurationError, DatabaseOperationError, DisjointError, EdgeNotFoundError, EndpointError, EndpointNotFoundError, KindNotFoundError, MigrationError, NodeConstraintNotFoundError, NodeNotFoundError, RestrictedDeleteError, SchemaMismatchError, TypeGraphError, UniquenessError, UnsupportedPredicateError, ValidationError, VersionConflictError, getErrorSuggestion, isConstraintError, isSystemError, isTypeGraphError, isUserRecoverable } from './chunk-WFMTAEEN.js';
|
|
7
|
-
import { MAX_PG_IDENTIFIER_LENGTH, DEFAULT_PAGINATION_LIMIT, getDialect, DEFAULT_STREAM_BATCH_SIZE, NODE_META_KEYS, EDGE_META_KEYS, SQLITE_PATH_DELIMITER, PG_ARRAY_START, PG_ARRAY_END, PG_PATH_ELEMENT_SEPARATOR, fnv1aBase36 } from './chunk-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { MAX_PG_IDENTIFIER_LENGTH, DEFAULT_PAGINATION_LIMIT, getDialect, DEFAULT_STREAM_BATCH_SIZE, NODE_META_KEYS, EDGE_META_KEYS, SQLITE_PATH_DELIMITER, PG_ARRAY_START, PG_ARRAY_END, PG_PATH_ELEMENT_SEPARATOR, fnv1aBase36 } from './chunk-753F33CV.js';
|
|
8
|
+
export { fts5Strategy, tsvectorStrategy } from './chunk-753F33CV.js';
|
|
9
|
+
import { jsonPointer, parseJsonPointer, createSchemaIntrospector, normalizeJsonPointer, joinJsonPointers, resolveFieldTypeInfoAtJsonPointer, getSearchableMetadata, DEFAULT_SEARCHABLE_LANGUAGE, getEmbeddingDimensions, isEmbeddingSchema } from './chunk-EYELZHKY.js';
|
|
10
|
+
export { DEFAULT_SEARCHABLE_LANGUAGE, MAX_JSON_POINTER_DEPTH, embedding, getEmbeddingDimensions, getSearchableMetadata, isEmbeddingSchema, isSearchableSchema, joinJsonPointers, jsonPointer, normalizeJsonPointer, parseJsonPointer, searchable } from './chunk-EYELZHKY.js';
|
|
10
11
|
import { z } from 'zod';
|
|
11
12
|
import { sql, Placeholder } from 'drizzle-orm';
|
|
12
13
|
import { nanoid } from 'nanoid';
|
|
@@ -31,6 +32,44 @@ var PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
|
|
|
31
32
|
"constructor",
|
|
32
33
|
"prototype"
|
|
33
34
|
]);
|
|
35
|
+
var RESERVED_PROPERTY_PREFIX = "$";
|
|
36
|
+
function isReservedPropertyName(name) {
|
|
37
|
+
return name.startsWith(RESERVED_PROPERTY_PREFIX);
|
|
38
|
+
}
|
|
39
|
+
function assertSchemaKeysAreFree(entityKind, name, keys, reservedStructuralKeys) {
|
|
40
|
+
const structuralConflicts = keys.filter(
|
|
41
|
+
(key) => reservedStructuralKeys.has(key)
|
|
42
|
+
);
|
|
43
|
+
if (structuralConflicts.length > 0) {
|
|
44
|
+
const label = entityKind.toLowerCase();
|
|
45
|
+
throw new ConfigurationError(
|
|
46
|
+
`${entityKind} "${name}" schema contains reserved property names: ${structuralConflicts.join(", ")}`,
|
|
47
|
+
{
|
|
48
|
+
[`${label}Type`]: name,
|
|
49
|
+
conflicts: structuralConflicts,
|
|
50
|
+
reservedKeys: [...reservedStructuralKeys]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
suggestion: `Rename the conflicting properties. Reserved names (${[...reservedStructuralKeys].join(", ")}) are added automatically to all ${label}s.`
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
const prefixConflicts = keys.filter((key) => isReservedPropertyName(key));
|
|
58
|
+
if (prefixConflicts.length > 0) {
|
|
59
|
+
const label = entityKind.toLowerCase();
|
|
60
|
+
throw new ConfigurationError(
|
|
61
|
+
`${entityKind} "${name}" schema uses the reserved "${RESERVED_PROPERTY_PREFIX}" prefix on: ${prefixConflicts.join(", ")}`,
|
|
62
|
+
{
|
|
63
|
+
[`${label}Type`]: name,
|
|
64
|
+
conflicts: prefixConflicts,
|
|
65
|
+
reservedPrefix: RESERVED_PROPERTY_PREFIX
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
suggestion: `Property names starting with "${RESERVED_PROPERTY_PREFIX}" are reserved for TypeGraph accessors (e.g. $fulltext). Rename each field.`
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
34
73
|
function validateProjectionField(field2, entityType, kind) {
|
|
35
74
|
const reserved = entityType === "node" ? RESERVED_NODE_KEYS : RESERVED_EDGE_KEYS;
|
|
36
75
|
if (reserved.has(field2)) {
|
|
@@ -64,19 +103,12 @@ function filterReservedKeys(props, reservedKeys) {
|
|
|
64
103
|
|
|
65
104
|
// src/core/node.ts
|
|
66
105
|
function validateSchemaKeys(schema, name) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
106
|
+
assertSchemaKeysAreFree(
|
|
107
|
+
"Node",
|
|
108
|
+
name,
|
|
109
|
+
Object.keys(schema.shape),
|
|
110
|
+
RESERVED_NODE_KEYS
|
|
70
111
|
);
|
|
71
|
-
if (conflicts.length > 0) {
|
|
72
|
-
throw new ConfigurationError(
|
|
73
|
-
`Node "${name}" schema contains reserved property names: ${conflicts.join(", ")}`,
|
|
74
|
-
{ nodeType: name, conflicts, reservedKeys: [...RESERVED_NODE_KEYS] },
|
|
75
|
-
{
|
|
76
|
-
suggestion: `Rename the conflicting properties. Reserved names (id, kind, meta) are added automatically to all nodes.`
|
|
77
|
-
}
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
112
|
}
|
|
81
113
|
function defineNode(name, options) {
|
|
82
114
|
validateSchemaKeys(options.schema, name);
|
|
@@ -89,19 +121,12 @@ function defineNode(name, options) {
|
|
|
89
121
|
}
|
|
90
122
|
var EMPTY_SCHEMA = z.object({});
|
|
91
123
|
function validateSchemaKeys2(schema, name) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
124
|
+
assertSchemaKeysAreFree(
|
|
125
|
+
"Edge",
|
|
126
|
+
name,
|
|
127
|
+
Object.keys(schema.shape),
|
|
128
|
+
RESERVED_EDGE_KEYS
|
|
95
129
|
);
|
|
96
|
-
if (conflicts.length > 0) {
|
|
97
|
-
throw new ConfigurationError(
|
|
98
|
-
`Edge "${name}" schema contains reserved property names: ${conflicts.join(", ")}`,
|
|
99
|
-
{ edgeType: name, conflicts, reservedKeys: [...RESERVED_EDGE_KEYS] },
|
|
100
|
-
{
|
|
101
|
-
suggestion: `Rename the conflicting properties. Reserved names are added automatically to all edges.`
|
|
102
|
-
}
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
130
|
}
|
|
106
131
|
function defineEdge(name, options) {
|
|
107
132
|
const schema = options?.schema ?? EMPTY_SCHEMA;
|
|
@@ -567,6 +592,41 @@ function stringField(field2) {
|
|
|
567
592
|
ilike: (pattern) => stringOp("ilike", field2, pattern)
|
|
568
593
|
};
|
|
569
594
|
}
|
|
595
|
+
var DEFAULT_FULLTEXT_MATCH_K = 50;
|
|
596
|
+
function createFulltextAccessor(alias, hasSearchableField) {
|
|
597
|
+
return {
|
|
598
|
+
matches: (query, k, options) => {
|
|
599
|
+
if (!hasSearchableField()) {
|
|
600
|
+
throw new UnsupportedPredicateError(
|
|
601
|
+
`Cannot call .$fulltext.matches() on alias "${alias}" \u2014 its node kind has no fields declared with searchable(). Add at least one: \`title: searchable({ language: "english" })\`.`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
return fulltextMatch(
|
|
605
|
+
fieldRef(alias, ["$fulltext"]),
|
|
606
|
+
query,
|
|
607
|
+
k ?? DEFAULT_FULLTEXT_MATCH_K,
|
|
608
|
+
options
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function fulltextMatch(field2, query, limit, options) {
|
|
614
|
+
if (!Number.isFinite(limit) || limit <= 0) {
|
|
615
|
+
throw new UnsupportedPredicateError(
|
|
616
|
+
`matches() limit must be a positive finite number, got ${String(limit)}`
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
const expr = {
|
|
620
|
+
__type: "fulltext_match",
|
|
621
|
+
field: field2,
|
|
622
|
+
query,
|
|
623
|
+
mode: options?.mode ?? "websearch",
|
|
624
|
+
limit,
|
|
625
|
+
...options?.language === void 0 ? {} : { language: options.language },
|
|
626
|
+
...options?.minScore === void 0 ? {} : { minScore: options.minScore }
|
|
627
|
+
};
|
|
628
|
+
return predicate(expr);
|
|
629
|
+
}
|
|
570
630
|
function numberField(field2) {
|
|
571
631
|
return {
|
|
572
632
|
...baseFieldBuilder(field2),
|
|
@@ -1142,11 +1202,13 @@ function compilePredicateExpression(expr, ctx) {
|
|
|
1142
1202
|
case "in_subquery": {
|
|
1143
1203
|
return compileInSubquery(expr, ctx);
|
|
1144
1204
|
}
|
|
1145
|
-
case "vector_similarity":
|
|
1146
|
-
|
|
1205
|
+
case "vector_similarity":
|
|
1206
|
+
case "fulltext_match": {
|
|
1207
|
+
return STRUCTURAL_PREDICATE_NOOP;
|
|
1147
1208
|
}
|
|
1148
1209
|
}
|
|
1149
1210
|
}
|
|
1211
|
+
var STRUCTURAL_PREDICATE_NOOP = sql.raw("1=1");
|
|
1150
1212
|
function compileComparisonPredicate(expr, dialect, cteColumnPrefix) {
|
|
1151
1213
|
if (isParameterRef(expr.right)) {
|
|
1152
1214
|
const parameterValueType = normalizeValueType2(expr.right.valueType) ?? normalizeValueType2(expr.left.valueType);
|
|
@@ -1400,45 +1462,61 @@ function compileInSubquery(expr, ctx) {
|
|
|
1400
1462
|
const subSql = ctx.compileQuery(expr.subquery, expr.subquery.graphId);
|
|
1401
1463
|
return expr.negated ? sql`${fieldSql} NOT IN (${subSql})` : sql`${fieldSql} IN (${subSql})`;
|
|
1402
1464
|
}
|
|
1403
|
-
function
|
|
1404
|
-
return sql.raw("1=1");
|
|
1405
|
-
}
|
|
1406
|
-
function extractVectorSimilarityPredicates(predicates) {
|
|
1465
|
+
function extractStructuralPredicates(predicates, isMatch, rejectionMessage) {
|
|
1407
1466
|
const results = [];
|
|
1408
1467
|
function visit(expr, inDisallowedBranch) {
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
throw new UnsupportedPredicateError(
|
|
1413
|
-
"Vector similarity predicates cannot be nested under OR or NOT. Use top-level AND combinations instead."
|
|
1414
|
-
);
|
|
1415
|
-
}
|
|
1416
|
-
results.push(expr);
|
|
1417
|
-
break;
|
|
1468
|
+
if (isMatch(expr)) {
|
|
1469
|
+
if (inDisallowedBranch) {
|
|
1470
|
+
throw new UnsupportedPredicateError(rejectionMessage);
|
|
1418
1471
|
}
|
|
1472
|
+
results.push(expr);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
switch (expr.__type) {
|
|
1419
1476
|
case "and": {
|
|
1420
|
-
for (const
|
|
1421
|
-
|
|
1422
|
-
}
|
|
1423
|
-
break;
|
|
1477
|
+
for (const child of expr.predicates) visit(child, inDisallowedBranch);
|
|
1478
|
+
return;
|
|
1424
1479
|
}
|
|
1425
1480
|
case "or": {
|
|
1426
|
-
for (const
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1429
|
-
break;
|
|
1481
|
+
for (const child of expr.predicates) visit(child, true);
|
|
1482
|
+
return;
|
|
1430
1483
|
}
|
|
1431
1484
|
case "not": {
|
|
1432
1485
|
visit(expr.predicate, true);
|
|
1433
|
-
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
case "comparison":
|
|
1489
|
+
case "string_op":
|
|
1490
|
+
case "null_check":
|
|
1491
|
+
case "between":
|
|
1492
|
+
case "array_op":
|
|
1493
|
+
case "object_op":
|
|
1494
|
+
case "aggregate_comparison":
|
|
1495
|
+
case "exists":
|
|
1496
|
+
case "in_subquery":
|
|
1497
|
+
case "vector_similarity":
|
|
1498
|
+
case "fulltext_match": {
|
|
1499
|
+
return;
|
|
1434
1500
|
}
|
|
1435
1501
|
}
|
|
1436
1502
|
}
|
|
1437
|
-
for (const pred of predicates)
|
|
1438
|
-
visit(pred.expression, false);
|
|
1439
|
-
}
|
|
1503
|
+
for (const pred of predicates) visit(pred.expression, false);
|
|
1440
1504
|
return results;
|
|
1441
1505
|
}
|
|
1506
|
+
function extractVectorSimilarityPredicates(predicates) {
|
|
1507
|
+
return extractStructuralPredicates(
|
|
1508
|
+
predicates,
|
|
1509
|
+
(expr) => expr.__type === "vector_similarity",
|
|
1510
|
+
"Vector similarity predicates cannot be nested under OR or NOT. Use top-level AND combinations instead."
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
function extractFulltextMatchPredicates(predicates) {
|
|
1514
|
+
return extractStructuralPredicates(
|
|
1515
|
+
predicates,
|
|
1516
|
+
(expr) => expr.__type === "fulltext_match",
|
|
1517
|
+
"Fulltext match predicates cannot be nested under OR or NOT. Use top-level AND combinations instead."
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1442
1520
|
|
|
1443
1521
|
// src/query/compiler/predicate-utils.ts
|
|
1444
1522
|
var EMPTY_PREDICATES = [];
|
|
@@ -1497,6 +1575,10 @@ function getNodeKindsForAlias(ast, alias) {
|
|
|
1497
1575
|
}
|
|
1498
1576
|
throw new CompilerInvariantError(`Unknown traversal source alias: ${alias}`);
|
|
1499
1577
|
}
|
|
1578
|
+
function getHybridTargetAlias(vectorPredicate, fulltextPredicate) {
|
|
1579
|
+
if (!vectorPredicate || !fulltextPredicate) return void 0;
|
|
1580
|
+
return vectorPredicate.field.alias === fulltextPredicate.field.alias ? vectorPredicate.field.alias : void 0;
|
|
1581
|
+
}
|
|
1500
1582
|
|
|
1501
1583
|
// src/query/compiler/emitter/plan-inspector.ts
|
|
1502
1584
|
function collectPlanOperations(node, ops) {
|
|
@@ -1504,6 +1586,7 @@ function collectPlanOperations(node, ops) {
|
|
|
1504
1586
|
switch (node.op) {
|
|
1505
1587
|
case "aggregate":
|
|
1506
1588
|
case "filter":
|
|
1589
|
+
case "fulltext_match":
|
|
1507
1590
|
case "join":
|
|
1508
1591
|
case "limit_offset":
|
|
1509
1592
|
case "project":
|
|
@@ -1532,6 +1615,7 @@ function findUnaryNodeInProjectChain(rootNode, op) {
|
|
|
1532
1615
|
switch (currentNode.op) {
|
|
1533
1616
|
case "aggregate":
|
|
1534
1617
|
case "filter":
|
|
1618
|
+
case "fulltext_match":
|
|
1535
1619
|
case "join":
|
|
1536
1620
|
case "limit_offset":
|
|
1537
1621
|
case "recursive_expand":
|
|
@@ -1567,6 +1651,7 @@ function inspectProjectPlan(logicalPlan) {
|
|
|
1567
1651
|
);
|
|
1568
1652
|
return {
|
|
1569
1653
|
hasAggregate: operations.has("aggregate"),
|
|
1654
|
+
hasFulltextMatch: operations.has("fulltext_match"),
|
|
1570
1655
|
hasLimitOffset: operations.has("limit_offset"),
|
|
1571
1656
|
hasRecursiveExpand: operations.has("recursive_expand"),
|
|
1572
1657
|
hasSetOperation: operations.has("set_op"),
|
|
@@ -1618,6 +1703,7 @@ function findTopLevelLimitOffsetNode(rootNode) {
|
|
|
1618
1703
|
switch (currentNode.op) {
|
|
1619
1704
|
case "aggregate":
|
|
1620
1705
|
case "filter":
|
|
1706
|
+
case "fulltext_match":
|
|
1621
1707
|
case "join":
|
|
1622
1708
|
case "project":
|
|
1623
1709
|
case "recursive_expand":
|
|
@@ -1642,6 +1728,7 @@ function findTopLevelSortNode(rootNode) {
|
|
|
1642
1728
|
switch (currentNode.op) {
|
|
1643
1729
|
case "aggregate":
|
|
1644
1730
|
case "filter":
|
|
1731
|
+
case "fulltext_match":
|
|
1645
1732
|
case "join":
|
|
1646
1733
|
case "limit_offset":
|
|
1647
1734
|
case "project":
|
|
@@ -1776,16 +1863,16 @@ function assertStandardEmitterClauseAlignment(logicalPlan, input) {
|
|
|
1776
1863
|
{ component: "standard-emitter" }
|
|
1777
1864
|
);
|
|
1778
1865
|
}
|
|
1779
|
-
const expectsOrderBy = planShape.hasSort || planShape.hasVectorKnn;
|
|
1866
|
+
const expectsOrderBy = planShape.hasSort || planShape.hasVectorKnn || planShape.hasFulltextMatch;
|
|
1780
1867
|
if (expectsOrderBy && input.orderBy === void 0) {
|
|
1781
1868
|
throw new CompilerInvariantError(
|
|
1782
|
-
"Standard SQL emitter expected ORDER BY clause for plan containing a sort or
|
|
1869
|
+
"Standard SQL emitter expected ORDER BY clause for plan containing a sort, vector_knn, or fulltext_match node",
|
|
1783
1870
|
{ component: "standard-emitter" }
|
|
1784
1871
|
);
|
|
1785
1872
|
}
|
|
1786
1873
|
if (!expectsOrderBy && input.orderBy !== void 0) {
|
|
1787
1874
|
throw new CompilerInvariantError(
|
|
1788
|
-
"Standard SQL emitter received ORDER BY clause for a plan without sort or
|
|
1875
|
+
"Standard SQL emitter received ORDER BY clause for a plan without sort, vector_knn, or fulltext_match nodes",
|
|
1789
1876
|
{ component: "standard-emitter" }
|
|
1790
1877
|
);
|
|
1791
1878
|
}
|
|
@@ -1824,6 +1911,62 @@ function emitStandardQuerySql(input) {
|
|
|
1824
1911
|
return sql.join(parts, sql` `);
|
|
1825
1912
|
}
|
|
1826
1913
|
|
|
1914
|
+
// src/query/ast.ts
|
|
1915
|
+
var DEFAULT_RRF_K = 60;
|
|
1916
|
+
var DEFAULT_RRF_WEIGHT = 1;
|
|
1917
|
+
function mergeEdgeKinds(traversal) {
|
|
1918
|
+
const inverse = traversal.inverseEdgeKinds;
|
|
1919
|
+
if (inverse === void 0 || inverse.length === 0) return traversal.edgeKinds;
|
|
1920
|
+
return [
|
|
1921
|
+
...traversal.edgeKinds,
|
|
1922
|
+
...inverse.filter((kind) => !traversal.edgeKinds.includes(kind))
|
|
1923
|
+
];
|
|
1924
|
+
}
|
|
1925
|
+
var DEFAULT_TABLE_NAMES = {
|
|
1926
|
+
nodes: "typegraph_nodes",
|
|
1927
|
+
edges: "typegraph_edges",
|
|
1928
|
+
embeddings: "typegraph_node_embeddings",
|
|
1929
|
+
fulltext: "typegraph_node_fulltext"
|
|
1930
|
+
};
|
|
1931
|
+
var VALID_IDENTIFIER_PATTERN = /^[a-z_][a-z0-9_$]*$/i;
|
|
1932
|
+
function validateTableName(name, label) {
|
|
1933
|
+
if (!name || name.length === 0) {
|
|
1934
|
+
throw new ConfigurationError(`${label} table name cannot be empty`);
|
|
1935
|
+
}
|
|
1936
|
+
if (name.length > MAX_PG_IDENTIFIER_LENGTH) {
|
|
1937
|
+
throw new ConfigurationError(
|
|
1938
|
+
`${label} table name exceeds maximum length of ${MAX_PG_IDENTIFIER_LENGTH} characters`
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
if (!VALID_IDENTIFIER_PATTERN.test(name)) {
|
|
1942
|
+
throw new ConfigurationError(
|
|
1943
|
+
`${label} table name "${name}" is not a valid SQL identifier. Table names must start with a letter or underscore and contain only letters, digits, underscores, or dollar signs.`
|
|
1944
|
+
);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
function quoteIdentifier(name) {
|
|
1948
|
+
return `"${name.replaceAll('"', '""')}"`;
|
|
1949
|
+
}
|
|
1950
|
+
function createSqlSchema(names = {}) {
|
|
1951
|
+
const tables = { ...DEFAULT_TABLE_NAMES, ...names };
|
|
1952
|
+
validateTableName(tables.nodes, "nodes");
|
|
1953
|
+
validateTableName(tables.edges, "edges");
|
|
1954
|
+
validateTableName(tables.embeddings, "embeddings");
|
|
1955
|
+
validateTableName(tables.fulltext, "fulltext");
|
|
1956
|
+
return {
|
|
1957
|
+
tables,
|
|
1958
|
+
nodesTable: sql.raw(quoteIdentifier(tables.nodes)),
|
|
1959
|
+
edgesTable: sql.raw(quoteIdentifier(tables.edges)),
|
|
1960
|
+
embeddingsTable: sql.raw(quoteIdentifier(tables.embeddings)),
|
|
1961
|
+
fulltextTable: sql.raw(quoteIdentifier(tables.fulltext))
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
var DEFAULT_SQL_SCHEMA = createSqlSchema();
|
|
1965
|
+
var ALIAS_CTE_PREFIX = "cte_";
|
|
1966
|
+
var EMBEDDINGS_CTE_ALIAS = "cte_embeddings";
|
|
1967
|
+
var FULLTEXT_CTE_ALIAS = "cte_fulltext";
|
|
1968
|
+
var HYBRID_CANDIDATES_CTE_ALIAS = "cte_relevance_candidates";
|
|
1969
|
+
|
|
1827
1970
|
// src/query/compiler/typed-json-extract.ts
|
|
1828
1971
|
function compileTypedJsonExtract(input) {
|
|
1829
1972
|
const { column, dialect, pointer, valueType } = input;
|
|
@@ -1877,7 +2020,7 @@ var EDGE_COLUMNS = [
|
|
|
1877
2020
|
"deleted_at"
|
|
1878
2021
|
];
|
|
1879
2022
|
var EMPTY_REQUIRED_COLUMNS = /* @__PURE__ */ new Set();
|
|
1880
|
-
function
|
|
2023
|
+
function quoteIdentifier2(identifier) {
|
|
1881
2024
|
return sql.raw(`"${identifier.replaceAll('"', '""')}"`);
|
|
1882
2025
|
}
|
|
1883
2026
|
function shouldProjectColumn(requiredColumns, column, alwaysRequiredColumns) {
|
|
@@ -1931,6 +2074,21 @@ function compileColumnReference(tableAlias, column) {
|
|
|
1931
2074
|
}
|
|
1932
2075
|
return sql`${sql.raw(tableAlias)}.${sql.raw(column)}`;
|
|
1933
2076
|
}
|
|
2077
|
+
function qualifyColumn(owner, name) {
|
|
2078
|
+
return sql`${quoteIdentifier2(owner)}.${quoteIdentifier2(name)}`;
|
|
2079
|
+
}
|
|
2080
|
+
var SCOPED_RELEVANCE_NODES_ALIAS = "scoped_relevance_nodes";
|
|
2081
|
+
function buildScopedNodeIdsSubquery(nodeAlias) {
|
|
2082
|
+
const cteAlias = `${ALIAS_CTE_PREFIX}${nodeAlias}`;
|
|
2083
|
+
return sql`
|
|
2084
|
+
(
|
|
2085
|
+
SELECT DISTINCT
|
|
2086
|
+
${qualifyColumn(cteAlias, `${nodeAlias}_id`)} AS node_id,
|
|
2087
|
+
${qualifyColumn(cteAlias, `${nodeAlias}_kind`)} AS node_kind
|
|
2088
|
+
FROM ${quoteIdentifier2(cteAlias)}
|
|
2089
|
+
) AS ${sql.raw(SCOPED_RELEVANCE_NODES_ALIAS)}
|
|
2090
|
+
`;
|
|
2091
|
+
}
|
|
1934
2092
|
function compileNodeSelectColumns(tableAlias, alias, requiredColumns) {
|
|
1935
2093
|
return NODE_COLUMNS.filter(
|
|
1936
2094
|
(column) => column === "id" || column === "kind" || shouldProjectColumn(requiredColumns, column)
|
|
@@ -1949,6 +2107,7 @@ function buildStandardStartCte(input) {
|
|
|
1949
2107
|
const { ast, ctx, graphId, predicateIndex, requiredColumnsByAlias } = input;
|
|
1950
2108
|
const alias = ast.start.alias;
|
|
1951
2109
|
const kinds = ast.start.kinds;
|
|
2110
|
+
const cteMaterialization = ctx.dialect.capabilities.emitNotMaterializedHint ? sql`NOT MATERIALIZED ` : sql``;
|
|
1952
2111
|
const kindFilter = compileKindFilter(sql.raw("kind"), kinds);
|
|
1953
2112
|
const temporalFilter = input.temporalFilterPass.forAlias();
|
|
1954
2113
|
const cteContext = { ...ctx, cteColumnPrefix: "" };
|
|
@@ -1963,14 +2122,19 @@ function buildStandardStartCte(input) {
|
|
|
1963
2122
|
...predicateClauses
|
|
1964
2123
|
];
|
|
1965
2124
|
const effectiveRequiredColumns = requiredColumnsByAlias ? requiredColumnsByAlias.get(alias) ?? EMPTY_REQUIRED_COLUMNS : void 0;
|
|
2125
|
+
const limitClause = input.limitOffset === void 0 ? sql`` : input.limitOffset.offset === void 0 ? sql`
|
|
2126
|
+
LIMIT ${input.limitOffset.limit}
|
|
2127
|
+
` : sql`
|
|
2128
|
+
LIMIT ${input.limitOffset.limit} OFFSET ${input.limitOffset.offset}
|
|
2129
|
+
`;
|
|
1966
2130
|
return sql`
|
|
1967
|
-
cte_${sql.raw(alias)} AS (
|
|
2131
|
+
cte_${sql.raw(alias)} AS ${cteMaterialization}(
|
|
1968
2132
|
SELECT ${sql.join(
|
|
1969
2133
|
compileNodeSelectColumns(void 0, alias, effectiveRequiredColumns),
|
|
1970
2134
|
sql`, `
|
|
1971
2135
|
)}
|
|
1972
2136
|
FROM ${ctx.schema.nodesTable}
|
|
1973
|
-
WHERE ${sql.join(whereClauses, sql` AND `)}
|
|
2137
|
+
WHERE ${sql.join(whereClauses, sql` AND `)}${limitClause}
|
|
1974
2138
|
)
|
|
1975
2139
|
`;
|
|
1976
2140
|
}
|
|
@@ -2034,7 +2198,7 @@ function buildStandardTraversalCte(input) {
|
|
|
2034
2198
|
...compileEdgeSelectColumns("e", edgeAlias, requiredEdgeColumns),
|
|
2035
2199
|
...compileNodeSelectColumns("n", nodeAlias, requiredNodeColumns)
|
|
2036
2200
|
];
|
|
2037
|
-
const cteMaterialization = materializeCte ? sql`MATERIALIZED ` : sql``;
|
|
2201
|
+
const cteMaterialization = materializeCte ? sql`MATERIALIZED ` : ctx.dialect.capabilities.emitNotMaterializedHint ? sql`NOT MATERIALIZED ` : sql``;
|
|
2038
2202
|
function compileTraversalBranch(branch) {
|
|
2039
2203
|
const whereClauses = [
|
|
2040
2204
|
...baseWhereClauses,
|
|
@@ -2183,7 +2347,7 @@ function buildStandardProjection(input) {
|
|
|
2183
2347
|
}
|
|
2184
2348
|
const projectedFields = fields.map((field2) => {
|
|
2185
2349
|
const source = compileProjectedSource(field2, dialect);
|
|
2186
|
-
return sql`${source} AS ${
|
|
2350
|
+
return sql`${source} AS ${quoteIdentifier2(field2.outputName)}`;
|
|
2187
2351
|
});
|
|
2188
2352
|
return sql.join(projectedFields, sql`, `);
|
|
2189
2353
|
}
|
|
@@ -2203,7 +2367,7 @@ function compileSelectiveProjection(fields, dialect, ast, collapsedTraversalCteA
|
|
|
2203
2367
|
const cteAlias = collapsedTraversalCteAlias ?? aliasToCte.get(field2.alias) ?? `cte_${field2.alias}`;
|
|
2204
2368
|
if (field2.isSystemField) {
|
|
2205
2369
|
const dbColumn = mapSelectiveSystemFieldToColumn(field2.field);
|
|
2206
|
-
return sql`${sql.raw(cteAlias)}.${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${
|
|
2370
|
+
return sql`${sql.raw(cteAlias)}.${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${quoteIdentifier2(field2.outputName)}`;
|
|
2207
2371
|
}
|
|
2208
2372
|
const propsColumn = `${field2.alias}_props`;
|
|
2209
2373
|
const column = sql`${sql.raw(cteAlias)}.${sql.raw(propsColumn)}`;
|
|
@@ -2214,12 +2378,56 @@ function compileSelectiveProjection(fields, dialect, ast, collapsedTraversalCteA
|
|
|
2214
2378
|
pointer,
|
|
2215
2379
|
valueType: field2.valueType
|
|
2216
2380
|
});
|
|
2217
|
-
return sql`${extracted} AS ${
|
|
2381
|
+
return sql`${extracted} AS ${quoteIdentifier2(field2.outputName)}`;
|
|
2218
2382
|
});
|
|
2219
2383
|
return sql.join(columns, sql`, `);
|
|
2220
2384
|
}
|
|
2385
|
+
function buildRelevanceJoins(vectorPredicate, fulltextPredicate) {
|
|
2386
|
+
const hybridTargetAlias = getHybridTargetAlias(
|
|
2387
|
+
vectorPredicate,
|
|
2388
|
+
fulltextPredicate
|
|
2389
|
+
);
|
|
2390
|
+
if (hybridTargetAlias !== void 0) {
|
|
2391
|
+
return [
|
|
2392
|
+
buildHybridCandidateJoin(hybridTargetAlias),
|
|
2393
|
+
buildRelevanceJoin(EMBEDDINGS_CTE_ALIAS, hybridTargetAlias, "LEFT JOIN"),
|
|
2394
|
+
buildRelevanceJoin(FULLTEXT_CTE_ALIAS, hybridTargetAlias, "LEFT JOIN")
|
|
2395
|
+
];
|
|
2396
|
+
}
|
|
2397
|
+
const joins = [];
|
|
2398
|
+
if (vectorPredicate) {
|
|
2399
|
+
joins.push(
|
|
2400
|
+
buildRelevanceJoin(EMBEDDINGS_CTE_ALIAS, vectorPredicate.field.alias)
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
if (fulltextPredicate) {
|
|
2404
|
+
joins.push(
|
|
2405
|
+
buildRelevanceJoin(FULLTEXT_CTE_ALIAS, fulltextPredicate.field.alias)
|
|
2406
|
+
);
|
|
2407
|
+
}
|
|
2408
|
+
return joins;
|
|
2409
|
+
}
|
|
2410
|
+
function buildHybridCandidateJoin(nodeAlias) {
|
|
2411
|
+
const candidateCte = sql.raw(HYBRID_CANDIDATES_CTE_ALIAS);
|
|
2412
|
+
const node = sql.raw(`${ALIAS_CTE_PREFIX}${nodeAlias}`);
|
|
2413
|
+
const idColumn = sql.raw(`${nodeAlias}_id`);
|
|
2414
|
+
const kindColumn = sql.raw(`${nodeAlias}_kind`);
|
|
2415
|
+
return sql`INNER JOIN ${candidateCte} ON ${candidateCte}.node_id = ${node}.${idColumn} AND ${candidateCte}.node_kind = ${node}.${kindColumn}`;
|
|
2416
|
+
}
|
|
2417
|
+
function buildRelevanceJoin(cteAlias, nodeAlias, joinType = "INNER JOIN") {
|
|
2418
|
+
const cte = sql.raw(cteAlias);
|
|
2419
|
+
const node = sql.raw(`${ALIAS_CTE_PREFIX}${nodeAlias}`);
|
|
2420
|
+
const idColumn = sql.raw(`${nodeAlias}_id`);
|
|
2421
|
+
const kindColumn = sql.raw(`${nodeAlias}_kind`);
|
|
2422
|
+
return sql`${sql.raw(joinType)} ${cte} ON ${cte}.node_id = ${node}.${idColumn} AND ${cte}.node_kind = ${node}.${kindColumn}`;
|
|
2423
|
+
}
|
|
2221
2424
|
function buildStandardFromClause(input) {
|
|
2222
|
-
const {
|
|
2425
|
+
const {
|
|
2426
|
+
ast,
|
|
2427
|
+
collapsedTraversalCteAlias,
|
|
2428
|
+
fulltextPredicate,
|
|
2429
|
+
vectorPredicate
|
|
2430
|
+
} = input;
|
|
2223
2431
|
if (collapsedTraversalCteAlias !== void 0) {
|
|
2224
2432
|
return sql`FROM ${sql.raw(collapsedTraversalCteAlias)}`;
|
|
2225
2433
|
}
|
|
@@ -2234,11 +2442,8 @@ function buildStandardFromClause(input) {
|
|
|
2234
2442
|
sql`${sql.raw(joinType)} ${sql.raw(cteAlias)} ON ${sql.raw(cteAlias)}.${sql.raw(previousAlias)}_id = cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_id AND ${sql.raw(cteAlias)}.${sql.raw(previousAlias)}_kind = cte_${sql.raw(previousAlias)}.${sql.raw(previousAlias)}_kind`
|
|
2235
2443
|
);
|
|
2236
2444
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
joins.push(
|
|
2240
|
-
sql`INNER JOIN cte_embeddings ON cte_embeddings.node_id = cte_${sql.raw(nodeAlias)}.${sql.raw(nodeAlias)}_id`
|
|
2241
|
-
);
|
|
2445
|
+
for (const join of buildRelevanceJoins(vectorPredicate, fulltextPredicate)) {
|
|
2446
|
+
joins.push(join);
|
|
2242
2447
|
}
|
|
2243
2448
|
return joins.length === 0 ? fromClause : sql`${fromClause} ${sql.join(joins, sql` `)}`;
|
|
2244
2449
|
}
|
|
@@ -2277,6 +2482,15 @@ function fieldRefKey(field2) {
|
|
|
2277
2482
|
const pointer = field2.jsonPointer ?? "";
|
|
2278
2483
|
return `${field2.alias}:${field2.path.join(".")}:${pointer}`;
|
|
2279
2484
|
}
|
|
2485
|
+
function resolveEmbeddingFieldPath(field2) {
|
|
2486
|
+
if (field2.jsonPointer !== void 0 && field2.jsonPointer !== "" && field2.jsonPointer !== "/") {
|
|
2487
|
+
return field2.jsonPointer.replace(/^\//u, "").replaceAll("/", ".");
|
|
2488
|
+
}
|
|
2489
|
+
if (field2.path[0] === "props") {
|
|
2490
|
+
return field2.path.slice(1).join(".");
|
|
2491
|
+
}
|
|
2492
|
+
return field2.path.join(".");
|
|
2493
|
+
}
|
|
2280
2494
|
function buildStandardGroupBy(input) {
|
|
2281
2495
|
const { ast, dialect } = input;
|
|
2282
2496
|
if (!ast.groupBy || ast.groupBy.fields.length === 0) {
|
|
@@ -2323,18 +2537,29 @@ function buildStandardHaving(input) {
|
|
|
2323
2537
|
return sql`HAVING ${condition}`;
|
|
2324
2538
|
}
|
|
2325
2539
|
function buildStandardEmbeddingsCte(input) {
|
|
2326
|
-
const { ctx, graphId, vectorPredicate } = input;
|
|
2540
|
+
const { ctx, graphId, nodeKinds, vectorPredicate } = input;
|
|
2327
2541
|
const { dialect } = ctx;
|
|
2328
2542
|
const { field: field2, metric, minScore, queryEmbedding } = vectorPredicate;
|
|
2329
|
-
const
|
|
2543
|
+
const embeddingsTableName = ctx.schema.tables.embeddings;
|
|
2544
|
+
if (nodeKinds.length === 0) {
|
|
2545
|
+
throw new UnsupportedPredicateError(
|
|
2546
|
+
"Vector predicate must resolve to at least one node kind"
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
const fieldPath = resolveEmbeddingFieldPath(field2);
|
|
2330
2550
|
const distanceExpr = dialect.vectorDistance(
|
|
2331
|
-
|
|
2551
|
+
qualifyColumn(embeddingsTableName, "embedding"),
|
|
2332
2552
|
queryEmbedding,
|
|
2333
2553
|
metric
|
|
2334
2554
|
);
|
|
2555
|
+
const scopedNodes = buildScopedNodeIdsSubquery(field2.alias);
|
|
2335
2556
|
const conditions = [
|
|
2336
|
-
sql
|
|
2337
|
-
|
|
2557
|
+
sql`${qualifyColumn(embeddingsTableName, "graph_id")} = ${graphId}`,
|
|
2558
|
+
compileKindFilter(
|
|
2559
|
+
qualifyColumn(embeddingsTableName, "node_kind"),
|
|
2560
|
+
nodeKinds
|
|
2561
|
+
),
|
|
2562
|
+
sql`${qualifyColumn(embeddingsTableName, "field_path")} = ${fieldPath}`
|
|
2338
2563
|
];
|
|
2339
2564
|
if (minScore !== void 0) {
|
|
2340
2565
|
conditions.push(
|
|
@@ -2343,14 +2568,29 @@ function buildStandardEmbeddingsCte(input) {
|
|
|
2343
2568
|
}
|
|
2344
2569
|
const scoreExpr = compileVectorScoreExpression(distanceExpr, metric);
|
|
2345
2570
|
return sql`
|
|
2346
|
-
|
|
2571
|
+
${sql.raw(EMBEDDINGS_CTE_ALIAS)} AS (
|
|
2347
2572
|
SELECT
|
|
2348
2573
|
node_id,
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2574
|
+
node_kind,
|
|
2575
|
+
distance,
|
|
2576
|
+
score,
|
|
2577
|
+
ROW_NUMBER() OVER (ORDER BY distance ASC) AS ord
|
|
2578
|
+
FROM (
|
|
2579
|
+
SELECT
|
|
2580
|
+
${qualifyColumn(embeddingsTableName, "node_id")} AS node_id,
|
|
2581
|
+
${qualifyColumn(embeddingsTableName, "node_kind")} AS node_kind,
|
|
2582
|
+
${distanceExpr} AS distance,
|
|
2583
|
+
${scoreExpr} AS score
|
|
2584
|
+
FROM ${ctx.schema.embeddingsTable}
|
|
2585
|
+
INNER JOIN ${scopedNodes}
|
|
2586
|
+
ON ${sql.raw(`${SCOPED_RELEVANCE_NODES_ALIAS}.node_id`)} =
|
|
2587
|
+
${qualifyColumn(embeddingsTableName, "node_id")}
|
|
2588
|
+
AND ${sql.raw(`${SCOPED_RELEVANCE_NODES_ALIAS}.node_kind`)} =
|
|
2589
|
+
${qualifyColumn(embeddingsTableName, "node_kind")}
|
|
2590
|
+
WHERE ${sql.join(conditions, sql` AND `)}
|
|
2591
|
+
ORDER BY distance ASC
|
|
2592
|
+
LIMIT ${vectorPredicate.limit}
|
|
2593
|
+
) AS vec_inner
|
|
2354
2594
|
)
|
|
2355
2595
|
`;
|
|
2356
2596
|
}
|
|
@@ -2380,37 +2620,130 @@ function compileVectorMinScoreCondition(distanceExpr, metric, minScore) {
|
|
|
2380
2620
|
}
|
|
2381
2621
|
}
|
|
2382
2622
|
}
|
|
2383
|
-
function
|
|
2384
|
-
const {
|
|
2385
|
-
const
|
|
2386
|
-
const
|
|
2387
|
-
if (
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2623
|
+
function buildStandardFulltextCte(input) {
|
|
2624
|
+
const { ctx, fulltextPredicate, graphId, nodeKinds } = input;
|
|
2625
|
+
const { dialect, schema } = ctx;
|
|
2626
|
+
const { query, mode, language, limit, minScore } = fulltextPredicate;
|
|
2627
|
+
if (nodeKinds.length === 0) {
|
|
2628
|
+
throw new UnsupportedPredicateError(
|
|
2629
|
+
"Fulltext predicate must resolve to at least one node kind"
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
const tableName = schema.tables.fulltext;
|
|
2633
|
+
const scopedNodes = buildScopedNodeIdsSubquery(fulltextPredicate.field.alias);
|
|
2634
|
+
const fulltextStrategy = dialect.fulltext;
|
|
2635
|
+
if (fulltextStrategy === void 0) {
|
|
2636
|
+
throw new UnsupportedPredicateError(
|
|
2637
|
+
`Fulltext match predicates are not supported for dialect "${dialect.name}"`
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
const matchCondition = fulltextStrategy.matchCondition(
|
|
2641
|
+
tableName,
|
|
2642
|
+
query,
|
|
2643
|
+
mode,
|
|
2644
|
+
language
|
|
2645
|
+
);
|
|
2646
|
+
const rankExpression = fulltextStrategy.rankExpression(
|
|
2647
|
+
tableName,
|
|
2648
|
+
query,
|
|
2649
|
+
mode,
|
|
2650
|
+
language
|
|
2651
|
+
);
|
|
2652
|
+
const conditions = [
|
|
2653
|
+
sql`${qualifyColumn(tableName, "graph_id")} = ${graphId}`,
|
|
2654
|
+
compileKindFilter(qualifyColumn(tableName, "node_kind"), nodeKinds),
|
|
2655
|
+
matchCondition
|
|
2656
|
+
];
|
|
2657
|
+
if (minScore !== void 0) {
|
|
2658
|
+
conditions.push(sql`${rankExpression} >= ${minScore}`);
|
|
2659
|
+
}
|
|
2660
|
+
return sql`
|
|
2661
|
+
${sql.raw(FULLTEXT_CTE_ALIAS)} AS (
|
|
2662
|
+
SELECT
|
|
2663
|
+
node_id,
|
|
2664
|
+
node_kind,
|
|
2665
|
+
rank,
|
|
2666
|
+
ROW_NUMBER() OVER (ORDER BY rank DESC, node_id ASC) AS ord
|
|
2667
|
+
FROM (
|
|
2668
|
+
SELECT
|
|
2669
|
+
${qualifyColumn(tableName, "node_id")} AS node_id,
|
|
2670
|
+
${qualifyColumn(tableName, "node_kind")} AS node_kind,
|
|
2671
|
+
${rankExpression} AS rank
|
|
2672
|
+
FROM ${schema.fulltextTable}
|
|
2673
|
+
INNER JOIN ${scopedNodes}
|
|
2674
|
+
ON ${sql.raw(`${SCOPED_RELEVANCE_NODES_ALIAS}.node_id`)} =
|
|
2675
|
+
${qualifyColumn(tableName, "node_id")}
|
|
2676
|
+
AND ${sql.raw(`${SCOPED_RELEVANCE_NODES_ALIAS}.node_kind`)} =
|
|
2677
|
+
${qualifyColumn(tableName, "node_kind")}
|
|
2678
|
+
WHERE ${sql.join(conditions, sql` AND `)}
|
|
2679
|
+
ORDER BY rank DESC, ${qualifyColumn(tableName, "node_id")} ASC
|
|
2680
|
+
LIMIT ${limit}
|
|
2681
|
+
) AS fts_inner
|
|
2682
|
+
)
|
|
2683
|
+
`;
|
|
2684
|
+
}
|
|
2685
|
+
function buildStandardHybridCandidateCte() {
|
|
2686
|
+
return sql`
|
|
2687
|
+
${sql.raw(HYBRID_CANDIDATES_CTE_ALIAS)} AS (
|
|
2688
|
+
SELECT node_id, node_kind FROM ${sql.raw(EMBEDDINGS_CTE_ALIAS)}
|
|
2689
|
+
UNION
|
|
2690
|
+
SELECT node_id, node_kind FROM ${sql.raw(FULLTEXT_CTE_ALIAS)}
|
|
2691
|
+
)
|
|
2692
|
+
`;
|
|
2693
|
+
}
|
|
2694
|
+
function compileUserOrderBy(ast, dialect) {
|
|
2695
|
+
if (!ast.orderBy || ast.orderBy.length === 0) return [];
|
|
2696
|
+
const aliasToCte = buildAliasToCteMap(ast);
|
|
2697
|
+
const fragments = [];
|
|
2698
|
+
for (const orderSpec of ast.orderBy) {
|
|
2699
|
+
const valueType = orderSpec.field.valueType;
|
|
2700
|
+
if (valueType === "array" || valueType === "object") {
|
|
2701
|
+
throw new UnsupportedPredicateError(
|
|
2702
|
+
"Ordering by JSON arrays or objects is not supported"
|
|
2409
2703
|
);
|
|
2410
2704
|
}
|
|
2705
|
+
const cteAlias = aliasToCte.get(orderSpec.field.alias) ?? `${ALIAS_CTE_PREFIX}${orderSpec.field.alias}`;
|
|
2706
|
+
const field2 = compileFieldValue(
|
|
2707
|
+
orderSpec.field,
|
|
2708
|
+
dialect,
|
|
2709
|
+
valueType,
|
|
2710
|
+
cteAlias
|
|
2711
|
+
);
|
|
2712
|
+
const direction = sql.raw(orderSpec.direction.toUpperCase());
|
|
2713
|
+
const nulls = orderSpec.nulls ?? (orderSpec.direction === "asc" ? "last" : "first");
|
|
2714
|
+
const nullsDirection = sql.raw(nulls === "first" ? "DESC" : "ASC");
|
|
2715
|
+
fragments.push(
|
|
2716
|
+
sql`(${field2} IS NULL) ${nullsDirection}`,
|
|
2717
|
+
sql`${field2} ${direction}`
|
|
2718
|
+
);
|
|
2411
2719
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2720
|
+
return fragments;
|
|
2721
|
+
}
|
|
2722
|
+
function buildStandardFulltextOrderBy(input) {
|
|
2723
|
+
const { ast, dialect } = input;
|
|
2724
|
+
const rankOrder = sql.raw(`${FULLTEXT_CTE_ALIAS}.rank DESC`);
|
|
2725
|
+
const userOrders = compileUserOrderBy(ast, dialect);
|
|
2726
|
+
return sql`ORDER BY ${sql.join([rankOrder, ...userOrders], sql`, `)}`;
|
|
2727
|
+
}
|
|
2728
|
+
function buildStandardHybridRrfOrderBy(input) {
|
|
2729
|
+
const { ast, dialect, fusion } = input;
|
|
2730
|
+
const k = fusion?.k ?? DEFAULT_RRF_K;
|
|
2731
|
+
const vectorWeight = fusion?.weights?.vector ?? DEFAULT_RRF_WEIGHT;
|
|
2732
|
+
const fulltextWeight = fusion?.weights?.fulltext ?? DEFAULT_RRF_WEIGHT;
|
|
2733
|
+
const rrfOrder = sql.raw(
|
|
2734
|
+
`(COALESCE(${vectorWeight} / (${k} + ${EMBEDDINGS_CTE_ALIAS}.ord), 0) + COALESCE(${fulltextWeight} / (${k} + ${FULLTEXT_CTE_ALIAS}.ord), 0)) DESC`
|
|
2735
|
+
);
|
|
2736
|
+
const idTiebreak = sql.raw(
|
|
2737
|
+
`COALESCE(${FULLTEXT_CTE_ALIAS}.node_id, ${EMBEDDINGS_CTE_ALIAS}.node_id) ASC`
|
|
2738
|
+
);
|
|
2739
|
+
const userOrders = compileUserOrderBy(ast, dialect);
|
|
2740
|
+
return sql`ORDER BY ${sql.join([rrfOrder, ...userOrders, idTiebreak], sql`, `)}`;
|
|
2741
|
+
}
|
|
2742
|
+
function buildStandardVectorOrderBy(input) {
|
|
2743
|
+
const { ast, dialect } = input;
|
|
2744
|
+
const distanceOrder = sql.raw(`${EMBEDDINGS_CTE_ALIAS}.distance ASC`);
|
|
2745
|
+
const userOrders = compileUserOrderBy(ast, dialect);
|
|
2746
|
+
return sql`ORDER BY ${sql.join([distanceOrder, ...userOrders], sql`, `)}`;
|
|
2414
2747
|
}
|
|
2415
2748
|
function buildLimitOffsetClause(input) {
|
|
2416
2749
|
const { limit, offset } = input;
|
|
@@ -2424,6 +2757,74 @@ function buildLimitOffsetClause(input) {
|
|
|
2424
2757
|
return parts.length > 0 ? sql.join(parts, sql` `) : void 0;
|
|
2425
2758
|
}
|
|
2426
2759
|
|
|
2760
|
+
// src/query/compiler/passes/fulltext.ts
|
|
2761
|
+
function runFulltextPredicatePass(ast, dialect) {
|
|
2762
|
+
const fulltextPredicates = extractFulltextMatchPredicates(ast.predicates);
|
|
2763
|
+
if (fulltextPredicates.length > 1) {
|
|
2764
|
+
throw new UnsupportedPredicateError(
|
|
2765
|
+
"Multiple fulltext match predicates in a single query are not supported"
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
const fulltextPredicate = fulltextPredicates[0];
|
|
2769
|
+
if (fulltextPredicate === void 0) {
|
|
2770
|
+
return { fulltextPredicate: void 0 };
|
|
2771
|
+
}
|
|
2772
|
+
if (!dialect.capabilities.supportsFulltext) {
|
|
2773
|
+
throw new UnsupportedPredicateError(
|
|
2774
|
+
`Fulltext match predicates are not supported for dialect "${dialect.name}"`
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
const strategy = dialect.fulltext;
|
|
2778
|
+
if (strategy === void 0) {
|
|
2779
|
+
throw new UnsupportedPredicateError(
|
|
2780
|
+
`Dialect "${dialect.name}" advertises fulltext support but has no strategy configured`
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
if (!strategy.supportedModes.includes(fulltextPredicate.mode)) {
|
|
2784
|
+
throw new UnsupportedPredicateError(
|
|
2785
|
+
`Fulltext query mode "${fulltextPredicate.mode}" is not supported by the "${strategy.name}" strategy. Supported modes: ${strategy.supportedModes.join(", ")}.`
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
if (!Number.isFinite(fulltextPredicate.limit) || fulltextPredicate.limit <= 0) {
|
|
2789
|
+
throw new UnsupportedPredicateError(
|
|
2790
|
+
`Fulltext match limit must be a positive finite number, got ${String(fulltextPredicate.limit)}`
|
|
2791
|
+
);
|
|
2792
|
+
}
|
|
2793
|
+
const { minScore } = fulltextPredicate;
|
|
2794
|
+
if (minScore !== void 0 && !Number.isFinite(minScore)) {
|
|
2795
|
+
throw new UnsupportedPredicateError(
|
|
2796
|
+
`Fulltext minScore must be a finite number, got ${String(minScore)}`
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
return { fulltextPredicate };
|
|
2800
|
+
}
|
|
2801
|
+
function resolveFulltextAwareLimit(astLimit, fulltextPredicate) {
|
|
2802
|
+
if (fulltextPredicate === void 0) {
|
|
2803
|
+
return astLimit;
|
|
2804
|
+
}
|
|
2805
|
+
if (astLimit === void 0) {
|
|
2806
|
+
return fulltextPredicate.limit;
|
|
2807
|
+
}
|
|
2808
|
+
return Math.min(astLimit, fulltextPredicate.limit);
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// src/query/compiler/passes/fusion.ts
|
|
2812
|
+
function runFusionConfigPass(ast, vectorPredicate, fulltextPredicate) {
|
|
2813
|
+
const fusion = ast.fusion;
|
|
2814
|
+
if (fusion === void 0) return { fusion: void 0 };
|
|
2815
|
+
if (vectorPredicate === void 0 || fulltextPredicate === void 0) {
|
|
2816
|
+
throw new UnsupportedPredicateError(
|
|
2817
|
+
`.fuseWith() requires both a .similarTo() and a .$fulltext.matches() predicate in the same query. Remove .fuseWith(), or add the missing predicate.`
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
if (vectorPredicate.field.alias !== fulltextPredicate.field.alias) {
|
|
2821
|
+
throw new UnsupportedPredicateError(
|
|
2822
|
+
`.fuseWith() requires .similarTo() and .$fulltext.matches() to target the same node alias. Got vector=${vectorPredicate.field.alias}, fulltext=${fulltextPredicate.field.alias}.`
|
|
2823
|
+
);
|
|
2824
|
+
}
|
|
2825
|
+
return { fusion };
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2427
2828
|
// src/query/compiler/passes/recursive.ts
|
|
2428
2829
|
function runRecursiveTraversalSelectionPass(ast) {
|
|
2429
2830
|
const variableLengthTraversal = ast.traversals.find(
|
|
@@ -2693,6 +3094,14 @@ function lowerStandardQueryToLogicalPlanNode(input) {
|
|
|
2693
3094
|
predicate: input.vectorPredicate
|
|
2694
3095
|
};
|
|
2695
3096
|
}
|
|
3097
|
+
if (input.fulltextPredicate !== void 0) {
|
|
3098
|
+
currentNode = {
|
|
3099
|
+
id: nextPlanNodeId(),
|
|
3100
|
+
input: currentNode,
|
|
3101
|
+
op: "fulltext_match",
|
|
3102
|
+
predicate: input.fulltextPredicate
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
2696
3105
|
return appendAggregateSortLimitAndProjectNodes(
|
|
2697
3106
|
currentNode,
|
|
2698
3107
|
ast,
|
|
@@ -2770,17 +3179,30 @@ function lowerComposableQueryToLogicalPlanNode(query, dialect, graphId, nextPlan
|
|
|
2770
3179
|
nextPlanNodeId
|
|
2771
3180
|
});
|
|
2772
3181
|
}
|
|
3182
|
+
const dialectAdapter = getDialect(dialect);
|
|
2773
3183
|
const vectorPredicate = runVectorPredicatePass(
|
|
2774
3184
|
query,
|
|
2775
|
-
|
|
3185
|
+
dialectAdapter
|
|
2776
3186
|
).vectorPredicate;
|
|
2777
|
-
const
|
|
3187
|
+
const fulltextPredicate = runFulltextPredicatePass(
|
|
3188
|
+
query,
|
|
3189
|
+
dialectAdapter
|
|
3190
|
+
).fulltextPredicate;
|
|
3191
|
+
const limitAfterVector = resolveVectorAwareLimit(
|
|
3192
|
+
query.limit,
|
|
3193
|
+
vectorPredicate
|
|
3194
|
+
);
|
|
3195
|
+
const effectiveLimit = resolveFulltextAwareLimit(
|
|
3196
|
+
limitAfterVector,
|
|
3197
|
+
fulltextPredicate
|
|
3198
|
+
);
|
|
2778
3199
|
const loweringInput = {
|
|
2779
3200
|
ast: query,
|
|
2780
3201
|
graphId,
|
|
2781
3202
|
nextPlanNodeId,
|
|
2782
3203
|
...effectiveLimit === void 0 ? {} : { effectiveLimit },
|
|
2783
|
-
...vectorPredicate === void 0 ? {} : { vectorPredicate }
|
|
3204
|
+
...vectorPredicate === void 0 ? {} : { vectorPredicate },
|
|
3205
|
+
...fulltextPredicate === void 0 ? {} : { fulltextPredicate }
|
|
2784
3206
|
};
|
|
2785
3207
|
return lowerStandardQueryToLogicalPlanNode(loweringInput);
|
|
2786
3208
|
}
|
|
@@ -3287,10 +3709,10 @@ function compileRecursiveProjection(ast, traversal, dialect) {
|
|
|
3287
3709
|
sql`${sql.raw(nodeAlias)}_deleted_at`
|
|
3288
3710
|
];
|
|
3289
3711
|
if (vl.depthAlias !== void 0) {
|
|
3290
|
-
fields.push(sql`depth AS ${
|
|
3712
|
+
fields.push(sql`depth AS ${quoteIdentifier2(vl.depthAlias)}`);
|
|
3291
3713
|
}
|
|
3292
3714
|
if (vl.pathAlias !== void 0) {
|
|
3293
|
-
fields.push(sql`path AS ${
|
|
3715
|
+
fields.push(sql`path AS ${quoteIdentifier2(vl.pathAlias)}`);
|
|
3294
3716
|
}
|
|
3295
3717
|
return sql.join(fields, sql`, `);
|
|
3296
3718
|
}
|
|
@@ -3304,7 +3726,7 @@ function compileRecursiveSelectiveProjection(fields, ast, traversal, dialect) {
|
|
|
3304
3726
|
}
|
|
3305
3727
|
if (field2.isSystemField) {
|
|
3306
3728
|
const dbColumn = mapSelectiveSystemFieldToColumn(field2.field);
|
|
3307
|
-
return sql`${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${
|
|
3729
|
+
return sql`${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${quoteIdentifier2(field2.outputName)}`;
|
|
3308
3730
|
}
|
|
3309
3731
|
const column = sql.raw(`${field2.alias}_props`);
|
|
3310
3732
|
const extracted = compileTypedJsonExtract({
|
|
@@ -3313,14 +3735,14 @@ function compileRecursiveSelectiveProjection(fields, ast, traversal, dialect) {
|
|
|
3313
3735
|
pointer: jsonPointer([field2.field]),
|
|
3314
3736
|
valueType: field2.valueType
|
|
3315
3737
|
});
|
|
3316
|
-
return sql`${extracted} AS ${
|
|
3738
|
+
return sql`${extracted} AS ${quoteIdentifier2(field2.outputName)}`;
|
|
3317
3739
|
});
|
|
3318
3740
|
const vl = traversal.variableLength;
|
|
3319
3741
|
if (vl.depthAlias !== void 0) {
|
|
3320
|
-
columns.push(sql`depth AS ${
|
|
3742
|
+
columns.push(sql`depth AS ${quoteIdentifier2(vl.depthAlias)}`);
|
|
3321
3743
|
}
|
|
3322
3744
|
if (vl.pathAlias !== void 0) {
|
|
3323
|
-
columns.push(sql`path AS ${
|
|
3745
|
+
columns.push(sql`path AS ${quoteIdentifier2(vl.pathAlias)}`);
|
|
3324
3746
|
}
|
|
3325
3747
|
return sql.join(columns, sql`, `);
|
|
3326
3748
|
}
|
|
@@ -3352,48 +3774,11 @@ function compileLimitOffset(ast) {
|
|
|
3352
3774
|
if (ast.limit !== void 0) {
|
|
3353
3775
|
parts.push(sql`LIMIT ${ast.limit}`);
|
|
3354
3776
|
}
|
|
3355
|
-
if (ast.offset !== void 0) {
|
|
3356
|
-
parts.push(sql`OFFSET ${ast.offset}`);
|
|
3357
|
-
}
|
|
3358
|
-
return parts.length > 0 ? sql.join(parts, sql` `) : void 0;
|
|
3359
|
-
}
|
|
3360
|
-
var DEFAULT_TABLE_NAMES = {
|
|
3361
|
-
nodes: "typegraph_nodes",
|
|
3362
|
-
edges: "typegraph_edges",
|
|
3363
|
-
embeddings: "typegraph_node_embeddings"
|
|
3364
|
-
};
|
|
3365
|
-
var VALID_IDENTIFIER_PATTERN = /^[a-z_][a-z0-9_$]*$/i;
|
|
3366
|
-
function validateTableName(name, label) {
|
|
3367
|
-
if (!name || name.length === 0) {
|
|
3368
|
-
throw new ConfigurationError(`${label} table name cannot be empty`);
|
|
3369
|
-
}
|
|
3370
|
-
if (name.length > MAX_PG_IDENTIFIER_LENGTH) {
|
|
3371
|
-
throw new ConfigurationError(
|
|
3372
|
-
`${label} table name exceeds maximum length of ${MAX_PG_IDENTIFIER_LENGTH} characters`
|
|
3373
|
-
);
|
|
3374
|
-
}
|
|
3375
|
-
if (!VALID_IDENTIFIER_PATTERN.test(name)) {
|
|
3376
|
-
throw new ConfigurationError(
|
|
3377
|
-
`${label} table name "${name}" is not a valid SQL identifier. Table names must start with a letter or underscore and contain only letters, digits, underscores, or dollar signs.`
|
|
3378
|
-
);
|
|
3379
|
-
}
|
|
3380
|
-
}
|
|
3381
|
-
function quoteIdentifier2(name) {
|
|
3382
|
-
return `"${name.replaceAll('"', '""')}"`;
|
|
3383
|
-
}
|
|
3384
|
-
function createSqlSchema(names = {}) {
|
|
3385
|
-
const tables = { ...DEFAULT_TABLE_NAMES, ...names };
|
|
3386
|
-
validateTableName(tables.nodes, "nodes");
|
|
3387
|
-
validateTableName(tables.edges, "edges");
|
|
3388
|
-
validateTableName(tables.embeddings, "embeddings");
|
|
3389
|
-
return {
|
|
3390
|
-
tables,
|
|
3391
|
-
nodesTable: sql.raw(quoteIdentifier2(tables.nodes)),
|
|
3392
|
-
edgesTable: sql.raw(quoteIdentifier2(tables.edges)),
|
|
3393
|
-
embeddingsTable: sql.raw(quoteIdentifier2(tables.embeddings))
|
|
3394
|
-
};
|
|
3777
|
+
if (ast.offset !== void 0) {
|
|
3778
|
+
parts.push(sql`OFFSET ${ast.offset}`);
|
|
3779
|
+
}
|
|
3780
|
+
return parts.length > 0 ? sql.join(parts, sql` `) : void 0;
|
|
3395
3781
|
}
|
|
3396
|
-
var DEFAULT_SQL_SCHEMA = createSqlSchema();
|
|
3397
3782
|
|
|
3398
3783
|
// src/query/execution/value-decoder.ts
|
|
3399
3784
|
function nullToUndefined(value) {
|
|
@@ -3698,12 +4083,7 @@ async function executeSubgraph(params) {
|
|
|
3698
4083
|
for (const row of edgeRows) {
|
|
3699
4084
|
const edge = mapSubgraphEdgeRow(row, edgeProjectionPlan);
|
|
3700
4085
|
insertAdjacencyEntry(adjacency, edge.fromId, edge.kind, edge);
|
|
3701
|
-
insertAdjacencyEntry(
|
|
3702
|
-
reverseAdjacency,
|
|
3703
|
-
edge.toId,
|
|
3704
|
-
edge.kind,
|
|
3705
|
-
edge
|
|
3706
|
-
);
|
|
4086
|
+
insertAdjacencyEntry(reverseAdjacency, edge.toId, edge.kind, edge);
|
|
3707
4087
|
}
|
|
3708
4088
|
const root = nodesMap.get(ctx.rootId);
|
|
3709
4089
|
return {
|
|
@@ -3877,7 +4257,7 @@ function buildProjectedPropertyColumns(alias, plan, dialect) {
|
|
|
3877
4257
|
valueType: fieldPlan.typeInfo?.valueType
|
|
3878
4258
|
});
|
|
3879
4259
|
columns.push(
|
|
3880
|
-
sql`CASE WHEN ${sql.raw(alias)}.kind = ${kind} THEN ${extracted} ELSE NULL END AS ${
|
|
4260
|
+
sql`CASE WHEN ${sql.raw(alias)}.kind = ${kind} THEN ${extracted} ELSE NULL END AS ${quoteIdentifier2(fieldPlan.outputName)}`
|
|
3881
4261
|
);
|
|
3882
4262
|
}
|
|
3883
4263
|
}
|
|
@@ -3944,16 +4324,6 @@ function mapSubgraphEdgeRow(row, projectionPlan) {
|
|
|
3944
4324
|
applyProjectedFields(projectedEdge, row, kindPlan);
|
|
3945
4325
|
return projectedEdge;
|
|
3946
4326
|
}
|
|
3947
|
-
|
|
3948
|
-
// src/query/ast.ts
|
|
3949
|
-
function mergeEdgeKinds(traversal) {
|
|
3950
|
-
const inverse = traversal.inverseEdgeKinds;
|
|
3951
|
-
if (inverse === void 0 || inverse.length === 0) return traversal.edgeKinds;
|
|
3952
|
-
return [
|
|
3953
|
-
...traversal.edgeKinds,
|
|
3954
|
-
...inverse.filter((kind) => !traversal.edgeKinds.includes(kind))
|
|
3955
|
-
];
|
|
3956
|
-
}
|
|
3957
4327
|
var OPERATOR_MAP = {
|
|
3958
4328
|
union: "UNION",
|
|
3959
4329
|
unionAll: "UNION ALL",
|
|
@@ -4110,6 +4480,9 @@ function validateSqliteSetOpLeaf(ast) {
|
|
|
4110
4480
|
if (hasVectorSimilarityPredicates(ast)) {
|
|
4111
4481
|
unsupported.push("vector similarity predicates");
|
|
4112
4482
|
}
|
|
4483
|
+
if (hasFulltextMatchPredicates(ast)) {
|
|
4484
|
+
unsupported.push("fulltext match predicates");
|
|
4485
|
+
}
|
|
4113
4486
|
if (ast.groupBy !== void 0) {
|
|
4114
4487
|
unsupported.push("GROUP BY");
|
|
4115
4488
|
}
|
|
@@ -4159,7 +4532,42 @@ function hasVectorSimilarityInExpression(expr) {
|
|
|
4159
4532
|
case "between":
|
|
4160
4533
|
case "array_op":
|
|
4161
4534
|
case "object_op":
|
|
4162
|
-
case "aggregate_comparison":
|
|
4535
|
+
case "aggregate_comparison":
|
|
4536
|
+
case "fulltext_match": {
|
|
4537
|
+
return false;
|
|
4538
|
+
}
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
return false;
|
|
4542
|
+
}
|
|
4543
|
+
function hasFulltextMatchPredicates(ast) {
|
|
4544
|
+
return ast.predicates.some(
|
|
4545
|
+
(predicate2) => hasFulltextMatchInExpression(predicate2.expression)
|
|
4546
|
+
);
|
|
4547
|
+
}
|
|
4548
|
+
function hasFulltextMatchInExpression(expr) {
|
|
4549
|
+
if ("__type" in expr) {
|
|
4550
|
+
switch (expr.__type) {
|
|
4551
|
+
case "and":
|
|
4552
|
+
case "or": {
|
|
4553
|
+
return expr.predicates.some((p) => hasFulltextMatchInExpression(p));
|
|
4554
|
+
}
|
|
4555
|
+
case "not": {
|
|
4556
|
+
return hasFulltextMatchInExpression(expr.predicate);
|
|
4557
|
+
}
|
|
4558
|
+
case "fulltext_match": {
|
|
4559
|
+
return true;
|
|
4560
|
+
}
|
|
4561
|
+
case "exists":
|
|
4562
|
+
case "in_subquery":
|
|
4563
|
+
case "comparison":
|
|
4564
|
+
case "string_op":
|
|
4565
|
+
case "null_check":
|
|
4566
|
+
case "between":
|
|
4567
|
+
case "array_op":
|
|
4568
|
+
case "object_op":
|
|
4569
|
+
case "aggregate_comparison":
|
|
4570
|
+
case "vector_similarity": {
|
|
4163
4571
|
return false;
|
|
4164
4572
|
}
|
|
4165
4573
|
}
|
|
@@ -4195,7 +4603,8 @@ function hasSubqueryInExpression(expr) {
|
|
|
4195
4603
|
case "array_op":
|
|
4196
4604
|
case "object_op":
|
|
4197
4605
|
case "aggregate_comparison":
|
|
4198
|
-
case "vector_similarity":
|
|
4606
|
+
case "vector_similarity":
|
|
4607
|
+
case "fulltext_match": {
|
|
4199
4608
|
return false;
|
|
4200
4609
|
}
|
|
4201
4610
|
}
|
|
@@ -4473,6 +4882,10 @@ function markPredicateFieldsAsRequired(requiredColumnsByAlias, expression) {
|
|
|
4473
4882
|
markFieldRefAsRequired(requiredColumnsByAlias, expression.field);
|
|
4474
4883
|
return;
|
|
4475
4884
|
}
|
|
4885
|
+
case "fulltext_match": {
|
|
4886
|
+
markFieldRefAsRequired(requiredColumnsByAlias, expression.field);
|
|
4887
|
+
return;
|
|
4888
|
+
}
|
|
4476
4889
|
case "exists": {
|
|
4477
4890
|
return;
|
|
4478
4891
|
}
|
|
@@ -4537,7 +4950,8 @@ function hasIdEqualityPredicate(expression, alias) {
|
|
|
4537
4950
|
case "aggregate_comparison":
|
|
4538
4951
|
case "exists":
|
|
4539
4952
|
case "in_subquery":
|
|
4540
|
-
case "vector_similarity":
|
|
4953
|
+
case "vector_similarity":
|
|
4954
|
+
case "fulltext_match": {
|
|
4541
4955
|
return false;
|
|
4542
4956
|
}
|
|
4543
4957
|
}
|
|
@@ -4578,10 +4992,13 @@ function resolveTraversalCteLimit(ast, predicateIndex) {
|
|
|
4578
4992
|
);
|
|
4579
4993
|
return Math.max(ast.limit, pushdownLimit);
|
|
4580
4994
|
}
|
|
4581
|
-
function canCollapseSelectiveTraversalRowset(ast, vectorPredicate) {
|
|
4995
|
+
function canCollapseSelectiveTraversalRowset(ast, vectorPredicate, fulltextPredicate) {
|
|
4582
4996
|
if (vectorPredicate !== void 0) {
|
|
4583
4997
|
return false;
|
|
4584
4998
|
}
|
|
4999
|
+
if (fulltextPredicate !== void 0) {
|
|
5000
|
+
return false;
|
|
5001
|
+
}
|
|
4585
5002
|
if (ast.traversals.length === 0) {
|
|
4586
5003
|
return false;
|
|
4587
5004
|
}
|
|
@@ -4621,6 +5038,8 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
|
|
|
4621
5038
|
collapsedTraversalCteAlias: void 0,
|
|
4622
5039
|
ctx,
|
|
4623
5040
|
effectiveLimit: void 0,
|
|
5041
|
+
fulltextPredicate: void 0,
|
|
5042
|
+
fusion: void 0,
|
|
4624
5043
|
logicalPlan: void 0,
|
|
4625
5044
|
predicateIndex: buildPredicateIndex(ast),
|
|
4626
5045
|
requiredColumnsByAlias: void 0,
|
|
@@ -4642,6 +5061,39 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
|
|
|
4642
5061
|
}
|
|
4643
5062
|
});
|
|
4644
5063
|
state = vectorPass.state;
|
|
5064
|
+
const fulltextPass = runCompilerPass(state, {
|
|
5065
|
+
name: "fulltext_predicate",
|
|
5066
|
+
execute(currentState) {
|
|
5067
|
+
return runFulltextPredicatePass(
|
|
5068
|
+
currentState.ast,
|
|
5069
|
+
currentState.ctx.dialect
|
|
5070
|
+
).fulltextPredicate;
|
|
5071
|
+
},
|
|
5072
|
+
update(currentState, fulltextPredicate) {
|
|
5073
|
+
return {
|
|
5074
|
+
...currentState,
|
|
5075
|
+
fulltextPredicate
|
|
5076
|
+
};
|
|
5077
|
+
}
|
|
5078
|
+
});
|
|
5079
|
+
state = fulltextPass.state;
|
|
5080
|
+
const fusionPass = runCompilerPass(state, {
|
|
5081
|
+
name: "fusion_config",
|
|
5082
|
+
execute(currentState) {
|
|
5083
|
+
return runFusionConfigPass(
|
|
5084
|
+
currentState.ast,
|
|
5085
|
+
currentState.vectorPredicate,
|
|
5086
|
+
currentState.fulltextPredicate
|
|
5087
|
+
).fusion;
|
|
5088
|
+
},
|
|
5089
|
+
update(currentState, fusion) {
|
|
5090
|
+
return {
|
|
5091
|
+
...currentState,
|
|
5092
|
+
fusion
|
|
5093
|
+
};
|
|
5094
|
+
}
|
|
5095
|
+
});
|
|
5096
|
+
state = fusionPass.state;
|
|
4645
5097
|
const temporalPass = runCompilerPass(state, {
|
|
4646
5098
|
name: "temporal_filters",
|
|
4647
5099
|
execute(currentState) {
|
|
@@ -4676,7 +5128,8 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
|
|
|
4676
5128
|
execute(currentState) {
|
|
4677
5129
|
const shouldCollapseSelectiveTraversalRowset = canCollapseSelectiveTraversalRowset(
|
|
4678
5130
|
currentState.ast,
|
|
4679
|
-
currentState.vectorPredicate
|
|
5131
|
+
currentState.vectorPredicate,
|
|
5132
|
+
currentState.fulltextPredicate
|
|
4680
5133
|
);
|
|
4681
5134
|
const lastTraversal = currentState.ast.traversals.at(-1);
|
|
4682
5135
|
const collapsedTraversalCteAlias = shouldCollapseSelectiveTraversalRowset && lastTraversal !== void 0 ? `cte_${lastTraversal.nodeAlias}` : void 0;
|
|
@@ -4712,9 +5165,9 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
|
|
|
4712
5165
|
state = traversalLimitPass.state;
|
|
4713
5166
|
state = {
|
|
4714
5167
|
...state,
|
|
4715
|
-
effectiveLimit:
|
|
4716
|
-
state.ast.limit,
|
|
4717
|
-
state.
|
|
5168
|
+
effectiveLimit: resolveFulltextAwareLimit(
|
|
5169
|
+
resolveVectorAwareLimit(state.ast.limit, state.vectorPredicate),
|
|
5170
|
+
state.fulltextPredicate
|
|
4718
5171
|
)
|
|
4719
5172
|
};
|
|
4720
5173
|
const logicalPlanPass = runCompilerPass(state, {
|
|
@@ -4731,7 +5184,8 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
|
|
|
4731
5184
|
collapsedTraversalCteAlias: currentState.collapsedTraversalCteAlias
|
|
4732
5185
|
},
|
|
4733
5186
|
...currentState.effectiveLimit === void 0 ? {} : { effectiveLimit: currentState.effectiveLimit },
|
|
4734
|
-
...currentState.vectorPredicate === void 0 ? {} : { vectorPredicate: currentState.vectorPredicate }
|
|
5187
|
+
...currentState.vectorPredicate === void 0 ? {} : { vectorPredicate: currentState.vectorPredicate },
|
|
5188
|
+
...currentState.fulltextPredicate === void 0 ? {} : { fulltextPredicate: currentState.fulltextPredicate }
|
|
4735
5189
|
});
|
|
4736
5190
|
},
|
|
4737
5191
|
update(currentState, logicalPlan) {
|
|
@@ -4750,11 +5204,11 @@ function compileQuery(ast, graphId, options = "sqlite") {
|
|
|
4750
5204
|
const options_ = typeof options === "string" ? { dialect: options } : options;
|
|
4751
5205
|
const dialect = options_.dialect ?? "sqlite";
|
|
4752
5206
|
const schema = options_.schema ?? DEFAULT_SQL_SCHEMA;
|
|
4753
|
-
const adapter =
|
|
5207
|
+
const adapter = resolveDialectAdapter(dialect, options_.fulltextStrategy);
|
|
4754
5208
|
const ctx = {
|
|
4755
5209
|
dialect: adapter,
|
|
4756
5210
|
schema,
|
|
4757
|
-
compileQuery: (subAst, subGraphId) => compileQuery(subAst, subGraphId,
|
|
5211
|
+
compileQuery: (subAst, subGraphId) => compileQuery(subAst, subGraphId, propagateOptions(options_))
|
|
4758
5212
|
};
|
|
4759
5213
|
if (hasVariableLengthTraversal(ast)) {
|
|
4760
5214
|
const lowered = tryLowerSingleHopRecursiveTraversal(ast);
|
|
@@ -4790,15 +5244,29 @@ function compileSetOperation2(op, graphId, options = "sqlite") {
|
|
|
4790
5244
|
const options_ = typeof options === "string" ? { dialect: options } : options;
|
|
4791
5245
|
const dialect = options_.dialect ?? "sqlite";
|
|
4792
5246
|
const schema = options_.schema ?? DEFAULT_SQL_SCHEMA;
|
|
4793
|
-
const adapter =
|
|
5247
|
+
const adapter = resolveDialectAdapter(dialect, options_.fulltextStrategy);
|
|
4794
5248
|
return compileSetOperation(
|
|
4795
5249
|
op,
|
|
4796
5250
|
graphId,
|
|
4797
5251
|
adapter,
|
|
4798
5252
|
schema,
|
|
4799
|
-
(ast, gid) => compileQuery(ast, gid,
|
|
5253
|
+
(ast, gid) => compileQuery(ast, gid, propagateOptions(options_))
|
|
4800
5254
|
);
|
|
4801
5255
|
}
|
|
5256
|
+
function resolveDialectAdapter(dialect, fulltextStrategy) {
|
|
5257
|
+
const baseAdapter = getDialect(dialect);
|
|
5258
|
+
if (fulltextStrategy === void 0 || fulltextStrategy === baseAdapter.fulltext) {
|
|
5259
|
+
return baseAdapter;
|
|
5260
|
+
}
|
|
5261
|
+
return { ...baseAdapter, fulltext: fulltextStrategy };
|
|
5262
|
+
}
|
|
5263
|
+
function propagateOptions(options_) {
|
|
5264
|
+
return {
|
|
5265
|
+
dialect: options_.dialect ?? "sqlite",
|
|
5266
|
+
...options_.schema === void 0 ? {} : { schema: options_.schema },
|
|
5267
|
+
...options_.fulltextStrategy === void 0 ? {} : { fulltextStrategy: options_.fulltextStrategy }
|
|
5268
|
+
};
|
|
5269
|
+
}
|
|
4802
5270
|
function resolveCountAggregateFastPath(ast) {
|
|
4803
5271
|
if (ast.traversals.length !== 1) {
|
|
4804
5272
|
return void 0;
|
|
@@ -4820,8 +5288,10 @@ function resolveCountAggregateFastPath(ast) {
|
|
|
4820
5288
|
if (groupField.alias !== ast.start.alias || !isIdFieldRef(groupField)) {
|
|
4821
5289
|
return void 0;
|
|
4822
5290
|
}
|
|
4823
|
-
let
|
|
4824
|
-
let
|
|
5291
|
+
let requiresNodeCount = false;
|
|
5292
|
+
let requiresNodeCountDistinct = false;
|
|
5293
|
+
let requiresEdgeCount = false;
|
|
5294
|
+
let requiresEdgeCountDistinct = false;
|
|
4825
5295
|
for (const projectedField of ast.projection.fields) {
|
|
4826
5296
|
const source = projectedField.source;
|
|
4827
5297
|
if (!isAggregateExpr(source)) {
|
|
@@ -4830,26 +5300,35 @@ function resolveCountAggregateFastPath(ast) {
|
|
|
4830
5300
|
}
|
|
4831
5301
|
continue;
|
|
4832
5302
|
}
|
|
4833
|
-
if (
|
|
5303
|
+
if (!isIdFieldRef(source.field)) {
|
|
5304
|
+
return void 0;
|
|
5305
|
+
}
|
|
5306
|
+
const isNodeTarget = source.field.alias === traversal.nodeAlias;
|
|
5307
|
+
const isEdgeTarget = source.field.alias === traversal.edgeAlias;
|
|
5308
|
+
if (!isNodeTarget && !isEdgeTarget) {
|
|
4834
5309
|
return void 0;
|
|
4835
5310
|
}
|
|
4836
5311
|
if (source.function === "count") {
|
|
4837
|
-
|
|
5312
|
+
if (isNodeTarget) requiresNodeCount = true;
|
|
5313
|
+
else requiresEdgeCount = true;
|
|
4838
5314
|
continue;
|
|
4839
5315
|
}
|
|
4840
5316
|
if (source.function === "countDistinct") {
|
|
4841
|
-
|
|
5317
|
+
if (isNodeTarget) requiresNodeCountDistinct = true;
|
|
5318
|
+
else requiresEdgeCountDistinct = true;
|
|
4842
5319
|
continue;
|
|
4843
5320
|
}
|
|
4844
5321
|
return void 0;
|
|
4845
5322
|
}
|
|
4846
|
-
if (!
|
|
5323
|
+
if (!requiresNodeCount && !requiresNodeCountDistinct && !requiresEdgeCount && !requiresEdgeCountDistinct) {
|
|
4847
5324
|
return void 0;
|
|
4848
5325
|
}
|
|
4849
5326
|
return {
|
|
4850
5327
|
traversal,
|
|
4851
|
-
|
|
4852
|
-
|
|
5328
|
+
requiresNodeCount,
|
|
5329
|
+
requiresNodeCountDistinct,
|
|
5330
|
+
requiresEdgeCount,
|
|
5331
|
+
requiresEdgeCountDistinct
|
|
4853
5332
|
};
|
|
4854
5333
|
}
|
|
4855
5334
|
function compileCountAggregateFastPath(ast, graphId, ctx, logicalPlan, requiredColumnsByAlias, predicateIndex, temporalFilterPass) {
|
|
@@ -4857,15 +5336,23 @@ function compileCountAggregateFastPath(ast, graphId, ctx, logicalPlan, requiredC
|
|
|
4857
5336
|
if (!plan) {
|
|
4858
5337
|
return void 0;
|
|
4859
5338
|
}
|
|
4860
|
-
const {
|
|
5339
|
+
const {
|
|
5340
|
+
traversal,
|
|
5341
|
+
requiresNodeCount,
|
|
5342
|
+
requiresNodeCountDistinct,
|
|
5343
|
+
requiresEdgeCount,
|
|
5344
|
+
requiresEdgeCountDistinct
|
|
5345
|
+
} = plan;
|
|
4861
5346
|
const { dialect } = ctx;
|
|
4862
5347
|
const startAlias = ast.start.alias;
|
|
4863
5348
|
const previousAlias = traversal.joinFromAlias;
|
|
4864
5349
|
const previousAliasIdColumn = `${previousAlias}_id`;
|
|
4865
5350
|
const previousAliasKindColumn = `${previousAlias}_kind`;
|
|
4866
5351
|
const countCteAlias = `cte_${traversal.nodeAlias}_counts`;
|
|
4867
|
-
const
|
|
4868
|
-
const
|
|
5352
|
+
const nodeCountColumn = `${traversal.nodeAlias}_count`;
|
|
5353
|
+
const nodeCountDistinctColumn = `${traversal.nodeAlias}_count_distinct`;
|
|
5354
|
+
const edgeCountColumn = `${traversal.edgeAlias}_count`;
|
|
5355
|
+
const edgeCountDistinctColumn = `${traversal.edgeAlias}_count_distinct`;
|
|
4869
5356
|
const previousNodeKinds = getNodeKindsForAlias(ast, traversal.joinFromAlias);
|
|
4870
5357
|
const edgeKinds = [...new Set(traversal.edgeKinds)];
|
|
4871
5358
|
const nodeKinds = traversal.nodeKinds;
|
|
@@ -4891,34 +5378,68 @@ function compileCountAggregateFastPath(ast, graphId, ctx, logicalPlan, requiredC
|
|
|
4891
5378
|
const targetField = traversal.direction === "out" ? "to_id" : "from_id";
|
|
4892
5379
|
const joinKindField = traversal.direction === "out" ? "from_kind" : "to_kind";
|
|
4893
5380
|
const targetKindField = traversal.direction === "out" ? "to_kind" : "from_kind";
|
|
5381
|
+
const hasNodePredicates = nodePredicateClauses.length > 0;
|
|
5382
|
+
const requiresNodeJoin = requiresNodeCount || requiresNodeCountDistinct || hasNodePredicates;
|
|
5383
|
+
const useInnerJoin = hasNodePredicates;
|
|
4894
5384
|
const whereClauses = [
|
|
4895
5385
|
sql`e.graph_id = ${graphId}`,
|
|
4896
5386
|
compileKindFilter(sql.raw("e.kind"), edgeKinds),
|
|
4897
5387
|
compileKindFilter(sql.raw(`e.${joinKindField}`), previousNodeKinds),
|
|
4898
5388
|
compileKindFilter(sql.raw(`e.${targetKindField}`), nodeKinds),
|
|
4899
|
-
compileKindFilter(sql.raw("n.kind"), nodeKinds),
|
|
4900
5389
|
edgeTemporalFilter,
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
...
|
|
5390
|
+
...edgePredicateClauses,
|
|
5391
|
+
// INNER JOIN mode: node-side filters in WHERE constrain every aggregate.
|
|
5392
|
+
...requiresNodeJoin && useInnerJoin ? [
|
|
5393
|
+
compileKindFilter(sql.raw("n.kind"), nodeKinds),
|
|
5394
|
+
nodeTemporalFilter,
|
|
5395
|
+
...nodePredicateClauses
|
|
5396
|
+
] : []
|
|
4904
5397
|
];
|
|
5398
|
+
const nodeJoinOnClauses = requiresNodeJoin ? useInnerJoin ? (
|
|
5399
|
+
// INNER JOIN: only the key conditions; filters live in WHERE.
|
|
5400
|
+
[
|
|
5401
|
+
sql`n.graph_id = e.graph_id`,
|
|
5402
|
+
sql`n.id = e.${sql.raw(targetField)}`,
|
|
5403
|
+
sql`n.kind = e.${sql.raw(targetKindField)}`
|
|
5404
|
+
]
|
|
5405
|
+
) : [
|
|
5406
|
+
sql`n.graph_id = e.graph_id`,
|
|
5407
|
+
sql`n.id = e.${sql.raw(targetField)}`,
|
|
5408
|
+
sql`n.kind = e.${sql.raw(targetKindField)}`,
|
|
5409
|
+
compileKindFilter(sql.raw("n.kind"), nodeKinds),
|
|
5410
|
+
nodeTemporalFilter
|
|
5411
|
+
] : [];
|
|
4905
5412
|
const aggregateColumns = [];
|
|
4906
|
-
if (
|
|
4907
|
-
aggregateColumns.push(sql`COUNT(n.id) AS ${sql.raw(
|
|
5413
|
+
if (requiresNodeCount) {
|
|
5414
|
+
aggregateColumns.push(sql`COUNT(n.id) AS ${sql.raw(nodeCountColumn)}`);
|
|
5415
|
+
}
|
|
5416
|
+
if (requiresNodeCountDistinct) {
|
|
5417
|
+
aggregateColumns.push(
|
|
5418
|
+
sql`COUNT(DISTINCT n.id) AS ${sql.raw(nodeCountDistinctColumn)}`
|
|
5419
|
+
);
|
|
4908
5420
|
}
|
|
4909
|
-
if (
|
|
5421
|
+
if (requiresEdgeCount) {
|
|
5422
|
+
aggregateColumns.push(sql`COUNT(e.id) AS ${sql.raw(edgeCountColumn)}`);
|
|
5423
|
+
}
|
|
5424
|
+
if (requiresEdgeCountDistinct) {
|
|
4910
5425
|
aggregateColumns.push(
|
|
4911
|
-
sql`COUNT(DISTINCT
|
|
5426
|
+
sql`COUNT(DISTINCT e.id) AS ${sql.raw(edgeCountDistinctColumn)}`
|
|
4912
5427
|
);
|
|
4913
5428
|
}
|
|
5429
|
+
const startCteLimit = traversal.optional && ast.orderBy === void 0 && ast.limit !== void 0 ? { limit: ast.limit, offset: ast.offset } : void 0;
|
|
4914
5430
|
const startCte = buildStandardStartCte({
|
|
4915
5431
|
ast,
|
|
4916
5432
|
ctx,
|
|
4917
5433
|
graphId,
|
|
4918
5434
|
predicateIndex,
|
|
4919
5435
|
requiredColumnsByAlias,
|
|
4920
|
-
temporalFilterPass
|
|
5436
|
+
temporalFilterPass,
|
|
5437
|
+
...startCteLimit === void 0 ? {} : { limitOffset: startCteLimit }
|
|
4921
5438
|
});
|
|
5439
|
+
const targetNodeJoinKeyword = useInnerJoin ? "JOIN" : "LEFT JOIN";
|
|
5440
|
+
const targetNodeJoin = requiresNodeJoin ? sql`
|
|
5441
|
+
${sql.raw(targetNodeJoinKeyword)} ${ctx.schema.nodesTable} n ON ${sql.join(nodeJoinOnClauses, sql` AND `)}
|
|
5442
|
+
` : sql``;
|
|
4922
5443
|
const countCte = sql`
|
|
4923
5444
|
${sql.raw(countCteAlias)} AS (
|
|
4924
5445
|
SELECT
|
|
@@ -4927,16 +5448,20 @@ function compileCountAggregateFastPath(ast, graphId, ctx, logicalPlan, requiredC
|
|
|
4927
5448
|
${sql.join(aggregateColumns, sql`, `)}
|
|
4928
5449
|
FROM cte_${sql.raw(previousAlias)}
|
|
4929
5450
|
JOIN ${ctx.schema.edgesTable} e ON cte_${sql.raw(previousAlias)}.${sql.raw(previousAliasIdColumn)} = e.${sql.raw(joinField)}
|
|
4930
|
-
AND cte_${sql.raw(previousAlias)}.${sql.raw(previousAliasKindColumn)} = e.${sql.raw(joinKindField)}
|
|
4931
|
-
JOIN ${ctx.schema.nodesTable} n ON n.graph_id = e.graph_id
|
|
4932
|
-
AND n.id = e.${sql.raw(targetField)}
|
|
4933
|
-
AND n.kind = e.${sql.raw(targetKindField)}
|
|
5451
|
+
AND cte_${sql.raw(previousAlias)}.${sql.raw(previousAliasKindColumn)} = e.${sql.raw(joinKindField)}${targetNodeJoin}
|
|
4934
5452
|
WHERE ${sql.join(whereClauses, sql` AND `)}
|
|
4935
5453
|
GROUP BY
|
|
4936
5454
|
cte_${sql.raw(previousAlias)}.${sql.raw(previousAliasIdColumn)},
|
|
4937
5455
|
cte_${sql.raw(previousAlias)}.${sql.raw(previousAliasKindColumn)}
|
|
4938
5456
|
)
|
|
4939
5457
|
`;
|
|
5458
|
+
function resolveCountColumn(source) {
|
|
5459
|
+
const isEdge = source.field.alias === traversal.edgeAlias;
|
|
5460
|
+
if (source.function === "countDistinct") {
|
|
5461
|
+
return isEdge ? edgeCountDistinctColumn : nodeCountDistinctColumn;
|
|
5462
|
+
}
|
|
5463
|
+
return isEdge ? edgeCountColumn : nodeCountColumn;
|
|
5464
|
+
}
|
|
4940
5465
|
const projection = sql.join(
|
|
4941
5466
|
ast.projection.fields.map((projectedField) => {
|
|
4942
5467
|
const source = projectedField.source;
|
|
@@ -4947,12 +5472,12 @@ function compileCountAggregateFastPath(ast, graphId, ctx, logicalPlan, requiredC
|
|
|
4947
5472
|
source.valueType,
|
|
4948
5473
|
`cte_${source.alias}`
|
|
4949
5474
|
);
|
|
4950
|
-
return sql`${value} AS ${
|
|
5475
|
+
return sql`${value} AS ${quoteIdentifier2(projectedField.outputName)}`;
|
|
4951
5476
|
}
|
|
4952
|
-
const projectedCountColumn = source
|
|
5477
|
+
const projectedCountColumn = resolveCountColumn(source);
|
|
4953
5478
|
const countValue = sql`${sql.raw(countCteAlias)}.${sql.raw(projectedCountColumn)}`;
|
|
4954
5479
|
const aggregateValue = traversal.optional ? sql`COALESCE(${countValue}, 0)` : countValue;
|
|
4955
|
-
return sql`${aggregateValue} AS ${
|
|
5480
|
+
return sql`${aggregateValue} AS ${quoteIdentifier2(projectedField.outputName)}`;
|
|
4956
5481
|
}),
|
|
4957
5482
|
sql`, `
|
|
4958
5483
|
);
|
|
@@ -4964,19 +5489,29 @@ function compileCountAggregateFastPath(ast, graphId, ctx, logicalPlan, requiredC
|
|
|
4964
5489
|
AND ${sql.raw(countCteAlias)}.${sql.raw(previousAliasKindColumn)} = cte_${sql.raw(previousAlias)}.${sql.raw(previousAliasKindColumn)}
|
|
4965
5490
|
`;
|
|
4966
5491
|
const orderBy = buildStandardOrderBy({ ast, dialect });
|
|
4967
|
-
const
|
|
4968
|
-
|
|
4969
|
-
offset: ast.offset
|
|
4970
|
-
});
|
|
5492
|
+
const emittedLogicalPlan = startCteLimit === void 0 ? logicalPlan : stripLimitOffsetFromPlan(logicalPlan);
|
|
5493
|
+
const limitOffset = startCteLimit === void 0 ? buildLimitOffsetClause({ limit: ast.limit, offset: ast.offset }) : void 0;
|
|
4971
5494
|
return emitStandardQuerySql({
|
|
4972
5495
|
ctes: [startCte, countCte],
|
|
4973
5496
|
fromClause,
|
|
4974
5497
|
...orderBy === void 0 ? {} : { orderBy },
|
|
4975
5498
|
...limitOffset === void 0 ? {} : { limitOffset },
|
|
4976
|
-
logicalPlan,
|
|
5499
|
+
logicalPlan: emittedLogicalPlan,
|
|
4977
5500
|
projection
|
|
4978
5501
|
});
|
|
4979
5502
|
}
|
|
5503
|
+
function stripLimitOffsetFromPlan(plan) {
|
|
5504
|
+
return { ...plan, root: stripLimitOffsetFromPlanNode(plan.root) };
|
|
5505
|
+
}
|
|
5506
|
+
function stripLimitOffsetFromPlanNode(node) {
|
|
5507
|
+
if (node.op === "limit_offset") {
|
|
5508
|
+
return stripLimitOffsetFromPlanNode(node.input);
|
|
5509
|
+
}
|
|
5510
|
+
if ("input" in node) {
|
|
5511
|
+
return { ...node, input: stripLimitOffsetFromPlanNode(node.input) };
|
|
5512
|
+
}
|
|
5513
|
+
return node;
|
|
5514
|
+
}
|
|
4980
5515
|
var STANDARD_QUERY_STRATEGY_HANDLERS = {
|
|
4981
5516
|
cte_project: compileStandardQueryWithCteStrategy
|
|
4982
5517
|
};
|
|
@@ -4985,11 +5520,37 @@ function compileStandardQuery(ast, graphId, ctx) {
|
|
|
4985
5520
|
const handler = STANDARD_QUERY_STRATEGY_HANDLERS[strategy];
|
|
4986
5521
|
return handler(ast, graphId, ctx);
|
|
4987
5522
|
}
|
|
5523
|
+
function selectStandardOrderBy(input) {
|
|
5524
|
+
const {
|
|
5525
|
+
ast,
|
|
5526
|
+
collapsedTraversalCteAlias,
|
|
5527
|
+
dialect,
|
|
5528
|
+
fulltextPredicate,
|
|
5529
|
+
fusion,
|
|
5530
|
+
vectorPredicate
|
|
5531
|
+
} = input;
|
|
5532
|
+
if (vectorPredicate && fulltextPredicate) {
|
|
5533
|
+
return buildStandardHybridRrfOrderBy({ ast, dialect, fusion });
|
|
5534
|
+
}
|
|
5535
|
+
if (vectorPredicate) {
|
|
5536
|
+
return buildStandardVectorOrderBy({ ast, dialect });
|
|
5537
|
+
}
|
|
5538
|
+
if (fulltextPredicate) {
|
|
5539
|
+
return buildStandardFulltextOrderBy({ ast, dialect });
|
|
5540
|
+
}
|
|
5541
|
+
return buildStandardOrderBy({
|
|
5542
|
+
ast,
|
|
5543
|
+
...collapsedTraversalCteAlias === void 0 ? {} : { collapsedTraversalCteAlias },
|
|
5544
|
+
dialect
|
|
5545
|
+
});
|
|
5546
|
+
}
|
|
4988
5547
|
function compileStandardQueryWithCteStrategy(ast, graphId, ctx) {
|
|
4989
5548
|
const { dialect } = ctx;
|
|
4990
5549
|
const {
|
|
4991
5550
|
collapsedTraversalCteAlias,
|
|
4992
5551
|
effectiveLimit,
|
|
5552
|
+
fulltextPredicate,
|
|
5553
|
+
fusion,
|
|
4993
5554
|
logicalPlan,
|
|
4994
5555
|
predicateIndex,
|
|
4995
5556
|
requiredColumnsByAlias,
|
|
@@ -5010,7 +5571,7 @@ function compileStandardQueryWithCteStrategy(ast, graphId, ctx) {
|
|
|
5010
5571
|
{ phase: "standard-pass-pipeline" }
|
|
5011
5572
|
);
|
|
5012
5573
|
}
|
|
5013
|
-
if (!vectorPredicate) {
|
|
5574
|
+
if (!vectorPredicate && !fulltextPredicate) {
|
|
5014
5575
|
const fastPathSql = compileCountAggregateFastPath(
|
|
5015
5576
|
ast,
|
|
5016
5577
|
graphId,
|
|
@@ -5056,7 +5617,29 @@ function compileStandardQueryWithCteStrategy(ast, graphId, ctx) {
|
|
|
5056
5617
|
);
|
|
5057
5618
|
}
|
|
5058
5619
|
if (vectorPredicate) {
|
|
5059
|
-
|
|
5620
|
+
const nodeKinds = getNodeKindsForAlias(ast, vectorPredicate.field.alias);
|
|
5621
|
+
ctes.push(
|
|
5622
|
+
buildStandardEmbeddingsCte({
|
|
5623
|
+
ctx,
|
|
5624
|
+
graphId,
|
|
5625
|
+
nodeKinds,
|
|
5626
|
+
vectorPredicate
|
|
5627
|
+
})
|
|
5628
|
+
);
|
|
5629
|
+
}
|
|
5630
|
+
if (fulltextPredicate) {
|
|
5631
|
+
const nodeKinds = getNodeKindsForAlias(ast, fulltextPredicate.field.alias);
|
|
5632
|
+
ctes.push(
|
|
5633
|
+
buildStandardFulltextCte({
|
|
5634
|
+
ctx,
|
|
5635
|
+
fulltextPredicate,
|
|
5636
|
+
graphId,
|
|
5637
|
+
nodeKinds
|
|
5638
|
+
})
|
|
5639
|
+
);
|
|
5640
|
+
}
|
|
5641
|
+
if (getHybridTargetAlias(vectorPredicate, fulltextPredicate) !== void 0) {
|
|
5642
|
+
ctes.push(buildStandardHybridCandidateCte());
|
|
5060
5643
|
}
|
|
5061
5644
|
const projection = buildStandardProjection({
|
|
5062
5645
|
ast,
|
|
@@ -5066,14 +5649,18 @@ function compileStandardQueryWithCteStrategy(ast, graphId, ctx) {
|
|
|
5066
5649
|
const fromClause = buildStandardFromClause({
|
|
5067
5650
|
ast,
|
|
5068
5651
|
...collapsedTraversalCteAlias === void 0 ? {} : { collapsedTraversalCteAlias },
|
|
5069
|
-
...vectorPredicate === void 0 ? {} : { vectorPredicate }
|
|
5652
|
+
...vectorPredicate === void 0 ? {} : { vectorPredicate },
|
|
5653
|
+
...fulltextPredicate === void 0 ? {} : { fulltextPredicate }
|
|
5070
5654
|
});
|
|
5071
5655
|
const groupBy = buildStandardGroupBy({ ast, dialect });
|
|
5072
5656
|
const having2 = buildStandardHaving({ ast, ctx });
|
|
5073
|
-
const orderBy =
|
|
5657
|
+
const orderBy = selectStandardOrderBy({
|
|
5074
5658
|
ast,
|
|
5075
|
-
|
|
5076
|
-
dialect
|
|
5659
|
+
collapsedTraversalCteAlias,
|
|
5660
|
+
dialect,
|
|
5661
|
+
fulltextPredicate,
|
|
5662
|
+
fusion,
|
|
5663
|
+
vectorPredicate
|
|
5077
5664
|
});
|
|
5078
5665
|
const limitOffset = buildLimitOffsetClause({
|
|
5079
5666
|
limit: effectiveLimit,
|
|
@@ -5494,11 +6081,7 @@ function buildComparisonPredicate(field2, op, value) {
|
|
|
5494
6081
|
};
|
|
5495
6082
|
}
|
|
5496
6083
|
function buildCursorFromContext(context, orderBy, direction) {
|
|
5497
|
-
return buildCursorFromRow(
|
|
5498
|
-
context,
|
|
5499
|
-
orderBy,
|
|
5500
|
-
direction
|
|
5501
|
-
);
|
|
6084
|
+
return buildCursorFromRow(context, orderBy, direction);
|
|
5502
6085
|
}
|
|
5503
6086
|
function buildPaginatedResult(data, orderedRows, orderBy, limit, hasMore, isBackward, cursor, buildContext) {
|
|
5504
6087
|
let nextCursor;
|
|
@@ -6091,59 +6674,41 @@ function validateSqlIdentifier(alias) {
|
|
|
6091
6674
|
);
|
|
6092
6675
|
}
|
|
6093
6676
|
}
|
|
6094
|
-
function
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
{
|
|
6100
|
-
path,
|
|
6101
|
-
message: "Vector similarity predicates are only supported at top-level or inside AND groups."
|
|
6102
|
-
}
|
|
6103
|
-
]
|
|
6104
|
-
},
|
|
6105
|
-
{
|
|
6106
|
-
suggestion: "Rewrite the predicate to keep vector similarity at top-level or combine with additional filters using AND."
|
|
6107
|
-
}
|
|
6108
|
-
);
|
|
6109
|
-
}
|
|
6110
|
-
function validateVectorPredicateExpression(expression, inDisallowedBranch, path) {
|
|
6677
|
+
function validateStructuralPredicatePlacement(expression, matchType, buildError, inDisallowedBranch, path) {
|
|
6678
|
+
if (expression.__type === matchType) {
|
|
6679
|
+
if (inDisallowedBranch) buildError(path);
|
|
6680
|
+
return;
|
|
6681
|
+
}
|
|
6111
6682
|
switch (expression.__type) {
|
|
6112
|
-
case "vector_similarity": {
|
|
6113
|
-
if (inDisallowedBranch) {
|
|
6114
|
-
throwInvalidVectorPredicatePlacement(path);
|
|
6115
|
-
}
|
|
6116
|
-
return;
|
|
6117
|
-
}
|
|
6118
6683
|
case "and": {
|
|
6119
|
-
for (const [
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
predicate2,
|
|
6684
|
+
for (const [index, child] of expression.predicates.entries()) {
|
|
6685
|
+
validateStructuralPredicatePlacement(
|
|
6686
|
+
child,
|
|
6687
|
+
matchType,
|
|
6688
|
+
buildError,
|
|
6125
6689
|
inDisallowedBranch,
|
|
6126
|
-
`${path}.predicates[${
|
|
6690
|
+
`${path}.predicates[${index}]`
|
|
6127
6691
|
);
|
|
6128
6692
|
}
|
|
6129
6693
|
return;
|
|
6130
6694
|
}
|
|
6131
6695
|
case "or": {
|
|
6132
|
-
for (const [
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
predicate2,
|
|
6696
|
+
for (const [index, child] of expression.predicates.entries()) {
|
|
6697
|
+
validateStructuralPredicatePlacement(
|
|
6698
|
+
child,
|
|
6699
|
+
matchType,
|
|
6700
|
+
buildError,
|
|
6138
6701
|
true,
|
|
6139
|
-
`${path}.predicates[${
|
|
6702
|
+
`${path}.predicates[${index}]`
|
|
6140
6703
|
);
|
|
6141
6704
|
}
|
|
6142
6705
|
return;
|
|
6143
6706
|
}
|
|
6144
6707
|
case "not": {
|
|
6145
|
-
|
|
6708
|
+
validateStructuralPredicatePlacement(
|
|
6146
6709
|
expression.predicate,
|
|
6710
|
+
matchType,
|
|
6711
|
+
buildError,
|
|
6147
6712
|
true,
|
|
6148
6713
|
`${path}.predicate`
|
|
6149
6714
|
);
|
|
@@ -6157,24 +6722,108 @@ function validateVectorPredicateExpression(expression, inDisallowedBranch, path)
|
|
|
6157
6722
|
case "object_op":
|
|
6158
6723
|
case "aggregate_comparison":
|
|
6159
6724
|
case "exists":
|
|
6160
|
-
case "in_subquery":
|
|
6725
|
+
case "in_subquery":
|
|
6726
|
+
case "vector_similarity":
|
|
6727
|
+
case "fulltext_match": {
|
|
6161
6728
|
return;
|
|
6162
6729
|
}
|
|
6163
6730
|
}
|
|
6164
6731
|
}
|
|
6732
|
+
function throwInvalidVectorPredicatePlacement(path) {
|
|
6733
|
+
throw new ValidationError(
|
|
6734
|
+
"Vector similarity predicates cannot be nested under OR or NOT. Use top-level AND combinations instead.",
|
|
6735
|
+
{
|
|
6736
|
+
issues: [
|
|
6737
|
+
{
|
|
6738
|
+
path,
|
|
6739
|
+
message: "Vector similarity predicates are only supported at top-level or inside AND groups."
|
|
6740
|
+
}
|
|
6741
|
+
]
|
|
6742
|
+
},
|
|
6743
|
+
{
|
|
6744
|
+
suggestion: "Rewrite the predicate to keep vector similarity at top-level or combine with additional filters using AND."
|
|
6745
|
+
}
|
|
6746
|
+
);
|
|
6747
|
+
}
|
|
6748
|
+
function throwInvalidFulltextPredicatePlacement(path) {
|
|
6749
|
+
throw new ValidationError(
|
|
6750
|
+
"Fulltext match predicates (.matches()) cannot be nested under OR or NOT. Use top-level AND combinations instead.",
|
|
6751
|
+
{
|
|
6752
|
+
issues: [
|
|
6753
|
+
{
|
|
6754
|
+
path,
|
|
6755
|
+
message: "Fulltext match predicates are only supported at top-level or inside AND groups."
|
|
6756
|
+
}
|
|
6757
|
+
]
|
|
6758
|
+
},
|
|
6759
|
+
{
|
|
6760
|
+
suggestion: "Restructure your query so the fulltext match appears at the top level of a whereNode() or inside an .and() combination."
|
|
6761
|
+
}
|
|
6762
|
+
);
|
|
6763
|
+
}
|
|
6165
6764
|
function validateVectorPredicatePlacement(predicates) {
|
|
6166
|
-
for (const [
|
|
6167
|
-
|
|
6765
|
+
for (const [index, predicate2] of predicates.entries()) {
|
|
6766
|
+
validateStructuralPredicatePlacement(
|
|
6767
|
+
predicate2.expression,
|
|
6768
|
+
"vector_similarity",
|
|
6769
|
+
throwInvalidVectorPredicatePlacement,
|
|
6770
|
+
false,
|
|
6771
|
+
`predicates[${index}].expression`
|
|
6772
|
+
);
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6775
|
+
function validateFulltextPredicatePlacement(predicates) {
|
|
6776
|
+
for (const [index, predicate2] of predicates.entries()) {
|
|
6777
|
+
validateStructuralPredicatePlacement(
|
|
6168
6778
|
predicate2.expression,
|
|
6779
|
+
"fulltext_match",
|
|
6780
|
+
throwInvalidFulltextPredicatePlacement,
|
|
6169
6781
|
false,
|
|
6170
|
-
`predicates[${
|
|
6782
|
+
`predicates[${index}].expression`
|
|
6783
|
+
);
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
function validateHybridFusionOptions(options) {
|
|
6787
|
+
const method = options.method;
|
|
6788
|
+
if (method !== void 0 && method !== "rrf") {
|
|
6789
|
+
throw new ValidationError(`Unsupported fusion method: ${method}`, {
|
|
6790
|
+
issues: [{ path: "fusion.method", message: `Only "rrf" is supported.` }]
|
|
6791
|
+
});
|
|
6792
|
+
}
|
|
6793
|
+
const { k } = options;
|
|
6794
|
+
if (k !== void 0 && (!Number.isFinite(k) || k <= 0)) {
|
|
6795
|
+
throw new ValidationError(
|
|
6796
|
+
`Fusion k must be a positive finite number, got: ${String(k)}`,
|
|
6797
|
+
{
|
|
6798
|
+
issues: [{ path: "fusion.k", message: "Must be a positive number." }]
|
|
6799
|
+
}
|
|
6171
6800
|
);
|
|
6172
6801
|
}
|
|
6802
|
+
const weights = options.weights;
|
|
6803
|
+
if (weights !== void 0) {
|
|
6804
|
+
for (const key of ["vector", "fulltext"]) {
|
|
6805
|
+
const weight = weights[key];
|
|
6806
|
+
if (weight !== void 0 && (!Number.isFinite(weight) || weight < 0)) {
|
|
6807
|
+
throw new ValidationError(
|
|
6808
|
+
`Fusion weight for "${key}" must be a non-negative finite number, got: ${String(weight)}`,
|
|
6809
|
+
{
|
|
6810
|
+
issues: [
|
|
6811
|
+
{
|
|
6812
|
+
path: `fusion.weights.${key}`,
|
|
6813
|
+
message: "Must be a non-negative number."
|
|
6814
|
+
}
|
|
6815
|
+
]
|
|
6816
|
+
}
|
|
6817
|
+
);
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
}
|
|
6173
6821
|
}
|
|
6174
6822
|
|
|
6175
6823
|
// src/query/builder/ast-builder.ts
|
|
6176
6824
|
function buildQueryAst(config, state) {
|
|
6177
6825
|
validateVectorPredicatePlacement(state.predicates);
|
|
6826
|
+
validateFulltextPredicatePlacement(state.predicates);
|
|
6178
6827
|
const temporalMode = {
|
|
6179
6828
|
mode: state.temporalMode
|
|
6180
6829
|
};
|
|
@@ -6198,7 +6847,18 @@ function buildQueryAst(config, state) {
|
|
|
6198
6847
|
...state.limit !== void 0 && { limit: state.limit },
|
|
6199
6848
|
...state.offset !== void 0 && { offset: state.offset },
|
|
6200
6849
|
...state.groupBy !== void 0 && { groupBy: state.groupBy },
|
|
6201
|
-
...state.having !== void 0 && { having: state.having }
|
|
6850
|
+
...state.having !== void 0 && { having: state.having },
|
|
6851
|
+
...state.fusion !== void 0 && { fusion: state.fusion }
|
|
6852
|
+
};
|
|
6853
|
+
}
|
|
6854
|
+
|
|
6855
|
+
// src/query/builder/compile-options.ts
|
|
6856
|
+
function buildCompileOptions(config) {
|
|
6857
|
+
const fulltextStrategy = config.backend?.fulltextStrategy;
|
|
6858
|
+
return {
|
|
6859
|
+
dialect: config.dialect ?? "sqlite",
|
|
6860
|
+
schema: config.schema,
|
|
6861
|
+
...fulltextStrategy === void 0 ? {} : { fulltextStrategy }
|
|
6202
6862
|
};
|
|
6203
6863
|
}
|
|
6204
6864
|
function toLiteral(value) {
|
|
@@ -6300,7 +6960,8 @@ function substitutePredicateExpression(expr, bindings) {
|
|
|
6300
6960
|
case "array_op":
|
|
6301
6961
|
case "object_op":
|
|
6302
6962
|
case "aggregate_comparison":
|
|
6303
|
-
case "vector_similarity":
|
|
6963
|
+
case "vector_similarity":
|
|
6964
|
+
case "fulltext_match": {
|
|
6304
6965
|
return expr;
|
|
6305
6966
|
}
|
|
6306
6967
|
case "exists": {
|
|
@@ -6552,7 +7213,8 @@ function collectParameterMetadataFromExpression(expression, names, stringOpParam
|
|
|
6552
7213
|
case "array_op":
|
|
6553
7214
|
case "object_op":
|
|
6554
7215
|
case "aggregate_comparison":
|
|
6555
|
-
case "vector_similarity":
|
|
7216
|
+
case "vector_similarity":
|
|
7217
|
+
case "fulltext_match": {
|
|
6556
7218
|
return;
|
|
6557
7219
|
}
|
|
6558
7220
|
case "exists": {
|
|
@@ -6907,10 +7569,7 @@ var ExecutableQuery = class _ExecutableQuery {
|
|
|
6907
7569
|
* Builds compile options from the config.
|
|
6908
7570
|
*/
|
|
6909
7571
|
#compileOptions() {
|
|
6910
|
-
return
|
|
6911
|
-
dialect: this.#config.dialect ?? "sqlite",
|
|
6912
|
-
schema: this.#config.schema
|
|
6913
|
-
};
|
|
7572
|
+
return buildCompileOptions(this.#config);
|
|
6914
7573
|
}
|
|
6915
7574
|
#requireBackend() {
|
|
6916
7575
|
const { backend } = this.#config;
|
|
@@ -7625,14 +8284,8 @@ var ExecutableAggregateQuery = class _ExecutableAggregateQuery {
|
|
|
7625
8284
|
this.#cachedCompiled = compiled;
|
|
7626
8285
|
return compiled;
|
|
7627
8286
|
}
|
|
7628
|
-
/**
|
|
7629
|
-
* Builds compile options from the config.
|
|
7630
|
-
*/
|
|
7631
8287
|
#compileOptions() {
|
|
7632
|
-
return
|
|
7633
|
-
dialect: this.#config.dialect ?? "sqlite",
|
|
7634
|
-
schema: this.#config.schema
|
|
7635
|
-
};
|
|
8288
|
+
return buildCompileOptions(this.#config);
|
|
7636
8289
|
}
|
|
7637
8290
|
/**
|
|
7638
8291
|
* Executes the query and returns typed results.
|
|
@@ -8359,6 +9012,36 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
8359
9012
|
having: predicate2
|
|
8360
9013
|
});
|
|
8361
9014
|
}
|
|
9015
|
+
/**
|
|
9016
|
+
* Sets fusion parameters for hybrid (vector + fulltext) queries.
|
|
9017
|
+
*
|
|
9018
|
+
* Applies only when the query contains both a `.similarTo()` and a
|
|
9019
|
+
* `.$fulltext.matches()` predicate. Without this call, the default is
|
|
9020
|
+
* RRF with k=60 and equal weights. A mismatch between `.fuseWith()`
|
|
9021
|
+
* configuration and the predicates on the query is caught during
|
|
9022
|
+
* compilation, not here.
|
|
9023
|
+
*
|
|
9024
|
+
* @example
|
|
9025
|
+
* ```typescript
|
|
9026
|
+
* store.query()
|
|
9027
|
+
* .from("Document", "d")
|
|
9028
|
+
* .whereNode("d", d =>
|
|
9029
|
+
* d.$fulltext.matches("renewable energy", 50)
|
|
9030
|
+
* .and(d.embedding.similarTo(vec, 50))
|
|
9031
|
+
* .and(d.tenantId.eq(tenant))
|
|
9032
|
+
* )
|
|
9033
|
+
* .fuseWith({ k: 60, weights: { fulltext: 1.5 } })
|
|
9034
|
+
* .limit(10)
|
|
9035
|
+
* .execute();
|
|
9036
|
+
* ```
|
|
9037
|
+
*/
|
|
9038
|
+
fuseWith(options) {
|
|
9039
|
+
validateHybridFusionOptions(options);
|
|
9040
|
+
return new _QueryBuilder(this.#config, {
|
|
9041
|
+
...this.#state,
|
|
9042
|
+
fusion: options
|
|
9043
|
+
});
|
|
9044
|
+
}
|
|
8362
9045
|
/**
|
|
8363
9046
|
* Applies a query fragment to transform this builder.
|
|
8364
9047
|
*
|
|
@@ -8417,35 +9100,7 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
8417
9100
|
return this.#buildFieldBuilderForTypeInfo(ref, typeInfo);
|
|
8418
9101
|
}
|
|
8419
9102
|
#buildFieldBuilderForTypeInfo(ref, typeInfo) {
|
|
8420
|
-
|
|
8421
|
-
return baseField(ref);
|
|
8422
|
-
}
|
|
8423
|
-
switch (typeInfo.valueType) {
|
|
8424
|
-
case "string": {
|
|
8425
|
-
return stringField(ref);
|
|
8426
|
-
}
|
|
8427
|
-
case "number": {
|
|
8428
|
-
return numberField(ref);
|
|
8429
|
-
}
|
|
8430
|
-
case "boolean": {
|
|
8431
|
-
return baseField(ref);
|
|
8432
|
-
}
|
|
8433
|
-
case "date": {
|
|
8434
|
-
return dateField(ref);
|
|
8435
|
-
}
|
|
8436
|
-
case "array": {
|
|
8437
|
-
return arrayField(ref);
|
|
8438
|
-
}
|
|
8439
|
-
case "object": {
|
|
8440
|
-
return objectField(ref, { typeInfo });
|
|
8441
|
-
}
|
|
8442
|
-
case "embedding": {
|
|
8443
|
-
return embeddingField(ref);
|
|
8444
|
-
}
|
|
8445
|
-
case "unknown": {
|
|
8446
|
-
return baseField(ref);
|
|
8447
|
-
}
|
|
8448
|
-
}
|
|
9103
|
+
return buildFieldBuilderForTypeInfo(ref, typeInfo);
|
|
8449
9104
|
}
|
|
8450
9105
|
#createNodeAccessor(alias) {
|
|
8451
9106
|
const kindNames = this.#getKindNamesForAlias(alias);
|
|
@@ -8455,6 +9110,10 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
8455
9110
|
const kindAccessor = stringField(
|
|
8456
9111
|
fieldRef(alias, ["kind"], { valueType: "string" })
|
|
8457
9112
|
);
|
|
9113
|
+
const fulltextAccessor = createFulltextAccessor(
|
|
9114
|
+
alias,
|
|
9115
|
+
() => this.#hasSearchableField(kindNames)
|
|
9116
|
+
);
|
|
8458
9117
|
return new Proxy({}, {
|
|
8459
9118
|
get: (_, property) => {
|
|
8460
9119
|
if (typeof property === "symbol") return;
|
|
@@ -8462,10 +9121,15 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
8462
9121
|
if (property === "toJSON") return;
|
|
8463
9122
|
if (property === "id") return idAccessor;
|
|
8464
9123
|
if (property === "kind") return kindAccessor;
|
|
9124
|
+
if (property === "$fulltext") return fulltextAccessor;
|
|
8465
9125
|
return this.#getFieldBuilderForProperty(kindNames, property, alias);
|
|
8466
9126
|
}
|
|
8467
9127
|
});
|
|
8468
9128
|
}
|
|
9129
|
+
#hasSearchableField(kindNames) {
|
|
9130
|
+
if (!kindNames) return false;
|
|
9131
|
+
return this.#config.schemaIntrospector.hasSearchableField(kindNames);
|
|
9132
|
+
}
|
|
8469
9133
|
#expandTraversalEdgeKinds(edgeKind, includeImplyingEdges) {
|
|
8470
9134
|
return includeImplyingEdges ? this.#config.registry.expandImplyingEdges(edgeKind) : [edgeKind];
|
|
8471
9135
|
}
|
|
@@ -8691,14 +9355,8 @@ var UnionableQuery = class _UnionableQuery {
|
|
|
8691
9355
|
this.#cachedCompiled = compiled;
|
|
8692
9356
|
return compiled;
|
|
8693
9357
|
}
|
|
8694
|
-
/**
|
|
8695
|
-
* Builds compile options from the config.
|
|
8696
|
-
*/
|
|
8697
9358
|
#compileOptions() {
|
|
8698
|
-
return
|
|
8699
|
-
dialect: this.#config.dialect ?? "sqlite",
|
|
8700
|
-
schema: this.#config.schema
|
|
8701
|
-
};
|
|
9359
|
+
return buildCompileOptions(this.#config);
|
|
8702
9360
|
}
|
|
8703
9361
|
/**
|
|
8704
9362
|
* Executes the combined query.
|
|
@@ -8778,6 +9436,12 @@ function countDistinct(alias, field2) {
|
|
|
8778
9436
|
}
|
|
8779
9437
|
};
|
|
8780
9438
|
}
|
|
9439
|
+
function countEdges(edgeAlias) {
|
|
9440
|
+
return count(edgeAlias);
|
|
9441
|
+
}
|
|
9442
|
+
function countDistinctEdges(edgeAlias) {
|
|
9443
|
+
return countDistinct(edgeAlias);
|
|
9444
|
+
}
|
|
8781
9445
|
function sum(alias, field2) {
|
|
8782
9446
|
return {
|
|
8783
9447
|
__type: "aggregate",
|
|
@@ -8914,12 +9578,8 @@ function offsetFragment(n) {
|
|
|
8914
9578
|
}
|
|
8915
9579
|
|
|
8916
9580
|
// src/query/builder/index.ts
|
|
8917
|
-
setQueryBuilderClass(
|
|
8918
|
-
|
|
8919
|
-
);
|
|
8920
|
-
setUnionableQueryClass(
|
|
8921
|
-
UnionableQuery
|
|
8922
|
-
);
|
|
9581
|
+
setQueryBuilderClass(QueryBuilder);
|
|
9582
|
+
setUnionableQueryClass(UnionableQuery);
|
|
8923
9583
|
|
|
8924
9584
|
// src/query/builder.ts
|
|
8925
9585
|
function createQueryBuilder(graphId, registry, options) {
|
|
@@ -8950,7 +9610,8 @@ function createQueryBuilder(graphId, registry, options) {
|
|
|
8950
9610
|
temporalMode: "current",
|
|
8951
9611
|
asOf: void 0,
|
|
8952
9612
|
groupBy: void 0,
|
|
8953
|
-
having: void 0
|
|
9613
|
+
having: void 0,
|
|
9614
|
+
fusion: void 0
|
|
8954
9615
|
};
|
|
8955
9616
|
return new QueryBuilder(config, initialState);
|
|
8956
9617
|
}
|
|
@@ -9291,20 +9952,13 @@ function createEdgeCollection(config) {
|
|
|
9291
9952
|
const result = {};
|
|
9292
9953
|
if (options?.matchOn !== void 0)
|
|
9293
9954
|
result.matchOn = options.matchOn;
|
|
9294
|
-
if (options?.props !== void 0)
|
|
9295
|
-
result.props = options.props;
|
|
9955
|
+
if (options?.props !== void 0) result.props = options.props;
|
|
9296
9956
|
return result;
|
|
9297
9957
|
}
|
|
9298
9958
|
return {
|
|
9299
9959
|
async create(from, to, props, options) {
|
|
9300
9960
|
const result = await executeEdgeCreate2(
|
|
9301
|
-
buildCreateEdgeInput(
|
|
9302
|
-
kind,
|
|
9303
|
-
from,
|
|
9304
|
-
to,
|
|
9305
|
-
props ?? {},
|
|
9306
|
-
options
|
|
9307
|
-
),
|
|
9961
|
+
buildCreateEdgeInput(kind, from, to, props ?? {}, options),
|
|
9308
9962
|
backend
|
|
9309
9963
|
);
|
|
9310
9964
|
return narrowEdge(result);
|
|
@@ -9430,10 +10084,7 @@ function createEdgeCollection(config) {
|
|
|
9430
10084
|
return backend.countEdgesByKind(params);
|
|
9431
10085
|
},
|
|
9432
10086
|
async bulkCreate(items) {
|
|
9433
|
-
const batchInputs = mapBulkEdgeInputs(
|
|
9434
|
-
kind,
|
|
9435
|
-
items
|
|
9436
|
-
);
|
|
10087
|
+
const batchInputs = mapBulkEdgeInputs(kind, items);
|
|
9437
10088
|
if (backend.capabilities.transactions && "transaction" in backend) {
|
|
9438
10089
|
const results2 = await backend.transaction(
|
|
9439
10090
|
async (txBackend) => executeEdgeCreateBatch2(batchInputs, txBackend)
|
|
@@ -9469,11 +10120,7 @@ function createEdgeCollection(config) {
|
|
|
9469
10120
|
if (existing) {
|
|
9470
10121
|
toUpdate.push({
|
|
9471
10122
|
index: itemIndex,
|
|
9472
|
-
input: buildUpdateEdgeInput(
|
|
9473
|
-
item.id,
|
|
9474
|
-
item.props ?? {},
|
|
9475
|
-
item
|
|
9476
|
-
),
|
|
10123
|
+
input: buildUpdateEdgeInput(item.id, item.props ?? {}, item),
|
|
9477
10124
|
clearDeleted: existing.deleted_at !== void 0
|
|
9478
10125
|
});
|
|
9479
10126
|
} else {
|
|
@@ -9512,10 +10159,7 @@ function createEdgeCollection(config) {
|
|
|
9512
10159
|
return upsertAll(backend);
|
|
9513
10160
|
},
|
|
9514
10161
|
async bulkInsert(items) {
|
|
9515
|
-
const batchInputs = mapBulkEdgeInputs(
|
|
9516
|
-
kind,
|
|
9517
|
-
items
|
|
9518
|
-
);
|
|
10162
|
+
const batchInputs = mapBulkEdgeInputs(kind, items);
|
|
9519
10163
|
if (backend.capabilities.transactions && "transaction" in backend) {
|
|
9520
10164
|
await backend.transaction(async (txBackend) => {
|
|
9521
10165
|
await executeEdgeCreateNoReturnBatch2(batchInputs, txBackend);
|
|
@@ -9666,11 +10310,7 @@ function createNodeCollection(config) {
|
|
|
9666
10310
|
async getByIds(ids, options) {
|
|
9667
10311
|
if (ids.length === 0) return [];
|
|
9668
10312
|
if (backend.getNodes !== void 0) {
|
|
9669
|
-
const rows = await backend.getNodes(
|
|
9670
|
-
graphId,
|
|
9671
|
-
kind,
|
|
9672
|
-
ids
|
|
9673
|
-
);
|
|
10313
|
+
const rows = await backend.getNodes(graphId, kind, ids);
|
|
9674
10314
|
const rowMap = /* @__PURE__ */ new Map();
|
|
9675
10315
|
for (const row of rows) {
|
|
9676
10316
|
rowMap.set(row.id, row);
|
|
@@ -9751,11 +10391,7 @@ function createNodeCollection(config) {
|
|
|
9751
10391
|
return backend.countNodesByKind(params);
|
|
9752
10392
|
},
|
|
9753
10393
|
async upsertById(id, props, options) {
|
|
9754
|
-
return this.upsertByIdFromRecord(
|
|
9755
|
-
id,
|
|
9756
|
-
props,
|
|
9757
|
-
options
|
|
9758
|
-
);
|
|
10394
|
+
return this.upsertByIdFromRecord(id, props, options);
|
|
9759
10395
|
},
|
|
9760
10396
|
async upsertByIdFromRecord(id, data, options) {
|
|
9761
10397
|
const existing = await backend.getNode(graphId, kind, id);
|
|
@@ -9775,10 +10411,7 @@ function createNodeCollection(config) {
|
|
|
9775
10411
|
return narrowNode(result);
|
|
9776
10412
|
},
|
|
9777
10413
|
async bulkCreate(items) {
|
|
9778
|
-
const batchInputs = mapBulkNodeInputs(
|
|
9779
|
-
kind,
|
|
9780
|
-
items
|
|
9781
|
-
);
|
|
10414
|
+
const batchInputs = mapBulkNodeInputs(kind, items);
|
|
9782
10415
|
if (backend.capabilities.transactions && "transaction" in backend) {
|
|
9783
10416
|
const results2 = await backend.transaction(
|
|
9784
10417
|
async (txBackend) => executeNodeCreateBatch2(batchInputs, txBackend)
|
|
@@ -9801,11 +10434,7 @@ function createNodeCollection(config) {
|
|
|
9801
10434
|
if (row !== void 0) existingMap.set(row.id, row);
|
|
9802
10435
|
}
|
|
9803
10436
|
} else {
|
|
9804
|
-
const rows = await target.getNodes(
|
|
9805
|
-
graphId,
|
|
9806
|
-
kind,
|
|
9807
|
-
ids
|
|
9808
|
-
);
|
|
10437
|
+
const rows = await target.getNodes(graphId, kind, ids);
|
|
9809
10438
|
for (const row of rows) {
|
|
9810
10439
|
existingMap.set(row.id, row);
|
|
9811
10440
|
}
|
|
@@ -9818,22 +10447,13 @@ function createNodeCollection(config) {
|
|
|
9818
10447
|
if (existing) {
|
|
9819
10448
|
toUpdate.push({
|
|
9820
10449
|
index: itemIndex,
|
|
9821
|
-
input: buildUpdateInput(
|
|
9822
|
-
kind,
|
|
9823
|
-
item.id,
|
|
9824
|
-
item.props,
|
|
9825
|
-
item
|
|
9826
|
-
),
|
|
10450
|
+
input: buildUpdateInput(kind, item.id, item.props, item),
|
|
9827
10451
|
clearDeleted: existing.deleted_at !== void 0
|
|
9828
10452
|
});
|
|
9829
10453
|
} else {
|
|
9830
10454
|
toCreate.push({
|
|
9831
10455
|
index: itemIndex,
|
|
9832
|
-
input: buildCreateInput(
|
|
9833
|
-
kind,
|
|
9834
|
-
item.props,
|
|
9835
|
-
item
|
|
9836
|
-
)
|
|
10456
|
+
input: buildCreateInput(kind, item.props, item)
|
|
9837
10457
|
});
|
|
9838
10458
|
}
|
|
9839
10459
|
itemIndex++;
|
|
@@ -9860,10 +10480,7 @@ function createNodeCollection(config) {
|
|
|
9860
10480
|
return upsertAll(backend);
|
|
9861
10481
|
},
|
|
9862
10482
|
async bulkInsert(items) {
|
|
9863
|
-
const batchInputs = mapBulkNodeInputs(
|
|
9864
|
-
kind,
|
|
9865
|
-
items
|
|
9866
|
-
);
|
|
10483
|
+
const batchInputs = mapBulkNodeInputs(kind, items);
|
|
9867
10484
|
if (backend.capabilities.transactions && "transaction" in backend) {
|
|
9868
10485
|
await backend.transaction(async (txBackend) => {
|
|
9869
10486
|
await executeNodeCreateNoReturnBatch2(batchInputs, txBackend);
|
|
@@ -10969,15 +11586,19 @@ async function executeEdgeBulkGetOrCreateByEndpoints(ctx, kind, items, backend,
|
|
|
10969
11586
|
}
|
|
10970
11587
|
|
|
10971
11588
|
// src/store/embedding-sync.ts
|
|
11589
|
+
var embeddingFieldsCache = /* @__PURE__ */ new WeakMap();
|
|
10972
11590
|
function getEmbeddingFields(schema) {
|
|
10973
|
-
|
|
10974
|
-
|
|
10975
|
-
|
|
11591
|
+
const cached = embeddingFieldsCache.get(schema);
|
|
11592
|
+
if (cached) return cached;
|
|
11593
|
+
const fields = computeEmbeddingFields(schema);
|
|
11594
|
+
embeddingFieldsCache.set(schema, fields);
|
|
11595
|
+
return fields;
|
|
11596
|
+
}
|
|
11597
|
+
function computeEmbeddingFields(schema) {
|
|
11598
|
+
if (schema.type !== "object") return [];
|
|
10976
11599
|
const def = schema.def;
|
|
10977
11600
|
const shape = def.shape;
|
|
10978
|
-
if (!shape)
|
|
10979
|
-
return [];
|
|
10980
|
-
}
|
|
11601
|
+
if (!shape) return [];
|
|
10981
11602
|
const fields = [];
|
|
10982
11603
|
for (const [fieldPath, fieldSchema] of Object.entries(shape)) {
|
|
10983
11604
|
const dimensions = getEmbeddingDimensionsFromField(fieldSchema);
|
|
@@ -11064,6 +11685,101 @@ function isValidEmbeddingValue(value) {
|
|
|
11064
11685
|
return value.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
11065
11686
|
}
|
|
11066
11687
|
|
|
11688
|
+
// src/store/fulltext-sync.ts
|
|
11689
|
+
var searchableFieldsCache = /* @__PURE__ */ new WeakMap();
|
|
11690
|
+
function getSearchableFields(schema) {
|
|
11691
|
+
const cached = searchableFieldsCache.get(schema);
|
|
11692
|
+
if (cached) return cached;
|
|
11693
|
+
const fields = computeSearchableFields(schema);
|
|
11694
|
+
searchableFieldsCache.set(schema, fields);
|
|
11695
|
+
return fields;
|
|
11696
|
+
}
|
|
11697
|
+
function computeSearchableFields(schema) {
|
|
11698
|
+
if (schema.type !== "object") return [];
|
|
11699
|
+
const def = schema.def;
|
|
11700
|
+
const shape = def.shape;
|
|
11701
|
+
if (!shape) return [];
|
|
11702
|
+
const fields = [];
|
|
11703
|
+
for (const [fieldPath, fieldSchema] of Object.entries(shape)) {
|
|
11704
|
+
const metadata = getSearchableMetadata(fieldSchema);
|
|
11705
|
+
if (metadata !== void 0) {
|
|
11706
|
+
fields.push({ fieldPath, metadata });
|
|
11707
|
+
}
|
|
11708
|
+
}
|
|
11709
|
+
warnIfConflictingLanguages(fields);
|
|
11710
|
+
return fields;
|
|
11711
|
+
}
|
|
11712
|
+
function warnIfConflictingLanguages(fields) {
|
|
11713
|
+
if (fields.length < 2) return;
|
|
11714
|
+
const languages = new Set(fields.map((field2) => field2.metadata.language));
|
|
11715
|
+
if (languages.size < 2) return;
|
|
11716
|
+
if (typeof console === "undefined" || typeof console.warn !== "function") {
|
|
11717
|
+
return;
|
|
11718
|
+
}
|
|
11719
|
+
const fieldSummary = fields.map((field2) => `${field2.fieldPath}=${field2.metadata.language}`).join(", ");
|
|
11720
|
+
const winning = fields[0]?.metadata.language ?? DEFAULT_SEARCHABLE_LANGUAGE;
|
|
11721
|
+
console.warn(
|
|
11722
|
+
`[typegraph] searchable() fields declare conflicting languages (${fieldSummary}). The first field's language ("${winning}") is recorded on the fulltext row; true multilingual indexing requires a dedicated node kind per language.`
|
|
11723
|
+
);
|
|
11724
|
+
}
|
|
11725
|
+
var FIELD_SEPARATOR = "\n";
|
|
11726
|
+
function resolveCombinedLanguage(fields) {
|
|
11727
|
+
return fields[0]?.metadata.language ?? DEFAULT_SEARCHABLE_LANGUAGE;
|
|
11728
|
+
}
|
|
11729
|
+
function computeFulltextContent(schema, props) {
|
|
11730
|
+
const searchableFields = getSearchableFields(schema);
|
|
11731
|
+
if (searchableFields.length === 0) return void 0;
|
|
11732
|
+
const parts = [];
|
|
11733
|
+
for (const field2 of searchableFields) {
|
|
11734
|
+
const value = props[field2.fieldPath];
|
|
11735
|
+
if (typeof value === "string" && value.length > 0) {
|
|
11736
|
+
parts.push(value);
|
|
11737
|
+
}
|
|
11738
|
+
}
|
|
11739
|
+
if (parts.length === 0) return void 0;
|
|
11740
|
+
return {
|
|
11741
|
+
content: parts.join(FIELD_SEPARATOR),
|
|
11742
|
+
language: resolveCombinedLanguage(searchableFields)
|
|
11743
|
+
};
|
|
11744
|
+
}
|
|
11745
|
+
async function syncFulltext(ctx, schema, props) {
|
|
11746
|
+
const { backend } = ctx;
|
|
11747
|
+
if (!backend.upsertFulltext || !backend.deleteFulltext) {
|
|
11748
|
+
return;
|
|
11749
|
+
}
|
|
11750
|
+
const computed = computeFulltextContent(schema, props);
|
|
11751
|
+
if (computed !== void 0) {
|
|
11752
|
+
await backend.upsertFulltext({
|
|
11753
|
+
graphId: ctx.graphId,
|
|
11754
|
+
nodeKind: ctx.nodeKind,
|
|
11755
|
+
nodeId: ctx.nodeId,
|
|
11756
|
+
content: computed.content,
|
|
11757
|
+
language: computed.language
|
|
11758
|
+
});
|
|
11759
|
+
return;
|
|
11760
|
+
}
|
|
11761
|
+
if (getSearchableFields(schema).length === 0) return;
|
|
11762
|
+
await backend.deleteFulltext({
|
|
11763
|
+
graphId: ctx.graphId,
|
|
11764
|
+
nodeKind: ctx.nodeKind,
|
|
11765
|
+
nodeId: ctx.nodeId
|
|
11766
|
+
});
|
|
11767
|
+
}
|
|
11768
|
+
async function deleteNodeFulltext(ctx, schema) {
|
|
11769
|
+
const { backend } = ctx;
|
|
11770
|
+
if (!backend.deleteFulltext) {
|
|
11771
|
+
return;
|
|
11772
|
+
}
|
|
11773
|
+
if (getSearchableFields(schema).length === 0) {
|
|
11774
|
+
return;
|
|
11775
|
+
}
|
|
11776
|
+
await backend.deleteFulltext({
|
|
11777
|
+
graphId: ctx.graphId,
|
|
11778
|
+
nodeKind: ctx.nodeKind,
|
|
11779
|
+
nodeId: ctx.nodeId
|
|
11780
|
+
});
|
|
11781
|
+
}
|
|
11782
|
+
|
|
11067
11783
|
// src/store/uniqueness.ts
|
|
11068
11784
|
async function checkUniquenessConstraints(ctx, kind, id, props, constraints) {
|
|
11069
11785
|
for (const constraint of constraints) {
|
|
@@ -11257,6 +11973,9 @@ function createUniquenessContext(graphId, registry, backend) {
|
|
|
11257
11973
|
function createEmbeddingSyncContext(graphId, nodeKind, nodeId, backend) {
|
|
11258
11974
|
return { graphId, nodeKind, nodeId, backend };
|
|
11259
11975
|
}
|
|
11976
|
+
function createFulltextSyncContext(graphId, nodeKind, nodeId, backend) {
|
|
11977
|
+
return { graphId, nodeKind, nodeId, backend };
|
|
11978
|
+
}
|
|
11260
11979
|
function createNodeBatchValidationBackend(graphId, registry, backend) {
|
|
11261
11980
|
const nodeCache = /* @__PURE__ */ new Map();
|
|
11262
11981
|
const pendingNodes = /* @__PURE__ */ new Map();
|
|
@@ -11414,6 +12133,11 @@ async function finalizeNodeCreate(ctx, prepared, backend) {
|
|
|
11414
12133
|
prepared.nodeKind.schema,
|
|
11415
12134
|
prepared.validatedProps
|
|
11416
12135
|
);
|
|
12136
|
+
await syncFulltext(
|
|
12137
|
+
createFulltextSyncContext(ctx.graphId, prepared.kind, prepared.id, backend),
|
|
12138
|
+
prepared.nodeKind.schema,
|
|
12139
|
+
prepared.validatedProps
|
|
12140
|
+
);
|
|
11417
12141
|
}
|
|
11418
12142
|
async function performNodeUpdate(ctx, input, backend, options) {
|
|
11419
12143
|
const { kind, id } = input;
|
|
@@ -11454,6 +12178,11 @@ async function performNodeUpdate(ctx, input, backend, options) {
|
|
|
11454
12178
|
nodeKind.schema,
|
|
11455
12179
|
validatedProps
|
|
11456
12180
|
);
|
|
12181
|
+
await syncFulltext(
|
|
12182
|
+
createFulltextSyncContext(ctx.graphId, kind, id, backend),
|
|
12183
|
+
nodeKind.schema,
|
|
12184
|
+
validatedProps
|
|
12185
|
+
);
|
|
11457
12186
|
return rowToNode(row);
|
|
11458
12187
|
}
|
|
11459
12188
|
async function enforceDeleteBehavior(ctx, kind, id, mode, backend, registration) {
|
|
@@ -11670,6 +12399,10 @@ async function executeNodeDelete(ctx, kind, id, backend) {
|
|
|
11670
12399
|
createEmbeddingSyncContext(ctx.graphId, kind, id, backend),
|
|
11671
12400
|
nodeKind.schema
|
|
11672
12401
|
);
|
|
12402
|
+
await deleteNodeFulltext(
|
|
12403
|
+
createFulltextSyncContext(ctx.graphId, kind, id, backend),
|
|
12404
|
+
nodeKind.schema
|
|
12405
|
+
);
|
|
11673
12406
|
});
|
|
11674
12407
|
}
|
|
11675
12408
|
async function executeNodeHardDelete(ctx, kind, id, backend) {
|
|
@@ -11976,6 +12709,503 @@ async function executeNodeBulkGetOrCreateByConstraint(ctx, kind, constraintName,
|
|
|
11976
12709
|
return results;
|
|
11977
12710
|
}
|
|
11978
12711
|
|
|
12712
|
+
// src/store/fulltext-rebuild.ts
|
|
12713
|
+
var DEFAULT_PAGE_SIZE = 500;
|
|
12714
|
+
var DEFAULT_MAX_SKIPPED_IDS = 1e4;
|
|
12715
|
+
function validatePageSize(value) {
|
|
12716
|
+
if (value === void 0) return DEFAULT_PAGE_SIZE;
|
|
12717
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
12718
|
+
throw new ValidationError(
|
|
12719
|
+
`pageSize must be a positive integer, got: ${String(value)}`,
|
|
12720
|
+
{
|
|
12721
|
+
issues: [{ path: "pageSize", message: "Must be a positive integer." }]
|
|
12722
|
+
}
|
|
12723
|
+
);
|
|
12724
|
+
}
|
|
12725
|
+
return value;
|
|
12726
|
+
}
|
|
12727
|
+
function validateMaxSkippedIds(value) {
|
|
12728
|
+
if (value === void 0) return DEFAULT_MAX_SKIPPED_IDS;
|
|
12729
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
12730
|
+
throw new ValidationError(
|
|
12731
|
+
`maxSkippedIds must be a non-negative integer, got: ${String(value)}`,
|
|
12732
|
+
{
|
|
12733
|
+
issues: [
|
|
12734
|
+
{
|
|
12735
|
+
path: "maxSkippedIds",
|
|
12736
|
+
message: "Must be a non-negative integer."
|
|
12737
|
+
}
|
|
12738
|
+
]
|
|
12739
|
+
}
|
|
12740
|
+
);
|
|
12741
|
+
}
|
|
12742
|
+
return value;
|
|
12743
|
+
}
|
|
12744
|
+
function isPropsObject(value) {
|
|
12745
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12746
|
+
}
|
|
12747
|
+
async function rebuildFulltextIndex(ctx, nodeKind, options) {
|
|
12748
|
+
const { backend, registry } = ctx;
|
|
12749
|
+
if (!backend.upsertFulltext || !backend.deleteFulltext) {
|
|
12750
|
+
throw new ConfigurationError(
|
|
12751
|
+
"Backend does not support fulltext; cannot rebuild index",
|
|
12752
|
+
{ backend: backend.dialect, capability: "fulltext" }
|
|
12753
|
+
);
|
|
12754
|
+
}
|
|
12755
|
+
const pageSize = validatePageSize(options.pageSize);
|
|
12756
|
+
const maxSkippedIds = validateMaxSkippedIds(options.maxSkippedIds);
|
|
12757
|
+
const targetKinds = nodeKind === void 0 ? [...registry.nodeKinds.keys()] : [nodeKind];
|
|
12758
|
+
const rebuiltKinds = [];
|
|
12759
|
+
let processed = 0;
|
|
12760
|
+
let upserted = 0;
|
|
12761
|
+
let cleared = 0;
|
|
12762
|
+
let skipped = 0;
|
|
12763
|
+
const skippedIds = [];
|
|
12764
|
+
for (const kind of targetKinds) {
|
|
12765
|
+
const nodeType = registry.getNodeType(kind);
|
|
12766
|
+
if (!nodeType) {
|
|
12767
|
+
throw new ConfigurationError(`Unknown node kind: ${kind}`, { kind });
|
|
12768
|
+
}
|
|
12769
|
+
if (getSearchableFields(nodeType.schema).length === 0) {
|
|
12770
|
+
continue;
|
|
12771
|
+
}
|
|
12772
|
+
rebuiltKinds.push(kind);
|
|
12773
|
+
let cursor;
|
|
12774
|
+
for (; ; ) {
|
|
12775
|
+
const rows = await backend.findNodesByKind({
|
|
12776
|
+
graphId: ctx.graphId,
|
|
12777
|
+
kind,
|
|
12778
|
+
limit: pageSize,
|
|
12779
|
+
excludeDeleted: false,
|
|
12780
|
+
orderBy: "id",
|
|
12781
|
+
...cursor === void 0 ? {} : { after: cursor }
|
|
12782
|
+
});
|
|
12783
|
+
if (rows.length === 0) break;
|
|
12784
|
+
const pageResult = processPage(nodeType.schema, rows);
|
|
12785
|
+
skipped += pageResult.skipped;
|
|
12786
|
+
const remaining = maxSkippedIds - skippedIds.length;
|
|
12787
|
+
if (remaining > 0 && pageResult.skippedIds.length > 0) {
|
|
12788
|
+
skippedIds.push(...pageResult.skippedIds.slice(0, remaining));
|
|
12789
|
+
}
|
|
12790
|
+
await backend.transaction(async (tx) => {
|
|
12791
|
+
if (pageResult.toUpsert.length > 0) {
|
|
12792
|
+
if (tx.upsertFulltextBatch) {
|
|
12793
|
+
await tx.upsertFulltextBatch({
|
|
12794
|
+
graphId: ctx.graphId,
|
|
12795
|
+
nodeKind: kind,
|
|
12796
|
+
rows: pageResult.toUpsert
|
|
12797
|
+
});
|
|
12798
|
+
} else if (tx.upsertFulltext) {
|
|
12799
|
+
for (const item of pageResult.toUpsert) {
|
|
12800
|
+
await tx.upsertFulltext({
|
|
12801
|
+
graphId: ctx.graphId,
|
|
12802
|
+
nodeKind: kind,
|
|
12803
|
+
nodeId: item.nodeId,
|
|
12804
|
+
content: item.content,
|
|
12805
|
+
language: item.language
|
|
12806
|
+
});
|
|
12807
|
+
}
|
|
12808
|
+
}
|
|
12809
|
+
}
|
|
12810
|
+
if (pageResult.toDelete.length > 0) {
|
|
12811
|
+
if (tx.deleteFulltextBatch) {
|
|
12812
|
+
await tx.deleteFulltextBatch({
|
|
12813
|
+
graphId: ctx.graphId,
|
|
12814
|
+
nodeKind: kind,
|
|
12815
|
+
nodeIds: pageResult.toDelete
|
|
12816
|
+
});
|
|
12817
|
+
} else if (tx.deleteFulltext) {
|
|
12818
|
+
for (const nodeId of pageResult.toDelete) {
|
|
12819
|
+
await tx.deleteFulltext({
|
|
12820
|
+
graphId: ctx.graphId,
|
|
12821
|
+
nodeKind: kind,
|
|
12822
|
+
nodeId
|
|
12823
|
+
});
|
|
12824
|
+
}
|
|
12825
|
+
}
|
|
12826
|
+
}
|
|
12827
|
+
});
|
|
12828
|
+
processed += rows.length;
|
|
12829
|
+
upserted += pageResult.toUpsert.length;
|
|
12830
|
+
cleared += pageResult.toDelete.length;
|
|
12831
|
+
const lastRow = rows.at(-1);
|
|
12832
|
+
if (!lastRow) break;
|
|
12833
|
+
cursor = lastRow.id;
|
|
12834
|
+
if (rows.length < pageSize) break;
|
|
12835
|
+
}
|
|
12836
|
+
}
|
|
12837
|
+
return {
|
|
12838
|
+
kinds: rebuiltKinds,
|
|
12839
|
+
processed,
|
|
12840
|
+
upserted,
|
|
12841
|
+
cleared,
|
|
12842
|
+
skipped,
|
|
12843
|
+
skippedIds,
|
|
12844
|
+
skippedTruncated: skipped > skippedIds.length
|
|
12845
|
+
};
|
|
12846
|
+
}
|
|
12847
|
+
function processPage(schema, rows) {
|
|
12848
|
+
const toUpsert = [];
|
|
12849
|
+
const toDelete = [];
|
|
12850
|
+
const skippedIds = [];
|
|
12851
|
+
let skipped = 0;
|
|
12852
|
+
for (const row of rows) {
|
|
12853
|
+
if (row.deleted_at !== void 0) {
|
|
12854
|
+
toDelete.push(row.id);
|
|
12855
|
+
continue;
|
|
12856
|
+
}
|
|
12857
|
+
let parsed;
|
|
12858
|
+
try {
|
|
12859
|
+
parsed = JSON.parse(row.props);
|
|
12860
|
+
} catch {
|
|
12861
|
+
skipped += 1;
|
|
12862
|
+
skippedIds.push(row.id);
|
|
12863
|
+
continue;
|
|
12864
|
+
}
|
|
12865
|
+
if (!isPropsObject(parsed)) {
|
|
12866
|
+
skipped += 1;
|
|
12867
|
+
skippedIds.push(row.id);
|
|
12868
|
+
continue;
|
|
12869
|
+
}
|
|
12870
|
+
const computed = computeFulltextContent(schema, parsed);
|
|
12871
|
+
if (computed === void 0) {
|
|
12872
|
+
toDelete.push(row.id);
|
|
12873
|
+
} else {
|
|
12874
|
+
toUpsert.push({
|
|
12875
|
+
nodeId: row.id,
|
|
12876
|
+
content: computed.content,
|
|
12877
|
+
language: computed.language
|
|
12878
|
+
});
|
|
12879
|
+
}
|
|
12880
|
+
}
|
|
12881
|
+
return { toUpsert, toDelete, skipped, skippedIds };
|
|
12882
|
+
}
|
|
12883
|
+
|
|
12884
|
+
// src/store/search.ts
|
|
12885
|
+
async function executeFulltextSearch(ctx, nodeKind, options) {
|
|
12886
|
+
const { backend, graphId } = ctx;
|
|
12887
|
+
if (!backend.fulltextSearch) {
|
|
12888
|
+
throw new ConfigurationError("Backend does not support fulltext search", {
|
|
12889
|
+
backend: backend.dialect,
|
|
12890
|
+
capability: "fulltext"
|
|
12891
|
+
});
|
|
12892
|
+
}
|
|
12893
|
+
if (!Number.isInteger(options.limit) || options.limit <= 0) {
|
|
12894
|
+
throw new RangeError(
|
|
12895
|
+
`fulltextSearch.limit must be a positive integer, got: ${options.limit}`
|
|
12896
|
+
);
|
|
12897
|
+
}
|
|
12898
|
+
validateFulltextCallOptions(backend, {
|
|
12899
|
+
mode: options.mode,
|
|
12900
|
+
includeSnippets: options.includeSnippets,
|
|
12901
|
+
language: options.language
|
|
12902
|
+
});
|
|
12903
|
+
const params = {
|
|
12904
|
+
graphId,
|
|
12905
|
+
nodeKind,
|
|
12906
|
+
query: options.query,
|
|
12907
|
+
limit: options.limit,
|
|
12908
|
+
...options.mode ? { mode: options.mode } : {},
|
|
12909
|
+
...options.language ? { language: options.language } : {},
|
|
12910
|
+
...options.minScore === void 0 ? {} : { minScore: options.minScore },
|
|
12911
|
+
...options.includeSnippets === void 0 ? {} : { includeSnippets: options.includeSnippets }
|
|
12912
|
+
};
|
|
12913
|
+
const rows = await backend.fulltextSearch(params);
|
|
12914
|
+
if (rows.length === 0) return [];
|
|
12915
|
+
const ids = rows.map((r) => r.nodeId);
|
|
12916
|
+
const nodeMap = await fetchNodesByIds(backend, graphId, nodeKind, ids);
|
|
12917
|
+
const hits = [];
|
|
12918
|
+
let rank = 1;
|
|
12919
|
+
for (const row of rows) {
|
|
12920
|
+
const node = nodeMap.get(row.nodeId);
|
|
12921
|
+
if (!node) continue;
|
|
12922
|
+
hits.push({
|
|
12923
|
+
node,
|
|
12924
|
+
score: row.score,
|
|
12925
|
+
rank,
|
|
12926
|
+
...row.snippet === void 0 ? {} : { snippet: row.snippet }
|
|
12927
|
+
});
|
|
12928
|
+
rank += 1;
|
|
12929
|
+
}
|
|
12930
|
+
return hits;
|
|
12931
|
+
}
|
|
12932
|
+
async function executeHybridSearch(ctx, nodeKind, options) {
|
|
12933
|
+
const { backend, graphId } = ctx;
|
|
12934
|
+
if (!backend.vectorSearch) {
|
|
12935
|
+
throw new ConfigurationError("Backend does not support vector search", {
|
|
12936
|
+
backend: backend.dialect,
|
|
12937
|
+
capability: "vector"
|
|
12938
|
+
});
|
|
12939
|
+
}
|
|
12940
|
+
if (!backend.fulltextSearch) {
|
|
12941
|
+
throw new ConfigurationError("Backend does not support fulltext search", {
|
|
12942
|
+
backend: backend.dialect,
|
|
12943
|
+
capability: "fulltext"
|
|
12944
|
+
});
|
|
12945
|
+
}
|
|
12946
|
+
if (!Number.isInteger(options.limit) || options.limit <= 0) {
|
|
12947
|
+
throw new RangeError(
|
|
12948
|
+
`hybridSearch.limit must be a positive integer, got: ${options.limit}`
|
|
12949
|
+
);
|
|
12950
|
+
}
|
|
12951
|
+
if (options.fusion !== void 0) {
|
|
12952
|
+
validateHybridFusionOptions(options.fusion);
|
|
12953
|
+
}
|
|
12954
|
+
validateFulltextCallOptions(backend, {
|
|
12955
|
+
mode: options.fulltext.mode,
|
|
12956
|
+
includeSnippets: options.fulltext.includeSnippets,
|
|
12957
|
+
language: options.fulltext.language
|
|
12958
|
+
});
|
|
12959
|
+
const fusionK = options.fusion?.k ?? DEFAULT_RRF_K;
|
|
12960
|
+
const vectorWeight = options.fusion?.weights?.vector ?? DEFAULT_RRF_WEIGHT;
|
|
12961
|
+
const fulltextWeight = options.fusion?.weights?.fulltext ?? DEFAULT_RRF_WEIGHT;
|
|
12962
|
+
const overFetchMultiplier = 4;
|
|
12963
|
+
const vectorK = options.vector.k ?? options.limit * overFetchMultiplier;
|
|
12964
|
+
const fulltextK = options.fulltext.k ?? options.limit * overFetchMultiplier;
|
|
12965
|
+
const vectorPromise = backend.vectorSearch({
|
|
12966
|
+
graphId,
|
|
12967
|
+
nodeKind,
|
|
12968
|
+
fieldPath: options.vector.fieldPath,
|
|
12969
|
+
queryEmbedding: options.vector.queryEmbedding,
|
|
12970
|
+
metric: options.vector.metric ?? "cosine",
|
|
12971
|
+
limit: vectorK,
|
|
12972
|
+
...options.vector.minScore === void 0 ? {} : { minScore: options.vector.minScore }
|
|
12973
|
+
});
|
|
12974
|
+
const fulltextPromise = backend.fulltextSearch({
|
|
12975
|
+
graphId,
|
|
12976
|
+
nodeKind,
|
|
12977
|
+
query: options.fulltext.query,
|
|
12978
|
+
limit: fulltextK,
|
|
12979
|
+
...options.fulltext.mode ? { mode: options.fulltext.mode } : {},
|
|
12980
|
+
...options.fulltext.language ? { language: options.fulltext.language } : {},
|
|
12981
|
+
...options.fulltext.minScore === void 0 ? {} : { minScore: options.fulltext.minScore },
|
|
12982
|
+
...options.fulltext.includeSnippets === void 0 ? {} : { includeSnippets: options.fulltext.includeSnippets }
|
|
12983
|
+
});
|
|
12984
|
+
const [vectorRows, fulltextRows] = await Promise.all([
|
|
12985
|
+
vectorPromise,
|
|
12986
|
+
fulltextPromise
|
|
12987
|
+
]);
|
|
12988
|
+
const fused = /* @__PURE__ */ new Map();
|
|
12989
|
+
for (const [index, row] of vectorRows.entries()) {
|
|
12990
|
+
const rank2 = index + 1;
|
|
12991
|
+
const contribution = vectorWeight / (fusionK + rank2);
|
|
12992
|
+
const entry = fused.get(row.nodeId) ?? {
|
|
12993
|
+
nodeId: row.nodeId,
|
|
12994
|
+
fusedScore: 0
|
|
12995
|
+
};
|
|
12996
|
+
entry.fusedScore += contribution;
|
|
12997
|
+
entry.vectorRank = rank2;
|
|
12998
|
+
entry.vectorScore = row.score;
|
|
12999
|
+
fused.set(row.nodeId, entry);
|
|
13000
|
+
}
|
|
13001
|
+
for (const [index, row] of fulltextRows.entries()) {
|
|
13002
|
+
const rank2 = index + 1;
|
|
13003
|
+
const contribution = fulltextWeight / (fusionK + rank2);
|
|
13004
|
+
const entry = fused.get(row.nodeId) ?? {
|
|
13005
|
+
nodeId: row.nodeId,
|
|
13006
|
+
fusedScore: 0
|
|
13007
|
+
};
|
|
13008
|
+
entry.fusedScore += contribution;
|
|
13009
|
+
entry.fulltextRank = rank2;
|
|
13010
|
+
entry.fulltextScore = row.score;
|
|
13011
|
+
if (row.snippet !== void 0) {
|
|
13012
|
+
entry.fulltextSnippet = row.snippet;
|
|
13013
|
+
}
|
|
13014
|
+
fused.set(row.nodeId, entry);
|
|
13015
|
+
}
|
|
13016
|
+
const ranked = [...fused.values()].toSorted(
|
|
13017
|
+
(a, b) => b.fusedScore - a.fusedScore || a.nodeId.localeCompare(b.nodeId)
|
|
13018
|
+
).slice(0, options.limit);
|
|
13019
|
+
const ids = ranked.map((entry) => entry.nodeId);
|
|
13020
|
+
const nodeMap = await fetchNodesByIds(backend, graphId, nodeKind, ids);
|
|
13021
|
+
const hits = [];
|
|
13022
|
+
let rank = 1;
|
|
13023
|
+
for (const entry of ranked) {
|
|
13024
|
+
const node = nodeMap.get(entry.nodeId);
|
|
13025
|
+
if (!node) continue;
|
|
13026
|
+
const typedNode = node;
|
|
13027
|
+
const hit = {
|
|
13028
|
+
node: typedNode,
|
|
13029
|
+
score: entry.fusedScore,
|
|
13030
|
+
rank,
|
|
13031
|
+
...entry.vectorRank !== void 0 && entry.vectorScore !== void 0 ? {
|
|
13032
|
+
vector: {
|
|
13033
|
+
node: typedNode,
|
|
13034
|
+
score: entry.vectorScore,
|
|
13035
|
+
rank: entry.vectorRank
|
|
13036
|
+
}
|
|
13037
|
+
} : {},
|
|
13038
|
+
...entry.fulltextRank !== void 0 && entry.fulltextScore !== void 0 ? {
|
|
13039
|
+
fulltext: {
|
|
13040
|
+
node: typedNode,
|
|
13041
|
+
score: entry.fulltextScore,
|
|
13042
|
+
rank: entry.fulltextRank,
|
|
13043
|
+
...entry.fulltextSnippet === void 0 ? {} : { snippet: entry.fulltextSnippet }
|
|
13044
|
+
}
|
|
13045
|
+
} : {}
|
|
13046
|
+
};
|
|
13047
|
+
hits.push(hit);
|
|
13048
|
+
rank += 1;
|
|
13049
|
+
}
|
|
13050
|
+
return hits;
|
|
13051
|
+
}
|
|
13052
|
+
function validateFulltextCallOptions(backend, options) {
|
|
13053
|
+
const strategy = backend.fulltextStrategy;
|
|
13054
|
+
const capabilities = backend.capabilities.fulltext;
|
|
13055
|
+
if (options.mode !== void 0) {
|
|
13056
|
+
validateFulltextMode(backend, strategy, capabilities, options.mode);
|
|
13057
|
+
}
|
|
13058
|
+
if (options.includeSnippets === true) {
|
|
13059
|
+
validateFulltextSnippets(backend, strategy, capabilities);
|
|
13060
|
+
}
|
|
13061
|
+
if (options.language !== void 0) {
|
|
13062
|
+
validateFulltextLanguageOption(
|
|
13063
|
+
backend,
|
|
13064
|
+
strategy,
|
|
13065
|
+
capabilities,
|
|
13066
|
+
options.language
|
|
13067
|
+
);
|
|
13068
|
+
}
|
|
13069
|
+
}
|
|
13070
|
+
function validateFulltextMode(backend, strategy, capabilities, mode) {
|
|
13071
|
+
if (strategy !== void 0) {
|
|
13072
|
+
if (strategy.supportedModes.includes(mode)) return;
|
|
13073
|
+
throw new ConfigurationError(
|
|
13074
|
+
`Backend "${backend.dialect}" fulltext strategy "${strategy.name}" does not support mode "${mode}". Supported modes: ${strategy.supportedModes.join(", ")}`,
|
|
13075
|
+
{
|
|
13076
|
+
backend: backend.dialect,
|
|
13077
|
+
strategy: strategy.name,
|
|
13078
|
+
capability: `fulltext.modes.${mode}`
|
|
13079
|
+
}
|
|
13080
|
+
);
|
|
13081
|
+
}
|
|
13082
|
+
if (mode === "phrase" && capabilities && !capabilities.phraseQueries) {
|
|
13083
|
+
throw new ConfigurationError(
|
|
13084
|
+
`Backend "${backend.dialect}" does not support phrase queries`,
|
|
13085
|
+
{ backend: backend.dialect, capability: "fulltext.phraseQueries" }
|
|
13086
|
+
);
|
|
13087
|
+
}
|
|
13088
|
+
}
|
|
13089
|
+
function validateFulltextSnippets(backend, strategy, capabilities) {
|
|
13090
|
+
const supportsSnippets = strategy === void 0 ? capabilities?.highlighting !== false : strategy.supportsSnippets;
|
|
13091
|
+
if (supportsSnippets) return;
|
|
13092
|
+
throw new ConfigurationError(
|
|
13093
|
+
`Backend "${backend.dialect}" does not support snippet highlighting; drop includeSnippets or switch to a backend where highlighting is supported`,
|
|
13094
|
+
{ backend: backend.dialect, capability: "fulltext.highlighting" }
|
|
13095
|
+
);
|
|
13096
|
+
}
|
|
13097
|
+
function validateFulltextLanguageOption(backend, strategy, capabilities, language) {
|
|
13098
|
+
if (strategy !== void 0 && !strategy.supportsLanguageOverride) {
|
|
13099
|
+
throw new ConfigurationError(
|
|
13100
|
+
`Backend "${backend.dialect}" fulltext strategy "${strategy.name}" does not honor a per-query \`language\` override (its tokenizer is fixed at table-create time). Drop the option, or pick a strategy that advertises \`supportsLanguageOverride: true\`.`,
|
|
13101
|
+
{
|
|
13102
|
+
backend: backend.dialect,
|
|
13103
|
+
strategy: strategy.name,
|
|
13104
|
+
capability: "fulltext.languageOverride"
|
|
13105
|
+
}
|
|
13106
|
+
);
|
|
13107
|
+
}
|
|
13108
|
+
if (capabilities !== void 0) {
|
|
13109
|
+
warnIfLanguageNotAdvertised(backend.dialect, capabilities, language);
|
|
13110
|
+
}
|
|
13111
|
+
}
|
|
13112
|
+
function warnIfLanguageNotAdvertised(dialect, capabilities, language) {
|
|
13113
|
+
if (capabilities.languages.length === 0) return;
|
|
13114
|
+
if (capabilities.languages.includes(language)) return;
|
|
13115
|
+
if (typeof console === "undefined" || typeof console.warn !== "function") {
|
|
13116
|
+
return;
|
|
13117
|
+
}
|
|
13118
|
+
console.warn(
|
|
13119
|
+
`[typegraph] fulltext language "${language}" is not in the advertised list for dialect "${dialect}" (${capabilities.languages.join(", ")}). If your backend has this language installed this warning is safe to ignore.`
|
|
13120
|
+
);
|
|
13121
|
+
}
|
|
13122
|
+
async function fetchNodesByIds(backend, graphId, nodeKind, ids) {
|
|
13123
|
+
if (ids.length === 0) return /* @__PURE__ */ new Map();
|
|
13124
|
+
const map = /* @__PURE__ */ new Map();
|
|
13125
|
+
if (backend.getNodes) {
|
|
13126
|
+
const rows = await backend.getNodes(graphId, nodeKind, ids);
|
|
13127
|
+
for (const row of rows) {
|
|
13128
|
+
if (row.deleted_at !== void 0) continue;
|
|
13129
|
+
map.set(row.id, rowToNode(row));
|
|
13130
|
+
}
|
|
13131
|
+
return map;
|
|
13132
|
+
}
|
|
13133
|
+
for (const id of ids) {
|
|
13134
|
+
const row = await backend.getNode(graphId, nodeKind, id);
|
|
13135
|
+
if (row && row.deleted_at === void 0) map.set(row.id, rowToNode(row));
|
|
13136
|
+
}
|
|
13137
|
+
return map;
|
|
13138
|
+
}
|
|
13139
|
+
|
|
13140
|
+
// src/store/search-facade.ts
|
|
13141
|
+
var StoreSearch = class {
|
|
13142
|
+
#context;
|
|
13143
|
+
constructor(context) {
|
|
13144
|
+
this.#context = context;
|
|
13145
|
+
}
|
|
13146
|
+
/**
|
|
13147
|
+
* Runs a fulltext search against nodes of the given kind.
|
|
13148
|
+
*
|
|
13149
|
+
* Requires fields on the node schema declared with `searchable()`.
|
|
13150
|
+
* The search hits the backend's fulltext index (tsvector + GIN on
|
|
13151
|
+
* Postgres, FTS5 on SQLite — or whatever strategy the backend is
|
|
13152
|
+
* configured with) and resolves the matching node IDs back to typed
|
|
13153
|
+
* `Node` objects.
|
|
13154
|
+
*/
|
|
13155
|
+
async fulltext(nodeKind, options) {
|
|
13156
|
+
return executeFulltextSearch(
|
|
13157
|
+
{ graphId: this.#context.graphId, backend: this.#context.backend },
|
|
13158
|
+
nodeKind,
|
|
13159
|
+
options
|
|
13160
|
+
);
|
|
13161
|
+
}
|
|
13162
|
+
/**
|
|
13163
|
+
* Runs a vector + fulltext hybrid search and fuses the results with
|
|
13164
|
+
* Reciprocal Rank Fusion.
|
|
13165
|
+
*
|
|
13166
|
+
* RRF is rank-based, so it composes well across heterogeneous score
|
|
13167
|
+
* scales (cosine similarity vs ts_rank_cd vs FTS5 BM25). Default
|
|
13168
|
+
* over-fetch is 4× `limit` from each source — tune via `vector.k` /
|
|
13169
|
+
* `fulltext.k` for higher-recall corpora.
|
|
13170
|
+
*/
|
|
13171
|
+
async hybrid(nodeKind, options) {
|
|
13172
|
+
return executeHybridSearch(
|
|
13173
|
+
{ graphId: this.#context.graphId, backend: this.#context.backend },
|
|
13174
|
+
nodeKind,
|
|
13175
|
+
options
|
|
13176
|
+
);
|
|
13177
|
+
}
|
|
13178
|
+
/**
|
|
13179
|
+
* Rebuilds the fulltext index from existing node data.
|
|
13180
|
+
*
|
|
13181
|
+
* Use when:
|
|
13182
|
+
* - A node kind gained a `searchable()` field after data was already
|
|
13183
|
+
* written and existing rows were never indexed.
|
|
13184
|
+
* - The fulltext table was dropped / truncated.
|
|
13185
|
+
* - `language` was changed on a `searchable()` field.
|
|
13186
|
+
*
|
|
13187
|
+
* Iterates nodes with keyset pagination (stable under shared
|
|
13188
|
+
* timestamps and light concurrent writes), transacts per page, skips
|
|
13189
|
+
* kinds with no searchable fields, and cleans up stale rows for
|
|
13190
|
+
* soft-deleted nodes. Corrupt or non-object `props` are counted in
|
|
13191
|
+
* `skipped` with their node IDs surfaced via `skippedIds` so operators
|
|
13192
|
+
* can investigate. Concurrent hard-deletes between page fetches can
|
|
13193
|
+
* be missed by a single pass — run during a maintenance window for
|
|
13194
|
+
* full consistency.
|
|
13195
|
+
*/
|
|
13196
|
+
async rebuildFulltext(nodeKind, options = {}) {
|
|
13197
|
+
return rebuildFulltextIndex(
|
|
13198
|
+
{
|
|
13199
|
+
graphId: this.#context.graphId,
|
|
13200
|
+
backend: this.#context.backend,
|
|
13201
|
+
registry: this.#context.registry
|
|
13202
|
+
},
|
|
13203
|
+
nodeKind,
|
|
13204
|
+
options
|
|
13205
|
+
);
|
|
13206
|
+
}
|
|
13207
|
+
};
|
|
13208
|
+
|
|
11979
13209
|
// src/store/store.ts
|
|
11980
13210
|
var Store = class {
|
|
11981
13211
|
#graph;
|
|
@@ -11987,6 +13217,7 @@ var Store = class {
|
|
|
11987
13217
|
#nodeCollections;
|
|
11988
13218
|
#edgeCollections;
|
|
11989
13219
|
#algorithms;
|
|
13220
|
+
#search;
|
|
11990
13221
|
constructor(graph, backend, options) {
|
|
11991
13222
|
this.#graph = graph;
|
|
11992
13223
|
this.#backend = backend;
|
|
@@ -12235,6 +13466,44 @@ var Store = class {
|
|
|
12235
13466
|
query() {
|
|
12236
13467
|
return this.#createQueryForBackend(this.#backend);
|
|
12237
13468
|
}
|
|
13469
|
+
// === Search ===
|
|
13470
|
+
/**
|
|
13471
|
+
* Search-related operations (fulltext, hybrid, and rebuild).
|
|
13472
|
+
*
|
|
13473
|
+
* All search methods live under this facade to keep the top-level
|
|
13474
|
+
* Store API focused on CRUD + graph traversal. The facade is
|
|
13475
|
+
* lazy-initialized and cached for the lifetime of the store.
|
|
13476
|
+
*
|
|
13477
|
+
* @example
|
|
13478
|
+
* ```typescript
|
|
13479
|
+
* // Fulltext
|
|
13480
|
+
* const hits = await store.search.fulltext("Document", {
|
|
13481
|
+
* query: "climate warming",
|
|
13482
|
+
* limit: 10,
|
|
13483
|
+
* includeSnippets: true,
|
|
13484
|
+
* });
|
|
13485
|
+
*
|
|
13486
|
+
* // Hybrid (vector + fulltext fused with RRF)
|
|
13487
|
+
* const ranked = await store.search.hybrid("Document", {
|
|
13488
|
+
* limit: 10,
|
|
13489
|
+
* vector: { fieldPath: "embedding", queryEmbedding: vec },
|
|
13490
|
+
* fulltext: { query: "climate warming" },
|
|
13491
|
+
* });
|
|
13492
|
+
*
|
|
13493
|
+
* // Rebuild index after schema change
|
|
13494
|
+
* const stats = await store.search.rebuildFulltext();
|
|
13495
|
+
* ```
|
|
13496
|
+
*/
|
|
13497
|
+
get search() {
|
|
13498
|
+
if (this.#search === void 0) {
|
|
13499
|
+
this.#search = new StoreSearch({
|
|
13500
|
+
graphId: this.graphId,
|
|
13501
|
+
backend: this.#backend,
|
|
13502
|
+
registry: this.#registry
|
|
13503
|
+
});
|
|
13504
|
+
}
|
|
13505
|
+
return this.#search;
|
|
13506
|
+
}
|
|
12238
13507
|
// === Batch Query Execution ===
|
|
12239
13508
|
/**
|
|
12240
13509
|
* Executes multiple queries over a single connection with snapshot consistency.
|
|
@@ -12379,6 +13648,7 @@ var Store = class {
|
|
|
12379
13648
|
this.#nodeCollections = void 0;
|
|
12380
13649
|
this.#edgeCollections = void 0;
|
|
12381
13650
|
this.#algorithms = void 0;
|
|
13651
|
+
this.#search = void 0;
|
|
12382
13652
|
}
|
|
12383
13653
|
// === Lifecycle ===
|
|
12384
13654
|
/**
|
|
@@ -12485,6 +13755,6 @@ async function createStoreWithSchema(graph, backend, options) {
|
|
|
12485
13755
|
return [store, result];
|
|
12486
13756
|
}
|
|
12487
13757
|
|
|
12488
|
-
export { DEFAULT_SQL_SCHEMA, ExecutableAggregateQuery, ExecutableQuery, MAX_EXPLICIT_RECURSIVE_DEPTH, MAX_RECURSIVE_DEPTH, PreparedQuery, QueryBuilder, UnionableQuery, avg, broader, composeFragments, core, count, countDistinct, createExternalRef, createFragment, createQueryBuilder, createSqlSchema, createStore, createStoreWithSchema, defineEdge, defineNode, defineSubgraphProject, differentFrom, disjointWith, equivalentTo, exists, externalRef, field, fieldRef, generateId, getExternalRefTable, hasPart, having, havingEq, havingGt, havingGte, havingLt, havingLte, implies, inSubquery, inverseOf, isExternalRefSchema, isParameterRef, limitFragment, max, metaEdge, min, narrower, notExists, notInSubquery, offsetFragment, orderByFragment, param, partOf, relatedTo, sameAs, subClassOf, sum };
|
|
13758
|
+
export { DEFAULT_SQL_SCHEMA, ExecutableAggregateQuery, ExecutableQuery, MAX_EXPLICIT_RECURSIVE_DEPTH, MAX_RECURSIVE_DEPTH, PreparedQuery, QueryBuilder, StoreSearch, UnionableQuery, avg, broader, composeFragments, core, count, countDistinct, countDistinctEdges, countEdges, createExternalRef, createFragment, createQueryBuilder, createSqlSchema, createStore, createStoreWithSchema, defineEdge, defineNode, defineSubgraphProject, differentFrom, disjointWith, equivalentTo, exists, externalRef, field, fieldRef, generateId, getExternalRefTable, hasPart, having, havingEq, havingGt, havingGte, havingLt, havingLte, implies, inSubquery, inverseOf, isExternalRefSchema, isParameterRef, limitFragment, max, metaEdge, min, narrower, notExists, notInSubquery, offsetFragment, orderByFragment, param, partOf, relatedTo, sameAs, subClassOf, sum };
|
|
12489
13759
|
//# sourceMappingURL=index.js.map
|
|
12490
13760
|
//# sourceMappingURL=index.js.map
|