@_linked/core 2.30.0 → 2.31.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 +54 -0
- package/README.md +54 -7
- package/lib/esm/queries/IRMutation.js +1 -1
- package/lib/esm/queries/IRMutation.js.map +1 -1
- package/lib/esm/queries/IntermediateRepresentation.d.ts +0 -5
- package/lib/esm/queries/IntermediateRepresentation.js.map +1 -1
- package/lib/esm/queries/MutationQuery.js +9 -19
- package/lib/esm/queries/MutationQuery.js.map +1 -1
- package/lib/esm/queries/MutationSerialization.d.ts +6 -5
- package/lib/esm/queries/MutationSerialization.js +30 -13
- package/lib/esm/queries/MutationSerialization.js.map +1 -1
- package/lib/esm/queries/QueryFactory.d.ts +0 -6
- package/lib/esm/queries/QueryFactory.js.map +1 -1
- package/lib/esm/queries/lowerMutationJSON.js +1 -6
- package/lib/esm/queries/lowerMutationJSON.js.map +1 -1
- package/lib/esm/shapes/List.d.ts +0 -1
- package/lib/esm/shapes/List.js.map +1 -1
- package/lib/esm/shapes/PathList.d.ts +27 -0
- package/lib/esm/shapes/PathList.js +63 -0
- package/lib/esm/shapes/PathList.js.map +1 -0
- package/lib/esm/shapes/PathNode.d.ts +10 -9
- package/lib/esm/shapes/PathNode.js +21 -13
- package/lib/esm/shapes/PathNode.js.map +1 -1
- package/lib/esm/shapes/index.d.ts +1 -0
- package/lib/esm/shapes/index.js +1 -0
- package/lib/esm/shapes/index.js.map +1 -1
- package/lib/esm/shapes/serializePathToNodeData.d.ts +12 -11
- package/lib/esm/shapes/serializePathToNodeData.js +12 -11
- package/lib/esm/shapes/serializePathToNodeData.js.map +1 -1
- package/lib/esm/shapes/validation.js +16 -12
- package/lib/esm/shapes/validation.js.map +1 -1
- package/lib/esm/sparql/irToAlgebra.d.ts +7 -2
- package/lib/esm/sparql/irToAlgebra.js +415 -181
- package/lib/esm/sparql/irToAlgebra.js.map +1 -1
- package/lib/esm/utils/Package.js +6 -3
- package/lib/esm/utils/Package.js.map +1 -1
- package/package.json +1 -1
|
@@ -21,7 +21,19 @@ const XSD_DOUBLE = xsd.double.id;
|
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Helpers
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
24
|
+
/**
|
|
25
|
+
* The IRI terms of triples (subjects, objects, ids of mutations and `.for()` targets) are built
|
|
26
|
+
* here, so this is where a blank-node label used as an id is refused: `_:b0` is SPARQL /
|
|
27
|
+
* N-Triples blank-node syntax, never an IRI (an IRI scheme starts with a letter). Filter
|
|
28
|
+
* expressions build their IRIs separately (`iri_expr`) and are not checked.
|
|
29
|
+
*/
|
|
24
30
|
function iriTerm(value) {
|
|
31
|
+
if (typeof value === 'string' && value.startsWith('_:')) {
|
|
32
|
+
throw new Error(`${JSON.stringify(value)} is a blank-node label, not an IRI. A blank node has no id to ` +
|
|
33
|
+
`address it by: it cannot be linked, updated with update().for() or deleted by id. ` +
|
|
34
|
+
`Change it through the property that holds it (replace or unset that property, or ` +
|
|
35
|
+
`delete its owner).`);
|
|
36
|
+
}
|
|
25
37
|
return { kind: 'iri', value };
|
|
26
38
|
}
|
|
27
39
|
function varTerm(name) {
|
|
@@ -1422,55 +1434,84 @@ function fieldValueToTerms(value, options, datatype) {
|
|
|
1422
1434
|
return [];
|
|
1423
1435
|
}
|
|
1424
1436
|
/**
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1437
|
+
* True when the property's `sh:nodeKind` allows blank-node values (`sh:BlankNode`,
|
|
1438
|
+
* `sh:BlankNodeOrIRI`, `sh:BlankNodeOrLiteral`). A nested create under such a property may be
|
|
1439
|
+
* minted as a blank node, and a delete / replace through it removes its blank value.
|
|
1427
1440
|
*/
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
shacl.
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1441
|
+
function isBlankCapable(ps) {
|
|
1442
|
+
var _a;
|
|
1443
|
+
const nk = (_a = ps === null || ps === void 0 ? void 0 : ps.nodeKind) === null || _a === void 0 ? void 0 : _a.id;
|
|
1444
|
+
return nk === shacl.BlankNode.id || nk === shacl.BlankNodeOrIRI.id || nk === shacl.BlankNodeOrLiteral.id;
|
|
1445
|
+
}
|
|
1446
|
+
/** `label` (shape) of a property shape, for error messages. */
|
|
1447
|
+
function describeProperty(propertyId) {
|
|
1448
|
+
const ps = findPropertyShapeById(propertyId);
|
|
1449
|
+
return (ps === null || ps === void 0 ? void 0 : ps.label) ? `'${ps.label}' (${propertyId})` : propertyId;
|
|
1450
|
+
}
|
|
1437
1451
|
/**
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
1452
|
+
* How a nested create under `propertyId` is written, from the property's `sh:nodeKind`:
|
|
1453
|
+
*
|
|
1454
|
+
* | nodeKind | without id | with id |
|
|
1455
|
+
* |---|---|---|
|
|
1456
|
+
* | unset, `sh:IRI`, `sh:IRIOrLiteral` | IRI (minted) | IRI |
|
|
1457
|
+
* | `sh:BlankNode` | blank node | error: a blank node has no id |
|
|
1458
|
+
* | `sh:BlankNodeOrIRI` | blank node | IRI |
|
|
1459
|
+
* | `sh:BlankNodeOrLiteral` | blank node | error: an IRI is not allowed |
|
|
1460
|
+
* | `sh:Literal` | error | error |
|
|
1461
|
+
*
|
|
1462
|
+
* The rule is read from the receiving side's registry, so nothing about it travels on the wire.
|
|
1442
1463
|
*/
|
|
1443
|
-
function
|
|
1444
|
-
var _a;
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1464
|
+
function nestedNodeKind(propertyId, data) {
|
|
1465
|
+
var _a, _b;
|
|
1466
|
+
const nk = (_b = (_a = findPropertyShapeById(propertyId)) === null || _a === void 0 ? void 0 : _a.nodeKind) === null || _b === void 0 ? void 0 : _b.id;
|
|
1467
|
+
switch (nk) {
|
|
1468
|
+
case shacl.BlankNodeOrIRI.id:
|
|
1469
|
+
return data.id ? 'iri' : 'blank';
|
|
1470
|
+
case shacl.BlankNode.id:
|
|
1471
|
+
case shacl.BlankNodeOrLiteral.id:
|
|
1472
|
+
if (data.id) {
|
|
1473
|
+
throw new Error(`Property ${describeProperty(propertyId)} is ${nk === shacl.BlankNode.id ? 'sh:BlankNode' : 'sh:BlankNodeOrLiteral'}: ` +
|
|
1474
|
+
`its nested value is written as a blank node, which has no id, but the value carries ` +
|
|
1475
|
+
`the id ${data.id}.`);
|
|
1476
|
+
}
|
|
1477
|
+
return 'blank';
|
|
1478
|
+
case shacl.Literal.id:
|
|
1479
|
+
throw new Error(`Property ${describeProperty(propertyId)} is sh:Literal and cannot hold a nested node.`);
|
|
1480
|
+
default:
|
|
1481
|
+
return 'iri';
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* The triples of a nested create held by `subjectTerm` through `propertyId`: the edge, then the
|
|
1486
|
+
* nested node's own triples. `blanks` is the create's label minter; without one (the update
|
|
1487
|
+
* paths) a value that would be minted blank throws, see `generateNodeDataTriples`.
|
|
1488
|
+
*/
|
|
1489
|
+
function nestedCreateTriples(subjectTerm, propertyId, propertyTerm, data, options, blanks) {
|
|
1490
|
+
var _a, _b;
|
|
1491
|
+
const kind = nestedNodeKind(propertyId, data);
|
|
1492
|
+
if (kind === 'blank' && !blanks) {
|
|
1493
|
+
throw new Error(`Cannot write a blank node in an update: property ${describeProperty(propertyId)} allows ` +
|
|
1494
|
+
`blank nodes and its nested value has no id. An update writes its INSERT once per WHERE ` +
|
|
1495
|
+
`solution, which would duplicate the blank node. Write the value with the create of its ` +
|
|
1496
|
+
`parent${((_b = (_a = findPropertyShapeById(propertyId)) === null || _a === void 0 ? void 0 : _a.nodeKind) === null || _b === void 0 ? void 0 : _b.id) === shacl.BlankNodeOrIRI.id ? ', or give it an id to write it as an IRI' : ''}.`);
|
|
1497
|
+
}
|
|
1498
|
+
const nested = generateNodeDataTriples(data, options, blanks, kind);
|
|
1499
|
+
return [tripleOf(subjectTerm, propertyTerm, nested.term), ...nested.triples];
|
|
1453
1500
|
}
|
|
1454
1501
|
/**
|
|
1455
1502
|
* Recursively generate triples for an IRNodeData (used in create and nested creates).
|
|
1456
1503
|
* Returns the term used for this node and all generated triples.
|
|
1457
1504
|
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1505
|
+
* `kind` is decided by the caller from the property that holds the node (`nestedNodeKind`);
|
|
1506
|
+
* the root of a create is always an IRI. A blank node gets a fresh label and only its field
|
|
1507
|
+
* triples, no `rdf:type`: its shape is known from the property, and a SHACL path operator, the
|
|
1508
|
+
* case blank nodes exist for here, must be the subject of exactly one triple.
|
|
1462
1509
|
*/
|
|
1463
|
-
function generateNodeDataTriples(data, options, blanks) {
|
|
1510
|
+
function generateNodeDataTriples(data, options, blanks, kind = 'iri') {
|
|
1464
1511
|
const triples = [];
|
|
1465
1512
|
let subjectTerm;
|
|
1466
|
-
if (
|
|
1467
|
-
|
|
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
|
-
}
|
|
1513
|
+
if (kind === 'blank') {
|
|
1514
|
+
// Callers only pass 'blank' with a minter (`nestedCreateTriples` checks).
|
|
1474
1515
|
subjectTerm = { kind: 'blank', label: `b${blanks.next++}` };
|
|
1475
1516
|
}
|
|
1476
1517
|
else {
|
|
@@ -1482,20 +1523,16 @@ function generateNodeDataTriples(data, options, blanks) {
|
|
|
1482
1523
|
}
|
|
1483
1524
|
// Field triples
|
|
1484
1525
|
for (const field of data.fields) {
|
|
1485
|
-
// Resolve the SHACL PropertyShapeData id to its declared `path` URI.
|
|
1486
1526
|
const propertyTerm = resolvePropertyPredicateTerm(field.property);
|
|
1487
|
-
|
|
1527
|
+
// Skip undefined/null values in create
|
|
1528
|
+
if (field.value === undefined || field.value === null) {
|
|
1488
1529
|
continue;
|
|
1489
1530
|
}
|
|
1490
|
-
// Handle arrays (
|
|
1531
|
+
// Handle arrays (multi-value fields)
|
|
1491
1532
|
if (Array.isArray(field.value)) {
|
|
1492
1533
|
for (const item of field.value) {
|
|
1493
1534
|
if (item && typeof item === 'object' && 'shape' in item && 'fields' in item) {
|
|
1494
|
-
|
|
1495
|
-
assertBlankIsOwned(item, field.property, propertyTerm, !!data.blank);
|
|
1496
|
-
const nested = generateNodeDataTriples(item, options, blanks);
|
|
1497
|
-
triples.push(tripleOf(subjectTerm, propertyTerm, nested.term));
|
|
1498
|
-
triples.push(...nested.triples);
|
|
1535
|
+
triples.push(...nestedCreateTriples(subjectTerm, field.property, propertyTerm, item, options, blanks));
|
|
1499
1536
|
}
|
|
1500
1537
|
else {
|
|
1501
1538
|
const terms = fieldValueToTerms(item, options, resolvePropertyDatatype(field.property));
|
|
@@ -1508,13 +1545,10 @@ function generateNodeDataTriples(data, options, blanks) {
|
|
|
1508
1545
|
}
|
|
1509
1546
|
// Handle nested IRNodeData
|
|
1510
1547
|
if (typeof field.value === 'object' && 'shape' in field.value && 'fields' in field.value) {
|
|
1511
|
-
|
|
1512
|
-
const nested = generateNodeDataTriples(field.value, options, blanks);
|
|
1513
|
-
triples.push(tripleOf(subjectTerm, propertyTerm, nested.term));
|
|
1514
|
-
triples.push(...nested.triples);
|
|
1548
|
+
triples.push(...nestedCreateTriples(subjectTerm, field.property, propertyTerm, field.value, options, blanks));
|
|
1515
1549
|
continue;
|
|
1516
1550
|
}
|
|
1517
|
-
// Simple
|
|
1551
|
+
// Simple value
|
|
1518
1552
|
const terms = fieldValueToTerms(field.value, options, resolvePropertyDatatype(field.property));
|
|
1519
1553
|
for (const term of terms) {
|
|
1520
1554
|
triples.push(tripleOf(subjectTerm, propertyTerm, term));
|
|
@@ -1526,9 +1560,6 @@ function generateNodeDataTriples(data, options, blanks) {
|
|
|
1526
1560
|
* Converts an IRCreateMutation to a SparqlInsertDataPlan.
|
|
1527
1561
|
*/
|
|
1528
1562
|
export function createToAlgebra(query, options) {
|
|
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
1563
|
const { triples } = generateNodeDataTriples(query.data, options, { next: 0 });
|
|
1533
1564
|
return {
|
|
1534
1565
|
type: 'insert_data',
|
|
@@ -1562,7 +1593,7 @@ function keptReferences(value) {
|
|
|
1562
1593
|
* Shared between updateToAlgebra (IRI subject) and updateWhereToAlgebra (variable subject).
|
|
1563
1594
|
*/
|
|
1564
1595
|
function processUpdateFields(data, subjectTerm, options) {
|
|
1565
|
-
var _a, _b;
|
|
1596
|
+
var _a, _b, _c;
|
|
1566
1597
|
const deletePatterns = [];
|
|
1567
1598
|
const insertPatterns = [];
|
|
1568
1599
|
const oldValueTriples = [];
|
|
@@ -1580,7 +1611,8 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1580
1611
|
// Only a `contains` property that declares a value shape deletes the old value's own
|
|
1581
1612
|
// triples. Without one (a polymorphic slot like `sh:path`) the value may be a shared
|
|
1582
1613
|
// IRI, so it is left to the cascade, which deletes it only when it is dependent-typed.
|
|
1583
|
-
const
|
|
1614
|
+
const valueShapeId = isContainsField ? (_b = (_a = findPropertyShapeById(field.property)) === null || _a === void 0 ? void 0 : _a.valueShape) === null || _b === void 0 ? void 0 : _b.id : undefined;
|
|
1615
|
+
const hasValueShape = !!valueShapeId;
|
|
1584
1616
|
// Check for set modification ({add, remove})
|
|
1585
1617
|
if (field.value &&
|
|
1586
1618
|
typeof field.value === 'object' &&
|
|
@@ -1593,7 +1625,7 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1593
1625
|
if (setMod.remove) {
|
|
1594
1626
|
// A value that is removed and added back in the same modification stays: its edge
|
|
1595
1627
|
// is deleted and re-inserted (a no-op), and the node itself is not touched.
|
|
1596
|
-
const readdedIds = new Set(((
|
|
1628
|
+
const readdedIds = new Set(((_c = setMod.add) !== null && _c !== void 0 ? _c : []).filter(isBareNodeReference).map((ref) => ref.id));
|
|
1597
1629
|
setMod.remove.forEach((removeItem, ri) => {
|
|
1598
1630
|
const removeId = removeItem.id;
|
|
1599
1631
|
const removeTerm = iriTerm(removeId);
|
|
@@ -1607,6 +1639,13 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1607
1639
|
const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, removeTerm, `ucr_${suffix}_${ri}_`);
|
|
1608
1640
|
deletePatterns.push(...self.deletePatterns);
|
|
1609
1641
|
cascadeOptionals.push(...self.whereOptionals);
|
|
1642
|
+
const blank = valueShapeId
|
|
1643
|
+
? subjectBlankCascade(valueShapeId, removeTerm, [edge], `ucr_${suffix}_${ri}_vb_`)
|
|
1644
|
+
: null;
|
|
1645
|
+
if (blank) {
|
|
1646
|
+
deletePatterns.push(...blank.deletePatterns);
|
|
1647
|
+
cascadeOptionals.push(blank.whereBlock);
|
|
1648
|
+
}
|
|
1610
1649
|
}
|
|
1611
1650
|
// The edge leads the cascade too: with a zero-or-more path the removed node is
|
|
1612
1651
|
// itself a match, and it may only be deleted while it is still owned here.
|
|
@@ -1619,9 +1658,7 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1619
1658
|
if (setMod.add) {
|
|
1620
1659
|
for (const addItem of setMod.add) {
|
|
1621
1660
|
if (addItem && typeof addItem === 'object' && 'shape' in addItem && 'fields' in addItem) {
|
|
1622
|
-
|
|
1623
|
-
insertPatterns.push(tripleOf(subjectTerm, propertyTerm, nested.term));
|
|
1624
|
-
insertPatterns.push(...nested.triples);
|
|
1661
|
+
insertPatterns.push(...nestedCreateTriples(subjectTerm, field.property, propertyTerm, addItem, options));
|
|
1625
1662
|
}
|
|
1626
1663
|
else {
|
|
1627
1664
|
const terms = fieldValueToTerms(addItem, options, resolvePropertyDatatype(field.property));
|
|
@@ -1633,6 +1670,16 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1633
1670
|
}
|
|
1634
1671
|
continue;
|
|
1635
1672
|
}
|
|
1673
|
+
// Replace / unset / overwrite of a blank-capable property: its old blank value and the
|
|
1674
|
+
// blank nodes below it go (the branch binds `old_${suffix}` through the edge itself).
|
|
1675
|
+
const fieldShape = findPropertyShapeById(field.property);
|
|
1676
|
+
if (isBlankCapable(fieldShape)) {
|
|
1677
|
+
const blank = buildBlankCascade([tripleOf(subjectTerm, propertyTerm, varTerm(`old_${suffix}`))], varTerm(`old_${suffix}`), null, blankWalkPredicates([fieldShape]), `ub_${suffix}_`);
|
|
1678
|
+
if (blank) {
|
|
1679
|
+
deletePatterns.push(...blank.deletePatterns);
|
|
1680
|
+
cascadeOptionals.push(blank.whereBlock);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1636
1683
|
// Non-set-modification replace of a `contains` property: cascade the old value's
|
|
1637
1684
|
// subtree (every branch below binds `old_${suffix}` in the WHERE).
|
|
1638
1685
|
if (isContainsField) {
|
|
@@ -1641,6 +1688,7 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1641
1688
|
propertyTerm,
|
|
1642
1689
|
keep: keptReferences(field.value),
|
|
1643
1690
|
hasValueShape,
|
|
1691
|
+
valueShapeId,
|
|
1644
1692
|
});
|
|
1645
1693
|
}
|
|
1646
1694
|
// Unset (undefined/null) — delete only
|
|
@@ -1657,9 +1705,7 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1657
1705
|
oldValueTriples.push(tripleOf(subjectTerm, propertyTerm, oldVar));
|
|
1658
1706
|
for (const item of field.value) {
|
|
1659
1707
|
if (item && typeof item === 'object' && 'shape' in item && 'fields' in item) {
|
|
1660
|
-
|
|
1661
|
-
insertPatterns.push(tripleOf(subjectTerm, propertyTerm, nested.term));
|
|
1662
|
-
insertPatterns.push(...nested.triples);
|
|
1708
|
+
insertPatterns.push(...nestedCreateTriples(subjectTerm, field.property, propertyTerm, item, options));
|
|
1663
1709
|
}
|
|
1664
1710
|
else {
|
|
1665
1711
|
const terms = fieldValueToTerms(item, options, resolvePropertyDatatype(field.property));
|
|
@@ -1675,9 +1721,7 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1675
1721
|
const oldVar = varTerm(`old_${suffix}`);
|
|
1676
1722
|
deletePatterns.push(tripleOf(subjectTerm, propertyTerm, oldVar));
|
|
1677
1723
|
oldValueTriples.push(tripleOf(subjectTerm, propertyTerm, oldVar));
|
|
1678
|
-
|
|
1679
|
-
insertPatterns.push(tripleOf(subjectTerm, propertyTerm, nested.term));
|
|
1680
|
-
insertPatterns.push(...nested.triples);
|
|
1724
|
+
insertPatterns.push(...nestedCreateTriples(subjectTerm, field.property, propertyTerm, field.value, options));
|
|
1681
1725
|
continue;
|
|
1682
1726
|
}
|
|
1683
1727
|
// IRExpression — computed value update (e.g. p.age.plus(1))
|
|
@@ -1728,12 +1772,20 @@ function processUpdateFields(data, subjectTerm, options) {
|
|
|
1728
1772
|
// (exclusive ownership via the edge — backlog 032) then cascade any owned descendants.
|
|
1729
1773
|
// Values re-sent as a bare `{id}` are kept: their blocks are filtered, so re-sending the
|
|
1730
1774
|
// current value is a no-op (the edge is deleted and re-inserted).
|
|
1731
|
-
containsOldVars.forEach(({ oldVar, propertyTerm, keep, hasValueShape }, n) => {
|
|
1775
|
+
containsOldVars.forEach(({ oldVar, propertyTerm, keep, hasValueShape, valueShapeId }, n) => {
|
|
1732
1776
|
const oldTerm = varTerm(oldVar);
|
|
1733
1777
|
if (hasValueShape) {
|
|
1734
1778
|
const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, `uc${n}_`, keep);
|
|
1735
1779
|
deletePatterns.push(...self.deletePatterns);
|
|
1736
1780
|
cascadeOptionals.push(...self.whereOptionals);
|
|
1781
|
+
// The old value's own blank values go with it.
|
|
1782
|
+
const blank = valueShapeId
|
|
1783
|
+
? subjectBlankCascade(valueShapeId, oldTerm, [tripleOf(subjectTerm, propertyTerm, oldTerm)], `uc${n}_vb_`)
|
|
1784
|
+
: null;
|
|
1785
|
+
if (blank) {
|
|
1786
|
+
deletePatterns.push(...blank.deletePatterns);
|
|
1787
|
+
cascadeOptionals.push(keepFilter(blank.whereBlock, oldTerm, keep));
|
|
1788
|
+
}
|
|
1737
1789
|
}
|
|
1738
1790
|
// Every cascade block binds `?old` itself through the owning edge, like the
|
|
1739
1791
|
// self-delete block does. Without it an absent old value leaves `?old` unbound and
|
|
@@ -1971,6 +2023,27 @@ function ownedValueShapePredicates(shapeId) {
|
|
|
1971
2023
|
}
|
|
1972
2024
|
return [...predicates];
|
|
1973
2025
|
}
|
|
2026
|
+
/**
|
|
2027
|
+
* The blank-node cascade of the immediately-owned children of `shapeId` on `predicate` (see
|
|
2028
|
+
* `ownedValueShapePredicates`): the blank values of every value shape its `contains` properties
|
|
2029
|
+
* on that predicate declare. `ownerTriples` bind the child.
|
|
2030
|
+
*/
|
|
2031
|
+
function ownedChildBlankCascade(shapeId, predicate, childTerm, ownerTriples, varPrefix) {
|
|
2032
|
+
var _a, _b, _c;
|
|
2033
|
+
const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
|
|
2034
|
+
if (!nodeShape)
|
|
2035
|
+
return [];
|
|
2036
|
+
const valueShapes = new Set();
|
|
2037
|
+
for (const ps of getPropertyShapes(nodeShape, true)) {
|
|
2038
|
+
if (ps.contains && ((_c = ps.valueShape) === null || _c === void 0 ? void 0 : _c.id) && getSimplePathId(ps.path) === predicate) {
|
|
2039
|
+
valueShapes.add(ps.valueShape.id);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return [...valueShapes].flatMap((valueShapeId, i) => {
|
|
2043
|
+
const cascade = subjectBlankCascade(valueShapeId, childTerm, ownerTriples, `${varPrefix}${i}_`);
|
|
2044
|
+
return cascade ? [cascade] : [];
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
1974
2047
|
/** True when the shape (or an ancestor) declares at least one `contains` property. */
|
|
1975
2048
|
function shapeHasContainsProperty(shapeId) {
|
|
1976
2049
|
var _a, _b;
|
|
@@ -1980,25 +2053,41 @@ function shapeHasContainsProperty(shapeId) {
|
|
|
1980
2053
|
return getPropertyShapes(nodeShape, true)
|
|
1981
2054
|
.some((ps) => ps.contains);
|
|
1982
2055
|
}
|
|
1983
|
-
// Memoized registry scan for cascade lowering — called once per update/delete and
|
|
1984
|
-
// again per owned-cascade item inside loops. Guarded by registry size (same rationale
|
|
1985
|
-
// as predicateTermCache). Callers treat the result as read-only.
|
|
1986
2056
|
let containmentCache = null;
|
|
1987
2057
|
let containmentCacheSize = -1;
|
|
1988
2058
|
/** Gather contains-predicate IRIs and dependent targetClass IRIs from the registry. */
|
|
1989
2059
|
export function collectContainment() {
|
|
1990
|
-
var _a;
|
|
2060
|
+
var _a, _b, _c;
|
|
1991
2061
|
const version = getRegistryVersion();
|
|
1992
2062
|
if (containmentCache && version === containmentCacheSize) {
|
|
1993
2063
|
return containmentCache;
|
|
1994
2064
|
}
|
|
1995
2065
|
const containsPreds = new Set();
|
|
1996
2066
|
const dependentTypes = new Set();
|
|
2067
|
+
const dependentBlankProps = new Map();
|
|
2068
|
+
// Per targetClass, the predicates some shape targeting it declares NOT blank-capable.
|
|
2069
|
+
const plainPredsByType = new Map();
|
|
1997
2070
|
for (const nodeShape of getAllNodeShapes().values()) {
|
|
1998
2071
|
if (!nodeShape)
|
|
1999
2072
|
continue;
|
|
2000
|
-
if (
|
|
2001
|
-
|
|
2073
|
+
if ((_a = nodeShape.targetClass) === null || _a === void 0 ? void 0 : _a.id) {
|
|
2074
|
+
const typeId = nodeShape.targetClass.id;
|
|
2075
|
+
for (const ps of getPropertyShapes(nodeShape, true)) {
|
|
2076
|
+
const predicate = getSimplePathId(ps.path);
|
|
2077
|
+
if (!predicate || isBlankCapable(ps))
|
|
2078
|
+
continue;
|
|
2079
|
+
if (!plainPredsByType.has(typeId))
|
|
2080
|
+
plainPredsByType.set(typeId, new Set());
|
|
2081
|
+
plainPredsByType.get(typeId).add(predicate);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
if (nodeShape.dependent && ((_b = nodeShape.targetClass) === null || _b === void 0 ? void 0 : _b.id)) {
|
|
2085
|
+
const typeId = nodeShape.targetClass.id;
|
|
2086
|
+
dependentTypes.add(typeId);
|
|
2087
|
+
const blank = getPropertyShapes(nodeShape, true).filter(isBlankCapable);
|
|
2088
|
+
if (blank.length > 0) {
|
|
2089
|
+
dependentBlankProps.set(typeId, [...((_c = dependentBlankProps.get(typeId)) !== null && _c !== void 0 ? _c : []), ...blank]);
|
|
2090
|
+
}
|
|
2002
2091
|
}
|
|
2003
2092
|
const propertyShapes = getPropertyShapes(nodeShape, false);
|
|
2004
2093
|
for (const ps of propertyShapes) {
|
|
@@ -2009,7 +2098,24 @@ export function collectContainment() {
|
|
|
2009
2098
|
}
|
|
2010
2099
|
}
|
|
2011
2100
|
}
|
|
2012
|
-
|
|
2101
|
+
// A typed node only says its class, not which of the shapes targeting it wrote it. When the
|
|
2102
|
+
// shapes disagree on a predicate (`PathList.first` is blank-capable, `List.first` is not, both
|
|
2103
|
+
// target `rdf:List`), the cascade does not follow it: a user list's blank `rdf:first` member
|
|
2104
|
+
// is content the list does not own.
|
|
2105
|
+
for (const [typeId, props] of dependentBlankProps) {
|
|
2106
|
+
const plain = plainPredsByType.get(typeId);
|
|
2107
|
+
if (!plain)
|
|
2108
|
+
continue;
|
|
2109
|
+
const kept = props.filter((ps) => {
|
|
2110
|
+
const predicate = getSimplePathId(ps.path);
|
|
2111
|
+
return !!predicate && !plain.has(predicate);
|
|
2112
|
+
});
|
|
2113
|
+
if (kept.length > 0)
|
|
2114
|
+
dependentBlankProps.set(typeId, kept);
|
|
2115
|
+
else
|
|
2116
|
+
dependentBlankProps.delete(typeId);
|
|
2117
|
+
}
|
|
2118
|
+
containmentCache = { containsPreds: [...containsPreds], dependentTypes: [...dependentTypes], dependentBlankProps };
|
|
2013
2119
|
containmentCacheSize = version;
|
|
2014
2120
|
return containmentCache;
|
|
2015
2121
|
}
|
|
@@ -2085,6 +2191,202 @@ function reachableDependentTypes(shapeId) {
|
|
|
2085
2191
|
reachableCache.set(shapeId, result);
|
|
2086
2192
|
return result;
|
|
2087
2193
|
}
|
|
2194
|
+
// ---------------------------------------------------------------------------
|
|
2195
|
+
// Blank-node cascade: blank values are deleted through the blank-capable properties
|
|
2196
|
+
// (`isBlankCapable`) that hold them, never through `contains`.
|
|
2197
|
+
// ---------------------------------------------------------------------------
|
|
2198
|
+
const shapeById = (id) => { var _a, _b; return (_b = (_a = getShapeClass(id)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(id); };
|
|
2199
|
+
/** The blank-capable property shapes of a shape (own and inherited) and of its sub-shapes. */
|
|
2200
|
+
function blankCapableProperties(shapeId) {
|
|
2201
|
+
const out = [];
|
|
2202
|
+
for (const sid of [shapeId, ...getSubShapes(shapeId).map((sub) => sub.id)]) {
|
|
2203
|
+
const nodeShape = shapeById(sid);
|
|
2204
|
+
if (!nodeShape)
|
|
2205
|
+
continue;
|
|
2206
|
+
for (const ps of getPropertyShapes(nodeShape, true)) {
|
|
2207
|
+
if (isBlankCapable(ps))
|
|
2208
|
+
out.push(ps);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
return out;
|
|
2212
|
+
}
|
|
2213
|
+
/** The simple-path predicates of property shapes, deduplicated in order. */
|
|
2214
|
+
function simplePredicates(props) {
|
|
2215
|
+
const predicates = new Set();
|
|
2216
|
+
for (const ps of props) {
|
|
2217
|
+
const predicate = getSimplePathId(ps.path);
|
|
2218
|
+
if (predicate)
|
|
2219
|
+
predicates.add(predicate);
|
|
2220
|
+
}
|
|
2221
|
+
return [...predicates];
|
|
2222
|
+
}
|
|
2223
|
+
let blankWalkCache = new Map();
|
|
2224
|
+
let blankWalkCacheVersion = -1;
|
|
2225
|
+
/**
|
|
2226
|
+
* The predicates the blank-node cascade follows *below* a blank value held by one of `start`:
|
|
2227
|
+
* the blank-capable properties of every shape the value (and, transitively, its blank values)
|
|
2228
|
+
* can have — a property's value shape and its sub-shapes. A blank-capable property without a
|
|
2229
|
+
* value shape is polymorphic (`sh:path`, the `PathNode` slots, `PathList.first`) and may hold
|
|
2230
|
+
* any shape, so it yields every blank-capable predicate in the registry, the same rule
|
|
2231
|
+
* `reachableDependentTypes` applies to `contains`. Precision is not what keeps the walk safe:
|
|
2232
|
+
* `buildBlankCascade` refuses any node reached through an IRI.
|
|
2233
|
+
*/
|
|
2234
|
+
function blankWalkPredicates(start) {
|
|
2235
|
+
const version = getRegistryVersion();
|
|
2236
|
+
if (version !== blankWalkCacheVersion) {
|
|
2237
|
+
blankWalkCache = new Map();
|
|
2238
|
+
blankWalkCacheVersion = version;
|
|
2239
|
+
}
|
|
2240
|
+
const key = start.map((ps) => ps.id).join('|');
|
|
2241
|
+
const cached = blankWalkCache.get(key);
|
|
2242
|
+
if (cached)
|
|
2243
|
+
return cached;
|
|
2244
|
+
let everything = false;
|
|
2245
|
+
const predicates = [];
|
|
2246
|
+
const seen = new Set();
|
|
2247
|
+
const queue = [];
|
|
2248
|
+
const follow = (ps) => {
|
|
2249
|
+
var _a;
|
|
2250
|
+
if (!((_a = ps.valueShape) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
2251
|
+
everything = true;
|
|
2252
|
+
return;
|
|
2253
|
+
}
|
|
2254
|
+
if (!seen.has(ps.valueShape.id)) {
|
|
2255
|
+
seen.add(ps.valueShape.id);
|
|
2256
|
+
queue.push(ps.valueShape.id);
|
|
2257
|
+
}
|
|
2258
|
+
};
|
|
2259
|
+
start.forEach(follow);
|
|
2260
|
+
while (queue.length > 0 && !everything) {
|
|
2261
|
+
for (const ps of blankCapableProperties(queue.shift())) {
|
|
2262
|
+
predicates.push(ps);
|
|
2263
|
+
follow(ps);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (everything) {
|
|
2267
|
+
predicates.length = 0;
|
|
2268
|
+
for (const nodeShape of getAllNodeShapes().values()) {
|
|
2269
|
+
if (!nodeShape)
|
|
2270
|
+
continue;
|
|
2271
|
+
for (const ps of getPropertyShapes(nodeShape, false)) {
|
|
2272
|
+
if (isBlankCapable(ps))
|
|
2273
|
+
predicates.push(ps);
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
const result = simplePredicates(predicates);
|
|
2278
|
+
blankWalkCache.set(key, result);
|
|
2279
|
+
return result;
|
|
2280
|
+
}
|
|
2281
|
+
/** A SPARQL property-path term over `predicates`: `(p1|…|pn)`, then `*` / `+` when given. */
|
|
2282
|
+
function predicatePathTerm(predicates, repeat) {
|
|
2283
|
+
const step = predicates.length === 1 ? { id: predicates[0] } : { alt: predicates.map((id) => ({ id })) };
|
|
2284
|
+
const expr = repeat === 'zeroOrMore' ? { zeroOrMore: step } : repeat === 'oneOrMore' ? { oneOrMore: step } : step;
|
|
2285
|
+
return { kind: 'path', value: pathExprToSparql(expr), uris: collectPathUris(expr) };
|
|
2286
|
+
}
|
|
2287
|
+
const isBlankExpr = (name) => ({
|
|
2288
|
+
kind: 'function_expr',
|
|
2289
|
+
name: 'isBlank',
|
|
2290
|
+
args: [{ kind: 'variable_expr', name }],
|
|
2291
|
+
});
|
|
2292
|
+
/**
|
|
2293
|
+
* One UNION branch that deletes the blank values `owner` holds through `first`, and every blank
|
|
2294
|
+
* node below them through `inner`:
|
|
2295
|
+
*
|
|
2296
|
+
* ```sparql
|
|
2297
|
+
* DELETE { ?bn ?bnp ?bno }
|
|
2298
|
+
* WHERE {
|
|
2299
|
+
* …ownerTriples… <owner> (first…) ?bn0 . FILTER(isBlank(?bn0))
|
|
2300
|
+
* ?bn0 (inner…)* ?bn . ?bn ?bnp ?bno . FILTER(isBlank(?bn))
|
|
2301
|
+
* FILTER NOT EXISTS { ?bn0 (inner…)* ?mid . ?mid (inner…)+ ?bn . FILTER(!isBlank(?mid)) }
|
|
2302
|
+
* }
|
|
2303
|
+
* ```
|
|
2304
|
+
*
|
|
2305
|
+
* `first: null` means the owner term *is* the blank value (the old value of a replaced
|
|
2306
|
+
* blank-capable property, bound by `ownerTriples`). Every deleted node is a blank node, so no IRI
|
|
2307
|
+
* — a predicate such as `schema:image`, an entity a list merely lists — is ever deleted. The
|
|
2308
|
+
* `NOT EXISTS` refuses a blank node reached *through* an IRI: a property path cannot constrain
|
|
2309
|
+
* the nodes between its ends, and the walk may include every blank-capable predicate
|
|
2310
|
+
* (`blankWalkPredicates`), so without it a blank list's `rdf:first` could lead into a listed
|
|
2311
|
+
* entity and delete that entity's blank values. Blank structures core writes are trees, so the
|
|
2312
|
+
* guard removes nothing they own. Returns null when there is nothing to follow.
|
|
2313
|
+
*/
|
|
2314
|
+
function buildBlankCascade(ownerTriples, ownerTerm, first, inner, varPrefix) {
|
|
2315
|
+
if (first !== null && first.length === 0)
|
|
2316
|
+
return null;
|
|
2317
|
+
let bn0Name;
|
|
2318
|
+
const reach = [...ownerTriples];
|
|
2319
|
+
if (first === null) {
|
|
2320
|
+
if (ownerTerm.kind !== 'variable')
|
|
2321
|
+
return null;
|
|
2322
|
+
bn0Name = ownerTerm.name;
|
|
2323
|
+
}
|
|
2324
|
+
else {
|
|
2325
|
+
bn0Name = `${varPrefix}bn0`;
|
|
2326
|
+
reach.push(tripleOf(ownerTerm, predicatePathTerm(first), varTerm(bn0Name)));
|
|
2327
|
+
}
|
|
2328
|
+
const bn0 = varTerm(bn0Name);
|
|
2329
|
+
const reached = {
|
|
2330
|
+
type: 'filter',
|
|
2331
|
+
expression: isBlankExpr(bn0Name),
|
|
2332
|
+
inner: { type: 'bgp', triples: reach },
|
|
2333
|
+
};
|
|
2334
|
+
const bp = varTerm(`${varPrefix}bnp`);
|
|
2335
|
+
const bo = varTerm(`${varPrefix}bno`);
|
|
2336
|
+
if (inner.length === 0) {
|
|
2337
|
+
const own = tripleOf(bn0, bp, bo);
|
|
2338
|
+
return {
|
|
2339
|
+
deletePatterns: [own],
|
|
2340
|
+
whereBlock: { type: 'join', left: reached, right: { type: 'bgp', triples: [own] } },
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
const bnName = `${varPrefix}bn`;
|
|
2344
|
+
const bn = varTerm(bnName);
|
|
2345
|
+
const mid = varTerm(`${varPrefix}bnm`);
|
|
2346
|
+
const own = tripleOf(bn, bp, bo);
|
|
2347
|
+
const throughIri = {
|
|
2348
|
+
kind: 'exists_expr',
|
|
2349
|
+
negated: true,
|
|
2350
|
+
pattern: {
|
|
2351
|
+
type: 'filter',
|
|
2352
|
+
expression: { kind: 'not_expr', inner: isBlankExpr(`${varPrefix}bnm`) },
|
|
2353
|
+
inner: {
|
|
2354
|
+
type: 'bgp',
|
|
2355
|
+
triples: [
|
|
2356
|
+
tripleOf(bn0, predicatePathTerm(inner, 'zeroOrMore'), mid),
|
|
2357
|
+
tripleOf(mid, predicatePathTerm(inner, 'oneOrMore'), bn),
|
|
2358
|
+
],
|
|
2359
|
+
},
|
|
2360
|
+
},
|
|
2361
|
+
};
|
|
2362
|
+
return {
|
|
2363
|
+
deletePatterns: [own],
|
|
2364
|
+
whereBlock: {
|
|
2365
|
+
type: 'filter',
|
|
2366
|
+
expression: { kind: 'logical_expr', op: 'and', exprs: [isBlankExpr(bnName), throughIri] },
|
|
2367
|
+
inner: {
|
|
2368
|
+
type: 'join',
|
|
2369
|
+
// Filter the blank values before the inner walk runs from each of them.
|
|
2370
|
+
left: reached,
|
|
2371
|
+
right: {
|
|
2372
|
+
type: 'bgp',
|
|
2373
|
+
triples: [tripleOf(bn0, predicatePathTerm(inner, 'zeroOrMore'), bn), own],
|
|
2374
|
+
},
|
|
2375
|
+
},
|
|
2376
|
+
},
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* The blank-node cascade of a subject: its blank values through the blank-capable properties of
|
|
2381
|
+
* its shape, and their blank descendants. `ownerTriples` anchors a variable subject (the bulk
|
|
2382
|
+
* deletes pass its type triple).
|
|
2383
|
+
*/
|
|
2384
|
+
function subjectBlankCascade(shapeId, subjectTerm, ownerTriples, varPrefix) {
|
|
2385
|
+
const props = blankCapableProperties(shapeId);
|
|
2386
|
+
if (props.length === 0)
|
|
2387
|
+
return null;
|
|
2388
|
+
return buildBlankCascade(ownerTriples, subjectTerm, simplePredicates(props), blankWalkPredicates(props), varPrefix);
|
|
2389
|
+
}
|
|
2088
2390
|
/**
|
|
2089
2391
|
* Build DELETE patterns + WHERE OPTIONAL blocks that cascade-delete the owned subtree
|
|
2090
2392
|
* reachable from `rootTerm` via `contains` edges. `varPrefix` keeps generated variables
|
|
@@ -2132,50 +2434,24 @@ export function buildOwnedCascade(rootTerm, varPrefix, anchor, keep, onlyTypes)
|
|
|
2132
2434
|
],
|
|
2133
2435
|
}, rootTerm, keep));
|
|
2134
2436
|
});
|
|
2135
|
-
//
|
|
2136
|
-
//
|
|
2137
|
-
//
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
const bo = varTerm(`${varPrefix}bno`);
|
|
2153
|
-
const isBlank = (name) => ({
|
|
2154
|
-
kind: 'function_expr',
|
|
2155
|
-
name: 'isBlank',
|
|
2156
|
-
args: [{ kind: 'variable_expr', name }],
|
|
2437
|
+
// The blank values of every dependent node deleted above, through the blank-capable
|
|
2438
|
+
// properties of its type's shapes (a property shape's `sh:path`). Blank nodes carry no type,
|
|
2439
|
+
// so the typed blocks cannot see them; see `buildBlankCascade`.
|
|
2440
|
+
dependentTypes.forEach((typeId, i) => {
|
|
2441
|
+
const props = containment.dependentBlankProps.get(typeId);
|
|
2442
|
+
if (!props)
|
|
2443
|
+
return;
|
|
2444
|
+
const owner = varTerm(`${varPrefix}bo${i}`);
|
|
2445
|
+
const cascade = buildBlankCascade([
|
|
2446
|
+
...(anchor ? [anchor] : []),
|
|
2447
|
+
tripleOf(rootTerm, pathTerm, owner),
|
|
2448
|
+
tripleOf(owner, iriTerm(RDF_TYPE), iriTerm(typeId)),
|
|
2449
|
+
], owner, simplePredicates(props), blankWalkPredicates(props), `${varPrefix}b${i}_`);
|
|
2450
|
+
if (!cascade)
|
|
2451
|
+
return;
|
|
2452
|
+
deletePatterns.push(...cascade.deletePatterns);
|
|
2453
|
+
whereOptionals.push(keepFilter(cascade.whereBlock, rootTerm, keep));
|
|
2157
2454
|
});
|
|
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));
|
|
2179
2455
|
return { deletePatterns, whereOptionals };
|
|
2180
2456
|
}
|
|
2181
2457
|
/**
|
|
@@ -2290,7 +2566,19 @@ export function deleteToAlgebra(query, _options) {
|
|
|
2290
2566
|
type: 'bgp',
|
|
2291
2567
|
triples: [tripleOf(subjectTerm, iriTerm(predicate), child), childOwn],
|
|
2292
2568
|
});
|
|
2569
|
+
// …and the child's own blank values.
|
|
2570
|
+
for (const blank of ownedChildBlankCascade(query.shape, predicate, child, [tripleOf(subjectTerm, iriTerm(predicate), child)], `d${n}${idx}_b`)) {
|
|
2571
|
+
deletePatterns.push(...blank.deletePatterns);
|
|
2572
|
+
blocks.push(blank.whereBlock);
|
|
2573
|
+
}
|
|
2293
2574
|
});
|
|
2575
|
+
// The subject's own blank values (through its blank-capable properties) and the blank
|
|
2576
|
+
// nodes below them.
|
|
2577
|
+
const blank = subjectBlankCascade(query.shape, subjectTerm, [], `b${idx}_`);
|
|
2578
|
+
if (blank) {
|
|
2579
|
+
deletePatterns.push(...blank.deletePatterns);
|
|
2580
|
+
blocks.push(blank.whereBlock);
|
|
2581
|
+
}
|
|
2294
2582
|
// Cascade-delete the owned subtree — only for shapes that actually own something
|
|
2295
2583
|
// (have a `contains` property); a plain entity delete is left untouched.
|
|
2296
2584
|
if (hasContains && cascadeTypes.length > 0) {
|
|
@@ -2311,74 +2599,12 @@ export function deleteToAlgebra(query, _options) {
|
|
|
2311
2599
|
whereAlgebra,
|
|
2312
2600
|
};
|
|
2313
2601
|
}
|
|
2314
|
-
// ---------------------------------------------------------------------------
|
|
2315
|
-
// Blank node tree walking for schema-aware delete cleanup
|
|
2316
|
-
// ---------------------------------------------------------------------------
|
|
2317
|
-
/**
|
|
2318
|
-
* Checks whether a PropertyShapeData points to blank nodes (sh:BlankNode or
|
|
2319
|
-
* sh:BlankNodeOrIRI). Returns true when the property's range *may* include
|
|
2320
|
-
* blank node values that should be cleaned up on delete.
|
|
2321
|
-
*/
|
|
2322
|
-
function isBlankNodeProperty(prop) {
|
|
2323
|
-
var _a;
|
|
2324
|
-
const nk = (_a = prop.nodeKind) === null || _a === void 0 ? void 0 : _a.id;
|
|
2325
|
-
if (!nk)
|
|
2326
|
-
return false;
|
|
2327
|
-
return nk === shacl.BlankNode.id || nk === shacl.BlankNodeOrIRI.id;
|
|
2328
|
-
}
|
|
2329
|
-
/**
|
|
2330
|
-
* Builds DELETE patterns + one WHERE block per blank node reachable from `parentVar`
|
|
2331
|
-
* through blank-node-typed properties, recursing into each property's valueShape for
|
|
2332
|
-
* nested blank nodes (e.g. Person → Address (blank) → GeoPoint (blank)).
|
|
2333
|
-
*
|
|
2334
|
-
* - DELETE: `?bnN ?pN ?oN .` (wildcard all triples on the blank node)
|
|
2335
|
-
* - WHERE block: the path of edges from the root down to `?bnN`, `?bnN ?pN ?oN`, and
|
|
2336
|
-
* `FILTER(isBlank(…))` for every blank node on that path.
|
|
2337
|
-
*
|
|
2338
|
-
* Each block carries its own path from the root, so the blocks are independent of one
|
|
2339
|
-
* another and are joined with a UNION by the caller. Nested as OPTIONALs, a node's
|
|
2340
|
-
* triples multiplied with every blank node below and beside it.
|
|
2341
|
-
*/
|
|
2342
|
-
function blankNodeBlocks(shapeId, parentVar, path, counter, deletePatterns, blocks) {
|
|
2343
|
-
var _a, _b, _c;
|
|
2344
|
-
const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
|
|
2345
|
-
if (!nodeShape)
|
|
2346
|
-
return;
|
|
2347
|
-
const props = getPropertyShapes(nodeShape, true);
|
|
2348
|
-
for (const prop of props) {
|
|
2349
|
-
if (!isBlankNodeProperty(prop))
|
|
2350
|
-
continue;
|
|
2351
|
-
const n = counter.next++;
|
|
2352
|
-
const bnVar = `bn${n}`;
|
|
2353
|
-
const wildcardTriple = tripleOf(varTerm(bnVar), varTerm(`p${n}`), varTerm(`o${n}`));
|
|
2354
|
-
deletePatterns.push(wildcardTriple);
|
|
2355
|
-
const here = {
|
|
2356
|
-
triples: [
|
|
2357
|
-
...path.triples,
|
|
2358
|
-
tripleOf(varTerm(parentVar), resolvePropertyPredicateTerm(prop.id), varTerm(bnVar)),
|
|
2359
|
-
],
|
|
2360
|
-
bnVars: [...path.bnVars, bnVar],
|
|
2361
|
-
};
|
|
2362
|
-
let block = { type: 'bgp', triples: [...here.triples, wildcardTriple] };
|
|
2363
|
-
for (const v of here.bnVars) {
|
|
2364
|
-
block = {
|
|
2365
|
-
type: 'filter',
|
|
2366
|
-
expression: { kind: 'function_expr', name: 'isBlank', args: [{ kind: 'variable_expr', name: v }] },
|
|
2367
|
-
inner: block,
|
|
2368
|
-
};
|
|
2369
|
-
}
|
|
2370
|
-
blocks.push(block);
|
|
2371
|
-
if ((_c = prop.valueShape) === null || _c === void 0 ? void 0 : _c.id) {
|
|
2372
|
-
blankNodeBlocks(prop.valueShape.id, bnVar, here, counter, deletePatterns, blocks);
|
|
2373
|
-
}
|
|
2374
|
-
}
|
|
2375
|
-
}
|
|
2376
2602
|
/**
|
|
2377
2603
|
* The cleanup blocks of a bulk delete, already joined with the root's own triples as ONE
|
|
2378
2604
|
* UNION so their matches add up instead of multiplying; `null` when there is nothing to
|
|
2379
2605
|
* clean up (the root wildcard then stays in the required BGP).
|
|
2380
2606
|
*
|
|
2381
|
-
* - The blank-node
|
|
2607
|
+
* - The blank-node cascade of the shape's blank-capable properties (`subjectBlankCascade`).
|
|
2382
2608
|
* - For a shape that owns something (a `contains` property), the same owned cleanup as a
|
|
2383
2609
|
* delete by id: the immediately-owned children of every `contains` property with a value
|
|
2384
2610
|
* shape, and the dependent cascade, rooted at the subject variable. Each owned block is led
|
|
@@ -2388,7 +2614,11 @@ function blankNodeBlocks(shapeId, parentVar, path, counter, deletePatterns, bloc
|
|
|
2388
2614
|
*/
|
|
2389
2615
|
function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, typeTriple, deletePatterns) {
|
|
2390
2616
|
const blocks = [];
|
|
2391
|
-
|
|
2617
|
+
const blank = subjectBlankCascade(shapeId, varTerm(subjectVar), [typeTriple], 'b_');
|
|
2618
|
+
if (blank) {
|
|
2619
|
+
deletePatterns.push(...blank.deletePatterns);
|
|
2620
|
+
blocks.push(blank.whereBlock);
|
|
2621
|
+
}
|
|
2392
2622
|
if (shapeHasContainsProperty(shapeId)) {
|
|
2393
2623
|
const subject = varTerm(subjectVar);
|
|
2394
2624
|
ownedValueShapePredicates(shapeId).forEach((predicate, n) => {
|
|
@@ -2399,6 +2629,10 @@ function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, typeTriple, deletePa
|
|
|
2399
2629
|
type: 'bgp',
|
|
2400
2630
|
triples: [typeTriple, tripleOf(subject, iriTerm(predicate), child), childOwn],
|
|
2401
2631
|
});
|
|
2632
|
+
for (const blank of ownedChildBlankCascade(shapeId, predicate, child, [typeTriple, tripleOf(subject, iriTerm(predicate), child)], `d${n}_b`)) {
|
|
2633
|
+
deletePatterns.push(...blank.deletePatterns);
|
|
2634
|
+
blocks.push(blank.whereBlock);
|
|
2635
|
+
}
|
|
2402
2636
|
});
|
|
2403
2637
|
const cascadeTypes = reachableDependentTypes(shapeId);
|
|
2404
2638
|
if (cascadeTypes.length > 0) {
|