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.
@@ -1,68 +1,41 @@
1
1
  import { getModel } from '../../model/getModel.js';
2
2
  import { restoreOmitFromDatabaseJson } from '../../model/modelUtils.js';
3
+ import { valuesKey } from '../../model/modelValues.js';
3
4
  import { UpdateOperation } from '../operations/UpdateOperation.js';
4
5
  import { getMaxRows, hasBatchInsert } from '../../utils/knexUtils.js';
5
6
  import { isInternalProp } from '../../utils/internalPropUtils.js';
6
- import {
7
- chunk,
8
- difference,
9
- get,
10
- groupBy,
11
- isObject,
12
- jsonEquals,
13
- nonStrictEquals,
14
- set,
15
- } from '../../utils/objectUtils.js';
7
+ import { chunk, difference, groupBy, isObject, nonStrictEquals } from '../../utils/objectUtils.js';
16
8
  import { map as promiseMap } from '../../utils/promiseUtils/index.js';
9
+ import {
10
+ clearKeysOfRemovedRows,
11
+ fetchGeneratedKeys,
12
+ getChangedKeysOfRelatedRows,
13
+ resolveDependencies,
14
+ resolveFilledDependencies,
15
+ resolveReference,
16
+ setKeyArrays,
17
+ } from './graphKeys.js';
17
18
 
18
19
  const MAX_INSERT_BATCH_SIZE = 100;
19
20
  const MAX_CONCURRENCY = 100;
20
21
 
21
- // Write a planned graph in steps, in an order that keeps keys and constraints
22
- // intact (#214):
23
- // 1. Move the rows that the graph relates to existing owners of HasMany and
24
- // HasOne relations, as they may still sit below rows that the next step
25
- // deletes. Rows that wait for inserts are related in step 3 or 4.
26
- // 2. Remove rows that reference their owners, before the inserts that may need
27
- // their unique keys.
28
- // 3. Insert the new rows in the order of their keys and references, then the
29
- // keys deferred to break cycles and the join rows. Rows that move out of
30
- // the slots of HasOneRelations that new rows go into are related in the
31
- // same order, as the foreign key may be unique.
32
- // 4. Patch the changed rows, the other relates, and the keys of owners whose
33
- // BelongsToOne rows were inserted, related or removed.
34
- // 5. Remove the BelongsToOne rows that the owners no longer reference.
35
- // Each step writes a row that goes into the slot of a HasOneRelation after the
36
- // row that moves out of it, unless that closes a cycle.
37
- // `insertGraph()` only inserts.
22
+ // Write a planned graph in the steps of its `GraphSchedule`.
38
23
  export class GraphWriter {
39
- constructor(plan) {
40
- this.plan = plan;
41
- // The nodes that each node waits for, through its keys, its references and
42
- // the rows that move out of the slots that it goes into:
43
- this.dependencies = createDependencies(plan);
44
- // Dependencies that aren't waited for, to break cycles. Deferred keys are
45
- // written after the inserts:
46
- this.deferredDependencies = new Set();
47
- // Relates whose rows are moved to their new owners before the removals:
48
- this.movedNodes = new Set(
49
- plan.graphOptions.isInsertOnly()
50
- ? []
51
- : plan.activeNodes.filter((node) => this._isMovedFirst(node)),
52
- );
53
- // Relates that move the rows out of the slots of HasOneRelations that new
54
- // rows go into, and that aren't moved first:
55
- this.slotMovedNodes = this._getSlotMovedNodes();
56
- this.steps = this._planSteps();
24
+ constructor(schedule) {
25
+ this.schedule = schedule;
26
+ this.plan = schedule.plan;
57
27
  }
58
28
 
59
29
  async write(builder) {
60
- // Fill in the keys that existing models leave out, before new children read them:
30
+ // Fill in the keys that are known before the writes. Inserted models get
31
+ // theirs right before their insert:
61
32
  for (const node of this.plan.activeNodes) {
62
- copyKeysFromExisting(node, this.plan.actions);
33
+ if (this.plan.actions.get(node) !== 'insert') {
34
+ this.schedule.keys.fill(node);
35
+ }
63
36
  }
64
37
 
65
- for (const step of this.steps) {
38
+ for (const step of this.schedule.steps) {
66
39
  await this._writeStep(builder, step);
67
40
  }
68
41
 
@@ -72,69 +45,6 @@ export class GraphWriter {
72
45
  }
73
46
  }
74
47
 
75
- _planSteps() {
76
- const { activeNodes, activeCurrentNodes, actions, graphOptions } = this.plan;
77
- const insertSteps = [
78
- ...this._planLevels(activeNodes, { resolvesReferences: true }).map((level) => ({
79
- kind: 'insert',
80
- ...level,
81
- })),
82
- {
83
- kind: 'deferredKeys',
84
- dependencies: [...this.deferredDependencies].filter((dependency) => dependency.relation),
85
- },
86
- {
87
- kind: 'joinRows',
88
- nodes: activeNodes.filter(
89
- (node) =>
90
- node.relation?.keySide === 'join' &&
91
- !node.current &&
92
- ['insert', 'relate'].includes(actions.get(node)) &&
93
- // Models that `noInsert` skips have no key for join rows (#369):
94
- actions.get(node.parent) !== 'ignore',
95
- ),
96
- },
97
- ];
98
-
99
- if (graphOptions.isInsertOnly()) {
100
- return insertSteps;
101
- } else {
102
- const removedNodes = activeCurrentNodes.filter((node) => actions.get(node) !== 'keep');
103
- const isReferencedByOwner = (currentNode) => currentNode.relation.keySide === 'owner';
104
-
105
- return [
106
- ...this._planLevels([...this.movedNodes]).map(({ nodes }) => ({ kind: 'move', nodes })),
107
- {
108
- kind: 'remove',
109
- nodes: removedNodes.filter((node) => !isReferencedByOwner(node)),
110
- areReferenced: true,
111
- },
112
- ...insertSteps,
113
- ...this._planLevels(
114
- activeNodes.filter((node) => {
115
- const action = actions.get(node);
116
- return (
117
- (action === 'patch' ||
118
- (action === 'relate' &&
119
- !this.movedNodes.has(node) &&
120
- !this.slotMovedNodes.has(node))) &&
121
- // The row of a `#ref` model is written once, by its model:
122
- !this._isWrittenByModel(node)
123
- );
124
- }),
125
- ).map(({ nodes }) => ({ kind: 'patch', nodes })),
126
- {
127
- kind: 'remove',
128
- // Owners unrelate their BelongsToOne rows by patching their keys:
129
- nodes: removedNodes.filter(
130
- (node) => isReferencedByOwner(node) && actions.get(node) === 'delete',
131
- ),
132
- areReferenced: false,
133
- },
134
- ];
135
- }
136
- }
137
-
138
48
  _writeStep(builder, step) {
139
49
  switch (step.kind) {
140
50
  case 'move':
@@ -152,282 +62,14 @@ export class GraphWriter {
152
62
  }
153
63
  }
154
64
 
155
- // Group the nodes into levels that can be written once the levels before
156
- // them are: Nodes wait for the nodes whose keys they store, the nodes that
157
- // they reference, and the rows that move out of the slots they go into. For
158
- // the inserts, all nodes take part, so that the keys of the nodes that aren't
159
- // inserted are resolved in order too, and `resolvesReferences` says so.
160
- _planLevels(nodes, { resolvesReferences = false } = {}) {
161
- const nodeSet = new Set(nodes);
162
- const handledNodes = new Set();
163
- const levels = [];
164
-
165
- // The keys of models with rows are known, also of related models that were
166
- // fetched, except for the props that the graph fills in:
167
- const hasRow = (node) => !!(node.referencedNode || node).existing;
168
- // Nodes outside this pass were written by an earlier one, or are never
169
- // written, see `checkReferences()`:
170
- const getPendingDependencies = (node) =>
171
- (this.dependencies.get(node) || []).filter(
172
- (dependency) =>
173
- nodeSet.has(dependency.node) &&
174
- !this.deferredDependencies.has(dependency) &&
175
- !handledNodes.has(dependency.node) &&
176
- (dependency.isSlotMove || !hasRow(dependency.node) || this._readsFilledProps(dependency)),
177
- );
178
-
179
- // Return true if `node` waits for `blockingNode`, through other nodes too.
180
- const isBlockedBy = (node, blockingNode, visitedNodes = new Set()) => {
181
- if (node === blockingNode) {
182
- return true;
183
- } else if (visitedNodes.has(node)) {
184
- return false;
185
- } else {
186
- visitedNodes.add(node);
187
- return getPendingDependencies(node).some((dependency) =>
188
- isBlockedBy(dependency.node, blockingNode, visitedNodes),
189
- );
190
- }
191
- };
192
-
193
- // Break a cycle by deferring one BelongsToOne key that points to a `#ref`
194
- // model: Its owner is inserted without the key, which is patched in once
195
- // the referenced model is inserted. Without `#ref`, the graph is a tree,
196
- // and only waits for the rows that move out of slots can close a cycle,
197
- // like rows that swap slots. One of these isn't waited for then.
198
- const breakCycle = () => {
199
- for (const canDefer of [
200
- (dependency, node) => this._canDefer(dependency, node),
201
- (dependency) => dependency.isSlotMove,
202
- ]) {
203
- for (const node of nodes) {
204
- if (!handledNodes.has(node)) {
205
- for (const dependency of getPendingDependencies(node)) {
206
- if (canDefer(dependency, node) && isBlockedBy(dependency.node, node)) {
207
- this.deferredDependencies.add(dependency);
208
- return true;
209
- }
210
- }
211
- }
212
- }
213
- }
214
-
215
- return false;
216
- };
217
-
218
- const isReady = (node) => !handledNodes.has(node) && getPendingDependencies(node).length === 0;
219
- const isReadyReference = (node) => resolvesReferences && node.isReference && isReady(node);
220
-
221
- while (handledNodes.size < nodes.length) {
222
- const levelNodes = nodes.filter(isReady);
223
-
224
- if (levelNodes.length > 0) {
225
- const level = { nodes: levelNodes, references: [] };
226
- levels.push(level);
227
-
228
- for (const node of levelNodes) {
229
- handledNodes.add(node);
230
- }
231
-
232
- // `#ref` models aren't inserted. They're resolved right after the
233
- // inserts of the level that completes their dependencies, so that the
234
- // nodes that wait for them can be inserted in the next level (#311).
235
- // The other steps write them in levels of their own.
236
- for (
237
- let references = nodes.filter(isReadyReference);
238
- references.length > 0;
239
- references = nodes.filter(isReadyReference)
240
- ) {
241
- for (const node of references) {
242
- handledNodes.add(node);
243
- level.references.push(node);
244
- }
245
- }
246
- } else if (!breakCycle()) {
247
- throw new Error('the object graph contains cyclic references');
248
- }
249
- }
250
-
251
- return levels;
252
- }
253
-
254
- _canDefer(dependency, node) {
255
- const { actions } = this.plan;
256
- const { relation } = dependency;
257
-
258
- return (
259
- relation &&
260
- hasOwnerKey(relation) &&
261
- dependency.node.isReference &&
262
- // The owner is patched by its id, which models without one don't have (#2305).
263
- node.modelClass.getIdColumnArray().length > 0 &&
264
- // Both ends need to be inserted for the key to be patched in later.
265
- actions.get(node) === 'insert' &&
266
- actions.get(dependency.node.referencedNode) === 'insert' &&
267
- // A `#ref{}` that reads the key might be resolved before it's patched in.
268
- !node.propertyReferencingNodes.some((referencingNode) =>
269
- referencingNode.propertyReferences.some(
270
- (reference) =>
271
- reference.referencedNode === node &&
272
- relation.ownerProp.props.includes(reference.refPath[0]),
273
- ),
274
- )
275
- );
276
- }
277
-
278
- // Return true if the HasMany or HasOne relate of `node` moves its row to an
279
- // existing owner before the removals, see the steps above. `#ref` models may
280
- // get their ids from the inserts, rows that wait for the inserts are written
281
- // once they're known, and a relate into the slot of a HasOne row waits for
282
- // the removal of that row, or its move, as its key may be unique.
283
- _isMovedFirst(node, visitedNodes = new Set()) {
284
- const { actions } = this.plan;
285
- visitedNodes.add(node);
286
-
287
- if (
288
- actions.get(node) === 'relate' &&
289
- !node.isReference &&
290
- node.relation.keySide === 'related' &&
291
- node.parent.existing &&
292
- !this._waitsForInserts(node)
293
- ) {
294
- const slotNode = getSlotNode(node);
295
- const moverNode = getSlotMover(this.plan, node);
296
- return (
297
- !(slotNode && actions.get(slotNode) !== 'keep') &&
298
- // Swapped rows are related together:
299
- (!moverNode || visitedNodes.has(moverNode) || this._isMovedFirst(moverNode, visitedNodes))
300
- );
301
- } else {
302
- return false;
303
- }
304
- }
305
-
306
- // Find the relates that move the rows out of the slots of HasOneRelations
307
- // that new rows go into, and that aren't moved first. They're related in the
308
- // inserts, along with the relates that move the rows out of their own slots,
309
- // as the foreign key may be unique.
310
- _getSlotMovedNodes() {
311
- const nodes = new Set();
312
- const addMoverNodes = (node) => {
313
- for (const { node: moverNode, isSlotMove } of this.dependencies.get(node) || []) {
314
- if (isSlotMove && !this.movedNodes.has(moverNode) && !nodes.has(moverNode)) {
315
- nodes.add(moverNode);
316
- addMoverNodes(moverNode);
317
- }
318
- }
319
- };
320
-
321
- for (const node of this.plan.activeNodes) {
322
- if (this.plan.actions.get(node) === 'insert') {
323
- addMoverNodes(node);
324
- }
325
- }
326
-
327
- return nodes;
328
- }
329
-
330
- // Return the props of `node` that the graph fills in from other models,
331
- // through its relations and its references.
332
- _getFilledProps(node) {
333
- return (this.dependencies.get(node) || []).flatMap((dependency) =>
334
- dependency.reference
335
- ? [dependency.reference.path[0]]
336
- : dependency.relation
337
- ? getKeyProps(dependency, node)
338
- : [],
339
- );
340
- }
341
-
342
- // Return true if `dependency` reads props that the graph fills into the
343
- // model it depends on: the keys of a relation, or the value of a `#ref{}`.
344
- // The other props of existing models are known right away, but these only
345
- // once they're filled in (#356). A `#ref` model is the model it refers to,
346
- // so its relations wait for the props filled into that model.
347
- _readsFilledProps(dependency) {
348
- const { node, relation, reference } = dependency;
349
- const filledProps = this._getFilledProps(node.referencedNode || node);
350
- const readProps = relation
351
- ? getKeyProps(dependency, node)
352
- : reference
353
- ? [reference.refPath[0]]
354
- : [];
355
- return readProps.some((prop) => filledProps.includes(prop));
356
- }
357
-
358
- // Return true if `node` waits for the keys or references of inserted models,
359
- // also through the models that it references (#356), and through the models
360
- // whose keys it reads once the graph fills them in.
361
- _waitsForInserts(node, visitedNodes = new Set()) {
362
- const { actions } = this.plan;
363
- visitedNodes.add(node);
364
- return (this.dependencies.get(node) || []).some((dependency) => {
365
- const { node: dependencyNode, relation } = dependency;
366
- const referencedNode = dependencyNode.referencedNode || dependencyNode;
367
- return (
368
- actions.get(referencedNode) === 'insert' ||
369
- ((!relation || this._readsFilledProps(dependency)) &&
370
- !visitedNodes.has(referencedNode) &&
371
- this._waitsForInserts(referencedNode, visitedNodes))
372
- );
373
- });
374
- }
375
-
376
- // Return true if the model of the `#ref` model `node` writes what `node`
377
- // would write to the row: The model is inserted, or it's written and its path
378
- // allows any write that the path of `node` allows.
379
- _isCoveredByModel(node) {
380
- const { actions, graphOptions } = this.plan;
381
- const { referencedNode } = node;
382
- const action = actions.get(referencedNode);
383
- const allowsWrite = (node) => graphOptions.shouldPatch(node) || graphOptions.shouldUpdate(node);
384
- return (
385
- action === 'insert' ||
386
- (['relate', 'patch'].includes(action) && (allowsWrite(referencedNode) || !allowsWrite(node)))
387
- );
388
- }
389
-
390
- // Return true if `node` is a `#ref` model whose model writes all that it would
391
- // write, as its place keeps nothing of its own: It's a BelongsToOne model, or
392
- // it's placed where its model is. Many-to-many models write their join rows.
393
- _isWrittenByModel(node) {
394
- const { referencedNode, relation, parent } = node;
395
- return (
396
- !!referencedNode &&
397
- (relation.keySide === 'owner' ||
398
- (relation.keySide === 'related' &&
399
- referencedNode.relation === relation &&
400
- referencedNode.parent === parent)) &&
401
- this._isCoveredByModel(node)
402
- );
403
- }
404
-
405
65
  _move(builder, nodes) {
406
66
  for (const node of nodes) {
407
- this._resolveFilledDependencies(node);
67
+ resolveFilledDependencies(this.schedule, node);
408
68
  }
409
69
 
410
70
  return this._patch(builder, nodes);
411
71
  }
412
72
 
413
- // Copy the keys and `#ref{}` values that `node` waits for into its model,
414
- // after those that the graph fills into the models that it reads them from,
415
- // e.g. the new key of an owner whose BelongsToOne row is replaced. Rows that
416
- // move first only wait for rows that exist, see `_waitsForInserts()`.
417
- _resolveFilledDependencies(node, visitedNodes = new Set()) {
418
- visitedNodes.add(node);
419
-
420
- for (const dependency of this.dependencies.get(node) || []) {
421
- const referencedNode = dependency.node.referencedNode || dependency.node;
422
-
423
- if (!visitedNodes.has(referencedNode) && this._readsFilledProps(dependency)) {
424
- this._resolveFilledDependencies(referencedNode, visitedNodes);
425
- }
426
- }
427
-
428
- this._resolveDependencies(node);
429
- }
430
-
431
73
  _remove(builder, nodes, areReferenced) {
432
74
  const { actions } = this.plan;
433
75
  const queries = [];
@@ -482,24 +124,11 @@ export class GraphWriter {
482
124
  });
483
125
  await this._patch(
484
126
  builder,
485
- nodes.filter((node) => this.slotMovedNodes.has(node)),
127
+ nodes.filter((node) => this.schedule.slotMovedNodes.has(node)),
486
128
  );
487
129
 
488
130
  for (const node of references) {
489
- this._resolveDependencies(node);
490
- }
491
- }
492
-
493
- // Copy the keys and `#ref{}` values that `node` waits for into its model.
494
- _resolveDependencies(node) {
495
- for (const dependency of this.dependencies.get(node) || []) {
496
- if (!this.deferredDependencies.has(dependency)) {
497
- if (dependency.relation) {
498
- resolveKeys(dependency.related);
499
- } else if (dependency.reference) {
500
- resolvePropertyReference(dependency.dependent, dependency.reference);
501
- }
502
- }
131
+ resolveDependencies(this.schedule, node);
503
132
  }
504
133
  }
505
134
 
@@ -507,7 +136,7 @@ export class GraphWriter {
507
136
  const { actions } = this.plan;
508
137
 
509
138
  for (const node of nodes) {
510
- this._resolveDependencies(node);
139
+ resolveDependencies(this.schedule, node);
511
140
 
512
141
  // Join table extras are written to the join rows.
513
142
  if (node.relation?.keySide === 'join') {
@@ -515,7 +144,7 @@ export class GraphWriter {
515
144
  }
516
145
 
517
146
  if (actions.get(node) === 'insert') {
518
- this._setKeyArrays(node);
147
+ setKeyArrays(this.plan, node);
519
148
  }
520
149
  }
521
150
 
@@ -543,6 +172,7 @@ export class GraphWriter {
543
172
  .execute();
544
173
 
545
174
  await fetchGeneratedKeys(builder, modelClass, insertNodes);
175
+ this.schedule.keys.setInserted(insertNodes);
546
176
  }
547
177
  }
548
178
 
@@ -556,9 +186,9 @@ export class GraphWriter {
556
186
  const idProp = modelClass.getIdRelationProperty();
557
187
  const patch = {};
558
188
 
559
- for (const { relation, related } of dependencies) {
560
- resolveKeys(related);
189
+ this.schedule.keys.fill(owner);
561
190
 
191
+ for (const { relation } of dependencies) {
562
192
  relation.ownerProp.forEach((i) => {
563
193
  relation.ownerProp.patch(patch, i, relation.ownerProp.getProp(owner.obj, i));
564
194
  });
@@ -589,8 +219,9 @@ export class GraphWriter {
589
219
  const { relation, parent } = node;
590
220
  resolveReference(node);
591
221
 
592
- const key = `${relation.name}:${relation.ownerProp.propKey(parent.obj)}:${relation.relatedProp.propKey(node.obj)}`;
593
- const row = relation.createJoinModel(relation.ownerProp.getProps(parent.obj), node.obj);
222
+ const ownerKeys = this.schedule.keys.getProps(parent, relation.ownerProp);
223
+ const key = `${relation.name}:${valuesKey(ownerKeys)}:${relation.relatedProp.propKey(node.obj)}`;
224
+ const row = relation.createJoinModel(ownerKeys, node.obj);
594
225
  joinRows.set(key, { relation, row: { ...row, ...joinRows.get(key)?.row } });
595
226
  }
596
227
 
@@ -624,7 +255,7 @@ export class GraphWriter {
624
255
 
625
256
  try {
626
257
  const patches = nodes.map((node) => this._preparePatch(builder, node)).filter(Boolean);
627
- const cycles = this._findReferenceCycles(patches.map(({ node }) => node));
258
+ const cycles = this.schedule.findReferenceCycles(patches.map(({ node }) => node));
628
259
  // Patches whose rows reference each other lock them in opposite order
629
260
  // when they run at the same time, which deadlocks on MySQL. We write the
630
261
  // patches of each cycle one after the other (#385):
@@ -645,63 +276,6 @@ export class GraphWriter {
645
276
  }
646
277
  }
647
278
 
648
- // Map the nodes whose rows reference each other through the keys and values
649
- // that they write, also through the rows of other nodes, to the first node of
650
- // their cycle: the strongly connected components of these references, found
651
- // with Tarjan's algorithm.
652
- _findReferenceCycles(nodes) {
653
- const getRow = (node) => node.referencedNode || node;
654
- const nodesByRow = groupBy(nodes, getRow);
655
- const getReferencedNodes = (node) =>
656
- (this.dependencies.get(node) || [])
657
- .flatMap((dependency) => nodesByRow.get(getRow(dependency.node)) || [])
658
- .filter((referencedNode) => referencedNode !== node);
659
-
660
- const indices = new Map();
661
- const lowLinks = new Map();
662
- const stack = [];
663
- const stackedNodes = new Set();
664
- const cycles = new Map();
665
-
666
- const visit = (node) => {
667
- indices.set(node, indices.size);
668
- lowLinks.set(node, indices.get(node));
669
- stack.push(node);
670
- stackedNodes.add(node);
671
-
672
- for (const referencedNode of getReferencedNodes(node)) {
673
- if (!indices.has(referencedNode)) {
674
- visit(referencedNode);
675
- lowLinks.set(node, Math.min(lowLinks.get(node), lowLinks.get(referencedNode)));
676
- } else if (stackedNodes.has(referencedNode)) {
677
- lowLinks.set(node, Math.min(lowLinks.get(node), indices.get(referencedNode)));
678
- }
679
- }
680
-
681
- // `node` is the first node of a component, which holds the nodes above
682
- // it on the stack:
683
- if (lowLinks.get(node) === indices.get(node)) {
684
- const componentNodes = stack.splice(stack.lastIndexOf(node));
685
-
686
- for (const componentNode of componentNodes) {
687
- stackedNodes.delete(componentNode);
688
-
689
- if (componentNodes.length > 1) {
690
- cycles.set(componentNode, node);
691
- }
692
- }
693
- }
694
- };
695
-
696
- for (const node of nodes) {
697
- if (!indices.has(node)) {
698
- visit(node);
699
- }
700
- }
701
-
702
- return cycles;
703
- }
704
-
705
279
  // Find out what to write for `node`, validate it, and return the patch, or
706
280
  // nothing if there is nothing to write.
707
281
  _preparePatch(builder, node) {
@@ -710,7 +284,7 @@ export class GraphWriter {
710
284
  // The props that the graph fills into the model of a `#ref` model are only
711
285
  // final now:
712
286
  if (referencedNode) {
713
- resolveReference(node, this._getFilledProps(referencedNode));
287
+ resolveReference(node, this.schedule.getFilledProps(referencedNode));
714
288
  }
715
289
 
716
290
  const { graphOptions } = this.plan;
@@ -720,9 +294,9 @@ export class GraphWriter {
720
294
  // their owners, and relates into other relations the keys of the related
721
295
  // rows, which are written even with `noUpdate` (#53):
722
296
  const keyProps = [
723
- ...this._setKeysOfRelatedRows(node),
724
- ...this._clearKeysOfRemovedRows(node),
725
- ...this._setKeyArrays(node),
297
+ ...getChangedKeysOfRelatedRows(this.plan, this.schedule.keys, node),
298
+ ...clearKeysOfRemovedRows(this.plan, node),
299
+ ...setKeyArrays(this.plan, node),
726
300
  ...(this.plan.actions.get(node) === 'relate' && node.relation.keySide === 'related'
727
301
  ? node.relation.relatedProp.props
728
302
  : []),
@@ -731,9 +305,10 @@ export class GraphWriter {
731
305
  // `#ref` model whose model writes its row only writes its join row:
732
306
  const idProps = node.modelClass.getIdPropertyArray();
733
307
  const isJoinRowOnly =
734
- !!referencedNode && node.relation.keySide === 'join' && this._isCoveredByModel(node);
308
+ !!referencedNode && node.relation.keySide === 'join' && this.plan.isCoveredByModel(node);
735
309
  const isWritable = (prop) =>
736
310
  !idProps.includes(prop) &&
311
+ !this.schedule.keys.isOnlyRead(node, prop) &&
737
312
  (!isJoinRowOnly || node.relation.joinTableExtras.some((extra) => extra.aliasProp === prop));
738
313
 
739
314
  const getPropsToUpdate = (changedProps) =>
@@ -848,116 +423,6 @@ export class GraphWriter {
848
423
  .execute();
849
424
  }
850
425
 
851
- // Change the arrays of keys that `node` holds for the relations that the graph
852
- // gives it: Remove the keys of the current related rows that it deletes or
853
- // unrelates, and add the keys of the related models that it writes. Other
854
- // keys stay, e.g. of rows that the relation's filter leaves out. Relations
855
- // that the graph leaves out keep their arrays (#1202). Return the props that
856
- // change.
857
- _setKeyArrays(node) {
858
- const { actions, graphOptions } = this.plan;
859
- const { existing } = node;
860
- const props = [];
861
-
862
- for (const name of node.modelClass.getRelationNames()) {
863
- const relation = node.obj[name] !== undefined && node.modelClass.getRelation(name);
864
-
865
- if (relation && hasOwnerKeyArray(relation)) {
866
- const { ownerProp, relatedProp } = relation;
867
- const getKey = (related) => relatedProp.getProp(related.obj, 0);
868
- const keys = relation.getKeys(existing ? existing.obj : node.obj);
869
- const removed = (existing?.activeChildren || []).filter(
870
- (child) =>
871
- child.relation.name === name && ['delete', 'unrelate'].includes(actions.get(child)),
872
- );
873
- const added = node.activeChildren.filter(
874
- (child) =>
875
- child.relation.name === name &&
876
- !child.toBeUnrelatedOrDeleted &&
877
- actions.get(child) !== 'ignore',
878
- );
879
-
880
- for (const related of added) {
881
- resolveReference(related);
882
- }
883
-
884
- const newKeys = relation.changeKeys(keys, {
885
- add: added.map(getKey),
886
- remove: removed.map(getKey),
887
- });
888
-
889
- if (!existing || graphOptions.shouldUpdate(node)) {
890
- // Inserts and updates write the whole row:
891
- ownerProp.setProp(node.obj, 0, newKeys);
892
- props.push(ownerProp.props[0]);
893
- } else if (!jsonEquals(keys, newKeys)) {
894
- const patch = relation.createKeysPatch(existing.obj, newKeys);
895
- Object.assign(node.obj, patch);
896
- props.push(...Object.keys(patch));
897
- }
898
- }
899
- }
900
-
901
- return props;
902
- }
903
-
904
- // Point the keys of `node` to its BelongsToOne rows that are inserted or
905
- // related, and return the props that change.
906
- _setKeysOfRelatedRows(node) {
907
- const { actions } = this.plan;
908
- const props = [];
909
-
910
- for (const child of node.activeChildren) {
911
- const { relation } = child;
912
-
913
- if (
914
- hasOwnerKey(relation) &&
915
- relation.relatedProp.hasProps(child.obj) &&
916
- // Ignored models don't replace the current one, and models marked with
917
- // `#unrelate` or `#delete` are handled below.
918
- actions.get(child) !== 'ignore'
919
- ) {
920
- relation.ownerProp.forEach((i) => {
921
- const currentValue = node.existing && relation.ownerProp.getProp(node.existing.obj, i);
922
- const value = relation.relatedProp.getProp(child.obj, i);
923
-
924
- if (currentValue != value) {
925
- relation.ownerProp.setProp(node.obj, i, value);
926
- props.push(relation.ownerProp.props[i]);
927
- }
928
- });
929
- }
930
- }
931
-
932
- return props;
933
- }
934
-
935
- // Clear the keys of `node` that point to BelongsToOne rows that are removed,
936
- // and return the props that change.
937
- _clearKeysOfRemovedRows(node) {
938
- const { actions } = this.plan;
939
- const props = [];
940
-
941
- for (const currentChild of node.existing?.activeChildren || []) {
942
- const { relation } = currentChild;
943
-
944
- if (
945
- hasOwnerKey(relation) &&
946
- (node.obj[relation.name] === null || currentChild.node?.toBeUnrelatedOrDeleted) &&
947
- actions.get(currentChild) !== 'keep'
948
- ) {
949
- relation.ownerProp.forEach((i) => {
950
- if (relation.ownerProp.getProp(node.existing.obj, i) != null) {
951
- relation.ownerProp.setProp(node.obj, i, null);
952
- props.push(relation.ownerProp.props[i]);
953
- }
954
- });
955
- }
956
- }
957
-
958
- return props;
959
- }
960
-
961
426
  _findChanges(node) {
962
427
  const { obj, relation } = node;
963
428
  const currentObj = node.current?.obj || {};
@@ -997,202 +462,6 @@ export class GraphWriter {
997
462
  }
998
463
  }
999
464
 
1000
- // Collect what each node waits for: The node on the other side of its relation
1001
- // if the node stores its keys, and the nodes it references with `#ref` and
1002
- // `#ref{}`.
1003
- function createDependencies(plan) {
1004
- const { actions } = plan;
1005
- const dependencies = new Map();
1006
-
1007
- const addDependency = (dependent, dependency) => {
1008
- const nodeDependencies = dependencies.get(dependent);
1009
- const entry = { dependent, ...dependency };
1010
-
1011
- if (nodeDependencies) {
1012
- nodeDependencies.push(entry);
1013
- } else {
1014
- dependencies.set(dependent, [entry]);
1015
- }
1016
- };
1017
-
1018
- for (const node of plan.activeNodes) {
1019
- const { parent, relation } = node;
1020
-
1021
- // Models marked with `#unrelate` or `#delete` are never inserted or related,
1022
- // so their keys aren't copied. Ignored models don't replace the current one,
1023
- // so their keys aren't copied to their owners either.
1024
- if (parent && !node.toBeUnrelatedOrDeleted) {
1025
- if (relation.keySide === 'related') {
1026
- addDependency(node, { node: parent, relation, related: node });
1027
- } else if (relation.keySide === 'owner' && actions.get(node) !== 'ignore') {
1028
- addDependency(parent, { node, relation, related: node });
1029
- }
1030
- }
1031
- }
1032
-
1033
- for (const node of plan.activeNodes) {
1034
- if (node.referencedNode) {
1035
- addDependency(node, { node: node.referencedNode });
1036
- }
1037
-
1038
- for (const reference of node.propertyReferences) {
1039
- addDependency(node, { node: reference.referencedNode, reference });
1040
- }
1041
-
1042
- // A missing row that the graph inserts elsewhere is related once it is:
1043
- const insertingNode =
1044
- node.isMissing && actions.get(node) === 'relate' && plan.findInsertingNode(node);
1045
-
1046
- if (insertingNode) {
1047
- addDependency(node, { node: insertingNode });
1048
- }
1049
-
1050
- // A row that goes into the slot of a HasOneRelation waits for the relate
1051
- // that moves the current row out of it, as the foreign key may be unique.
1052
- const moverNode = getSlotMover(plan, node);
1053
-
1054
- if (moverNode) {
1055
- addDependency(node, { node: moverNode, isSlotMove: true });
1056
- }
1057
- }
1058
-
1059
- return dependencies;
1060
- }
1061
-
1062
- // Find the relate that moves the current row in the slot of the HasOneRelation
1063
- // that `node` is inserted or related into to another owner.
1064
- function getSlotMover(plan, node) {
1065
- const { actions } = plan;
1066
- const slotNode = ['insert', 'relate'].includes(actions.get(node)) && getSlotNode(node);
1067
- const keptNode = slotNode && plan.keptNodeForRow(slotNode);
1068
- return keptNode && actions.get(keptNode) === 'relate' ? keptNode : null;
1069
- }
1070
-
1071
- // Fetch the keys that the relations of the inserted nodes need, but that the
1072
- // database generated and the insert didn't return, like a uuid that isn't the
1073
- // id (Vincit#853).
1074
- async function fetchGeneratedKeys(builder, modelClass, nodes) {
1075
- const keyProps = (node) => [
1076
- ...(node.relation && node.relation.keySide !== 'related' ? [node.relation.relatedProp] : []),
1077
- ...node.activeChildren
1078
- .filter((child) => child.relation.keySide !== 'owner')
1079
- .map((child) => child.relation.ownerProp),
1080
- ];
1081
- const missingNodes = nodes.filter((node) =>
1082
- keyProps(node).some((prop) => prop.getProps(node.obj).some((value) => value === undefined)),
1083
- );
1084
-
1085
- if (missingNodes.length > 0 && missingNodes.every((node) => node.obj.$hasId())) {
1086
- // SQL Server returns columns selected twice as arrays, so the ids are
1087
- // part of the set:
1088
- const columns = new Set([
1089
- ...modelClass.getIdColumnArray(),
1090
- ...missingNodes.flatMap((node) => keyProps(node)).flatMap((prop) => prop.cols),
1091
- ]);
1092
- const rows = await modelClass
1093
- .query()
1094
- .childQueryOf(builder, { fork: true, isInternalQuery: true })
1095
- .findByIds(missingNodes.map((node) => node.obj.$id()))
1096
- .select([...columns]);
1097
- const rowsByKey = new Map(rows.map((row) => [row.$idKey(), row]));
1098
-
1099
- for (const node of missingNodes) {
1100
- const row = rowsByKey.get(node.obj.$idKey());
1101
-
1102
- for (const prop of keyProps(node)) {
1103
- prop.forEach((i) => {
1104
- if (prop.getProp(node.obj, i) === undefined) {
1105
- prop.setProp(node.obj, i, prop.getProp(row, i));
1106
- }
1107
- });
1108
- }
1109
- }
1110
- }
1111
- }
1112
-
1113
- // Copy the keys of the relation of `node` to the side that stores them,
1114
- // whichever node was inserted first (#311).
1115
- function resolveKeys(node) {
1116
- const { relation, parent } = node;
1117
-
1118
- // The arrays of keys of owners are written by `_setKeyArrays()`.
1119
- if (relation.keySide === 'related' || hasOwnerKey(relation)) {
1120
- const { ownerProp, relatedProp } = relation;
1121
- const [source, sourceProp, target, targetProp] =
1122
- relation.keySide === 'owner'
1123
- ? [node, relatedProp, parent, ownerProp]
1124
- : [parent, ownerProp, node, relatedProp];
1125
-
1126
- resolveReference(source);
1127
- resolveReference(target);
1128
-
1129
- targetProp.forEach((i) => {
1130
- targetProp.setProp(target.obj, i, sourceProp.getProp(source.obj, i));
1131
- });
1132
- }
1133
- }
1134
-
1135
- // Give a `#ref` model the props of the model it refers to that it doesn't have,
1136
- // and the `filledProps` again, as they may have changed since. Its own keys
1137
- // stay its own, and the join table extras of the place of the model belong to
1138
- // the join row there.
1139
- function resolveReference(node, filledProps = []) {
1140
- const { referencedNode, relation } = node;
1141
-
1142
- if (referencedNode) {
1143
- const ownKeys = relation?.keySide === 'related' ? relation.relatedProp.props : [];
1144
- const modelExtras =
1145
- referencedNode.relation?.keySide === 'join'
1146
- ? referencedNode.relation.joinTableExtras.map((extra) => extra.aliasProp)
1147
- : [];
1148
-
1149
- for (const prop of difference(Object.keys(referencedNode.obj), modelExtras)) {
1150
- if (
1151
- !Object.hasOwn(node.obj, prop) ||
1152
- (filledProps.includes(prop) && !ownKeys.includes(prop))
1153
- ) {
1154
- node.obj[prop] = referencedNode.obj[prop];
1155
- }
1156
- }
1157
- }
1158
- }
1159
-
1160
- // Replace a `#ref{}` in a prop of `node` with the value it refers to. A whole
1161
- // value is replaced even if the relation set the prop already (Vincit#1378).
1162
- function resolvePropertyReference(node, { path, match, refPath, isWholeValue, referencedNode }) {
1163
- const value = get(referencedNode.obj, refPath);
1164
- set(node.obj, path, isWholeValue ? value : get(node.obj, path).replace(match, value));
1165
- }
1166
-
1167
- // Fill in the keys of `node` that the graph leaves out from its row: the keys
1168
- // that its related models read from wherever the row was found, and the key
1169
- // of its parent relation only from its row in the same place. The keys that it
1170
- // holds for the BelongsToOne rows that the graph writes come from the graph.
1171
- function copyKeysFromExisting(node, actions) {
1172
- const { existing, current } = node;
1173
-
1174
- if (existing) {
1175
- const copies = [
1176
- ...(node.parent && current ? [[node.relation.relatedProp, current]] : []),
1177
- ...node.activeChildren
1178
- .filter(
1179
- (child) =>
1180
- !hasOwnerKeyArray(child.relation) &&
1181
- (child.relation.keySide !== 'owner' || actions.get(child) === 'ignore'),
1182
- )
1183
- .map((child) => [child.relation.ownerProp, existing]),
1184
- ];
1185
-
1186
- for (const [prop, source] of copies) {
1187
- prop.forEach((i) => {
1188
- if (prop.getProp(node.obj, i) === undefined) {
1189
- prop.setProp(node.obj, i, prop.getProp(source.obj, i));
1190
- }
1191
- });
1192
- }
1193
- }
1194
- }
1195
-
1196
465
  function hasHook(obj, name) {
1197
466
  const Model = getModel();
1198
467
  return obj.constructor[name] !== Model[name] || obj[`$${name}`] !== Model.prototype[`$${name}`];
@@ -1222,30 +491,6 @@ function createPatchNotFoundError(builder, node) {
1222
491
  });
1223
492
  }
1224
493
 
1225
- // Find the current row in the slot of the HasOneRelation that `node` relates
1226
- // or is inserted into, or null if it's in another relation or an empty slot.
1227
- function getSlotNode(node) {
1228
- const { relation } = node;
1229
- return relation?.keySide === 'related' && relation.isOneToOne()
1230
- ? (node.parent.existing?.activeChildren.find(
1231
- (child) => child.relation.name === relation.name,
1232
- ) ?? null)
1233
- : null;
1234
- }
1235
-
1236
- // Return the key props of `node` on its side of the relation of `dependency`.
1237
- function getKeyProps({ relation, related }, node) {
1238
- return (related === node ? relation.relatedProp : relation.ownerProp).props;
1239
- }
1240
-
1241
- function hasOwnerKey(relation) {
1242
- return relation.keySide === 'owner' && relation.isOneToOne();
1243
- }
1244
-
1245
- function hasOwnerKeyArray(relation) {
1246
- return relation.keySide === 'owner' && !relation.isOneToOne();
1247
- }
1248
-
1249
494
  function getBatchSize(builder) {
1250
495
  return hasBatchInsert(builder.unsafeKnex()) ? MAX_INSERT_BATCH_SIZE : 1;
1251
496
  }