@_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.
- package/CHANGELOG.md +67 -0
- package/README.md +107 -60
- package/lib/esm/ontologies/linked-core.js +3 -2
- package/lib/esm/ontologies/linked-core.js.map +1 -1
- package/lib/esm/ontologies/shacl.d.ts +3 -0
- package/lib/esm/ontologies/shacl.js +6 -0
- package/lib/esm/ontologies/shacl.js.map +1 -1
- package/lib/esm/queries/IRMutation.js +1 -1
- package/lib/esm/queries/IRMutation.js.map +1 -1
- package/lib/esm/queries/IntermediateRepresentation.d.ts +0 -5
- package/lib/esm/queries/IntermediateRepresentation.js.map +1 -1
- package/lib/esm/queries/MutationQuery.js +0 -15
- package/lib/esm/queries/MutationQuery.js.map +1 -1
- package/lib/esm/queries/MutationSerialization.d.ts +6 -5
- package/lib/esm/queries/MutationSerialization.js +1 -9
- package/lib/esm/queries/MutationSerialization.js.map +1 -1
- package/lib/esm/queries/QueryFactory.d.ts +0 -6
- package/lib/esm/queries/QueryFactory.js.map +1 -1
- package/lib/esm/queries/lowerMutationJSON.js +1 -6
- package/lib/esm/queries/lowerMutationJSON.js.map +1 -1
- package/lib/esm/shapes/List.class.d.ts +3 -3
- package/lib/esm/shapes/List.class.js +3 -3
- package/lib/esm/shapes/List.class.js.map +1 -1
- package/lib/esm/shapes/List.d.ts +0 -1
- package/lib/esm/shapes/List.js.map +1 -1
- package/lib/esm/shapes/PathList.d.ts +24 -0
- package/lib/esm/shapes/PathList.js +60 -0
- package/lib/esm/shapes/PathList.js.map +1 -0
- package/lib/esm/shapes/PathNode.d.ts +8 -9
- package/lib/esm/shapes/PathNode.js +20 -14
- package/lib/esm/shapes/PathNode.js.map +1 -1
- package/lib/esm/shapes/SHACL.d.ts +15 -13
- package/lib/esm/shapes/SHACL.js +8 -0
- package/lib/esm/shapes/SHACL.js.map +1 -1
- package/lib/esm/shapes/index.d.ts +1 -0
- package/lib/esm/shapes/index.js +1 -0
- package/lib/esm/shapes/index.js.map +1 -1
- package/lib/esm/shapes/nodeShapeData.d.ts +4 -1
- package/lib/esm/shapes/nodeShapeData.js.map +1 -1
- package/lib/esm/shapes/serializePathToNodeData.d.ts +12 -11
- package/lib/esm/shapes/serializePathToNodeData.js +12 -11
- package/lib/esm/shapes/serializePathToNodeData.js.map +1 -1
- package/lib/esm/shapes/singleOwnerConstraint.d.ts +5 -7
- package/lib/esm/shapes/singleOwnerConstraint.js +65 -21
- package/lib/esm/shapes/singleOwnerConstraint.js.map +1 -1
- package/lib/esm/shapes/syncShapes.js +2 -2
- package/lib/esm/shapes/syncShapes.js.map +1 -1
- package/lib/esm/shapes/validation.js +2 -2
- package/lib/esm/shapes/validation.js.map +1 -1
- package/lib/esm/sparql/irToAlgebra.d.ts +15 -33
- package/lib/esm/sparql/irToAlgebra.js +680 -431
- package/lib/esm/sparql/irToAlgebra.js.map +1 -1
- package/lib/esm/sparql/ownedLinkCheck.d.ts +6 -10
- package/lib/esm/sparql/ownedLinkCheck.js +10 -11
- package/lib/esm/sparql/ownedLinkCheck.js.map +1 -1
- package/lib/esm/utils/Package.d.ts +3 -2
- package/lib/esm/utils/Package.js +21 -8
- package/lib/esm/utils/Package.js.map +1 -1
- 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
|
-
*
|
|
1426
|
-
*
|
|
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
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
shacl.
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
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
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
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
|
|
1444
|
-
var _a;
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
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 (
|
|
1467
|
-
|
|
1468
|
-
throw new Error(`A blank nested node (__blank) cannot carry an id (${data.id}).`);
|
|
1469
|
-
}
|
|
1470
|
-
if (!blanks) {
|
|
1471
|
-
throw new Error('A blank nested node (__blank) can only be written by a create. An update writes its ' +
|
|
1472
|
-
'INSERT once per WHERE solution, which would duplicate the blank node.');
|
|
1473
|
-
}
|
|
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
|
-
|
|
1528
|
+
// Skip undefined/null values in create
|
|
1529
|
+
if (field.value === undefined || field.value === null) {
|
|
1488
1530
|
continue;
|
|
1489
1531
|
}
|
|
1490
|
-
// Handle arrays (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1580
|
-
//
|
|
1581
|
-
//
|
|
1582
|
-
//
|
|
1583
|
-
const
|
|
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 (
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
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
|
|
1612
|
-
//
|
|
1613
|
-
const
|
|
1614
|
-
deletePatterns.push(...
|
|
1615
|
-
cascadeOptionals.push(...
|
|
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
|
-
|
|
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 (
|
|
1680
|
+
if (valueShapeId) {
|
|
1639
1681
|
containsOldVars.push({
|
|
1640
1682
|
oldVar: `old_${suffix}`,
|
|
1641
1683
|
propertyTerm,
|
|
1642
1684
|
keep: keptReferences(field.value),
|
|
1643
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
1769
|
+
containsOldVars.forEach(({ oldVar, propertyTerm, keep, valueShapeId }, n) => {
|
|
1732
1770
|
const oldTerm = varTerm(oldVar);
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
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
|
-
|
|
1975
|
-
|
|
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
|
|
2020
|
-
*
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2025
|
-
*
|
|
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
|
|
2031
|
-
var _a, _b;
|
|
2019
|
+
function blankWalkPredicates(start) {
|
|
2032
2020
|
const version = getRegistryVersion();
|
|
2033
|
-
if (version !==
|
|
2034
|
-
|
|
2035
|
-
|
|
2021
|
+
if (version !== blankWalkCacheVersion) {
|
|
2022
|
+
blankWalkCache = new Map();
|
|
2023
|
+
blankWalkCacheVersion = version;
|
|
2036
2024
|
}
|
|
2037
|
-
const
|
|
2025
|
+
const key = start.map((ps) => ps.id).join('|');
|
|
2026
|
+
const cached = blankWalkCache.get(key);
|
|
2038
2027
|
if (cached)
|
|
2039
2028
|
return cached;
|
|
2040
|
-
|
|
2041
|
-
const
|
|
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
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
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
|
-
|
|
2059
|
-
let everything = false;
|
|
2044
|
+
start.forEach(follow);
|
|
2060
2045
|
while (queue.length > 0 && !everything) {
|
|
2061
|
-
const
|
|
2062
|
-
|
|
2063
|
-
|
|
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 (
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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 =
|
|
2085
|
-
|
|
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
|
-
*
|
|
2090
|
-
*
|
|
2091
|
-
*
|
|
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
|
-
* `
|
|
2094
|
-
*
|
|
2095
|
-
* `
|
|
2096
|
-
*
|
|
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
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
const
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
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
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
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: [
|
|
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
|
-
}
|
|
2179
|
-
|
|
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).
|
|
2208
|
-
*
|
|
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
|
|
2244
|
-
*
|
|
2245
|
-
*
|
|
2246
|
-
*
|
|
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
|
-
*
|
|
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-
|
|
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
|
|
2283
|
-
//
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
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
|
|
2382
|
-
* -
|
|
2383
|
-
*
|
|
2384
|
-
*
|
|
2385
|
-
*
|
|
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
|
-
|
|
2392
|
-
if (
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
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
|
|
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
|
|
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) {
|