@nicia-ai/typegraph 0.19.0 → 0.21.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 +11 -7
- package/dist/backend/sqlite/index.d.ts +11 -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 +9 -9
- package/dist/backend/sqlite/local.d.cts +4 -5
- package/dist/backend/sqlite/local.d.ts +4 -5
- package/dist/backend/sqlite/local.js +5 -5
- package/dist/{chunk-LELLOHJK.cjs → chunk-2X7P7AD5.cjs} +35 -35
- package/dist/{chunk-LELLOHJK.cjs.map → chunk-2X7P7AD5.cjs.map} +1 -1
- package/dist/{chunk-GNIYZKBI.js → chunk-4XNE2JG3.js} +376 -8
- package/dist/chunk-4XNE2JG3.js.map +1 -0
- package/dist/{chunk-HXJXTOXR.cjs → chunk-CQL6FOTT.cjs} +82 -21
- package/dist/chunk-CQL6FOTT.cjs.map +1 -0
- package/dist/{chunk-K7SQ3SWP.js → chunk-EYELZHKY.js} +120 -12
- package/dist/chunk-EYELZHKY.js.map +1 -0
- package/dist/{chunk-MME3H4ZF.cjs → chunk-FQCQM3KG.cjs} +385 -9
- package/dist/chunk-FQCQM3KG.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-NRNJ52AA.js} +72 -11
- package/dist/chunk-NRNJ52AA.js.map +1 -0
- package/dist/{chunk-IYWWXOVK.cjs → chunk-Q73OWTJY.cjs} +204 -89
- package/dist/chunk-Q73OWTJY.cjs.map +1 -0
- 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-6GWJH6AR.js → chunk-SMMOAVJ5.js} +4 -4
- package/dist/{chunk-6GWJH6AR.js.map → chunk-SMMOAVJ5.js.map} +1 -1
- package/dist/{chunk-QFZQGEQ3.js → chunk-UQVBVBVG.js} +151 -36
- package/dist/chunk-UQVBVBVG.js.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 +2102 -556
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -10
- package/dist/index.d.ts +8 -10
- package/dist/index.js +1961 -439
- 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-BnUu4qo5.d.cts → store-2QW2SIRk.d.cts} +626 -7
- package/dist/{store-CCOwNft_.d.ts → store-C0q13mtJ.d.ts} +626 -7
- 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 +10 -10
- 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-4XNE2JG3.js';
|
|
8
|
+
export { fts5Strategy, tsvectorStrategy } from './chunk-4XNE2JG3.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)
|
|
@@ -2183,7 +2341,7 @@ function buildStandardProjection(input) {
|
|
|
2183
2341
|
}
|
|
2184
2342
|
const projectedFields = fields.map((field2) => {
|
|
2185
2343
|
const source = compileProjectedSource(field2, dialect);
|
|
2186
|
-
return sql`${source} AS ${
|
|
2344
|
+
return sql`${source} AS ${quoteIdentifier2(field2.outputName)}`;
|
|
2187
2345
|
});
|
|
2188
2346
|
return sql.join(projectedFields, sql`, `);
|
|
2189
2347
|
}
|
|
@@ -2203,7 +2361,7 @@ function compileSelectiveProjection(fields, dialect, ast, collapsedTraversalCteA
|
|
|
2203
2361
|
const cteAlias = collapsedTraversalCteAlias ?? aliasToCte.get(field2.alias) ?? `cte_${field2.alias}`;
|
|
2204
2362
|
if (field2.isSystemField) {
|
|
2205
2363
|
const dbColumn = mapSelectiveSystemFieldToColumn(field2.field);
|
|
2206
|
-
return sql`${sql.raw(cteAlias)}.${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${
|
|
2364
|
+
return sql`${sql.raw(cteAlias)}.${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${quoteIdentifier2(field2.outputName)}`;
|
|
2207
2365
|
}
|
|
2208
2366
|
const propsColumn = `${field2.alias}_props`;
|
|
2209
2367
|
const column = sql`${sql.raw(cteAlias)}.${sql.raw(propsColumn)}`;
|
|
@@ -2214,12 +2372,56 @@ function compileSelectiveProjection(fields, dialect, ast, collapsedTraversalCteA
|
|
|
2214
2372
|
pointer,
|
|
2215
2373
|
valueType: field2.valueType
|
|
2216
2374
|
});
|
|
2217
|
-
return sql`${extracted} AS ${
|
|
2375
|
+
return sql`${extracted} AS ${quoteIdentifier2(field2.outputName)}`;
|
|
2218
2376
|
});
|
|
2219
2377
|
return sql.join(columns, sql`, `);
|
|
2220
2378
|
}
|
|
2379
|
+
function buildRelevanceJoins(vectorPredicate, fulltextPredicate) {
|
|
2380
|
+
const hybridTargetAlias = getHybridTargetAlias(
|
|
2381
|
+
vectorPredicate,
|
|
2382
|
+
fulltextPredicate
|
|
2383
|
+
);
|
|
2384
|
+
if (hybridTargetAlias !== void 0) {
|
|
2385
|
+
return [
|
|
2386
|
+
buildHybridCandidateJoin(hybridTargetAlias),
|
|
2387
|
+
buildRelevanceJoin(EMBEDDINGS_CTE_ALIAS, hybridTargetAlias, "LEFT JOIN"),
|
|
2388
|
+
buildRelevanceJoin(FULLTEXT_CTE_ALIAS, hybridTargetAlias, "LEFT JOIN")
|
|
2389
|
+
];
|
|
2390
|
+
}
|
|
2391
|
+
const joins = [];
|
|
2392
|
+
if (vectorPredicate) {
|
|
2393
|
+
joins.push(
|
|
2394
|
+
buildRelevanceJoin(EMBEDDINGS_CTE_ALIAS, vectorPredicate.field.alias)
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
if (fulltextPredicate) {
|
|
2398
|
+
joins.push(
|
|
2399
|
+
buildRelevanceJoin(FULLTEXT_CTE_ALIAS, fulltextPredicate.field.alias)
|
|
2400
|
+
);
|
|
2401
|
+
}
|
|
2402
|
+
return joins;
|
|
2403
|
+
}
|
|
2404
|
+
function buildHybridCandidateJoin(nodeAlias) {
|
|
2405
|
+
const candidateCte = sql.raw(HYBRID_CANDIDATES_CTE_ALIAS);
|
|
2406
|
+
const node = sql.raw(`${ALIAS_CTE_PREFIX}${nodeAlias}`);
|
|
2407
|
+
const idColumn = sql.raw(`${nodeAlias}_id`);
|
|
2408
|
+
const kindColumn = sql.raw(`${nodeAlias}_kind`);
|
|
2409
|
+
return sql`INNER JOIN ${candidateCte} ON ${candidateCte}.node_id = ${node}.${idColumn} AND ${candidateCte}.node_kind = ${node}.${kindColumn}`;
|
|
2410
|
+
}
|
|
2411
|
+
function buildRelevanceJoin(cteAlias, nodeAlias, joinType = "INNER JOIN") {
|
|
2412
|
+
const cte = sql.raw(cteAlias);
|
|
2413
|
+
const node = sql.raw(`${ALIAS_CTE_PREFIX}${nodeAlias}`);
|
|
2414
|
+
const idColumn = sql.raw(`${nodeAlias}_id`);
|
|
2415
|
+
const kindColumn = sql.raw(`${nodeAlias}_kind`);
|
|
2416
|
+
return sql`${sql.raw(joinType)} ${cte} ON ${cte}.node_id = ${node}.${idColumn} AND ${cte}.node_kind = ${node}.${kindColumn}`;
|
|
2417
|
+
}
|
|
2221
2418
|
function buildStandardFromClause(input) {
|
|
2222
|
-
const {
|
|
2419
|
+
const {
|
|
2420
|
+
ast,
|
|
2421
|
+
collapsedTraversalCteAlias,
|
|
2422
|
+
fulltextPredicate,
|
|
2423
|
+
vectorPredicate
|
|
2424
|
+
} = input;
|
|
2223
2425
|
if (collapsedTraversalCteAlias !== void 0) {
|
|
2224
2426
|
return sql`FROM ${sql.raw(collapsedTraversalCteAlias)}`;
|
|
2225
2427
|
}
|
|
@@ -2234,11 +2436,8 @@ function buildStandardFromClause(input) {
|
|
|
2234
2436
|
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
2437
|
);
|
|
2236
2438
|
}
|
|
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
|
-
);
|
|
2439
|
+
for (const join of buildRelevanceJoins(vectorPredicate, fulltextPredicate)) {
|
|
2440
|
+
joins.push(join);
|
|
2242
2441
|
}
|
|
2243
2442
|
return joins.length === 0 ? fromClause : sql`${fromClause} ${sql.join(joins, sql` `)}`;
|
|
2244
2443
|
}
|
|
@@ -2277,6 +2476,15 @@ function fieldRefKey(field2) {
|
|
|
2277
2476
|
const pointer = field2.jsonPointer ?? "";
|
|
2278
2477
|
return `${field2.alias}:${field2.path.join(".")}:${pointer}`;
|
|
2279
2478
|
}
|
|
2479
|
+
function resolveEmbeddingFieldPath(field2) {
|
|
2480
|
+
if (field2.jsonPointer !== void 0 && field2.jsonPointer !== "" && field2.jsonPointer !== "/") {
|
|
2481
|
+
return field2.jsonPointer.replace(/^\//u, "").replaceAll("/", ".");
|
|
2482
|
+
}
|
|
2483
|
+
if (field2.path[0] === "props") {
|
|
2484
|
+
return field2.path.slice(1).join(".");
|
|
2485
|
+
}
|
|
2486
|
+
return field2.path.join(".");
|
|
2487
|
+
}
|
|
2280
2488
|
function buildStandardGroupBy(input) {
|
|
2281
2489
|
const { ast, dialect } = input;
|
|
2282
2490
|
if (!ast.groupBy || ast.groupBy.fields.length === 0) {
|
|
@@ -2323,18 +2531,29 @@ function buildStandardHaving(input) {
|
|
|
2323
2531
|
return sql`HAVING ${condition}`;
|
|
2324
2532
|
}
|
|
2325
2533
|
function buildStandardEmbeddingsCte(input) {
|
|
2326
|
-
const { ctx, graphId, vectorPredicate } = input;
|
|
2534
|
+
const { ctx, graphId, nodeKinds, vectorPredicate } = input;
|
|
2327
2535
|
const { dialect } = ctx;
|
|
2328
2536
|
const { field: field2, metric, minScore, queryEmbedding } = vectorPredicate;
|
|
2329
|
-
const
|
|
2537
|
+
const embeddingsTableName = ctx.schema.tables.embeddings;
|
|
2538
|
+
if (nodeKinds.length === 0) {
|
|
2539
|
+
throw new UnsupportedPredicateError(
|
|
2540
|
+
"Vector predicate must resolve to at least one node kind"
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
const fieldPath = resolveEmbeddingFieldPath(field2);
|
|
2330
2544
|
const distanceExpr = dialect.vectorDistance(
|
|
2331
|
-
|
|
2545
|
+
qualifyColumn(embeddingsTableName, "embedding"),
|
|
2332
2546
|
queryEmbedding,
|
|
2333
2547
|
metric
|
|
2334
2548
|
);
|
|
2549
|
+
const scopedNodes = buildScopedNodeIdsSubquery(field2.alias);
|
|
2335
2550
|
const conditions = [
|
|
2336
|
-
sql
|
|
2337
|
-
|
|
2551
|
+
sql`${qualifyColumn(embeddingsTableName, "graph_id")} = ${graphId}`,
|
|
2552
|
+
compileKindFilter(
|
|
2553
|
+
qualifyColumn(embeddingsTableName, "node_kind"),
|
|
2554
|
+
nodeKinds
|
|
2555
|
+
),
|
|
2556
|
+
sql`${qualifyColumn(embeddingsTableName, "field_path")} = ${fieldPath}`
|
|
2338
2557
|
];
|
|
2339
2558
|
if (minScore !== void 0) {
|
|
2340
2559
|
conditions.push(
|
|
@@ -2343,14 +2562,29 @@ function buildStandardEmbeddingsCte(input) {
|
|
|
2343
2562
|
}
|
|
2344
2563
|
const scoreExpr = compileVectorScoreExpression(distanceExpr, metric);
|
|
2345
2564
|
return sql`
|
|
2346
|
-
|
|
2565
|
+
${sql.raw(EMBEDDINGS_CTE_ALIAS)} AS (
|
|
2347
2566
|
SELECT
|
|
2348
2567
|
node_id,
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2568
|
+
node_kind,
|
|
2569
|
+
distance,
|
|
2570
|
+
score,
|
|
2571
|
+
ROW_NUMBER() OVER (ORDER BY distance ASC) AS ord
|
|
2572
|
+
FROM (
|
|
2573
|
+
SELECT
|
|
2574
|
+
${qualifyColumn(embeddingsTableName, "node_id")} AS node_id,
|
|
2575
|
+
${qualifyColumn(embeddingsTableName, "node_kind")} AS node_kind,
|
|
2576
|
+
${distanceExpr} AS distance,
|
|
2577
|
+
${scoreExpr} AS score
|
|
2578
|
+
FROM ${ctx.schema.embeddingsTable}
|
|
2579
|
+
INNER JOIN ${scopedNodes}
|
|
2580
|
+
ON ${sql.raw(`${SCOPED_RELEVANCE_NODES_ALIAS}.node_id`)} =
|
|
2581
|
+
${qualifyColumn(embeddingsTableName, "node_id")}
|
|
2582
|
+
AND ${sql.raw(`${SCOPED_RELEVANCE_NODES_ALIAS}.node_kind`)} =
|
|
2583
|
+
${qualifyColumn(embeddingsTableName, "node_kind")}
|
|
2584
|
+
WHERE ${sql.join(conditions, sql` AND `)}
|
|
2585
|
+
ORDER BY distance ASC
|
|
2586
|
+
LIMIT ${vectorPredicate.limit}
|
|
2587
|
+
) AS vec_inner
|
|
2354
2588
|
)
|
|
2355
2589
|
`;
|
|
2356
2590
|
}
|
|
@@ -2380,37 +2614,130 @@ function compileVectorMinScoreCondition(distanceExpr, metric, minScore) {
|
|
|
2380
2614
|
}
|
|
2381
2615
|
}
|
|
2382
2616
|
}
|
|
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
|
-
|
|
2617
|
+
function buildStandardFulltextCte(input) {
|
|
2618
|
+
const { ctx, fulltextPredicate, graphId, nodeKinds } = input;
|
|
2619
|
+
const { dialect, schema } = ctx;
|
|
2620
|
+
const { query, mode, language, limit, minScore } = fulltextPredicate;
|
|
2621
|
+
if (nodeKinds.length === 0) {
|
|
2622
|
+
throw new UnsupportedPredicateError(
|
|
2623
|
+
"Fulltext predicate must resolve to at least one node kind"
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
const tableName = schema.tables.fulltext;
|
|
2627
|
+
const scopedNodes = buildScopedNodeIdsSubquery(fulltextPredicate.field.alias);
|
|
2628
|
+
const fulltextStrategy = dialect.fulltext;
|
|
2629
|
+
if (fulltextStrategy === void 0) {
|
|
2630
|
+
throw new UnsupportedPredicateError(
|
|
2631
|
+
`Fulltext match predicates are not supported for dialect "${dialect.name}"`
|
|
2632
|
+
);
|
|
2633
|
+
}
|
|
2634
|
+
const matchCondition = fulltextStrategy.matchCondition(
|
|
2635
|
+
tableName,
|
|
2636
|
+
query,
|
|
2637
|
+
mode,
|
|
2638
|
+
language
|
|
2639
|
+
);
|
|
2640
|
+
const rankExpression = fulltextStrategy.rankExpression(
|
|
2641
|
+
tableName,
|
|
2642
|
+
query,
|
|
2643
|
+
mode,
|
|
2644
|
+
language
|
|
2645
|
+
);
|
|
2646
|
+
const conditions = [
|
|
2647
|
+
sql`${qualifyColumn(tableName, "graph_id")} = ${graphId}`,
|
|
2648
|
+
compileKindFilter(qualifyColumn(tableName, "node_kind"), nodeKinds),
|
|
2649
|
+
matchCondition
|
|
2650
|
+
];
|
|
2651
|
+
if (minScore !== void 0) {
|
|
2652
|
+
conditions.push(sql`${rankExpression} >= ${minScore}`);
|
|
2653
|
+
}
|
|
2654
|
+
return sql`
|
|
2655
|
+
${sql.raw(FULLTEXT_CTE_ALIAS)} AS (
|
|
2656
|
+
SELECT
|
|
2657
|
+
node_id,
|
|
2658
|
+
node_kind,
|
|
2659
|
+
rank,
|
|
2660
|
+
ROW_NUMBER() OVER (ORDER BY rank DESC, node_id ASC) AS ord
|
|
2661
|
+
FROM (
|
|
2662
|
+
SELECT
|
|
2663
|
+
${qualifyColumn(tableName, "node_id")} AS node_id,
|
|
2664
|
+
${qualifyColumn(tableName, "node_kind")} AS node_kind,
|
|
2665
|
+
${rankExpression} AS rank
|
|
2666
|
+
FROM ${schema.fulltextTable}
|
|
2667
|
+
INNER JOIN ${scopedNodes}
|
|
2668
|
+
ON ${sql.raw(`${SCOPED_RELEVANCE_NODES_ALIAS}.node_id`)} =
|
|
2669
|
+
${qualifyColumn(tableName, "node_id")}
|
|
2670
|
+
AND ${sql.raw(`${SCOPED_RELEVANCE_NODES_ALIAS}.node_kind`)} =
|
|
2671
|
+
${qualifyColumn(tableName, "node_kind")}
|
|
2672
|
+
WHERE ${sql.join(conditions, sql` AND `)}
|
|
2673
|
+
ORDER BY rank DESC, ${qualifyColumn(tableName, "node_id")} ASC
|
|
2674
|
+
LIMIT ${limit}
|
|
2675
|
+
) AS fts_inner
|
|
2676
|
+
)
|
|
2677
|
+
`;
|
|
2678
|
+
}
|
|
2679
|
+
function buildStandardHybridCandidateCte() {
|
|
2680
|
+
return sql`
|
|
2681
|
+
${sql.raw(HYBRID_CANDIDATES_CTE_ALIAS)} AS (
|
|
2682
|
+
SELECT node_id, node_kind FROM ${sql.raw(EMBEDDINGS_CTE_ALIAS)}
|
|
2683
|
+
UNION
|
|
2684
|
+
SELECT node_id, node_kind FROM ${sql.raw(FULLTEXT_CTE_ALIAS)}
|
|
2685
|
+
)
|
|
2686
|
+
`;
|
|
2687
|
+
}
|
|
2688
|
+
function compileUserOrderBy(ast, dialect) {
|
|
2689
|
+
if (!ast.orderBy || ast.orderBy.length === 0) return [];
|
|
2690
|
+
const aliasToCte = buildAliasToCteMap(ast);
|
|
2691
|
+
const fragments = [];
|
|
2692
|
+
for (const orderSpec of ast.orderBy) {
|
|
2693
|
+
const valueType = orderSpec.field.valueType;
|
|
2694
|
+
if (valueType === "array" || valueType === "object") {
|
|
2695
|
+
throw new UnsupportedPredicateError(
|
|
2696
|
+
"Ordering by JSON arrays or objects is not supported"
|
|
2409
2697
|
);
|
|
2410
2698
|
}
|
|
2699
|
+
const cteAlias = aliasToCte.get(orderSpec.field.alias) ?? `${ALIAS_CTE_PREFIX}${orderSpec.field.alias}`;
|
|
2700
|
+
const field2 = compileFieldValue(
|
|
2701
|
+
orderSpec.field,
|
|
2702
|
+
dialect,
|
|
2703
|
+
valueType,
|
|
2704
|
+
cteAlias
|
|
2705
|
+
);
|
|
2706
|
+
const direction = sql.raw(orderSpec.direction.toUpperCase());
|
|
2707
|
+
const nulls = orderSpec.nulls ?? (orderSpec.direction === "asc" ? "last" : "first");
|
|
2708
|
+
const nullsDirection = sql.raw(nulls === "first" ? "DESC" : "ASC");
|
|
2709
|
+
fragments.push(
|
|
2710
|
+
sql`(${field2} IS NULL) ${nullsDirection}`,
|
|
2711
|
+
sql`${field2} ${direction}`
|
|
2712
|
+
);
|
|
2411
2713
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2714
|
+
return fragments;
|
|
2715
|
+
}
|
|
2716
|
+
function buildStandardFulltextOrderBy(input) {
|
|
2717
|
+
const { ast, dialect } = input;
|
|
2718
|
+
const rankOrder = sql.raw(`${FULLTEXT_CTE_ALIAS}.rank DESC`);
|
|
2719
|
+
const userOrders = compileUserOrderBy(ast, dialect);
|
|
2720
|
+
return sql`ORDER BY ${sql.join([rankOrder, ...userOrders], sql`, `)}`;
|
|
2721
|
+
}
|
|
2722
|
+
function buildStandardHybridRrfOrderBy(input) {
|
|
2723
|
+
const { ast, dialect, fusion } = input;
|
|
2724
|
+
const k = fusion?.k ?? DEFAULT_RRF_K;
|
|
2725
|
+
const vectorWeight = fusion?.weights?.vector ?? DEFAULT_RRF_WEIGHT;
|
|
2726
|
+
const fulltextWeight = fusion?.weights?.fulltext ?? DEFAULT_RRF_WEIGHT;
|
|
2727
|
+
const rrfOrder = sql.raw(
|
|
2728
|
+
`(COALESCE(${vectorWeight} / (${k} + ${EMBEDDINGS_CTE_ALIAS}.ord), 0) + COALESCE(${fulltextWeight} / (${k} + ${FULLTEXT_CTE_ALIAS}.ord), 0)) DESC`
|
|
2729
|
+
);
|
|
2730
|
+
const idTiebreak = sql.raw(
|
|
2731
|
+
`COALESCE(${FULLTEXT_CTE_ALIAS}.node_id, ${EMBEDDINGS_CTE_ALIAS}.node_id) ASC`
|
|
2732
|
+
);
|
|
2733
|
+
const userOrders = compileUserOrderBy(ast, dialect);
|
|
2734
|
+
return sql`ORDER BY ${sql.join([rrfOrder, ...userOrders, idTiebreak], sql`, `)}`;
|
|
2735
|
+
}
|
|
2736
|
+
function buildStandardVectorOrderBy(input) {
|
|
2737
|
+
const { ast, dialect } = input;
|
|
2738
|
+
const distanceOrder = sql.raw(`${EMBEDDINGS_CTE_ALIAS}.distance ASC`);
|
|
2739
|
+
const userOrders = compileUserOrderBy(ast, dialect);
|
|
2740
|
+
return sql`ORDER BY ${sql.join([distanceOrder, ...userOrders], sql`, `)}`;
|
|
2414
2741
|
}
|
|
2415
2742
|
function buildLimitOffsetClause(input) {
|
|
2416
2743
|
const { limit, offset } = input;
|
|
@@ -2424,6 +2751,74 @@ function buildLimitOffsetClause(input) {
|
|
|
2424
2751
|
return parts.length > 0 ? sql.join(parts, sql` `) : void 0;
|
|
2425
2752
|
}
|
|
2426
2753
|
|
|
2754
|
+
// src/query/compiler/passes/fulltext.ts
|
|
2755
|
+
function runFulltextPredicatePass(ast, dialect) {
|
|
2756
|
+
const fulltextPredicates = extractFulltextMatchPredicates(ast.predicates);
|
|
2757
|
+
if (fulltextPredicates.length > 1) {
|
|
2758
|
+
throw new UnsupportedPredicateError(
|
|
2759
|
+
"Multiple fulltext match predicates in a single query are not supported"
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
const fulltextPredicate = fulltextPredicates[0];
|
|
2763
|
+
if (fulltextPredicate === void 0) {
|
|
2764
|
+
return { fulltextPredicate: void 0 };
|
|
2765
|
+
}
|
|
2766
|
+
if (!dialect.capabilities.supportsFulltext) {
|
|
2767
|
+
throw new UnsupportedPredicateError(
|
|
2768
|
+
`Fulltext match predicates are not supported for dialect "${dialect.name}"`
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
const strategy = dialect.fulltext;
|
|
2772
|
+
if (strategy === void 0) {
|
|
2773
|
+
throw new UnsupportedPredicateError(
|
|
2774
|
+
`Dialect "${dialect.name}" advertises fulltext support but has no strategy configured`
|
|
2775
|
+
);
|
|
2776
|
+
}
|
|
2777
|
+
if (!strategy.supportedModes.includes(fulltextPredicate.mode)) {
|
|
2778
|
+
throw new UnsupportedPredicateError(
|
|
2779
|
+
`Fulltext query mode "${fulltextPredicate.mode}" is not supported by the "${strategy.name}" strategy. Supported modes: ${strategy.supportedModes.join(", ")}.`
|
|
2780
|
+
);
|
|
2781
|
+
}
|
|
2782
|
+
if (!Number.isFinite(fulltextPredicate.limit) || fulltextPredicate.limit <= 0) {
|
|
2783
|
+
throw new UnsupportedPredicateError(
|
|
2784
|
+
`Fulltext match limit must be a positive finite number, got ${String(fulltextPredicate.limit)}`
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
const { minScore } = fulltextPredicate;
|
|
2788
|
+
if (minScore !== void 0 && !Number.isFinite(minScore)) {
|
|
2789
|
+
throw new UnsupportedPredicateError(
|
|
2790
|
+
`Fulltext minScore must be a finite number, got ${String(minScore)}`
|
|
2791
|
+
);
|
|
2792
|
+
}
|
|
2793
|
+
return { fulltextPredicate };
|
|
2794
|
+
}
|
|
2795
|
+
function resolveFulltextAwareLimit(astLimit, fulltextPredicate) {
|
|
2796
|
+
if (fulltextPredicate === void 0) {
|
|
2797
|
+
return astLimit;
|
|
2798
|
+
}
|
|
2799
|
+
if (astLimit === void 0) {
|
|
2800
|
+
return fulltextPredicate.limit;
|
|
2801
|
+
}
|
|
2802
|
+
return Math.min(astLimit, fulltextPredicate.limit);
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// src/query/compiler/passes/fusion.ts
|
|
2806
|
+
function runFusionConfigPass(ast, vectorPredicate, fulltextPredicate) {
|
|
2807
|
+
const fusion = ast.fusion;
|
|
2808
|
+
if (fusion === void 0) return { fusion: void 0 };
|
|
2809
|
+
if (vectorPredicate === void 0 || fulltextPredicate === void 0) {
|
|
2810
|
+
throw new UnsupportedPredicateError(
|
|
2811
|
+
`.fuseWith() requires both a .similarTo() and a .$fulltext.matches() predicate in the same query. Remove .fuseWith(), or add the missing predicate.`
|
|
2812
|
+
);
|
|
2813
|
+
}
|
|
2814
|
+
if (vectorPredicate.field.alias !== fulltextPredicate.field.alias) {
|
|
2815
|
+
throw new UnsupportedPredicateError(
|
|
2816
|
+
`.fuseWith() requires .similarTo() and .$fulltext.matches() to target the same node alias. Got vector=${vectorPredicate.field.alias}, fulltext=${fulltextPredicate.field.alias}.`
|
|
2817
|
+
);
|
|
2818
|
+
}
|
|
2819
|
+
return { fusion };
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2427
2822
|
// src/query/compiler/passes/recursive.ts
|
|
2428
2823
|
function runRecursiveTraversalSelectionPass(ast) {
|
|
2429
2824
|
const variableLengthTraversal = ast.traversals.find(
|
|
@@ -2693,6 +3088,14 @@ function lowerStandardQueryToLogicalPlanNode(input) {
|
|
|
2693
3088
|
predicate: input.vectorPredicate
|
|
2694
3089
|
};
|
|
2695
3090
|
}
|
|
3091
|
+
if (input.fulltextPredicate !== void 0) {
|
|
3092
|
+
currentNode = {
|
|
3093
|
+
id: nextPlanNodeId(),
|
|
3094
|
+
input: currentNode,
|
|
3095
|
+
op: "fulltext_match",
|
|
3096
|
+
predicate: input.fulltextPredicate
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
2696
3099
|
return appendAggregateSortLimitAndProjectNodes(
|
|
2697
3100
|
currentNode,
|
|
2698
3101
|
ast,
|
|
@@ -2770,17 +3173,30 @@ function lowerComposableQueryToLogicalPlanNode(query, dialect, graphId, nextPlan
|
|
|
2770
3173
|
nextPlanNodeId
|
|
2771
3174
|
});
|
|
2772
3175
|
}
|
|
3176
|
+
const dialectAdapter = getDialect(dialect);
|
|
2773
3177
|
const vectorPredicate = runVectorPredicatePass(
|
|
2774
3178
|
query,
|
|
2775
|
-
|
|
3179
|
+
dialectAdapter
|
|
2776
3180
|
).vectorPredicate;
|
|
2777
|
-
const
|
|
3181
|
+
const fulltextPredicate = runFulltextPredicatePass(
|
|
3182
|
+
query,
|
|
3183
|
+
dialectAdapter
|
|
3184
|
+
).fulltextPredicate;
|
|
3185
|
+
const limitAfterVector = resolveVectorAwareLimit(
|
|
3186
|
+
query.limit,
|
|
3187
|
+
vectorPredicate
|
|
3188
|
+
);
|
|
3189
|
+
const effectiveLimit = resolveFulltextAwareLimit(
|
|
3190
|
+
limitAfterVector,
|
|
3191
|
+
fulltextPredicate
|
|
3192
|
+
);
|
|
2778
3193
|
const loweringInput = {
|
|
2779
3194
|
ast: query,
|
|
2780
3195
|
graphId,
|
|
2781
3196
|
nextPlanNodeId,
|
|
2782
3197
|
...effectiveLimit === void 0 ? {} : { effectiveLimit },
|
|
2783
|
-
...vectorPredicate === void 0 ? {} : { vectorPredicate }
|
|
3198
|
+
...vectorPredicate === void 0 ? {} : { vectorPredicate },
|
|
3199
|
+
...fulltextPredicate === void 0 ? {} : { fulltextPredicate }
|
|
2784
3200
|
};
|
|
2785
3201
|
return lowerStandardQueryToLogicalPlanNode(loweringInput);
|
|
2786
3202
|
}
|
|
@@ -3287,10 +3703,10 @@ function compileRecursiveProjection(ast, traversal, dialect) {
|
|
|
3287
3703
|
sql`${sql.raw(nodeAlias)}_deleted_at`
|
|
3288
3704
|
];
|
|
3289
3705
|
if (vl.depthAlias !== void 0) {
|
|
3290
|
-
fields.push(sql`depth AS ${
|
|
3706
|
+
fields.push(sql`depth AS ${quoteIdentifier2(vl.depthAlias)}`);
|
|
3291
3707
|
}
|
|
3292
3708
|
if (vl.pathAlias !== void 0) {
|
|
3293
|
-
fields.push(sql`path AS ${
|
|
3709
|
+
fields.push(sql`path AS ${quoteIdentifier2(vl.pathAlias)}`);
|
|
3294
3710
|
}
|
|
3295
3711
|
return sql.join(fields, sql`, `);
|
|
3296
3712
|
}
|
|
@@ -3304,7 +3720,7 @@ function compileRecursiveSelectiveProjection(fields, ast, traversal, dialect) {
|
|
|
3304
3720
|
}
|
|
3305
3721
|
if (field2.isSystemField) {
|
|
3306
3722
|
const dbColumn = mapSelectiveSystemFieldToColumn(field2.field);
|
|
3307
|
-
return sql`${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${
|
|
3723
|
+
return sql`${sql.raw(`${field2.alias}_${dbColumn}`)} AS ${quoteIdentifier2(field2.outputName)}`;
|
|
3308
3724
|
}
|
|
3309
3725
|
const column = sql.raw(`${field2.alias}_props`);
|
|
3310
3726
|
const extracted = compileTypedJsonExtract({
|
|
@@ -3313,14 +3729,14 @@ function compileRecursiveSelectiveProjection(fields, ast, traversal, dialect) {
|
|
|
3313
3729
|
pointer: jsonPointer([field2.field]),
|
|
3314
3730
|
valueType: field2.valueType
|
|
3315
3731
|
});
|
|
3316
|
-
return sql`${extracted} AS ${
|
|
3732
|
+
return sql`${extracted} AS ${quoteIdentifier2(field2.outputName)}`;
|
|
3317
3733
|
});
|
|
3318
3734
|
const vl = traversal.variableLength;
|
|
3319
3735
|
if (vl.depthAlias !== void 0) {
|
|
3320
|
-
columns.push(sql`depth AS ${
|
|
3736
|
+
columns.push(sql`depth AS ${quoteIdentifier2(vl.depthAlias)}`);
|
|
3321
3737
|
}
|
|
3322
3738
|
if (vl.pathAlias !== void 0) {
|
|
3323
|
-
columns.push(sql`path AS ${
|
|
3739
|
+
columns.push(sql`path AS ${quoteIdentifier2(vl.pathAlias)}`);
|
|
3324
3740
|
}
|
|
3325
3741
|
return sql.join(columns, sql`, `);
|
|
3326
3742
|
}
|
|
@@ -3357,43 +3773,6 @@ function compileLimitOffset(ast) {
|
|
|
3357
3773
|
}
|
|
3358
3774
|
return parts.length > 0 ? sql.join(parts, sql` `) : void 0;
|
|
3359
3775
|
}
|
|
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
|
-
};
|
|
3395
|
-
}
|
|
3396
|
-
var DEFAULT_SQL_SCHEMA = createSqlSchema();
|
|
3397
3776
|
|
|
3398
3777
|
// src/query/execution/value-decoder.ts
|
|
3399
3778
|
function nullToUndefined(value) {
|
|
@@ -3451,6 +3830,116 @@ function decodeByValueType(value, valueType) {
|
|
|
3451
3830
|
}
|
|
3452
3831
|
}
|
|
3453
3832
|
}
|
|
3833
|
+
function buildReachableCte(options) {
|
|
3834
|
+
const trackPath = options.cyclePolicy === "prevent" || options.includePath;
|
|
3835
|
+
const edgeKindFilter = compileKindFilter(
|
|
3836
|
+
sql.raw("e.kind"),
|
|
3837
|
+
options.edgeKinds
|
|
3838
|
+
);
|
|
3839
|
+
const currentTimestamp = options.dialect.currentTimestamp();
|
|
3840
|
+
const nodeTemporalFilter = compileTemporalFilter({
|
|
3841
|
+
mode: options.temporalMode,
|
|
3842
|
+
asOf: options.asOf,
|
|
3843
|
+
tableAlias: "n",
|
|
3844
|
+
currentTimestamp
|
|
3845
|
+
});
|
|
3846
|
+
const edgeTemporalFilter = compileTemporalFilter({
|
|
3847
|
+
mode: options.temporalMode,
|
|
3848
|
+
asOf: options.asOf,
|
|
3849
|
+
tableAlias: "e",
|
|
3850
|
+
currentTimestamp
|
|
3851
|
+
});
|
|
3852
|
+
const initialPath = trackPath ? options.dialect.initializePath(sql.raw("n.id")) : void 0;
|
|
3853
|
+
const pathExtension = trackPath ? options.dialect.extendPath(sql.raw("r.path"), sql.raw("n.id")) : void 0;
|
|
3854
|
+
const cycleCheck = options.cyclePolicy === "prevent" ? options.dialect.cycleCheck(sql.raw("n.id"), sql.raw("r.path")) : void 0;
|
|
3855
|
+
const baseColumns = [sql`n.id`, sql`n.kind`, sql`0 AS depth`];
|
|
3856
|
+
if (initialPath !== void 0) {
|
|
3857
|
+
baseColumns.push(sql`${initialPath} AS path`);
|
|
3858
|
+
}
|
|
3859
|
+
const baseCase = sql`SELECT ${sql.join(baseColumns, sql`, `)} FROM ${options.schema.nodesTable} n WHERE n.graph_id = ${options.graphId} AND n.id = ${options.sourceId} AND ${nodeTemporalFilter}`;
|
|
3860
|
+
const recursiveColumns = [
|
|
3861
|
+
sql`n.id`,
|
|
3862
|
+
sql`n.kind`,
|
|
3863
|
+
sql`r.depth + 1 AS depth`
|
|
3864
|
+
];
|
|
3865
|
+
if (pathExtension !== void 0) {
|
|
3866
|
+
recursiveColumns.push(sql`${pathExtension} AS path`);
|
|
3867
|
+
}
|
|
3868
|
+
const recursiveWhere = [
|
|
3869
|
+
sql`e.graph_id = ${options.graphId}`,
|
|
3870
|
+
edgeKindFilter,
|
|
3871
|
+
edgeTemporalFilter,
|
|
3872
|
+
nodeTemporalFilter,
|
|
3873
|
+
sql`r.depth < ${options.maxHops}`
|
|
3874
|
+
];
|
|
3875
|
+
if (cycleCheck !== void 0) recursiveWhere.push(cycleCheck);
|
|
3876
|
+
const forceWorktableOuterJoinOrder = options.dialect.capabilities.forceRecursiveWorktableOuterJoinOrder;
|
|
3877
|
+
const recursiveCase = compileRecursiveBranch({
|
|
3878
|
+
recursiveColumns,
|
|
3879
|
+
whereClauses: recursiveWhere,
|
|
3880
|
+
direction: options.direction,
|
|
3881
|
+
forceWorktableOuterJoinOrder,
|
|
3882
|
+
schema: options.schema
|
|
3883
|
+
});
|
|
3884
|
+
return sql`WITH RECURSIVE reachable AS (${baseCase} UNION ALL ${recursiveCase})`;
|
|
3885
|
+
}
|
|
3886
|
+
function compileRecursiveBranch(options) {
|
|
3887
|
+
const selectClause = sql`SELECT ${sql.join([...options.recursiveColumns], sql`, `)}`;
|
|
3888
|
+
switch (options.direction) {
|
|
3889
|
+
case "out": {
|
|
3890
|
+
return buildDirectionalBranch({
|
|
3891
|
+
selectClause,
|
|
3892
|
+
whereClauses: options.whereClauses,
|
|
3893
|
+
joinField: "from_id",
|
|
3894
|
+
targetField: "to_id",
|
|
3895
|
+
targetKindField: "to_kind",
|
|
3896
|
+
forceWorktableOuterJoinOrder: options.forceWorktableOuterJoinOrder,
|
|
3897
|
+
schema: options.schema
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
case "in": {
|
|
3901
|
+
return buildDirectionalBranch({
|
|
3902
|
+
selectClause,
|
|
3903
|
+
whereClauses: options.whereClauses,
|
|
3904
|
+
joinField: "to_id",
|
|
3905
|
+
targetField: "from_id",
|
|
3906
|
+
targetKindField: "from_kind",
|
|
3907
|
+
forceWorktableOuterJoinOrder: options.forceWorktableOuterJoinOrder,
|
|
3908
|
+
schema: options.schema
|
|
3909
|
+
});
|
|
3910
|
+
}
|
|
3911
|
+
case "both": {
|
|
3912
|
+
return buildBidirectionalBranch({
|
|
3913
|
+
selectClause,
|
|
3914
|
+
whereClauses: options.whereClauses,
|
|
3915
|
+
forceWorktableOuterJoinOrder: options.forceWorktableOuterJoinOrder,
|
|
3916
|
+
schema: options.schema
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
function buildDirectionalBranch(options) {
|
|
3922
|
+
const nodeJoin = sql`JOIN ${options.schema.nodesTable} n ON n.graph_id = e.graph_id AND n.id = e.${sql.raw(options.targetField)} AND n.kind = e.${sql.raw(options.targetKindField)}`;
|
|
3923
|
+
if (options.forceWorktableOuterJoinOrder) {
|
|
3924
|
+
const allWhere = [
|
|
3925
|
+
...options.whereClauses,
|
|
3926
|
+
sql`e.${sql.raw(options.joinField)} = r.id`
|
|
3927
|
+
];
|
|
3928
|
+
return sql`${options.selectClause} FROM reachable r CROSS JOIN ${options.schema.edgesTable} e ${nodeJoin} WHERE ${sql.join(allWhere, sql` AND `)}`;
|
|
3929
|
+
}
|
|
3930
|
+
return sql`${options.selectClause} FROM reachable r JOIN ${options.schema.edgesTable} e ON e.${sql.raw(options.joinField)} = r.id ${nodeJoin} WHERE ${sql.join([...options.whereClauses], sql` AND `)}`;
|
|
3931
|
+
}
|
|
3932
|
+
function buildBidirectionalBranch(options) {
|
|
3933
|
+
const nodeJoin = sql`JOIN ${options.schema.nodesTable} n ON n.graph_id = e.graph_id AND ((e.to_id = r.id AND n.id = e.from_id AND n.kind = e.from_kind) OR (e.from_id = r.id AND n.id = e.to_id AND n.kind = e.to_kind))`;
|
|
3934
|
+
if (options.forceWorktableOuterJoinOrder) {
|
|
3935
|
+
const allWhere = [
|
|
3936
|
+
...options.whereClauses,
|
|
3937
|
+
sql`(e.from_id = r.id OR e.to_id = r.id)`
|
|
3938
|
+
];
|
|
3939
|
+
return sql`${options.selectClause} FROM reachable r CROSS JOIN ${options.schema.edgesTable} e ${nodeJoin} WHERE ${sql.join(allWhere, sql` AND `)}`;
|
|
3940
|
+
}
|
|
3941
|
+
return sql`${options.selectClause} FROM reachable r JOIN ${options.schema.edgesTable} e ON (e.from_id = r.id OR e.to_id = r.id) ${nodeJoin} WHERE ${sql.join([...options.whereClauses], sql` AND `)}`;
|
|
3942
|
+
}
|
|
3454
3943
|
|
|
3455
3944
|
// src/store/row-mappers.ts
|
|
3456
3945
|
function nullToUndefined2(value) {
|
|
@@ -3541,6 +4030,8 @@ async function executeSubgraph(params) {
|
|
|
3541
4030
|
excludeRoot: options.excludeRoot ?? false,
|
|
3542
4031
|
direction: options.direction ?? "out",
|
|
3543
4032
|
cyclePolicy: options.cyclePolicy ?? "prevent",
|
|
4033
|
+
temporalMode: options.temporalMode ?? params.graph.defaults.temporalMode,
|
|
4034
|
+
asOf: options.asOf,
|
|
3544
4035
|
dialect: params.dialect,
|
|
3545
4036
|
schema: params.schema ?? DEFAULT_SQL_SCHEMA,
|
|
3546
4037
|
backend: params.backend
|
|
@@ -3558,7 +4049,19 @@ async function executeSubgraph(params) {
|
|
|
3558
4049
|
(kind, field2) => schemaIntrospector.getEdgeFieldTypeInfo(kind, field2),
|
|
3559
4050
|
"edge"
|
|
3560
4051
|
);
|
|
3561
|
-
const reachableCte = buildReachableCte(
|
|
4052
|
+
const reachableCte = buildReachableCte({
|
|
4053
|
+
graphId: ctx.graphId,
|
|
4054
|
+
sourceId: ctx.rootId,
|
|
4055
|
+
edgeKinds: ctx.edgeKinds,
|
|
4056
|
+
maxHops: ctx.maxDepth,
|
|
4057
|
+
direction: ctx.direction,
|
|
4058
|
+
cyclePolicy: ctx.cyclePolicy,
|
|
4059
|
+
includePath: false,
|
|
4060
|
+
temporalMode: ctx.temporalMode,
|
|
4061
|
+
...ctx.asOf !== void 0 && { asOf: ctx.asOf },
|
|
4062
|
+
dialect: ctx.dialect,
|
|
4063
|
+
schema: ctx.schema
|
|
4064
|
+
});
|
|
3562
4065
|
const includedIdsCte = buildIncludedIdsCte(ctx);
|
|
3563
4066
|
const [nodeRows, edgeRows] = await Promise.all([
|
|
3564
4067
|
fetchSubgraphNodes(ctx, reachableCte, includedIdsCte, nodeProjectionPlan),
|
|
@@ -3574,12 +4077,7 @@ async function executeSubgraph(params) {
|
|
|
3574
4077
|
for (const row of edgeRows) {
|
|
3575
4078
|
const edge = mapSubgraphEdgeRow(row, edgeProjectionPlan);
|
|
3576
4079
|
insertAdjacencyEntry(adjacency, edge.fromId, edge.kind, edge);
|
|
3577
|
-
insertAdjacencyEntry(
|
|
3578
|
-
reverseAdjacency,
|
|
3579
|
-
edge.toId,
|
|
3580
|
-
edge.kind,
|
|
3581
|
-
edge
|
|
3582
|
-
);
|
|
4080
|
+
insertAdjacencyEntry(reverseAdjacency, edge.toId, edge.kind, edge);
|
|
3583
4081
|
}
|
|
3584
4082
|
const root = nodesMap.get(ctx.rootId);
|
|
3585
4083
|
return {
|
|
@@ -3656,79 +4154,6 @@ function getIncludedNodeKinds(graph, includeKinds) {
|
|
|
3656
4154
|
function dedupeStrings(values) {
|
|
3657
4155
|
return [...new Set(values)];
|
|
3658
4156
|
}
|
|
3659
|
-
function buildReachableCte(ctx) {
|
|
3660
|
-
const shouldTrackPath = ctx.cyclePolicy === "prevent";
|
|
3661
|
-
const edgeKindFilter = compileKindFilter(sql.raw("e.kind"), ctx.edgeKinds);
|
|
3662
|
-
const initialPath = shouldTrackPath ? ctx.dialect.initializePath(sql.raw("n.id")) : void 0;
|
|
3663
|
-
const pathExtension = shouldTrackPath ? ctx.dialect.extendPath(sql.raw("r.path"), sql.raw("n.id")) : void 0;
|
|
3664
|
-
const cycleCheck = shouldTrackPath ? ctx.dialect.cycleCheck(sql.raw("n.id"), sql.raw("r.path")) : void 0;
|
|
3665
|
-
const baseColumns = [sql`n.id`, sql`n.kind`, sql`0 AS depth`];
|
|
3666
|
-
if (initialPath !== void 0) {
|
|
3667
|
-
baseColumns.push(sql`${initialPath} AS path`);
|
|
3668
|
-
}
|
|
3669
|
-
const baseCase = sql`SELECT ${sql.join(baseColumns, sql`, `)} FROM ${ctx.schema.nodesTable} n WHERE n.graph_id = ${ctx.graphId} AND n.id = ${ctx.rootId} AND n.deleted_at IS NULL`;
|
|
3670
|
-
const recursiveColumns = [
|
|
3671
|
-
sql`n.id`,
|
|
3672
|
-
sql`n.kind`,
|
|
3673
|
-
sql`r.depth + 1 AS depth`
|
|
3674
|
-
];
|
|
3675
|
-
if (pathExtension !== void 0) {
|
|
3676
|
-
recursiveColumns.push(sql`${pathExtension} AS path`);
|
|
3677
|
-
}
|
|
3678
|
-
const recursiveWhereClauses = [
|
|
3679
|
-
sql`e.graph_id = ${ctx.graphId}`,
|
|
3680
|
-
edgeKindFilter,
|
|
3681
|
-
sql`e.deleted_at IS NULL`,
|
|
3682
|
-
sql`n.deleted_at IS NULL`,
|
|
3683
|
-
sql`r.depth < ${ctx.maxDepth}`
|
|
3684
|
-
];
|
|
3685
|
-
if (cycleCheck !== void 0) {
|
|
3686
|
-
recursiveWhereClauses.push(cycleCheck);
|
|
3687
|
-
}
|
|
3688
|
-
const forceWorktableOuterJoinOrder = ctx.dialect.capabilities.forceRecursiveWorktableOuterJoinOrder;
|
|
3689
|
-
const recursiveCase = ctx.direction === "both" ? compileBidirectionalBranch({
|
|
3690
|
-
recursiveColumns,
|
|
3691
|
-
whereClauses: recursiveWhereClauses,
|
|
3692
|
-
forceWorktableOuterJoinOrder,
|
|
3693
|
-
schema: ctx.schema
|
|
3694
|
-
}) : compileRecursiveBranch({
|
|
3695
|
-
recursiveColumns,
|
|
3696
|
-
whereClauses: recursiveWhereClauses,
|
|
3697
|
-
joinField: "from_id",
|
|
3698
|
-
targetField: "to_id",
|
|
3699
|
-
targetKindField: "to_kind",
|
|
3700
|
-
forceWorktableOuterJoinOrder,
|
|
3701
|
-
schema: ctx.schema
|
|
3702
|
-
});
|
|
3703
|
-
return sql`WITH RECURSIVE reachable AS (${baseCase} UNION ALL ${recursiveCase})`;
|
|
3704
|
-
}
|
|
3705
|
-
function compileRecursiveBranch(params) {
|
|
3706
|
-
const columns = [...params.recursiveColumns];
|
|
3707
|
-
const selectClause = sql`SELECT ${sql.join(columns, sql`, `)}`;
|
|
3708
|
-
const nodeJoin = sql`JOIN ${params.schema.nodesTable} n ON n.graph_id = e.graph_id AND n.id = e.${sql.raw(params.targetField)} AND n.kind = e.${sql.raw(params.targetKindField)}`;
|
|
3709
|
-
if (params.forceWorktableOuterJoinOrder) {
|
|
3710
|
-
const allWhere = [
|
|
3711
|
-
...params.whereClauses,
|
|
3712
|
-
sql`e.${sql.raw(params.joinField)} = r.id`
|
|
3713
|
-
];
|
|
3714
|
-
return sql`${selectClause} FROM reachable r CROSS JOIN ${params.schema.edgesTable} e ${nodeJoin} WHERE ${sql.join(allWhere, sql` AND `)}`;
|
|
3715
|
-
}
|
|
3716
|
-
const where = [...params.whereClauses];
|
|
3717
|
-
return sql`${selectClause} FROM reachable r JOIN ${params.schema.edgesTable} e ON e.${sql.raw(params.joinField)} = r.id ${nodeJoin} WHERE ${sql.join(where, sql` AND `)}`;
|
|
3718
|
-
}
|
|
3719
|
-
function compileBidirectionalBranch(params) {
|
|
3720
|
-
const columns = [...params.recursiveColumns];
|
|
3721
|
-
const selectClause = sql`SELECT ${sql.join(columns, sql`, `)}`;
|
|
3722
|
-
const nodeJoin = sql`JOIN ${params.schema.nodesTable} n ON n.graph_id = e.graph_id AND ((e.to_id = r.id AND n.id = e.from_id AND n.kind = e.from_kind) OR (e.from_id = r.id AND n.id = e.to_id AND n.kind = e.to_kind))`;
|
|
3723
|
-
if (params.forceWorktableOuterJoinOrder) {
|
|
3724
|
-
const allWhere = [
|
|
3725
|
-
...params.whereClauses,
|
|
3726
|
-
sql`(e.from_id = r.id OR e.to_id = r.id)`
|
|
3727
|
-
];
|
|
3728
|
-
return sql`${selectClause} FROM reachable r CROSS JOIN ${params.schema.edgesTable} e ${nodeJoin} WHERE ${sql.join(allWhere, sql` AND `)}`;
|
|
3729
|
-
}
|
|
3730
|
-
return sql`${selectClause} FROM reachable r JOIN ${params.schema.edgesTable} e ON (e.from_id = r.id OR e.to_id = r.id) ${nodeJoin} WHERE ${sql.join([...params.whereClauses], sql` AND `)}`;
|
|
3731
|
-
}
|
|
3732
4157
|
function buildIncludedIdsCte(ctx) {
|
|
3733
4158
|
const filters = [];
|
|
3734
4159
|
if (ctx.includeKinds !== void 0 && ctx.includeKinds.length > 0) {
|
|
@@ -3760,6 +4185,12 @@ async function fetchSubgraphNodes(ctx, reachableCte, includedIdsCte, projectionP
|
|
|
3760
4185
|
}
|
|
3761
4186
|
async function fetchSubgraphEdges(ctx, reachableCte, includedIdsCte, projectionPlan) {
|
|
3762
4187
|
const edgeKindFilter = compileKindFilter(sql.raw("e.kind"), ctx.edgeKinds);
|
|
4188
|
+
const edgeTemporalFilter = compileTemporalFilter({
|
|
4189
|
+
mode: ctx.temporalMode,
|
|
4190
|
+
asOf: ctx.asOf,
|
|
4191
|
+
tableAlias: "e",
|
|
4192
|
+
currentTimestamp: ctx.dialect.currentTimestamp()
|
|
4193
|
+
});
|
|
3763
4194
|
const columns = [
|
|
3764
4195
|
sql`e.id`,
|
|
3765
4196
|
sql`e.kind`,
|
|
@@ -3777,7 +4208,7 @@ async function fetchSubgraphEdges(ctx, reachableCte, includedIdsCte, projectionP
|
|
|
3777
4208
|
]),
|
|
3778
4209
|
...buildProjectedPropertyColumns("e", projectionPlan, ctx.dialect)
|
|
3779
4210
|
];
|
|
3780
|
-
const query = sql`${reachableCte}${includedIdsCte} SELECT ${sql.join(columns, sql`, `)} FROM ${ctx.schema.edgesTable} e WHERE e.graph_id = ${ctx.graphId} AND ${edgeKindFilter} AND
|
|
4211
|
+
const query = sql`${reachableCte}${includedIdsCte} SELECT ${sql.join(columns, sql`, `)} FROM ${ctx.schema.edgesTable} e WHERE e.graph_id = ${ctx.graphId} AND ${edgeKindFilter} AND ${edgeTemporalFilter} AND e.from_id IN (SELECT id FROM included_ids) AND e.to_id IN (SELECT id FROM included_ids)`;
|
|
3781
4212
|
return ctx.backend.execute(query);
|
|
3782
4213
|
}
|
|
3783
4214
|
function buildMetadataColumns(alias, plan, columns) {
|
|
@@ -3820,7 +4251,7 @@ function buildProjectedPropertyColumns(alias, plan, dialect) {
|
|
|
3820
4251
|
valueType: fieldPlan.typeInfo?.valueType
|
|
3821
4252
|
});
|
|
3822
4253
|
columns.push(
|
|
3823
|
-
sql`CASE WHEN ${sql.raw(alias)}.kind = ${kind} THEN ${extracted} ELSE NULL END AS ${
|
|
4254
|
+
sql`CASE WHEN ${sql.raw(alias)}.kind = ${kind} THEN ${extracted} ELSE NULL END AS ${quoteIdentifier2(fieldPlan.outputName)}`
|
|
3824
4255
|
);
|
|
3825
4256
|
}
|
|
3826
4257
|
}
|
|
@@ -3887,16 +4318,6 @@ function mapSubgraphEdgeRow(row, projectionPlan) {
|
|
|
3887
4318
|
applyProjectedFields(projectedEdge, row, kindPlan);
|
|
3888
4319
|
return projectedEdge;
|
|
3889
4320
|
}
|
|
3890
|
-
|
|
3891
|
-
// src/query/ast.ts
|
|
3892
|
-
function mergeEdgeKinds(traversal) {
|
|
3893
|
-
const inverse = traversal.inverseEdgeKinds;
|
|
3894
|
-
if (inverse === void 0 || inverse.length === 0) return traversal.edgeKinds;
|
|
3895
|
-
return [
|
|
3896
|
-
...traversal.edgeKinds,
|
|
3897
|
-
...inverse.filter((kind) => !traversal.edgeKinds.includes(kind))
|
|
3898
|
-
];
|
|
3899
|
-
}
|
|
3900
4321
|
var OPERATOR_MAP = {
|
|
3901
4322
|
union: "UNION",
|
|
3902
4323
|
unionAll: "UNION ALL",
|
|
@@ -4053,6 +4474,9 @@ function validateSqliteSetOpLeaf(ast) {
|
|
|
4053
4474
|
if (hasVectorSimilarityPredicates(ast)) {
|
|
4054
4475
|
unsupported.push("vector similarity predicates");
|
|
4055
4476
|
}
|
|
4477
|
+
if (hasFulltextMatchPredicates(ast)) {
|
|
4478
|
+
unsupported.push("fulltext match predicates");
|
|
4479
|
+
}
|
|
4056
4480
|
if (ast.groupBy !== void 0) {
|
|
4057
4481
|
unsupported.push("GROUP BY");
|
|
4058
4482
|
}
|
|
@@ -4102,7 +4526,42 @@ function hasVectorSimilarityInExpression(expr) {
|
|
|
4102
4526
|
case "between":
|
|
4103
4527
|
case "array_op":
|
|
4104
4528
|
case "object_op":
|
|
4105
|
-
case "aggregate_comparison":
|
|
4529
|
+
case "aggregate_comparison":
|
|
4530
|
+
case "fulltext_match": {
|
|
4531
|
+
return false;
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
return false;
|
|
4536
|
+
}
|
|
4537
|
+
function hasFulltextMatchPredicates(ast) {
|
|
4538
|
+
return ast.predicates.some(
|
|
4539
|
+
(predicate2) => hasFulltextMatchInExpression(predicate2.expression)
|
|
4540
|
+
);
|
|
4541
|
+
}
|
|
4542
|
+
function hasFulltextMatchInExpression(expr) {
|
|
4543
|
+
if ("__type" in expr) {
|
|
4544
|
+
switch (expr.__type) {
|
|
4545
|
+
case "and":
|
|
4546
|
+
case "or": {
|
|
4547
|
+
return expr.predicates.some((p) => hasFulltextMatchInExpression(p));
|
|
4548
|
+
}
|
|
4549
|
+
case "not": {
|
|
4550
|
+
return hasFulltextMatchInExpression(expr.predicate);
|
|
4551
|
+
}
|
|
4552
|
+
case "fulltext_match": {
|
|
4553
|
+
return true;
|
|
4554
|
+
}
|
|
4555
|
+
case "exists":
|
|
4556
|
+
case "in_subquery":
|
|
4557
|
+
case "comparison":
|
|
4558
|
+
case "string_op":
|
|
4559
|
+
case "null_check":
|
|
4560
|
+
case "between":
|
|
4561
|
+
case "array_op":
|
|
4562
|
+
case "object_op":
|
|
4563
|
+
case "aggregate_comparison":
|
|
4564
|
+
case "vector_similarity": {
|
|
4106
4565
|
return false;
|
|
4107
4566
|
}
|
|
4108
4567
|
}
|
|
@@ -4138,7 +4597,8 @@ function hasSubqueryInExpression(expr) {
|
|
|
4138
4597
|
case "array_op":
|
|
4139
4598
|
case "object_op":
|
|
4140
4599
|
case "aggregate_comparison":
|
|
4141
|
-
case "vector_similarity":
|
|
4600
|
+
case "vector_similarity":
|
|
4601
|
+
case "fulltext_match": {
|
|
4142
4602
|
return false;
|
|
4143
4603
|
}
|
|
4144
4604
|
}
|
|
@@ -4416,6 +4876,10 @@ function markPredicateFieldsAsRequired(requiredColumnsByAlias, expression) {
|
|
|
4416
4876
|
markFieldRefAsRequired(requiredColumnsByAlias, expression.field);
|
|
4417
4877
|
return;
|
|
4418
4878
|
}
|
|
4879
|
+
case "fulltext_match": {
|
|
4880
|
+
markFieldRefAsRequired(requiredColumnsByAlias, expression.field);
|
|
4881
|
+
return;
|
|
4882
|
+
}
|
|
4419
4883
|
case "exists": {
|
|
4420
4884
|
return;
|
|
4421
4885
|
}
|
|
@@ -4480,7 +4944,8 @@ function hasIdEqualityPredicate(expression, alias) {
|
|
|
4480
4944
|
case "aggregate_comparison":
|
|
4481
4945
|
case "exists":
|
|
4482
4946
|
case "in_subquery":
|
|
4483
|
-
case "vector_similarity":
|
|
4947
|
+
case "vector_similarity":
|
|
4948
|
+
case "fulltext_match": {
|
|
4484
4949
|
return false;
|
|
4485
4950
|
}
|
|
4486
4951
|
}
|
|
@@ -4521,10 +4986,13 @@ function resolveTraversalCteLimit(ast, predicateIndex) {
|
|
|
4521
4986
|
);
|
|
4522
4987
|
return Math.max(ast.limit, pushdownLimit);
|
|
4523
4988
|
}
|
|
4524
|
-
function canCollapseSelectiveTraversalRowset(ast, vectorPredicate) {
|
|
4989
|
+
function canCollapseSelectiveTraversalRowset(ast, vectorPredicate, fulltextPredicate) {
|
|
4525
4990
|
if (vectorPredicate !== void 0) {
|
|
4526
4991
|
return false;
|
|
4527
4992
|
}
|
|
4993
|
+
if (fulltextPredicate !== void 0) {
|
|
4994
|
+
return false;
|
|
4995
|
+
}
|
|
4528
4996
|
if (ast.traversals.length === 0) {
|
|
4529
4997
|
return false;
|
|
4530
4998
|
}
|
|
@@ -4564,6 +5032,8 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
|
|
|
4564
5032
|
collapsedTraversalCteAlias: void 0,
|
|
4565
5033
|
ctx,
|
|
4566
5034
|
effectiveLimit: void 0,
|
|
5035
|
+
fulltextPredicate: void 0,
|
|
5036
|
+
fusion: void 0,
|
|
4567
5037
|
logicalPlan: void 0,
|
|
4568
5038
|
predicateIndex: buildPredicateIndex(ast),
|
|
4569
5039
|
requiredColumnsByAlias: void 0,
|
|
@@ -4577,14 +5047,47 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
|
|
|
4577
5047
|
execute(currentState) {
|
|
4578
5048
|
return runVectorPredicatePass(currentState.ast, currentState.ctx.dialect).vectorPredicate;
|
|
4579
5049
|
},
|
|
4580
|
-
update(currentState, vectorPredicate) {
|
|
5050
|
+
update(currentState, vectorPredicate) {
|
|
5051
|
+
return {
|
|
5052
|
+
...currentState,
|
|
5053
|
+
vectorPredicate
|
|
5054
|
+
};
|
|
5055
|
+
}
|
|
5056
|
+
});
|
|
5057
|
+
state = vectorPass.state;
|
|
5058
|
+
const fulltextPass = runCompilerPass(state, {
|
|
5059
|
+
name: "fulltext_predicate",
|
|
5060
|
+
execute(currentState) {
|
|
5061
|
+
return runFulltextPredicatePass(
|
|
5062
|
+
currentState.ast,
|
|
5063
|
+
currentState.ctx.dialect
|
|
5064
|
+
).fulltextPredicate;
|
|
5065
|
+
},
|
|
5066
|
+
update(currentState, fulltextPredicate) {
|
|
5067
|
+
return {
|
|
5068
|
+
...currentState,
|
|
5069
|
+
fulltextPredicate
|
|
5070
|
+
};
|
|
5071
|
+
}
|
|
5072
|
+
});
|
|
5073
|
+
state = fulltextPass.state;
|
|
5074
|
+
const fusionPass = runCompilerPass(state, {
|
|
5075
|
+
name: "fusion_config",
|
|
5076
|
+
execute(currentState) {
|
|
5077
|
+
return runFusionConfigPass(
|
|
5078
|
+
currentState.ast,
|
|
5079
|
+
currentState.vectorPredicate,
|
|
5080
|
+
currentState.fulltextPredicate
|
|
5081
|
+
).fusion;
|
|
5082
|
+
},
|
|
5083
|
+
update(currentState, fusion) {
|
|
4581
5084
|
return {
|
|
4582
5085
|
...currentState,
|
|
4583
|
-
|
|
5086
|
+
fusion
|
|
4584
5087
|
};
|
|
4585
5088
|
}
|
|
4586
5089
|
});
|
|
4587
|
-
state =
|
|
5090
|
+
state = fusionPass.state;
|
|
4588
5091
|
const temporalPass = runCompilerPass(state, {
|
|
4589
5092
|
name: "temporal_filters",
|
|
4590
5093
|
execute(currentState) {
|
|
@@ -4619,7 +5122,8 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
|
|
|
4619
5122
|
execute(currentState) {
|
|
4620
5123
|
const shouldCollapseSelectiveTraversalRowset = canCollapseSelectiveTraversalRowset(
|
|
4621
5124
|
currentState.ast,
|
|
4622
|
-
currentState.vectorPredicate
|
|
5125
|
+
currentState.vectorPredicate,
|
|
5126
|
+
currentState.fulltextPredicate
|
|
4623
5127
|
);
|
|
4624
5128
|
const lastTraversal = currentState.ast.traversals.at(-1);
|
|
4625
5129
|
const collapsedTraversalCteAlias = shouldCollapseSelectiveTraversalRowset && lastTraversal !== void 0 ? `cte_${lastTraversal.nodeAlias}` : void 0;
|
|
@@ -4655,9 +5159,9 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
|
|
|
4655
5159
|
state = traversalLimitPass.state;
|
|
4656
5160
|
state = {
|
|
4657
5161
|
...state,
|
|
4658
|
-
effectiveLimit:
|
|
4659
|
-
state.ast.limit,
|
|
4660
|
-
state.
|
|
5162
|
+
effectiveLimit: resolveFulltextAwareLimit(
|
|
5163
|
+
resolveVectorAwareLimit(state.ast.limit, state.vectorPredicate),
|
|
5164
|
+
state.fulltextPredicate
|
|
4661
5165
|
)
|
|
4662
5166
|
};
|
|
4663
5167
|
const logicalPlanPass = runCompilerPass(state, {
|
|
@@ -4674,7 +5178,8 @@ function runStandardQueryPassPipeline(ast, graphId, ctx) {
|
|
|
4674
5178
|
collapsedTraversalCteAlias: currentState.collapsedTraversalCteAlias
|
|
4675
5179
|
},
|
|
4676
5180
|
...currentState.effectiveLimit === void 0 ? {} : { effectiveLimit: currentState.effectiveLimit },
|
|
4677
|
-
...currentState.vectorPredicate === void 0 ? {} : { vectorPredicate: currentState.vectorPredicate }
|
|
5181
|
+
...currentState.vectorPredicate === void 0 ? {} : { vectorPredicate: currentState.vectorPredicate },
|
|
5182
|
+
...currentState.fulltextPredicate === void 0 ? {} : { fulltextPredicate: currentState.fulltextPredicate }
|
|
4678
5183
|
});
|
|
4679
5184
|
},
|
|
4680
5185
|
update(currentState, logicalPlan) {
|
|
@@ -4693,11 +5198,11 @@ function compileQuery(ast, graphId, options = "sqlite") {
|
|
|
4693
5198
|
const options_ = typeof options === "string" ? { dialect: options } : options;
|
|
4694
5199
|
const dialect = options_.dialect ?? "sqlite";
|
|
4695
5200
|
const schema = options_.schema ?? DEFAULT_SQL_SCHEMA;
|
|
4696
|
-
const adapter =
|
|
5201
|
+
const adapter = resolveDialectAdapter(dialect, options_.fulltextStrategy);
|
|
4697
5202
|
const ctx = {
|
|
4698
5203
|
dialect: adapter,
|
|
4699
5204
|
schema,
|
|
4700
|
-
compileQuery: (subAst, subGraphId) => compileQuery(subAst, subGraphId,
|
|
5205
|
+
compileQuery: (subAst, subGraphId) => compileQuery(subAst, subGraphId, propagateOptions(options_))
|
|
4701
5206
|
};
|
|
4702
5207
|
if (hasVariableLengthTraversal(ast)) {
|
|
4703
5208
|
const lowered = tryLowerSingleHopRecursiveTraversal(ast);
|
|
@@ -4733,15 +5238,29 @@ function compileSetOperation2(op, graphId, options = "sqlite") {
|
|
|
4733
5238
|
const options_ = typeof options === "string" ? { dialect: options } : options;
|
|
4734
5239
|
const dialect = options_.dialect ?? "sqlite";
|
|
4735
5240
|
const schema = options_.schema ?? DEFAULT_SQL_SCHEMA;
|
|
4736
|
-
const adapter =
|
|
5241
|
+
const adapter = resolveDialectAdapter(dialect, options_.fulltextStrategy);
|
|
4737
5242
|
return compileSetOperation(
|
|
4738
5243
|
op,
|
|
4739
5244
|
graphId,
|
|
4740
5245
|
adapter,
|
|
4741
5246
|
schema,
|
|
4742
|
-
(ast, gid) => compileQuery(ast, gid,
|
|
5247
|
+
(ast, gid) => compileQuery(ast, gid, propagateOptions(options_))
|
|
4743
5248
|
);
|
|
4744
5249
|
}
|
|
5250
|
+
function resolveDialectAdapter(dialect, fulltextStrategy) {
|
|
5251
|
+
const baseAdapter = getDialect(dialect);
|
|
5252
|
+
if (fulltextStrategy === void 0 || fulltextStrategy === baseAdapter.fulltext) {
|
|
5253
|
+
return baseAdapter;
|
|
5254
|
+
}
|
|
5255
|
+
return { ...baseAdapter, fulltext: fulltextStrategy };
|
|
5256
|
+
}
|
|
5257
|
+
function propagateOptions(options_) {
|
|
5258
|
+
return {
|
|
5259
|
+
dialect: options_.dialect ?? "sqlite",
|
|
5260
|
+
...options_.schema === void 0 ? {} : { schema: options_.schema },
|
|
5261
|
+
...options_.fulltextStrategy === void 0 ? {} : { fulltextStrategy: options_.fulltextStrategy }
|
|
5262
|
+
};
|
|
5263
|
+
}
|
|
4745
5264
|
function resolveCountAggregateFastPath(ast) {
|
|
4746
5265
|
if (ast.traversals.length !== 1) {
|
|
4747
5266
|
return void 0;
|
|
@@ -4890,12 +5409,12 @@ function compileCountAggregateFastPath(ast, graphId, ctx, logicalPlan, requiredC
|
|
|
4890
5409
|
source.valueType,
|
|
4891
5410
|
`cte_${source.alias}`
|
|
4892
5411
|
);
|
|
4893
|
-
return sql`${value} AS ${
|
|
5412
|
+
return sql`${value} AS ${quoteIdentifier2(projectedField.outputName)}`;
|
|
4894
5413
|
}
|
|
4895
5414
|
const projectedCountColumn = source.function === "countDistinct" ? countDistinctColumn : countColumn;
|
|
4896
5415
|
const countValue = sql`${sql.raw(countCteAlias)}.${sql.raw(projectedCountColumn)}`;
|
|
4897
5416
|
const aggregateValue = traversal.optional ? sql`COALESCE(${countValue}, 0)` : countValue;
|
|
4898
|
-
return sql`${aggregateValue} AS ${
|
|
5417
|
+
return sql`${aggregateValue} AS ${quoteIdentifier2(projectedField.outputName)}`;
|
|
4899
5418
|
}),
|
|
4900
5419
|
sql`, `
|
|
4901
5420
|
);
|
|
@@ -4928,11 +5447,37 @@ function compileStandardQuery(ast, graphId, ctx) {
|
|
|
4928
5447
|
const handler = STANDARD_QUERY_STRATEGY_HANDLERS[strategy];
|
|
4929
5448
|
return handler(ast, graphId, ctx);
|
|
4930
5449
|
}
|
|
5450
|
+
function selectStandardOrderBy(input) {
|
|
5451
|
+
const {
|
|
5452
|
+
ast,
|
|
5453
|
+
collapsedTraversalCteAlias,
|
|
5454
|
+
dialect,
|
|
5455
|
+
fulltextPredicate,
|
|
5456
|
+
fusion,
|
|
5457
|
+
vectorPredicate
|
|
5458
|
+
} = input;
|
|
5459
|
+
if (vectorPredicate && fulltextPredicate) {
|
|
5460
|
+
return buildStandardHybridRrfOrderBy({ ast, dialect, fusion });
|
|
5461
|
+
}
|
|
5462
|
+
if (vectorPredicate) {
|
|
5463
|
+
return buildStandardVectorOrderBy({ ast, dialect });
|
|
5464
|
+
}
|
|
5465
|
+
if (fulltextPredicate) {
|
|
5466
|
+
return buildStandardFulltextOrderBy({ ast, dialect });
|
|
5467
|
+
}
|
|
5468
|
+
return buildStandardOrderBy({
|
|
5469
|
+
ast,
|
|
5470
|
+
...collapsedTraversalCteAlias === void 0 ? {} : { collapsedTraversalCteAlias },
|
|
5471
|
+
dialect
|
|
5472
|
+
});
|
|
5473
|
+
}
|
|
4931
5474
|
function compileStandardQueryWithCteStrategy(ast, graphId, ctx) {
|
|
4932
5475
|
const { dialect } = ctx;
|
|
4933
5476
|
const {
|
|
4934
5477
|
collapsedTraversalCteAlias,
|
|
4935
5478
|
effectiveLimit,
|
|
5479
|
+
fulltextPredicate,
|
|
5480
|
+
fusion,
|
|
4936
5481
|
logicalPlan,
|
|
4937
5482
|
predicateIndex,
|
|
4938
5483
|
requiredColumnsByAlias,
|
|
@@ -4953,7 +5498,7 @@ function compileStandardQueryWithCteStrategy(ast, graphId, ctx) {
|
|
|
4953
5498
|
{ phase: "standard-pass-pipeline" }
|
|
4954
5499
|
);
|
|
4955
5500
|
}
|
|
4956
|
-
if (!vectorPredicate) {
|
|
5501
|
+
if (!vectorPredicate && !fulltextPredicate) {
|
|
4957
5502
|
const fastPathSql = compileCountAggregateFastPath(
|
|
4958
5503
|
ast,
|
|
4959
5504
|
graphId,
|
|
@@ -4999,7 +5544,29 @@ function compileStandardQueryWithCteStrategy(ast, graphId, ctx) {
|
|
|
4999
5544
|
);
|
|
5000
5545
|
}
|
|
5001
5546
|
if (vectorPredicate) {
|
|
5002
|
-
|
|
5547
|
+
const nodeKinds = getNodeKindsForAlias(ast, vectorPredicate.field.alias);
|
|
5548
|
+
ctes.push(
|
|
5549
|
+
buildStandardEmbeddingsCte({
|
|
5550
|
+
ctx,
|
|
5551
|
+
graphId,
|
|
5552
|
+
nodeKinds,
|
|
5553
|
+
vectorPredicate
|
|
5554
|
+
})
|
|
5555
|
+
);
|
|
5556
|
+
}
|
|
5557
|
+
if (fulltextPredicate) {
|
|
5558
|
+
const nodeKinds = getNodeKindsForAlias(ast, fulltextPredicate.field.alias);
|
|
5559
|
+
ctes.push(
|
|
5560
|
+
buildStandardFulltextCte({
|
|
5561
|
+
ctx,
|
|
5562
|
+
fulltextPredicate,
|
|
5563
|
+
graphId,
|
|
5564
|
+
nodeKinds
|
|
5565
|
+
})
|
|
5566
|
+
);
|
|
5567
|
+
}
|
|
5568
|
+
if (getHybridTargetAlias(vectorPredicate, fulltextPredicate) !== void 0) {
|
|
5569
|
+
ctes.push(buildStandardHybridCandidateCte());
|
|
5003
5570
|
}
|
|
5004
5571
|
const projection = buildStandardProjection({
|
|
5005
5572
|
ast,
|
|
@@ -5009,14 +5576,18 @@ function compileStandardQueryWithCteStrategy(ast, graphId, ctx) {
|
|
|
5009
5576
|
const fromClause = buildStandardFromClause({
|
|
5010
5577
|
ast,
|
|
5011
5578
|
...collapsedTraversalCteAlias === void 0 ? {} : { collapsedTraversalCteAlias },
|
|
5012
|
-
...vectorPredicate === void 0 ? {} : { vectorPredicate }
|
|
5579
|
+
...vectorPredicate === void 0 ? {} : { vectorPredicate },
|
|
5580
|
+
...fulltextPredicate === void 0 ? {} : { fulltextPredicate }
|
|
5013
5581
|
});
|
|
5014
5582
|
const groupBy = buildStandardGroupBy({ ast, dialect });
|
|
5015
5583
|
const having2 = buildStandardHaving({ ast, ctx });
|
|
5016
|
-
const orderBy =
|
|
5584
|
+
const orderBy = selectStandardOrderBy({
|
|
5017
5585
|
ast,
|
|
5018
|
-
|
|
5019
|
-
dialect
|
|
5586
|
+
collapsedTraversalCteAlias,
|
|
5587
|
+
dialect,
|
|
5588
|
+
fulltextPredicate,
|
|
5589
|
+
fusion,
|
|
5590
|
+
vectorPredicate
|
|
5020
5591
|
});
|
|
5021
5592
|
const limitOffset = buildLimitOffsetClause({
|
|
5022
5593
|
limit: effectiveLimit,
|
|
@@ -5437,11 +6008,7 @@ function buildComparisonPredicate(field2, op, value) {
|
|
|
5437
6008
|
};
|
|
5438
6009
|
}
|
|
5439
6010
|
function buildCursorFromContext(context, orderBy, direction) {
|
|
5440
|
-
return buildCursorFromRow(
|
|
5441
|
-
context,
|
|
5442
|
-
orderBy,
|
|
5443
|
-
direction
|
|
5444
|
-
);
|
|
6011
|
+
return buildCursorFromRow(context, orderBy, direction);
|
|
5445
6012
|
}
|
|
5446
6013
|
function buildPaginatedResult(data, orderedRows, orderBy, limit, hasMore, isBackward, cursor, buildContext) {
|
|
5447
6014
|
let nextCursor;
|
|
@@ -6034,59 +6601,41 @@ function validateSqlIdentifier(alias) {
|
|
|
6034
6601
|
);
|
|
6035
6602
|
}
|
|
6036
6603
|
}
|
|
6037
|
-
function
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
{
|
|
6043
|
-
path,
|
|
6044
|
-
message: "Vector similarity predicates are only supported at top-level or inside AND groups."
|
|
6045
|
-
}
|
|
6046
|
-
]
|
|
6047
|
-
},
|
|
6048
|
-
{
|
|
6049
|
-
suggestion: "Rewrite the predicate to keep vector similarity at top-level or combine with additional filters using AND."
|
|
6050
|
-
}
|
|
6051
|
-
);
|
|
6052
|
-
}
|
|
6053
|
-
function validateVectorPredicateExpression(expression, inDisallowedBranch, path) {
|
|
6604
|
+
function validateStructuralPredicatePlacement(expression, matchType, buildError, inDisallowedBranch, path) {
|
|
6605
|
+
if (expression.__type === matchType) {
|
|
6606
|
+
if (inDisallowedBranch) buildError(path);
|
|
6607
|
+
return;
|
|
6608
|
+
}
|
|
6054
6609
|
switch (expression.__type) {
|
|
6055
|
-
case "vector_similarity": {
|
|
6056
|
-
if (inDisallowedBranch) {
|
|
6057
|
-
throwInvalidVectorPredicatePlacement(path);
|
|
6058
|
-
}
|
|
6059
|
-
return;
|
|
6060
|
-
}
|
|
6061
6610
|
case "and": {
|
|
6062
|
-
for (const [
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
predicate2,
|
|
6611
|
+
for (const [index, child] of expression.predicates.entries()) {
|
|
6612
|
+
validateStructuralPredicatePlacement(
|
|
6613
|
+
child,
|
|
6614
|
+
matchType,
|
|
6615
|
+
buildError,
|
|
6068
6616
|
inDisallowedBranch,
|
|
6069
|
-
`${path}.predicates[${
|
|
6617
|
+
`${path}.predicates[${index}]`
|
|
6070
6618
|
);
|
|
6071
6619
|
}
|
|
6072
6620
|
return;
|
|
6073
6621
|
}
|
|
6074
6622
|
case "or": {
|
|
6075
|
-
for (const [
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
predicate2,
|
|
6623
|
+
for (const [index, child] of expression.predicates.entries()) {
|
|
6624
|
+
validateStructuralPredicatePlacement(
|
|
6625
|
+
child,
|
|
6626
|
+
matchType,
|
|
6627
|
+
buildError,
|
|
6081
6628
|
true,
|
|
6082
|
-
`${path}.predicates[${
|
|
6629
|
+
`${path}.predicates[${index}]`
|
|
6083
6630
|
);
|
|
6084
6631
|
}
|
|
6085
6632
|
return;
|
|
6086
6633
|
}
|
|
6087
6634
|
case "not": {
|
|
6088
|
-
|
|
6635
|
+
validateStructuralPredicatePlacement(
|
|
6089
6636
|
expression.predicate,
|
|
6637
|
+
matchType,
|
|
6638
|
+
buildError,
|
|
6090
6639
|
true,
|
|
6091
6640
|
`${path}.predicate`
|
|
6092
6641
|
);
|
|
@@ -6100,24 +6649,108 @@ function validateVectorPredicateExpression(expression, inDisallowedBranch, path)
|
|
|
6100
6649
|
case "object_op":
|
|
6101
6650
|
case "aggregate_comparison":
|
|
6102
6651
|
case "exists":
|
|
6103
|
-
case "in_subquery":
|
|
6652
|
+
case "in_subquery":
|
|
6653
|
+
case "vector_similarity":
|
|
6654
|
+
case "fulltext_match": {
|
|
6104
6655
|
return;
|
|
6105
6656
|
}
|
|
6106
6657
|
}
|
|
6107
6658
|
}
|
|
6659
|
+
function throwInvalidVectorPredicatePlacement(path) {
|
|
6660
|
+
throw new ValidationError(
|
|
6661
|
+
"Vector similarity predicates cannot be nested under OR or NOT. Use top-level AND combinations instead.",
|
|
6662
|
+
{
|
|
6663
|
+
issues: [
|
|
6664
|
+
{
|
|
6665
|
+
path,
|
|
6666
|
+
message: "Vector similarity predicates are only supported at top-level or inside AND groups."
|
|
6667
|
+
}
|
|
6668
|
+
]
|
|
6669
|
+
},
|
|
6670
|
+
{
|
|
6671
|
+
suggestion: "Rewrite the predicate to keep vector similarity at top-level or combine with additional filters using AND."
|
|
6672
|
+
}
|
|
6673
|
+
);
|
|
6674
|
+
}
|
|
6675
|
+
function throwInvalidFulltextPredicatePlacement(path) {
|
|
6676
|
+
throw new ValidationError(
|
|
6677
|
+
"Fulltext match predicates (.matches()) cannot be nested under OR or NOT. Use top-level AND combinations instead.",
|
|
6678
|
+
{
|
|
6679
|
+
issues: [
|
|
6680
|
+
{
|
|
6681
|
+
path,
|
|
6682
|
+
message: "Fulltext match predicates are only supported at top-level or inside AND groups."
|
|
6683
|
+
}
|
|
6684
|
+
]
|
|
6685
|
+
},
|
|
6686
|
+
{
|
|
6687
|
+
suggestion: "Restructure your query so the fulltext match appears at the top level of a whereNode() or inside an .and() combination."
|
|
6688
|
+
}
|
|
6689
|
+
);
|
|
6690
|
+
}
|
|
6108
6691
|
function validateVectorPredicatePlacement(predicates) {
|
|
6109
|
-
for (const [
|
|
6110
|
-
|
|
6692
|
+
for (const [index, predicate2] of predicates.entries()) {
|
|
6693
|
+
validateStructuralPredicatePlacement(
|
|
6694
|
+
predicate2.expression,
|
|
6695
|
+
"vector_similarity",
|
|
6696
|
+
throwInvalidVectorPredicatePlacement,
|
|
6697
|
+
false,
|
|
6698
|
+
`predicates[${index}].expression`
|
|
6699
|
+
);
|
|
6700
|
+
}
|
|
6701
|
+
}
|
|
6702
|
+
function validateFulltextPredicatePlacement(predicates) {
|
|
6703
|
+
for (const [index, predicate2] of predicates.entries()) {
|
|
6704
|
+
validateStructuralPredicatePlacement(
|
|
6111
6705
|
predicate2.expression,
|
|
6706
|
+
"fulltext_match",
|
|
6707
|
+
throwInvalidFulltextPredicatePlacement,
|
|
6112
6708
|
false,
|
|
6113
|
-
`predicates[${
|
|
6709
|
+
`predicates[${index}].expression`
|
|
6710
|
+
);
|
|
6711
|
+
}
|
|
6712
|
+
}
|
|
6713
|
+
function validateHybridFusionOptions(options) {
|
|
6714
|
+
const method = options.method;
|
|
6715
|
+
if (method !== void 0 && method !== "rrf") {
|
|
6716
|
+
throw new ValidationError(`Unsupported fusion method: ${method}`, {
|
|
6717
|
+
issues: [{ path: "fusion.method", message: `Only "rrf" is supported.` }]
|
|
6718
|
+
});
|
|
6719
|
+
}
|
|
6720
|
+
const { k } = options;
|
|
6721
|
+
if (k !== void 0 && (!Number.isFinite(k) || k <= 0)) {
|
|
6722
|
+
throw new ValidationError(
|
|
6723
|
+
`Fusion k must be a positive finite number, got: ${String(k)}`,
|
|
6724
|
+
{
|
|
6725
|
+
issues: [{ path: "fusion.k", message: "Must be a positive number." }]
|
|
6726
|
+
}
|
|
6114
6727
|
);
|
|
6115
6728
|
}
|
|
6729
|
+
const weights = options.weights;
|
|
6730
|
+
if (weights !== void 0) {
|
|
6731
|
+
for (const key of ["vector", "fulltext"]) {
|
|
6732
|
+
const weight = weights[key];
|
|
6733
|
+
if (weight !== void 0 && (!Number.isFinite(weight) || weight < 0)) {
|
|
6734
|
+
throw new ValidationError(
|
|
6735
|
+
`Fusion weight for "${key}" must be a non-negative finite number, got: ${String(weight)}`,
|
|
6736
|
+
{
|
|
6737
|
+
issues: [
|
|
6738
|
+
{
|
|
6739
|
+
path: `fusion.weights.${key}`,
|
|
6740
|
+
message: "Must be a non-negative number."
|
|
6741
|
+
}
|
|
6742
|
+
]
|
|
6743
|
+
}
|
|
6744
|
+
);
|
|
6745
|
+
}
|
|
6746
|
+
}
|
|
6747
|
+
}
|
|
6116
6748
|
}
|
|
6117
6749
|
|
|
6118
6750
|
// src/query/builder/ast-builder.ts
|
|
6119
6751
|
function buildQueryAst(config, state) {
|
|
6120
6752
|
validateVectorPredicatePlacement(state.predicates);
|
|
6753
|
+
validateFulltextPredicatePlacement(state.predicates);
|
|
6121
6754
|
const temporalMode = {
|
|
6122
6755
|
mode: state.temporalMode
|
|
6123
6756
|
};
|
|
@@ -6141,7 +6774,18 @@ function buildQueryAst(config, state) {
|
|
|
6141
6774
|
...state.limit !== void 0 && { limit: state.limit },
|
|
6142
6775
|
...state.offset !== void 0 && { offset: state.offset },
|
|
6143
6776
|
...state.groupBy !== void 0 && { groupBy: state.groupBy },
|
|
6144
|
-
...state.having !== void 0 && { having: state.having }
|
|
6777
|
+
...state.having !== void 0 && { having: state.having },
|
|
6778
|
+
...state.fusion !== void 0 && { fusion: state.fusion }
|
|
6779
|
+
};
|
|
6780
|
+
}
|
|
6781
|
+
|
|
6782
|
+
// src/query/builder/compile-options.ts
|
|
6783
|
+
function buildCompileOptions(config) {
|
|
6784
|
+
const fulltextStrategy = config.backend?.fulltextStrategy;
|
|
6785
|
+
return {
|
|
6786
|
+
dialect: config.dialect ?? "sqlite",
|
|
6787
|
+
schema: config.schema,
|
|
6788
|
+
...fulltextStrategy === void 0 ? {} : { fulltextStrategy }
|
|
6145
6789
|
};
|
|
6146
6790
|
}
|
|
6147
6791
|
function toLiteral(value) {
|
|
@@ -6243,7 +6887,8 @@ function substitutePredicateExpression(expr, bindings) {
|
|
|
6243
6887
|
case "array_op":
|
|
6244
6888
|
case "object_op":
|
|
6245
6889
|
case "aggregate_comparison":
|
|
6246
|
-
case "vector_similarity":
|
|
6890
|
+
case "vector_similarity":
|
|
6891
|
+
case "fulltext_match": {
|
|
6247
6892
|
return expr;
|
|
6248
6893
|
}
|
|
6249
6894
|
case "exists": {
|
|
@@ -6495,7 +7140,8 @@ function collectParameterMetadataFromExpression(expression, names, stringOpParam
|
|
|
6495
7140
|
case "array_op":
|
|
6496
7141
|
case "object_op":
|
|
6497
7142
|
case "aggregate_comparison":
|
|
6498
|
-
case "vector_similarity":
|
|
7143
|
+
case "vector_similarity":
|
|
7144
|
+
case "fulltext_match": {
|
|
6499
7145
|
return;
|
|
6500
7146
|
}
|
|
6501
7147
|
case "exists": {
|
|
@@ -6850,10 +7496,7 @@ var ExecutableQuery = class _ExecutableQuery {
|
|
|
6850
7496
|
* Builds compile options from the config.
|
|
6851
7497
|
*/
|
|
6852
7498
|
#compileOptions() {
|
|
6853
|
-
return
|
|
6854
|
-
dialect: this.#config.dialect ?? "sqlite",
|
|
6855
|
-
schema: this.#config.schema
|
|
6856
|
-
};
|
|
7499
|
+
return buildCompileOptions(this.#config);
|
|
6857
7500
|
}
|
|
6858
7501
|
#requireBackend() {
|
|
6859
7502
|
const { backend } = this.#config;
|
|
@@ -7568,14 +8211,8 @@ var ExecutableAggregateQuery = class _ExecutableAggregateQuery {
|
|
|
7568
8211
|
this.#cachedCompiled = compiled;
|
|
7569
8212
|
return compiled;
|
|
7570
8213
|
}
|
|
7571
|
-
/**
|
|
7572
|
-
* Builds compile options from the config.
|
|
7573
|
-
*/
|
|
7574
8214
|
#compileOptions() {
|
|
7575
|
-
return
|
|
7576
|
-
dialect: this.#config.dialect ?? "sqlite",
|
|
7577
|
-
schema: this.#config.schema
|
|
7578
|
-
};
|
|
8215
|
+
return buildCompileOptions(this.#config);
|
|
7579
8216
|
}
|
|
7580
8217
|
/**
|
|
7581
8218
|
* Executes the query and returns typed results.
|
|
@@ -8302,6 +8939,36 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
8302
8939
|
having: predicate2
|
|
8303
8940
|
});
|
|
8304
8941
|
}
|
|
8942
|
+
/**
|
|
8943
|
+
* Sets fusion parameters for hybrid (vector + fulltext) queries.
|
|
8944
|
+
*
|
|
8945
|
+
* Applies only when the query contains both a `.similarTo()` and a
|
|
8946
|
+
* `.$fulltext.matches()` predicate. Without this call, the default is
|
|
8947
|
+
* RRF with k=60 and equal weights. A mismatch between `.fuseWith()`
|
|
8948
|
+
* configuration and the predicates on the query is caught during
|
|
8949
|
+
* compilation, not here.
|
|
8950
|
+
*
|
|
8951
|
+
* @example
|
|
8952
|
+
* ```typescript
|
|
8953
|
+
* store.query()
|
|
8954
|
+
* .from("Document", "d")
|
|
8955
|
+
* .whereNode("d", d =>
|
|
8956
|
+
* d.$fulltext.matches("renewable energy", 50)
|
|
8957
|
+
* .and(d.embedding.similarTo(vec, 50))
|
|
8958
|
+
* .and(d.tenantId.eq(tenant))
|
|
8959
|
+
* )
|
|
8960
|
+
* .fuseWith({ k: 60, weights: { fulltext: 1.5 } })
|
|
8961
|
+
* .limit(10)
|
|
8962
|
+
* .execute();
|
|
8963
|
+
* ```
|
|
8964
|
+
*/
|
|
8965
|
+
fuseWith(options) {
|
|
8966
|
+
validateHybridFusionOptions(options);
|
|
8967
|
+
return new _QueryBuilder(this.#config, {
|
|
8968
|
+
...this.#state,
|
|
8969
|
+
fusion: options
|
|
8970
|
+
});
|
|
8971
|
+
}
|
|
8305
8972
|
/**
|
|
8306
8973
|
* Applies a query fragment to transform this builder.
|
|
8307
8974
|
*
|
|
@@ -8360,35 +9027,7 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
8360
9027
|
return this.#buildFieldBuilderForTypeInfo(ref, typeInfo);
|
|
8361
9028
|
}
|
|
8362
9029
|
#buildFieldBuilderForTypeInfo(ref, typeInfo) {
|
|
8363
|
-
|
|
8364
|
-
return baseField(ref);
|
|
8365
|
-
}
|
|
8366
|
-
switch (typeInfo.valueType) {
|
|
8367
|
-
case "string": {
|
|
8368
|
-
return stringField(ref);
|
|
8369
|
-
}
|
|
8370
|
-
case "number": {
|
|
8371
|
-
return numberField(ref);
|
|
8372
|
-
}
|
|
8373
|
-
case "boolean": {
|
|
8374
|
-
return baseField(ref);
|
|
8375
|
-
}
|
|
8376
|
-
case "date": {
|
|
8377
|
-
return dateField(ref);
|
|
8378
|
-
}
|
|
8379
|
-
case "array": {
|
|
8380
|
-
return arrayField(ref);
|
|
8381
|
-
}
|
|
8382
|
-
case "object": {
|
|
8383
|
-
return objectField(ref, { typeInfo });
|
|
8384
|
-
}
|
|
8385
|
-
case "embedding": {
|
|
8386
|
-
return embeddingField(ref);
|
|
8387
|
-
}
|
|
8388
|
-
case "unknown": {
|
|
8389
|
-
return baseField(ref);
|
|
8390
|
-
}
|
|
8391
|
-
}
|
|
9030
|
+
return buildFieldBuilderForTypeInfo(ref, typeInfo);
|
|
8392
9031
|
}
|
|
8393
9032
|
#createNodeAccessor(alias) {
|
|
8394
9033
|
const kindNames = this.#getKindNamesForAlias(alias);
|
|
@@ -8398,6 +9037,10 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
8398
9037
|
const kindAccessor = stringField(
|
|
8399
9038
|
fieldRef(alias, ["kind"], { valueType: "string" })
|
|
8400
9039
|
);
|
|
9040
|
+
const fulltextAccessor = createFulltextAccessor(
|
|
9041
|
+
alias,
|
|
9042
|
+
() => this.#hasSearchableField(kindNames)
|
|
9043
|
+
);
|
|
8401
9044
|
return new Proxy({}, {
|
|
8402
9045
|
get: (_, property) => {
|
|
8403
9046
|
if (typeof property === "symbol") return;
|
|
@@ -8405,10 +9048,15 @@ var QueryBuilder = class _QueryBuilder {
|
|
|
8405
9048
|
if (property === "toJSON") return;
|
|
8406
9049
|
if (property === "id") return idAccessor;
|
|
8407
9050
|
if (property === "kind") return kindAccessor;
|
|
9051
|
+
if (property === "$fulltext") return fulltextAccessor;
|
|
8408
9052
|
return this.#getFieldBuilderForProperty(kindNames, property, alias);
|
|
8409
9053
|
}
|
|
8410
9054
|
});
|
|
8411
9055
|
}
|
|
9056
|
+
#hasSearchableField(kindNames) {
|
|
9057
|
+
if (!kindNames) return false;
|
|
9058
|
+
return this.#config.schemaIntrospector.hasSearchableField(kindNames);
|
|
9059
|
+
}
|
|
8412
9060
|
#expandTraversalEdgeKinds(edgeKind, includeImplyingEdges) {
|
|
8413
9061
|
return includeImplyingEdges ? this.#config.registry.expandImplyingEdges(edgeKind) : [edgeKind];
|
|
8414
9062
|
}
|
|
@@ -8634,14 +9282,8 @@ var UnionableQuery = class _UnionableQuery {
|
|
|
8634
9282
|
this.#cachedCompiled = compiled;
|
|
8635
9283
|
return compiled;
|
|
8636
9284
|
}
|
|
8637
|
-
/**
|
|
8638
|
-
* Builds compile options from the config.
|
|
8639
|
-
*/
|
|
8640
9285
|
#compileOptions() {
|
|
8641
|
-
return
|
|
8642
|
-
dialect: this.#config.dialect ?? "sqlite",
|
|
8643
|
-
schema: this.#config.schema
|
|
8644
|
-
};
|
|
9286
|
+
return buildCompileOptions(this.#config);
|
|
8645
9287
|
}
|
|
8646
9288
|
/**
|
|
8647
9289
|
* Executes the combined query.
|
|
@@ -8857,12 +9499,8 @@ function offsetFragment(n) {
|
|
|
8857
9499
|
}
|
|
8858
9500
|
|
|
8859
9501
|
// src/query/builder/index.ts
|
|
8860
|
-
setQueryBuilderClass(
|
|
8861
|
-
|
|
8862
|
-
);
|
|
8863
|
-
setUnionableQueryClass(
|
|
8864
|
-
UnionableQuery
|
|
8865
|
-
);
|
|
9502
|
+
setQueryBuilderClass(QueryBuilder);
|
|
9503
|
+
setUnionableQueryClass(UnionableQuery);
|
|
8866
9504
|
|
|
8867
9505
|
// src/query/builder.ts
|
|
8868
9506
|
function createQueryBuilder(graphId, registry, options) {
|
|
@@ -8893,7 +9531,8 @@ function createQueryBuilder(graphId, registry, options) {
|
|
|
8893
9531
|
temporalMode: "current",
|
|
8894
9532
|
asOf: void 0,
|
|
8895
9533
|
groupBy: void 0,
|
|
8896
|
-
having: void 0
|
|
9534
|
+
having: void 0,
|
|
9535
|
+
fusion: void 0
|
|
8897
9536
|
};
|
|
8898
9537
|
return new QueryBuilder(config, initialState);
|
|
8899
9538
|
}
|
|
@@ -8922,6 +9561,246 @@ function extractEdgeTypes(graph) {
|
|
|
8922
9561
|
return result;
|
|
8923
9562
|
}
|
|
8924
9563
|
|
|
9564
|
+
// src/store/algorithms/context.ts
|
|
9565
|
+
var DEFAULT_ALGORITHM_MAX_HOPS = 10;
|
|
9566
|
+
var DEFAULT_NEIGHBOR_DEPTH = 1;
|
|
9567
|
+
function resolveTemporalOptions(ctx, options) {
|
|
9568
|
+
return {
|
|
9569
|
+
temporalMode: options.temporalMode ?? ctx.defaultTemporalMode,
|
|
9570
|
+
...options.asOf !== void 0 && { asOf: options.asOf }
|
|
9571
|
+
};
|
|
9572
|
+
}
|
|
9573
|
+
function resolveTemporalFilter(ctx, options, tableAlias) {
|
|
9574
|
+
const resolved = resolveTemporalOptions(ctx, options);
|
|
9575
|
+
const filterOptions = {
|
|
9576
|
+
mode: resolved.temporalMode,
|
|
9577
|
+
asOf: resolved.asOf,
|
|
9578
|
+
tableAlias,
|
|
9579
|
+
currentTimestamp: ctx.dialect.currentTimestamp()
|
|
9580
|
+
};
|
|
9581
|
+
return compileTemporalFilter(filterOptions);
|
|
9582
|
+
}
|
|
9583
|
+
function resolveMaxHops(rawMaxHops, fallback, optionName) {
|
|
9584
|
+
const value = rawMaxHops ?? fallback;
|
|
9585
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
9586
|
+
throw new ConfigurationError(
|
|
9587
|
+
`Graph algorithm ${optionName} must be a finite integer, got ${String(value)}.`,
|
|
9588
|
+
{ option: optionName, value }
|
|
9589
|
+
);
|
|
9590
|
+
}
|
|
9591
|
+
if (value < 1) {
|
|
9592
|
+
throw new ConfigurationError(
|
|
9593
|
+
`Graph algorithm ${optionName} must be at least 1, got ${value}.`,
|
|
9594
|
+
{ option: optionName, value }
|
|
9595
|
+
);
|
|
9596
|
+
}
|
|
9597
|
+
if (value > MAX_EXPLICIT_RECURSIVE_DEPTH) {
|
|
9598
|
+
throw new ConfigurationError(
|
|
9599
|
+
`Graph algorithm ${optionName} (${value}) exceeds the maximum of ${MAX_EXPLICIT_RECURSIVE_DEPTH}.`,
|
|
9600
|
+
{ option: optionName, value, limit: MAX_EXPLICIT_RECURSIVE_DEPTH }
|
|
9601
|
+
);
|
|
9602
|
+
}
|
|
9603
|
+
return value;
|
|
9604
|
+
}
|
|
9605
|
+
function assertEdgeKinds(edges) {
|
|
9606
|
+
if (edges.length === 0) {
|
|
9607
|
+
throw new ConfigurationError(
|
|
9608
|
+
`Graph algorithms require at least one edge kind in 'edges'.`,
|
|
9609
|
+
{ edges }
|
|
9610
|
+
);
|
|
9611
|
+
}
|
|
9612
|
+
}
|
|
9613
|
+
|
|
9614
|
+
// src/store/algorithms/degree.ts
|
|
9615
|
+
async function executeDegree(ctx, nodeId, options = {}) {
|
|
9616
|
+
const direction = options.direction ?? "both";
|
|
9617
|
+
const whereClauses = [
|
|
9618
|
+
sql`graph_id = ${ctx.graphId}`,
|
|
9619
|
+
resolveTemporalFilter(ctx, options),
|
|
9620
|
+
compileDirectionFilter(direction, nodeId)
|
|
9621
|
+
];
|
|
9622
|
+
if (options.edges !== void 0) {
|
|
9623
|
+
whereClauses.push(compileKindFilter(sql.raw("kind"), options.edges));
|
|
9624
|
+
}
|
|
9625
|
+
const countExpr = direction === "both" ? sql`COUNT(DISTINCT id)` : sql`COUNT(*)`;
|
|
9626
|
+
const query = sql`SELECT ${countExpr} AS count FROM ${ctx.schema.edgesTable} WHERE ${sql.join(whereClauses, sql` AND `)}`;
|
|
9627
|
+
const rows = await ctx.backend.execute(query);
|
|
9628
|
+
return Number(rows[0]?.count ?? 0);
|
|
9629
|
+
}
|
|
9630
|
+
function compileDirectionFilter(direction, nodeId) {
|
|
9631
|
+
switch (direction) {
|
|
9632
|
+
case "out": {
|
|
9633
|
+
return sql`from_id = ${nodeId}`;
|
|
9634
|
+
}
|
|
9635
|
+
case "in": {
|
|
9636
|
+
return sql`to_id = ${nodeId}`;
|
|
9637
|
+
}
|
|
9638
|
+
case "both": {
|
|
9639
|
+
return sql`(from_id = ${nodeId} OR to_id = ${nodeId})`;
|
|
9640
|
+
}
|
|
9641
|
+
}
|
|
9642
|
+
}
|
|
9643
|
+
async function executeReachable(ctx, sourceId, options) {
|
|
9644
|
+
assertEdgeKinds(options.edges);
|
|
9645
|
+
const maxHops = resolveMaxHops(
|
|
9646
|
+
options.maxHops,
|
|
9647
|
+
DEFAULT_ALGORITHM_MAX_HOPS,
|
|
9648
|
+
"maxHops"
|
|
9649
|
+
);
|
|
9650
|
+
const cte = buildReachableCte({
|
|
9651
|
+
graphId: ctx.graphId,
|
|
9652
|
+
sourceId,
|
|
9653
|
+
edgeKinds: options.edges,
|
|
9654
|
+
maxHops,
|
|
9655
|
+
direction: options.direction ?? "out",
|
|
9656
|
+
cyclePolicy: options.cyclePolicy ?? "prevent",
|
|
9657
|
+
includePath: false,
|
|
9658
|
+
...resolveTemporalOptions(ctx, options),
|
|
9659
|
+
dialect: ctx.dialect,
|
|
9660
|
+
schema: ctx.schema
|
|
9661
|
+
});
|
|
9662
|
+
const sourceFilter = options.excludeSource === true ? sql` WHERE id != ${sourceId}` : sql``;
|
|
9663
|
+
const query = sql`${cte} SELECT id, kind, MIN(depth) AS depth FROM reachable${sourceFilter} GROUP BY id, kind ORDER BY depth ASC, id ASC`;
|
|
9664
|
+
const rows = await ctx.backend.execute(query);
|
|
9665
|
+
return rows.map((row) => ({
|
|
9666
|
+
id: row.id,
|
|
9667
|
+
kind: row.kind,
|
|
9668
|
+
depth: Number(row.depth)
|
|
9669
|
+
}));
|
|
9670
|
+
}
|
|
9671
|
+
async function executeCanReach(ctx, sourceId, targetId, options) {
|
|
9672
|
+
assertEdgeKinds(options.edges);
|
|
9673
|
+
const maxHops = resolveMaxHops(
|
|
9674
|
+
options.maxHops,
|
|
9675
|
+
DEFAULT_ALGORITHM_MAX_HOPS,
|
|
9676
|
+
"maxHops"
|
|
9677
|
+
);
|
|
9678
|
+
const cte = buildReachableCte({
|
|
9679
|
+
graphId: ctx.graphId,
|
|
9680
|
+
sourceId,
|
|
9681
|
+
edgeKinds: options.edges,
|
|
9682
|
+
maxHops,
|
|
9683
|
+
direction: options.direction ?? "out",
|
|
9684
|
+
cyclePolicy: options.cyclePolicy ?? "prevent",
|
|
9685
|
+
includePath: false,
|
|
9686
|
+
...resolveTemporalOptions(ctx, options),
|
|
9687
|
+
dialect: ctx.dialect,
|
|
9688
|
+
schema: ctx.schema
|
|
9689
|
+
});
|
|
9690
|
+
const query = sql`${cte} SELECT 1 AS hit FROM reachable WHERE id = ${targetId} LIMIT 1`;
|
|
9691
|
+
const rows = await ctx.backend.execute(query);
|
|
9692
|
+
return rows.length > 0;
|
|
9693
|
+
}
|
|
9694
|
+
async function executeNeighbors(ctx, sourceId, options) {
|
|
9695
|
+
const depth = resolveMaxHops(options.depth, DEFAULT_NEIGHBOR_DEPTH, "depth");
|
|
9696
|
+
return executeReachable(ctx, sourceId, {
|
|
9697
|
+
edges: options.edges,
|
|
9698
|
+
maxHops: depth,
|
|
9699
|
+
direction: options.direction ?? "out",
|
|
9700
|
+
cyclePolicy: options.cyclePolicy ?? "prevent",
|
|
9701
|
+
...options.temporalMode !== void 0 && {
|
|
9702
|
+
temporalMode: options.temporalMode
|
|
9703
|
+
},
|
|
9704
|
+
...options.asOf !== void 0 && { asOf: options.asOf },
|
|
9705
|
+
excludeSource: true
|
|
9706
|
+
});
|
|
9707
|
+
}
|
|
9708
|
+
async function executeShortestPath(ctx, sourceId, targetId, options) {
|
|
9709
|
+
assertEdgeKinds(options.edges);
|
|
9710
|
+
const maxHops = resolveMaxHops(
|
|
9711
|
+
options.maxHops,
|
|
9712
|
+
DEFAULT_ALGORITHM_MAX_HOPS,
|
|
9713
|
+
"maxHops"
|
|
9714
|
+
);
|
|
9715
|
+
if (sourceId === targetId) {
|
|
9716
|
+
const source = await fetchNodeKind(ctx, sourceId, options);
|
|
9717
|
+
if (source === void 0) return void 0;
|
|
9718
|
+
return { nodes: [source], depth: 0 };
|
|
9719
|
+
}
|
|
9720
|
+
const cte = buildReachableCte({
|
|
9721
|
+
graphId: ctx.graphId,
|
|
9722
|
+
sourceId,
|
|
9723
|
+
edgeKinds: options.edges,
|
|
9724
|
+
maxHops,
|
|
9725
|
+
direction: options.direction ?? "out",
|
|
9726
|
+
cyclePolicy: options.cyclePolicy ?? "prevent",
|
|
9727
|
+
includePath: true,
|
|
9728
|
+
...resolveTemporalOptions(ctx, options),
|
|
9729
|
+
dialect: ctx.dialect,
|
|
9730
|
+
schema: ctx.schema
|
|
9731
|
+
});
|
|
9732
|
+
const pathContainsCheck = sql`NOT (${ctx.dialect.cycleCheck(sql.raw("r.id"), sql.raw("h.path"))})`;
|
|
9733
|
+
const query = sql`${cte}, hit AS (SELECT id, kind, depth, path FROM reachable WHERE id = ${targetId} ORDER BY depth ASC LIMIT 1) SELECT h.id AS hit_id, h.kind AS hit_kind, h.depth AS hit_depth, h.path AS hit_path, r.id AS node_id, r.kind AS node_kind FROM hit h LEFT JOIN reachable r ON ${pathContainsCheck}`;
|
|
9734
|
+
const rows = await ctx.backend.execute(query);
|
|
9735
|
+
const first = rows[0];
|
|
9736
|
+
if (first === void 0) return void 0;
|
|
9737
|
+
const pathIds = normalizePath(first.hit_path);
|
|
9738
|
+
const kindById = buildKindIndex(rows);
|
|
9739
|
+
const pathNodes = pathIds.map((id) => ({
|
|
9740
|
+
id,
|
|
9741
|
+
kind: kindById.get(id) ?? ""
|
|
9742
|
+
}));
|
|
9743
|
+
return { nodes: pathNodes, depth: Number(first.hit_depth) };
|
|
9744
|
+
}
|
|
9745
|
+
function buildKindIndex(rows) {
|
|
9746
|
+
const map = /* @__PURE__ */ new Map();
|
|
9747
|
+
for (const row of rows) {
|
|
9748
|
+
if (row.node_id !== void 0 && row.node_kind !== void 0) {
|
|
9749
|
+
map.set(row.node_id, row.node_kind);
|
|
9750
|
+
}
|
|
9751
|
+
}
|
|
9752
|
+
return map;
|
|
9753
|
+
}
|
|
9754
|
+
async function fetchNodeKind(ctx, nodeId, options) {
|
|
9755
|
+
const temporalFilter = resolveTemporalFilter(ctx, options);
|
|
9756
|
+
const query = sql`SELECT id, kind FROM ${ctx.schema.nodesTable} WHERE graph_id = ${ctx.graphId} AND id = ${nodeId} AND ${temporalFilter} LIMIT 1`;
|
|
9757
|
+
const rows = await ctx.backend.execute(query);
|
|
9758
|
+
const row = rows[0];
|
|
9759
|
+
if (row === void 0) return void 0;
|
|
9760
|
+
return { id: row.id, kind: row.kind };
|
|
9761
|
+
}
|
|
9762
|
+
|
|
9763
|
+
// src/store/algorithms/index.ts
|
|
9764
|
+
function resolveNodeId(value) {
|
|
9765
|
+
return typeof value === "string" ? value : value.id;
|
|
9766
|
+
}
|
|
9767
|
+
function createGraphAlgorithms(params) {
|
|
9768
|
+
const ctx = {
|
|
9769
|
+
graphId: params.graphId,
|
|
9770
|
+
backend: params.backend,
|
|
9771
|
+
dialect: getDialect(params.backend.dialect),
|
|
9772
|
+
schema: params.schema ?? (params.backend.tableNames ? createSqlSchema(params.backend.tableNames) : createSqlSchema()),
|
|
9773
|
+
defaultTemporalMode: params.defaultTemporalMode
|
|
9774
|
+
};
|
|
9775
|
+
return {
|
|
9776
|
+
shortestPath(from, to, options) {
|
|
9777
|
+
return executeShortestPath(
|
|
9778
|
+
ctx,
|
|
9779
|
+
resolveNodeId(from),
|
|
9780
|
+
resolveNodeId(to),
|
|
9781
|
+
options
|
|
9782
|
+
);
|
|
9783
|
+
},
|
|
9784
|
+
reachable(from, options) {
|
|
9785
|
+
return executeReachable(ctx, resolveNodeId(from), options);
|
|
9786
|
+
},
|
|
9787
|
+
canReach(from, to, options) {
|
|
9788
|
+
return executeCanReach(
|
|
9789
|
+
ctx,
|
|
9790
|
+
resolveNodeId(from),
|
|
9791
|
+
resolveNodeId(to),
|
|
9792
|
+
options
|
|
9793
|
+
);
|
|
9794
|
+
},
|
|
9795
|
+
neighbors(node, options) {
|
|
9796
|
+
return executeNeighbors(ctx, resolveNodeId(node), options);
|
|
9797
|
+
},
|
|
9798
|
+
degree(node, options) {
|
|
9799
|
+
return executeDegree(ctx, resolveNodeId(node), options ?? {});
|
|
9800
|
+
}
|
|
9801
|
+
};
|
|
9802
|
+
}
|
|
9803
|
+
|
|
8925
9804
|
// src/store/collections/edge-collection.ts
|
|
8926
9805
|
function narrowEdge(edge) {
|
|
8927
9806
|
return edge;
|
|
@@ -8994,20 +9873,13 @@ function createEdgeCollection(config) {
|
|
|
8994
9873
|
const result = {};
|
|
8995
9874
|
if (options?.matchOn !== void 0)
|
|
8996
9875
|
result.matchOn = options.matchOn;
|
|
8997
|
-
if (options?.props !== void 0)
|
|
8998
|
-
result.props = options.props;
|
|
9876
|
+
if (options?.props !== void 0) result.props = options.props;
|
|
8999
9877
|
return result;
|
|
9000
9878
|
}
|
|
9001
9879
|
return {
|
|
9002
9880
|
async create(from, to, props, options) {
|
|
9003
9881
|
const result = await executeEdgeCreate2(
|
|
9004
|
-
buildCreateEdgeInput(
|
|
9005
|
-
kind,
|
|
9006
|
-
from,
|
|
9007
|
-
to,
|
|
9008
|
-
props ?? {},
|
|
9009
|
-
options
|
|
9010
|
-
),
|
|
9882
|
+
buildCreateEdgeInput(kind, from, to, props ?? {}, options),
|
|
9011
9883
|
backend
|
|
9012
9884
|
);
|
|
9013
9885
|
return narrowEdge(result);
|
|
@@ -9133,10 +10005,7 @@ function createEdgeCollection(config) {
|
|
|
9133
10005
|
return backend.countEdgesByKind(params);
|
|
9134
10006
|
},
|
|
9135
10007
|
async bulkCreate(items) {
|
|
9136
|
-
const batchInputs = mapBulkEdgeInputs(
|
|
9137
|
-
kind,
|
|
9138
|
-
items
|
|
9139
|
-
);
|
|
10008
|
+
const batchInputs = mapBulkEdgeInputs(kind, items);
|
|
9140
10009
|
if (backend.capabilities.transactions && "transaction" in backend) {
|
|
9141
10010
|
const results2 = await backend.transaction(
|
|
9142
10011
|
async (txBackend) => executeEdgeCreateBatch2(batchInputs, txBackend)
|
|
@@ -9172,11 +10041,7 @@ function createEdgeCollection(config) {
|
|
|
9172
10041
|
if (existing) {
|
|
9173
10042
|
toUpdate.push({
|
|
9174
10043
|
index: itemIndex,
|
|
9175
|
-
input: buildUpdateEdgeInput(
|
|
9176
|
-
item.id,
|
|
9177
|
-
item.props ?? {},
|
|
9178
|
-
item
|
|
9179
|
-
),
|
|
10044
|
+
input: buildUpdateEdgeInput(item.id, item.props ?? {}, item),
|
|
9180
10045
|
clearDeleted: existing.deleted_at !== void 0
|
|
9181
10046
|
});
|
|
9182
10047
|
} else {
|
|
@@ -9215,10 +10080,7 @@ function createEdgeCollection(config) {
|
|
|
9215
10080
|
return upsertAll(backend);
|
|
9216
10081
|
},
|
|
9217
10082
|
async bulkInsert(items) {
|
|
9218
|
-
const batchInputs = mapBulkEdgeInputs(
|
|
9219
|
-
kind,
|
|
9220
|
-
items
|
|
9221
|
-
);
|
|
10083
|
+
const batchInputs = mapBulkEdgeInputs(kind, items);
|
|
9222
10084
|
if (backend.capabilities.transactions && "transaction" in backend) {
|
|
9223
10085
|
await backend.transaction(async (txBackend) => {
|
|
9224
10086
|
await executeEdgeCreateNoReturnBatch2(batchInputs, txBackend);
|
|
@@ -9369,11 +10231,7 @@ function createNodeCollection(config) {
|
|
|
9369
10231
|
async getByIds(ids, options) {
|
|
9370
10232
|
if (ids.length === 0) return [];
|
|
9371
10233
|
if (backend.getNodes !== void 0) {
|
|
9372
|
-
const rows = await backend.getNodes(
|
|
9373
|
-
graphId,
|
|
9374
|
-
kind,
|
|
9375
|
-
ids
|
|
9376
|
-
);
|
|
10234
|
+
const rows = await backend.getNodes(graphId, kind, ids);
|
|
9377
10235
|
const rowMap = /* @__PURE__ */ new Map();
|
|
9378
10236
|
for (const row of rows) {
|
|
9379
10237
|
rowMap.set(row.id, row);
|
|
@@ -9454,11 +10312,7 @@ function createNodeCollection(config) {
|
|
|
9454
10312
|
return backend.countNodesByKind(params);
|
|
9455
10313
|
},
|
|
9456
10314
|
async upsertById(id, props, options) {
|
|
9457
|
-
return this.upsertByIdFromRecord(
|
|
9458
|
-
id,
|
|
9459
|
-
props,
|
|
9460
|
-
options
|
|
9461
|
-
);
|
|
10315
|
+
return this.upsertByIdFromRecord(id, props, options);
|
|
9462
10316
|
},
|
|
9463
10317
|
async upsertByIdFromRecord(id, data, options) {
|
|
9464
10318
|
const existing = await backend.getNode(graphId, kind, id);
|
|
@@ -9478,10 +10332,7 @@ function createNodeCollection(config) {
|
|
|
9478
10332
|
return narrowNode(result);
|
|
9479
10333
|
},
|
|
9480
10334
|
async bulkCreate(items) {
|
|
9481
|
-
const batchInputs = mapBulkNodeInputs(
|
|
9482
|
-
kind,
|
|
9483
|
-
items
|
|
9484
|
-
);
|
|
10335
|
+
const batchInputs = mapBulkNodeInputs(kind, items);
|
|
9485
10336
|
if (backend.capabilities.transactions && "transaction" in backend) {
|
|
9486
10337
|
const results2 = await backend.transaction(
|
|
9487
10338
|
async (txBackend) => executeNodeCreateBatch2(batchInputs, txBackend)
|
|
@@ -9504,11 +10355,7 @@ function createNodeCollection(config) {
|
|
|
9504
10355
|
if (row !== void 0) existingMap.set(row.id, row);
|
|
9505
10356
|
}
|
|
9506
10357
|
} else {
|
|
9507
|
-
const rows = await target.getNodes(
|
|
9508
|
-
graphId,
|
|
9509
|
-
kind,
|
|
9510
|
-
ids
|
|
9511
|
-
);
|
|
10358
|
+
const rows = await target.getNodes(graphId, kind, ids);
|
|
9512
10359
|
for (const row of rows) {
|
|
9513
10360
|
existingMap.set(row.id, row);
|
|
9514
10361
|
}
|
|
@@ -9521,22 +10368,13 @@ function createNodeCollection(config) {
|
|
|
9521
10368
|
if (existing) {
|
|
9522
10369
|
toUpdate.push({
|
|
9523
10370
|
index: itemIndex,
|
|
9524
|
-
input: buildUpdateInput(
|
|
9525
|
-
kind,
|
|
9526
|
-
item.id,
|
|
9527
|
-
item.props,
|
|
9528
|
-
item
|
|
9529
|
-
),
|
|
10371
|
+
input: buildUpdateInput(kind, item.id, item.props, item),
|
|
9530
10372
|
clearDeleted: existing.deleted_at !== void 0
|
|
9531
10373
|
});
|
|
9532
10374
|
} else {
|
|
9533
10375
|
toCreate.push({
|
|
9534
10376
|
index: itemIndex,
|
|
9535
|
-
input: buildCreateInput(
|
|
9536
|
-
kind,
|
|
9537
|
-
item.props,
|
|
9538
|
-
item
|
|
9539
|
-
)
|
|
10377
|
+
input: buildCreateInput(kind, item.props, item)
|
|
9540
10378
|
});
|
|
9541
10379
|
}
|
|
9542
10380
|
itemIndex++;
|
|
@@ -9563,10 +10401,7 @@ function createNodeCollection(config) {
|
|
|
9563
10401
|
return upsertAll(backend);
|
|
9564
10402
|
},
|
|
9565
10403
|
async bulkInsert(items) {
|
|
9566
|
-
const batchInputs = mapBulkNodeInputs(
|
|
9567
|
-
kind,
|
|
9568
|
-
items
|
|
9569
|
-
);
|
|
10404
|
+
const batchInputs = mapBulkNodeInputs(kind, items);
|
|
9570
10405
|
if (backend.capabilities.transactions && "transaction" in backend) {
|
|
9571
10406
|
await backend.transaction(async (txBackend) => {
|
|
9572
10407
|
await executeNodeCreateNoReturnBatch2(batchInputs, txBackend);
|
|
@@ -10672,15 +11507,19 @@ async function executeEdgeBulkGetOrCreateByEndpoints(ctx, kind, items, backend,
|
|
|
10672
11507
|
}
|
|
10673
11508
|
|
|
10674
11509
|
// src/store/embedding-sync.ts
|
|
11510
|
+
var embeddingFieldsCache = /* @__PURE__ */ new WeakMap();
|
|
10675
11511
|
function getEmbeddingFields(schema) {
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
11512
|
+
const cached = embeddingFieldsCache.get(schema);
|
|
11513
|
+
if (cached) return cached;
|
|
11514
|
+
const fields = computeEmbeddingFields(schema);
|
|
11515
|
+
embeddingFieldsCache.set(schema, fields);
|
|
11516
|
+
return fields;
|
|
11517
|
+
}
|
|
11518
|
+
function computeEmbeddingFields(schema) {
|
|
11519
|
+
if (schema.type !== "object") return [];
|
|
10679
11520
|
const def = schema.def;
|
|
10680
11521
|
const shape = def.shape;
|
|
10681
|
-
if (!shape)
|
|
10682
|
-
return [];
|
|
10683
|
-
}
|
|
11522
|
+
if (!shape) return [];
|
|
10684
11523
|
const fields = [];
|
|
10685
11524
|
for (const [fieldPath, fieldSchema] of Object.entries(shape)) {
|
|
10686
11525
|
const dimensions = getEmbeddingDimensionsFromField(fieldSchema);
|
|
@@ -10767,6 +11606,101 @@ function isValidEmbeddingValue(value) {
|
|
|
10767
11606
|
return value.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
10768
11607
|
}
|
|
10769
11608
|
|
|
11609
|
+
// src/store/fulltext-sync.ts
|
|
11610
|
+
var searchableFieldsCache = /* @__PURE__ */ new WeakMap();
|
|
11611
|
+
function getSearchableFields(schema) {
|
|
11612
|
+
const cached = searchableFieldsCache.get(schema);
|
|
11613
|
+
if (cached) return cached;
|
|
11614
|
+
const fields = computeSearchableFields(schema);
|
|
11615
|
+
searchableFieldsCache.set(schema, fields);
|
|
11616
|
+
return fields;
|
|
11617
|
+
}
|
|
11618
|
+
function computeSearchableFields(schema) {
|
|
11619
|
+
if (schema.type !== "object") return [];
|
|
11620
|
+
const def = schema.def;
|
|
11621
|
+
const shape = def.shape;
|
|
11622
|
+
if (!shape) return [];
|
|
11623
|
+
const fields = [];
|
|
11624
|
+
for (const [fieldPath, fieldSchema] of Object.entries(shape)) {
|
|
11625
|
+
const metadata = getSearchableMetadata(fieldSchema);
|
|
11626
|
+
if (metadata !== void 0) {
|
|
11627
|
+
fields.push({ fieldPath, metadata });
|
|
11628
|
+
}
|
|
11629
|
+
}
|
|
11630
|
+
warnIfConflictingLanguages(fields);
|
|
11631
|
+
return fields;
|
|
11632
|
+
}
|
|
11633
|
+
function warnIfConflictingLanguages(fields) {
|
|
11634
|
+
if (fields.length < 2) return;
|
|
11635
|
+
const languages = new Set(fields.map((field2) => field2.metadata.language));
|
|
11636
|
+
if (languages.size < 2) return;
|
|
11637
|
+
if (typeof console === "undefined" || typeof console.warn !== "function") {
|
|
11638
|
+
return;
|
|
11639
|
+
}
|
|
11640
|
+
const fieldSummary = fields.map((field2) => `${field2.fieldPath}=${field2.metadata.language}`).join(", ");
|
|
11641
|
+
const winning = fields[0]?.metadata.language ?? DEFAULT_SEARCHABLE_LANGUAGE;
|
|
11642
|
+
console.warn(
|
|
11643
|
+
`[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.`
|
|
11644
|
+
);
|
|
11645
|
+
}
|
|
11646
|
+
var FIELD_SEPARATOR = "\n";
|
|
11647
|
+
function resolveCombinedLanguage(fields) {
|
|
11648
|
+
return fields[0]?.metadata.language ?? DEFAULT_SEARCHABLE_LANGUAGE;
|
|
11649
|
+
}
|
|
11650
|
+
function computeFulltextContent(schema, props) {
|
|
11651
|
+
const searchableFields = getSearchableFields(schema);
|
|
11652
|
+
if (searchableFields.length === 0) return void 0;
|
|
11653
|
+
const parts = [];
|
|
11654
|
+
for (const field2 of searchableFields) {
|
|
11655
|
+
const value = props[field2.fieldPath];
|
|
11656
|
+
if (typeof value === "string" && value.length > 0) {
|
|
11657
|
+
parts.push(value);
|
|
11658
|
+
}
|
|
11659
|
+
}
|
|
11660
|
+
if (parts.length === 0) return void 0;
|
|
11661
|
+
return {
|
|
11662
|
+
content: parts.join(FIELD_SEPARATOR),
|
|
11663
|
+
language: resolveCombinedLanguage(searchableFields)
|
|
11664
|
+
};
|
|
11665
|
+
}
|
|
11666
|
+
async function syncFulltext(ctx, schema, props) {
|
|
11667
|
+
const { backend } = ctx;
|
|
11668
|
+
if (!backend.upsertFulltext || !backend.deleteFulltext) {
|
|
11669
|
+
return;
|
|
11670
|
+
}
|
|
11671
|
+
const computed = computeFulltextContent(schema, props);
|
|
11672
|
+
if (computed !== void 0) {
|
|
11673
|
+
await backend.upsertFulltext({
|
|
11674
|
+
graphId: ctx.graphId,
|
|
11675
|
+
nodeKind: ctx.nodeKind,
|
|
11676
|
+
nodeId: ctx.nodeId,
|
|
11677
|
+
content: computed.content,
|
|
11678
|
+
language: computed.language
|
|
11679
|
+
});
|
|
11680
|
+
return;
|
|
11681
|
+
}
|
|
11682
|
+
if (getSearchableFields(schema).length === 0) return;
|
|
11683
|
+
await backend.deleteFulltext({
|
|
11684
|
+
graphId: ctx.graphId,
|
|
11685
|
+
nodeKind: ctx.nodeKind,
|
|
11686
|
+
nodeId: ctx.nodeId
|
|
11687
|
+
});
|
|
11688
|
+
}
|
|
11689
|
+
async function deleteNodeFulltext(ctx, schema) {
|
|
11690
|
+
const { backend } = ctx;
|
|
11691
|
+
if (!backend.deleteFulltext) {
|
|
11692
|
+
return;
|
|
11693
|
+
}
|
|
11694
|
+
if (getSearchableFields(schema).length === 0) {
|
|
11695
|
+
return;
|
|
11696
|
+
}
|
|
11697
|
+
await backend.deleteFulltext({
|
|
11698
|
+
graphId: ctx.graphId,
|
|
11699
|
+
nodeKind: ctx.nodeKind,
|
|
11700
|
+
nodeId: ctx.nodeId
|
|
11701
|
+
});
|
|
11702
|
+
}
|
|
11703
|
+
|
|
10770
11704
|
// src/store/uniqueness.ts
|
|
10771
11705
|
async function checkUniquenessConstraints(ctx, kind, id, props, constraints) {
|
|
10772
11706
|
for (const constraint of constraints) {
|
|
@@ -10960,6 +11894,9 @@ function createUniquenessContext(graphId, registry, backend) {
|
|
|
10960
11894
|
function createEmbeddingSyncContext(graphId, nodeKind, nodeId, backend) {
|
|
10961
11895
|
return { graphId, nodeKind, nodeId, backend };
|
|
10962
11896
|
}
|
|
11897
|
+
function createFulltextSyncContext(graphId, nodeKind, nodeId, backend) {
|
|
11898
|
+
return { graphId, nodeKind, nodeId, backend };
|
|
11899
|
+
}
|
|
10963
11900
|
function createNodeBatchValidationBackend(graphId, registry, backend) {
|
|
10964
11901
|
const nodeCache = /* @__PURE__ */ new Map();
|
|
10965
11902
|
const pendingNodes = /* @__PURE__ */ new Map();
|
|
@@ -11117,6 +12054,11 @@ async function finalizeNodeCreate(ctx, prepared, backend) {
|
|
|
11117
12054
|
prepared.nodeKind.schema,
|
|
11118
12055
|
prepared.validatedProps
|
|
11119
12056
|
);
|
|
12057
|
+
await syncFulltext(
|
|
12058
|
+
createFulltextSyncContext(ctx.graphId, prepared.kind, prepared.id, backend),
|
|
12059
|
+
prepared.nodeKind.schema,
|
|
12060
|
+
prepared.validatedProps
|
|
12061
|
+
);
|
|
11120
12062
|
}
|
|
11121
12063
|
async function performNodeUpdate(ctx, input, backend, options) {
|
|
11122
12064
|
const { kind, id } = input;
|
|
@@ -11157,6 +12099,11 @@ async function performNodeUpdate(ctx, input, backend, options) {
|
|
|
11157
12099
|
nodeKind.schema,
|
|
11158
12100
|
validatedProps
|
|
11159
12101
|
);
|
|
12102
|
+
await syncFulltext(
|
|
12103
|
+
createFulltextSyncContext(ctx.graphId, kind, id, backend),
|
|
12104
|
+
nodeKind.schema,
|
|
12105
|
+
validatedProps
|
|
12106
|
+
);
|
|
11160
12107
|
return rowToNode(row);
|
|
11161
12108
|
}
|
|
11162
12109
|
async function enforceDeleteBehavior(ctx, kind, id, mode, backend, registration) {
|
|
@@ -11373,6 +12320,10 @@ async function executeNodeDelete(ctx, kind, id, backend) {
|
|
|
11373
12320
|
createEmbeddingSyncContext(ctx.graphId, kind, id, backend),
|
|
11374
12321
|
nodeKind.schema
|
|
11375
12322
|
);
|
|
12323
|
+
await deleteNodeFulltext(
|
|
12324
|
+
createFulltextSyncContext(ctx.graphId, kind, id, backend),
|
|
12325
|
+
nodeKind.schema
|
|
12326
|
+
);
|
|
11376
12327
|
});
|
|
11377
12328
|
}
|
|
11378
12329
|
async function executeNodeHardDelete(ctx, kind, id, backend) {
|
|
@@ -11679,6 +12630,503 @@ async function executeNodeBulkGetOrCreateByConstraint(ctx, kind, constraintName,
|
|
|
11679
12630
|
return results;
|
|
11680
12631
|
}
|
|
11681
12632
|
|
|
12633
|
+
// src/store/fulltext-rebuild.ts
|
|
12634
|
+
var DEFAULT_PAGE_SIZE = 500;
|
|
12635
|
+
var DEFAULT_MAX_SKIPPED_IDS = 1e4;
|
|
12636
|
+
function validatePageSize(value) {
|
|
12637
|
+
if (value === void 0) return DEFAULT_PAGE_SIZE;
|
|
12638
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
12639
|
+
throw new ValidationError(
|
|
12640
|
+
`pageSize must be a positive integer, got: ${String(value)}`,
|
|
12641
|
+
{
|
|
12642
|
+
issues: [{ path: "pageSize", message: "Must be a positive integer." }]
|
|
12643
|
+
}
|
|
12644
|
+
);
|
|
12645
|
+
}
|
|
12646
|
+
return value;
|
|
12647
|
+
}
|
|
12648
|
+
function validateMaxSkippedIds(value) {
|
|
12649
|
+
if (value === void 0) return DEFAULT_MAX_SKIPPED_IDS;
|
|
12650
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
12651
|
+
throw new ValidationError(
|
|
12652
|
+
`maxSkippedIds must be a non-negative integer, got: ${String(value)}`,
|
|
12653
|
+
{
|
|
12654
|
+
issues: [
|
|
12655
|
+
{
|
|
12656
|
+
path: "maxSkippedIds",
|
|
12657
|
+
message: "Must be a non-negative integer."
|
|
12658
|
+
}
|
|
12659
|
+
]
|
|
12660
|
+
}
|
|
12661
|
+
);
|
|
12662
|
+
}
|
|
12663
|
+
return value;
|
|
12664
|
+
}
|
|
12665
|
+
function isPropsObject(value) {
|
|
12666
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12667
|
+
}
|
|
12668
|
+
async function rebuildFulltextIndex(ctx, nodeKind, options) {
|
|
12669
|
+
const { backend, registry } = ctx;
|
|
12670
|
+
if (!backend.upsertFulltext || !backend.deleteFulltext) {
|
|
12671
|
+
throw new ConfigurationError(
|
|
12672
|
+
"Backend does not support fulltext; cannot rebuild index",
|
|
12673
|
+
{ backend: backend.dialect, capability: "fulltext" }
|
|
12674
|
+
);
|
|
12675
|
+
}
|
|
12676
|
+
const pageSize = validatePageSize(options.pageSize);
|
|
12677
|
+
const maxSkippedIds = validateMaxSkippedIds(options.maxSkippedIds);
|
|
12678
|
+
const targetKinds = nodeKind === void 0 ? [...registry.nodeKinds.keys()] : [nodeKind];
|
|
12679
|
+
const rebuiltKinds = [];
|
|
12680
|
+
let processed = 0;
|
|
12681
|
+
let upserted = 0;
|
|
12682
|
+
let cleared = 0;
|
|
12683
|
+
let skipped = 0;
|
|
12684
|
+
const skippedIds = [];
|
|
12685
|
+
for (const kind of targetKinds) {
|
|
12686
|
+
const nodeType = registry.getNodeType(kind);
|
|
12687
|
+
if (!nodeType) {
|
|
12688
|
+
throw new ConfigurationError(`Unknown node kind: ${kind}`, { kind });
|
|
12689
|
+
}
|
|
12690
|
+
if (getSearchableFields(nodeType.schema).length === 0) {
|
|
12691
|
+
continue;
|
|
12692
|
+
}
|
|
12693
|
+
rebuiltKinds.push(kind);
|
|
12694
|
+
let cursor;
|
|
12695
|
+
for (; ; ) {
|
|
12696
|
+
const rows = await backend.findNodesByKind({
|
|
12697
|
+
graphId: ctx.graphId,
|
|
12698
|
+
kind,
|
|
12699
|
+
limit: pageSize,
|
|
12700
|
+
excludeDeleted: false,
|
|
12701
|
+
orderBy: "id",
|
|
12702
|
+
...cursor === void 0 ? {} : { after: cursor }
|
|
12703
|
+
});
|
|
12704
|
+
if (rows.length === 0) break;
|
|
12705
|
+
const pageResult = processPage(nodeType.schema, rows);
|
|
12706
|
+
skipped += pageResult.skipped;
|
|
12707
|
+
const remaining = maxSkippedIds - skippedIds.length;
|
|
12708
|
+
if (remaining > 0 && pageResult.skippedIds.length > 0) {
|
|
12709
|
+
skippedIds.push(...pageResult.skippedIds.slice(0, remaining));
|
|
12710
|
+
}
|
|
12711
|
+
await backend.transaction(async (tx) => {
|
|
12712
|
+
if (pageResult.toUpsert.length > 0) {
|
|
12713
|
+
if (tx.upsertFulltextBatch) {
|
|
12714
|
+
await tx.upsertFulltextBatch({
|
|
12715
|
+
graphId: ctx.graphId,
|
|
12716
|
+
nodeKind: kind,
|
|
12717
|
+
rows: pageResult.toUpsert
|
|
12718
|
+
});
|
|
12719
|
+
} else if (tx.upsertFulltext) {
|
|
12720
|
+
for (const item of pageResult.toUpsert) {
|
|
12721
|
+
await tx.upsertFulltext({
|
|
12722
|
+
graphId: ctx.graphId,
|
|
12723
|
+
nodeKind: kind,
|
|
12724
|
+
nodeId: item.nodeId,
|
|
12725
|
+
content: item.content,
|
|
12726
|
+
language: item.language
|
|
12727
|
+
});
|
|
12728
|
+
}
|
|
12729
|
+
}
|
|
12730
|
+
}
|
|
12731
|
+
if (pageResult.toDelete.length > 0) {
|
|
12732
|
+
if (tx.deleteFulltextBatch) {
|
|
12733
|
+
await tx.deleteFulltextBatch({
|
|
12734
|
+
graphId: ctx.graphId,
|
|
12735
|
+
nodeKind: kind,
|
|
12736
|
+
nodeIds: pageResult.toDelete
|
|
12737
|
+
});
|
|
12738
|
+
} else if (tx.deleteFulltext) {
|
|
12739
|
+
for (const nodeId of pageResult.toDelete) {
|
|
12740
|
+
await tx.deleteFulltext({
|
|
12741
|
+
graphId: ctx.graphId,
|
|
12742
|
+
nodeKind: kind,
|
|
12743
|
+
nodeId
|
|
12744
|
+
});
|
|
12745
|
+
}
|
|
12746
|
+
}
|
|
12747
|
+
}
|
|
12748
|
+
});
|
|
12749
|
+
processed += rows.length;
|
|
12750
|
+
upserted += pageResult.toUpsert.length;
|
|
12751
|
+
cleared += pageResult.toDelete.length;
|
|
12752
|
+
const lastRow = rows.at(-1);
|
|
12753
|
+
if (!lastRow) break;
|
|
12754
|
+
cursor = lastRow.id;
|
|
12755
|
+
if (rows.length < pageSize) break;
|
|
12756
|
+
}
|
|
12757
|
+
}
|
|
12758
|
+
return {
|
|
12759
|
+
kinds: rebuiltKinds,
|
|
12760
|
+
processed,
|
|
12761
|
+
upserted,
|
|
12762
|
+
cleared,
|
|
12763
|
+
skipped,
|
|
12764
|
+
skippedIds,
|
|
12765
|
+
skippedTruncated: skipped > skippedIds.length
|
|
12766
|
+
};
|
|
12767
|
+
}
|
|
12768
|
+
function processPage(schema, rows) {
|
|
12769
|
+
const toUpsert = [];
|
|
12770
|
+
const toDelete = [];
|
|
12771
|
+
const skippedIds = [];
|
|
12772
|
+
let skipped = 0;
|
|
12773
|
+
for (const row of rows) {
|
|
12774
|
+
if (row.deleted_at !== void 0) {
|
|
12775
|
+
toDelete.push(row.id);
|
|
12776
|
+
continue;
|
|
12777
|
+
}
|
|
12778
|
+
let parsed;
|
|
12779
|
+
try {
|
|
12780
|
+
parsed = JSON.parse(row.props);
|
|
12781
|
+
} catch {
|
|
12782
|
+
skipped += 1;
|
|
12783
|
+
skippedIds.push(row.id);
|
|
12784
|
+
continue;
|
|
12785
|
+
}
|
|
12786
|
+
if (!isPropsObject(parsed)) {
|
|
12787
|
+
skipped += 1;
|
|
12788
|
+
skippedIds.push(row.id);
|
|
12789
|
+
continue;
|
|
12790
|
+
}
|
|
12791
|
+
const computed = computeFulltextContent(schema, parsed);
|
|
12792
|
+
if (computed === void 0) {
|
|
12793
|
+
toDelete.push(row.id);
|
|
12794
|
+
} else {
|
|
12795
|
+
toUpsert.push({
|
|
12796
|
+
nodeId: row.id,
|
|
12797
|
+
content: computed.content,
|
|
12798
|
+
language: computed.language
|
|
12799
|
+
});
|
|
12800
|
+
}
|
|
12801
|
+
}
|
|
12802
|
+
return { toUpsert, toDelete, skipped, skippedIds };
|
|
12803
|
+
}
|
|
12804
|
+
|
|
12805
|
+
// src/store/search.ts
|
|
12806
|
+
async function executeFulltextSearch(ctx, nodeKind, options) {
|
|
12807
|
+
const { backend, graphId } = ctx;
|
|
12808
|
+
if (!backend.fulltextSearch) {
|
|
12809
|
+
throw new ConfigurationError("Backend does not support fulltext search", {
|
|
12810
|
+
backend: backend.dialect,
|
|
12811
|
+
capability: "fulltext"
|
|
12812
|
+
});
|
|
12813
|
+
}
|
|
12814
|
+
if (!Number.isInteger(options.limit) || options.limit <= 0) {
|
|
12815
|
+
throw new RangeError(
|
|
12816
|
+
`fulltextSearch.limit must be a positive integer, got: ${options.limit}`
|
|
12817
|
+
);
|
|
12818
|
+
}
|
|
12819
|
+
validateFulltextCallOptions(backend, {
|
|
12820
|
+
mode: options.mode,
|
|
12821
|
+
includeSnippets: options.includeSnippets,
|
|
12822
|
+
language: options.language
|
|
12823
|
+
});
|
|
12824
|
+
const params = {
|
|
12825
|
+
graphId,
|
|
12826
|
+
nodeKind,
|
|
12827
|
+
query: options.query,
|
|
12828
|
+
limit: options.limit,
|
|
12829
|
+
...options.mode ? { mode: options.mode } : {},
|
|
12830
|
+
...options.language ? { language: options.language } : {},
|
|
12831
|
+
...options.minScore === void 0 ? {} : { minScore: options.minScore },
|
|
12832
|
+
...options.includeSnippets === void 0 ? {} : { includeSnippets: options.includeSnippets }
|
|
12833
|
+
};
|
|
12834
|
+
const rows = await backend.fulltextSearch(params);
|
|
12835
|
+
if (rows.length === 0) return [];
|
|
12836
|
+
const ids = rows.map((r) => r.nodeId);
|
|
12837
|
+
const nodeMap = await fetchNodesByIds(backend, graphId, nodeKind, ids);
|
|
12838
|
+
const hits = [];
|
|
12839
|
+
let rank = 1;
|
|
12840
|
+
for (const row of rows) {
|
|
12841
|
+
const node = nodeMap.get(row.nodeId);
|
|
12842
|
+
if (!node) continue;
|
|
12843
|
+
hits.push({
|
|
12844
|
+
node,
|
|
12845
|
+
score: row.score,
|
|
12846
|
+
rank,
|
|
12847
|
+
...row.snippet === void 0 ? {} : { snippet: row.snippet }
|
|
12848
|
+
});
|
|
12849
|
+
rank += 1;
|
|
12850
|
+
}
|
|
12851
|
+
return hits;
|
|
12852
|
+
}
|
|
12853
|
+
async function executeHybridSearch(ctx, nodeKind, options) {
|
|
12854
|
+
const { backend, graphId } = ctx;
|
|
12855
|
+
if (!backend.vectorSearch) {
|
|
12856
|
+
throw new ConfigurationError("Backend does not support vector search", {
|
|
12857
|
+
backend: backend.dialect,
|
|
12858
|
+
capability: "vector"
|
|
12859
|
+
});
|
|
12860
|
+
}
|
|
12861
|
+
if (!backend.fulltextSearch) {
|
|
12862
|
+
throw new ConfigurationError("Backend does not support fulltext search", {
|
|
12863
|
+
backend: backend.dialect,
|
|
12864
|
+
capability: "fulltext"
|
|
12865
|
+
});
|
|
12866
|
+
}
|
|
12867
|
+
if (!Number.isInteger(options.limit) || options.limit <= 0) {
|
|
12868
|
+
throw new RangeError(
|
|
12869
|
+
`hybridSearch.limit must be a positive integer, got: ${options.limit}`
|
|
12870
|
+
);
|
|
12871
|
+
}
|
|
12872
|
+
if (options.fusion !== void 0) {
|
|
12873
|
+
validateHybridFusionOptions(options.fusion);
|
|
12874
|
+
}
|
|
12875
|
+
validateFulltextCallOptions(backend, {
|
|
12876
|
+
mode: options.fulltext.mode,
|
|
12877
|
+
includeSnippets: options.fulltext.includeSnippets,
|
|
12878
|
+
language: options.fulltext.language
|
|
12879
|
+
});
|
|
12880
|
+
const fusionK = options.fusion?.k ?? DEFAULT_RRF_K;
|
|
12881
|
+
const vectorWeight = options.fusion?.weights?.vector ?? DEFAULT_RRF_WEIGHT;
|
|
12882
|
+
const fulltextWeight = options.fusion?.weights?.fulltext ?? DEFAULT_RRF_WEIGHT;
|
|
12883
|
+
const overFetchMultiplier = 4;
|
|
12884
|
+
const vectorK = options.vector.k ?? options.limit * overFetchMultiplier;
|
|
12885
|
+
const fulltextK = options.fulltext.k ?? options.limit * overFetchMultiplier;
|
|
12886
|
+
const vectorPromise = backend.vectorSearch({
|
|
12887
|
+
graphId,
|
|
12888
|
+
nodeKind,
|
|
12889
|
+
fieldPath: options.vector.fieldPath,
|
|
12890
|
+
queryEmbedding: options.vector.queryEmbedding,
|
|
12891
|
+
metric: options.vector.metric ?? "cosine",
|
|
12892
|
+
limit: vectorK,
|
|
12893
|
+
...options.vector.minScore === void 0 ? {} : { minScore: options.vector.minScore }
|
|
12894
|
+
});
|
|
12895
|
+
const fulltextPromise = backend.fulltextSearch({
|
|
12896
|
+
graphId,
|
|
12897
|
+
nodeKind,
|
|
12898
|
+
query: options.fulltext.query,
|
|
12899
|
+
limit: fulltextK,
|
|
12900
|
+
...options.fulltext.mode ? { mode: options.fulltext.mode } : {},
|
|
12901
|
+
...options.fulltext.language ? { language: options.fulltext.language } : {},
|
|
12902
|
+
...options.fulltext.minScore === void 0 ? {} : { minScore: options.fulltext.minScore },
|
|
12903
|
+
...options.fulltext.includeSnippets === void 0 ? {} : { includeSnippets: options.fulltext.includeSnippets }
|
|
12904
|
+
});
|
|
12905
|
+
const [vectorRows, fulltextRows] = await Promise.all([
|
|
12906
|
+
vectorPromise,
|
|
12907
|
+
fulltextPromise
|
|
12908
|
+
]);
|
|
12909
|
+
const fused = /* @__PURE__ */ new Map();
|
|
12910
|
+
for (const [index, row] of vectorRows.entries()) {
|
|
12911
|
+
const rank2 = index + 1;
|
|
12912
|
+
const contribution = vectorWeight / (fusionK + rank2);
|
|
12913
|
+
const entry = fused.get(row.nodeId) ?? {
|
|
12914
|
+
nodeId: row.nodeId,
|
|
12915
|
+
fusedScore: 0
|
|
12916
|
+
};
|
|
12917
|
+
entry.fusedScore += contribution;
|
|
12918
|
+
entry.vectorRank = rank2;
|
|
12919
|
+
entry.vectorScore = row.score;
|
|
12920
|
+
fused.set(row.nodeId, entry);
|
|
12921
|
+
}
|
|
12922
|
+
for (const [index, row] of fulltextRows.entries()) {
|
|
12923
|
+
const rank2 = index + 1;
|
|
12924
|
+
const contribution = fulltextWeight / (fusionK + rank2);
|
|
12925
|
+
const entry = fused.get(row.nodeId) ?? {
|
|
12926
|
+
nodeId: row.nodeId,
|
|
12927
|
+
fusedScore: 0
|
|
12928
|
+
};
|
|
12929
|
+
entry.fusedScore += contribution;
|
|
12930
|
+
entry.fulltextRank = rank2;
|
|
12931
|
+
entry.fulltextScore = row.score;
|
|
12932
|
+
if (row.snippet !== void 0) {
|
|
12933
|
+
entry.fulltextSnippet = row.snippet;
|
|
12934
|
+
}
|
|
12935
|
+
fused.set(row.nodeId, entry);
|
|
12936
|
+
}
|
|
12937
|
+
const ranked = [...fused.values()].toSorted(
|
|
12938
|
+
(a, b) => b.fusedScore - a.fusedScore || a.nodeId.localeCompare(b.nodeId)
|
|
12939
|
+
).slice(0, options.limit);
|
|
12940
|
+
const ids = ranked.map((entry) => entry.nodeId);
|
|
12941
|
+
const nodeMap = await fetchNodesByIds(backend, graphId, nodeKind, ids);
|
|
12942
|
+
const hits = [];
|
|
12943
|
+
let rank = 1;
|
|
12944
|
+
for (const entry of ranked) {
|
|
12945
|
+
const node = nodeMap.get(entry.nodeId);
|
|
12946
|
+
if (!node) continue;
|
|
12947
|
+
const typedNode = node;
|
|
12948
|
+
const hit = {
|
|
12949
|
+
node: typedNode,
|
|
12950
|
+
score: entry.fusedScore,
|
|
12951
|
+
rank,
|
|
12952
|
+
...entry.vectorRank !== void 0 && entry.vectorScore !== void 0 ? {
|
|
12953
|
+
vector: {
|
|
12954
|
+
node: typedNode,
|
|
12955
|
+
score: entry.vectorScore,
|
|
12956
|
+
rank: entry.vectorRank
|
|
12957
|
+
}
|
|
12958
|
+
} : {},
|
|
12959
|
+
...entry.fulltextRank !== void 0 && entry.fulltextScore !== void 0 ? {
|
|
12960
|
+
fulltext: {
|
|
12961
|
+
node: typedNode,
|
|
12962
|
+
score: entry.fulltextScore,
|
|
12963
|
+
rank: entry.fulltextRank,
|
|
12964
|
+
...entry.fulltextSnippet === void 0 ? {} : { snippet: entry.fulltextSnippet }
|
|
12965
|
+
}
|
|
12966
|
+
} : {}
|
|
12967
|
+
};
|
|
12968
|
+
hits.push(hit);
|
|
12969
|
+
rank += 1;
|
|
12970
|
+
}
|
|
12971
|
+
return hits;
|
|
12972
|
+
}
|
|
12973
|
+
function validateFulltextCallOptions(backend, options) {
|
|
12974
|
+
const strategy = backend.fulltextStrategy;
|
|
12975
|
+
const capabilities = backend.capabilities.fulltext;
|
|
12976
|
+
if (options.mode !== void 0) {
|
|
12977
|
+
validateFulltextMode(backend, strategy, capabilities, options.mode);
|
|
12978
|
+
}
|
|
12979
|
+
if (options.includeSnippets === true) {
|
|
12980
|
+
validateFulltextSnippets(backend, strategy, capabilities);
|
|
12981
|
+
}
|
|
12982
|
+
if (options.language !== void 0) {
|
|
12983
|
+
validateFulltextLanguageOption(
|
|
12984
|
+
backend,
|
|
12985
|
+
strategy,
|
|
12986
|
+
capabilities,
|
|
12987
|
+
options.language
|
|
12988
|
+
);
|
|
12989
|
+
}
|
|
12990
|
+
}
|
|
12991
|
+
function validateFulltextMode(backend, strategy, capabilities, mode) {
|
|
12992
|
+
if (strategy !== void 0) {
|
|
12993
|
+
if (strategy.supportedModes.includes(mode)) return;
|
|
12994
|
+
throw new ConfigurationError(
|
|
12995
|
+
`Backend "${backend.dialect}" fulltext strategy "${strategy.name}" does not support mode "${mode}". Supported modes: ${strategy.supportedModes.join(", ")}`,
|
|
12996
|
+
{
|
|
12997
|
+
backend: backend.dialect,
|
|
12998
|
+
strategy: strategy.name,
|
|
12999
|
+
capability: `fulltext.modes.${mode}`
|
|
13000
|
+
}
|
|
13001
|
+
);
|
|
13002
|
+
}
|
|
13003
|
+
if (mode === "phrase" && capabilities && !capabilities.phraseQueries) {
|
|
13004
|
+
throw new ConfigurationError(
|
|
13005
|
+
`Backend "${backend.dialect}" does not support phrase queries`,
|
|
13006
|
+
{ backend: backend.dialect, capability: "fulltext.phraseQueries" }
|
|
13007
|
+
);
|
|
13008
|
+
}
|
|
13009
|
+
}
|
|
13010
|
+
function validateFulltextSnippets(backend, strategy, capabilities) {
|
|
13011
|
+
const supportsSnippets = strategy === void 0 ? capabilities?.highlighting !== false : strategy.supportsSnippets;
|
|
13012
|
+
if (supportsSnippets) return;
|
|
13013
|
+
throw new ConfigurationError(
|
|
13014
|
+
`Backend "${backend.dialect}" does not support snippet highlighting; drop includeSnippets or switch to a backend where highlighting is supported`,
|
|
13015
|
+
{ backend: backend.dialect, capability: "fulltext.highlighting" }
|
|
13016
|
+
);
|
|
13017
|
+
}
|
|
13018
|
+
function validateFulltextLanguageOption(backend, strategy, capabilities, language) {
|
|
13019
|
+
if (strategy !== void 0 && !strategy.supportsLanguageOverride) {
|
|
13020
|
+
throw new ConfigurationError(
|
|
13021
|
+
`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\`.`,
|
|
13022
|
+
{
|
|
13023
|
+
backend: backend.dialect,
|
|
13024
|
+
strategy: strategy.name,
|
|
13025
|
+
capability: "fulltext.languageOverride"
|
|
13026
|
+
}
|
|
13027
|
+
);
|
|
13028
|
+
}
|
|
13029
|
+
if (capabilities !== void 0) {
|
|
13030
|
+
warnIfLanguageNotAdvertised(backend.dialect, capabilities, language);
|
|
13031
|
+
}
|
|
13032
|
+
}
|
|
13033
|
+
function warnIfLanguageNotAdvertised(dialect, capabilities, language) {
|
|
13034
|
+
if (capabilities.languages.length === 0) return;
|
|
13035
|
+
if (capabilities.languages.includes(language)) return;
|
|
13036
|
+
if (typeof console === "undefined" || typeof console.warn !== "function") {
|
|
13037
|
+
return;
|
|
13038
|
+
}
|
|
13039
|
+
console.warn(
|
|
13040
|
+
`[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.`
|
|
13041
|
+
);
|
|
13042
|
+
}
|
|
13043
|
+
async function fetchNodesByIds(backend, graphId, nodeKind, ids) {
|
|
13044
|
+
if (ids.length === 0) return /* @__PURE__ */ new Map();
|
|
13045
|
+
const map = /* @__PURE__ */ new Map();
|
|
13046
|
+
if (backend.getNodes) {
|
|
13047
|
+
const rows = await backend.getNodes(graphId, nodeKind, ids);
|
|
13048
|
+
for (const row of rows) {
|
|
13049
|
+
if (row.deleted_at !== void 0) continue;
|
|
13050
|
+
map.set(row.id, rowToNode(row));
|
|
13051
|
+
}
|
|
13052
|
+
return map;
|
|
13053
|
+
}
|
|
13054
|
+
for (const id of ids) {
|
|
13055
|
+
const row = await backend.getNode(graphId, nodeKind, id);
|
|
13056
|
+
if (row && row.deleted_at === void 0) map.set(row.id, rowToNode(row));
|
|
13057
|
+
}
|
|
13058
|
+
return map;
|
|
13059
|
+
}
|
|
13060
|
+
|
|
13061
|
+
// src/store/search-facade.ts
|
|
13062
|
+
var StoreSearch = class {
|
|
13063
|
+
#context;
|
|
13064
|
+
constructor(context) {
|
|
13065
|
+
this.#context = context;
|
|
13066
|
+
}
|
|
13067
|
+
/**
|
|
13068
|
+
* Runs a fulltext search against nodes of the given kind.
|
|
13069
|
+
*
|
|
13070
|
+
* Requires fields on the node schema declared with `searchable()`.
|
|
13071
|
+
* The search hits the backend's fulltext index (tsvector + GIN on
|
|
13072
|
+
* Postgres, FTS5 on SQLite — or whatever strategy the backend is
|
|
13073
|
+
* configured with) and resolves the matching node IDs back to typed
|
|
13074
|
+
* `Node` objects.
|
|
13075
|
+
*/
|
|
13076
|
+
async fulltext(nodeKind, options) {
|
|
13077
|
+
return executeFulltextSearch(
|
|
13078
|
+
{ graphId: this.#context.graphId, backend: this.#context.backend },
|
|
13079
|
+
nodeKind,
|
|
13080
|
+
options
|
|
13081
|
+
);
|
|
13082
|
+
}
|
|
13083
|
+
/**
|
|
13084
|
+
* Runs a vector + fulltext hybrid search and fuses the results with
|
|
13085
|
+
* Reciprocal Rank Fusion.
|
|
13086
|
+
*
|
|
13087
|
+
* RRF is rank-based, so it composes well across heterogeneous score
|
|
13088
|
+
* scales (cosine similarity vs ts_rank_cd vs FTS5 BM25). Default
|
|
13089
|
+
* over-fetch is 4× `limit` from each source — tune via `vector.k` /
|
|
13090
|
+
* `fulltext.k` for higher-recall corpora.
|
|
13091
|
+
*/
|
|
13092
|
+
async hybrid(nodeKind, options) {
|
|
13093
|
+
return executeHybridSearch(
|
|
13094
|
+
{ graphId: this.#context.graphId, backend: this.#context.backend },
|
|
13095
|
+
nodeKind,
|
|
13096
|
+
options
|
|
13097
|
+
);
|
|
13098
|
+
}
|
|
13099
|
+
/**
|
|
13100
|
+
* Rebuilds the fulltext index from existing node data.
|
|
13101
|
+
*
|
|
13102
|
+
* Use when:
|
|
13103
|
+
* - A node kind gained a `searchable()` field after data was already
|
|
13104
|
+
* written and existing rows were never indexed.
|
|
13105
|
+
* - The fulltext table was dropped / truncated.
|
|
13106
|
+
* - `language` was changed on a `searchable()` field.
|
|
13107
|
+
*
|
|
13108
|
+
* Iterates nodes with keyset pagination (stable under shared
|
|
13109
|
+
* timestamps and light concurrent writes), transacts per page, skips
|
|
13110
|
+
* kinds with no searchable fields, and cleans up stale rows for
|
|
13111
|
+
* soft-deleted nodes. Corrupt or non-object `props` are counted in
|
|
13112
|
+
* `skipped` with their node IDs surfaced via `skippedIds` so operators
|
|
13113
|
+
* can investigate. Concurrent hard-deletes between page fetches can
|
|
13114
|
+
* be missed by a single pass — run during a maintenance window for
|
|
13115
|
+
* full consistency.
|
|
13116
|
+
*/
|
|
13117
|
+
async rebuildFulltext(nodeKind, options = {}) {
|
|
13118
|
+
return rebuildFulltextIndex(
|
|
13119
|
+
{
|
|
13120
|
+
graphId: this.#context.graphId,
|
|
13121
|
+
backend: this.#context.backend,
|
|
13122
|
+
registry: this.#context.registry
|
|
13123
|
+
},
|
|
13124
|
+
nodeKind,
|
|
13125
|
+
options
|
|
13126
|
+
);
|
|
13127
|
+
}
|
|
13128
|
+
};
|
|
13129
|
+
|
|
11682
13130
|
// src/store/store.ts
|
|
11683
13131
|
var Store = class {
|
|
11684
13132
|
#graph;
|
|
@@ -11689,6 +13137,8 @@ var Store = class {
|
|
|
11689
13137
|
#defaultTraversalExpansion;
|
|
11690
13138
|
#nodeCollections;
|
|
11691
13139
|
#edgeCollections;
|
|
13140
|
+
#algorithms;
|
|
13141
|
+
#search;
|
|
11692
13142
|
constructor(graph, backend, options) {
|
|
11693
13143
|
this.#graph = graph;
|
|
11694
13144
|
this.#backend = backend;
|
|
@@ -11770,6 +13220,38 @@ var Store = class {
|
|
|
11770
13220
|
}
|
|
11771
13221
|
return this.#edgeCollections;
|
|
11772
13222
|
}
|
|
13223
|
+
// === Graph Algorithms ===
|
|
13224
|
+
/**
|
|
13225
|
+
* Tier 1 graph algorithms: shortest path, reachability, neighborhoods, and
|
|
13226
|
+
* degree centrality.
|
|
13227
|
+
*
|
|
13228
|
+
* Each call compiles to a single recursive-CTE query against the backend.
|
|
13229
|
+
* The facade is built lazily on first access and cached for the lifetime
|
|
13230
|
+
* of the store.
|
|
13231
|
+
*
|
|
13232
|
+
* @example
|
|
13233
|
+
* ```typescript
|
|
13234
|
+
* const path = await store.algorithms.shortestPath(alice, bob, {
|
|
13235
|
+
* edges: ["knows"],
|
|
13236
|
+
* });
|
|
13237
|
+
*
|
|
13238
|
+
* const friends = await store.algorithms.neighbors(alice, {
|
|
13239
|
+
* edges: ["knows"],
|
|
13240
|
+
* depth: 2,
|
|
13241
|
+
* });
|
|
13242
|
+
* ```
|
|
13243
|
+
*/
|
|
13244
|
+
get algorithms() {
|
|
13245
|
+
if (this.#algorithms === void 0) {
|
|
13246
|
+
this.#algorithms = createGraphAlgorithms({
|
|
13247
|
+
graphId: this.graphId,
|
|
13248
|
+
backend: this.#backend,
|
|
13249
|
+
schema: this.#schema,
|
|
13250
|
+
defaultTemporalMode: this.#graph.defaults.temporalMode
|
|
13251
|
+
});
|
|
13252
|
+
}
|
|
13253
|
+
return this.#algorithms;
|
|
13254
|
+
}
|
|
11773
13255
|
// === Dynamic Collection Access ===
|
|
11774
13256
|
/**
|
|
11775
13257
|
* Returns the node collection for the given kind, or undefined if the kind
|
|
@@ -11905,6 +13387,44 @@ var Store = class {
|
|
|
11905
13387
|
query() {
|
|
11906
13388
|
return this.#createQueryForBackend(this.#backend);
|
|
11907
13389
|
}
|
|
13390
|
+
// === Search ===
|
|
13391
|
+
/**
|
|
13392
|
+
* Search-related operations (fulltext, hybrid, and rebuild).
|
|
13393
|
+
*
|
|
13394
|
+
* All search methods live under this facade to keep the top-level
|
|
13395
|
+
* Store API focused on CRUD + graph traversal. The facade is
|
|
13396
|
+
* lazy-initialized and cached for the lifetime of the store.
|
|
13397
|
+
*
|
|
13398
|
+
* @example
|
|
13399
|
+
* ```typescript
|
|
13400
|
+
* // Fulltext
|
|
13401
|
+
* const hits = await store.search.fulltext("Document", {
|
|
13402
|
+
* query: "climate warming",
|
|
13403
|
+
* limit: 10,
|
|
13404
|
+
* includeSnippets: true,
|
|
13405
|
+
* });
|
|
13406
|
+
*
|
|
13407
|
+
* // Hybrid (vector + fulltext fused with RRF)
|
|
13408
|
+
* const ranked = await store.search.hybrid("Document", {
|
|
13409
|
+
* limit: 10,
|
|
13410
|
+
* vector: { fieldPath: "embedding", queryEmbedding: vec },
|
|
13411
|
+
* fulltext: { query: "climate warming" },
|
|
13412
|
+
* });
|
|
13413
|
+
*
|
|
13414
|
+
* // Rebuild index after schema change
|
|
13415
|
+
* const stats = await store.search.rebuildFulltext();
|
|
13416
|
+
* ```
|
|
13417
|
+
*/
|
|
13418
|
+
get search() {
|
|
13419
|
+
if (this.#search === void 0) {
|
|
13420
|
+
this.#search = new StoreSearch({
|
|
13421
|
+
graphId: this.graphId,
|
|
13422
|
+
backend: this.#backend,
|
|
13423
|
+
registry: this.#registry
|
|
13424
|
+
});
|
|
13425
|
+
}
|
|
13426
|
+
return this.#search;
|
|
13427
|
+
}
|
|
11908
13428
|
// === Batch Query Execution ===
|
|
11909
13429
|
/**
|
|
11910
13430
|
* Executes multiple queries over a single connection with snapshot consistency.
|
|
@@ -12048,6 +13568,8 @@ var Store = class {
|
|
|
12048
13568
|
await (this.#backend.capabilities.transactions ? this.#backend.transaction(async (tx) => doClear(tx)) : doClear(this.#backend));
|
|
12049
13569
|
this.#nodeCollections = void 0;
|
|
12050
13570
|
this.#edgeCollections = void 0;
|
|
13571
|
+
this.#algorithms = void 0;
|
|
13572
|
+
this.#search = void 0;
|
|
12051
13573
|
}
|
|
12052
13574
|
// === Lifecycle ===
|
|
12053
13575
|
/**
|
|
@@ -12154,6 +13676,6 @@ async function createStoreWithSchema(graph, backend, options) {
|
|
|
12154
13676
|
return [store, result];
|
|
12155
13677
|
}
|
|
12156
13678
|
|
|
12157
|
-
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 };
|
|
13679
|
+
export { DEFAULT_SQL_SCHEMA, ExecutableAggregateQuery, ExecutableQuery, MAX_EXPLICIT_RECURSIVE_DEPTH, MAX_RECURSIVE_DEPTH, PreparedQuery, QueryBuilder, StoreSearch, 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 };
|
|
12158
13680
|
//# sourceMappingURL=index.js.map
|
|
12159
13681
|
//# sourceMappingURL=index.js.map
|