@_linked/core 1.0.0 → 1.2.0-next.20260302120536

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.
Files changed (187) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +321 -43
  3. package/lib/cjs/index.js +4 -0
  4. package/lib/cjs/index.js.map +1 -1
  5. package/lib/cjs/interfaces/IQuadStore.d.ts +19 -7
  6. package/lib/cjs/queries/CreateQuery.d.ts +7 -8
  7. package/lib/cjs/queries/CreateQuery.js +4 -4
  8. package/lib/cjs/queries/CreateQuery.js.map +1 -1
  9. package/lib/cjs/queries/DeleteQuery.d.ts +7 -8
  10. package/lib/cjs/queries/DeleteQuery.js +4 -4
  11. package/lib/cjs/queries/DeleteQuery.js.map +1 -1
  12. package/lib/cjs/queries/IRAliasScope.d.ts +20 -0
  13. package/lib/cjs/queries/IRAliasScope.js +52 -0
  14. package/lib/cjs/queries/IRAliasScope.js.map +1 -0
  15. package/lib/cjs/queries/IRCanonicalize.d.ts +36 -0
  16. package/lib/cjs/queries/IRCanonicalize.js +121 -0
  17. package/lib/cjs/queries/IRCanonicalize.js.map +1 -0
  18. package/lib/cjs/queries/IRDesugar.d.ts +98 -0
  19. package/lib/cjs/queries/IRDesugar.js +244 -0
  20. package/lib/cjs/queries/IRDesugar.js.map +1 -0
  21. package/lib/cjs/queries/IRLower.d.ts +8 -0
  22. package/lib/cjs/queries/IRLower.js +272 -0
  23. package/lib/cjs/queries/IRLower.js.map +1 -0
  24. package/lib/cjs/queries/IRMutation.d.ts +23 -0
  25. package/lib/cjs/queries/IRMutation.js +77 -0
  26. package/lib/cjs/queries/IRMutation.js.map +1 -0
  27. package/lib/cjs/queries/IRPipeline.d.ts +8 -0
  28. package/lib/cjs/queries/IRPipeline.js +25 -0
  29. package/lib/cjs/queries/IRPipeline.js.map +1 -0
  30. package/lib/cjs/queries/IRProjection.d.ts +38 -0
  31. package/lib/cjs/queries/IRProjection.js +98 -0
  32. package/lib/cjs/queries/IRProjection.js.map +1 -0
  33. package/lib/cjs/queries/IntermediateRepresentation.d.ts +210 -0
  34. package/lib/cjs/queries/IntermediateRepresentation.js +3 -0
  35. package/lib/cjs/queries/IntermediateRepresentation.js.map +1 -0
  36. package/lib/cjs/queries/MutationQuery.js +9 -23
  37. package/lib/cjs/queries/MutationQuery.js.map +1 -1
  38. package/lib/cjs/queries/QueryFactory.d.ts +0 -2
  39. package/lib/cjs/queries/QueryFactory.js +0 -3
  40. package/lib/cjs/queries/QueryFactory.js.map +1 -1
  41. package/lib/cjs/queries/QueryParser.d.ts +6 -1
  42. package/lib/cjs/queries/QueryParser.js +14 -22
  43. package/lib/cjs/queries/QueryParser.js.map +1 -1
  44. package/lib/cjs/queries/SelectQuery.d.ts +18 -27
  45. package/lib/cjs/queries/SelectQuery.js +54 -45
  46. package/lib/cjs/queries/SelectQuery.js.map +1 -1
  47. package/lib/cjs/queries/UpdateQuery.d.ts +8 -9
  48. package/lib/cjs/queries/UpdateQuery.js +4 -4
  49. package/lib/cjs/queries/UpdateQuery.js.map +1 -1
  50. package/lib/cjs/shapes/SHACL.d.ts +1 -0
  51. package/lib/cjs/shapes/SHACL.js +82 -2
  52. package/lib/cjs/shapes/SHACL.js.map +1 -1
  53. package/lib/cjs/shapes/Shape.d.ts +11 -10
  54. package/lib/cjs/shapes/Shape.js +11 -5
  55. package/lib/cjs/shapes/Shape.js.map +1 -1
  56. package/lib/cjs/sparql/SparqlAlgebra.d.ts +158 -0
  57. package/lib/cjs/sparql/SparqlAlgebra.js +4 -0
  58. package/lib/cjs/sparql/SparqlAlgebra.js.map +1 -0
  59. package/lib/cjs/sparql/SparqlStore.d.ts +52 -0
  60. package/lib/cjs/sparql/SparqlStore.js +81 -0
  61. package/lib/cjs/sparql/SparqlStore.js.map +1 -0
  62. package/lib/cjs/sparql/algebraToString.d.ts +13 -0
  63. package/lib/cjs/sparql/algebraToString.js +298 -0
  64. package/lib/cjs/sparql/algebraToString.js.map +1 -0
  65. package/lib/cjs/sparql/index.d.ts +9 -0
  66. package/lib/cjs/sparql/index.js +40 -0
  67. package/lib/cjs/sparql/index.js.map +1 -0
  68. package/lib/cjs/sparql/irToAlgebra.d.ts +39 -0
  69. package/lib/cjs/sparql/irToAlgebra.js +927 -0
  70. package/lib/cjs/sparql/irToAlgebra.js.map +1 -0
  71. package/lib/cjs/sparql/resultMapping.d.ts +36 -0
  72. package/lib/cjs/sparql/resultMapping.js +501 -0
  73. package/lib/cjs/sparql/resultMapping.js.map +1 -0
  74. package/lib/cjs/sparql/sparqlUtils.d.ts +32 -0
  75. package/lib/cjs/sparql/sparqlUtils.js +89 -0
  76. package/lib/cjs/sparql/sparqlUtils.js.map +1 -0
  77. package/lib/cjs/test-helpers/FusekiStore.d.ts +29 -0
  78. package/lib/cjs/test-helpers/FusekiStore.js +82 -0
  79. package/lib/cjs/test-helpers/FusekiStore.js.map +1 -0
  80. package/lib/cjs/test-helpers/fuseki-test-store.d.ts +43 -0
  81. package/lib/cjs/test-helpers/fuseki-test-store.js +144 -0
  82. package/lib/cjs/test-helpers/fuseki-test-store.js.map +1 -0
  83. package/lib/cjs/test-helpers/query-capture-store.d.ts +5 -0
  84. package/lib/cjs/test-helpers/query-capture-store.js +59 -0
  85. package/lib/cjs/test-helpers/query-capture-store.js.map +1 -0
  86. package/lib/cjs/test-helpers/query-fixtures.d.ts +700 -117
  87. package/lib/cjs/test-helpers/query-fixtures.js +39 -1
  88. package/lib/cjs/test-helpers/query-fixtures.js.map +1 -1
  89. package/lib/cjs/utils/LinkedStorage.d.ts +7 -7
  90. package/lib/cjs/utils/LinkedStorage.js +4 -3
  91. package/lib/cjs/utils/LinkedStorage.js.map +1 -1
  92. package/lib/esm/index.js +4 -0
  93. package/lib/esm/index.js.map +1 -1
  94. package/lib/esm/interfaces/IQuadStore.d.ts +19 -7
  95. package/lib/esm/queries/CreateQuery.d.ts +7 -8
  96. package/lib/esm/queries/CreateQuery.js +4 -4
  97. package/lib/esm/queries/CreateQuery.js.map +1 -1
  98. package/lib/esm/queries/DeleteQuery.d.ts +7 -8
  99. package/lib/esm/queries/DeleteQuery.js +4 -4
  100. package/lib/esm/queries/DeleteQuery.js.map +1 -1
  101. package/lib/esm/queries/IRAliasScope.d.ts +20 -0
  102. package/lib/esm/queries/IRAliasScope.js +47 -0
  103. package/lib/esm/queries/IRAliasScope.js.map +1 -0
  104. package/lib/esm/queries/IRCanonicalize.d.ts +36 -0
  105. package/lib/esm/queries/IRCanonicalize.js +116 -0
  106. package/lib/esm/queries/IRCanonicalize.js.map +1 -0
  107. package/lib/esm/queries/IRDesugar.d.ts +98 -0
  108. package/lib/esm/queries/IRDesugar.js +240 -0
  109. package/lib/esm/queries/IRDesugar.js.map +1 -0
  110. package/lib/esm/queries/IRLower.d.ts +8 -0
  111. package/lib/esm/queries/IRLower.js +268 -0
  112. package/lib/esm/queries/IRLower.js.map +1 -0
  113. package/lib/esm/queries/IRMutation.d.ts +23 -0
  114. package/lib/esm/queries/IRMutation.js +71 -0
  115. package/lib/esm/queries/IRMutation.js.map +1 -0
  116. package/lib/esm/queries/IRPipeline.d.ts +8 -0
  117. package/lib/esm/queries/IRPipeline.js +21 -0
  118. package/lib/esm/queries/IRPipeline.js.map +1 -0
  119. package/lib/esm/queries/IRProjection.d.ts +38 -0
  120. package/lib/esm/queries/IRProjection.js +92 -0
  121. package/lib/esm/queries/IRProjection.js.map +1 -0
  122. package/lib/esm/queries/IntermediateRepresentation.d.ts +210 -0
  123. package/lib/esm/queries/IntermediateRepresentation.js +2 -0
  124. package/lib/esm/queries/IntermediateRepresentation.js.map +1 -0
  125. package/lib/esm/queries/MutationQuery.js +9 -23
  126. package/lib/esm/queries/MutationQuery.js.map +1 -1
  127. package/lib/esm/queries/QueryFactory.d.ts +0 -2
  128. package/lib/esm/queries/QueryFactory.js +0 -3
  129. package/lib/esm/queries/QueryFactory.js.map +1 -1
  130. package/lib/esm/queries/QueryParser.d.ts +6 -1
  131. package/lib/esm/queries/QueryParser.js +14 -23
  132. package/lib/esm/queries/QueryParser.js.map +1 -1
  133. package/lib/esm/queries/SelectQuery.d.ts +18 -27
  134. package/lib/esm/queries/SelectQuery.js +54 -45
  135. package/lib/esm/queries/SelectQuery.js.map +1 -1
  136. package/lib/esm/queries/UpdateQuery.d.ts +8 -9
  137. package/lib/esm/queries/UpdateQuery.js +4 -4
  138. package/lib/esm/queries/UpdateQuery.js.map +1 -1
  139. package/lib/esm/shapes/SHACL.d.ts +1 -0
  140. package/lib/esm/shapes/SHACL.js +82 -2
  141. package/lib/esm/shapes/SHACL.js.map +1 -1
  142. package/lib/esm/shapes/Shape.d.ts +11 -10
  143. package/lib/esm/shapes/Shape.js +11 -5
  144. package/lib/esm/shapes/Shape.js.map +1 -1
  145. package/lib/esm/sparql/SparqlAlgebra.d.ts +158 -0
  146. package/lib/esm/sparql/SparqlAlgebra.js +3 -0
  147. package/lib/esm/sparql/SparqlAlgebra.js.map +1 -0
  148. package/lib/esm/sparql/SparqlStore.d.ts +52 -0
  149. package/lib/esm/sparql/SparqlStore.js +77 -0
  150. package/lib/esm/sparql/SparqlStore.js.map +1 -0
  151. package/lib/esm/sparql/algebraToString.d.ts +13 -0
  152. package/lib/esm/sparql/algebraToString.js +289 -0
  153. package/lib/esm/sparql/algebraToString.js.map +1 -0
  154. package/lib/esm/sparql/index.d.ts +9 -0
  155. package/lib/esm/sparql/index.js +13 -0
  156. package/lib/esm/sparql/index.js.map +1 -0
  157. package/lib/esm/sparql/irToAlgebra.d.ts +39 -0
  158. package/lib/esm/sparql/irToAlgebra.js +917 -0
  159. package/lib/esm/sparql/irToAlgebra.js.map +1 -0
  160. package/lib/esm/sparql/resultMapping.d.ts +36 -0
  161. package/lib/esm/sparql/resultMapping.js +496 -0
  162. package/lib/esm/sparql/resultMapping.js.map +1 -0
  163. package/lib/esm/sparql/sparqlUtils.d.ts +32 -0
  164. package/lib/esm/sparql/sparqlUtils.js +82 -0
  165. package/lib/esm/sparql/sparqlUtils.js.map +1 -0
  166. package/lib/esm/test-helpers/FusekiStore.d.ts +29 -0
  167. package/lib/esm/test-helpers/FusekiStore.js +78 -0
  168. package/lib/esm/test-helpers/FusekiStore.js.map +1 -0
  169. package/lib/esm/test-helpers/fuseki-test-store.d.ts +43 -0
  170. package/lib/esm/test-helpers/fuseki-test-store.js +135 -0
  171. package/lib/esm/test-helpers/fuseki-test-store.js.map +1 -0
  172. package/lib/esm/test-helpers/query-capture-store.d.ts +5 -0
  173. package/lib/esm/test-helpers/query-capture-store.js +55 -0
  174. package/lib/esm/test-helpers/query-capture-store.js.map +1 -0
  175. package/lib/esm/test-helpers/query-fixtures.d.ts +700 -117
  176. package/lib/esm/test-helpers/query-fixtures.js +38 -0
  177. package/lib/esm/test-helpers/query-fixtures.js.map +1 -1
  178. package/lib/esm/utils/LinkedStorage.d.ts +7 -7
  179. package/lib/esm/utils/LinkedStorage.js +4 -3
  180. package/lib/esm/utils/LinkedStorage.js.map +1 -1
  181. package/package.json +7 -3
  182. package/lib/cjs/interfaces/IQueryParser.d.ts +0 -13
  183. package/lib/cjs/interfaces/IQueryParser.js +0 -10
  184. package/lib/cjs/interfaces/IQueryParser.js.map +0 -1
  185. package/lib/esm/interfaces/IQueryParser.d.ts +0 -13
  186. package/lib/esm/interfaces/IQueryParser.js +0 -7
  187. package/lib/esm/interfaces/IQueryParser.js.map +0 -1
@@ -0,0 +1,917 @@
1
+ import { generateEntityUri } from './sparqlUtils.js';
2
+ import { selectPlanToSparql, insertDataPlanToSparql, deleteInsertPlanToSparql, } from './algebraToString.js';
3
+ import { rdf } from '../ontologies/rdf.js';
4
+ import { xsd } from '../ontologies/xsd.js';
5
+ // ---------------------------------------------------------------------------
6
+ // Constants
7
+ // ---------------------------------------------------------------------------
8
+ const RDF_TYPE = rdf.type.id;
9
+ const XSD_DATETIME = xsd.dateTime.id;
10
+ const XSD_BOOLEAN = xsd.boolean.id;
11
+ const XSD_INTEGER = xsd.integer.id;
12
+ const XSD_DOUBLE = xsd.double.id;
13
+ // ---------------------------------------------------------------------------
14
+ // Helpers
15
+ // ---------------------------------------------------------------------------
16
+ function iriTerm(value) {
17
+ return { kind: 'iri', value };
18
+ }
19
+ function varTerm(name) {
20
+ return { kind: 'variable', name };
21
+ }
22
+ function literalTerm(value, datatype) {
23
+ if (datatype) {
24
+ return { kind: 'literal', value, datatype };
25
+ }
26
+ return { kind: 'literal', value };
27
+ }
28
+ function tripleOf(subject, predicate, object) {
29
+ return { subject, predicate, object };
30
+ }
31
+ /** Produce variable name suffix from the last segment of a property URI. */
32
+ function propertySuffix(propertyUri) {
33
+ const hashIdx = propertyUri.lastIndexOf('#');
34
+ if (hashIdx >= 0)
35
+ return propertyUri.substring(hashIdx + 1);
36
+ const slashIdx = propertyUri.lastIndexOf('/');
37
+ return slashIdx >= 0 ? propertyUri.substring(slashIdx + 1) : propertyUri;
38
+ }
39
+ /**
40
+ * Sanitize a string so it's valid in a SPARQL variable name.
41
+ * Replaces any non-alphanumeric/underscore characters with underscores.
42
+ */
43
+ function sanitizeVarName(name) {
44
+ return name.replace(/[^A-Za-z0-9_]/g, '_');
45
+ }
46
+ /**
47
+ * Wrap a single node in a LeftJoin, making `right` optional relative to `left`.
48
+ */
49
+ function wrapOptional(left, right) {
50
+ return { type: 'left_join', left, right };
51
+ }
52
+ /**
53
+ * Join two algebra nodes. If left is null, returns right.
54
+ */
55
+ function joinNodes(left, right) {
56
+ if (!left)
57
+ return right;
58
+ return { type: 'join', left, right };
59
+ }
60
+ // ---------------------------------------------------------------------------
61
+ // Pattern helpers
62
+ // ---------------------------------------------------------------------------
63
+ /**
64
+ * Recursively collects all traversal alias target variables from IR patterns.
65
+ * Used to ensure traversal aliases appear in the SELECT projection for result grouping.
66
+ */
67
+ function collectTraversalAliases(patterns) {
68
+ const aliases = [];
69
+ for (const p of patterns) {
70
+ if (p.kind === 'traverse') {
71
+ aliases.push(p.to);
72
+ }
73
+ else if (p.kind === 'join') {
74
+ aliases.push(...collectTraversalAliases(p.patterns));
75
+ }
76
+ else if (p.kind === 'optional') {
77
+ aliases.push(...collectTraversalAliases([p.pattern]));
78
+ }
79
+ else if (p.kind === 'union') {
80
+ for (const branch of p.branches) {
81
+ aliases.push(...collectTraversalAliases([branch]));
82
+ }
83
+ }
84
+ }
85
+ return aliases;
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // Variable Registry
89
+ // ---------------------------------------------------------------------------
90
+ /**
91
+ * Maps (alias, property) → SPARQL variable name.
92
+ * Used to deduplicate variables across traverse and property_expr nodes.
93
+ */
94
+ class VariableRegistry {
95
+ constructor() {
96
+ this.map = new Map();
97
+ this.usedVarNames = new Set();
98
+ }
99
+ key(alias, property) {
100
+ return `${alias}::${property}`;
101
+ }
102
+ has(alias, property) {
103
+ return this.map.has(this.key(alias, property));
104
+ }
105
+ get(alias, property) {
106
+ return this.map.get(this.key(alias, property));
107
+ }
108
+ set(alias, property, variable) {
109
+ this.map.set(this.key(alias, property), variable);
110
+ this.usedVarNames.add(variable);
111
+ }
112
+ getOrCreate(alias, property) {
113
+ const existing = this.get(alias, property);
114
+ if (existing)
115
+ return existing;
116
+ const suffix = propertySuffix(property);
117
+ let varName = `${sanitizeVarName(alias)}_${suffix}`;
118
+ // Deduplicate: if varName is already used by a different (alias, property),
119
+ // append a counter to ensure unique SPARQL variable names
120
+ let counter = 2;
121
+ while (this.usedVarNames.has(varName)) {
122
+ varName = `${sanitizeVarName(alias)}_${suffix}_${counter}`;
123
+ counter++;
124
+ }
125
+ this.set(alias, property, varName);
126
+ return varName;
127
+ }
128
+ }
129
+ // ---------------------------------------------------------------------------
130
+ // Aggregate detection
131
+ // ---------------------------------------------------------------------------
132
+ /**
133
+ * Checks whether a SparqlExpression tree contains an aggregate sub-expression.
134
+ * Used to route aggregate-containing filters to HAVING instead of FILTER.
135
+ */
136
+ function containsAggregate(expr) {
137
+ switch (expr.kind) {
138
+ case 'aggregate_expr':
139
+ return true;
140
+ case 'binary_expr':
141
+ return containsAggregate(expr.left) || containsAggregate(expr.right);
142
+ case 'logical_expr':
143
+ return expr.exprs.some(containsAggregate);
144
+ case 'not_expr':
145
+ return containsAggregate(expr.inner);
146
+ case 'function_expr':
147
+ return expr.args.some(containsAggregate);
148
+ default:
149
+ return false;
150
+ }
151
+ }
152
+ // ---------------------------------------------------------------------------
153
+ // Select conversion
154
+ // ---------------------------------------------------------------------------
155
+ /**
156
+ * Converts an IRSelectQuery to a SparqlSelectPlan.
157
+ */
158
+ export function selectToAlgebra(query, _options) {
159
+ const registry = new VariableRegistry();
160
+ // Track property triples that need to be added as OPTIONAL
161
+ const optionalPropertyTriples = [];
162
+ // Track filtered traversals (inline where) — these get their own OPTIONAL blocks
163
+ const filteredTraverseBlocks = [];
164
+ // 1. Root shape scan → BGP with type triple
165
+ const rootAlias = query.root.alias;
166
+ const shapeUri = query.root.shape;
167
+ const typeTriple = tripleOf(varTerm(rootAlias), iriTerm(RDF_TYPE), iriTerm(shapeUri));
168
+ const requiredTriples = [typeTriple];
169
+ // Track traverse triples (required pattern)
170
+ const traverseTriples = [];
171
+ // 2. Process patterns → traverse triples, populate variable registry
172
+ for (const pattern of query.patterns) {
173
+ processPattern(pattern, registry, traverseTriples, optionalPropertyTriples, filteredTraverseBlocks);
174
+ }
175
+ // 3. Process projection expressions, where clause, orderBy expressions
176
+ // to discover any additional property_expr references
177
+ for (const item of query.projection) {
178
+ processExpressionForProperties(item.expression, registry, optionalPropertyTriples);
179
+ }
180
+ if (query.where) {
181
+ processExpressionForProperties(query.where, registry, optionalPropertyTriples);
182
+ }
183
+ if (query.orderBy) {
184
+ for (const orderItem of query.orderBy) {
185
+ processExpressionForProperties(orderItem.expression, registry, optionalPropertyTriples);
186
+ }
187
+ }
188
+ // 4. Build the algebra tree
189
+ // - Start with the required BGP (type triple + traverse triples)
190
+ // - Wrap each optional property triple in a LeftJoin
191
+ const requiredBgp = {
192
+ type: 'bgp',
193
+ triples: [...requiredTriples, ...traverseTriples],
194
+ };
195
+ let algebra = requiredBgp;
196
+ // 4b. Build filtered OPTIONAL blocks for inline where traversals.
197
+ // Each block contains: traverse triple + filter property triples + FILTER.
198
+ // Property triples referenced by the filter are co-located inside the OPTIONAL
199
+ // so that the filter can reference them.
200
+ for (const block of filteredTraverseBlocks) {
201
+ const filterPropertyTriples = [];
202
+ processExpressionForProperties(block.filter, registry, filterPropertyTriples);
203
+ const filterExpr = convertExpression(block.filter, registry, filterPropertyTriples);
204
+ const blockTriples = [block.traverseTriple, ...filterPropertyTriples];
205
+ const blockBgp = { type: 'bgp', triples: blockTriples };
206
+ const filteredBlock = { type: 'filter', expression: filterExpr, inner: blockBgp };
207
+ algebra = wrapOptional(algebra, filteredBlock);
208
+ }
209
+ // Wrap each optional property triple in its own OPTIONAL (LeftJoin)
210
+ for (const propTriple of optionalPropertyTriples) {
211
+ algebra = wrapOptional(algebra, {
212
+ type: 'bgp',
213
+ triples: [propTriple],
214
+ });
215
+ }
216
+ // 5. Where clause → Filter wrapping (or HAVING if aggregate-containing)
217
+ let havingExpr;
218
+ if (query.where) {
219
+ const filterExpr = convertExpression(query.where, registry, optionalPropertyTriples);
220
+ if (containsAggregate(filterExpr)) {
221
+ havingExpr = filterExpr;
222
+ }
223
+ else {
224
+ algebra = {
225
+ type: 'filter',
226
+ expression: filterExpr,
227
+ inner: algebra,
228
+ };
229
+ }
230
+ }
231
+ // 6. SubjectId → Filter
232
+ if (query.subjectId) {
233
+ const subjectFilter = {
234
+ kind: 'binary_expr',
235
+ op: '=',
236
+ left: { kind: 'variable_expr', name: rootAlias },
237
+ right: { kind: 'iri_expr', value: query.subjectId },
238
+ };
239
+ algebra = {
240
+ type: 'filter',
241
+ expression: subjectFilter,
242
+ inner: algebra,
243
+ };
244
+ }
245
+ // 7. Build projection
246
+ const projection = [];
247
+ const aggregates = [];
248
+ let hasAggregates = false;
249
+ // Always include root alias as first projection variable
250
+ projection.push({ kind: 'variable', name: rootAlias });
251
+ // Collect traversal aliases upfront to detect aggregate alias collisions
252
+ const traversalAliasSet = new Set(collectTraversalAliases(query.patterns));
253
+ // Track traversal aliases consumed by aggregate renames (should not be
254
+ // re-projected as plain variables, which would alter GROUP BY semantics)
255
+ const aggregateRenamedAliases = new Set();
256
+ for (const item of query.projection) {
257
+ const sparqlExpr = convertExpression(item.expression, registry, optionalPropertyTriples);
258
+ if (sparqlExpr.kind === 'aggregate_expr') {
259
+ hasAggregates = true;
260
+ // Avoid collision: if aggregate alias matches a traversal alias,
261
+ // rename it so SPARQL doesn't produce duplicate variable bindings
262
+ let aggAlias = item.alias;
263
+ if (traversalAliasSet.has(aggAlias)) {
264
+ aggregateRenamedAliases.add(aggAlias);
265
+ aggAlias = `${aggAlias}_agg`;
266
+ // Update resultMap so result mapping uses the renamed alias
267
+ for (const rm of query.resultMap) {
268
+ if (rm.alias === item.alias)
269
+ rm.alias = aggAlias;
270
+ }
271
+ }
272
+ projection.push({
273
+ kind: 'aggregate',
274
+ expression: sparqlExpr,
275
+ alias: aggAlias,
276
+ });
277
+ aggregates.push({
278
+ variable: aggAlias,
279
+ aggregate: sparqlExpr,
280
+ });
281
+ }
282
+ else {
283
+ // For property_expr, the variable is the resolved name from registry
284
+ const varName = resolveExpressionVariable(item.expression, registry);
285
+ if (varName && varName !== rootAlias) {
286
+ projection.push({ kind: 'variable', name: varName });
287
+ }
288
+ else if (!varName) {
289
+ // Non-variable expression (binary_expr, function_expr, etc.)
290
+ // → project as (expr AS ?alias)
291
+ projection.push({ kind: 'expression', expression: sparqlExpr, alias: item.alias });
292
+ }
293
+ }
294
+ }
295
+ // 7b. Include traversal aliases needed for result grouping
296
+ // When nested results are projected (e.g. p.friends.name), the result
297
+ // mapping needs the traversal alias variable (?a1) in the bindings to
298
+ // group nested rows by entity. Without this, mapNestedRows() can't
299
+ // identify which nested fields belong to which traversed entity.
300
+ const projectedNames = new Set();
301
+ for (const p of projection) {
302
+ if (p.kind === 'variable')
303
+ projectedNames.add(p.name);
304
+ else if (p.kind === 'aggregate' || p.kind === 'expression')
305
+ projectedNames.add(p.alias);
306
+ }
307
+ for (const alias of collectTraversalAliases(query.patterns)) {
308
+ if (!projectedNames.has(alias) && !aggregateRenamedAliases.has(alias)) {
309
+ projection.push({ kind: 'variable', name: alias });
310
+ projectedNames.add(alias);
311
+ }
312
+ }
313
+ // 8. GROUP BY inference
314
+ let groupBy;
315
+ if (havingExpr) {
316
+ hasAggregates = true;
317
+ }
318
+ if (hasAggregates) {
319
+ // All non-aggregate projected variables become GROUP BY targets
320
+ groupBy = projection
321
+ .filter((p) => p.kind === 'variable')
322
+ .map((p) => p.name);
323
+ }
324
+ // 9. OrderBy
325
+ let orderBy;
326
+ if (query.orderBy) {
327
+ orderBy = query.orderBy.map((item) => ({
328
+ expression: convertExpression(item.expression, registry, optionalPropertyTriples),
329
+ direction: item.direction,
330
+ }));
331
+ }
332
+ return {
333
+ type: 'select',
334
+ algebra,
335
+ projection,
336
+ distinct: !hasAggregates ? true : undefined,
337
+ orderBy,
338
+ limit: query.limit,
339
+ offset: query.offset,
340
+ groupBy,
341
+ having: havingExpr,
342
+ aggregates: aggregates.length > 0 ? aggregates : undefined,
343
+ };
344
+ }
345
+ // ---------------------------------------------------------------------------
346
+ // Pattern processing
347
+ // ---------------------------------------------------------------------------
348
+ function processPattern(pattern, registry, traverseTriples, optionalPropertyTriples, filteredTraverseBlocks) {
349
+ switch (pattern.kind) {
350
+ case 'shape_scan':
351
+ // Additional shape scans (non-root) are handled as type triples
352
+ // but this case is rare — root is handled separately
353
+ break;
354
+ case 'traverse': {
355
+ // Register the traverse variable: (from, property) → to
356
+ registry.set(pattern.from, pattern.property, pattern.to);
357
+ // Add traverse triple to required pattern (or filtered block if inline where)
358
+ const triple = tripleOf(varTerm(pattern.from), iriTerm(pattern.property), varTerm(pattern.to));
359
+ if (pattern.filter && filteredTraverseBlocks) {
360
+ filteredTraverseBlocks.push({
361
+ traverseTriple: triple,
362
+ filter: pattern.filter,
363
+ toAlias: pattern.to,
364
+ });
365
+ }
366
+ else {
367
+ traverseTriples.push(triple);
368
+ }
369
+ break;
370
+ }
371
+ case 'join': {
372
+ for (const sub of pattern.patterns) {
373
+ processPattern(sub, registry, traverseTriples, optionalPropertyTriples, filteredTraverseBlocks);
374
+ }
375
+ break;
376
+ }
377
+ case 'optional': {
378
+ // Optional patterns — process inner patterns but keep them optional
379
+ processPattern(pattern.pattern, registry, traverseTriples, optionalPropertyTriples, filteredTraverseBlocks);
380
+ break;
381
+ }
382
+ case 'union': {
383
+ for (const branch of pattern.branches) {
384
+ processPattern(branch, registry, traverseTriples, optionalPropertyTriples, filteredTraverseBlocks);
385
+ }
386
+ break;
387
+ }
388
+ case 'exists': {
389
+ processPattern(pattern.pattern, registry, traverseTriples, optionalPropertyTriples, filteredTraverseBlocks);
390
+ break;
391
+ }
392
+ }
393
+ }
394
+ // ---------------------------------------------------------------------------
395
+ // Expression processing — discover property_expr references
396
+ // ---------------------------------------------------------------------------
397
+ function processExpressionForProperties(expr, registry, optionalPropertyTriples) {
398
+ switch (expr.kind) {
399
+ case 'property_expr': {
400
+ if (!registry.has(expr.sourceAlias, expr.property)) {
401
+ // Create a new OPTIONAL triple for this property
402
+ const varName = registry.getOrCreate(expr.sourceAlias, expr.property);
403
+ optionalPropertyTriples.push(tripleOf(varTerm(expr.sourceAlias), iriTerm(expr.property), varTerm(varName)));
404
+ }
405
+ break;
406
+ }
407
+ case 'binary_expr':
408
+ processExpressionForProperties(expr.left, registry, optionalPropertyTriples);
409
+ processExpressionForProperties(expr.right, registry, optionalPropertyTriples);
410
+ break;
411
+ case 'logical_expr':
412
+ for (const sub of expr.expressions) {
413
+ processExpressionForProperties(sub, registry, optionalPropertyTriples);
414
+ }
415
+ break;
416
+ case 'not_expr':
417
+ processExpressionForProperties(expr.expression, registry, optionalPropertyTriples);
418
+ break;
419
+ case 'function_expr':
420
+ for (const arg of expr.args) {
421
+ processExpressionForProperties(arg, registry, optionalPropertyTriples);
422
+ }
423
+ break;
424
+ case 'aggregate_expr':
425
+ for (const arg of expr.args) {
426
+ processExpressionForProperties(arg, registry, optionalPropertyTriples);
427
+ }
428
+ break;
429
+ case 'exists_expr':
430
+ // exists_expr in IR has pattern + filter
431
+ // Process the filter for property references
432
+ if (expr.filter) {
433
+ processExpressionForProperties(expr.filter, registry, optionalPropertyTriples);
434
+ }
435
+ break;
436
+ case 'context_property_expr': {
437
+ // Context entity property — emit a triple with fixed IRI as subject.
438
+ // Use raw IRI as registry key to avoid collision between IRIs that
439
+ // sanitize to the same string (e.g. ctx-1 vs ctx_1).
440
+ const ctxKey = `__ctx__${expr.contextIri}`;
441
+ if (!registry.has(ctxKey, expr.property)) {
442
+ const varName = registry.getOrCreate(ctxKey, expr.property);
443
+ optionalPropertyTriples.push(tripleOf(iriTerm(expr.contextIri), iriTerm(expr.property), varTerm(varName)));
444
+ }
445
+ break;
446
+ }
447
+ case 'literal_expr':
448
+ case 'reference_expr':
449
+ case 'alias_expr':
450
+ // No property references to discover
451
+ break;
452
+ }
453
+ }
454
+ // ---------------------------------------------------------------------------
455
+ // Expression conversion
456
+ // ---------------------------------------------------------------------------
457
+ function convertExpression(expr, registry, optionalPropertyTriples) {
458
+ switch (expr.kind) {
459
+ case 'literal_expr': {
460
+ const value = expr.value;
461
+ if (value === null || value === undefined) {
462
+ return { kind: 'literal_expr', value: '' };
463
+ }
464
+ if (typeof value === 'boolean') {
465
+ return {
466
+ kind: 'literal_expr',
467
+ value: String(value),
468
+ datatype: XSD_BOOLEAN,
469
+ };
470
+ }
471
+ if (typeof value === 'number') {
472
+ if (Number.isInteger(value)) {
473
+ return {
474
+ kind: 'literal_expr',
475
+ value: String(value),
476
+ datatype: XSD_INTEGER,
477
+ };
478
+ }
479
+ return {
480
+ kind: 'literal_expr',
481
+ value: String(value),
482
+ datatype: XSD_DOUBLE,
483
+ };
484
+ }
485
+ return { kind: 'literal_expr', value: String(value) };
486
+ }
487
+ case 'reference_expr':
488
+ return { kind: 'iri_expr', value: expr.value };
489
+ case 'alias_expr':
490
+ return { kind: 'variable_expr', name: expr.alias };
491
+ case 'context_property_expr': {
492
+ const ctxKey = `__ctx__${expr.contextIri}`;
493
+ const ctxVarName = registry.getOrCreate(ctxKey, expr.property);
494
+ return { kind: 'variable_expr', name: ctxVarName };
495
+ }
496
+ case 'property_expr': {
497
+ const varName = registry.getOrCreate(expr.sourceAlias, expr.property);
498
+ return { kind: 'variable_expr', name: varName };
499
+ }
500
+ case 'binary_expr':
501
+ return {
502
+ kind: 'binary_expr',
503
+ op: expr.operator,
504
+ left: convertExpression(expr.left, registry, optionalPropertyTriples),
505
+ right: convertExpression(expr.right, registry, optionalPropertyTriples),
506
+ };
507
+ case 'logical_expr':
508
+ return {
509
+ kind: 'logical_expr',
510
+ op: expr.operator,
511
+ exprs: expr.expressions.map((e) => convertExpression(e, registry, optionalPropertyTriples)),
512
+ };
513
+ case 'not_expr':
514
+ return {
515
+ kind: 'not_expr',
516
+ inner: convertExpression(expr.expression, registry, optionalPropertyTriples),
517
+ };
518
+ case 'function_expr':
519
+ return {
520
+ kind: 'function_expr',
521
+ name: expr.name,
522
+ args: expr.args.map((a) => convertExpression(a, registry, optionalPropertyTriples)),
523
+ };
524
+ case 'aggregate_expr':
525
+ return {
526
+ kind: 'aggregate_expr',
527
+ name: expr.name,
528
+ args: expr.args.map((a) => convertExpression(a, registry, optionalPropertyTriples)),
529
+ };
530
+ case 'exists_expr': {
531
+ // Convert exists expression with inner pattern + filter
532
+ const innerAlgebra = convertExistsPattern(expr.pattern, registry);
533
+ if (expr.filter) {
534
+ const filterExpr = convertExpression(expr.filter, registry, optionalPropertyTriples);
535
+ // Wrap the inner pattern with a filter
536
+ const filteredInner = {
537
+ type: 'filter',
538
+ expression: filterExpr,
539
+ inner: innerAlgebra,
540
+ };
541
+ return {
542
+ kind: 'exists_expr',
543
+ pattern: filteredInner,
544
+ negated: false,
545
+ };
546
+ }
547
+ return {
548
+ kind: 'exists_expr',
549
+ pattern: innerAlgebra,
550
+ negated: false,
551
+ };
552
+ }
553
+ default:
554
+ throw new Error(`Unknown IR expression kind: ${expr.kind}`);
555
+ }
556
+ }
557
+ /**
558
+ * Convert an exists pattern (from exists_expr) into an algebra node.
559
+ * Recursively handles all IR graph pattern kinds.
560
+ */
561
+ function convertExistsPattern(pattern, registry) {
562
+ switch (pattern.kind) {
563
+ case 'traverse': {
564
+ const triple = tripleOf(varTerm(pattern.from), iriTerm(pattern.property), varTerm(pattern.to));
565
+ return { type: 'bgp', triples: [triple] };
566
+ }
567
+ case 'join': {
568
+ let result = null;
569
+ for (const sub of pattern.patterns) {
570
+ const subNode = convertExistsPattern(sub, registry);
571
+ result = result ? joinNodes(result, subNode) : subNode;
572
+ }
573
+ return result || { type: 'bgp', triples: [] };
574
+ }
575
+ case 'shape_scan': {
576
+ return {
577
+ type: 'bgp',
578
+ triples: [
579
+ tripleOf(varTerm(pattern.alias), iriTerm(RDF_TYPE), iriTerm(pattern.shape)),
580
+ ],
581
+ };
582
+ }
583
+ case 'optional': {
584
+ const inner = convertExistsPattern(pattern.pattern, registry);
585
+ return wrapOptional({ type: 'bgp', triples: [] }, inner);
586
+ }
587
+ case 'union': {
588
+ let result = null;
589
+ for (const branch of pattern.branches) {
590
+ const branchNode = convertExistsPattern(branch, registry);
591
+ if (!result) {
592
+ result = branchNode;
593
+ }
594
+ else {
595
+ result = { type: 'union', left: result, right: branchNode };
596
+ }
597
+ }
598
+ return result || { type: 'bgp', triples: [] };
599
+ }
600
+ case 'exists': {
601
+ return convertExistsPattern(pattern.pattern, registry);
602
+ }
603
+ default:
604
+ throw new Error(`Unsupported pattern kind in EXISTS: ${pattern.kind}`);
605
+ }
606
+ }
607
+ /**
608
+ * Resolve what variable name an IR expression ultimately refers to.
609
+ */
610
+ function resolveExpressionVariable(expr, registry) {
611
+ switch (expr.kind) {
612
+ case 'alias_expr':
613
+ return expr.alias;
614
+ case 'property_expr':
615
+ return registry.getOrCreate(expr.sourceAlias, expr.property);
616
+ default:
617
+ return null;
618
+ }
619
+ }
620
+ // ---------------------------------------------------------------------------
621
+ // Mutation conversions
622
+ // ---------------------------------------------------------------------------
623
+ /**
624
+ * Convert a field value to one or more SparqlTerm objects for triple objects.
625
+ */
626
+ function fieldValueToTerms(value, options) {
627
+ if (value === null || value === undefined) {
628
+ return [];
629
+ }
630
+ if (typeof value === 'string') {
631
+ return [literalTerm(value)];
632
+ }
633
+ if (typeof value === 'number') {
634
+ if (Number.isInteger(value)) {
635
+ return [literalTerm(String(value), XSD_INTEGER)];
636
+ }
637
+ return [literalTerm(String(value), XSD_DOUBLE)];
638
+ }
639
+ if (typeof value === 'boolean') {
640
+ return [literalTerm(String(value), XSD_BOOLEAN)];
641
+ }
642
+ if (value instanceof Date) {
643
+ return [literalTerm(value.toISOString(), XSD_DATETIME)];
644
+ }
645
+ // NodeReferenceValue
646
+ if (typeof value === 'object' && 'id' in value && !('shape' in value) && !('fields' in value)) {
647
+ return [iriTerm(value.id)];
648
+ }
649
+ // IRNodeData — should not produce a term directly (handled by nested create)
650
+ if (typeof value === 'object' && 'shape' in value && 'fields' in value) {
651
+ return []; // Handled separately
652
+ }
653
+ // Array
654
+ if (Array.isArray(value)) {
655
+ const terms = [];
656
+ for (const item of value) {
657
+ terms.push(...fieldValueToTerms(item, options));
658
+ }
659
+ return terms;
660
+ }
661
+ return [];
662
+ }
663
+ /**
664
+ * Recursively generate triples for an IRNodeData (used in create and nested creates).
665
+ * Returns the URI used for this node and all generated triples.
666
+ */
667
+ function generateNodeDataTriples(data, options) {
668
+ const uri = data.id || generateEntityUri(data.shape, options);
669
+ const triples = [];
670
+ const subjectTerm = iriTerm(uri);
671
+ // Type triple
672
+ triples.push(tripleOf(subjectTerm, iriTerm(RDF_TYPE), iriTerm(data.shape)));
673
+ // Field triples
674
+ for (const field of data.fields) {
675
+ const propertyTerm = iriTerm(field.property);
676
+ if (field.value === null || field.value === undefined) {
677
+ continue;
678
+ }
679
+ // Handle arrays (including mixed arrays of references and nested creates)
680
+ if (Array.isArray(field.value)) {
681
+ for (const item of field.value) {
682
+ if (item && typeof item === 'object' && 'shape' in item && 'fields' in item) {
683
+ // Nested create
684
+ const nested = generateNodeDataTriples(item, options);
685
+ triples.push(tripleOf(subjectTerm, propertyTerm, iriTerm(nested.uri)));
686
+ triples.push(...nested.triples);
687
+ }
688
+ else {
689
+ const terms = fieldValueToTerms(item, options);
690
+ for (const term of terms) {
691
+ triples.push(tripleOf(subjectTerm, propertyTerm, term));
692
+ }
693
+ }
694
+ }
695
+ continue;
696
+ }
697
+ // Handle nested IRNodeData
698
+ if (typeof field.value === 'object' && 'shape' in field.value && 'fields' in field.value) {
699
+ const nested = generateNodeDataTriples(field.value, options);
700
+ triples.push(tripleOf(subjectTerm, propertyTerm, iriTerm(nested.uri)));
701
+ triples.push(...nested.triples);
702
+ continue;
703
+ }
704
+ // Simple values
705
+ const terms = fieldValueToTerms(field.value, options);
706
+ for (const term of terms) {
707
+ triples.push(tripleOf(subjectTerm, propertyTerm, term));
708
+ }
709
+ }
710
+ return { uri, triples };
711
+ }
712
+ /**
713
+ * Converts an IRCreateMutation to a SparqlInsertDataPlan.
714
+ */
715
+ export function createToAlgebra(query, options) {
716
+ const { triples } = generateNodeDataTriples(query.data, options);
717
+ return {
718
+ type: 'insert_data',
719
+ triples,
720
+ };
721
+ }
722
+ /**
723
+ * Converts an IRUpdateMutation to a SparqlDeleteInsertPlan.
724
+ */
725
+ export function updateToAlgebra(query, options) {
726
+ const subjectTerm = iriTerm(query.id);
727
+ const deletePatterns = [];
728
+ const insertPatterns = [];
729
+ const whereTriples = [];
730
+ for (const field of query.data.fields) {
731
+ const propertyTerm = iriTerm(field.property);
732
+ const suffix = propertySuffix(field.property);
733
+ // Check for set modification ({add, remove})
734
+ if (field.value &&
735
+ typeof field.value === 'object' &&
736
+ !Array.isArray(field.value) &&
737
+ !(field.value instanceof Date) &&
738
+ !('id' in field.value) &&
739
+ !('shape' in field.value) &&
740
+ ('add' in field.value || 'remove' in field.value)) {
741
+ const setMod = field.value;
742
+ // Remove specific values
743
+ if (setMod.remove) {
744
+ for (const removeItem of setMod.remove) {
745
+ const removeTerm = iriTerm(removeItem.id);
746
+ deletePatterns.push(tripleOf(subjectTerm, propertyTerm, removeTerm));
747
+ whereTriples.push(tripleOf(subjectTerm, propertyTerm, removeTerm));
748
+ }
749
+ }
750
+ // Add new values
751
+ if (setMod.add) {
752
+ for (const addItem of setMod.add) {
753
+ if (addItem && typeof addItem === 'object' && 'shape' in addItem && 'fields' in addItem) {
754
+ // Nested create in add
755
+ const nested = generateNodeDataTriples(addItem, options);
756
+ insertPatterns.push(tripleOf(subjectTerm, propertyTerm, iriTerm(nested.uri)));
757
+ insertPatterns.push(...nested.triples);
758
+ }
759
+ else {
760
+ const terms = fieldValueToTerms(addItem, options);
761
+ for (const term of terms) {
762
+ insertPatterns.push(tripleOf(subjectTerm, propertyTerm, term));
763
+ }
764
+ }
765
+ }
766
+ }
767
+ continue;
768
+ }
769
+ // Unset (undefined/null) — delete only
770
+ if (field.value === undefined || field.value === null) {
771
+ const oldVar = varTerm(`old_${suffix}`);
772
+ deletePatterns.push(tripleOf(subjectTerm, propertyTerm, oldVar));
773
+ whereTriples.push(tripleOf(subjectTerm, propertyTerm, oldVar));
774
+ continue;
775
+ }
776
+ // Array overwrite — delete old values + insert new ones
777
+ if (Array.isArray(field.value)) {
778
+ const oldVar = varTerm(`old_${suffix}`);
779
+ deletePatterns.push(tripleOf(subjectTerm, propertyTerm, oldVar));
780
+ whereTriples.push(tripleOf(subjectTerm, propertyTerm, oldVar));
781
+ for (const item of field.value) {
782
+ if (item && typeof item === 'object' && 'shape' in item && 'fields' in item) {
783
+ const nested = generateNodeDataTriples(item, options);
784
+ insertPatterns.push(tripleOf(subjectTerm, propertyTerm, iriTerm(nested.uri)));
785
+ insertPatterns.push(...nested.triples);
786
+ }
787
+ else {
788
+ const terms = fieldValueToTerms(item, options);
789
+ for (const term of terms) {
790
+ insertPatterns.push(tripleOf(subjectTerm, propertyTerm, term));
791
+ }
792
+ }
793
+ }
794
+ continue;
795
+ }
796
+ // Nested create (single object field)
797
+ if (typeof field.value === 'object' && 'shape' in field.value && 'fields' in field.value) {
798
+ const oldVar = varTerm(`old_${suffix}`);
799
+ deletePatterns.push(tripleOf(subjectTerm, propertyTerm, oldVar));
800
+ whereTriples.push(tripleOf(subjectTerm, propertyTerm, oldVar));
801
+ const nested = generateNodeDataTriples(field.value, options);
802
+ insertPatterns.push(tripleOf(subjectTerm, propertyTerm, iriTerm(nested.uri)));
803
+ insertPatterns.push(...nested.triples);
804
+ continue;
805
+ }
806
+ // Simple value update — delete old + insert new
807
+ const oldVar = varTerm(`old_${suffix}`);
808
+ deletePatterns.push(tripleOf(subjectTerm, propertyTerm, oldVar));
809
+ whereTriples.push(tripleOf(subjectTerm, propertyTerm, oldVar));
810
+ const terms = fieldValueToTerms(field.value, options);
811
+ for (const term of terms) {
812
+ insertPatterns.push(tripleOf(subjectTerm, propertyTerm, term));
813
+ }
814
+ }
815
+ // Wrap WHERE triples in OPTIONAL so UPDATE succeeds even when the old
816
+ // value doesn't exist (e.g. setting bestFriend when none was set before).
817
+ let whereAlgebra;
818
+ if (whereTriples.length === 0) {
819
+ whereAlgebra = { type: 'bgp', triples: [] };
820
+ }
821
+ else if (whereTriples.length === 1) {
822
+ whereAlgebra = {
823
+ type: 'left_join',
824
+ left: { type: 'bgp', triples: [] },
825
+ right: { type: 'bgp', triples: whereTriples },
826
+ };
827
+ }
828
+ else {
829
+ // Wrap each triple in its own OPTIONAL for independent matching
830
+ whereAlgebra = { type: 'bgp', triples: [] };
831
+ for (const triple of whereTriples) {
832
+ whereAlgebra = {
833
+ type: 'left_join',
834
+ left: whereAlgebra,
835
+ right: { type: 'bgp', triples: [triple] },
836
+ };
837
+ }
838
+ }
839
+ return {
840
+ type: 'delete_insert',
841
+ deletePatterns,
842
+ insertPatterns,
843
+ whereAlgebra,
844
+ };
845
+ }
846
+ /**
847
+ * Converts an IRDeleteMutation to a SparqlDeleteInsertPlan (DELETE + WHERE).
848
+ */
849
+ export function deleteToAlgebra(query, _options) {
850
+ const deletePatterns = [];
851
+ const requiredTriples = [];
852
+ const optionalTriples = [];
853
+ for (let i = 0; i < query.ids.length; i++) {
854
+ const subjectTerm = iriTerm(query.ids[i].id);
855
+ const idx = query.ids.length > 1 ? `_${i}` : '';
856
+ const subjWild = tripleOf(subjectTerm, varTerm(`p${idx}`), varTerm(`o${idx}`));
857
+ const objWild = tripleOf(varTerm(`s${idx}`), varTerm(`p2${idx}`), subjectTerm);
858
+ const typeGuard = tripleOf(subjectTerm, iriTerm(RDF_TYPE), iriTerm(query.shape));
859
+ // DELETE block: all patterns (subject-wildcard, object-wildcard, type)
860
+ deletePatterns.push(subjWild, objWild, typeGuard);
861
+ // WHERE block: subject-wildcard and type guard are required;
862
+ // object-wildcard is OPTIONAL (entity may have no incoming references)
863
+ requiredTriples.push(subjWild, typeGuard);
864
+ optionalTriples.push(objWild);
865
+ }
866
+ // Build WHERE algebra: required BGP + OPTIONAL for each object-wildcard
867
+ let whereAlgebra = { type: 'bgp', triples: requiredTriples };
868
+ for (const triple of optionalTriples) {
869
+ whereAlgebra = {
870
+ type: 'left_join',
871
+ left: whereAlgebra,
872
+ right: { type: 'bgp', triples: [triple] },
873
+ };
874
+ }
875
+ return {
876
+ type: 'delete_insert',
877
+ deletePatterns,
878
+ insertPatterns: [],
879
+ whereAlgebra,
880
+ };
881
+ }
882
+ // ---------------------------------------------------------------------------
883
+ // Convenience wrappers: IR → algebra → SPARQL string in one call
884
+ // ---------------------------------------------------------------------------
885
+ /**
886
+ * Converts an IRSelectQuery to a SPARQL string.
887
+ * Stub: will be implemented when algebraToString is available.
888
+ */
889
+ export function selectToSparql(query, options) {
890
+ const plan = selectToAlgebra(query, options);
891
+ return selectPlanToSparql(plan, options);
892
+ }
893
+ /**
894
+ * Converts an IRCreateMutation to a SPARQL string.
895
+ * Stub: will be implemented when algebraToString is available.
896
+ */
897
+ export function createToSparql(query, options) {
898
+ const plan = createToAlgebra(query, options);
899
+ return insertDataPlanToSparql(plan, options);
900
+ }
901
+ /**
902
+ * Converts an IRUpdateMutation to a SPARQL string.
903
+ * Stub: will be implemented when algebraToString is available.
904
+ */
905
+ export function updateToSparql(query, options) {
906
+ const plan = updateToAlgebra(query, options);
907
+ return deleteInsertPlanToSparql(plan, options);
908
+ }
909
+ /**
910
+ * Converts an IRDeleteMutation to a SPARQL string.
911
+ * Stub: will be implemented when algebraToString is available.
912
+ */
913
+ export function deleteToSparql(query, options) {
914
+ const plan = deleteToAlgebra(query, options);
915
+ return deleteInsertPlanToSparql(plan, options);
916
+ }
917
+ //# sourceMappingURL=irToAlgebra.js.map