@_linked/core 2.30.1 → 2.32.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 (59) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +107 -60
  3. package/lib/esm/ontologies/linked-core.js +3 -2
  4. package/lib/esm/ontologies/linked-core.js.map +1 -1
  5. package/lib/esm/ontologies/shacl.d.ts +3 -0
  6. package/lib/esm/ontologies/shacl.js +6 -0
  7. package/lib/esm/ontologies/shacl.js.map +1 -1
  8. package/lib/esm/queries/IRMutation.js +1 -1
  9. package/lib/esm/queries/IRMutation.js.map +1 -1
  10. package/lib/esm/queries/IntermediateRepresentation.d.ts +0 -5
  11. package/lib/esm/queries/IntermediateRepresentation.js.map +1 -1
  12. package/lib/esm/queries/MutationQuery.js +0 -15
  13. package/lib/esm/queries/MutationQuery.js.map +1 -1
  14. package/lib/esm/queries/MutationSerialization.d.ts +6 -5
  15. package/lib/esm/queries/MutationSerialization.js +1 -9
  16. package/lib/esm/queries/MutationSerialization.js.map +1 -1
  17. package/lib/esm/queries/QueryFactory.d.ts +0 -6
  18. package/lib/esm/queries/QueryFactory.js.map +1 -1
  19. package/lib/esm/queries/lowerMutationJSON.js +1 -6
  20. package/lib/esm/queries/lowerMutationJSON.js.map +1 -1
  21. package/lib/esm/shapes/List.class.d.ts +3 -3
  22. package/lib/esm/shapes/List.class.js +3 -3
  23. package/lib/esm/shapes/List.class.js.map +1 -1
  24. package/lib/esm/shapes/List.d.ts +0 -1
  25. package/lib/esm/shapes/List.js.map +1 -1
  26. package/lib/esm/shapes/PathList.d.ts +24 -0
  27. package/lib/esm/shapes/PathList.js +60 -0
  28. package/lib/esm/shapes/PathList.js.map +1 -0
  29. package/lib/esm/shapes/PathNode.d.ts +8 -9
  30. package/lib/esm/shapes/PathNode.js +20 -14
  31. package/lib/esm/shapes/PathNode.js.map +1 -1
  32. package/lib/esm/shapes/SHACL.d.ts +15 -13
  33. package/lib/esm/shapes/SHACL.js +8 -0
  34. package/lib/esm/shapes/SHACL.js.map +1 -1
  35. package/lib/esm/shapes/index.d.ts +1 -0
  36. package/lib/esm/shapes/index.js +1 -0
  37. package/lib/esm/shapes/index.js.map +1 -1
  38. package/lib/esm/shapes/nodeShapeData.d.ts +4 -1
  39. package/lib/esm/shapes/nodeShapeData.js.map +1 -1
  40. package/lib/esm/shapes/serializePathToNodeData.d.ts +12 -11
  41. package/lib/esm/shapes/serializePathToNodeData.js +12 -11
  42. package/lib/esm/shapes/serializePathToNodeData.js.map +1 -1
  43. package/lib/esm/shapes/singleOwnerConstraint.d.ts +5 -7
  44. package/lib/esm/shapes/singleOwnerConstraint.js +65 -21
  45. package/lib/esm/shapes/singleOwnerConstraint.js.map +1 -1
  46. package/lib/esm/shapes/syncShapes.js +2 -2
  47. package/lib/esm/shapes/syncShapes.js.map +1 -1
  48. package/lib/esm/shapes/validation.js +2 -2
  49. package/lib/esm/shapes/validation.js.map +1 -1
  50. package/lib/esm/sparql/irToAlgebra.d.ts +15 -33
  51. package/lib/esm/sparql/irToAlgebra.js +680 -431
  52. package/lib/esm/sparql/irToAlgebra.js.map +1 -1
  53. package/lib/esm/sparql/ownedLinkCheck.d.ts +6 -10
  54. package/lib/esm/sparql/ownedLinkCheck.js +10 -11
  55. package/lib/esm/sparql/ownedLinkCheck.js.map +1 -1
  56. package/lib/esm/utils/Package.d.ts +3 -2
  57. package/lib/esm/utils/Package.js +21 -8
  58. package/lib/esm/utils/Package.js.map +1 -1
  59. package/package.json +1 -1
@@ -6,12 +6,13 @@ 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, getSubShapes, getTargetClassId, } from '../utils/ShapeClass.js';
9
+ import { getAllNodeShapes, getNodeShape, getRegistryVersion, getShapeClass, getSubShapes, getTargetClassId, isSubShapeOf, } from '../utils/ShapeClass.js';
10
10
  import { UnresolvedContextError } from '../queries/QueryContext.js';
11
11
  // ---------------------------------------------------------------------------
12
12
  // Constants
13
13
  // ---------------------------------------------------------------------------
14
14
  const RDF_TYPE = rdf.type.id;
15
+ const RDF_NIL = rdf.nil.id;
15
16
  const XSD_DATETIME = xsd.dateTime.id;
16
17
  const XSD_DATE = xsd.date.id;
17
18
  const XSD_TIME = xsd.time.id;
@@ -21,7 +22,19 @@ const XSD_DOUBLE = xsd.double.id;
21
22
  // ---------------------------------------------------------------------------
22
23
  // Helpers
23
24
  // ---------------------------------------------------------------------------
25
+ /**
26
+ * The IRI terms of triples (subjects, objects, ids of mutations and `.for()` targets) are built
27
+ * here, so this is where a blank-node label used as an id is refused: `_:b0` is SPARQL /
28
+ * N-Triples blank-node syntax, never an IRI (an IRI scheme starts with a letter). Filter
29
+ * expressions build their IRIs separately (`iri_expr`) and are not checked.
30
+ */
24
31
  function iriTerm(value) {
32
+ if (typeof value === 'string' && value.startsWith('_:')) {
33
+ throw new Error(`${JSON.stringify(value)} is a blank-node label, not an IRI. A blank node has no id to ` +
34
+ `address it by: it cannot be linked, updated with update().for() or deleted by id. ` +
35
+ `Change it through the property that holds it (replace or unset that property, or ` +
36
+ `delete its owner).`);
37
+ }
25
38
  return { kind: 'iri', value };
26
39
  }
27
40
  function varTerm(name) {
@@ -1422,55 +1435,84 @@ function fieldValueToTerms(value, options, datatype) {
1422
1435
  return [];
1423
1436
  }
1424
1437
  /**
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`).
1438
+ * True when the property's `sh:nodeKind` allows blank-node values (`sh:BlankNode`,
1439
+ * `sh:BlankNodeOrIRI`, `sh:BlankNodeOrLiteral`). A nested create under such a property may be
1440
+ * minted as a blank node, and a delete / replace through it removes its blank value.
1427
1441
  */
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
- ];
1442
+ function isBlankCapable(ps) {
1443
+ var _a;
1444
+ const nk = (_a = ps === null || ps === void 0 ? void 0 : ps.nodeKind) === null || _a === void 0 ? void 0 : _a.id;
1445
+ return nk === shacl.BlankNode.id || nk === shacl.BlankNodeOrIRI.id || nk === shacl.BlankNodeOrLiteral.id;
1446
+ }
1447
+ /** `label` (shape) of a property shape, for error messages. */
1448
+ function describeProperty(propertyId) {
1449
+ const ps = findPropertyShapeById(propertyId);
1450
+ return (ps === null || ps === void 0 ? void 0 : ps.label) ? `'${ps.label}' (${propertyId})` : propertyId;
1451
+ }
1437
1452
  /**
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.
1453
+ * How a nested create under `propertyId` is written, from the property's `sh:nodeKind`:
1454
+ *
1455
+ * | nodeKind | without id | with id |
1456
+ * |---|---|---|
1457
+ * | unset, `sh:IRI`, `sh:IRIOrLiteral` | IRI (minted) | IRI |
1458
+ * | `sh:BlankNode` | blank node | error: a blank node has no id |
1459
+ * | `sh:BlankNodeOrIRI` | blank node | IRI |
1460
+ * | `sh:BlankNodeOrLiteral` | blank node | error: an IRI is not allowed |
1461
+ * | `sh:Literal` | error | error |
1462
+ *
1463
+ * The rule is read from the receiving side's registry, so nothing about it travels on the wire.
1442
1464
  */
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.`);
1465
+ function nestedNodeKind(propertyId, data) {
1466
+ var _a, _b;
1467
+ const nk = (_b = (_a = findPropertyShapeById(propertyId)) === null || _a === void 0 ? void 0 : _a.nodeKind) === null || _b === void 0 ? void 0 : _b.id;
1468
+ switch (nk) {
1469
+ case shacl.BlankNodeOrIRI.id:
1470
+ return data.id ? 'iri' : 'blank';
1471
+ case shacl.BlankNode.id:
1472
+ case shacl.BlankNodeOrLiteral.id:
1473
+ if (data.id) {
1474
+ throw new Error(`Property ${describeProperty(propertyId)} is ${nk === shacl.BlankNode.id ? 'sh:BlankNode' : 'sh:BlankNodeOrLiteral'}: ` +
1475
+ `its nested value is written as a blank node, which has no id, but the value carries ` +
1476
+ `the id ${data.id}.`);
1477
+ }
1478
+ return 'blank';
1479
+ case shacl.Literal.id:
1480
+ throw new Error(`Property ${describeProperty(propertyId)} is sh:Literal and cannot hold a nested node.`);
1481
+ default:
1482
+ return 'iri';
1483
+ }
1484
+ }
1485
+ /**
1486
+ * The triples of a nested create held by `subjectTerm` through `propertyId`: the edge, then the
1487
+ * nested node's own triples. `blanks` is the create's label minter; without one (the update
1488
+ * paths) a value that would be minted blank throws, see `generateNodeDataTriples`.
1489
+ */
1490
+ function nestedCreateTriples(subjectTerm, propertyId, propertyTerm, data, options, blanks) {
1491
+ var _a, _b;
1492
+ const kind = nestedNodeKind(propertyId, data);
1493
+ if (kind === 'blank' && !blanks) {
1494
+ throw new Error(`Cannot write a blank node in an update: property ${describeProperty(propertyId)} allows ` +
1495
+ `blank nodes and its nested value has no id. An update writes its INSERT once per WHERE ` +
1496
+ `solution, which would duplicate the blank node. Write the value with the create of its ` +
1497
+ `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' : ''}.`);
1498
+ }
1499
+ const nested = generateNodeDataTriples(data, options, blanks, kind);
1500
+ return [tripleOf(subjectTerm, propertyTerm, nested.term), ...nested.triples];
1453
1501
  }
1454
1502
  /**
1455
1503
  * Recursively generate triples for an IRNodeData (used in create and nested creates).
1456
1504
  * Returns the term used for this node and all generated triples.
1457
1505
  *
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.
1506
+ * `kind` is decided by the caller from the property that holds the node (`nestedNodeKind`);
1507
+ * the root of a create is always an IRI. A blank node gets a fresh label and only its field
1508
+ * triples, no `rdf:type`: its shape is known from the property, and a SHACL path operator, the
1509
+ * case blank nodes exist for here, must be the subject of exactly one triple.
1462
1510
  */
1463
- function generateNodeDataTriples(data, options, blanks) {
1511
+ function generateNodeDataTriples(data, options, blanks, kind = 'iri') {
1464
1512
  const triples = [];
1465
1513
  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
- }
1514
+ if (kind === 'blank') {
1515
+ // Callers only pass 'blank' with a minter (`nestedCreateTriples` checks).
1474
1516
  subjectTerm = { kind: 'blank', label: `b${blanks.next++}` };
1475
1517
  }
1476
1518
  else {
@@ -1482,20 +1524,16 @@ function generateNodeDataTriples(data, options, blanks) {
1482
1524
  }
1483
1525
  // Field triples
1484
1526
  for (const field of data.fields) {
1485
- // Resolve the SHACL PropertyShapeData id to its declared `path` URI.
1486
1527
  const propertyTerm = resolvePropertyPredicateTerm(field.property);
1487
- if (field.value === null || field.value === undefined) {
1528
+ // Skip undefined/null values in create
1529
+ if (field.value === undefined || field.value === null) {
1488
1530
  continue;
1489
1531
  }
1490
- // Handle arrays (including mixed arrays of references and nested creates)
1532
+ // Handle arrays (multi-value fields)
1491
1533
  if (Array.isArray(field.value)) {
1492
1534
  for (const item of field.value) {
1493
1535
  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);
1536
+ triples.push(...nestedCreateTriples(subjectTerm, field.property, propertyTerm, item, options, blanks));
1499
1537
  }
1500
1538
  else {
1501
1539
  const terms = fieldValueToTerms(item, options, resolvePropertyDatatype(field.property));
@@ -1508,13 +1546,10 @@ function generateNodeDataTriples(data, options, blanks) {
1508
1546
  }
1509
1547
  // Handle nested IRNodeData
1510
1548
  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);
1549
+ triples.push(...nestedCreateTriples(subjectTerm, field.property, propertyTerm, field.value, options, blanks));
1515
1550
  continue;
1516
1551
  }
1517
- // Simple values
1552
+ // Simple value
1518
1553
  const terms = fieldValueToTerms(field.value, options, resolvePropertyDatatype(field.property));
1519
1554
  for (const term of terms) {
1520
1555
  triples.push(tripleOf(subjectTerm, propertyTerm, term));
@@ -1526,9 +1561,6 @@ function generateNodeDataTriples(data, options, blanks) {
1526
1561
  * Converts an IRCreateMutation to a SparqlInsertDataPlan.
1527
1562
  */
1528
1563
  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
1564
  const { triples } = generateNodeDataTriples(query.data, options, { next: 0 });
1533
1565
  return {
1534
1566
  type: 'insert_data',
@@ -1570,17 +1602,16 @@ function processUpdateFields(data, subjectTerm, options) {
1570
1602
  // Owned cleanup on `contains` replace/remove: dropping a `contains` edge must delete
1571
1603
  // the old object itself (its own triples) AND its owned subtree, not just unlink the
1572
1604
  // one-hop edge — otherwise the old node orphans (backlog 032).
1573
- const { containsPreds: updContainsPreds } = collectContainment();
1574
1605
  const containsOldVars = [];
1575
1606
  const cascadeOptionals = [];
1576
1607
  for (const field of data.fields) {
1577
1608
  const propertyTerm = resolvePropertyPredicateTerm(field.property);
1578
1609
  const suffix = propertySuffix(field.property);
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);
1610
+ const fieldShape = findPropertyShapeById(field.property);
1611
+ // Ownership is the property's own: a `contains` property with a value shape and a simple
1612
+ // path (an IRI predicate term). A plain property on a predicate another shape declares
1613
+ // `contains` owns nothing.
1614
+ const valueShapeId = propertyTerm.kind === 'iri' && (fieldShape === null || fieldShape === void 0 ? void 0 : fieldShape.contains) ? (_a = fieldShape.valueShape) === null || _a === void 0 ? void 0 : _a.id : undefined;
1584
1615
  // Check for set modification ({add, remove})
1585
1616
  if (field.value &&
1586
1617
  typeof field.value === 'object' &&
@@ -1602,26 +1633,28 @@ function processUpdateFields(data, subjectTerm, options) {
1602
1633
  oldValueTriples.push(edge);
1603
1634
  // For a `contains` set, removing a value deletes the removed node itself
1604
1635
  // (its own triples) and its owned subtree — exclusive ownership via the edge.
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);
1636
+ if (valueShapeId && !readdedIds.has(removeId)) {
1637
+ const prefix = `ucr_${suffix}_${ri}_`;
1638
+ const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, removeTerm, prefix);
1639
+ deletePatterns.push(...self.deletePatterns);
1640
+ cascadeOptionals.push(...self.whereOptionals);
1641
+ const blank = subjectBlankCascade(valueShapeId, removeTerm, [edge], `${prefix}vb_`);
1642
+ if (blank) {
1643
+ deletePatterns.push(...blank.deletePatterns);
1644
+ cascadeOptionals.push(blank.whereBlock);
1610
1645
  }
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);
1614
- deletePatterns.push(...cascade.deletePatterns);
1615
- cascadeOptionals.push(...cascade.whereOptionals);
1646
+ // The edge leads every branch: the removed node's subtree may only be deleted
1647
+ // while the node is still owned here.
1648
+ const owned = ownedSubtreeBlocks(valueShapeId, removeTerm, [edge], prefix);
1649
+ deletePatterns.push(...owned.deletePatterns);
1650
+ cascadeOptionals.push(...owned.whereBlocks);
1616
1651
  }
1617
1652
  });
1618
1653
  }
1619
1654
  if (setMod.add) {
1620
1655
  for (const addItem of setMod.add) {
1621
1656
  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);
1657
+ insertPatterns.push(...nestedCreateTriples(subjectTerm, field.property, propertyTerm, addItem, options));
1625
1658
  }
1626
1659
  else {
1627
1660
  const terms = fieldValueToTerms(addItem, options, resolvePropertyDatatype(field.property));
@@ -1633,14 +1666,23 @@ function processUpdateFields(data, subjectTerm, options) {
1633
1666
  }
1634
1667
  continue;
1635
1668
  }
1669
+ // Replace / unset / overwrite of a blank-capable property: its old blank value and the
1670
+ // blank nodes below it go (the branch binds `old_${suffix}` through the edge itself).
1671
+ if (isBlankCapable(fieldShape)) {
1672
+ const blank = buildBlankCascade([tripleOf(subjectTerm, propertyTerm, varTerm(`old_${suffix}`))], varTerm(`old_${suffix}`), null, blankWalkPredicates([fieldShape]), `ub_${suffix}_`);
1673
+ if (blank) {
1674
+ deletePatterns.push(...blank.deletePatterns);
1675
+ cascadeOptionals.push(blank.whereBlock);
1676
+ }
1677
+ }
1636
1678
  // Non-set-modification replace of a `contains` property: cascade the old value's
1637
1679
  // subtree (every branch below binds `old_${suffix}` in the WHERE).
1638
- if (isContainsField) {
1680
+ if (valueShapeId) {
1639
1681
  containsOldVars.push({
1640
1682
  oldVar: `old_${suffix}`,
1641
1683
  propertyTerm,
1642
1684
  keep: keptReferences(field.value),
1643
- hasValueShape,
1685
+ valueShapeId,
1644
1686
  });
1645
1687
  }
1646
1688
  // Unset (undefined/null) — delete only
@@ -1657,9 +1699,7 @@ function processUpdateFields(data, subjectTerm, options) {
1657
1699
  oldValueTriples.push(tripleOf(subjectTerm, propertyTerm, oldVar));
1658
1700
  for (const item of field.value) {
1659
1701
  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);
1702
+ insertPatterns.push(...nestedCreateTriples(subjectTerm, field.property, propertyTerm, item, options));
1663
1703
  }
1664
1704
  else {
1665
1705
  const terms = fieldValueToTerms(item, options, resolvePropertyDatatype(field.property));
@@ -1675,9 +1715,7 @@ function processUpdateFields(data, subjectTerm, options) {
1675
1715
  const oldVar = varTerm(`old_${suffix}`);
1676
1716
  deletePatterns.push(tripleOf(subjectTerm, propertyTerm, oldVar));
1677
1717
  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);
1718
+ insertPatterns.push(...nestedCreateTriples(subjectTerm, field.property, propertyTerm, field.value, options));
1681
1719
  continue;
1682
1720
  }
1683
1721
  // IRExpression — computed value update (e.g. p.age.plus(1))
@@ -1728,19 +1766,24 @@ function processUpdateFields(data, subjectTerm, options) {
1728
1766
  // (exclusive ownership via the edge — backlog 032) then cascade any owned descendants.
1729
1767
  // Values re-sent as a bare `{id}` are kept: their blocks are filtered, so re-sending the
1730
1768
  // current value is a no-op (the edge is deleted and re-inserted).
1731
- containsOldVars.forEach(({ oldVar, propertyTerm, keep, hasValueShape }, n) => {
1769
+ containsOldVars.forEach(({ oldVar, propertyTerm, keep, valueShapeId }, n) => {
1732
1770
  const oldTerm = varTerm(oldVar);
1733
- if (hasValueShape) {
1734
- const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, `uc${n}_`, keep);
1735
- deletePatterns.push(...self.deletePatterns);
1736
- cascadeOptionals.push(...self.whereOptionals);
1737
- }
1738
- // Every cascade block binds `?old` itself through the owning edge, like the
1739
- // self-delete block does. Without it an absent old value leaves `?old` unbound and
1740
- // the block matches the owned subtree of EVERY node in the graph.
1741
- const cascade = buildOwnedCascade(oldTerm, `uc${n}_`, tripleOf(subjectTerm, propertyTerm, oldTerm), keep);
1742
- deletePatterns.push(...cascade.deletePatterns);
1743
- cascadeOptionals.push(...cascade.whereOptionals);
1771
+ const edge = tripleOf(subjectTerm, propertyTerm, oldTerm);
1772
+ const self = buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, `uc${n}_`, keep);
1773
+ deletePatterns.push(...self.deletePatterns);
1774
+ cascadeOptionals.push(...self.whereOptionals);
1775
+ // The old value's own blank values go with it.
1776
+ const blank = subjectBlankCascade(valueShapeId, oldTerm, [edge], `uc${n}_vb_`);
1777
+ if (blank) {
1778
+ deletePatterns.push(...blank.deletePatterns);
1779
+ cascadeOptionals.push(keepFilter(blank.whereBlock, oldTerm, keep));
1780
+ }
1781
+ // Every subtree branch binds `?old` itself through the owning edge, like the self-delete
1782
+ // block does. Without it an absent old value leaves `?old` unbound and the branch matches
1783
+ // the owned subtree of EVERY node in the graph.
1784
+ const owned = ownedSubtreeBlocks(valueShapeId, oldTerm, [edge], `uc${n}_`, keep);
1785
+ deletePatterns.push(...owned.deletePatterns);
1786
+ cascadeOptionals.push(...owned.whereBlocks);
1744
1787
  });
1745
1788
  return {
1746
1789
  deletePatterns,
@@ -1934,249 +1977,549 @@ function buildUpdateWhere(base, result, traversalPatterns, subjectTerm) {
1934
1977
  return whereAlgebra;
1935
1978
  }
1936
1979
  // ---------------------------------------------------------------------------
1937
- // Owned-subtree cascade
1938
- //
1939
- // Composition cleanup driven by two declarative flags:
1940
- // - a property marked `contains` → the cascade FOLLOWS that edge,
1941
- // - a shape marked `dependent` → its instances may be DELETED when reached.
1942
- // Both are read from the live registry, so no vocabulary is hardcoded. From a root
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.
1980
+ // Blank-node cascade: blank values are deleted through the blank-capable properties
1981
+ // (`isBlankCapable`) that hold them, never through `contains`.
1953
1982
  // ---------------------------------------------------------------------------
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)
1983
+ 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); };
1984
+ /** The blank-capable property shapes of a shape (own and inherited) and of its sub-shapes. */
1985
+ function blankCapableProperties(shapeId) {
1986
+ const out = [];
1987
+ for (const sid of [shapeId, ...getSubShapes(shapeId).map((sub) => sub.id)]) {
1988
+ const nodeShape = shapeById(sid);
1989
+ if (!nodeShape)
1967
1990
  continue;
1991
+ for (const ps of getPropertyShapes(nodeShape, true)) {
1992
+ if (isBlankCapable(ps))
1993
+ out.push(ps);
1994
+ }
1995
+ }
1996
+ return out;
1997
+ }
1998
+ /** The simple-path predicates of property shapes, deduplicated in order. */
1999
+ function simplePredicates(props) {
2000
+ const predicates = new Set();
2001
+ for (const ps of props) {
1968
2002
  const predicate = getSimplePathId(ps.path);
1969
2003
  if (predicate)
1970
2004
  predicates.add(predicate);
1971
2005
  }
1972
2006
  return [...predicates];
1973
2007
  }
1974
- /** True when the shape (or an ancestor) declares at least one `contains` property. */
1975
- function shapeHasContainsProperty(shapeId) {
1976
- var _a, _b;
1977
- const nodeShape = (_b = (_a = getShapeClass(shapeId)) === null || _a === void 0 ? void 0 : _a.shape) !== null && _b !== void 0 ? _b : getNodeShape(shapeId);
1978
- if (!nodeShape)
1979
- return false;
1980
- return getPropertyShapes(nodeShape, true)
1981
- .some((ps) => ps.contains);
1982
- }
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
- let containmentCache = null;
1987
- let containmentCacheSize = -1;
1988
- /** Gather contains-predicate IRIs and dependent targetClass IRIs from the registry. */
1989
- export function collectContainment() {
1990
- var _a;
1991
- const version = getRegistryVersion();
1992
- if (containmentCache && version === containmentCacheSize) {
1993
- return containmentCache;
1994
- }
1995
- const containsPreds = new Set();
1996
- const dependentTypes = new Set();
1997
- for (const nodeShape of getAllNodeShapes().values()) {
1998
- if (!nodeShape)
1999
- continue;
2000
- if (nodeShape.dependent && ((_a = nodeShape.targetClass) === null || _a === void 0 ? void 0 : _a.id)) {
2001
- dependentTypes.add(nodeShape.targetClass.id);
2002
- }
2003
- const propertyShapes = getPropertyShapes(nodeShape, false);
2004
- for (const ps of propertyShapes) {
2005
- if (ps.contains) {
2006
- const predId = getSimplePathId(ps.path);
2007
- if (predId)
2008
- containsPreds.add(predId);
2009
- }
2010
- }
2011
- }
2012
- containmentCache = { containsPreds: [...containsPreds], dependentTypes: [...dependentTypes] };
2013
- containmentCacheSize = version;
2014
- return containmentCache;
2015
- }
2016
- let reachableCache = new Map();
2017
- let reachableCacheVersion = -1;
2008
+ let blankWalkCache = new Map();
2009
+ let blankWalkCacheVersion = -1;
2018
2010
  /**
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.
2011
+ * The predicates the blank-node cascade follows *below* a blank value held by one of `start`:
2012
+ * the blank-capable properties of every shape the value (and, transitively, its blank values)
2013
+ * can have — a property's value shape and its sub-shapes. A blank-capable property without a
2014
+ * value shape is polymorphic (`sh:path`, the `PathNode` slots, `PathList.first`) and may hold
2015
+ * any shape, so it yields every blank-capable predicate in the registry. Precision is not what
2016
+ * keeps the walk safe:
2017
+ * `buildBlankCascade` refuses any node reached through an IRI.
2029
2018
  */
2030
- function reachableDependentTypes(shapeId) {
2031
- var _a, _b;
2019
+ function blankWalkPredicates(start) {
2032
2020
  const version = getRegistryVersion();
2033
- if (version !== reachableCacheVersion) {
2034
- reachableCache = new Map();
2035
- reachableCacheVersion = version;
2021
+ if (version !== blankWalkCacheVersion) {
2022
+ blankWalkCache = new Map();
2023
+ blankWalkCacheVersion = version;
2036
2024
  }
2037
- const cached = reachableCache.get(shapeId);
2025
+ const key = start.map((ps) => ps.id).join('|');
2026
+ const cached = blankWalkCache.get(key);
2038
2027
  if (cached)
2039
2028
  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();
2029
+ let everything = false;
2030
+ const predicates = [];
2046
2031
  const seen = new Set();
2047
2032
  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);
2033
+ const follow = (ps) => {
2034
+ var _a;
2035
+ if (!((_a = ps.valueShape) === null || _a === void 0 ? void 0 : _a.id)) {
2036
+ everything = true;
2037
+ return;
2038
+ }
2039
+ if (!seen.has(ps.valueShape.id)) {
2040
+ seen.add(ps.valueShape.id);
2041
+ queue.push(ps.valueShape.id);
2056
2042
  }
2057
2043
  };
2058
- visit(shapeId, false);
2059
- let everything = false;
2044
+ start.forEach(follow);
2060
2045
  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);
2046
+ for (const ps of blankCapableProperties(queue.shift())) {
2047
+ predicates.push(ps);
2048
+ follow(ps);
2073
2049
  }
2074
2050
  }
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);
2051
+ if (everything) {
2052
+ predicates.length = 0;
2053
+ for (const nodeShape of getAllNodeShapes().values()) {
2054
+ if (!nodeShape)
2055
+ continue;
2056
+ for (const ps of getPropertyShapes(nodeShape, false)) {
2057
+ if (isBlankCapable(ps))
2058
+ predicates.push(ps);
2081
2059
  }
2082
2060
  }
2083
2061
  }
2084
- const result = everything ? dependentTypes : dependentTypes.filter((t) => reached.has(t));
2085
- reachableCache.set(shapeId, result);
2062
+ const result = simplePredicates(predicates);
2063
+ blankWalkCache.set(key, result);
2086
2064
  return result;
2087
2065
  }
2066
+ /** A SPARQL property-path term over `predicates`: `(p1|…|pn)`, then `*` / `+` when given. */
2067
+ function predicatePathTerm(predicates, repeat) {
2068
+ const step = predicates.length === 1 ? { id: predicates[0] } : { alt: predicates.map((id) => ({ id })) };
2069
+ const expr = repeat === 'zeroOrMore' ? { zeroOrMore: step } : repeat === 'oneOrMore' ? { oneOrMore: step } : step;
2070
+ return { kind: 'path', value: pathExprToSparql(expr), uris: collectPathUris(expr) };
2071
+ }
2072
+ const isBlankExpr = (name) => ({
2073
+ kind: 'function_expr',
2074
+ name: 'isBlank',
2075
+ args: [{ kind: 'variable_expr', name }],
2076
+ });
2088
2077
  /**
2089
- * Build DELETE patterns + WHERE OPTIONAL blocks that cascade-delete the owned subtree
2090
- * reachable from `rootTerm` via `contains` edges. `varPrefix` keeps generated variables
2091
- * unique across multiple roots in one query. Returns empty when nothing is owned.
2078
+ * One UNION branch that deletes the blank values `owner` holds through `first`, and every blank
2079
+ * node below them through `inner`:
2080
+ *
2081
+ * ```sparql
2082
+ * DELETE { ?bn ?bnp ?bno }
2083
+ * WHERE {
2084
+ * …ownerTriples… <owner> (first…) ?bn0 . FILTER(isBlank(?bn0))
2085
+ * ?bn0 (inner…)* ?bn . ?bn ?bnp ?bno . FILTER(isBlank(?bn))
2086
+ * FILTER NOT EXISTS { ?bn0 (inner…)* ?mid . ?mid (inner…)+ ?bn . FILTER(!isBlank(?mid)) }
2087
+ * }
2088
+ * ```
2092
2089
  *
2093
- * `anchor`, when given, leads every block: the triple that binds a variable root, so
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.
2090
+ * `first: null` means the owner term *is* the blank value (the old value of a replaced
2091
+ * blank-capable property, bound by `ownerTriples`). Every deleted node is a blank node, so no IRI
2092
+ * — a predicate such as `schema:image`, an entity a list merely lists — is ever deleted. The
2093
+ * `NOT EXISTS` refuses a blank node reached *through* an IRI: a property path cannot constrain
2094
+ * the nodes between its ends, and the walk may include every blank-capable predicate
2095
+ * (`blankWalkPredicates`), so without it a blank list's `rdf:first` could lead into a listed
2096
+ * entity and delete that entity's blank values. Blank structures core writes are trees, so the
2097
+ * guard removes nothing they own. Returns null when there is nothing to follow.
2097
2098
  */
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;
2102
- if (containsPreds.length === 0 || dependentTypes.length === 0) {
2103
- return { deletePatterns: [], whereOptionals: [] };
2104
- }
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.
2110
- const pathExpr = containsPreds.length === 1
2111
- ? { zeroOrMore: { id: containsPreds[0] } }
2112
- : { zeroOrMore: { alt: containsPreds.map((id) => ({ id })) } };
2113
- const pathTerm = {
2114
- kind: 'path',
2115
- value: pathExprToSparql(pathExpr),
2116
- uris: collectPathUris(pathExpr),
2117
- };
2118
- const deletePatterns = [];
2119
- const whereOptionals = [];
2120
- dependentTypes.forEach((typeId, i) => {
2121
- const owned = varTerm(`${varPrefix}own${i}`);
2122
- const p = varTerm(`${varPrefix}op${i}`);
2123
- const o = varTerm(`${varPrefix}ov${i}`);
2124
- deletePatterns.push(tripleOf(owned, p, o));
2125
- whereOptionals.push(keepFilter({
2126
- type: 'bgp',
2127
- triples: [
2128
- ...(anchor ? [anchor] : []),
2129
- tripleOf(rootTerm, pathTerm, owned),
2130
- tripleOf(owned, iriTerm(RDF_TYPE), iriTerm(typeId)),
2131
- tripleOf(owned, p, o),
2132
- ],
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),
2099
+ function buildBlankCascade(ownerTriples, ownerTerm, first, inner, varPrefix) {
2100
+ if (first !== null && first.length === 0)
2101
+ return null;
2102
+ let bn0Name;
2103
+ const reach = [...ownerTriples];
2104
+ if (first === null) {
2105
+ if (ownerTerm.kind !== 'variable')
2106
+ return null;
2107
+ bn0Name = ownerTerm.name;
2108
+ }
2109
+ else {
2110
+ bn0Name = `${varPrefix}bn0`;
2111
+ reach.push(tripleOf(ownerTerm, predicatePathTerm(first), varTerm(bn0Name)));
2112
+ }
2113
+ const bn0 = varTerm(bn0Name);
2114
+ const reached = {
2115
+ type: 'filter',
2116
+ expression: isBlankExpr(bn0Name),
2117
+ inner: { type: 'bgp', triples: reach },
2148
2118
  };
2149
- const ownedBlank = varTerm(`${varPrefix}bn0`);
2150
- const blank = varTerm(`${varPrefix}bn`);
2151
2119
  const bp = varTerm(`${varPrefix}bnp`);
2152
2120
  const bo = varTerm(`${varPrefix}bno`);
2153
- const isBlank = (name) => ({
2154
- kind: 'function_expr',
2155
- name: 'isBlank',
2156
- args: [{ kind: 'variable_expr', name }],
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: {
2121
+ if (inner.length === 0) {
2122
+ const own = tripleOf(bn0, bp, bo);
2123
+ return {
2124
+ deletePatterns: [own],
2125
+ whereBlock: { type: 'join', left: reached, right: { type: 'bgp', triples: [own] } },
2126
+ };
2127
+ }
2128
+ const bnName = `${varPrefix}bn`;
2129
+ const bn = varTerm(bnName);
2130
+ const mid = varTerm(`${varPrefix}bnm`);
2131
+ const own = tripleOf(bn, bp, bo);
2132
+ const throughIri = {
2133
+ kind: 'exists_expr',
2134
+ negated: true,
2135
+ pattern: {
2136
+ type: 'filter',
2137
+ expression: { kind: 'not_expr', inner: isBlankExpr(`${varPrefix}bnm`) },
2138
+ inner: {
2139
+ type: 'bgp',
2140
+ triples: [
2141
+ tripleOf(bn0, predicatePathTerm(inner, 'zeroOrMore'), mid),
2142
+ tripleOf(mid, predicatePathTerm(inner, 'oneOrMore'), bn),
2143
+ ],
2144
+ },
2145
+ },
2146
+ };
2147
+ return {
2148
+ deletePatterns: [own],
2149
+ whereBlock: {
2150
+ type: 'filter',
2151
+ expression: { kind: 'logical_expr', op: 'and', exprs: [isBlankExpr(bnName), throughIri] },
2152
+ inner: {
2153
+ type: 'join',
2154
+ // Filter the blank values before the inner walk runs from each of them.
2155
+ left: reached,
2156
+ right: {
2169
2157
  type: 'bgp',
2170
- triples: [...(anchor ? [anchor] : []), tripleOf(rootTerm, pathTerm, ownedBlank)],
2158
+ triples: [tripleOf(bn0, predicatePathTerm(inner, 'zeroOrMore'), bn), own],
2171
2159
  },
2172
2160
  },
2173
- right: {
2174
- type: 'bgp',
2175
- triples: [tripleOf(ownedBlank, insidePathTerm, blank), tripleOf(blank, bp, bo)],
2176
- },
2177
2161
  },
2178
- }, rootTerm, keep));
2179
- return { deletePatterns, whereOptionals };
2162
+ };
2163
+ }
2164
+ /**
2165
+ * The blank-node cascade of a subject: its blank values through the blank-capable properties of
2166
+ * its shape, and their blank descendants. `ownerTriples` anchors a variable subject (the bulk
2167
+ * deletes pass its type triple).
2168
+ */
2169
+ function subjectBlankCascade(shapeId, subjectTerm, ownerTriples, varPrefix) {
2170
+ const props = blankCapableProperties(shapeId);
2171
+ if (props.length === 0)
2172
+ return null;
2173
+ return buildBlankCascade(ownerTriples, subjectTerm, simplePredicates(props), blankWalkPredicates(props), varPrefix);
2174
+ }
2175
+ // ---------------------------------------------------------------------------
2176
+ // Owned-subtree walk
2177
+ //
2178
+ // A `contains` property (with a value shape and a simple path) owns its value: replacing,
2179
+ // unsetting or set-removing the value, or deleting the subject, deletes the value node, then
2180
+ // the values that node's shape owns, and so on down. Ownership is decided per property: the
2181
+ // walk starts at the root's shape and follows only the `contains` properties the reached
2182
+ // shapes declare (own and inherited), so a plain property on a predicate that another shape
2183
+ // declares `contains` is never followed. A registered sub-shape that adds `contains`
2184
+ // properties contributes hops guarded by its concrete class (`?n a <SubClass>`). Every node
2185
+ // the walk reaches loses its own triples and its blank values; `dependent` plays no part.
2186
+ //
2187
+ // Recursion: a shape that owns itself through an unguarded edge (`List.rest`) is followed with
2188
+ // a closure (`rdf:rest*`), since every such edge from one of its nodes is its own. Any other
2189
+ // recursion (a cycle through several shapes, a sub-shape's edge back into the walk) is unrolled
2190
+ // into typed hops, up to `MAX_OWNED_RECURSION` repeated shapes per path; deeper values are left.
2191
+ // ---------------------------------------------------------------------------
2192
+ /**
2193
+ * How many times one walk path may re-enter a shape it already passed through (a cycle through
2194
+ * several shapes, or a sub-shape's edge back into its own kind). Values below are not deleted.
2195
+ */
2196
+ export const MAX_OWNED_RECURSION = 4;
2197
+ /** Above this many branches for one root shape, lowering warns once (see backlog 055). */
2198
+ export const OWNED_BRANCH_WARNING = 64;
2199
+ /** The simple-path predicate through which a property owns its value, or null when it does not. */
2200
+ function ownedPredicate(ps) {
2201
+ var _a, _b;
2202
+ if (!ps.contains || !((_a = ps.valueShape) === null || _a === void 0 ? void 0 : _a.id))
2203
+ return null;
2204
+ return (_b = getSimplePathId(ps.path)) !== null && _b !== void 0 ? _b : null;
2205
+ }
2206
+ let classHoldersCache = new Map();
2207
+ let classHoldersVersion = -1;
2208
+ /** Every registered shape id per `targetClass` (memoized per registry version). */
2209
+ function classHolders(classId) {
2210
+ var _a, _b, _c;
2211
+ const version = getRegistryVersion();
2212
+ if (version !== classHoldersVersion) {
2213
+ classHoldersCache = new Map();
2214
+ for (const nodeShape of getAllNodeShapes().values()) {
2215
+ const cls = (_a = nodeShape === null || nodeShape === void 0 ? void 0 : nodeShape.targetClass) === null || _a === void 0 ? void 0 : _a.id;
2216
+ if (!cls)
2217
+ continue;
2218
+ classHoldersCache.set(cls, [...((_b = classHoldersCache.get(cls)) !== null && _b !== void 0 ? _b : []), nodeShape.id]);
2219
+ }
2220
+ classHoldersVersion = version;
2221
+ }
2222
+ return (_c = classHoldersCache.get(classId)) !== null && _c !== void 0 ? _c : [];
2223
+ }
2224
+ /**
2225
+ * Why the added `contains` property `ps` of the sub-shape `sub` cannot be followed from nodes of
2226
+ * the sub-shape's class, or null when it can. The guard `?n a <C>` also matches the nodes of
2227
+ * every other shape targeting `C`, so it is refused when one of them is a super-shape of `sub`
2228
+ * (its own nodes would match) or declares the same predicate as a plain property (following it
2229
+ * would delete that shape's references). A holder that does not declare the predicate, or owns
2230
+ * it too, does not block it.
2231
+ */
2232
+ function blockedGuard(sub, ps) {
2233
+ var _a;
2234
+ const cls = (_a = sub.targetClass) === null || _a === void 0 ? void 0 : _a.id;
2235
+ if (!cls)
2236
+ return 'has no targetClass';
2237
+ const predicate = getSimplePathId(ps.path);
2238
+ for (const id of classHolders(cls)) {
2239
+ if (id === sub.id || isSubShapeOf(id, sub.id))
2240
+ continue;
2241
+ if (isSubShapeOf(sub.id, id))
2242
+ return `shares its targetClass with its super-shape '${id}'`;
2243
+ const holder = shapeById(id);
2244
+ const plain = holder && getPropertyShapes(holder, true).some((q) => !q.contains && getSimplePathId(q.path) === predicate);
2245
+ if (plain)
2246
+ return `shares its targetClass with '${id}', which holds <${predicate}> as a plain property`;
2247
+ }
2248
+ return null;
2249
+ }
2250
+ const warnedUnguardable = new Set();
2251
+ /**
2252
+ * The owned edges of a shape: its own and inherited `contains` properties, then, per registered
2253
+ * sub-shape, the `contains` properties it adds, guarded by its `targetClass` (a node carries only
2254
+ * its concrete class's `rdf:type`, so each sub-shape lists its full extra set). An added property
2255
+ * whose guard cannot tell the sub-shape's nodes apart (`blockedGuard`) is not followed and is
2256
+ * warned once, unless the sub-shape is a profile or data-only copy of its direct super-shape (same
2257
+ * class or none, the property inherited from that super-shape), which is skipped silently: that
2258
+ * super-shape's entry covers it.
2259
+ */
2260
+ function ownedEdges(shapeId) {
2261
+ var _a, _b, _c;
2262
+ const nodeShape = shapeById(shapeId);
2263
+ if (!nodeShape)
2264
+ return [];
2265
+ const edges = new Map();
2266
+ const add = (ps, guard) => {
2267
+ const predicate = ownedPredicate(ps);
2268
+ if (!predicate)
2269
+ return;
2270
+ const target = ps.valueShape.id;
2271
+ edges.set(`${guard !== null && guard !== void 0 ? guard : ''} ${predicate} ${target}`, { guard, predicate, target });
2272
+ };
2273
+ const own = getPropertyShapes(nodeShape, true);
2274
+ own.forEach((ps) => add(ps));
2275
+ const ownIds = new Set(own.map((ps) => ps.id));
2276
+ for (const sub of getSubShapes(shapeId)) {
2277
+ const extra = getPropertyShapes(sub, true).filter((ps) => !ownIds.has(ps.id) && ownedPredicate(ps));
2278
+ if (extra.length === 0)
2279
+ continue;
2280
+ const parent = ((_a = sub.extends) === null || _a === void 0 ? void 0 : _a.id) ? shapeById(sub.extends.id) : undefined;
2281
+ const inherited = new Set(parent ? getPropertyShapes(parent, true).map((ps) => ps.id) : []);
2282
+ for (const ps of extra) {
2283
+ const reason = blockedGuard(sub, ps);
2284
+ if (!reason) {
2285
+ add(ps, sub.targetClass.id);
2286
+ continue;
2287
+ }
2288
+ // A profile or data-only copy of its direct super-shape (same class, or none) is that
2289
+ // super-shape's node by type: the super-shape's own entry covers the property, unless
2290
+ // that super-shape is this walk's shape itself (no other entry covers it, so warn).
2291
+ const sameKind = !((_b = sub.targetClass) === null || _b === void 0 ? void 0 : _b.id) || sub.targetClass.id === ((_c = parent === null || parent === void 0 ? void 0 : parent.targetClass) === null || _c === void 0 ? void 0 : _c.id);
2292
+ if (sameKind && inherited.has(ps.id) && (parent === null || parent === void 0 ? void 0 : parent.id) !== shapeId)
2293
+ continue;
2294
+ const key = `${sub.id} ${ps.id}`;
2295
+ if (warnedUnguardable.has(key))
2296
+ continue;
2297
+ warnedUnguardable.add(key);
2298
+ console.warn(`[linked] Shape '${sub.id}' adds the contains property "${ps.label}" to '${shapeId}', but ` +
2299
+ `${reason}, so its nodes cannot be told apart by rdf:type: deleting through ` +
2300
+ `'${shapeId}' does not follow it. Give the shape a targetClass of its own.`);
2301
+ }
2302
+ }
2303
+ return [...edges.values()];
2304
+ }
2305
+ /** The classes a node of `shapeId` may carry: its targetClass and every registered sub-shape's. */
2306
+ function shapeClasses(shapeId) {
2307
+ var _a;
2308
+ const nodeShape = shapeById(shapeId);
2309
+ const classes = new Set();
2310
+ for (const shape of nodeShape ? [nodeShape, ...getSubShapes(shapeId)] : []) {
2311
+ if ((_a = shape.targetClass) === null || _a === void 0 ? void 0 : _a.id)
2312
+ classes.add(shape.targetClass.id);
2313
+ }
2314
+ return [...classes];
2315
+ }
2316
+ let ownedWalkCache = new Map();
2317
+ let ownedWalkCacheVersion = -1;
2318
+ const warnedBranches = new Set();
2319
+ /**
2320
+ * The owned subtree of a root shape, as hops (memoized per registry version; read-only).
2321
+ *
2322
+ * One target per path from the root. A self-loop (`S` owns `S` through an unguarded edge) is a
2323
+ * closure, exact because every such edge from an `S` node is `S`'s own. Every other edge is an
2324
+ * explicit hop. A hop is typed when it is a sub-shape's (its class) or when it stays inside a
2325
+ * cycle of several shapes (the declaring shape's classes), and a path stops after
2326
+ * `MAX_OWNED_RECURSION` hops into a shape already on it.
2327
+ */
2328
+ function ownedWalk(rootShapeId) {
2329
+ const version = getRegistryVersion();
2330
+ if (version !== ownedWalkCacheVersion) {
2331
+ ownedWalkCache = new Map();
2332
+ ownedWalkCacheVersion = version;
2333
+ }
2334
+ const cached = ownedWalkCache.get(rootShapeId);
2335
+ if (cached)
2336
+ return cached;
2337
+ const edgesOf = new Map();
2338
+ const pending = [rootShapeId];
2339
+ while (pending.length > 0) {
2340
+ const id = pending.pop();
2341
+ if (edgesOf.has(id))
2342
+ continue;
2343
+ const edges = ownedEdges(id);
2344
+ edgesOf.set(id, edges);
2345
+ for (const edge of edges)
2346
+ if (!edgesOf.has(edge.target))
2347
+ pending.push(edge.target);
2348
+ }
2349
+ const isSelfLoop = (from, e) => !e.guard && e.target === from;
2350
+ const closureOf = (id) => [
2351
+ ...new Set(edgesOf.get(id).filter((e) => isSelfLoop(id, e)).map((e) => e.predicate)),
2352
+ ];
2353
+ // Tarjan's strongly connected components, over the edges that are not closures.
2354
+ const index = new Map();
2355
+ const low = new Map();
2356
+ const onStack = new Set();
2357
+ const stack = [];
2358
+ const groupOf = new Map();
2359
+ const groupSize = [];
2360
+ const strong = (v) => {
2361
+ index.set(v, index.size);
2362
+ low.set(v, index.get(v));
2363
+ stack.push(v);
2364
+ onStack.add(v);
2365
+ for (const e of edgesOf.get(v)) {
2366
+ if (isSelfLoop(v, e))
2367
+ continue;
2368
+ const w = e.target;
2369
+ if (!index.has(w)) {
2370
+ strong(w);
2371
+ low.set(v, Math.min(low.get(v), low.get(w)));
2372
+ }
2373
+ else if (onStack.has(w)) {
2374
+ low.set(v, Math.min(low.get(v), index.get(w)));
2375
+ }
2376
+ }
2377
+ if (low.get(v) !== index.get(v))
2378
+ return;
2379
+ let w;
2380
+ let size = 0;
2381
+ do {
2382
+ w = stack.pop();
2383
+ onStack.delete(w);
2384
+ groupOf.set(w, groupSize.length);
2385
+ size++;
2386
+ } while (w !== v);
2387
+ groupSize.push(size);
2388
+ };
2389
+ strong(rootShapeId);
2390
+ // A non-closure edge back to its own shape (a sub-shape's guarded edge) counts as a cycle too.
2391
+ const inCycle = (from, e) => e.target === from || (groupOf.get(from) === groupOf.get(e.target) && groupSize[groupOf.get(from)] > 1);
2392
+ const targets = [];
2393
+ const visit = (shape, hops, path, repeats) => {
2394
+ for (const edge of edgesOf.get(shape)) {
2395
+ if (isSelfLoop(shape, edge))
2396
+ continue;
2397
+ const repeat = path.includes(edge.target) ? 1 : 0;
2398
+ if (repeats + repeat > MAX_OWNED_RECURSION)
2399
+ continue;
2400
+ const typeClasses = edge.guard
2401
+ ? [edge.guard]
2402
+ : inCycle(shape, edge)
2403
+ ? shapeClasses(shape)
2404
+ : undefined;
2405
+ const hop = Object.assign(Object.assign({}, ((typeClasses === null || typeClasses === void 0 ? void 0 : typeClasses.length) ? { typeClasses } : {})), { predicate: edge.predicate, closure: closureOf(edge.target) });
2406
+ const next = [...hops, hop];
2407
+ targets.push({ hops: next, blankProps: blankCapableProperties(edge.target) });
2408
+ visit(edge.target, next, [...path, edge.target], repeats + repeat);
2409
+ }
2410
+ };
2411
+ visit(rootShapeId, [], [rootShapeId], 0);
2412
+ if (targets.length > OWNED_BRANCH_WARNING && !warnedBranches.has(rootShapeId)) {
2413
+ warnedBranches.add(rootShapeId);
2414
+ console.warn(`[linked] Deleting a '${rootShapeId}' walks ${targets.length} owned paths (more than ` +
2415
+ `${OWNED_BRANCH_WARNING}); each is one UNION branch of the delete.`);
2416
+ }
2417
+ const walk = {
2418
+ rootClosure: closureOf(rootShapeId),
2419
+ rootBlankProps: blankCapableProperties(rootShapeId),
2420
+ targets,
2421
+ };
2422
+ ownedWalkCache.set(rootShapeId, walk);
2423
+ return walk;
2424
+ }
2425
+ /** `FILTER(!sameTerm(?n, rdf:nil))`: the walk never deletes `rdf:nil`, which every list ends in. */
2426
+ function notNil(term) {
2427
+ if (term.kind !== 'variable')
2428
+ return null;
2429
+ return {
2430
+ kind: 'not_expr',
2431
+ inner: {
2432
+ kind: 'function_expr',
2433
+ name: 'sameTerm',
2434
+ args: [{ kind: 'variable_expr', name: term.name }, { kind: 'iri_expr', value: RDF_NIL }],
2435
+ },
2436
+ };
2437
+ }
2438
+ /** `inner` filtered by every expression in `filters` (unchanged when there are none). */
2439
+ function filtered(inner, filters) {
2440
+ if (filters.length === 0)
2441
+ return inner;
2442
+ return {
2443
+ type: 'filter',
2444
+ expression: filters.length === 1 ? filters[0] : { kind: 'logical_expr', op: 'and', exprs: filters },
2445
+ inner,
2446
+ };
2447
+ }
2448
+ /**
2449
+ * DELETE patterns and UNION branches that delete the owned subtree below `rootTerm`, a node of
2450
+ * `rootShapeId` (see `ownedWalk`). The root itself is the caller's: the subject wildcard of a
2451
+ * delete, or `buildOwnedSelfDelete` for a replaced value. Each branch deletes one node per
2452
+ * solution, its own triples and its blank values. Schematically (variable names simplified):
2453
+ *
2454
+ * ```sparql
2455
+ * …anchor… <root> (r…)* ?w . # the root shape's self-loop, if any
2456
+ * ?w a <SubClass> . # a typed hop only
2457
+ * ?w <c1> ?x0e . ?x0e (s…)* ?x0 . # closure only for a self-owning target shape
2458
+ * ?x0 ?x0p ?x0o . FILTER(!sameTerm(?x0, rdf:nil))
2459
+ * ```
2460
+ *
2461
+ * `anchor` leads every branch: the owning edge that binds a variable root (`<s> <p> ?old`), or a
2462
+ * bulk delete's type triple, so a branch evaluated on its own never runs with an unbound root.
2463
+ * `keep` filters every branch on the root (`FILTER(?old NOT IN (…))`).
2464
+ */
2465
+ function ownedSubtreeBlocks(rootShapeId, rootTerm, anchor, varPrefix, keep) {
2466
+ const walk = ownedWalk(rootShapeId);
2467
+ const deletePatterns = [];
2468
+ const whereBlocks = [];
2469
+ const emit = (reach, filters, node, blankProps, prefix) => {
2470
+ const nil = notNil(node);
2471
+ const all = nil ? [...filters, nil] : filters;
2472
+ const own = tripleOf(node, varTerm(`${prefix}p`), varTerm(`${prefix}o`));
2473
+ deletePatterns.push(own);
2474
+ whereBlocks.push(keepFilter(filtered({ type: 'bgp', triples: [...anchor, ...reach, own] }, all), rootTerm, keep));
2475
+ if (blankProps.length === 0)
2476
+ return;
2477
+ const blank = buildBlankCascade([...anchor, ...reach], node, simplePredicates(blankProps), blankWalkPredicates(blankProps), `${prefix}b_`);
2478
+ if (!blank)
2479
+ return;
2480
+ deletePatterns.push(...blank.deletePatterns);
2481
+ whereBlocks.push(keepFilter(filtered(blank.whereBlock, all), rootTerm, keep));
2482
+ };
2483
+ let base = rootTerm;
2484
+ const baseReach = [];
2485
+ if (walk.rootClosure.length > 0) {
2486
+ // The other nodes of the root's own kind (the tail cells of a list).
2487
+ const others = varTerm(`${varPrefix}r`);
2488
+ emit([tripleOf(rootTerm, predicatePathTerm(walk.rootClosure, 'oneOrMore'), others)], [], others, walk.rootBlankProps, `${varPrefix}r`);
2489
+ if (walk.targets.length > 0) {
2490
+ base = varTerm(`${varPrefix}w`);
2491
+ baseReach.push(tripleOf(rootTerm, predicatePathTerm(walk.rootClosure, 'zeroOrMore'), base));
2492
+ }
2493
+ }
2494
+ walk.targets.forEach((target, t) => {
2495
+ const reach = [...baseReach];
2496
+ const filters = [];
2497
+ let at = base;
2498
+ target.hops.forEach((hop, h) => {
2499
+ var _a;
2500
+ const name = `${varPrefix}${t}_${h}`;
2501
+ if (((_a = hop.typeClasses) === null || _a === void 0 ? void 0 : _a.length) === 1) {
2502
+ reach.push(tripleOf(at, iriTerm(RDF_TYPE), iriTerm(hop.typeClasses[0])));
2503
+ }
2504
+ else if (hop.typeClasses) {
2505
+ reach.push(tripleOf(at, iriTerm(RDF_TYPE), varTerm(`${name}t`)));
2506
+ filters.push({
2507
+ kind: 'in_expr',
2508
+ negated: false,
2509
+ value: { kind: 'variable_expr', name: `${name}t` },
2510
+ list: hop.typeClasses.map((value) => ({ kind: 'iri_expr', value })),
2511
+ });
2512
+ }
2513
+ const end = varTerm(hop.closure.length > 0 ? `${name}e` : name);
2514
+ reach.push(tripleOf(at, iriTerm(hop.predicate), end));
2515
+ if (hop.closure.length > 0) {
2516
+ reach.push(tripleOf(end, predicatePathTerm(hop.closure, 'zeroOrMore'), varTerm(name)));
2517
+ }
2518
+ at = varTerm(name);
2519
+ });
2520
+ emit(reach, filters, at, target.blankProps, `${varPrefix}${t}_`);
2521
+ });
2522
+ return { deletePatterns, whereBlocks };
2180
2523
  }
2181
2524
  /**
2182
2525
  * Wrap an owned-cleanup block in `FILTER(?old NOT IN (<k1>, …))` so a value the mutation
@@ -2204,13 +2547,8 @@ function keepFilter(block, oldTerm, keep) {
2204
2547
  * Build DELETE + WHERE OPTIONAL that removes the *old node's own* one-hop triples when a
2205
2548
  * `contains` edge is replaced, unset, or set-removed. A `contains` edge asserts exclusive
2206
2549
  * ownership, so overwriting it must delete the previously owned child fully — not merely
2207
- * unlink it — or the child orphans (backlog 032). This is `contains`-driven and deliberately
2208
- * does NOT require the child shape to be `dependent`: ownership via the edge is sufficient.
2209
- * Owned descendants *below* the old node are cascaded separately by `buildOwnedCascade`.
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.
2550
+ * unlink it — or the child orphans (backlog 032). Ownership via the edge is sufficient.
2551
+ * Owned descendants *below* the old node are deleted separately (`ownedSubtreeBlocks`).
2214
2552
  *
2215
2553
  * The owning edge `<subject> <prop> ?old` is re-asserted INSIDE the WHERE group so `?old`
2216
2554
  * is bound there: when no old value exists the group matches nothing and the DELETE is a
@@ -2226,13 +2564,11 @@ export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPref
2226
2564
  const p = varTerm(`${varPrefix}sp`);
2227
2565
  const o = varTerm(`${varPrefix}so`);
2228
2566
  const wildcard = tripleOf(oldTerm, p, o);
2567
+ const nil = notNil(oldTerm);
2229
2568
  return {
2230
2569
  deletePatterns: [wildcard],
2231
2570
  whereOptionals: [
2232
- keepFilter({
2233
- type: 'bgp',
2234
- triples: [tripleOf(subjectTerm, propertyTerm, oldTerm), wildcard],
2235
- }, oldTerm, keep),
2571
+ keepFilter(filtered({ type: 'bgp', triples: [tripleOf(subjectTerm, propertyTerm, oldTerm), wildcard] }, nil ? [nil] : []), oldTerm, keep),
2236
2572
  ],
2237
2573
  };
2238
2574
  }
@@ -2240,19 +2576,18 @@ export function buildOwnedSelfDelete(subjectTerm, propertyTerm, oldTerm, varPref
2240
2576
  * Converts an IRDeleteMutation to a SparqlDeleteInsertPlan (DELETE + WHERE).
2241
2577
  *
2242
2578
  * The WHERE is the type guard of every id, joined with ONE UNION of independent
2243
- * blocks: per id its own triples, its incoming references and, for a shape that
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
2579
+ * blocks: per id its own triples, its incoming references, its blank values and one branch
2580
+ * per owned path, plus that node's blank values (`ownedSubtreeBlocks`: the shape's own and
2581
+ * inherited `contains` properties, their value shapes' in turn, and the extra ones of
2582
+ * registered sub-shapes behind a type guard; none for a shape that owns nothing). The
2248
2583
  * bulk deletes (`deleteAllToAlgebra`, `deleteWhereToAlgebra`) add the same owned blocks,
2249
2584
  * rooted at the subject variable (see `bulkDeleteUnion`).
2250
2585
  *
2251
2586
  * The blocks must not be chained as OPTIONALs. A chain of OPTIONALs is a join, so
2252
2587
  * the solutions multiply: own triples x incoming references x owned triples of each
2253
- * dependent type. Deleting one shape description with the shape sync's cascade
2588
+ * owned path. Deleting one shape description with the shape sync's cascade
2254
2589
  * produced ~27,000 solutions for ~130 triples and took over 30 seconds on Fuseki,
2255
- * which re-evaluates the cascade property path for every left-hand row. A UNION
2590
+ * which re-evaluated the former registry-wide cascade path for every left-hand row. A UNION
2256
2591
  * yields each block's matches once, so the solutions add up instead, and it deletes
2257
2592
  * exactly the same triples: a DELETE template triple whose variables a solution
2258
2593
  * leaves unbound is skipped, as it was for an OPTIONAL that did not match.
@@ -2264,9 +2599,6 @@ export function deleteToAlgebra(query, _options) {
2264
2599
  const deletePatterns = [];
2265
2600
  const guards = [];
2266
2601
  const blocks = [];
2267
- const hasContains = shapeHasContainsProperty(query.shape);
2268
- const ownedChildPredicates = hasContains ? ownedValueShapePredicates(query.shape) : [];
2269
- const cascadeTypes = hasContains ? reachableDependentTypes(query.shape) : [];
2270
2602
  for (let i = 0; i < query.ids.length; i++) {
2271
2603
  const subjectTerm = iriTerm(query.ids[i].id);
2272
2604
  const idx = query.ids.length > 1 ? `_${i}` : '';
@@ -2279,25 +2611,17 @@ export function deleteToAlgebra(query, _options) {
2279
2611
  // incoming references (the entity may have none) are separate UNION blocks.
2280
2612
  guards.push(typeGuard);
2281
2613
  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
- });
2294
- // Cascade-delete the owned subtree — only for shapes that actually own something
2295
- // (have a `contains` property); a plain entity delete is left untouched.
2296
- if (hasContains && cascadeTypes.length > 0) {
2297
- const cascade = buildOwnedCascade(subjectTerm, `c${idx}_`, undefined, undefined, cascadeTypes);
2298
- deletePatterns.push(...cascade.deletePatterns);
2299
- blocks.push(...cascade.whereOptionals);
2300
- }
2614
+ // The subject's own blank values (through its blank-capable properties) and the blank
2615
+ // nodes below them.
2616
+ const blank = subjectBlankCascade(query.shape, subjectTerm, [], `b${idx}_`);
2617
+ if (blank) {
2618
+ deletePatterns.push(...blank.deletePatterns);
2619
+ blocks.push(blank.whereBlock);
2620
+ }
2621
+ // The owned subtree: every node the subject's `contains` properties reach (`ownedWalk`).
2622
+ const owned = ownedSubtreeBlocks(query.shape, subjectTerm, [], `c${idx}_`);
2623
+ deletePatterns.push(...owned.deletePatterns);
2624
+ blocks.push(...owned.whereBlocks);
2301
2625
  }
2302
2626
  const whereAlgebra = {
2303
2627
  type: 'join',
@@ -2311,102 +2635,27 @@ export function deleteToAlgebra(query, _options) {
2311
2635
  whereAlgebra,
2312
2636
  };
2313
2637
  }
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
2638
  /**
2377
2639
  * The cleanup blocks of a bulk delete, already joined with the root's own triples as ONE
2378
2640
  * UNION so their matches add up instead of multiplying; `null` when there is nothing to
2379
2641
  * clean up (the root wildcard then stays in the required BGP).
2380
2642
  *
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.
2643
+ * - The blank-node cascade of the shape's blank-capable properties (`subjectBlankCascade`).
2644
+ * - The same owned subtree as a delete by id (`ownedSubtreeBlocks`), rooted at the subject
2645
+ * variable. Each owned block is led by the subject's type triple. With a linear join it is
2646
+ * redundant; it keeps a branch that is evaluated on its own from running `?a0 <c> ?x` with
2647
+ * both ends unbound, which matches every edge in the graph.
2388
2648
  */
2389
2649
  function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, typeTriple, deletePatterns) {
2390
2650
  const blocks = [];
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
- }
2651
+ const blank = subjectBlankCascade(shapeId, varTerm(subjectVar), [typeTriple], 'b_');
2652
+ if (blank) {
2653
+ deletePatterns.push(...blank.deletePatterns);
2654
+ blocks.push(blank.whereBlock);
2655
+ }
2656
+ const owned = ownedSubtreeBlocks(shapeId, varTerm(subjectVar), [typeTriple], 'c_');
2657
+ deletePatterns.push(...owned.deletePatterns);
2658
+ blocks.push(...owned.whereBlocks);
2410
2659
  if (blocks.length === 0)
2411
2660
  return null;
2412
2661
  return unionOf([{ type: 'bgp', triples: [rootWildcard] }, ...blocks]);
@@ -2416,7 +2665,7 @@ function bulkDeleteUnion(shapeId, subjectVar, rootWildcard, typeTriple, deletePa
2416
2665
  *
2417
2666
  * Generates DELETE { ?a0 ?p ?o . [blank-node and owned-cleanup wildcards] }
2418
2667
  * WHERE { ?a0 a <Shape> . { ?a0 ?p ?o } UNION [one block per blank node,
2419
- * owned child and dependent type] }
2668
+ * owned group] }
2420
2669
  * (see `bulkDeleteUnion`). Unlike a delete by id, incoming references are not deleted.
2421
2670
  */
2422
2671
  export function deleteAllToAlgebra(query, _options) {
@@ -2443,7 +2692,7 @@ export function deleteAllToAlgebra(query, _options) {
2443
2692
  * Converts an IRDeleteWhereMutation to a SparqlDeleteInsertPlan.
2444
2693
  *
2445
2694
  * 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,
2695
+ * UNION (blank nodes, owned subtree) is joined with the filtered match,
2447
2696
  * so only the matched subjects' children are deleted.
2448
2697
  */
2449
2698
  export function deleteWhereToAlgebra(query, _options) {