@_linked/core 2.28.2 → 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.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.28.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#322](https://github.com/linked-fw/core/pull/322) [`720e284`](https://github.com/linked-fw/core/commit/720e28438842cc8aa617a818c086b6279e87ade0) Thanks [@flyon](https://github.com/flyon)! - An update's WHERE, and a bulk delete's, no longer multiply their parts — the same fix [#318](https://github.com/linked-fw/core/issues/318) made for a delete by id. `update`, `upsert` and `updateWhere` chained the old value of every property and every owned-cleanup block as OPTIONALs, so the solutions were the product of all of them: updating two multi-valued properties gave one solution per pair of old values, and replacing a `contains` property gave the old node's own triples times every triple of its owned subtree. They are now one OPTIONAL over a UNION of those blocks, which deletes and inserts the same triples. Old values a computed field's BIND reads stay joined to it, as before. `deleteAll` and `deleteWhere` likewise join the root's triples and each blank-node subtree with a UNION instead of nested OPTIONALs.
|
|
8
|
+
|
|
9
|
+
Also fixes a data-loss bug: replacing a `contains` property that had no old value deleted the owned subtree of every other node in the graph, because the cascade block's `?old` variable was left unbound. Each cascade block now binds it through the owning edge.
|
|
10
|
+
|
|
3
11
|
## 2.28.2
|
|
4
12
|
|
|
5
13
|
### Patch Changes
|
|
@@ -27,8 +27,11 @@ export declare function upsertToAlgebra(query: IRUpsertMutation, options?: Sparq
|
|
|
27
27
|
* Build DELETE patterns + WHERE OPTIONAL blocks that cascade-delete the owned subtree
|
|
28
28
|
* reachable from `rootTerm` via `contains` edges. `varPrefix` keeps generated variables
|
|
29
29
|
* unique across multiple roots in one query. Returns empty when nothing is owned.
|
|
30
|
+
*
|
|
31
|
+
* `anchor`, when given, leads every block: the triple that binds a variable root, so
|
|
32
|
+
* each block stays correct on its own (as a UNION branch) when no such root exists.
|
|
30
33
|
*/
|
|
31
|
-
export declare function buildOwnedCascade(rootTerm: SparqlTerm, varPrefix: string): {
|
|
34
|
+
export declare function buildOwnedCascade(rootTerm: SparqlTerm, varPrefix: string, anchor?: SparqlTriple): {
|
|
32
35
|
deletePatterns: SparqlTriple[];
|
|
33
36
|
whereOptionals: SparqlAlgebraNode[];
|
|
34
37
|
};
|
|
@@ -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
|
-
|
|
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((
|
|
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 = (
|
|
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
|
-
|
|
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
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
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
|
-
//
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
-
|
|
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),
|
|
@@ -1936,14 +2004,10 @@ export function deleteToAlgebra(query, _options) {
|
|
|
1936
2004
|
blocks.push(...cascade.whereOptionals);
|
|
1937
2005
|
}
|
|
1938
2006
|
}
|
|
1939
|
-
let union = blocks[0];
|
|
1940
|
-
for (let i = 1; i < blocks.length; i++) {
|
|
1941
|
-
union = { type: 'union', left: union, right: blocks[i] };
|
|
1942
|
-
}
|
|
1943
2007
|
const whereAlgebra = {
|
|
1944
2008
|
type: 'join',
|
|
1945
2009
|
left: { type: 'bgp', triples: guards },
|
|
1946
|
-
right:
|
|
2010
|
+
right: unionOf(blocks),
|
|
1947
2011
|
};
|
|
1948
2012
|
return {
|
|
1949
2013
|
type: 'delete_insert',
|
|
@@ -1968,63 +2032,63 @@ function isBlankNodeProperty(prop) {
|
|
|
1968
2032
|
return nk === shacl.BlankNode.id || nk === shacl.BlankNodeOrIRI.id;
|
|
1969
2033
|
}
|
|
1970
2034
|
/**
|
|
1971
|
-
*
|
|
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)).
|
|
1972
2038
|
*
|
|
1973
|
-
*
|
|
1974
|
-
* -
|
|
1975
|
-
*
|
|
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.
|
|
1976
2042
|
*
|
|
1977
|
-
*
|
|
1978
|
-
*
|
|
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.
|
|
1979
2046
|
*/
|
|
1980
|
-
function
|
|
2047
|
+
function blankNodeBlocks(shapeId, parentVar, path, counter, deletePatterns, blocks) {
|
|
1981
2048
|
var _a, _b, _c;
|
|
1982
2049
|
const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
|
|
1983
2050
|
if (!nodeShape)
|
|
1984
|
-
return
|
|
1985
|
-
let optionals = null;
|
|
2051
|
+
return;
|
|
1986
2052
|
const props = getPropertyShapes(nodeShape, true);
|
|
1987
2053
|
for (const prop of props) {
|
|
1988
2054
|
if (!isBlankNodeProperty(prop))
|
|
1989
2055
|
continue;
|
|
1990
|
-
const
|
|
1991
|
-
const
|
|
1992
|
-
const
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
name: 'isBlank',
|
|
2001
|
-
args: [{ kind: 'variable_expr', name: bnVar }],
|
|
2002
|
-
};
|
|
2003
|
-
// ?bnVar ?pN ?oN
|
|
2004
|
-
const wildcardTriple = tripleOf(varTerm(bnVar), varTerm(pVar), varTerm(oVar));
|
|
2005
|
-
// Build inner pattern: traverse + filter + wildcard
|
|
2006
|
-
let innerPattern = {
|
|
2007
|
-
type: 'bgp',
|
|
2008
|
-
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],
|
|
2009
2066
|
};
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
// Wrap in OPTIONAL (left_join)
|
|
2019
|
-
if (optionals) {
|
|
2020
|
-
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
|
+
};
|
|
2021
2074
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
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);
|
|
2024
2078
|
}
|
|
2025
|
-
depth++;
|
|
2026
2079
|
}
|
|
2027
|
-
|
|
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]);
|
|
2028
2092
|
}
|
|
2029
2093
|
/**
|
|
2030
2094
|
* Converts an IRDeleteAllMutation to a SparqlDeleteInsertPlan.
|
|
@@ -2038,15 +2102,13 @@ export function deleteAllToAlgebra(query, _options) {
|
|
|
2038
2102
|
const deletePatterns = [
|
|
2039
2103
|
tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o')),
|
|
2040
2104
|
];
|
|
2041
|
-
// WHERE: type triple
|
|
2105
|
+
// WHERE: type triple, then the root wildcard and the blank-node cleanup
|
|
2042
2106
|
const typeTriple = tripleOf(varTerm(subjectVar), iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
|
|
2043
2107
|
const rootWildcard = tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o'));
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
whereAlgebra = { type: 'left_join', left: whereAlgebra, right: blankNodeOptional };
|
|
2049
|
-
}
|
|
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] };
|
|
2050
2112
|
return {
|
|
2051
2113
|
type: 'delete_insert',
|
|
2052
2114
|
deletePatterns,
|
|
@@ -2066,10 +2128,14 @@ export function deleteWhereToAlgebra(query, _options) {
|
|
|
2066
2128
|
const deletePatterns = [
|
|
2067
2129
|
tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o')),
|
|
2068
2130
|
];
|
|
2069
|
-
// WHERE: type triple + root wildcard
|
|
2131
|
+
// WHERE: type triple + the where clause; the root wildcard and blank-node cleanup follow
|
|
2070
2132
|
const typeTriple = tripleOf(varTerm(subjectVar), iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
|
|
2071
2133
|
const rootWildcard = tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o'));
|
|
2072
|
-
|
|
2134
|
+
const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, deletePatterns);
|
|
2135
|
+
let whereAlgebra = {
|
|
2136
|
+
type: 'bgp',
|
|
2137
|
+
triples: union ? [typeTriple] : [typeTriple, rootWildcard],
|
|
2138
|
+
};
|
|
2073
2139
|
// Process where patterns (traversals from the where clause)
|
|
2074
2140
|
const traverseTriples = [];
|
|
2075
2141
|
const optionalPropertyTriples = [];
|
|
@@ -2089,10 +2155,8 @@ export function deleteWhereToAlgebra(query, _options) {
|
|
|
2089
2155
|
// Convert and add filter expression
|
|
2090
2156
|
const filterExpr = convertExpression(query.where, registry, []);
|
|
2091
2157
|
whereAlgebra = { type: 'filter', expression: filterExpr, inner: whereAlgebra };
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
if (blankNodeOptional) {
|
|
2095
|
-
whereAlgebra = { type: 'left_join', left: whereAlgebra, right: blankNodeOptional };
|
|
2158
|
+
if (union) {
|
|
2159
|
+
whereAlgebra = { type: 'join', left: whereAlgebra, right: union };
|
|
2096
2160
|
}
|
|
2097
2161
|
return {
|
|
2098
2162
|
type: 'delete_insert',
|
|
@@ -2109,7 +2173,6 @@ export function deleteWhereToAlgebra(query, _options) {
|
|
|
2109
2173
|
* filter conditions from the where clause.
|
|
2110
2174
|
*/
|
|
2111
2175
|
export function updateWhereToAlgebra(query, options) {
|
|
2112
|
-
var _a, _b, _c;
|
|
2113
2176
|
const subjectTerm = varTerm('a0');
|
|
2114
2177
|
const result = processUpdateFields(query.data, subjectTerm, options);
|
|
2115
2178
|
// WHERE: type triple is always required
|
|
@@ -2134,62 +2197,11 @@ export function updateWhereToAlgebra(query, options) {
|
|
|
2134
2197
|
whereAlgebra = { type: 'filter', expression: filterExpr, inner: whereAlgebra };
|
|
2135
2198
|
}
|
|
2136
2199
|
// Old-value triples anchored on a traversal *target* belong INSIDE that
|
|
2137
|
-
// traversal's OPTIONAL group, not beside it
|
|
2138
|
-
//
|
|
2139
|
-
//
|
|
2140
|
-
//
|
|
2141
|
-
|
|
2142
|
-
// removes rows. `updateToAlgebra` performs the same split; this path did not.
|
|
2143
|
-
const travTos = new Set(((_a = query.traversalPatterns) !== null && _a !== void 0 ? _a : []).map((t) => t.to));
|
|
2144
|
-
const travAnchoredByTo = new Map();
|
|
2145
|
-
const subjectAnchored = [];
|
|
2146
|
-
for (const triple of result.oldValueTriples) {
|
|
2147
|
-
if (triple.subject.kind === 'variable' && travTos.has(triple.subject.name)) {
|
|
2148
|
-
const list = (_b = travAnchoredByTo.get(triple.subject.name)) !== null && _b !== void 0 ? _b : [];
|
|
2149
|
-
list.push(triple);
|
|
2150
|
-
travAnchoredByTo.set(triple.subject.name, list);
|
|
2151
|
-
}
|
|
2152
|
-
else {
|
|
2153
|
-
subjectAnchored.push(triple);
|
|
2154
|
-
}
|
|
2155
|
-
}
|
|
2156
|
-
whereAlgebra = wrapOldValueOptionals(whereAlgebra, subjectAnchored);
|
|
2157
|
-
// Add traversal OPTIONAL patterns (for multi-segment expression refs)
|
|
2158
|
-
// These must come BEFORE expression BINDs since the BINDs reference traversal variables.
|
|
2159
|
-
if (query.traversalPatterns) {
|
|
2160
|
-
for (const trav of query.traversalPatterns) {
|
|
2161
|
-
const fromTerm = trav.from === '__mutation_subject__' ? varTerm('a0') : varTerm(trav.from);
|
|
2162
|
-
const traversalTriple = tripleOf(fromTerm,
|
|
2163
|
-
// The declared `sh:path`, like every other predicate — not the property
|
|
2164
|
-
// shape's own IRI, which identifies the description of the property.
|
|
2165
|
-
resolvePropertyPredicateTerm(trav.property), varTerm(trav.to));
|
|
2166
|
-
// The edge binds the target variable; each dependent leaf property is a
|
|
2167
|
-
// nested OPTIONAL within that scope, so a missing leaf does not drop the
|
|
2168
|
-
// group while an absent edge leaves every leaf variable unbound.
|
|
2169
|
-
let travNode = { type: 'bgp', triples: [traversalTriple] };
|
|
2170
|
-
for (const leaf of (_c = travAnchoredByTo.get(trav.to)) !== null && _c !== void 0 ? _c : []) {
|
|
2171
|
-
travNode = {
|
|
2172
|
-
type: 'left_join',
|
|
2173
|
-
left: travNode,
|
|
2174
|
-
right: { type: 'bgp', triples: [leaf] },
|
|
2175
|
-
};
|
|
2176
|
-
}
|
|
2177
|
-
whereAlgebra = { type: 'left_join', left: whereAlgebra, right: travNode };
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
// Owned-subtree cascade OPTIONALs for replaced `contains` properties.
|
|
2181
|
-
for (const optional of result.cascadeOptionals) {
|
|
2182
|
-
whereAlgebra = { type: 'left_join', left: whereAlgebra, right: optional };
|
|
2183
|
-
}
|
|
2184
|
-
// Add BIND expressions for computed fields
|
|
2185
|
-
for (const ext of result.extends) {
|
|
2186
|
-
whereAlgebra = {
|
|
2187
|
-
type: 'extend',
|
|
2188
|
-
inner: whereAlgebra,
|
|
2189
|
-
variable: ext.variable,
|
|
2190
|
-
expression: ext.expression,
|
|
2191
|
-
};
|
|
2192
|
-
}
|
|
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);
|
|
2193
2205
|
return {
|
|
2194
2206
|
type: 'delete_insert',
|
|
2195
2207
|
deletePatterns: result.deletePatterns,
|