@_linked/core 2.28.1 → 2.28.3

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.
@@ -1641,7 +1641,10 @@ function processUpdateFields(data, subjectTerm, options) {
1641
1641
  const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, `uc${n}_`);
1642
1642
  deletePatterns.push(...self.deletePatterns);
1643
1643
  cascadeOptionals.push(...self.whereOptionals);
1644
- const cascade = buildOwnedCascade(oldTerm, `uc${n}_`);
1644
+ // Every cascade block binds `?old` itself through the owning edge, like the
1645
+ // self-delete block does. Without it an absent old value leaves `?old` unbound and
1646
+ // the block matches the owned subtree of EVERY node in the graph.
1647
+ const cascade = buildOwnedCascade(oldTerm, `uc${n}_`, tripleOf(subjectTerm, propertyTerm, oldTerm));
1645
1648
  deletePatterns.push(...cascade.deletePatterns);
1646
1649
  cascadeOptionals.push(...cascade.whereOptionals);
1647
1650
  });
@@ -1699,21 +1702,80 @@ export function upsertToAlgebra(query, options) {
1699
1702
  * update path has always emitted.
1700
1703
  */
1701
1704
  function buildDeleteInsertPlan(query, options, opts) {
1702
- var _a, _b, _c;
1703
1705
  const subjectTerm = iriTerm(query.id);
1704
1706
  const result = processUpdateFields(query.data, subjectTerm, options);
1707
+ const whereAlgebra = buildUpdateWhere({ type: 'bgp', triples: [] }, result, query.traversalPatterns, subjectTerm);
1708
+ // The type triple leads the INSERT, mirroring `create`'s triple order.
1709
+ const insertPatterns = (opts === null || opts === void 0 ? void 0 : opts.ensureType)
1710
+ ? [
1711
+ tripleOf(subjectTerm, iriTerm(RDF_TYPE), iriTerm(opts.ensureType)),
1712
+ ...result.insertPatterns,
1713
+ ]
1714
+ : result.insertPatterns;
1715
+ return {
1716
+ type: 'delete_insert',
1717
+ deletePatterns: result.deletePatterns,
1718
+ insertPatterns,
1719
+ whereAlgebra,
1720
+ };
1721
+ }
1722
+ /** Left-nested UNION of `blocks` (at least one). */
1723
+ function unionOf(blocks) {
1724
+ let union = blocks[0];
1725
+ for (let i = 1; i < blocks.length; i++) {
1726
+ union = { type: 'union', left: union, right: blocks[i] };
1727
+ }
1728
+ return union;
1729
+ }
1730
+ /** Every variable name an expression mentions, including inside EXISTS patterns. */
1731
+ function collectVariableNames(value, into) {
1732
+ if (Array.isArray(value)) {
1733
+ for (const item of value)
1734
+ collectVariableNames(item, into);
1735
+ }
1736
+ else if (value && typeof value === 'object') {
1737
+ const node = value;
1738
+ if ((node.kind === 'variable_expr' || node.kind === 'variable') && typeof node.name === 'string') {
1739
+ into.add(node.name);
1740
+ }
1741
+ if (node.kind === 'bound_expr' && typeof node.variable === 'string') {
1742
+ into.add(node.variable);
1743
+ }
1744
+ for (const child of Object.values(value))
1745
+ collectVariableNames(child, into);
1746
+ }
1747
+ return into;
1748
+ }
1749
+ /**
1750
+ * The WHERE shared by `update`, `upsert` and `updateWhere`, on top of `base` (empty for
1751
+ * an id-targeted update; the type guard and filter for an update-where).
1752
+ *
1753
+ * The old values and owned-cleanup blocks are independent of each other, so they are
1754
+ * joined as ONE OPTIONAL over a UNION, never as a chain of OPTIONALs. A chain is a join:
1755
+ * the old values of two multi-valued properties, or an owned node's own triples and its
1756
+ * owned subtree, multiply into one solution per combination. A UNION yields each block's
1757
+ * matches once, so the solutions add up, and the DELETE removes exactly the same triples
1758
+ * (a template triple a solution leaves unbound is skipped). The OPTIONAL keeps one
1759
+ * solution when nothing matches, so the INSERT still happens.
1760
+ *
1761
+ * Old values that a computed field's BIND reads stay in `base` as before, together with
1762
+ * the traversals they need, because the BIND needs them bound in the same solution. With
1763
+ * a single independent block the output is the one OPTIONAL it always was.
1764
+ */
1765
+ function buildUpdateWhere(base, result, traversalPatterns, subjectTerm) {
1766
+ var _a, _b;
1705
1767
  // Partition old-value/optional triples into subject-anchored ones and those
1706
1768
  // anchored on a traversal target variable (e.g. `?a1 <name> ?a1_name` from a
1707
1769
  // `p.bestFriend.name.ucase()` expression). Traversal-anchored triples MUST be
1708
1770
  // nested inside the same OPTIONAL group as their traversal edge — otherwise,
1709
1771
  // when the subject has no such edge, the leaf variable is unbound and the
1710
1772
  // property triple matches every entity in the graph (data-corruption bug).
1711
- const travTos = new Set(((_a = query.traversalPatterns) !== null && _a !== void 0 ? _a : []).map((t) => t.to));
1773
+ const travTos = new Set((traversalPatterns !== null && traversalPatterns !== void 0 ? traversalPatterns : []).map((t) => t.to));
1712
1774
  const travAnchoredByTo = new Map();
1713
1775
  const subjectAnchored = [];
1714
1776
  for (const triple of result.oldValueTriples) {
1715
1777
  if (triple.subject.kind === 'variable' && travTos.has(triple.subject.name)) {
1716
- const list = (_b = travAnchoredByTo.get(triple.subject.name)) !== null && _b !== void 0 ? _b : [];
1778
+ const list = (_a = travAnchoredByTo.get(triple.subject.name)) !== null && _a !== void 0 ? _a : [];
1717
1779
  list.push(triple);
1718
1780
  travAnchoredByTo.set(triple.subject.name, list);
1719
1781
  }
@@ -1721,36 +1783,50 @@ function buildDeleteInsertPlan(query, options, opts) {
1721
1783
  subjectAnchored.push(triple);
1722
1784
  }
1723
1785
  }
1724
- let whereAlgebra = wrapOldValueOptionals({ type: 'bgp', triples: [] }, subjectAnchored);
1786
+ // Old values a BIND reads must share its solution; the rest are independent blocks.
1787
+ const readByBinds = new Set();
1788
+ for (const ext of result.extends)
1789
+ collectVariableNames(ext.expression, readByBinds);
1790
+ const correlated = [];
1791
+ const independent = [];
1792
+ for (const triple of subjectAnchored) {
1793
+ const vars = collectVariableNames([triple.subject, triple.predicate, triple.object], new Set());
1794
+ if ([...vars].some((v) => readByBinds.has(v))) {
1795
+ correlated.push(triple);
1796
+ }
1797
+ else {
1798
+ independent.push({ type: 'bgp', triples: [triple] });
1799
+ }
1800
+ }
1801
+ let whereAlgebra = wrapOldValueOptionals(base, correlated);
1725
1802
  // Add traversal OPTIONAL patterns (for multi-segment expression refs). The
1726
1803
  // traversal edge and its dependent leaf property triples share one OPTIONAL
1727
1804
  // group so the leaf variable is scoped to the traversal target. These come
1728
1805
  // BEFORE expression BINDs since the BINDs reference the traversal variables.
1729
- if (query.traversalPatterns) {
1730
- for (const trav of query.traversalPatterns) {
1731
- const fromTerm = trav.from === '__mutation_subject__' ? subjectTerm : varTerm(trav.from);
1732
- const traversalTriple = tripleOf(fromTerm,
1733
- // The declared `sh:path`, like every other predicate — not the property
1734
- // shape's own IRI, which identifies the description of the property.
1735
- resolvePropertyPredicateTerm(trav.property), varTerm(trav.to));
1736
- // The traversal edge binds the target var; each dependent leaf property is
1737
- // a nested OPTIONAL *within* that scope, so a missing optional property
1738
- // (e.g. a bestFriend with no hobby) doesn't drop the whole group, while an
1739
- // absent edge still leaves every leaf var unbound (no cross-entity match).
1740
- let travNode = { type: 'bgp', triples: [traversalTriple] };
1741
- for (const leaf of (_c = travAnchoredByTo.get(trav.to)) !== null && _c !== void 0 ? _c : []) {
1742
- travNode = {
1743
- type: 'left_join',
1744
- left: travNode,
1745
- right: { type: 'bgp', triples: [leaf] },
1746
- };
1747
- }
1748
- whereAlgebra = { type: 'left_join', left: whereAlgebra, right: travNode };
1806
+ for (const trav of traversalPatterns !== null && traversalPatterns !== void 0 ? traversalPatterns : []) {
1807
+ const fromTerm = trav.from === '__mutation_subject__' ? subjectTerm : varTerm(trav.from);
1808
+ const traversalTriple = tripleOf(fromTerm,
1809
+ // The declared `sh:path`, like every other predicate — not the property
1810
+ // shape's own IRI, which identifies the description of the property.
1811
+ resolvePropertyPredicateTerm(trav.property), varTerm(trav.to));
1812
+ // The traversal edge binds the target var; each dependent leaf property is
1813
+ // a nested OPTIONAL *within* that scope, so a missing optional property
1814
+ // (e.g. a bestFriend with no hobby) doesn't drop the whole group, while an
1815
+ // absent edge still leaves every leaf var unbound (no cross-entity match).
1816
+ let travNode = { type: 'bgp', triples: [traversalTriple] };
1817
+ for (const leaf of (_b = travAnchoredByTo.get(trav.to)) !== null && _b !== void 0 ? _b : []) {
1818
+ travNode = {
1819
+ type: 'left_join',
1820
+ left: travNode,
1821
+ right: { type: 'bgp', triples: [leaf] },
1822
+ };
1749
1823
  }
1824
+ whereAlgebra = { type: 'left_join', left: whereAlgebra, right: travNode };
1750
1825
  }
1751
- // Owned-subtree cascade OPTIONALs for replaced `contains` properties.
1752
- for (const optional of result.cascadeOptionals) {
1753
- whereAlgebra = { type: 'left_join', left: whereAlgebra, right: optional };
1826
+ // Independent old values + owned-subtree cleanup for `contains` replace/remove.
1827
+ const blocks = [...independent, ...result.cascadeOptionals];
1828
+ if (blocks.length > 0) {
1829
+ whereAlgebra = { type: 'left_join', left: whereAlgebra, right: unionOf(blocks) };
1754
1830
  }
1755
1831
  // Add BIND expressions for computed fields
1756
1832
  for (const ext of result.extends) {
@@ -1761,19 +1837,7 @@ function buildDeleteInsertPlan(query, options, opts) {
1761
1837
  expression: ext.expression,
1762
1838
  };
1763
1839
  }
1764
- // The type triple leads the INSERT, mirroring `create`'s triple order.
1765
- const insertPatterns = (opts === null || opts === void 0 ? void 0 : opts.ensureType)
1766
- ? [
1767
- tripleOf(subjectTerm, iriTerm(RDF_TYPE), iriTerm(opts.ensureType)),
1768
- ...result.insertPatterns,
1769
- ]
1770
- : result.insertPatterns;
1771
- return {
1772
- type: 'delete_insert',
1773
- deletePatterns: result.deletePatterns,
1774
- insertPatterns,
1775
- whereAlgebra,
1776
- };
1840
+ return whereAlgebra;
1777
1841
  }
1778
1842
  // ---------------------------------------------------------------------------
1779
1843
  // Owned-subtree cascade
@@ -1833,8 +1897,11 @@ function collectContainment() {
1833
1897
  * Build DELETE patterns + WHERE OPTIONAL blocks that cascade-delete the owned subtree
1834
1898
  * reachable from `rootTerm` via `contains` edges. `varPrefix` keeps generated variables
1835
1899
  * unique across multiple roots in one query. Returns empty when nothing is owned.
1900
+ *
1901
+ * `anchor`, when given, leads every block: the triple that binds a variable root, so
1902
+ * each block stays correct on its own (as a UNION branch) when no such root exists.
1836
1903
  */
1837
- export function buildOwnedCascade(rootTerm, varPrefix) {
1904
+ export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
1838
1905
  const { containsPreds, dependentTypes } = collectContainment();
1839
1906
  if (containsPreds.length === 0 || dependentTypes.length === 0) {
1840
1907
  return { deletePatterns: [], whereOptionals: [] };
@@ -1858,6 +1925,7 @@ export function buildOwnedCascade(rootTerm, varPrefix) {
1858
1925
  whereOptionals.push({
1859
1926
  type: 'bgp',
1860
1927
  triples: [
1928
+ ...(anchor ? [anchor] : []),
1861
1929
  tripleOf(rootTerm, pathTerm, owned),
1862
1930
  tripleOf(owned, iriTerm(RDF_TYPE), iriTerm(typeId)),
1863
1931
  tripleOf(owned, p, o),
@@ -1895,12 +1963,27 @@ export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPref
1895
1963
  }
1896
1964
  /**
1897
1965
  * Converts an IRDeleteMutation to a SparqlDeleteInsertPlan (DELETE + WHERE).
1966
+ *
1967
+ * The WHERE is the type guard of every id, joined with ONE UNION of independent
1968
+ * blocks: per id its own triples, its incoming references and, for a shape that
1969
+ * owns something, one block per dependent type of its owned subtree.
1970
+ *
1971
+ * The blocks must not be chained as OPTIONALs. A chain of OPTIONALs is a join, so
1972
+ * the solutions multiply: own triples x incoming references x owned triples of each
1973
+ * dependent type. Deleting one shape description with the shape sync's cascade
1974
+ * produced ~27,000 solutions for ~130 triples and took over 30 seconds on Fuseki,
1975
+ * which re-evaluates the cascade property path for every left-hand row. A UNION
1976
+ * yields each block's matches once, so the solutions add up instead, and it deletes
1977
+ * exactly the same triples: a DELETE template triple whose variables a solution
1978
+ * leaves unbound is skipped, as it was for an OPTIONAL that did not match.
1979
+ *
1980
+ * The guards stay required for every id, as before: when any id is not of the
1981
+ * shape, the WHERE has no solution and nothing is deleted.
1898
1982
  */
1899
1983
  export function deleteToAlgebra(query, _options) {
1900
1984
  const deletePatterns = [];
1901
- const requiredTriples = [];
1902
- const optionalTriples = [];
1903
- const cascadeOptionals = [];
1985
+ const guards = [];
1986
+ const blocks = [];
1904
1987
  for (let i = 0; i < query.ids.length; i++) {
1905
1988
  const subjectTerm = iriTerm(query.ids[i].id);
1906
1989
  const idx = query.ids.length > 1 ? `_${i}` : '';
@@ -1909,30 +1992,23 @@ export function deleteToAlgebra(query, _options) {
1909
1992
  const typeGuard = tripleOf(subjectTerm, iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
1910
1993
  // DELETE block: all patterns (subject-wildcard, object-wildcard, type)
1911
1994
  deletePatterns.push(subjWild, objWild, typeGuard);
1912
- // WHERE block: subject-wildcard and type guard are required;
1913
- // object-wildcard is OPTIONAL (entity may have no incoming references)
1914
- requiredTriples.push(subjWild, typeGuard);
1915
- optionalTriples.push(objWild);
1995
+ // WHERE: the type guard is required; the subject's own triples and its
1996
+ // incoming references (the entity may have none) are separate UNION blocks.
1997
+ guards.push(typeGuard);
1998
+ blocks.push({ type: 'bgp', triples: [subjWild] }, { type: 'bgp', triples: [objWild] });
1916
1999
  // Cascade-delete the owned subtree — only for shapes that actually own something
1917
2000
  // (have a `contains` property); a plain entity delete is left untouched.
1918
2001
  if (shapeHasContainsProperty(query.shape)) {
1919
2002
  const cascade = buildOwnedCascade(subjectTerm, `c${idx}_`);
1920
2003
  deletePatterns.push(...cascade.deletePatterns);
1921
- cascadeOptionals.push(...cascade.whereOptionals);
2004
+ blocks.push(...cascade.whereOptionals);
1922
2005
  }
1923
2006
  }
1924
- // Build WHERE algebra: required BGP + OPTIONAL for each object-wildcard + cascade OPTIONALs
1925
- let whereAlgebra = { type: 'bgp', triples: requiredTriples };
1926
- for (const triple of optionalTriples) {
1927
- whereAlgebra = {
1928
- type: 'left_join',
1929
- left: whereAlgebra,
1930
- right: { type: 'bgp', triples: [triple] },
1931
- };
1932
- }
1933
- for (const optional of cascadeOptionals) {
1934
- whereAlgebra = { type: 'left_join', left: whereAlgebra, right: optional };
1935
- }
2007
+ const whereAlgebra = {
2008
+ type: 'join',
2009
+ left: { type: 'bgp', triples: guards },
2010
+ right: unionOf(blocks),
2011
+ };
1936
2012
  return {
1937
2013
  type: 'delete_insert',
1938
2014
  deletePatterns,
@@ -1956,63 +2032,63 @@ function isBlankNodeProperty(prop) {
1956
2032
  return nk === shacl.BlankNode.id || nk === shacl.BlankNodeOrIRI.id;
1957
2033
  }
1958
2034
  /**
1959
- * Recursively builds DELETE + WHERE patterns for blank-node-typed properties.
2035
+ * Builds DELETE patterns + one WHERE block per blank node reachable from `parentVar`
2036
+ * through blank-node-typed properties, recursing into each property's valueShape for
2037
+ * nested blank nodes (e.g. Person → Address (blank) → GeoPoint (blank)).
1960
2038
  *
1961
- * For each blank-node property on the shape:
1962
- * - DELETE: `?bnVar ?pN ?oN .` (wildcard all triples on the blank node)
1963
- * - WHERE: `OPTIONAL { ?parent <property> ?bnVar . FILTER(isBlank(?bnVar)) . ?bnVar ?pN ?oN . }`
2039
+ * - DELETE: `?bnN ?pN ?oN .` (wildcard all triples on the blank node)
2040
+ * - WHERE block: the path of edges from the root down to `?bnN`, `?bnN ?pN ?oN`, and
2041
+ * `FILTER(isBlank(…))` for every blank node on that path.
1964
2042
  *
1965
- * Recurses into the property's valueShape to handle nested blank nodes
1966
- * (e.g. Person → Address (blank) → GeoPoint (blank)).
2043
+ * Each block carries its own path from the root, so the blocks are independent of one
2044
+ * another and are joined with a UNION by the caller. Nested as OPTIONALs, a node's
2045
+ * triples multiplied with every blank node below and beside it.
1967
2046
  */
1968
- function walkBlankNodeTree(shapeId, parentVar, depth, deletePatterns) {
2047
+ function blankNodeBlocks(shapeId, parentVar, path, counter, deletePatterns, blocks) {
1969
2048
  var _a, _b, _c;
1970
2049
  const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
1971
2050
  if (!nodeShape)
1972
- return null;
1973
- let optionals = null;
2051
+ return;
1974
2052
  const props = getPropertyShapes(nodeShape, true);
1975
2053
  for (const prop of props) {
1976
2054
  if (!isBlankNodeProperty(prop))
1977
2055
  continue;
1978
- const bnVar = `bn${depth}`;
1979
- const pVar = `p${depth}`;
1980
- const oVar = `o${depth}`;
1981
- // DELETE pattern: wildcard all triples on the blank node
1982
- deletePatterns.push(tripleOf(varTerm(bnVar), varTerm(pVar), varTerm(oVar)));
1983
- // WHERE: parent --<property>--> ?bnVar
1984
- const traverseTriple = tripleOf(varTerm(parentVar), resolvePropertyPredicateTerm(prop.id), varTerm(bnVar));
1985
- // FILTER(isBlank(?bnVar))
1986
- const isBlankFilter = {
1987
- kind: 'function_expr',
1988
- name: 'isBlank',
1989
- args: [{ kind: 'variable_expr', name: bnVar }],
1990
- };
1991
- // ?bnVar ?pN ?oN
1992
- const wildcardTriple = tripleOf(varTerm(bnVar), varTerm(pVar), varTerm(oVar));
1993
- // Build inner pattern: traverse + filter + wildcard
1994
- let innerPattern = {
1995
- type: 'bgp',
1996
- triples: [traverseTriple, wildcardTriple],
2056
+ const n = counter.next++;
2057
+ const bnVar = `bn${n}`;
2058
+ const wildcardTriple = tripleOf(varTerm(bnVar), varTerm(`p${n}`), varTerm(`o${n}`));
2059
+ deletePatterns.push(wildcardTriple);
2060
+ const here = {
2061
+ triples: [
2062
+ ...path.triples,
2063
+ tripleOf(varTerm(parentVar), resolvePropertyPredicateTerm(prop.id), varTerm(bnVar)),
2064
+ ],
2065
+ bnVars: [...path.bnVars, bnVar],
1997
2066
  };
1998
- innerPattern = { type: 'filter', expression: isBlankFilter, inner: innerPattern };
1999
- // Recurse into valueShape for nested blank nodes
2000
- if ((_c = prop.valueShape) === null || _c === void 0 ? void 0 : _c.id) {
2001
- const nestedOptional = walkBlankNodeTree(prop.valueShape.id, bnVar, depth + 1, deletePatterns);
2002
- if (nestedOptional) {
2003
- innerPattern = { type: 'left_join', left: innerPattern, right: nestedOptional };
2004
- }
2005
- }
2006
- // Wrap in OPTIONAL (left_join)
2007
- if (optionals) {
2008
- optionals = { type: 'left_join', left: optionals, right: innerPattern };
2067
+ let block = { type: 'bgp', triples: [...here.triples, wildcardTriple] };
2068
+ for (const v of here.bnVars) {
2069
+ block = {
2070
+ type: 'filter',
2071
+ expression: { kind: 'function_expr', name: 'isBlank', args: [{ kind: 'variable_expr', name: v }] },
2072
+ inner: block,
2073
+ };
2009
2074
  }
2010
- else {
2011
- optionals = innerPattern;
2075
+ blocks.push(block);
2076
+ if ((_c = prop.valueShape) === null || _c === void 0 ? void 0 : _c.id) {
2077
+ blankNodeBlocks(prop.valueShape.id, bnVar, here, counter, deletePatterns, blocks);
2012
2078
  }
2013
- depth++;
2014
2079
  }
2015
- return optionals;
2080
+ }
2081
+ /**
2082
+ * The blank-node cleanup blocks of a bulk delete, already joined with the root's own
2083
+ * triples as ONE UNION so their matches add up instead of multiplying; `null` when the
2084
+ * shape has no blank-node property (the root wildcard then stays in the required BGP).
2085
+ */
2086
+ function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, deletePatterns) {
2087
+ const blocks = [];
2088
+ blankNodeBlocks(shapeId, subjectVar, { triples: [], bnVars: [] }, { next: 1 }, deletePatterns, blocks);
2089
+ if (blocks.length === 0)
2090
+ return null;
2091
+ return unionOf([{ type: 'bgp', triples: [rootWildcard] }, ...blocks]);
2016
2092
  }
2017
2093
  /**
2018
2094
  * Converts an IRDeleteAllMutation to a SparqlDeleteInsertPlan.
@@ -2026,15 +2102,13 @@ export function deleteAllToAlgebra(query, _options) {
2026
2102
  const deletePatterns = [
2027
2103
  tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o')),
2028
2104
  ];
2029
- // WHERE: type triple + root wildcard
2105
+ // WHERE: type triple, then the root wildcard and the blank-node cleanup
2030
2106
  const typeTriple = tripleOf(varTerm(subjectVar), iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
2031
2107
  const rootWildcard = tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o'));
2032
- let whereAlgebra = { type: 'bgp', triples: [typeTriple, rootWildcard] };
2033
- // Walk blank node tree for cleanup
2034
- const blankNodeOptional = walkBlankNodeTree(query.shape, subjectVar, 1, deletePatterns);
2035
- if (blankNodeOptional) {
2036
- whereAlgebra = { type: 'left_join', left: whereAlgebra, right: blankNodeOptional };
2037
- }
2108
+ const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, deletePatterns);
2109
+ const whereAlgebra = union
2110
+ ? { type: 'join', left: { type: 'bgp', triples: [typeTriple] }, right: union }
2111
+ : { type: 'bgp', triples: [typeTriple, rootWildcard] };
2038
2112
  return {
2039
2113
  type: 'delete_insert',
2040
2114
  deletePatterns,
@@ -2054,10 +2128,14 @@ export function deleteWhereToAlgebra(query, _options) {
2054
2128
  const deletePatterns = [
2055
2129
  tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o')),
2056
2130
  ];
2057
- // WHERE: type triple + root wildcard
2131
+ // WHERE: type triple + the where clause; the root wildcard and blank-node cleanup follow
2058
2132
  const typeTriple = tripleOf(varTerm(subjectVar), iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
2059
2133
  const rootWildcard = tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o'));
2060
- let whereAlgebra = { type: 'bgp', triples: [typeTriple, rootWildcard] };
2134
+ const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, deletePatterns);
2135
+ let whereAlgebra = {
2136
+ type: 'bgp',
2137
+ triples: union ? [typeTriple] : [typeTriple, rootWildcard],
2138
+ };
2061
2139
  // Process where patterns (traversals from the where clause)
2062
2140
  const traverseTriples = [];
2063
2141
  const optionalPropertyTriples = [];
@@ -2077,10 +2155,8 @@ export function deleteWhereToAlgebra(query, _options) {
2077
2155
  // Convert and add filter expression
2078
2156
  const filterExpr = convertExpression(query.where, registry, []);
2079
2157
  whereAlgebra = { type: 'filter', expression: filterExpr, inner: whereAlgebra };
2080
- // Walk blank node tree for cleanup
2081
- const blankNodeOptional = walkBlankNodeTree(query.shape, subjectVar, 1, deletePatterns);
2082
- if (blankNodeOptional) {
2083
- whereAlgebra = { type: 'left_join', left: whereAlgebra, right: blankNodeOptional };
2158
+ if (union) {
2159
+ whereAlgebra = { type: 'join', left: whereAlgebra, right: union };
2084
2160
  }
2085
2161
  return {
2086
2162
  type: 'delete_insert',
@@ -2097,7 +2173,6 @@ export function deleteWhereToAlgebra(query, _options) {
2097
2173
  * filter conditions from the where clause.
2098
2174
  */
2099
2175
  export function updateWhereToAlgebra(query, options) {
2100
- var _a, _b, _c;
2101
2176
  const subjectTerm = varTerm('a0');
2102
2177
  const result = processUpdateFields(query.data, subjectTerm, options);
2103
2178
  // WHERE: type triple is always required
@@ -2122,62 +2197,11 @@ export function updateWhereToAlgebra(query, options) {
2122
2197
  whereAlgebra = { type: 'filter', expression: filterExpr, inner: whereAlgebra };
2123
2198
  }
2124
2199
  // Old-value triples anchored on a traversal *target* belong INSIDE that
2125
- // traversal's OPTIONAL group, not beside it. Emitted beside it, the leaf's
2126
- // subject variable is introduced by an OPTIONAL that shares no variable with
2127
- // anything to its left — a left join with no join condition, i.e. a cartesian
2128
- // product over every node in the store carrying that predicate. The following
2129
- // OPTIONAL cannot repair it: the variable is already bound, and OPTIONAL never
2130
- // removes rows. `updateToAlgebra` performs the same split; this path did not.
2131
- const travTos = new Set(((_a = query.traversalPatterns) !== null && _a !== void 0 ? _a : []).map((t) => t.to));
2132
- const travAnchoredByTo = new Map();
2133
- const subjectAnchored = [];
2134
- for (const triple of result.oldValueTriples) {
2135
- if (triple.subject.kind === 'variable' && travTos.has(triple.subject.name)) {
2136
- const list = (_b = travAnchoredByTo.get(triple.subject.name)) !== null && _b !== void 0 ? _b : [];
2137
- list.push(triple);
2138
- travAnchoredByTo.set(triple.subject.name, list);
2139
- }
2140
- else {
2141
- subjectAnchored.push(triple);
2142
- }
2143
- }
2144
- whereAlgebra = wrapOldValueOptionals(whereAlgebra, subjectAnchored);
2145
- // Add traversal OPTIONAL patterns (for multi-segment expression refs)
2146
- // These must come BEFORE expression BINDs since the BINDs reference traversal variables.
2147
- if (query.traversalPatterns) {
2148
- for (const trav of query.traversalPatterns) {
2149
- const fromTerm = trav.from === '__mutation_subject__' ? varTerm('a0') : varTerm(trav.from);
2150
- const traversalTriple = tripleOf(fromTerm,
2151
- // The declared `sh:path`, like every other predicate — not the property
2152
- // shape's own IRI, which identifies the description of the property.
2153
- resolvePropertyPredicateTerm(trav.property), varTerm(trav.to));
2154
- // The edge binds the target variable; each dependent leaf property is a
2155
- // nested OPTIONAL within that scope, so a missing leaf does not drop the
2156
- // group while an absent edge leaves every leaf variable unbound.
2157
- let travNode = { type: 'bgp', triples: [traversalTriple] };
2158
- for (const leaf of (_c = travAnchoredByTo.get(trav.to)) !== null && _c !== void 0 ? _c : []) {
2159
- travNode = {
2160
- type: 'left_join',
2161
- left: travNode,
2162
- right: { type: 'bgp', triples: [leaf] },
2163
- };
2164
- }
2165
- whereAlgebra = { type: 'left_join', left: whereAlgebra, right: travNode };
2166
- }
2167
- }
2168
- // Owned-subtree cascade OPTIONALs for replaced `contains` properties.
2169
- for (const optional of result.cascadeOptionals) {
2170
- whereAlgebra = { type: 'left_join', left: whereAlgebra, right: optional };
2171
- }
2172
- // Add BIND expressions for computed fields
2173
- for (const ext of result.extends) {
2174
- whereAlgebra = {
2175
- type: 'extend',
2176
- inner: whereAlgebra,
2177
- variable: ext.variable,
2178
- expression: ext.expression,
2179
- };
2180
- }
2200
+ // traversal's OPTIONAL group, not beside it: beside it, the leaf's subject variable
2201
+ // is introduced by an OPTIONAL that shares no variable with anything to its left — a
2202
+ // cartesian product over every node carrying that predicate. `buildUpdateWhere` does
2203
+ // that split for both update paths.
2204
+ whereAlgebra = buildUpdateWhere(whereAlgebra, result, query.traversalPatterns, subjectTerm);
2181
2205
  return {
2182
2206
  type: 'delete_insert',
2183
2207
  deletePatterns: result.deletePatterns,