objection 4.0.0-rc.2 → 4.0.0-rc.4
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/lib/queryBuilder/InternalOptions.js +4 -0
- package/lib/queryBuilder/graph/GraphLoader.js +201 -126
- package/lib/queryBuilder/graph/GraphNode.js +18 -3
- package/lib/queryBuilder/graph/GraphOptions.js +24 -49
- package/lib/queryBuilder/graph/GraphPlan.js +105 -108
- package/lib/queryBuilder/graph/GraphSchedule.js +102 -55
- package/lib/queryBuilder/graph/GraphUpsert.js +48 -548
- package/lib/queryBuilder/graph/GraphWriter.js +355 -28
- package/lib/queryBuilder/graph/graphChecks.js +534 -0
- package/lib/queryBuilder/graph/graphKeys.js +189 -418
- package/lib/queryBuilder/jsonDialects/JsonDialect.js +6 -0
- package/lib/queryBuilder/operations/UpdateOperation.js +5 -2
- package/lib/queryBuilder/operations/fieldExpressions.js +10 -7
- package/lib/relations/RelationProperty.js +6 -0
- package/package.json +3 -2
- package/typings/objection/index.d.ts +516 -163
|
@@ -1,27 +1,14 @@
|
|
|
1
|
-
import { hasSameColumns, hasSameKey } from './graphKeys.js';
|
|
1
|
+
import { hasOwnerKey, hasSameColumns, hasSameKey } from './graphKeys.js';
|
|
2
|
+
import { groupBy } from '../../utils/objectUtils.js';
|
|
2
3
|
|
|
3
4
|
// What a graph insert or upsert does with each model of the graph and each row
|
|
4
5
|
// of the current graph: The rules that decide it, and the pruning of branches
|
|
5
6
|
// that nothing is written in.
|
|
6
7
|
export class GraphPlan {
|
|
7
|
-
constructor({ nodes, graphOptions
|
|
8
|
+
constructor({ nodes, graphOptions }) {
|
|
8
9
|
this.nodes = nodes;
|
|
9
|
-
// The nodes that an earlier pass left to this one before writing them:
|
|
10
|
-
this.laterNodes = laterNodes;
|
|
11
|
-
this.currentNodes = [];
|
|
12
|
-
this.deferredNodes = new Set();
|
|
13
|
-
// The nodes that earlier passes of the graph decided to keep or inserted,
|
|
14
|
-
// by row:
|
|
15
|
-
this.earlierKeptNodes = keptNodes;
|
|
16
10
|
this.graphOptions = graphOptions;
|
|
17
|
-
this.
|
|
18
|
-
// The decisions only change while the current graph is loaded, and are
|
|
19
|
-
// cached once it is, see `cacheDecisions()`:
|
|
20
|
-
this._intents = null;
|
|
21
|
-
this._nodeActions = null;
|
|
22
|
-
this._activeNodes = null;
|
|
23
|
-
this._activeCurrentNodes = null;
|
|
24
|
-
this._keptNodesByRowKey = null;
|
|
11
|
+
this.reset();
|
|
25
12
|
}
|
|
26
13
|
|
|
27
14
|
get activeNodes() {
|
|
@@ -34,16 +21,24 @@ export class GraphPlan {
|
|
|
34
21
|
));
|
|
35
22
|
}
|
|
36
23
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
24
|
+
// Forget the current graph and what was decided with it, before it's loaded.
|
|
25
|
+
// The decisions only change while it's loaded, and are cached once it is,
|
|
26
|
+
// see `plan()`.
|
|
27
|
+
reset() {
|
|
28
|
+
for (const node of this.nodes) {
|
|
29
|
+
node.current = null;
|
|
30
|
+
node.row = null;
|
|
31
|
+
node.isMissing = false;
|
|
32
|
+
node.isPruned = false;
|
|
33
|
+
}
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
this.
|
|
46
|
-
this.
|
|
35
|
+
this.currentNodes = [];
|
|
36
|
+
this.actions = null;
|
|
37
|
+
this._intents = null;
|
|
38
|
+
this._nodeActions = null;
|
|
39
|
+
this._activeNodes = null;
|
|
40
|
+
this._activeCurrentNodes = null;
|
|
41
|
+
this._keptNodesByRowKey = null;
|
|
47
42
|
}
|
|
48
43
|
|
|
49
44
|
// What the data asks to do with `node`, before the options that disable it:
|
|
@@ -65,23 +60,48 @@ export class GraphPlan {
|
|
|
65
60
|
_getIntent(node) {
|
|
66
61
|
const insertMissing = this.graphOptions.has(node, 'insertMissing');
|
|
67
62
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
63
|
+
if (node.toBeUnrelatedOrDeleted) {
|
|
64
|
+
return 'remove';
|
|
65
|
+
} else if (node.isReference) {
|
|
66
|
+
return 'relate';
|
|
67
|
+
} else if (node.isMissing) {
|
|
68
|
+
return insertMissing ? 'insert' : this.findInsertingNode(node) ? 'relate' : 'missing';
|
|
69
|
+
} else if (node.isDbReference || this._canRelate(node)) {
|
|
70
|
+
return 'relate';
|
|
71
|
+
} else if (node.current) {
|
|
72
|
+
return 'patch';
|
|
73
|
+
} else if (!node.hasId || insertMissing) {
|
|
74
|
+
return 'insert';
|
|
75
|
+
} else {
|
|
76
|
+
return this._isRelatedByOthers(node) ? 'relate' : 'missing';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Return true if another model of the graph relates the row of `node` to the
|
|
81
|
+
// same place, the same relation of the same row, as below the owner that a
|
|
82
|
+
// moved row names.
|
|
83
|
+
_isRelatedByOthers(node) {
|
|
84
|
+
return this.nodes.some(
|
|
85
|
+
(other) =>
|
|
86
|
+
other !== node &&
|
|
87
|
+
other.rowKey === node.rowKey &&
|
|
88
|
+
other.relation === node.relation &&
|
|
89
|
+
other.parent?.rowKey === node.parent?.rowKey &&
|
|
90
|
+
this._canRelate(other),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Return true if the graph relates the row of `node` by the key of its
|
|
95
|
+
// BelongsToOneRelation alone, without an id or a reference.
|
|
96
|
+
isRelatedByKey(node) {
|
|
97
|
+
return (
|
|
98
|
+
this.actions.get(node) === 'relate' &&
|
|
99
|
+
node.relation.keySide === 'owner' &&
|
|
100
|
+
!node.hasId &&
|
|
101
|
+
!node.isReference &&
|
|
102
|
+
!node.isDbReference &&
|
|
103
|
+
node.propertyReferences.length === 0
|
|
104
|
+
);
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
// Return true if the model of the `#ref` model `node` writes what `node`
|
|
@@ -182,13 +202,11 @@ export class GraphPlan {
|
|
|
182
202
|
|
|
183
203
|
// Leave out the branches that require no writes. We never do anything below
|
|
184
204
|
// models that are removed, and we never delete recursively.
|
|
185
|
-
|
|
205
|
+
_prune() {
|
|
186
206
|
const isInsertOnly = this.graphOptions.isInsertOnly();
|
|
187
|
-
|
|
188
|
-
// The rows that were fetched for pruned nodes are left alone too. Nodes of
|
|
189
|
-
// later passes aren't pruned here:
|
|
207
|
+
// The rows that were fetched for pruned nodes are left alone too:
|
|
190
208
|
const pruneNodes = (nodes) => {
|
|
191
|
-
for (const node of nodes
|
|
209
|
+
for (const node of nodes) {
|
|
192
210
|
node.isPruned = true;
|
|
193
211
|
|
|
194
212
|
for (const row of node.row ? [node.row, ...node.row.descendants] : []) {
|
|
@@ -210,10 +228,7 @@ export class GraphPlan {
|
|
|
210
228
|
(node) =>
|
|
211
229
|
!node.current &&
|
|
212
230
|
this.intentFor(node) !== 'insert' &&
|
|
213
|
-
!(
|
|
214
|
-
this.actionFor(node) === 'relate' &&
|
|
215
|
-
(node.row || (node.isReference && !this.graphOptions.isInsertOnly()))
|
|
216
|
-
),
|
|
231
|
+
!(this.actionFor(node) === 'relate' && (node.row || (node.isReference && !isInsertOnly))),
|
|
217
232
|
),
|
|
218
233
|
);
|
|
219
234
|
|
|
@@ -289,14 +304,16 @@ export class GraphPlan {
|
|
|
289
304
|
pruneDescendants(markedNodes);
|
|
290
305
|
pruneDescendants(markedNodes.map((node) => node.current).filter(Boolean));
|
|
291
306
|
}
|
|
292
|
-
|
|
293
|
-
return this;
|
|
294
307
|
}
|
|
295
308
|
|
|
296
|
-
// Decide what to do with each node once
|
|
297
|
-
// writes change the models that the rules
|
|
298
|
-
//
|
|
309
|
+
// Decide what to do with each node once the current graph is loaded, and
|
|
310
|
+
// before the first write, since the writes change the models that the rules
|
|
311
|
+
// read: Cache the decisions, leave out the branches that require no writes,
|
|
312
|
+
// and set `actions`.
|
|
299
313
|
plan() {
|
|
314
|
+
this._intents = new Map();
|
|
315
|
+
this._nodeActions = new Map();
|
|
316
|
+
this._prune();
|
|
300
317
|
const actions = new Map();
|
|
301
318
|
|
|
302
319
|
for (const node of this.activeNodes) {
|
|
@@ -308,7 +325,6 @@ export class GraphPlan {
|
|
|
308
325
|
}
|
|
309
326
|
|
|
310
327
|
this.actions = actions;
|
|
311
|
-
return this;
|
|
312
328
|
}
|
|
313
329
|
|
|
314
330
|
_canRelate(node) {
|
|
@@ -336,14 +352,13 @@ export class GraphPlan {
|
|
|
336
352
|
);
|
|
337
353
|
}
|
|
338
354
|
|
|
339
|
-
// A row that the graph keeps in another place
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
// unique (#398).
|
|
355
|
+
// A row that the graph keeps in another place is unrelated here, unless one
|
|
356
|
+
// of these places sets the same key column, by relating, patching or
|
|
357
|
+
// inserting it through such a relation, or through its own
|
|
358
|
+
// BelongsToOneRelation, e.g. `parent` for `children`: Then that place writes
|
|
359
|
+
// the key, which may not be nullable. A has-one row that its own
|
|
360
|
+
// BelongsToOneRelation keeps is still unrelated if the graph fills its slot
|
|
361
|
+
// with another row, as the foreign key may be unique (#398).
|
|
347
362
|
_isUnrelatedWhenKept(currentNode, keptNode) {
|
|
348
363
|
const { relation } = currentNode;
|
|
349
364
|
const isKeptInRelation = hasSameKey(relation, keptNode.relation);
|
|
@@ -358,11 +373,11 @@ export class GraphPlan {
|
|
|
358
373
|
return (
|
|
359
374
|
relation.keySide !== 'related' ||
|
|
360
375
|
!(isKeptInRelation || isKeptByOwner) ||
|
|
361
|
-
(
|
|
376
|
+
(isKeptByOwner && this._hasNewRowInSlot(currentNode))
|
|
362
377
|
);
|
|
363
378
|
}
|
|
364
379
|
|
|
365
|
-
// Return true if
|
|
380
|
+
// Return true if the graph inserts or relates a row into the slot of the
|
|
366
381
|
// has-one relation that `currentNode` is in.
|
|
367
382
|
_hasNewRowInSlot(currentNode) {
|
|
368
383
|
return (
|
|
@@ -376,51 +391,33 @@ export class GraphPlan {
|
|
|
376
391
|
);
|
|
377
392
|
}
|
|
378
393
|
|
|
379
|
-
// The
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
return
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
for (const node of this.activeNodes) {
|
|
392
|
-
if (this.actions.get(node) === 'insert') {
|
|
393
|
-
addKeptNode(keptNodes, node);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return keptNodes;
|
|
394
|
+
// The BelongsToOne relations whose current rows `node` removes with `null`,
|
|
395
|
+
// `#unrelate` or `#delete`, and whose keys it clears.
|
|
396
|
+
getRemovedOwnerRelations(node) {
|
|
397
|
+
return (node.existing?.activeChildren || [])
|
|
398
|
+
.filter(
|
|
399
|
+
(currentChild) =>
|
|
400
|
+
hasOwnerKey(currentChild.relation) &&
|
|
401
|
+
(node.obj[currentChild.relation.name] === null ||
|
|
402
|
+
currentChild.node?.toBeUnrelatedOrDeleted) &&
|
|
403
|
+
this.actions.get(currentChild) !== 'keep',
|
|
404
|
+
)
|
|
405
|
+
.map(({ relation }) => relation);
|
|
398
406
|
}
|
|
399
407
|
|
|
400
|
-
// The nodes that keep
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
if (node.obj.$hasId() && (action === 'relate' || action === 'patch')) {
|
|
411
|
-
addKeptNode(this._keptNodesByRowKey, node);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return this._keptNodesByRowKey;
|
|
408
|
+
// The nodes that keep the row of `node` in other places of the graph, by
|
|
409
|
+
// relating or patching it there. A row may be kept in several places.
|
|
410
|
+
getKeptNodes(node) {
|
|
411
|
+
this._keptNodesByRowKey ??= groupBy(
|
|
412
|
+
this.activeNodes.filter(
|
|
413
|
+
(node) => node.obj.$hasId() && ['relate', 'patch'].includes(this.actionFor(node)),
|
|
414
|
+
),
|
|
415
|
+
(node) => node.rowKey,
|
|
416
|
+
);
|
|
417
|
+
return this._keptNodesByRowKey.get(node.rowKey) || [];
|
|
417
418
|
}
|
|
418
419
|
}
|
|
419
420
|
|
|
420
|
-
function addKeptNode(keptNodes, node) {
|
|
421
|
-
keptNodes.set(node.rowKey, [...(keptNodes.get(node.rowKey) || []), node]);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
421
|
// For `HasMany` and `HasOne` relations, the relate prop is the id of the
|
|
425
422
|
// related model. Use the id state of the input graph, since the parent's
|
|
426
423
|
// insert copies the foreign key into the related model, which can complete
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import { hasOwnerKey, hasSameKey } from './graphKeys.js';
|
|
1
|
+
import { hasOwnerKey, hasSameColumns, hasSameKey } from './graphKeys.js';
|
|
2
2
|
import { groupBy } from '../../utils/objectUtils.js';
|
|
3
|
+
import { ValidationErrorType } from '../../model/ValidationError.js';
|
|
4
|
+
import { formatPath } from './GraphNode.js';
|
|
3
5
|
|
|
4
6
|
// Order the writes of a planned graph in steps that keep keys and constraints
|
|
5
7
|
// intact (#214), which `GraphWriter` writes:
|
|
6
8
|
// 1. Move the rows that the graph relates to existing owners of HasMany and
|
|
7
9
|
// HasOne relations, as they may still sit below rows that the next step
|
|
8
|
-
// deletes. Rows that wait for inserts are related in step 3
|
|
10
|
+
// deletes. Rows that wait for inserts or changed keys are related in step 3
|
|
11
|
+
// or 4.
|
|
9
12
|
// 2. Remove rows that reference their owners, before the inserts that may need
|
|
10
13
|
// their unique keys.
|
|
11
14
|
// 3. Insert the new rows in the order of their keys and references, then the
|
|
12
15
|
// keys deferred to break cycles and the join rows. Rows that move out of
|
|
13
16
|
// the slots of HasOneRelations that new rows go into are related in the
|
|
14
|
-
// same order, as the foreign key may be unique
|
|
17
|
+
// same order, as the foreign key may be unique, and so are the patches and
|
|
18
|
+
// relates that change the keys that these rows read, as a foreign key may
|
|
19
|
+
// reference them (#428).
|
|
15
20
|
// 4. Patch the changed rows, the other relates, and the keys of owners whose
|
|
16
21
|
// BelongsToOne rows were inserted, related or removed.
|
|
17
|
-
// 5.
|
|
22
|
+
// 5. Unrelate the rows that their own to-many relation on their own key leaves
|
|
23
|
+
// out, e.g. a pet in its own `siblings`, after their own writes.
|
|
24
|
+
// 6. Remove the BelongsToOne rows that the owners no longer reference.
|
|
18
25
|
// Each step writes a row that goes into the slot of a HasOneRelation after the
|
|
19
26
|
// row that moves out of it, unless that closes a cycle.
|
|
20
27
|
// `insertGraph()` only inserts.
|
|
@@ -23,27 +30,20 @@ export class GraphSchedule {
|
|
|
23
30
|
this.plan = plan;
|
|
24
31
|
this.keys = keys;
|
|
25
32
|
// The nodes that each node waits for, through its keys, its references and
|
|
26
|
-
// the rows that move out of the slots that it goes into
|
|
33
|
+
// the rows that move out of the slots that it goes into. `isKeyChange`
|
|
34
|
+
// marks the patches and relates that change the keys it reads:
|
|
27
35
|
this.dependencies = createDependencies(plan, this.keys);
|
|
28
|
-
// Dependencies that aren't waited for, to break cycles
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
// after the inserts:
|
|
32
|
-
const activeNodes = new Set(plan.activeNodes);
|
|
33
|
-
this.deferredDependencies = new Set(
|
|
34
|
-
[...this.dependencies].flatMap(([node, dependencies]) =>
|
|
35
|
-
activeNodes.has(node) ? [] : dependencies.filter((dependency) => dependency.relation),
|
|
36
|
-
),
|
|
37
|
-
);
|
|
36
|
+
// Dependencies that aren't waited for, to break cycles. Deferred keys are
|
|
37
|
+
// written after the inserts:
|
|
38
|
+
this.deferredDependencies = new Set();
|
|
38
39
|
// Relates whose rows are moved to their new owners before the removals:
|
|
39
40
|
this.movedNodes = new Set(
|
|
40
41
|
plan.graphOptions.isInsertOnly()
|
|
41
42
|
? []
|
|
42
43
|
: plan.activeNodes.filter((node) => this._isMovedFirst(node)),
|
|
43
44
|
);
|
|
44
|
-
// Relates
|
|
45
|
-
|
|
46
|
-
this.slotMovedNodes = this._getSlotMovedNodes();
|
|
45
|
+
// Relates and patches that aren't moved first but written in the inserts:
|
|
46
|
+
this.earlyNodes = this._getEarlyNodes();
|
|
47
47
|
this.steps = this._planSteps();
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -76,12 +76,19 @@ export class GraphSchedule {
|
|
|
76
76
|
} else {
|
|
77
77
|
const removedNodes = activeCurrentNodes.filter((node) => actions.get(node) !== 'keep');
|
|
78
78
|
const isReferencedByOwner = (currentNode) => currentNode.relation.keySide === 'owner';
|
|
79
|
+
// A row that its own to-many relation on its own key leaves out, e.g. a
|
|
80
|
+
// pet in its own `siblings`, is unrelated after its own writes, which keep
|
|
81
|
+
// its key. A row in its own slot leaves it before another one goes in:
|
|
82
|
+
const isOwnRow = ({ relation, parent, rowKey }) =>
|
|
83
|
+
rowKey === parent.rowKey &&
|
|
84
|
+
!relation.isOneToOne() &&
|
|
85
|
+
hasSameColumns(relation.relatedProp, relation.ownerProp);
|
|
79
86
|
|
|
80
87
|
return [
|
|
81
88
|
...this._planLevels([...this.movedNodes]).map(({ nodes }) => ({ kind: 'move', nodes })),
|
|
82
89
|
{
|
|
83
90
|
kind: 'remove',
|
|
84
|
-
nodes: removedNodes.filter((node) => !isReferencedByOwner(node)),
|
|
91
|
+
nodes: removedNodes.filter((node) => !isReferencedByOwner(node) && !isOwnRow(node)),
|
|
85
92
|
areReferenced: true,
|
|
86
93
|
},
|
|
87
94
|
...insertSteps,
|
|
@@ -89,15 +96,19 @@ export class GraphSchedule {
|
|
|
89
96
|
activeNodes.filter((node) => {
|
|
90
97
|
const action = actions.get(node);
|
|
91
98
|
return (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
!this.slotMovedNodes.has(node))) &&
|
|
99
|
+
['patch', 'relate'].includes(action) &&
|
|
100
|
+
!this.movedNodes.has(node) &&
|
|
101
|
+
!this.earlyNodes.has(node) &&
|
|
96
102
|
// The row of a `#ref` model is written once, by its model:
|
|
97
103
|
!this._isWrittenByModel(node)
|
|
98
104
|
);
|
|
99
105
|
}),
|
|
100
106
|
).map(({ nodes }) => ({ kind: 'patch', nodes })),
|
|
107
|
+
{
|
|
108
|
+
kind: 'remove',
|
|
109
|
+
nodes: removedNodes.filter(isOwnRow),
|
|
110
|
+
areReferenced: true,
|
|
111
|
+
},
|
|
101
112
|
{
|
|
102
113
|
kind: 'remove',
|
|
103
114
|
// Owners unrelate their BelongsToOne rows by patching their keys:
|
|
@@ -123,8 +134,8 @@ export class GraphSchedule {
|
|
|
123
134
|
// The keys of models with rows are known, also of related models that were
|
|
124
135
|
// fetched, except for the props that the graph fills in:
|
|
125
136
|
const hasRow = (node) => !!(node.referencedNode || node).existing;
|
|
126
|
-
// Nodes outside
|
|
127
|
-
//
|
|
137
|
+
// Nodes outside of `nodes` are written in other steps, or never, see
|
|
138
|
+
// `checkReferences()`:
|
|
128
139
|
const getPendingDependencies = (node) =>
|
|
129
140
|
(this.dependencies.get(node) || []).filter(
|
|
130
141
|
(dependency) =>
|
|
@@ -205,7 +216,25 @@ export class GraphSchedule {
|
|
|
205
216
|
}
|
|
206
217
|
}
|
|
207
218
|
} else if (!breakCycle()) {
|
|
208
|
-
|
|
219
|
+
// A patch that changes a key that the rows it waits for read can be
|
|
220
|
+
// written neither before nor after them:
|
|
221
|
+
const { node: changingNode } =
|
|
222
|
+
nodes
|
|
223
|
+
.filter((node) => !handledNodes.has(node))
|
|
224
|
+
.flatMap(getPendingDependencies)
|
|
225
|
+
.find(
|
|
226
|
+
({ node, dependent, isKeyChange }) => isKeyChange && isBlockedBy(node, dependent),
|
|
227
|
+
) || {};
|
|
228
|
+
|
|
229
|
+
if (changingNode) {
|
|
230
|
+
const { modelClass, dataPathKey } = changingNode;
|
|
231
|
+
throw modelClass.createValidationError({
|
|
232
|
+
type: ValidationErrorType.InvalidGraph,
|
|
233
|
+
message: `the graph changes a key of the ${modelClass.name} at ${formatPath(dataPathKey)} that rows read which it also waits for; write the key in a separate query first`,
|
|
234
|
+
});
|
|
235
|
+
} else {
|
|
236
|
+
throw new Error('the object graph contains cyclic references');
|
|
237
|
+
}
|
|
209
238
|
}
|
|
210
239
|
}
|
|
211
240
|
|
|
@@ -238,9 +267,10 @@ export class GraphSchedule {
|
|
|
238
267
|
|
|
239
268
|
// Return true if the HasMany or HasOne relate of `node` moves its row to an
|
|
240
269
|
// existing owner before the removals, see the steps above. `#ref` models may
|
|
241
|
-
// get their ids from the inserts, rows that wait for the inserts
|
|
242
|
-
//
|
|
243
|
-
// the removal of that row, or its move, as its key may be
|
|
270
|
+
// get their ids from the inserts, rows that wait for the inserts or for
|
|
271
|
+
// changed keys are written after them, and a relate into the slot of a HasOne
|
|
272
|
+
// row waits for the removal of that row, or its move, as its key may be
|
|
273
|
+
// unique.
|
|
244
274
|
_isMovedFirst(node, visitedNodes = new Set()) {
|
|
245
275
|
const { actions } = this.plan;
|
|
246
276
|
visitedNodes.add(node);
|
|
@@ -250,7 +280,7 @@ export class GraphSchedule {
|
|
|
250
280
|
!node.isReference &&
|
|
251
281
|
node.relation.keySide === 'related' &&
|
|
252
282
|
node.parent.existing &&
|
|
253
|
-
!this.
|
|
283
|
+
!this._waitsForWrites(node)
|
|
254
284
|
) {
|
|
255
285
|
const slotNode = getSlotNode(node);
|
|
256
286
|
const moverNode = getSlotMover(this.plan, node);
|
|
@@ -264,24 +294,32 @@ export class GraphSchedule {
|
|
|
264
294
|
}
|
|
265
295
|
}
|
|
266
296
|
|
|
267
|
-
// Find the relates
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// as the foreign key may be unique
|
|
271
|
-
|
|
297
|
+
// Find the relates and patches that are written in the inserts, as the rows
|
|
298
|
+
// written there wait for them: The relates that move the rows out of the
|
|
299
|
+
// slots of HasOneRelations that new rows go into, along with the relates that
|
|
300
|
+
// move the rows out of their own slots, as the foreign key may be unique, and
|
|
301
|
+
// the patches and relates that change the keys that these rows or join rows
|
|
302
|
+
// read, as a foreign key may reference them (#428).
|
|
303
|
+
_getEarlyNodes() {
|
|
304
|
+
const { actions } = this.plan;
|
|
272
305
|
const nodes = new Set();
|
|
273
|
-
const
|
|
274
|
-
for (const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
306
|
+
const addEarlyNodes = (node) => {
|
|
307
|
+
for (const dependency of this.dependencies.get(node) || []) {
|
|
308
|
+
const { node: earlyNode, isSlotMove, isKeyChange } = dependency;
|
|
309
|
+
if (
|
|
310
|
+
(isSlotMove || isKeyChange) &&
|
|
311
|
+
!this.movedNodes.has(earlyNode) &&
|
|
312
|
+
!nodes.has(earlyNode)
|
|
313
|
+
) {
|
|
314
|
+
nodes.add(earlyNode);
|
|
315
|
+
addEarlyNodes(earlyNode);
|
|
278
316
|
}
|
|
279
317
|
}
|
|
280
318
|
};
|
|
281
319
|
|
|
282
320
|
for (const node of this.plan.activeNodes) {
|
|
283
|
-
if (
|
|
284
|
-
|
|
321
|
+
if (actions.get(node) === 'insert' || (node.relation?.keySide === 'join' && !node.current)) {
|
|
322
|
+
addEarlyNodes(node);
|
|
285
323
|
}
|
|
286
324
|
}
|
|
287
325
|
|
|
@@ -310,19 +348,21 @@ export class GraphSchedule {
|
|
|
310
348
|
}
|
|
311
349
|
|
|
312
350
|
// Return true if `node` waits for the keys or references of inserted models,
|
|
313
|
-
//
|
|
314
|
-
// that it waits for to get its
|
|
315
|
-
|
|
351
|
+
// or for keys that patches change (#428), also through the models that it
|
|
352
|
+
// references (#356), and through the models that it waits for to get its
|
|
353
|
+
// keys.
|
|
354
|
+
_waitsForWrites(node, visitedNodes = new Set()) {
|
|
316
355
|
const { actions } = this.plan;
|
|
317
356
|
visitedNodes.add(node);
|
|
318
357
|
return (this.dependencies.get(node) || []).some((dependency) => {
|
|
319
|
-
const { node: dependencyNode, relation } = dependency;
|
|
358
|
+
const { node: dependencyNode, relation, isKeyWait, isKeyChange } = dependency;
|
|
320
359
|
const referencedNode = dependencyNode.referencedNode || dependencyNode;
|
|
321
360
|
return (
|
|
322
361
|
actions.get(referencedNode) === 'insert' ||
|
|
323
|
-
|
|
362
|
+
isKeyChange ||
|
|
363
|
+
((!relation || isKeyWait) &&
|
|
324
364
|
!visitedNodes.has(referencedNode) &&
|
|
325
|
-
this.
|
|
365
|
+
this._waitsForWrites(referencedNode, visitedNodes))
|
|
326
366
|
);
|
|
327
367
|
});
|
|
328
368
|
}
|
|
@@ -347,7 +387,6 @@ export class GraphSchedule {
|
|
|
347
387
|
// their cycle: the strongly connected components of these references, found
|
|
348
388
|
// with Tarjan's algorithm.
|
|
349
389
|
findReferenceCycles(nodes) {
|
|
350
|
-
const getRow = (node) => node.referencedNode || node;
|
|
351
390
|
const nodesByRow = groupBy(nodes, getRow);
|
|
352
391
|
const getReferencedNodes = (node) =>
|
|
353
392
|
(this.dependencies.get(node) || [])
|
|
@@ -452,20 +491,23 @@ function createDependencies(plan, keys) {
|
|
|
452
491
|
|
|
453
492
|
// A place waits for the inserts and `#ref{}` values that bind the slots of
|
|
454
493
|
// its keys. A dependency on the same row through the same key becomes such
|
|
455
|
-
// a wait, and stays deferrable to break cycles. The other waits aren't
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
for (const { node: waitedNode, prop } of keys.waitsFor(node)) {
|
|
494
|
+
// a wait, and stays deferrable to break cycles. The other waits aren't. A
|
|
495
|
+
// key change is waited for at the node that writes it, not at a `#ref`
|
|
496
|
+
// model of its row, which writes nothing:
|
|
497
|
+
for (const { node: waitedNode, prop, isKeyChange } of keys.waitsFor(node)) {
|
|
459
498
|
const dependency = (dependencies.get(node) || []).find(
|
|
460
499
|
(dependency) =>
|
|
461
|
-
|
|
500
|
+
(isKeyChange
|
|
501
|
+
? dependency.node === waitedNode
|
|
502
|
+
: getRow(dependency.node) === getRow(waitedNode)) &&
|
|
462
503
|
(!dependency.relation || getKeyProps(dependency, node).includes(prop)),
|
|
463
504
|
);
|
|
464
505
|
|
|
465
506
|
if (dependency) {
|
|
466
507
|
dependency.isKeyWait = true;
|
|
508
|
+
dependency.isKeyChange ||= isKeyChange;
|
|
467
509
|
} else {
|
|
468
|
-
addDependency(node, { node: waitedNode, isKeyWait: true });
|
|
510
|
+
addDependency(node, { node: waitedNode, isKeyWait: true, isKeyChange });
|
|
469
511
|
}
|
|
470
512
|
}
|
|
471
513
|
|
|
@@ -507,6 +549,11 @@ function getSlotNode(node) {
|
|
|
507
549
|
: null;
|
|
508
550
|
}
|
|
509
551
|
|
|
552
|
+
// The row of `node`: a `#ref` model is the model it refers to.
|
|
553
|
+
function getRow(node) {
|
|
554
|
+
return node.referencedNode || node;
|
|
555
|
+
}
|
|
556
|
+
|
|
510
557
|
// Return the key props of `node` on its side of the relation of `dependency`.
|
|
511
558
|
function getKeyProps({ relation, related }, node) {
|
|
512
559
|
return (related === node ? relation.relatedProp : relation.ownerProp).props;
|