@_linked/core 2.28.4 → 2.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/README.md +113 -1
- package/lib/esm/index.d.ts +1 -0
- package/lib/esm/index.js +1 -0
- package/lib/esm/index.js.map +1 -1
- package/lib/esm/ontologies/linked-core.d.ts +2 -0
- package/lib/esm/ontologies/linked-core.js +10 -0
- package/lib/esm/ontologies/linked-core.js.map +1 -1
- package/lib/esm/ontologies/shacl.d.ts +1 -0
- package/lib/esm/ontologies/shacl.js +2 -0
- package/lib/esm/ontologies/shacl.js.map +1 -1
- package/lib/esm/shapes/SHACL.d.ts +15 -2
- package/lib/esm/shapes/SHACL.js.map +1 -1
- package/lib/esm/shapes/singleOwnerConstraint.d.ts +27 -0
- package/lib/esm/shapes/singleOwnerConstraint.js +89 -0
- package/lib/esm/shapes/singleOwnerConstraint.js.map +1 -0
- package/lib/esm/shapes/syncShapes.js +48 -9
- package/lib/esm/shapes/syncShapes.js.map +1 -1
- package/lib/esm/sparql/SparqlDataset.d.ts +9 -1
- package/lib/esm/sparql/SparqlDataset.js +19 -0
- package/lib/esm/sparql/SparqlDataset.js.map +1 -1
- package/lib/esm/sparql/irToAlgebra.d.ts +34 -6
- package/lib/esm/sparql/irToAlgebra.js +271 -42
- package/lib/esm/sparql/irToAlgebra.js.map +1 -1
- package/lib/esm/sparql/ownedLinkCheck.d.ts +50 -0
- package/lib/esm/sparql/ownedLinkCheck.js +142 -0
- package/lib/esm/sparql/ownedLinkCheck.js.map +1 -0
- package/lib/esm/utils/Package.js +8 -0
- package/lib/esm/utils/Package.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { rdf } from '../ontologies/rdf.js';
|
|
|
6
6
|
import { shacl } from '../ontologies/shacl.js';
|
|
7
7
|
import { xsd } from '../ontologies/xsd.js';
|
|
8
8
|
import { getSimplePathId } from '../paths/normalizePropertyPath.js';
|
|
9
|
-
import { getAllNodeShapes, getNodeShape, getRegistryVersion, getShapeClass, getTargetClassId, } from '../utils/ShapeClass.js';
|
|
9
|
+
import { getAllNodeShapes, getNodeShape, getRegistryVersion, getShapeClass, getSubShapes, getTargetClassId, } from '../utils/ShapeClass.js';
|
|
10
10
|
import { UnresolvedContextError } from '../queries/QueryContext.js';
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
12
|
// Constants
|
|
@@ -1486,11 +1486,31 @@ export function createToAlgebra(query, options) {
|
|
|
1486
1486
|
// ---------------------------------------------------------------------------
|
|
1487
1487
|
// Shared update field processing
|
|
1488
1488
|
// ---------------------------------------------------------------------------
|
|
1489
|
+
/** A bare `{id}` reference: a link to an existing node, not a nested create. */
|
|
1490
|
+
export function isBareNodeReference(value) {
|
|
1491
|
+
return (!!value &&
|
|
1492
|
+
typeof value === 'object' &&
|
|
1493
|
+
!Array.isArray(value) &&
|
|
1494
|
+
!(value instanceof Date) &&
|
|
1495
|
+
typeof value.id === 'string' &&
|
|
1496
|
+
!('shape' in value) &&
|
|
1497
|
+
!('fields' in value));
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* The values a `contains` replace keeps: the bare `{id}`s of the new single value or of
|
|
1501
|
+
* the overwrite array. A nested create that carries an id is not kept — it rewrites that
|
|
1502
|
+
* node completely, as it always has.
|
|
1503
|
+
*/
|
|
1504
|
+
function keptReferences(value) {
|
|
1505
|
+
const items = Array.isArray(value) ? value : [value];
|
|
1506
|
+
return items.filter(isBareNodeReference).map((ref) => iriTerm(ref.id));
|
|
1507
|
+
}
|
|
1489
1508
|
/**
|
|
1490
1509
|
* Processes IRNodeData fields into DELETE/INSERT/WHERE triples.
|
|
1491
1510
|
* Shared between updateToAlgebra (IRI subject) and updateWhereToAlgebra (variable subject).
|
|
1492
1511
|
*/
|
|
1493
1512
|
function processUpdateFields(data, subjectTerm, options) {
|
|
1513
|
+
var _a, _b;
|
|
1494
1514
|
const deletePatterns = [];
|
|
1495
1515
|
const insertPatterns = [];
|
|
1496
1516
|
const oldValueTriples = [];
|
|
@@ -1505,6 +1525,10 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1505
1525
|
const propertyTerm = resolvePropertyPredicateTerm(field.property);
|
|
1506
1526
|
const suffix = propertySuffix(field.property);
|
|
1507
1527
|
const isContainsField = propertyTerm.kind === 'iri' && updContainsPreds.includes(propertyTerm.value);
|
|
1528
|
+
// Only a `contains` property that declares a value shape deletes the old value's own
|
|
1529
|
+
// triples. Without one (a polymorphic slot like `sh:path`) the value may be a shared
|
|
1530
|
+
// IRI, so it is left to the cascade, which deletes it only when it is dependent-typed.
|
|
1531
|
+
const hasValueShape = isContainsField && !!((_a = findPropertyShapeById(field.property)) === null || _a === void 0 ? void 0 : _a.valueShape);
|
|
1508
1532
|
// Check for set modification ({add, remove})
|
|
1509
1533
|
if (field.value &&
|
|
1510
1534
|
typeof field.value === 'object' &&
|
|
@@ -1515,17 +1539,26 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1515
1539
|
('add' in field.value || 'remove' in field.value)) {
|
|
1516
1540
|
const setMod = field.value;
|
|
1517
1541
|
if (setMod.remove) {
|
|
1542
|
+
// A value that is removed and added back in the same modification stays: its edge
|
|
1543
|
+
// is deleted and re-inserted (a no-op), and the node itself is not touched.
|
|
1544
|
+
const readdedIds = new Set(((_b = setMod.add) !== null && _b !== void 0 ? _b : []).filter(isBareNodeReference).map((ref) => ref.id));
|
|
1518
1545
|
setMod.remove.forEach((removeItem, ri) => {
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1546
|
+
const removeId = removeItem.id;
|
|
1547
|
+
const removeTerm = iriTerm(removeId);
|
|
1548
|
+
const edge = tripleOf(subjectTerm, propertyTerm, removeTerm);
|
|
1549
|
+
deletePatterns.push(edge);
|
|
1550
|
+
oldValueTriples.push(edge);
|
|
1522
1551
|
// For a `contains` set, removing a value deletes the removed node itself
|
|
1523
1552
|
// (its own triples) and its owned subtree — exclusive ownership via the edge.
|
|
1524
|
-
if (isContainsField) {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1553
|
+
if (isContainsField && !readdedIds.has(removeId)) {
|
|
1554
|
+
if (hasValueShape) {
|
|
1555
|
+
const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, removeTerm, `ucr_${suffix}_${ri}_`);
|
|
1556
|
+
deletePatterns.push(...self.deletePatterns);
|
|
1557
|
+
cascadeOptionals.push(...self.whereOptionals);
|
|
1558
|
+
}
|
|
1559
|
+
// The edge leads the cascade too: with a zero-or-more path the removed node is
|
|
1560
|
+
// itself a match, and it may only be deleted while it is still owned here.
|
|
1561
|
+
const cascade = buildOwnedCascade(removeTerm, `ucr_${suffix}_${ri}_`, edge);
|
|
1529
1562
|
deletePatterns.push(...cascade.deletePatterns);
|
|
1530
1563
|
cascadeOptionals.push(...cascade.whereOptionals);
|
|
1531
1564
|
}
|
|
@@ -1551,7 +1584,12 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1551
1584
|
// Non-set-modification replace of a `contains` property: cascade the old value's
|
|
1552
1585
|
// subtree (every branch below binds `old_${suffix}` in the WHERE).
|
|
1553
1586
|
if (isContainsField) {
|
|
1554
|
-
containsOldVars.push({
|
|
1587
|
+
containsOldVars.push({
|
|
1588
|
+
oldVar: `old_${suffix}`,
|
|
1589
|
+
propertyTerm,
|
|
1590
|
+
keep: keptReferences(field.value),
|
|
1591
|
+
hasValueShape,
|
|
1592
|
+
});
|
|
1555
1593
|
}
|
|
1556
1594
|
// Unset (undefined/null) — delete only
|
|
1557
1595
|
if (field.value === undefined || field.value === null) {
|
|
@@ -1636,15 +1674,19 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1636
1674
|
}
|
|
1637
1675
|
// For each replaced `contains` property: delete the old value's own triples
|
|
1638
1676
|
// (exclusive ownership via the edge — backlog 032) then cascade any owned descendants.
|
|
1639
|
-
|
|
1677
|
+
// Values re-sent as a bare `{id}` are kept: their blocks are filtered, so re-sending the
|
|
1678
|
+
// current value is a no-op (the edge is deleted and re-inserted).
|
|
1679
|
+
containsOldVars.forEach(({ oldVar, propertyTerm, keep, hasValueShape }, n) => {
|
|
1640
1680
|
const oldTerm = varTerm(oldVar);
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1681
|
+
if (hasValueShape) {
|
|
1682
|
+
const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, `uc${n}_`, keep);
|
|
1683
|
+
deletePatterns.push(...self.deletePatterns);
|
|
1684
|
+
cascadeOptionals.push(...self.whereOptionals);
|
|
1685
|
+
}
|
|
1644
1686
|
// Every cascade block binds `?old` itself through the owning edge, like the
|
|
1645
1687
|
// self-delete block does. Without it an absent old value leaves `?old` unbound and
|
|
1646
1688
|
// the block matches the owned subtree of EVERY node in the graph.
|
|
1647
|
-
const cascade = buildOwnedCascade(oldTerm, `uc${n}_`, tripleOf(subjectTerm, propertyTerm, oldTerm));
|
|
1689
|
+
const cascade = buildOwnedCascade(oldTerm, `uc${n}_`, tripleOf(subjectTerm, propertyTerm, oldTerm), keep);
|
|
1648
1690
|
deletePatterns.push(...cascade.deletePatterns);
|
|
1649
1691
|
cascadeOptionals.push(...cascade.whereOptionals);
|
|
1650
1692
|
});
|
|
@@ -1846,11 +1888,37 @@ function buildUpdateWhere(base, result, traversalPatterns, subjectTerm) {
|
|
|
1846
1888
|
// - a property marked `contains` → the cascade FOLLOWS that edge,
|
|
1847
1889
|
// - a shape marked `dependent` → its instances may be DELETED when reached.
|
|
1848
1890
|
// Both are read from the live registry, so no vocabulary is hardcoded. From a root
|
|
1849
|
-
// node we follow `(c1|c2|…)
|
|
1850
|
-
//
|
|
1851
|
-
//
|
|
1852
|
-
// `sh:path`) and `rdf:nil` (never
|
|
1891
|
+
// node we follow `(c1|c2|…)*` over all contains predicates (zero hops included, so a
|
|
1892
|
+
// dependent-typed root is itself a match) and, for each dependent targetClass,
|
|
1893
|
+
// wildcard-delete the reached typed nodes. Requiring an asserted `?owned a <dependentType>`
|
|
1894
|
+
// naturally excludes shared predicate IRIs (e.g. a simple `sh:path`) and `rdf:nil` (never
|
|
1895
|
+
// typed), so only owned structural nodes are removed.
|
|
1896
|
+
//
|
|
1897
|
+
// Whether a field owns its value is decided per predicate across the registry
|
|
1898
|
+
// (`collectContainment`): the replace and set-remove deletes, and the class check in
|
|
1899
|
+
// `ownedLinkCheck.ts`, apply that same rule. The subject deletes (by id and bulk) use the
|
|
1900
|
+
// deleted shape's own `contains` properties with a value shape for the immediate children.
|
|
1853
1901
|
// ---------------------------------------------------------------------------
|
|
1902
|
+
/**
|
|
1903
|
+
* The simple-path predicates of the shape's own and inherited `contains` properties that
|
|
1904
|
+
* declare a value shape, deduplicated in declaration order. These are the properties
|
|
1905
|
+
* whose immediate values a subject delete removes.
|
|
1906
|
+
*/
|
|
1907
|
+
function ownedValueShapePredicates(shapeId) {
|
|
1908
|
+
var _a, _b;
|
|
1909
|
+
const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
|
|
1910
|
+
if (!nodeShape)
|
|
1911
|
+
return [];
|
|
1912
|
+
const predicates = new Set();
|
|
1913
|
+
for (const ps of getPropertyShapes(nodeShape, true)) {
|
|
1914
|
+
if (!ps.contains || !ps.valueShape)
|
|
1915
|
+
continue;
|
|
1916
|
+
const predicate = getSimplePathId(ps.path);
|
|
1917
|
+
if (predicate)
|
|
1918
|
+
predicates.add(predicate);
|
|
1919
|
+
}
|
|
1920
|
+
return [...predicates];
|
|
1921
|
+
}
|
|
1854
1922
|
/** True when the shape (or an ancestor) declares at least one `contains` property. */
|
|
1855
1923
|
function shapeHasContainsProperty(shapeId) {
|
|
1856
1924
|
var _a, _b;
|
|
@@ -1866,7 +1934,7 @@ function shapeHasContainsProperty(shapeId) {
|
|
|
1866
1934
|
let containmentCache = null;
|
|
1867
1935
|
let containmentCacheSize = -1;
|
|
1868
1936
|
/** Gather contains-predicate IRIs and dependent targetClass IRIs from the registry. */
|
|
1869
|
-
function collectContainment() {
|
|
1937
|
+
export function collectContainment() {
|
|
1870
1938
|
var _a;
|
|
1871
1939
|
const version = getRegistryVersion();
|
|
1872
1940
|
if (containmentCache && version === containmentCacheSize) {
|
|
@@ -1893,6 +1961,78 @@ function collectContainment() {
|
|
|
1893
1961
|
containmentCacheSize = version;
|
|
1894
1962
|
return containmentCache;
|
|
1895
1963
|
}
|
|
1964
|
+
let reachableCache = new Map();
|
|
1965
|
+
let reachableCacheVersion = -1;
|
|
1966
|
+
/**
|
|
1967
|
+
* The dependent targetClasses a subject delete of `shapeId` can reach through `contains`
|
|
1968
|
+
* edges, in `collectContainment` order. A subject delete emits one cascade block per type it
|
|
1969
|
+
* returns, and each block is a property-path closure per subject, so a shape that owns only
|
|
1970
|
+
* non-dependent values (every `Thing.image` owner) pays for none.
|
|
1971
|
+
*
|
|
1972
|
+
* The walk starts at the shape and its registered sub-shapes and follows every flattened
|
|
1973
|
+
* property on a `contains` predicate (the registry-wide predicate rule the cascade path uses)
|
|
1974
|
+
* to its value shape and that shape's sub-shapes. A property on a `contains` predicate with no
|
|
1975
|
+
* value shape is polymorphic (`sh:path`) and may reach anything, so it yields every dependent
|
|
1976
|
+
* type; that keeps the shape-sync delete of a NodeShape exact.
|
|
1977
|
+
*/
|
|
1978
|
+
function reachableDependentTypes(shapeId) {
|
|
1979
|
+
var _a, _b;
|
|
1980
|
+
const version = getRegistryVersion();
|
|
1981
|
+
if (version !== reachableCacheVersion) {
|
|
1982
|
+
reachableCache = new Map();
|
|
1983
|
+
reachableCacheVersion = version;
|
|
1984
|
+
}
|
|
1985
|
+
const cached = reachableCache.get(shapeId);
|
|
1986
|
+
if (cached)
|
|
1987
|
+
return cached;
|
|
1988
|
+
const { containsPreds, dependentTypes } = collectContainment();
|
|
1989
|
+
const shapeOf = (id) => { var _a, _b; return (_b = (_a = getShapeClass(id)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(id); };
|
|
1990
|
+
const reached = new Set();
|
|
1991
|
+
// Shapes whose instances an owned edge can lead to. The start shapes join only when an
|
|
1992
|
+
// edge leads back to them (a dependent shape that owns its own kind, like a list cell).
|
|
1993
|
+
const owned = new Set();
|
|
1994
|
+
const seen = new Set();
|
|
1995
|
+
const queue = [];
|
|
1996
|
+
const visit = (id, viaEdge) => {
|
|
1997
|
+
for (const sid of [id, ...getSubShapes(id).map((sub) => sub.id)]) {
|
|
1998
|
+
if (viaEdge)
|
|
1999
|
+
owned.add(sid);
|
|
2000
|
+
if (seen.has(sid))
|
|
2001
|
+
continue;
|
|
2002
|
+
seen.add(sid);
|
|
2003
|
+
queue.push(sid);
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
visit(shapeId, false);
|
|
2007
|
+
let everything = false;
|
|
2008
|
+
while (queue.length > 0 && !everything) {
|
|
2009
|
+
const nodeShape = shapeOf(queue.shift());
|
|
2010
|
+
if (!nodeShape)
|
|
2011
|
+
continue;
|
|
2012
|
+
for (const ps of getPropertyShapes(nodeShape, true)) {
|
|
2013
|
+
const predicate = getSimplePathId(ps.path);
|
|
2014
|
+
if (!predicate || !containsPreds.includes(predicate))
|
|
2015
|
+
continue;
|
|
2016
|
+
if (!((_a = ps.valueShape) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
2017
|
+
everything = true;
|
|
2018
|
+
break;
|
|
2019
|
+
}
|
|
2020
|
+
visit(ps.valueShape.id, true);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
if (!everything) {
|
|
2024
|
+
for (const id of owned) {
|
|
2025
|
+
const nodeShape = shapeOf(id);
|
|
2026
|
+
const dependent = nodeShape === null || nodeShape === void 0 ? void 0 : nodeShape.dependent;
|
|
2027
|
+
if (dependent && ((_b = nodeShape === null || nodeShape === void 0 ? void 0 : nodeShape.targetClass) === null || _b === void 0 ? void 0 : _b.id)) {
|
|
2028
|
+
reached.add(nodeShape.targetClass.id);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
const result = everything ? dependentTypes : dependentTypes.filter((t) => reached.has(t));
|
|
2033
|
+
reachableCache.set(shapeId, result);
|
|
2034
|
+
return result;
|
|
2035
|
+
}
|
|
1896
2036
|
/**
|
|
1897
2037
|
* Build DELETE patterns + WHERE OPTIONAL blocks that cascade-delete the owned subtree
|
|
1898
2038
|
* reachable from `rootTerm` via `contains` edges. `varPrefix` keeps generated variables
|
|
@@ -1900,16 +2040,24 @@ function collectContainment() {
|
|
|
1900
2040
|
*
|
|
1901
2041
|
* `anchor`, when given, leads every block: the triple that binds a variable root, so
|
|
1902
2042
|
* each block stays correct on its own (as a UNION branch) when no such root exists.
|
|
2043
|
+
* `onlyTypes`, when given, restricts the blocks to those dependent types (a subject delete
|
|
2044
|
+
* passes the types its shape can reach); by default every registered dependent type gets one.
|
|
1903
2045
|
*/
|
|
1904
|
-
export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
|
|
1905
|
-
const
|
|
2046
|
+
export function buildOwnedCascade(rootTerm, varPrefix, anchor, keep, onlyTypes) {
|
|
2047
|
+
const containment = collectContainment();
|
|
2048
|
+
const { containsPreds } = containment;
|
|
2049
|
+
const dependentTypes = onlyTypes !== null && onlyTypes !== void 0 ? onlyTypes : containment.dependentTypes;
|
|
1906
2050
|
if (containsPreds.length === 0 || dependentTypes.length === 0) {
|
|
1907
2051
|
return { deletePatterns: [], whereOptionals: [] };
|
|
1908
2052
|
}
|
|
1909
|
-
// (c1|c2|…)
|
|
2053
|
+
// (c1|c2|…)* — zero or more contains edges, so the root itself is matched when it is
|
|
2054
|
+
// dependent-typed. That is what cleans up the value of a `contains` property that has
|
|
2055
|
+
// no value shape (a polymorphic slot like `sh:path`): it is deleted only when it is a
|
|
2056
|
+
// dependent node, never when it is a shared IRI. On a subject delete `*` also matches
|
|
2057
|
+
// the subject, which is being deleted anyway.
|
|
1910
2058
|
const pathExpr = containsPreds.length === 1
|
|
1911
|
-
? {
|
|
1912
|
-
: {
|
|
2059
|
+
? { zeroOrMore: { id: containsPreds[0] } }
|
|
2060
|
+
: { zeroOrMore: { alt: containsPreds.map((id) => ({ id })) } };
|
|
1913
2061
|
const pathTerm = {
|
|
1914
2062
|
kind: 'path',
|
|
1915
2063
|
value: pathExprToSparql(pathExpr),
|
|
@@ -1922,7 +2070,7 @@ export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
|
|
|
1922
2070
|
const p = varTerm(`${varPrefix}op${i}`);
|
|
1923
2071
|
const o = varTerm(`${varPrefix}ov${i}`);
|
|
1924
2072
|
deletePatterns.push(tripleOf(owned, p, o));
|
|
1925
|
-
whereOptionals.push({
|
|
2073
|
+
whereOptionals.push(keepFilter({
|
|
1926
2074
|
type: 'bgp',
|
|
1927
2075
|
triples: [
|
|
1928
2076
|
...(anchor ? [anchor] : []),
|
|
@@ -1930,10 +2078,32 @@ export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
|
|
|
1930
2078
|
tripleOf(owned, iriTerm(RDF_TYPE), iriTerm(typeId)),
|
|
1931
2079
|
tripleOf(owned, p, o),
|
|
1932
2080
|
],
|
|
1933
|
-
});
|
|
2081
|
+
}, rootTerm, keep));
|
|
1934
2082
|
});
|
|
1935
2083
|
return { deletePatterns, whereOptionals };
|
|
1936
2084
|
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Wrap an owned-cleanup block in `FILTER(?old NOT IN (<k1>, …))` so a value the mutation
|
|
2087
|
+
* keeps (re-sent as a bare `{id}`) is not deleted. The filter sits inside the block, next
|
|
2088
|
+
* to the edge triple that binds `?old`, so it only removes that block's matches; the
|
|
2089
|
+
* edge-only delete of the same value stays and is undone by the INSERT. With an IRI old
|
|
2090
|
+
* term, or nothing to keep, the block is returned unchanged.
|
|
2091
|
+
*/
|
|
2092
|
+
function keepFilter(block, oldTerm, keep) {
|
|
2093
|
+
const kept = (keep !== null && keep !== void 0 ? keep : []).flatMap((term) => term.kind === 'iri' ? [{ kind: 'iri_expr', value: term.value }] : []);
|
|
2094
|
+
if (oldTerm.kind !== 'variable' || kept.length === 0)
|
|
2095
|
+
return block;
|
|
2096
|
+
return {
|
|
2097
|
+
type: 'filter',
|
|
2098
|
+
expression: {
|
|
2099
|
+
kind: 'in_expr',
|
|
2100
|
+
negated: true,
|
|
2101
|
+
value: { kind: 'variable_expr', name: oldTerm.name },
|
|
2102
|
+
list: kept,
|
|
2103
|
+
},
|
|
2104
|
+
inner: block,
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
1937
2107
|
/**
|
|
1938
2108
|
* Build DELETE + WHERE OPTIONAL that removes the *old node's own* one-hop triples when a
|
|
1939
2109
|
* `contains` edge is replaced, unset, or set-removed. A `contains` edge asserts exclusive
|
|
@@ -1942,22 +2112,31 @@ export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
|
|
|
1942
2112
|
* does NOT require the child shape to be `dependent`: ownership via the edge is sufficient.
|
|
1943
2113
|
* Owned descendants *below* the old node are cascaded separately by `buildOwnedCascade`.
|
|
1944
2114
|
*
|
|
2115
|
+
* Callers use it only for a property that declares a value shape. A shape-less `contains`
|
|
2116
|
+
* slot (like `sh:path`) may hold a shared IRI, so its old value is left to
|
|
2117
|
+
* `buildOwnedCascade`, which deletes it only when it is dependent-typed.
|
|
2118
|
+
*
|
|
1945
2119
|
* The owning edge `<subject> <prop> ?old` is re-asserted INSIDE the WHERE group so `?old`
|
|
1946
2120
|
* is bound there: when no old value exists the group matches nothing and the DELETE is a
|
|
1947
2121
|
* no-op, rather than an unbound `?old` matching (and deleting) the whole graph. When
|
|
1948
2122
|
* `oldTerm` is a concrete IRI (set-remove), the guard just confirms the link still holds.
|
|
2123
|
+
*
|
|
2124
|
+
* `keep` lists the bare `{id}`s the mutation writes again (a re-sent current value, the
|
|
2125
|
+
* retained ids of an array overwrite). With a variable `oldTerm` the block is wrapped in
|
|
2126
|
+
* `FILTER(?old NOT IN (…))`, so a kept value is not deleted; its edge is deleted and
|
|
2127
|
+
* re-inserted, a no-op. A nested create carrying an id is never kept: it rewrites that node.
|
|
1949
2128
|
*/
|
|
1950
|
-
export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPrefix) {
|
|
2129
|
+
export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPrefix, keep) {
|
|
1951
2130
|
const p = varTerm(`${varPrefix}sp`);
|
|
1952
2131
|
const o = varTerm(`${varPrefix}so`);
|
|
1953
2132
|
const wildcard = tripleOf(oldTerm, p, o);
|
|
1954
2133
|
return {
|
|
1955
2134
|
deletePatterns: [wildcard],
|
|
1956
2135
|
whereOptionals: [
|
|
1957
|
-
{
|
|
2136
|
+
keepFilter({
|
|
1958
2137
|
type: 'bgp',
|
|
1959
2138
|
triples: [tripleOf(subjectTerm, propertyTerm, oldTerm), wildcard],
|
|
1960
|
-
},
|
|
2139
|
+
}, oldTerm, keep),
|
|
1961
2140
|
],
|
|
1962
2141
|
};
|
|
1963
2142
|
}
|
|
@@ -1966,7 +2145,12 @@ export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPref
|
|
|
1966
2145
|
*
|
|
1967
2146
|
* The WHERE is the type guard of every id, joined with ONE UNION of independent
|
|
1968
2147
|
* blocks: per id its own triples, its incoming references and, for a shape that
|
|
1969
|
-
* owns something, one block per
|
|
2148
|
+
* owns something, one block per immediately-owned child (`<s> <p> ?dN . ?dN ?dN_p ?dN_o`,
|
|
2149
|
+
* for each of the shape's own and inherited `contains` properties with a value shape and
|
|
2150
|
+
* a simple path) and one block per dependent type its owned subtree can reach
|
|
2151
|
+
* (`reachableDependentTypes`; none for a shape that owns only non-dependent values). The
|
|
2152
|
+
* bulk deletes (`deleteAllToAlgebra`, `deleteWhereToAlgebra`) add the same owned blocks,
|
|
2153
|
+
* rooted at the subject variable (see `bulkDeleteUnion`).
|
|
1970
2154
|
*
|
|
1971
2155
|
* The blocks must not be chained as OPTIONALs. A chain of OPTIONALs is a join, so
|
|
1972
2156
|
* the solutions multiply: own triples x incoming references x owned triples of each
|
|
@@ -1984,6 +2168,9 @@ export function deleteToAlgebra(query, _options) {
|
|
|
1984
2168
|
const deletePatterns = [];
|
|
1985
2169
|
const guards = [];
|
|
1986
2170
|
const blocks = [];
|
|
2171
|
+
const hasContains = shapeHasContainsProperty(query.shape);
|
|
2172
|
+
const ownedChildPredicates = hasContains ? ownedValueShapePredicates(query.shape) : [];
|
|
2173
|
+
const cascadeTypes = hasContains ? reachableDependentTypes(query.shape) : [];
|
|
1987
2174
|
for (let i = 0; i < query.ids.length; i++) {
|
|
1988
2175
|
const subjectTerm = iriTerm(query.ids[i].id);
|
|
1989
2176
|
const idx = query.ids.length > 1 ? `_${i}` : '';
|
|
@@ -1996,10 +2183,22 @@ export function deleteToAlgebra(query, _options) {
|
|
|
1996
2183
|
// incoming references (the entity may have none) are separate UNION blocks.
|
|
1997
2184
|
guards.push(typeGuard);
|
|
1998
2185
|
blocks.push({ type: 'bgp', triples: [subjWild] }, { type: 'bgp', triples: [objWild] });
|
|
2186
|
+
// The immediately-owned children: for every `contains` property with a value shape,
|
|
2187
|
+
// the child's own one-hop triples, as a replace or a set-remove deletes them. No type
|
|
2188
|
+
// guard and no reference check: the `contains` edge alone asserts ownership.
|
|
2189
|
+
ownedChildPredicates.forEach((predicate, n) => {
|
|
2190
|
+
const child = varTerm(`d${n}${idx}`);
|
|
2191
|
+
const childOwn = tripleOf(child, varTerm(`d${n}${idx}_p`), varTerm(`d${n}${idx}_o`));
|
|
2192
|
+
deletePatterns.push(childOwn);
|
|
2193
|
+
blocks.push({
|
|
2194
|
+
type: 'bgp',
|
|
2195
|
+
triples: [tripleOf(subjectTerm, iriTerm(predicate), child), childOwn],
|
|
2196
|
+
});
|
|
2197
|
+
});
|
|
1999
2198
|
// Cascade-delete the owned subtree — only for shapes that actually own something
|
|
2000
2199
|
// (have a `contains` property); a plain entity delete is left untouched.
|
|
2001
|
-
if (
|
|
2002
|
-
const cascade = buildOwnedCascade(subjectTerm, `c${idx}_
|
|
2200
|
+
if (hasContains && cascadeTypes.length > 0) {
|
|
2201
|
+
const cascade = buildOwnedCascade(subjectTerm, `c${idx}_`, undefined, undefined, cascadeTypes);
|
|
2003
2202
|
deletePatterns.push(...cascade.deletePatterns);
|
|
2004
2203
|
blocks.push(...cascade.whereOptionals);
|
|
2005
2204
|
}
|
|
@@ -2079,13 +2278,39 @@ function blankNodeBlocks(shapeId, parentVar, path, counter, deletePatterns, bloc
|
|
|
2079
2278
|
}
|
|
2080
2279
|
}
|
|
2081
2280
|
/**
|
|
2082
|
-
* The
|
|
2083
|
-
*
|
|
2084
|
-
*
|
|
2281
|
+
* The cleanup blocks of a bulk delete, already joined with the root's own triples as ONE
|
|
2282
|
+
* UNION so their matches add up instead of multiplying; `null` when there is nothing to
|
|
2283
|
+
* clean up (the root wildcard then stays in the required BGP).
|
|
2284
|
+
*
|
|
2285
|
+
* - The blank-node blocks of the shape's blank-node properties.
|
|
2286
|
+
* - For a shape that owns something (a `contains` property), the same owned cleanup as a
|
|
2287
|
+
* delete by id: the immediately-owned children of every `contains` property with a value
|
|
2288
|
+
* shape, and the dependent cascade, rooted at the subject variable. Each owned block is led
|
|
2289
|
+
* by the subject's type triple. With a linear join it is redundant; it keeps a branch that
|
|
2290
|
+
* is evaluated on its own from running `?a0 (c…)* ?own` with both ends unbound, which
|
|
2291
|
+
* matches every node in the graph.
|
|
2085
2292
|
*/
|
|
2086
|
-
function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, deletePatterns) {
|
|
2293
|
+
function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, typeTriple, deletePatterns) {
|
|
2087
2294
|
const blocks = [];
|
|
2088
2295
|
blankNodeBlocks(shapeId, subjectVar, { triples: [], bnVars: [] }, { next: 1 }, deletePatterns, blocks);
|
|
2296
|
+
if (shapeHasContainsProperty(shapeId)) {
|
|
2297
|
+
const subject = varTerm(subjectVar);
|
|
2298
|
+
ownedValueShapePredicates(shapeId).forEach((predicate, n) => {
|
|
2299
|
+
const child = varTerm(`d${n}`);
|
|
2300
|
+
const childOwn = tripleOf(child, varTerm(`d${n}_p`), varTerm(`d${n}_o`));
|
|
2301
|
+
deletePatterns.push(childOwn);
|
|
2302
|
+
blocks.push({
|
|
2303
|
+
type: 'bgp',
|
|
2304
|
+
triples: [typeTriple, tripleOf(subject, iriTerm(predicate), child), childOwn],
|
|
2305
|
+
});
|
|
2306
|
+
});
|
|
2307
|
+
const cascadeTypes = reachableDependentTypes(shapeId);
|
|
2308
|
+
if (cascadeTypes.length > 0) {
|
|
2309
|
+
const cascade = buildOwnedCascade(subject, 'c_', typeTriple, undefined, cascadeTypes);
|
|
2310
|
+
deletePatterns.push(...cascade.deletePatterns);
|
|
2311
|
+
blocks.push(...cascade.whereOptionals);
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2089
2314
|
if (blocks.length === 0)
|
|
2090
2315
|
return null;
|
|
2091
2316
|
return unionOf([{ type: 'bgp', triples: [rootWildcard] }, ...blocks]);
|
|
@@ -2093,8 +2318,10 @@ function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, deletePatterns) {
|
|
|
2093
2318
|
/**
|
|
2094
2319
|
* Converts an IRDeleteAllMutation to a SparqlDeleteInsertPlan.
|
|
2095
2320
|
*
|
|
2096
|
-
* Generates DELETE { ?a0 ?p ?o . [blank
|
|
2097
|
-
* WHERE { ?a0 a <Shape> . ?a0 ?p ?o
|
|
2321
|
+
* Generates DELETE { ?a0 ?p ?o . [blank-node and owned-cleanup wildcards] }
|
|
2322
|
+
* WHERE { ?a0 a <Shape> . { ?a0 ?p ?o } UNION [one block per blank node,
|
|
2323
|
+
* owned child and dependent type] }
|
|
2324
|
+
* (see `bulkDeleteUnion`). Unlike a delete by id, incoming references are not deleted.
|
|
2098
2325
|
*/
|
|
2099
2326
|
export function deleteAllToAlgebra(query, _options) {
|
|
2100
2327
|
const subjectVar = 'a0';
|
|
@@ -2105,7 +2332,7 @@ export function deleteAllToAlgebra(query, _options) {
|
|
|
2105
2332
|
// WHERE: type triple, then the root wildcard and the blank-node cleanup
|
|
2106
2333
|
const typeTriple = tripleOf(varTerm(subjectVar), iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
|
|
2107
2334
|
const rootWildcard = tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o'));
|
|
2108
|
-
const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, deletePatterns);
|
|
2335
|
+
const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, typeTriple, deletePatterns);
|
|
2109
2336
|
const whereAlgebra = union
|
|
2110
2337
|
? { type: 'join', left: { type: 'bgp', triples: [typeTriple] }, right: union }
|
|
2111
2338
|
: { type: 'bgp', triples: [typeTriple, rootWildcard] };
|
|
@@ -2119,7 +2346,9 @@ export function deleteAllToAlgebra(query, _options) {
|
|
|
2119
2346
|
/**
|
|
2120
2347
|
* Converts an IRDeleteWhereMutation to a SparqlDeleteInsertPlan.
|
|
2121
2348
|
*
|
|
2122
|
-
* Like deleteAllToAlgebra but adds filter conditions from the where clause.
|
|
2349
|
+
* Like deleteAllToAlgebra but adds filter conditions from the where clause. The cleanup
|
|
2350
|
+
* UNION (blank nodes, owned children, dependent cascade) is joined with the filtered match,
|
|
2351
|
+
* so only the matched subjects' children are deleted.
|
|
2123
2352
|
*/
|
|
2124
2353
|
export function deleteWhereToAlgebra(query, _options) {
|
|
2125
2354
|
const subjectVar = 'a0';
|
|
@@ -2131,7 +2360,7 @@ export function deleteWhereToAlgebra(query, _options) {
|
|
|
2131
2360
|
// WHERE: type triple + the where clause; the root wildcard and blank-node cleanup follow
|
|
2132
2361
|
const typeTriple = tripleOf(varTerm(subjectVar), iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
|
|
2133
2362
|
const rootWildcard = tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o'));
|
|
2134
|
-
const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, deletePatterns);
|
|
2363
|
+
const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, typeTriple, deletePatterns);
|
|
2135
2364
|
let whereAlgebra = {
|
|
2136
2365
|
type: 'bgp',
|
|
2137
2366
|
triples: union ? [typeTriple] : [typeTriple, rootWildcard],
|