@_linked/core 2.28.4 → 2.30.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 +87 -0
- package/README.md +131 -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/queries/IRMutation.js +1 -1
- package/lib/esm/queries/IRMutation.js.map +1 -1
- package/lib/esm/queries/IntermediateRepresentation.d.ts +5 -0
- package/lib/esm/queries/IntermediateRepresentation.js.map +1 -1
- package/lib/esm/queries/MutationQuery.js +15 -0
- package/lib/esm/queries/MutationQuery.js.map +1 -1
- package/lib/esm/queries/MutationSerialization.d.ts +5 -4
- package/lib/esm/queries/MutationSerialization.js +9 -1
- package/lib/esm/queries/MutationSerialization.js.map +1 -1
- package/lib/esm/queries/QueryFactory.d.ts +6 -0
- package/lib/esm/queries/QueryFactory.js.map +1 -1
- package/lib/esm/queries/SelectQuery.js +3 -1
- package/lib/esm/queries/SelectQuery.js.map +1 -1
- package/lib/esm/queries/lowerMutationJSON.js +6 -1
- package/lib/esm/queries/lowerMutationJSON.js.map +1 -1
- package/lib/esm/shapes/List.d.ts +1 -0
- package/lib/esm/shapes/List.js.map +1 -1
- package/lib/esm/shapes/PathNode.d.ts +5 -2
- package/lib/esm/shapes/PathNode.js +5 -2
- package/lib/esm/shapes/PathNode.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/serializePathToNodeData.d.ts +17 -5
- package/lib/esm/shapes/serializePathToNodeData.js +41 -27
- package/lib/esm/shapes/serializePathToNodeData.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 +49 -10
- package/lib/esm/shapes/syncShapes.js.map +1 -1
- package/lib/esm/shapes/validation.js +2 -2
- package/lib/esm/shapes/validation.js.map +1 -1
- package/lib/esm/sparql/SparqlAlgebra.d.ts +5 -0
- package/lib/esm/sparql/SparqlAlgebra.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/algebraToString.js +2 -0
- package/lib/esm/sparql/algebraToString.js.map +1 -1
- package/lib/esm/sparql/irToAlgebra.d.ts +34 -6
- package/lib/esm/sparql/irToAlgebra.js +384 -59
- 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 +2 -2
|
@@ -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
|
|
@@ -1421,18 +1421,65 @@ function fieldValueToTerms(value, options, datatype) {
|
|
|
1421
1421
|
}
|
|
1422
1422
|
return [];
|
|
1423
1423
|
}
|
|
1424
|
+
/**
|
|
1425
|
+
* The RDF-collection and SHACL-path predicates the cascade's blank-node block walks inside a
|
|
1426
|
+
* blank structure: the syntax core writes as blank nodes (`serializePathToNodeData`).
|
|
1427
|
+
*/
|
|
1428
|
+
const BLANK_SYNTAX_PREDICATES = [
|
|
1429
|
+
rdf.first.id,
|
|
1430
|
+
rdf.rest.id,
|
|
1431
|
+
shacl.inversePath.id,
|
|
1432
|
+
shacl.alternativePath.id,
|
|
1433
|
+
shacl.zeroOrMorePath.id,
|
|
1434
|
+
shacl.oneOrMorePath.id,
|
|
1435
|
+
shacl.zeroOrOnePath.id,
|
|
1436
|
+
];
|
|
1437
|
+
/**
|
|
1438
|
+
* A blank node is removed only by the cascade's blank-node block, which reaches it through a
|
|
1439
|
+
* `contains` edge, or, inside a blank structure, through a list / path syntax predicate
|
|
1440
|
+
* (`rdf:first` of a blank list cell). A blank node held any other way could never be deleted:
|
|
1441
|
+
* refuse to write one.
|
|
1442
|
+
*/
|
|
1443
|
+
function assertBlankIsOwned(data, propertyId, propertyTerm, parentIsBlank) {
|
|
1444
|
+
var _a;
|
|
1445
|
+
if (!data.blank || ((_a = findPropertyShapeById(propertyId)) === null || _a === void 0 ? void 0 : _a.contains))
|
|
1446
|
+
return;
|
|
1447
|
+
const syntaxEdge = parentIsBlank && propertyTerm.kind === 'iri' && BLANK_SYNTAX_PREDICATES.includes(propertyTerm.value);
|
|
1448
|
+
if (syntaxEdge)
|
|
1449
|
+
return;
|
|
1450
|
+
throw new Error(`A blank nested node (__blank) must be the value of a \`contains\` property, or of a list / ` +
|
|
1451
|
+
`path syntax predicate inside a blank node; ${propertyId} is neither, so nothing would ` +
|
|
1452
|
+
`ever delete it.`);
|
|
1453
|
+
}
|
|
1424
1454
|
/**
|
|
1425
1455
|
* Recursively generate triples for an IRNodeData (used in create and nested creates).
|
|
1426
|
-
* Returns the
|
|
1456
|
+
* Returns the term used for this node and all generated triples.
|
|
1457
|
+
*
|
|
1458
|
+
* A node marked `blank` (nested only, see `NodeDescriptionValue.__blank`) becomes a fresh
|
|
1459
|
+
* blank node with its field triples and NO `rdf:type`: it is RDF syntax, like a SHACL path
|
|
1460
|
+
* operator, which the spec defines as a blank node that is the subject of exactly one triple.
|
|
1461
|
+
* Without a `blanks` minter (the update paths) a blank node throws.
|
|
1427
1462
|
*/
|
|
1428
|
-
function generateNodeDataTriples(data, options) {
|
|
1429
|
-
const uri = data.id || generateEntityUri(data.shape, options);
|
|
1463
|
+
function generateNodeDataTriples(data, options, blanks) {
|
|
1430
1464
|
const triples = [];
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1465
|
+
let subjectTerm;
|
|
1466
|
+
if (data.blank) {
|
|
1467
|
+
if (data.id) {
|
|
1468
|
+
throw new Error(`A blank nested node (__blank) cannot carry an id (${data.id}).`);
|
|
1469
|
+
}
|
|
1470
|
+
if (!blanks) {
|
|
1471
|
+
throw new Error('A blank nested node (__blank) can only be written by a create. An update writes its ' +
|
|
1472
|
+
'INSERT once per WHERE solution, which would duplicate the blank node.');
|
|
1473
|
+
}
|
|
1474
|
+
subjectTerm = { kind: 'blank', label: `b${blanks.next++}` };
|
|
1475
|
+
}
|
|
1476
|
+
else {
|
|
1477
|
+
subjectTerm = iriTerm(data.id || generateEntityUri(data.shape, options));
|
|
1478
|
+
// Type triple — resolve the SHACL NodeShapeData id in the IR to the ontology
|
|
1479
|
+
// targetClass URI (sibling of PR #77's SELECT-side resolveShapeScanIri).
|
|
1480
|
+
// Mutations were not covered by #77; see mutation-uri-fidelity.test.ts.
|
|
1481
|
+
triples.push(tripleOf(subjectTerm, iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(data.shape))));
|
|
1482
|
+
}
|
|
1436
1483
|
// Field triples
|
|
1437
1484
|
for (const field of data.fields) {
|
|
1438
1485
|
// Resolve the SHACL PropertyShapeData id to its declared `path` URI.
|
|
@@ -1445,8 +1492,9 @@ function generateNodeDataTriples(data, options) {
|
|
|
1445
1492
|
for (const item of field.value) {
|
|
1446
1493
|
if (item && typeof item === 'object' && 'shape' in item && 'fields' in item) {
|
|
1447
1494
|
// Nested create
|
|
1448
|
-
|
|
1449
|
-
|
|
1495
|
+
assertBlankIsOwned(item, field.property, propertyTerm, !!data.blank);
|
|
1496
|
+
const nested = generateNodeDataTriples(item, options, blanks);
|
|
1497
|
+
triples.push(tripleOf(subjectTerm, propertyTerm, nested.term));
|
|
1450
1498
|
triples.push(...nested.triples);
|
|
1451
1499
|
}
|
|
1452
1500
|
else {
|
|
@@ -1460,8 +1508,9 @@ function generateNodeDataTriples(data, options) {
|
|
|
1460
1508
|
}
|
|
1461
1509
|
// Handle nested IRNodeData
|
|
1462
1510
|
if (typeof field.value === 'object' && 'shape' in field.value && 'fields' in field.value) {
|
|
1463
|
-
|
|
1464
|
-
|
|
1511
|
+
assertBlankIsOwned(field.value, field.property, propertyTerm, !!data.blank);
|
|
1512
|
+
const nested = generateNodeDataTriples(field.value, options, blanks);
|
|
1513
|
+
triples.push(tripleOf(subjectTerm, propertyTerm, nested.term));
|
|
1465
1514
|
triples.push(...nested.triples);
|
|
1466
1515
|
continue;
|
|
1467
1516
|
}
|
|
@@ -1471,13 +1520,16 @@ function generateNodeDataTriples(data, options) {
|
|
|
1471
1520
|
triples.push(tripleOf(subjectTerm, propertyTerm, term));
|
|
1472
1521
|
}
|
|
1473
1522
|
}
|
|
1474
|
-
return {
|
|
1523
|
+
return { term: subjectTerm, triples };
|
|
1475
1524
|
}
|
|
1476
1525
|
/**
|
|
1477
1526
|
* Converts an IRCreateMutation to a SparqlInsertDataPlan.
|
|
1478
1527
|
*/
|
|
1479
1528
|
export function createToAlgebra(query, options) {
|
|
1480
|
-
|
|
1529
|
+
if (query.data.blank) {
|
|
1530
|
+
throw new Error('The root node of a create cannot be blank (__blank): it must be addressable.');
|
|
1531
|
+
}
|
|
1532
|
+
const { triples } = generateNodeDataTriples(query.data, options, { next: 0 });
|
|
1481
1533
|
return {
|
|
1482
1534
|
type: 'insert_data',
|
|
1483
1535
|
triples,
|
|
@@ -1486,11 +1538,31 @@ export function createToAlgebra(query, options) {
|
|
|
1486
1538
|
// ---------------------------------------------------------------------------
|
|
1487
1539
|
// Shared update field processing
|
|
1488
1540
|
// ---------------------------------------------------------------------------
|
|
1541
|
+
/** A bare `{id}` reference: a link to an existing node, not a nested create. */
|
|
1542
|
+
export function isBareNodeReference(value) {
|
|
1543
|
+
return (!!value &&
|
|
1544
|
+
typeof value === 'object' &&
|
|
1545
|
+
!Array.isArray(value) &&
|
|
1546
|
+
!(value instanceof Date) &&
|
|
1547
|
+
typeof value.id === 'string' &&
|
|
1548
|
+
!('shape' in value) &&
|
|
1549
|
+
!('fields' in value));
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* The values a `contains` replace keeps: the bare `{id}`s of the new single value or of
|
|
1553
|
+
* the overwrite array. A nested create that carries an id is not kept — it rewrites that
|
|
1554
|
+
* node completely, as it always has.
|
|
1555
|
+
*/
|
|
1556
|
+
function keptReferences(value) {
|
|
1557
|
+
const items = Array.isArray(value) ? value : [value];
|
|
1558
|
+
return items.filter(isBareNodeReference).map((ref) => iriTerm(ref.id));
|
|
1559
|
+
}
|
|
1489
1560
|
/**
|
|
1490
1561
|
* Processes IRNodeData fields into DELETE/INSERT/WHERE triples.
|
|
1491
1562
|
* Shared between updateToAlgebra (IRI subject) and updateWhereToAlgebra (variable subject).
|
|
1492
1563
|
*/
|
|
1493
1564
|
function processUpdateFields(data, subjectTerm, options) {
|
|
1565
|
+
var _a, _b;
|
|
1494
1566
|
const deletePatterns = [];
|
|
1495
1567
|
const insertPatterns = [];
|
|
1496
1568
|
const oldValueTriples = [];
|
|
@@ -1505,6 +1577,10 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1505
1577
|
const propertyTerm = resolvePropertyPredicateTerm(field.property);
|
|
1506
1578
|
const suffix = propertySuffix(field.property);
|
|
1507
1579
|
const isContainsField = propertyTerm.kind === 'iri' && updContainsPreds.includes(propertyTerm.value);
|
|
1580
|
+
// Only a `contains` property that declares a value shape deletes the old value's own
|
|
1581
|
+
// triples. Without one (a polymorphic slot like `sh:path`) the value may be a shared
|
|
1582
|
+
// IRI, so it is left to the cascade, which deletes it only when it is dependent-typed.
|
|
1583
|
+
const hasValueShape = isContainsField && !!((_a = findPropertyShapeById(field.property)) === null || _a === void 0 ? void 0 : _a.valueShape);
|
|
1508
1584
|
// Check for set modification ({add, remove})
|
|
1509
1585
|
if (field.value &&
|
|
1510
1586
|
typeof field.value === 'object' &&
|
|
@@ -1515,17 +1591,26 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1515
1591
|
('add' in field.value || 'remove' in field.value)) {
|
|
1516
1592
|
const setMod = field.value;
|
|
1517
1593
|
if (setMod.remove) {
|
|
1594
|
+
// A value that is removed and added back in the same modification stays: its edge
|
|
1595
|
+
// is deleted and re-inserted (a no-op), and the node itself is not touched.
|
|
1596
|
+
const readdedIds = new Set(((_b = setMod.add) !== null && _b !== void 0 ? _b : []).filter(isBareNodeReference).map((ref) => ref.id));
|
|
1518
1597
|
setMod.remove.forEach((removeItem, ri) => {
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1598
|
+
const removeId = removeItem.id;
|
|
1599
|
+
const removeTerm = iriTerm(removeId);
|
|
1600
|
+
const edge = tripleOf(subjectTerm, propertyTerm, removeTerm);
|
|
1601
|
+
deletePatterns.push(edge);
|
|
1602
|
+
oldValueTriples.push(edge);
|
|
1522
1603
|
// For a `contains` set, removing a value deletes the removed node itself
|
|
1523
1604
|
// (its own triples) and its owned subtree — exclusive ownership via the edge.
|
|
1524
|
-
if (isContainsField) {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1605
|
+
if (isContainsField && !readdedIds.has(removeId)) {
|
|
1606
|
+
if (hasValueShape) {
|
|
1607
|
+
const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, removeTerm, `ucr_${suffix}_${ri}_`);
|
|
1608
|
+
deletePatterns.push(...self.deletePatterns);
|
|
1609
|
+
cascadeOptionals.push(...self.whereOptionals);
|
|
1610
|
+
}
|
|
1611
|
+
// The edge leads the cascade too: with a zero-or-more path the removed node is
|
|
1612
|
+
// itself a match, and it may only be deleted while it is still owned here.
|
|
1613
|
+
const cascade = buildOwnedCascade(removeTerm, `ucr_${suffix}_${ri}_`, edge);
|
|
1529
1614
|
deletePatterns.push(...cascade.deletePatterns);
|
|
1530
1615
|
cascadeOptionals.push(...cascade.whereOptionals);
|
|
1531
1616
|
}
|
|
@@ -1535,7 +1620,7 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1535
1620
|
for (const addItem of setMod.add) {
|
|
1536
1621
|
if (addItem && typeof addItem === 'object' && 'shape' in addItem && 'fields' in addItem) {
|
|
1537
1622
|
const nested = generateNodeDataTriples(addItem, options);
|
|
1538
|
-
insertPatterns.push(tripleOf(subjectTerm, propertyTerm,
|
|
1623
|
+
insertPatterns.push(tripleOf(subjectTerm, propertyTerm, nested.term));
|
|
1539
1624
|
insertPatterns.push(...nested.triples);
|
|
1540
1625
|
}
|
|
1541
1626
|
else {
|
|
@@ -1551,7 +1636,12 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1551
1636
|
// Non-set-modification replace of a `contains` property: cascade the old value's
|
|
1552
1637
|
// subtree (every branch below binds `old_${suffix}` in the WHERE).
|
|
1553
1638
|
if (isContainsField) {
|
|
1554
|
-
containsOldVars.push({
|
|
1639
|
+
containsOldVars.push({
|
|
1640
|
+
oldVar: `old_${suffix}`,
|
|
1641
|
+
propertyTerm,
|
|
1642
|
+
keep: keptReferences(field.value),
|
|
1643
|
+
hasValueShape,
|
|
1644
|
+
});
|
|
1555
1645
|
}
|
|
1556
1646
|
// Unset (undefined/null) — delete only
|
|
1557
1647
|
if (field.value === undefined || field.value === null) {
|
|
@@ -1568,7 +1658,7 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1568
1658
|
for (const item of field.value) {
|
|
1569
1659
|
if (item && typeof item === 'object' && 'shape' in item && 'fields' in item) {
|
|
1570
1660
|
const nested = generateNodeDataTriples(item, options);
|
|
1571
|
-
insertPatterns.push(tripleOf(subjectTerm, propertyTerm,
|
|
1661
|
+
insertPatterns.push(tripleOf(subjectTerm, propertyTerm, nested.term));
|
|
1572
1662
|
insertPatterns.push(...nested.triples);
|
|
1573
1663
|
}
|
|
1574
1664
|
else {
|
|
@@ -1586,7 +1676,7 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1586
1676
|
deletePatterns.push(tripleOf(subjectTerm, propertyTerm, oldVar));
|
|
1587
1677
|
oldValueTriples.push(tripleOf(subjectTerm, propertyTerm, oldVar));
|
|
1588
1678
|
const nested = generateNodeDataTriples(field.value, options);
|
|
1589
|
-
insertPatterns.push(tripleOf(subjectTerm, propertyTerm,
|
|
1679
|
+
insertPatterns.push(tripleOf(subjectTerm, propertyTerm, nested.term));
|
|
1590
1680
|
insertPatterns.push(...nested.triples);
|
|
1591
1681
|
continue;
|
|
1592
1682
|
}
|
|
@@ -1636,15 +1726,19 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1636
1726
|
}
|
|
1637
1727
|
// For each replaced `contains` property: delete the old value's own triples
|
|
1638
1728
|
// (exclusive ownership via the edge — backlog 032) then cascade any owned descendants.
|
|
1639
|
-
|
|
1729
|
+
// Values re-sent as a bare `{id}` are kept: their blocks are filtered, so re-sending the
|
|
1730
|
+
// current value is a no-op (the edge is deleted and re-inserted).
|
|
1731
|
+
containsOldVars.forEach(({ oldVar, propertyTerm, keep, hasValueShape }, n) => {
|
|
1640
1732
|
const oldTerm = varTerm(oldVar);
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1733
|
+
if (hasValueShape) {
|
|
1734
|
+
const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, `uc${n}_`, keep);
|
|
1735
|
+
deletePatterns.push(...self.deletePatterns);
|
|
1736
|
+
cascadeOptionals.push(...self.whereOptionals);
|
|
1737
|
+
}
|
|
1644
1738
|
// Every cascade block binds `?old` itself through the owning edge, like the
|
|
1645
1739
|
// self-delete block does. Without it an absent old value leaves `?old` unbound and
|
|
1646
1740
|
// the block matches the owned subtree of EVERY node in the graph.
|
|
1647
|
-
const cascade = buildOwnedCascade(oldTerm, `uc${n}_`, tripleOf(subjectTerm, propertyTerm, oldTerm));
|
|
1741
|
+
const cascade = buildOwnedCascade(oldTerm, `uc${n}_`, tripleOf(subjectTerm, propertyTerm, oldTerm), keep);
|
|
1648
1742
|
deletePatterns.push(...cascade.deletePatterns);
|
|
1649
1743
|
cascadeOptionals.push(...cascade.whereOptionals);
|
|
1650
1744
|
});
|
|
@@ -1846,11 +1940,37 @@ function buildUpdateWhere(base, result, traversalPatterns, subjectTerm) {
|
|
|
1846
1940
|
// - a property marked `contains` → the cascade FOLLOWS that edge,
|
|
1847
1941
|
// - a shape marked `dependent` → its instances may be DELETED when reached.
|
|
1848
1942
|
// 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
|
|
1943
|
+
// node we follow `(c1|c2|…)*` over all contains predicates (zero hops included, so a
|
|
1944
|
+
// dependent-typed root is itself a match) and, for each dependent targetClass,
|
|
1945
|
+
// wildcard-delete the reached typed nodes. Requiring an asserted `?owned a <dependentType>`
|
|
1946
|
+
// naturally excludes shared predicate IRIs (e.g. a simple `sh:path`) and `rdf:nil` (never
|
|
1947
|
+
// typed), so only owned structural nodes are removed.
|
|
1948
|
+
//
|
|
1949
|
+
// Whether a field owns its value is decided per predicate across the registry
|
|
1950
|
+
// (`collectContainment`): the replace and set-remove deletes, and the class check in
|
|
1951
|
+
// `ownedLinkCheck.ts`, apply that same rule. The subject deletes (by id and bulk) use the
|
|
1952
|
+
// deleted shape's own `contains` properties with a value shape for the immediate children.
|
|
1853
1953
|
// ---------------------------------------------------------------------------
|
|
1954
|
+
/**
|
|
1955
|
+
* The simple-path predicates of the shape's own and inherited `contains` properties that
|
|
1956
|
+
* declare a value shape, deduplicated in declaration order. These are the properties
|
|
1957
|
+
* whose immediate values a subject delete removes.
|
|
1958
|
+
*/
|
|
1959
|
+
function ownedValueShapePredicates(shapeId) {
|
|
1960
|
+
var _a, _b;
|
|
1961
|
+
const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
|
|
1962
|
+
if (!nodeShape)
|
|
1963
|
+
return [];
|
|
1964
|
+
const predicates = new Set();
|
|
1965
|
+
for (const ps of getPropertyShapes(nodeShape, true)) {
|
|
1966
|
+
if (!ps.contains || !ps.valueShape)
|
|
1967
|
+
continue;
|
|
1968
|
+
const predicate = getSimplePathId(ps.path);
|
|
1969
|
+
if (predicate)
|
|
1970
|
+
predicates.add(predicate);
|
|
1971
|
+
}
|
|
1972
|
+
return [...predicates];
|
|
1973
|
+
}
|
|
1854
1974
|
/** True when the shape (or an ancestor) declares at least one `contains` property. */
|
|
1855
1975
|
function shapeHasContainsProperty(shapeId) {
|
|
1856
1976
|
var _a, _b;
|
|
@@ -1866,7 +1986,7 @@ function shapeHasContainsProperty(shapeId) {
|
|
|
1866
1986
|
let containmentCache = null;
|
|
1867
1987
|
let containmentCacheSize = -1;
|
|
1868
1988
|
/** Gather contains-predicate IRIs and dependent targetClass IRIs from the registry. */
|
|
1869
|
-
function collectContainment() {
|
|
1989
|
+
export function collectContainment() {
|
|
1870
1990
|
var _a;
|
|
1871
1991
|
const version = getRegistryVersion();
|
|
1872
1992
|
if (containmentCache && version === containmentCacheSize) {
|
|
@@ -1893,6 +2013,78 @@ function collectContainment() {
|
|
|
1893
2013
|
containmentCacheSize = version;
|
|
1894
2014
|
return containmentCache;
|
|
1895
2015
|
}
|
|
2016
|
+
let reachableCache = new Map();
|
|
2017
|
+
let reachableCacheVersion = -1;
|
|
2018
|
+
/**
|
|
2019
|
+
* The dependent targetClasses a subject delete of `shapeId` can reach through `contains`
|
|
2020
|
+
* edges, in `collectContainment` order. A subject delete emits one cascade block per type it
|
|
2021
|
+
* returns, and each block is a property-path closure per subject, so a shape that owns only
|
|
2022
|
+
* non-dependent values (every `Thing.image` owner) pays for none.
|
|
2023
|
+
*
|
|
2024
|
+
* The walk starts at the shape and its registered sub-shapes and follows every flattened
|
|
2025
|
+
* property on a `contains` predicate (the registry-wide predicate rule the cascade path uses)
|
|
2026
|
+
* to its value shape and that shape's sub-shapes. A property on a `contains` predicate with no
|
|
2027
|
+
* value shape is polymorphic (`sh:path`) and may reach anything, so it yields every dependent
|
|
2028
|
+
* type; that keeps the shape-sync delete of a NodeShape exact.
|
|
2029
|
+
*/
|
|
2030
|
+
function reachableDependentTypes(shapeId) {
|
|
2031
|
+
var _a, _b;
|
|
2032
|
+
const version = getRegistryVersion();
|
|
2033
|
+
if (version !== reachableCacheVersion) {
|
|
2034
|
+
reachableCache = new Map();
|
|
2035
|
+
reachableCacheVersion = version;
|
|
2036
|
+
}
|
|
2037
|
+
const cached = reachableCache.get(shapeId);
|
|
2038
|
+
if (cached)
|
|
2039
|
+
return cached;
|
|
2040
|
+
const { containsPreds, dependentTypes } = collectContainment();
|
|
2041
|
+
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); };
|
|
2042
|
+
const reached = new Set();
|
|
2043
|
+
// Shapes whose instances an owned edge can lead to. The start shapes join only when an
|
|
2044
|
+
// edge leads back to them (a dependent shape that owns its own kind, like a list cell).
|
|
2045
|
+
const owned = new Set();
|
|
2046
|
+
const seen = new Set();
|
|
2047
|
+
const queue = [];
|
|
2048
|
+
const visit = (id, viaEdge) => {
|
|
2049
|
+
for (const sid of [id, ...getSubShapes(id).map((sub) => sub.id)]) {
|
|
2050
|
+
if (viaEdge)
|
|
2051
|
+
owned.add(sid);
|
|
2052
|
+
if (seen.has(sid))
|
|
2053
|
+
continue;
|
|
2054
|
+
seen.add(sid);
|
|
2055
|
+
queue.push(sid);
|
|
2056
|
+
}
|
|
2057
|
+
};
|
|
2058
|
+
visit(shapeId, false);
|
|
2059
|
+
let everything = false;
|
|
2060
|
+
while (queue.length > 0 && !everything) {
|
|
2061
|
+
const nodeShape = shapeOf(queue.shift());
|
|
2062
|
+
if (!nodeShape)
|
|
2063
|
+
continue;
|
|
2064
|
+
for (const ps of getPropertyShapes(nodeShape, true)) {
|
|
2065
|
+
const predicate = getSimplePathId(ps.path);
|
|
2066
|
+
if (!predicate || !containsPreds.includes(predicate))
|
|
2067
|
+
continue;
|
|
2068
|
+
if (!((_a = ps.valueShape) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
2069
|
+
everything = true;
|
|
2070
|
+
break;
|
|
2071
|
+
}
|
|
2072
|
+
visit(ps.valueShape.id, true);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
if (!everything) {
|
|
2076
|
+
for (const id of owned) {
|
|
2077
|
+
const nodeShape = shapeOf(id);
|
|
2078
|
+
const dependent = nodeShape === null || nodeShape === void 0 ? void 0 : nodeShape.dependent;
|
|
2079
|
+
if (dependent && ((_b = nodeShape === null || nodeShape === void 0 ? void 0 : nodeShape.targetClass) === null || _b === void 0 ? void 0 : _b.id)) {
|
|
2080
|
+
reached.add(nodeShape.targetClass.id);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
const result = everything ? dependentTypes : dependentTypes.filter((t) => reached.has(t));
|
|
2085
|
+
reachableCache.set(shapeId, result);
|
|
2086
|
+
return result;
|
|
2087
|
+
}
|
|
1896
2088
|
/**
|
|
1897
2089
|
* Build DELETE patterns + WHERE OPTIONAL blocks that cascade-delete the owned subtree
|
|
1898
2090
|
* reachable from `rootTerm` via `contains` edges. `varPrefix` keeps generated variables
|
|
@@ -1900,16 +2092,24 @@ function collectContainment() {
|
|
|
1900
2092
|
*
|
|
1901
2093
|
* `anchor`, when given, leads every block: the triple that binds a variable root, so
|
|
1902
2094
|
* each block stays correct on its own (as a UNION branch) when no such root exists.
|
|
2095
|
+
* `onlyTypes`, when given, restricts the blocks to those dependent types (a subject delete
|
|
2096
|
+
* passes the types its shape can reach); by default every registered dependent type gets one.
|
|
1903
2097
|
*/
|
|
1904
|
-
export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
|
|
1905
|
-
const
|
|
2098
|
+
export function buildOwnedCascade(rootTerm, varPrefix, anchor, keep, onlyTypes) {
|
|
2099
|
+
const containment = collectContainment();
|
|
2100
|
+
const { containsPreds } = containment;
|
|
2101
|
+
const dependentTypes = onlyTypes !== null && onlyTypes !== void 0 ? onlyTypes : containment.dependentTypes;
|
|
1906
2102
|
if (containsPreds.length === 0 || dependentTypes.length === 0) {
|
|
1907
2103
|
return { deletePatterns: [], whereOptionals: [] };
|
|
1908
2104
|
}
|
|
1909
|
-
// (c1|c2|…)
|
|
2105
|
+
// (c1|c2|…)* — zero or more contains edges, so the root itself is matched when it is
|
|
2106
|
+
// dependent-typed. That is what cleans up the value of a `contains` property that has
|
|
2107
|
+
// no value shape (a polymorphic slot like `sh:path`): it is deleted only when it is a
|
|
2108
|
+
// dependent node, never when it is a shared IRI. On a subject delete `*` also matches
|
|
2109
|
+
// the subject, which is being deleted anyway.
|
|
1910
2110
|
const pathExpr = containsPreds.length === 1
|
|
1911
|
-
? {
|
|
1912
|
-
: {
|
|
2111
|
+
? { zeroOrMore: { id: containsPreds[0] } }
|
|
2112
|
+
: { zeroOrMore: { alt: containsPreds.map((id) => ({ id })) } };
|
|
1913
2113
|
const pathTerm = {
|
|
1914
2114
|
kind: 'path',
|
|
1915
2115
|
value: pathExprToSparql(pathExpr),
|
|
@@ -1922,7 +2122,7 @@ export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
|
|
|
1922
2122
|
const p = varTerm(`${varPrefix}op${i}`);
|
|
1923
2123
|
const o = varTerm(`${varPrefix}ov${i}`);
|
|
1924
2124
|
deletePatterns.push(tripleOf(owned, p, o));
|
|
1925
|
-
whereOptionals.push({
|
|
2125
|
+
whereOptionals.push(keepFilter({
|
|
1926
2126
|
type: 'bgp',
|
|
1927
2127
|
triples: [
|
|
1928
2128
|
...(anchor ? [anchor] : []),
|
|
@@ -1930,10 +2130,76 @@ export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
|
|
|
1930
2130
|
tripleOf(owned, iriTerm(RDF_TYPE), iriTerm(typeId)),
|
|
1931
2131
|
tripleOf(owned, p, o),
|
|
1932
2132
|
],
|
|
1933
|
-
});
|
|
2133
|
+
}, rootTerm, keep));
|
|
2134
|
+
});
|
|
2135
|
+
// Blank nodes carry no type (a SHACL path operator must be the subject of exactly one
|
|
2136
|
+
// triple), so the typed blocks above cannot see them. Every blank node owned through
|
|
2137
|
+
// `contains` edges (`?b0`) is deleted, together with the RDF-list / SHACL-path syntax below it
|
|
2138
|
+
// (`?bn`). That inner walk follows `rdf:first` too, because a sequence member that is itself a
|
|
2139
|
+
// path operator is held by `rdf:first`, which is not a `contains` edge. It is restricted to the
|
|
2140
|
+
// syntax predicates on purpose: SPARQL cannot require the nodes *between* `?b0` and `?bn` to be
|
|
2141
|
+
// blank, so following every `contains` edge there would let a list's `rdf:first` lead into an
|
|
2142
|
+
// entity it merely lists and delete that entity's blank children.
|
|
2143
|
+
const insidePathExpr = { zeroOrMore: { alt: BLANK_SYNTAX_PREDICATES.map((id) => ({ id })) } };
|
|
2144
|
+
const insidePathTerm = {
|
|
2145
|
+
kind: 'path',
|
|
2146
|
+
value: pathExprToSparql(insidePathExpr),
|
|
2147
|
+
uris: collectPathUris(insidePathExpr),
|
|
2148
|
+
};
|
|
2149
|
+
const ownedBlank = varTerm(`${varPrefix}bn0`);
|
|
2150
|
+
const blank = varTerm(`${varPrefix}bn`);
|
|
2151
|
+
const bp = varTerm(`${varPrefix}bnp`);
|
|
2152
|
+
const bo = varTerm(`${varPrefix}bno`);
|
|
2153
|
+
const isBlank = (name) => ({
|
|
2154
|
+
kind: 'function_expr',
|
|
2155
|
+
name: 'isBlank',
|
|
2156
|
+
args: [{ kind: 'variable_expr', name }],
|
|
1934
2157
|
});
|
|
2158
|
+
deletePatterns.push(tripleOf(blank, bp, bo));
|
|
2159
|
+
whereOptionals.push(keepFilter({
|
|
2160
|
+
type: 'filter',
|
|
2161
|
+
expression: isBlank(`${varPrefix}bn`),
|
|
2162
|
+
inner: {
|
|
2163
|
+
type: 'join',
|
|
2164
|
+
// Filter the owned blank nodes before the inner walk runs from each of them.
|
|
2165
|
+
left: {
|
|
2166
|
+
type: 'filter',
|
|
2167
|
+
expression: isBlank(`${varPrefix}bn0`),
|
|
2168
|
+
inner: {
|
|
2169
|
+
type: 'bgp',
|
|
2170
|
+
triples: [...(anchor ? [anchor] : []), tripleOf(rootTerm, pathTerm, ownedBlank)],
|
|
2171
|
+
},
|
|
2172
|
+
},
|
|
2173
|
+
right: {
|
|
2174
|
+
type: 'bgp',
|
|
2175
|
+
triples: [tripleOf(ownedBlank, insidePathTerm, blank), tripleOf(blank, bp, bo)],
|
|
2176
|
+
},
|
|
2177
|
+
},
|
|
2178
|
+
}, rootTerm, keep));
|
|
1935
2179
|
return { deletePatterns, whereOptionals };
|
|
1936
2180
|
}
|
|
2181
|
+
/**
|
|
2182
|
+
* Wrap an owned-cleanup block in `FILTER(?old NOT IN (<k1>, …))` so a value the mutation
|
|
2183
|
+
* keeps (re-sent as a bare `{id}`) is not deleted. The filter sits inside the block, next
|
|
2184
|
+
* to the edge triple that binds `?old`, so it only removes that block's matches; the
|
|
2185
|
+
* edge-only delete of the same value stays and is undone by the INSERT. With an IRI old
|
|
2186
|
+
* term, or nothing to keep, the block is returned unchanged.
|
|
2187
|
+
*/
|
|
2188
|
+
function keepFilter(block, oldTerm, keep) {
|
|
2189
|
+
const kept = (keep !== null && keep !== void 0 ? keep : []).flatMap((term) => term.kind === 'iri' ? [{ kind: 'iri_expr', value: term.value }] : []);
|
|
2190
|
+
if (oldTerm.kind !== 'variable' || kept.length === 0)
|
|
2191
|
+
return block;
|
|
2192
|
+
return {
|
|
2193
|
+
type: 'filter',
|
|
2194
|
+
expression: {
|
|
2195
|
+
kind: 'in_expr',
|
|
2196
|
+
negated: true,
|
|
2197
|
+
value: { kind: 'variable_expr', name: oldTerm.name },
|
|
2198
|
+
list: kept,
|
|
2199
|
+
},
|
|
2200
|
+
inner: block,
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
1937
2203
|
/**
|
|
1938
2204
|
* Build DELETE + WHERE OPTIONAL that removes the *old node's own* one-hop triples when a
|
|
1939
2205
|
* `contains` edge is replaced, unset, or set-removed. A `contains` edge asserts exclusive
|
|
@@ -1942,22 +2208,31 @@ export function buildOwnedCascade(rootTerm, varPrefix, anchor) {
|
|
|
1942
2208
|
* does NOT require the child shape to be `dependent`: ownership via the edge is sufficient.
|
|
1943
2209
|
* Owned descendants *below* the old node are cascaded separately by `buildOwnedCascade`.
|
|
1944
2210
|
*
|
|
2211
|
+
* Callers use it only for a property that declares a value shape. A shape-less `contains`
|
|
2212
|
+
* slot (like `sh:path`) may hold a shared IRI, so its old value is left to
|
|
2213
|
+
* `buildOwnedCascade`, which deletes it only when it is dependent-typed.
|
|
2214
|
+
*
|
|
1945
2215
|
* The owning edge `<subject> <prop> ?old` is re-asserted INSIDE the WHERE group so `?old`
|
|
1946
2216
|
* is bound there: when no old value exists the group matches nothing and the DELETE is a
|
|
1947
2217
|
* no-op, rather than an unbound `?old` matching (and deleting) the whole graph. When
|
|
1948
2218
|
* `oldTerm` is a concrete IRI (set-remove), the guard just confirms the link still holds.
|
|
2219
|
+
*
|
|
2220
|
+
* `keep` lists the bare `{id}`s the mutation writes again (a re-sent current value, the
|
|
2221
|
+
* retained ids of an array overwrite). With a variable `oldTerm` the block is wrapped in
|
|
2222
|
+
* `FILTER(?old NOT IN (…))`, so a kept value is not deleted; its edge is deleted and
|
|
2223
|
+
* re-inserted, a no-op. A nested create carrying an id is never kept: it rewrites that node.
|
|
1949
2224
|
*/
|
|
1950
|
-
export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPrefix) {
|
|
2225
|
+
export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPrefix, keep) {
|
|
1951
2226
|
const p = varTerm(`${varPrefix}sp`);
|
|
1952
2227
|
const o = varTerm(`${varPrefix}so`);
|
|
1953
2228
|
const wildcard = tripleOf(oldTerm, p, o);
|
|
1954
2229
|
return {
|
|
1955
2230
|
deletePatterns: [wildcard],
|
|
1956
2231
|
whereOptionals: [
|
|
1957
|
-
{
|
|
2232
|
+
keepFilter({
|
|
1958
2233
|
type: 'bgp',
|
|
1959
2234
|
triples: [tripleOf(subjectTerm, propertyTerm, oldTerm), wildcard],
|
|
1960
|
-
},
|
|
2235
|
+
}, oldTerm, keep),
|
|
1961
2236
|
],
|
|
1962
2237
|
};
|
|
1963
2238
|
}
|
|
@@ -1966,7 +2241,12 @@ export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPref
|
|
|
1966
2241
|
*
|
|
1967
2242
|
* The WHERE is the type guard of every id, joined with ONE UNION of independent
|
|
1968
2243
|
* blocks: per id its own triples, its incoming references and, for a shape that
|
|
1969
|
-
* owns something, one block per
|
|
2244
|
+
* owns something, one block per immediately-owned child (`<s> <p> ?dN . ?dN ?dN_p ?dN_o`,
|
|
2245
|
+
* for each of the shape's own and inherited `contains` properties with a value shape and
|
|
2246
|
+
* a simple path) and one block per dependent type its owned subtree can reach
|
|
2247
|
+
* (`reachableDependentTypes`; none for a shape that owns only non-dependent values). The
|
|
2248
|
+
* bulk deletes (`deleteAllToAlgebra`, `deleteWhereToAlgebra`) add the same owned blocks,
|
|
2249
|
+
* rooted at the subject variable (see `bulkDeleteUnion`).
|
|
1970
2250
|
*
|
|
1971
2251
|
* The blocks must not be chained as OPTIONALs. A chain of OPTIONALs is a join, so
|
|
1972
2252
|
* the solutions multiply: own triples x incoming references x owned triples of each
|
|
@@ -1984,6 +2264,9 @@ export function deleteToAlgebra(query, _options) {
|
|
|
1984
2264
|
const deletePatterns = [];
|
|
1985
2265
|
const guards = [];
|
|
1986
2266
|
const blocks = [];
|
|
2267
|
+
const hasContains = shapeHasContainsProperty(query.shape);
|
|
2268
|
+
const ownedChildPredicates = hasContains ? ownedValueShapePredicates(query.shape) : [];
|
|
2269
|
+
const cascadeTypes = hasContains ? reachableDependentTypes(query.shape) : [];
|
|
1987
2270
|
for (let i = 0; i < query.ids.length; i++) {
|
|
1988
2271
|
const subjectTerm = iriTerm(query.ids[i].id);
|
|
1989
2272
|
const idx = query.ids.length > 1 ? `_${i}` : '';
|
|
@@ -1996,10 +2279,22 @@ export function deleteToAlgebra(query, _options) {
|
|
|
1996
2279
|
// incoming references (the entity may have none) are separate UNION blocks.
|
|
1997
2280
|
guards.push(typeGuard);
|
|
1998
2281
|
blocks.push({ type: 'bgp', triples: [subjWild] }, { type: 'bgp', triples: [objWild] });
|
|
2282
|
+
// The immediately-owned children: for every `contains` property with a value shape,
|
|
2283
|
+
// the child's own one-hop triples, as a replace or a set-remove deletes them. No type
|
|
2284
|
+
// guard and no reference check: the `contains` edge alone asserts ownership.
|
|
2285
|
+
ownedChildPredicates.forEach((predicate, n) => {
|
|
2286
|
+
const child = varTerm(`d${n}${idx}`);
|
|
2287
|
+
const childOwn = tripleOf(child, varTerm(`d${n}${idx}_p`), varTerm(`d${n}${idx}_o`));
|
|
2288
|
+
deletePatterns.push(childOwn);
|
|
2289
|
+
blocks.push({
|
|
2290
|
+
type: 'bgp',
|
|
2291
|
+
triples: [tripleOf(subjectTerm, iriTerm(predicate), child), childOwn],
|
|
2292
|
+
});
|
|
2293
|
+
});
|
|
1999
2294
|
// Cascade-delete the owned subtree — only for shapes that actually own something
|
|
2000
2295
|
// (have a `contains` property); a plain entity delete is left untouched.
|
|
2001
|
-
if (
|
|
2002
|
-
const cascade = buildOwnedCascade(subjectTerm, `c${idx}_
|
|
2296
|
+
if (hasContains && cascadeTypes.length > 0) {
|
|
2297
|
+
const cascade = buildOwnedCascade(subjectTerm, `c${idx}_`, undefined, undefined, cascadeTypes);
|
|
2003
2298
|
deletePatterns.push(...cascade.deletePatterns);
|
|
2004
2299
|
blocks.push(...cascade.whereOptionals);
|
|
2005
2300
|
}
|
|
@@ -2079,13 +2374,39 @@ function blankNodeBlocks(shapeId, parentVar, path, counter, deletePatterns, bloc
|
|
|
2079
2374
|
}
|
|
2080
2375
|
}
|
|
2081
2376
|
/**
|
|
2082
|
-
* The
|
|
2083
|
-
*
|
|
2084
|
-
*
|
|
2377
|
+
* The cleanup blocks of a bulk delete, already joined with the root's own triples as ONE
|
|
2378
|
+
* UNION so their matches add up instead of multiplying; `null` when there is nothing to
|
|
2379
|
+
* clean up (the root wildcard then stays in the required BGP).
|
|
2380
|
+
*
|
|
2381
|
+
* - The blank-node blocks of the shape's blank-node properties.
|
|
2382
|
+
* - For a shape that owns something (a `contains` property), the same owned cleanup as a
|
|
2383
|
+
* delete by id: the immediately-owned children of every `contains` property with a value
|
|
2384
|
+
* shape, and the dependent cascade, rooted at the subject variable. Each owned block is led
|
|
2385
|
+
* by the subject's type triple. With a linear join it is redundant; it keeps a branch that
|
|
2386
|
+
* is evaluated on its own from running `?a0 (c…)* ?own` with both ends unbound, which
|
|
2387
|
+
* matches every node in the graph.
|
|
2085
2388
|
*/
|
|
2086
|
-
function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, deletePatterns) {
|
|
2389
|
+
function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, typeTriple, deletePatterns) {
|
|
2087
2390
|
const blocks = [];
|
|
2088
2391
|
blankNodeBlocks(shapeId, subjectVar, { triples: [], bnVars: [] }, { next: 1 }, deletePatterns, blocks);
|
|
2392
|
+
if (shapeHasContainsProperty(shapeId)) {
|
|
2393
|
+
const subject = varTerm(subjectVar);
|
|
2394
|
+
ownedValueShapePredicates(shapeId).forEach((predicate, n) => {
|
|
2395
|
+
const child = varTerm(`d${n}`);
|
|
2396
|
+
const childOwn = tripleOf(child, varTerm(`d${n}_p`), varTerm(`d${n}_o`));
|
|
2397
|
+
deletePatterns.push(childOwn);
|
|
2398
|
+
blocks.push({
|
|
2399
|
+
type: 'bgp',
|
|
2400
|
+
triples: [typeTriple, tripleOf(subject, iriTerm(predicate), child), childOwn],
|
|
2401
|
+
});
|
|
2402
|
+
});
|
|
2403
|
+
const cascadeTypes = reachableDependentTypes(shapeId);
|
|
2404
|
+
if (cascadeTypes.length > 0) {
|
|
2405
|
+
const cascade = buildOwnedCascade(subject, 'c_', typeTriple, undefined, cascadeTypes);
|
|
2406
|
+
deletePatterns.push(...cascade.deletePatterns);
|
|
2407
|
+
blocks.push(...cascade.whereOptionals);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2089
2410
|
if (blocks.length === 0)
|
|
2090
2411
|
return null;
|
|
2091
2412
|
return unionOf([{ type: 'bgp', triples: [rootWildcard] }, ...blocks]);
|
|
@@ -2093,8 +2414,10 @@ function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, deletePatterns) {
|
|
|
2093
2414
|
/**
|
|
2094
2415
|
* Converts an IRDeleteAllMutation to a SparqlDeleteInsertPlan.
|
|
2095
2416
|
*
|
|
2096
|
-
* Generates DELETE { ?a0 ?p ?o . [blank
|
|
2097
|
-
* WHERE { ?a0 a <Shape> . ?a0 ?p ?o
|
|
2417
|
+
* Generates DELETE { ?a0 ?p ?o . [blank-node and owned-cleanup wildcards] }
|
|
2418
|
+
* WHERE { ?a0 a <Shape> . { ?a0 ?p ?o } UNION [one block per blank node,
|
|
2419
|
+
* owned child and dependent type] }
|
|
2420
|
+
* (see `bulkDeleteUnion`). Unlike a delete by id, incoming references are not deleted.
|
|
2098
2421
|
*/
|
|
2099
2422
|
export function deleteAllToAlgebra(query, _options) {
|
|
2100
2423
|
const subjectVar = 'a0';
|
|
@@ -2105,7 +2428,7 @@ export function deleteAllToAlgebra(query, _options) {
|
|
|
2105
2428
|
// WHERE: type triple, then the root wildcard and the blank-node cleanup
|
|
2106
2429
|
const typeTriple = tripleOf(varTerm(subjectVar), iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
|
|
2107
2430
|
const rootWildcard = tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o'));
|
|
2108
|
-
const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, deletePatterns);
|
|
2431
|
+
const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, typeTriple, deletePatterns);
|
|
2109
2432
|
const whereAlgebra = union
|
|
2110
2433
|
? { type: 'join', left: { type: 'bgp', triples: [typeTriple] }, right: union }
|
|
2111
2434
|
: { type: 'bgp', triples: [typeTriple, rootWildcard] };
|
|
@@ -2119,7 +2442,9 @@ export function deleteAllToAlgebra(query, _options) {
|
|
|
2119
2442
|
/**
|
|
2120
2443
|
* Converts an IRDeleteWhereMutation to a SparqlDeleteInsertPlan.
|
|
2121
2444
|
*
|
|
2122
|
-
* Like deleteAllToAlgebra but adds filter conditions from the where clause.
|
|
2445
|
+
* Like deleteAllToAlgebra but adds filter conditions from the where clause. The cleanup
|
|
2446
|
+
* UNION (blank nodes, owned children, dependent cascade) is joined with the filtered match,
|
|
2447
|
+
* so only the matched subjects' children are deleted.
|
|
2123
2448
|
*/
|
|
2124
2449
|
export function deleteWhereToAlgebra(query, _options) {
|
|
2125
2450
|
const subjectVar = 'a0';
|
|
@@ -2131,7 +2456,7 @@ export function deleteWhereToAlgebra(query, _options) {
|
|
|
2131
2456
|
// WHERE: type triple + the where clause; the root wildcard and blank-node cleanup follow
|
|
2132
2457
|
const typeTriple = tripleOf(varTerm(subjectVar), iriTerm(RDF_TYPE), iriTerm(resolveShapeScanIri(query.shape)));
|
|
2133
2458
|
const rootWildcard = tripleOf(varTerm(subjectVar), varTerm('p'), varTerm('o'));
|
|
2134
|
-
const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, deletePatterns);
|
|
2459
|
+
const union = bulkDeleteUnion(query.shape, subjectVar, rootWildcard, typeTriple, deletePatterns);
|
|
2135
2460
|
let whereAlgebra = {
|
|
2136
2461
|
type: 'bgp',
|
|
2137
2462
|
triples: union ? [typeTriple] : [typeTriple, rootWildcard],
|