@_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.
Files changed (37) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +54 -7
  3. package/lib/esm/queries/IRMutation.js +1 -1
  4. package/lib/esm/queries/IRMutation.js.map +1 -1
  5. package/lib/esm/queries/IntermediateRepresentation.d.ts +0 -5
  6. package/lib/esm/queries/IntermediateRepresentation.js.map +1 -1
  7. package/lib/esm/queries/MutationQuery.js +9 -19
  8. package/lib/esm/queries/MutationQuery.js.map +1 -1
  9. package/lib/esm/queries/MutationSerialization.d.ts +6 -5
  10. package/lib/esm/queries/MutationSerialization.js +30 -13
  11. package/lib/esm/queries/MutationSerialization.js.map +1 -1
  12. package/lib/esm/queries/QueryFactory.d.ts +0 -6
  13. package/lib/esm/queries/QueryFactory.js.map +1 -1
  14. package/lib/esm/queries/lowerMutationJSON.js +1 -6
  15. package/lib/esm/queries/lowerMutationJSON.js.map +1 -1
  16. package/lib/esm/shapes/List.d.ts +0 -1
  17. package/lib/esm/shapes/List.js.map +1 -1
  18. package/lib/esm/shapes/PathList.d.ts +27 -0
  19. package/lib/esm/shapes/PathList.js +63 -0
  20. package/lib/esm/shapes/PathList.js.map +1 -0
  21. package/lib/esm/shapes/PathNode.d.ts +10 -9
  22. package/lib/esm/shapes/PathNode.js +21 -13
  23. package/lib/esm/shapes/PathNode.js.map +1 -1
  24. package/lib/esm/shapes/index.d.ts +1 -0
  25. package/lib/esm/shapes/index.js +1 -0
  26. package/lib/esm/shapes/index.js.map +1 -1
  27. package/lib/esm/shapes/serializePathToNodeData.d.ts +12 -11
  28. package/lib/esm/shapes/serializePathToNodeData.js +12 -11
  29. package/lib/esm/shapes/serializePathToNodeData.js.map +1 -1
  30. package/lib/esm/shapes/validation.js +16 -12
  31. package/lib/esm/shapes/validation.js.map +1 -1
  32. package/lib/esm/sparql/irToAlgebra.d.ts +7 -2
  33. package/lib/esm/sparql/irToAlgebra.js +415 -181
  34. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  35. package/lib/esm/utils/Package.js +6 -3
  36. package/lib/esm/utils/Package.js.map +1 -1
  37. 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
- * 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`).
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
- 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
- ];
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
- * 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.
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 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.`);
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
- * 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.
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 (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
- }
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
- if (field.value === null || field.value === undefined) {
1527
+ // Skip undefined/null values in create
1528
+ if (field.value === undefined || field.value === null) {
1488
1529
  continue;
1489
1530
  }
1490
- // Handle arrays (including mixed arrays of references and nested creates)
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
- // Nested create
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
- assertBlankIsOwned(field.value, field.property, propertyTerm, !!data.blank);
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 values
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 hasValueShape = isContainsField && !!((_a = findPropertyShapeById(field.property)) === null || _a === void 0 ? void 0 : _a.valueShape);
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(((_b = setMod.add) !== null && _b !== void 0 ? _b : []).filter(isBareNodeReference).map((ref) => ref.id));
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
- const nested = generateNodeDataTriples(addItem, options);
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
- const nested = generateNodeDataTriples(item, options);
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
- const nested = generateNodeDataTriples(field.value, options);
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 (nodeShape.dependent && ((_a = nodeShape.targetClass) === null || _a === void 0 ? void 0 : _a.id)) {
2001
- dependentTypes.add(nodeShape.targetClass.id);
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
- containmentCache = { containsPreds: [...containsPreds], dependentTypes: [...dependentTypes] };
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
- // 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 }],
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 blocks of the shape's blank-node properties.
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
- blankNodeBlocks(shapeId, subjectVar, { triples: [], bnVars: [] }, { next: 1 }, deletePatterns, blocks);
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) {