objection 4.0.0-rc.1 → 4.0.0-rc.2
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/graph/GraphLoader.js +18 -8
- package/lib/queryBuilder/graph/GraphNode.js +6 -0
- package/lib/queryBuilder/graph/GraphPlan.js +68 -32
- package/lib/queryBuilder/graph/GraphSchedule.js +513 -0
- package/lib/queryBuilder/graph/GraphUpsert.js +69 -14
- package/lib/queryBuilder/graph/GraphWriter.js +39 -794
- package/lib/queryBuilder/graph/graphKeys.js +929 -0
- package/package.json +1 -1
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import { hasOwnerKey, hasSameKey } from './graphKeys.js';
|
|
2
|
+
import { groupBy } from '../../utils/objectUtils.js';
|
|
3
|
+
|
|
4
|
+
// Order the writes of a planned graph in steps that keep keys and constraints
|
|
5
|
+
// intact (#214), which `GraphWriter` writes:
|
|
6
|
+
// 1. Move the rows that the graph relates to existing owners of HasMany and
|
|
7
|
+
// 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 or 4.
|
|
9
|
+
// 2. Remove rows that reference their owners, before the inserts that may need
|
|
10
|
+
// their unique keys.
|
|
11
|
+
// 3. Insert the new rows in the order of their keys and references, then the
|
|
12
|
+
// keys deferred to break cycles and the join rows. Rows that move out of
|
|
13
|
+
// the slots of HasOneRelations that new rows go into are related in the
|
|
14
|
+
// same order, as the foreign key may be unique.
|
|
15
|
+
// 4. Patch the changed rows, the other relates, and the keys of owners whose
|
|
16
|
+
// BelongsToOne rows were inserted, related or removed.
|
|
17
|
+
// 5. Remove the BelongsToOne rows that the owners no longer reference.
|
|
18
|
+
// Each step writes a row that goes into the slot of a HasOneRelation after the
|
|
19
|
+
// row that moves out of it, unless that closes a cycle.
|
|
20
|
+
// `insertGraph()` only inserts.
|
|
21
|
+
export class GraphSchedule {
|
|
22
|
+
constructor(plan, keys) {
|
|
23
|
+
this.plan = plan;
|
|
24
|
+
this.keys = keys;
|
|
25
|
+
// 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:
|
|
27
|
+
this.dependencies = createDependencies(plan, this.keys);
|
|
28
|
+
// Dependencies that aren't waited for, to break cycles, and the ones of
|
|
29
|
+
// owners that earlier passes wrote, whose keys are patched in once their
|
|
30
|
+
// BelongsToOne rows of this pass are written. Deferred keys are written
|
|
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
|
+
);
|
|
38
|
+
// Relates whose rows are moved to their new owners before the removals:
|
|
39
|
+
this.movedNodes = new Set(
|
|
40
|
+
plan.graphOptions.isInsertOnly()
|
|
41
|
+
? []
|
|
42
|
+
: plan.activeNodes.filter((node) => this._isMovedFirst(node)),
|
|
43
|
+
);
|
|
44
|
+
// Relates that move the rows out of the slots of HasOneRelations that new
|
|
45
|
+
// rows go into, and that aren't moved first:
|
|
46
|
+
this.slotMovedNodes = this._getSlotMovedNodes();
|
|
47
|
+
this.steps = this._planSteps();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_planSteps() {
|
|
51
|
+
const { activeNodes, activeCurrentNodes, actions, graphOptions } = this.plan;
|
|
52
|
+
const insertSteps = [
|
|
53
|
+
...this._planLevels(activeNodes, { resolvesReferences: true }).map((level) => ({
|
|
54
|
+
kind: 'insert',
|
|
55
|
+
...level,
|
|
56
|
+
})),
|
|
57
|
+
{
|
|
58
|
+
kind: 'deferredKeys',
|
|
59
|
+
dependencies: [...this.deferredDependencies].filter((dependency) => dependency.relation),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
kind: 'joinRows',
|
|
63
|
+
nodes: activeNodes.filter(
|
|
64
|
+
(node) =>
|
|
65
|
+
node.relation?.keySide === 'join' &&
|
|
66
|
+
!node.current &&
|
|
67
|
+
['insert', 'relate'].includes(actions.get(node)) &&
|
|
68
|
+
// Models that `noInsert` skips have no key for join rows (#369):
|
|
69
|
+
actions.get(node.parent) !== 'ignore',
|
|
70
|
+
),
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
if (graphOptions.isInsertOnly()) {
|
|
75
|
+
return insertSteps;
|
|
76
|
+
} else {
|
|
77
|
+
const removedNodes = activeCurrentNodes.filter((node) => actions.get(node) !== 'keep');
|
|
78
|
+
const isReferencedByOwner = (currentNode) => currentNode.relation.keySide === 'owner';
|
|
79
|
+
|
|
80
|
+
return [
|
|
81
|
+
...this._planLevels([...this.movedNodes]).map(({ nodes }) => ({ kind: 'move', nodes })),
|
|
82
|
+
{
|
|
83
|
+
kind: 'remove',
|
|
84
|
+
nodes: removedNodes.filter((node) => !isReferencedByOwner(node)),
|
|
85
|
+
areReferenced: true,
|
|
86
|
+
},
|
|
87
|
+
...insertSteps,
|
|
88
|
+
...this._planLevels(
|
|
89
|
+
activeNodes.filter((node) => {
|
|
90
|
+
const action = actions.get(node);
|
|
91
|
+
return (
|
|
92
|
+
(action === 'patch' ||
|
|
93
|
+
(action === 'relate' &&
|
|
94
|
+
!this.movedNodes.has(node) &&
|
|
95
|
+
!this.slotMovedNodes.has(node))) &&
|
|
96
|
+
// The row of a `#ref` model is written once, by its model:
|
|
97
|
+
!this._isWrittenByModel(node)
|
|
98
|
+
);
|
|
99
|
+
}),
|
|
100
|
+
).map(({ nodes }) => ({ kind: 'patch', nodes })),
|
|
101
|
+
{
|
|
102
|
+
kind: 'remove',
|
|
103
|
+
// Owners unrelate their BelongsToOne rows by patching their keys:
|
|
104
|
+
nodes: removedNodes.filter(
|
|
105
|
+
(node) => isReferencedByOwner(node) && actions.get(node) === 'delete',
|
|
106
|
+
),
|
|
107
|
+
areReferenced: false,
|
|
108
|
+
},
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Group the nodes into levels that can be written once the levels before
|
|
114
|
+
// them are: Nodes wait for the nodes whose keys they store, the nodes that
|
|
115
|
+
// they reference, and the rows that move out of the slots they go into. For
|
|
116
|
+
// the inserts, all nodes take part, so that the keys of the nodes that aren't
|
|
117
|
+
// inserted are resolved in order too, and `resolvesReferences` says so.
|
|
118
|
+
_planLevels(nodes, { resolvesReferences = false } = {}) {
|
|
119
|
+
const nodeSet = new Set(nodes);
|
|
120
|
+
const handledNodes = new Set();
|
|
121
|
+
const levels = [];
|
|
122
|
+
|
|
123
|
+
// The keys of models with rows are known, also of related models that were
|
|
124
|
+
// fetched, except for the props that the graph fills in:
|
|
125
|
+
const hasRow = (node) => !!(node.referencedNode || node).existing;
|
|
126
|
+
// Nodes outside this pass were written by an earlier one, or are never
|
|
127
|
+
// written, see `checkReferences()`:
|
|
128
|
+
const getPendingDependencies = (node) =>
|
|
129
|
+
(this.dependencies.get(node) || []).filter(
|
|
130
|
+
(dependency) =>
|
|
131
|
+
nodeSet.has(dependency.node) &&
|
|
132
|
+
!this.deferredDependencies.has(dependency) &&
|
|
133
|
+
!handledNodes.has(dependency.node) &&
|
|
134
|
+
(dependency.isSlotMove ||
|
|
135
|
+
dependency.isKeyWait ||
|
|
136
|
+
!hasRow(dependency.node) ||
|
|
137
|
+
this.readsFilledProps(dependency)),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Return true if `node` waits for `blockingNode`, through other nodes too.
|
|
141
|
+
const isBlockedBy = (node, blockingNode, visitedNodes = new Set()) => {
|
|
142
|
+
if (node === blockingNode) {
|
|
143
|
+
return true;
|
|
144
|
+
} else if (visitedNodes.has(node)) {
|
|
145
|
+
return false;
|
|
146
|
+
} else {
|
|
147
|
+
visitedNodes.add(node);
|
|
148
|
+
return getPendingDependencies(node).some((dependency) =>
|
|
149
|
+
isBlockedBy(dependency.node, blockingNode, visitedNodes),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Break a cycle by deferring one BelongsToOne key that points to a `#ref`
|
|
155
|
+
// model: Its owner is inserted without the key, which is patched in once
|
|
156
|
+
// the referenced model is inserted. Without `#ref`, the graph is a tree,
|
|
157
|
+
// and only waits for the rows that move out of slots can close a cycle,
|
|
158
|
+
// like rows that swap slots. One of these isn't waited for then.
|
|
159
|
+
const breakCycle = () => {
|
|
160
|
+
for (const canDefer of [
|
|
161
|
+
(dependency, node) => this._canDefer(dependency, node),
|
|
162
|
+
(dependency) => dependency.isSlotMove,
|
|
163
|
+
]) {
|
|
164
|
+
for (const node of nodes) {
|
|
165
|
+
if (!handledNodes.has(node)) {
|
|
166
|
+
for (const dependency of getPendingDependencies(node)) {
|
|
167
|
+
if (canDefer(dependency, node) && isBlockedBy(dependency.node, node)) {
|
|
168
|
+
this.deferredDependencies.add(dependency);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return false;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const isReady = (node) => !handledNodes.has(node) && getPendingDependencies(node).length === 0;
|
|
180
|
+
const isReadyReference = (node) => resolvesReferences && node.isReference && isReady(node);
|
|
181
|
+
|
|
182
|
+
while (handledNodes.size < nodes.length) {
|
|
183
|
+
const levelNodes = nodes.filter(isReady);
|
|
184
|
+
|
|
185
|
+
if (levelNodes.length > 0) {
|
|
186
|
+
const level = { nodes: levelNodes, references: [] };
|
|
187
|
+
levels.push(level);
|
|
188
|
+
|
|
189
|
+
for (const node of levelNodes) {
|
|
190
|
+
handledNodes.add(node);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// `#ref` models aren't inserted. They're resolved right after the
|
|
194
|
+
// inserts of the level that completes their dependencies, so that the
|
|
195
|
+
// nodes that wait for them can be inserted in the next level (#311).
|
|
196
|
+
// The other steps write them in levels of their own.
|
|
197
|
+
for (
|
|
198
|
+
let references = nodes.filter(isReadyReference);
|
|
199
|
+
references.length > 0;
|
|
200
|
+
references = nodes.filter(isReadyReference)
|
|
201
|
+
) {
|
|
202
|
+
for (const node of references) {
|
|
203
|
+
handledNodes.add(node);
|
|
204
|
+
level.references.push(node);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else if (!breakCycle()) {
|
|
208
|
+
throw new Error('the object graph contains cyclic references');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return levels;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_canDefer(dependency, node) {
|
|
216
|
+
const { actions } = this.plan;
|
|
217
|
+
const { relation } = dependency;
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
relation &&
|
|
221
|
+
hasOwnerKey(relation) &&
|
|
222
|
+
dependency.node.isReference &&
|
|
223
|
+
// The owner is patched by its id, which models without one don't have (#2305).
|
|
224
|
+
node.modelClass.getIdColumnArray().length > 0 &&
|
|
225
|
+
// Both ends need to be inserted for the key to be patched in later.
|
|
226
|
+
actions.get(node) === 'insert' &&
|
|
227
|
+
actions.get(dependency.node.referencedNode) === 'insert' &&
|
|
228
|
+
// A `#ref{}` that reads the key might be resolved before it's patched in.
|
|
229
|
+
!node.propertyReferencingNodes.some((referencingNode) =>
|
|
230
|
+
referencingNode.propertyReferences.some(
|
|
231
|
+
(reference) =>
|
|
232
|
+
reference.referencedNode === node &&
|
|
233
|
+
relation.ownerProp.props.includes(reference.refPath[0]),
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Return true if the HasMany or HasOne relate of `node` moves its row to an
|
|
240
|
+
// 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 are written
|
|
242
|
+
// once they're known, and a relate into the slot of a HasOne row waits for
|
|
243
|
+
// the removal of that row, or its move, as its key may be unique.
|
|
244
|
+
_isMovedFirst(node, visitedNodes = new Set()) {
|
|
245
|
+
const { actions } = this.plan;
|
|
246
|
+
visitedNodes.add(node);
|
|
247
|
+
|
|
248
|
+
if (
|
|
249
|
+
actions.get(node) === 'relate' &&
|
|
250
|
+
!node.isReference &&
|
|
251
|
+
node.relation.keySide === 'related' &&
|
|
252
|
+
node.parent.existing &&
|
|
253
|
+
!this._waitsForInserts(node)
|
|
254
|
+
) {
|
|
255
|
+
const slotNode = getSlotNode(node);
|
|
256
|
+
const moverNode = getSlotMover(this.plan, node);
|
|
257
|
+
return (
|
|
258
|
+
!(slotNode && actions.get(slotNode) !== 'keep') &&
|
|
259
|
+
// Swapped rows are related together:
|
|
260
|
+
(!moverNode || visitedNodes.has(moverNode) || this._isMovedFirst(moverNode, visitedNodes))
|
|
261
|
+
);
|
|
262
|
+
} else {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Find the relates that move the rows out of the slots of HasOneRelations
|
|
268
|
+
// that new rows go into, and that aren't moved first. They're related in the
|
|
269
|
+
// inserts, along with the relates that move the rows out of their own slots,
|
|
270
|
+
// as the foreign key may be unique.
|
|
271
|
+
_getSlotMovedNodes() {
|
|
272
|
+
const nodes = new Set();
|
|
273
|
+
const addMoverNodes = (node) => {
|
|
274
|
+
for (const { node: moverNode, isSlotMove } of this.dependencies.get(node) || []) {
|
|
275
|
+
if (isSlotMove && !this.movedNodes.has(moverNode) && !nodes.has(moverNode)) {
|
|
276
|
+
nodes.add(moverNode);
|
|
277
|
+
addMoverNodes(moverNode);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
for (const node of this.plan.activeNodes) {
|
|
283
|
+
if (this.plan.actions.get(node) === 'insert') {
|
|
284
|
+
addMoverNodes(node);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return nodes;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Return the props of `node` that the graph fills in from other models: its
|
|
292
|
+
// keys and its `#ref{}` values.
|
|
293
|
+
getFilledProps(node) {
|
|
294
|
+
return [
|
|
295
|
+
...this.keys.getFilledProps(node),
|
|
296
|
+
...(this.dependencies.get(node) || []).flatMap((dependency) =>
|
|
297
|
+
dependency.reference ? [dependency.reference.path[0]] : [],
|
|
298
|
+
),
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Return true if the `#ref{}` of `dependency` reads a prop that the graph
|
|
303
|
+
// fills into the model it depends on. The other props of existing models are
|
|
304
|
+
// known right away, but these only once they're filled in (#356). A `#ref`
|
|
305
|
+
// model is the model it refers to.
|
|
306
|
+
readsFilledProps(dependency) {
|
|
307
|
+
const { node, reference } = dependency;
|
|
308
|
+
const filledProps = this.getFilledProps(node.referencedNode || node);
|
|
309
|
+
return !!reference && filledProps.includes(reference.refPath[0]);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Return true if `node` waits for the keys or references of inserted models,
|
|
313
|
+
// also through the models that it references (#356), and through the models
|
|
314
|
+
// that it waits for to get its keys.
|
|
315
|
+
_waitsForInserts(node, visitedNodes = new Set()) {
|
|
316
|
+
const { actions } = this.plan;
|
|
317
|
+
visitedNodes.add(node);
|
|
318
|
+
return (this.dependencies.get(node) || []).some((dependency) => {
|
|
319
|
+
const { node: dependencyNode, relation } = dependency;
|
|
320
|
+
const referencedNode = dependencyNode.referencedNode || dependencyNode;
|
|
321
|
+
return (
|
|
322
|
+
actions.get(referencedNode) === 'insert' ||
|
|
323
|
+
((!relation || dependency.isKeyWait) &&
|
|
324
|
+
!visitedNodes.has(referencedNode) &&
|
|
325
|
+
this._waitsForInserts(referencedNode, visitedNodes))
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Return true if `node` is a `#ref` model whose model writes all that it would
|
|
331
|
+
// write, as its place keeps nothing of its own: It's a BelongsToOne model, or
|
|
332
|
+
// it's placed where its model is. Many-to-many models write their join rows.
|
|
333
|
+
_isWrittenByModel(node) {
|
|
334
|
+
const { referencedNode, relation, parent } = node;
|
|
335
|
+
return (
|
|
336
|
+
!!referencedNode &&
|
|
337
|
+
(relation.keySide === 'owner' ||
|
|
338
|
+
(relation.keySide === 'related' &&
|
|
339
|
+
referencedNode.relation === relation &&
|
|
340
|
+
referencedNode.parent === parent)) &&
|
|
341
|
+
this.plan.isCoveredByModel(node)
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Map the nodes whose rows reference each other through the keys and values
|
|
346
|
+
// that they write, also through the rows of other nodes, to the first node of
|
|
347
|
+
// their cycle: the strongly connected components of these references, found
|
|
348
|
+
// with Tarjan's algorithm.
|
|
349
|
+
findReferenceCycles(nodes) {
|
|
350
|
+
const getRow = (node) => node.referencedNode || node;
|
|
351
|
+
const nodesByRow = groupBy(nodes, getRow);
|
|
352
|
+
const getReferencedNodes = (node) =>
|
|
353
|
+
(this.dependencies.get(node) || [])
|
|
354
|
+
.flatMap((dependency) => nodesByRow.get(getRow(dependency.node)) || [])
|
|
355
|
+
.filter((referencedNode) => referencedNode !== node);
|
|
356
|
+
|
|
357
|
+
const indices = new Map();
|
|
358
|
+
const lowLinks = new Map();
|
|
359
|
+
const stack = [];
|
|
360
|
+
const stackedNodes = new Set();
|
|
361
|
+
const cycles = new Map();
|
|
362
|
+
|
|
363
|
+
const visit = (node) => {
|
|
364
|
+
indices.set(node, indices.size);
|
|
365
|
+
lowLinks.set(node, indices.get(node));
|
|
366
|
+
stack.push(node);
|
|
367
|
+
stackedNodes.add(node);
|
|
368
|
+
|
|
369
|
+
for (const referencedNode of getReferencedNodes(node)) {
|
|
370
|
+
if (!indices.has(referencedNode)) {
|
|
371
|
+
visit(referencedNode);
|
|
372
|
+
lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(referencedNode)));
|
|
373
|
+
} else if (stackedNodes.has(referencedNode)) {
|
|
374
|
+
lowLinks.set(node, Math.min(lowLinks.get(node), indices.get(referencedNode)));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// `node` is the first node of a component, which holds the nodes above
|
|
379
|
+
// it on the stack:
|
|
380
|
+
if (lowLinks.get(node) === indices.get(node)) {
|
|
381
|
+
const componentNodes = stack.splice(stack.lastIndexOf(node));
|
|
382
|
+
|
|
383
|
+
for (const componentNode of componentNodes) {
|
|
384
|
+
stackedNodes.delete(componentNode);
|
|
385
|
+
|
|
386
|
+
if (componentNodes.length > 1) {
|
|
387
|
+
cycles.set(componentNode, node);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
for (const node of nodes) {
|
|
394
|
+
if (!indices.has(node)) {
|
|
395
|
+
visit(node);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return cycles;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Collect what each node waits for: The node on the other side of its relation
|
|
404
|
+
// if the node stores its keys, and the nodes it references with `#ref` and
|
|
405
|
+
// `#ref{}`.
|
|
406
|
+
function createDependencies(plan, keys) {
|
|
407
|
+
const { actions } = plan;
|
|
408
|
+
const dependencies = new Map();
|
|
409
|
+
|
|
410
|
+
const addDependency = (dependent, dependency) => {
|
|
411
|
+
const nodeDependencies = dependencies.get(dependent);
|
|
412
|
+
const entry = { dependent, ...dependency };
|
|
413
|
+
|
|
414
|
+
if (nodeDependencies) {
|
|
415
|
+
nodeDependencies.push(entry);
|
|
416
|
+
} else {
|
|
417
|
+
dependencies.set(dependent, [entry]);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
for (const node of plan.activeNodes) {
|
|
422
|
+
const { parent, relation } = node;
|
|
423
|
+
|
|
424
|
+
// Models marked with `#unrelate` or `#delete` are never inserted or related,
|
|
425
|
+
// so they don't wait for keys. Ignored models don't replace the current one,
|
|
426
|
+
// so their owners don't wait for them either.
|
|
427
|
+
if (parent && !node.toBeUnrelatedOrDeleted) {
|
|
428
|
+
if (relation.keySide === 'related') {
|
|
429
|
+
addDependency(node, { node: parent, relation, related: node });
|
|
430
|
+
} else if (relation.keySide === 'owner' && actions.get(node) !== 'ignore') {
|
|
431
|
+
addDependency(parent, { node, relation, related: node });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
for (const node of plan.activeNodes) {
|
|
437
|
+
if (node.referencedNode) {
|
|
438
|
+
addDependency(node, { node: node.referencedNode });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for (const reference of node.propertyReferences) {
|
|
442
|
+
addDependency(node, { node: reference.referencedNode, reference });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// A missing row that the graph inserts elsewhere is related once it is:
|
|
446
|
+
const insertingNode =
|
|
447
|
+
node.isMissing && actions.get(node) === 'relate' && plan.findInsertingNode(node);
|
|
448
|
+
|
|
449
|
+
if (insertingNode) {
|
|
450
|
+
addDependency(node, { node: insertingNode });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// A place waits for the inserts and `#ref{}` values that bind the slots of
|
|
454
|
+
// 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
|
+
const getRow = (node) => node.referencedNode || node;
|
|
457
|
+
|
|
458
|
+
for (const { node: waitedNode, prop } of keys.waitsFor(node)) {
|
|
459
|
+
const dependency = (dependencies.get(node) || []).find(
|
|
460
|
+
(dependency) =>
|
|
461
|
+
getRow(dependency.node) === getRow(waitedNode) &&
|
|
462
|
+
(!dependency.relation || getKeyProps(dependency, node).includes(prop)),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
if (dependency) {
|
|
466
|
+
dependency.isKeyWait = true;
|
|
467
|
+
} else {
|
|
468
|
+
addDependency(node, { node: waitedNode, isKeyWait: true });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// A row that goes into the slot of a HasOneRelation waits for the relate
|
|
473
|
+
// that moves the current row out of it, as the foreign key may be unique.
|
|
474
|
+
const moverNode = getSlotMover(plan, node);
|
|
475
|
+
|
|
476
|
+
if (moverNode) {
|
|
477
|
+
addDependency(node, { node: moverNode, isSlotMove: true });
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return dependencies;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Find the relate that moves the current row in the slot of the HasOneRelation
|
|
485
|
+
// that `node` is inserted or related into to another owner, through the same
|
|
486
|
+
// key column.
|
|
487
|
+
function getSlotMover(plan, node) {
|
|
488
|
+
const { actions } = plan;
|
|
489
|
+
const slotNode = ['insert', 'relate'].includes(actions.get(node)) && getSlotNode(node);
|
|
490
|
+
const keptNodes = slotNode ? plan.getKeptNodes(slotNode) : [];
|
|
491
|
+
return (
|
|
492
|
+
keptNodes.find(
|
|
493
|
+
(keptNode) =>
|
|
494
|
+
actions.get(keptNode) === 'relate' && hasSameKey(slotNode.relation, keptNode.relation),
|
|
495
|
+
) || null
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Find the current row in the slot of the HasOneRelation that `node` relates
|
|
500
|
+
// or is inserted into, or null if it's in another relation or an empty slot.
|
|
501
|
+
function getSlotNode(node) {
|
|
502
|
+
const { relation } = node;
|
|
503
|
+
return relation?.keySide === 'related' && relation.isOneToOne()
|
|
504
|
+
? (node.parent.existing?.activeChildren.find(
|
|
505
|
+
(child) => child.relation.name === relation.name,
|
|
506
|
+
) ?? null)
|
|
507
|
+
: null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Return the key props of `node` on its side of the relation of `dependency`.
|
|
511
|
+
function getKeyProps({ relation, related }, node) {
|
|
512
|
+
return (related === node ? relation.relatedProp : relation.ownerProp).props;
|
|
513
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { GraphNode, createNotModelError } from './GraphNode.js';
|
|
1
|
+
import { GraphNode, createNotModelError, formatPath } from './GraphNode.js';
|
|
2
2
|
import { GraphOptions } from './GraphOptions.js';
|
|
3
3
|
import { GraphPlan } from './GraphPlan.js';
|
|
4
|
+
import { GraphKeys, hasSameColumns, readsWrittenValue } from './graphKeys.js';
|
|
5
|
+
import { GraphSchedule } from './GraphSchedule.js';
|
|
4
6
|
import { GraphWriter } from './GraphWriter.js';
|
|
5
7
|
import { loadCurrentGraph } from './GraphLoader.js';
|
|
6
8
|
import { ValidationErrorType } from '../../model/ValidationError.js';
|
|
@@ -68,8 +70,10 @@ export class GraphUpsert {
|
|
|
68
70
|
try {
|
|
69
71
|
checkGraph(builder, new GraphPlan({ nodes, graphOptions }));
|
|
70
72
|
checkPlacements(builder, nodes);
|
|
73
|
+
checkReferenceCycles(nodes);
|
|
71
74
|
const writtenNodes = new Set();
|
|
72
75
|
let keptNodes = new Map();
|
|
76
|
+
let laterNodes = new Set();
|
|
73
77
|
|
|
74
78
|
// Rows that the graph relates into a HasManyRelation or HasOneRelation
|
|
75
79
|
// and gives relations are moved first, and their relations are upserted
|
|
@@ -79,6 +83,7 @@ export class GraphUpsert {
|
|
|
79
83
|
nodes: passNodes,
|
|
80
84
|
graphOptions,
|
|
81
85
|
keptNodes,
|
|
86
|
+
laterNodes,
|
|
82
87
|
});
|
|
83
88
|
const movedNodes = graphOptions.isInsertOnly() ? [] : await loadCurrentGraph(builder, plan);
|
|
84
89
|
|
|
@@ -87,9 +92,12 @@ export class GraphUpsert {
|
|
|
87
92
|
checkMissingModels(builder, plan);
|
|
88
93
|
plan.prune().plan();
|
|
89
94
|
checkChangedIds(builder, plan);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
const [nextLaterNodes, keys] = deferLaterNodes(plan);
|
|
96
|
+
|
|
97
|
+
// A pass that only leaves its writes to the next one waits for itself:
|
|
98
|
+
if (plan.activeNodes.length === 0) {
|
|
99
|
+
throw new Error('the object graph contains cyclic references');
|
|
100
|
+
}
|
|
93
101
|
|
|
94
102
|
// Models that `noInsert` or `noRelate` ignore aren't written, but the
|
|
95
103
|
// ones marked with `#delete` or `#unrelate` keep their values:
|
|
@@ -101,9 +109,16 @@ export class GraphUpsert {
|
|
|
101
109
|
|
|
102
110
|
checkReferences(builder, plan, writtenNodes);
|
|
103
111
|
checkJoinTables(builder, plan);
|
|
104
|
-
await new GraphWriter(plan).write(builder);
|
|
105
|
-
|
|
106
|
-
|
|
112
|
+
await new GraphWriter(new GraphSchedule(plan, keys)).write(builder);
|
|
113
|
+
keptNodes = plan.getNextKeptNodes();
|
|
114
|
+
|
|
115
|
+
// Moved rows may also wait for a later pass:
|
|
116
|
+
passNodes = [
|
|
117
|
+
...new Set(
|
|
118
|
+
[...movedNodes, ...nextLaterNodes].flatMap((node) => [node, ...node.descendants]),
|
|
119
|
+
),
|
|
120
|
+
];
|
|
121
|
+
laterNodes = new Set(nextLaterNodes);
|
|
107
122
|
identifyReferences(passNodes);
|
|
108
123
|
}
|
|
109
124
|
} catch (err) {
|
|
@@ -120,6 +135,22 @@ export class GraphUpsert {
|
|
|
120
135
|
}
|
|
121
136
|
}
|
|
122
137
|
|
|
138
|
+
// Leave the writes that need keys or `#ref{}` values that only a later pass
|
|
139
|
+
// gives to that pass, with their branches, and return them with the keys of
|
|
140
|
+
// the rest.
|
|
141
|
+
function deferLaterNodes(plan) {
|
|
142
|
+
const laterNodes = [];
|
|
143
|
+
let keys = new GraphKeys(plan);
|
|
144
|
+
|
|
145
|
+
for (let nodes = keys.getLaterNodes(); nodes.length > 0; nodes = keys.getLaterNodes()) {
|
|
146
|
+
laterNodes.push(...nodes);
|
|
147
|
+
plan.defer([...plan.deferredNodes, ...nodes.flatMap((node) => [node, ...node.descendants])]);
|
|
148
|
+
keys = new GraphKeys(plan);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return [laterNodes, keys];
|
|
152
|
+
}
|
|
153
|
+
|
|
123
154
|
// Check the models with ids that are neither at their place nor related, once
|
|
124
155
|
// the current graph of a pass is loaded.
|
|
125
156
|
function checkMissingModels(builder, plan) {
|
|
@@ -417,7 +448,12 @@ function checkPlacements(builder, nodes) {
|
|
|
417
448
|
const isDeleted = node.toBeDeleted && !isUnrelated;
|
|
418
449
|
|
|
419
450
|
if (relation?.keySide === 'related') {
|
|
420
|
-
|
|
451
|
+
// A row that lists itself on the same column restates its own key:
|
|
452
|
+
const isSelfLink =
|
|
453
|
+
getRowId(node) === getRowId(parent) &&
|
|
454
|
+
hasSameColumns(relation.relatedProp, relation.ownerProp);
|
|
455
|
+
|
|
456
|
+
if (!isDeleted && !isSelfLink) {
|
|
421
457
|
relation.relatedProp.forEach((i) => {
|
|
422
458
|
const key = isUnrelated ? null : getKey(parent, relation.ownerProp, i);
|
|
423
459
|
setNodeValue(node, relation.relatedProp.props[i], key, path);
|
|
@@ -541,6 +577,31 @@ function checkReferences(builder, plan, writtenNodes) {
|
|
|
541
577
|
}
|
|
542
578
|
}
|
|
543
579
|
|
|
580
|
+
// A `#ref{}` waits for the model it reads if the value only exists once that
|
|
581
|
+
// model is written. These waits follow from the graph alone, so their cycles
|
|
582
|
+
// are found across all passes, before the first write.
|
|
583
|
+
function checkReferenceCycles(nodes) {
|
|
584
|
+
const getWaitedNodes = (node) =>
|
|
585
|
+
node.propertyReferences
|
|
586
|
+
.filter((reference) => reference.referencedNode !== node && readsWrittenValue(reference))
|
|
587
|
+
.map(({ referencedNode }) => referencedNode);
|
|
588
|
+
const visitingNodes = new Set();
|
|
589
|
+
const visitedNodes = new Set();
|
|
590
|
+
|
|
591
|
+
const visit = (node) => {
|
|
592
|
+
if (visitingNodes.has(node)) {
|
|
593
|
+
throw new Error('the object graph contains cyclic references');
|
|
594
|
+
} else if (!visitedNodes.has(node)) {
|
|
595
|
+
visitingNodes.add(node);
|
|
596
|
+
getWaitedNodes(node).forEach(visit);
|
|
597
|
+
visitingNodes.delete(node);
|
|
598
|
+
visitedNodes.add(node);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
nodes.forEach(visit);
|
|
603
|
+
}
|
|
604
|
+
|
|
544
605
|
// Inserting or relating a model of a many-to-many relation inserts a join row,
|
|
545
606
|
// which would be a new related row if the join table is also the related table
|
|
546
607
|
// (#133).
|
|
@@ -558,12 +619,6 @@ function checkJoinTables(builder, plan) {
|
|
|
558
619
|
}
|
|
559
620
|
}
|
|
560
621
|
|
|
561
|
-
// Name the place of a model in error messages, by its data path.
|
|
562
|
-
function formatPath(dataPathKey) {
|
|
563
|
-
const path = dataPathKey.replace(/^\./, '');
|
|
564
|
-
return path ? `'${path}'` : 'the root';
|
|
565
|
-
}
|
|
566
|
-
|
|
567
622
|
function isOnConflictOperation(operation) {
|
|
568
623
|
return ['onConflict', 'ignore', 'merge'].includes(operation.name);
|
|
569
624
|
}
|