objection 4.0.0-rc.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/CheckViolationError/parser.js +2 -9
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/ForeignKeyViolationError/parser.js +2 -9
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/NotNullViolationError/parser.js +4 -2
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/UniqueViolationError/parser.js +4 -2
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/constraintMessage.js +16 -0
- package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/parser.js +2 -1
- package/lib/dbErrors/parsers/sqlite/DBError/parser.js +2 -1
- package/lib/model/modelJsonAttributes.js +15 -6
- package/lib/objection.js +1 -0
- package/lib/queryBuilder/InternalOptions.js +4 -0
- package/lib/queryBuilder/StaticHookArguments.js +4 -2
- package/lib/queryBuilder/graph/GraphNode.js +8 -0
- package/lib/queryBuilder/graph/GraphPlan.js +13 -0
- package/lib/queryBuilder/graph/GraphSchedule.js +92 -40
- package/lib/queryBuilder/graph/GraphUpsert.js +2 -0
- package/lib/queryBuilder/graph/GraphWriter.js +159 -18
- package/lib/queryBuilder/graph/graphChecks.js +40 -3
- package/lib/queryBuilder/graph/graphKeys.js +146 -61
- package/lib/queryBuilder/jsonDialects/JsonDialect.js +6 -0
- package/lib/queryBuilder/operations/AsArrayOperation.js +10 -0
- package/lib/queryBuilder/operations/UpdateOperation.js +20 -7
- package/lib/queryBuilder/operations/fieldExpressions.js +15 -4
- package/lib/relations/RelationProperty.js +11 -0
- package/package.json +11 -4
- package/typings/objection/index.d.ts +191 -184
package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/CheckViolationError/parser.js
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { CheckViolationError } from '../../../../../errors/CheckViolationError.js';
|
|
2
|
-
|
|
3
|
-
const REGEX = /SQLITE_CONSTRAINT: CHECK constraint failed/;
|
|
2
|
+
import { getConstraintMessage } from '../constraintMessage.js';
|
|
4
3
|
|
|
5
4
|
export const checkViolationErrorParser = {
|
|
6
5
|
error: CheckViolationError,
|
|
7
6
|
|
|
8
7
|
parse: (err) => {
|
|
9
|
-
const match = REGEX.exec(err.message);
|
|
10
|
-
|
|
11
|
-
if (!match) {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
8
|
// No way to extract anything reliably.
|
|
16
|
-
return {};
|
|
9
|
+
return getConstraintMessage(err, 'CHECK') ? {} : null;
|
|
17
10
|
},
|
|
18
11
|
|
|
19
12
|
subclassParsers: [],
|
|
@@ -1,19 +1,12 @@
|
|
|
1
1
|
import { ForeignKeyViolationError } from '../../../../../errors/ForeignKeyViolationError.js';
|
|
2
|
-
|
|
3
|
-
const REGEX = /SQLITE_CONSTRAINT: FOREIGN KEY constraint failed/;
|
|
2
|
+
import { getConstraintMessage } from '../constraintMessage.js';
|
|
4
3
|
|
|
5
4
|
export const foreignKeyViolationErrorParser = {
|
|
6
5
|
error: ForeignKeyViolationError,
|
|
7
6
|
|
|
8
7
|
parse: (err) => {
|
|
9
|
-
const match = REGEX.exec(err.message);
|
|
10
|
-
|
|
11
|
-
if (!match) {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
8
|
// No way to extract anything reliably.
|
|
16
|
-
return {};
|
|
9
|
+
return getConstraintMessage(err, 'FOREIGN KEY') ? {} : null;
|
|
17
10
|
},
|
|
18
11
|
|
|
19
12
|
subclassParsers: [],
|
package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/NotNullViolationError/parser.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { NotNullViolationError } from '../../../../../errors/NotNullViolationError.js';
|
|
2
|
+
import { getConstraintMessage } from '../constraintMessage.js';
|
|
2
3
|
|
|
3
|
-
const NOT_COLUMN_REGEX =
|
|
4
|
+
const NOT_COLUMN_REGEX = /^NOT NULL constraint failed: (.+)$/s;
|
|
4
5
|
|
|
5
6
|
export const notNullViolationErrorParser = {
|
|
6
7
|
error: NotNullViolationError,
|
|
7
8
|
|
|
8
9
|
parse: (err) => {
|
|
9
|
-
const
|
|
10
|
+
const message = getConstraintMessage(err, 'NOT NULL');
|
|
11
|
+
const colMatch = message && NOT_COLUMN_REGEX.exec(message);
|
|
10
12
|
|
|
11
13
|
if (!colMatch) {
|
|
12
14
|
return null;
|
package/lib/dbErrors/parsers/sqlite/DBError/ConstraintViolationError/UniqueViolationError/parser.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { UniqueViolationError } from '../../../../../errors/UniqueViolationError.js';
|
|
2
|
+
import { getConstraintMessage } from '../constraintMessage.js';
|
|
2
3
|
|
|
3
|
-
const UNIQUE_COLUMNS_REGEX =
|
|
4
|
+
const UNIQUE_COLUMNS_REGEX = /^UNIQUE constraint failed: (.+)$/s;
|
|
4
5
|
|
|
5
6
|
export const uniqueViolationErrorParser = {
|
|
6
7
|
error: UniqueViolationError,
|
|
7
8
|
|
|
8
9
|
parse: (err) => {
|
|
9
|
-
const
|
|
10
|
+
const message = getConstraintMessage(err, 'UNIQUE');
|
|
11
|
+
const colsMatch = message && UNIQUE_COLUMNS_REGEX.exec(message);
|
|
10
12
|
|
|
11
13
|
if (!colsMatch) {
|
|
12
14
|
return null;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Return the text of the SQLite error `err` from `<kind> constraint failed` on,
|
|
2
|
+
// e.g. 'UNIQUE constraint failed: t.a', if it's a constraint error of that kind,
|
|
3
|
+
// or else `null`. `better-sqlite3` names the kind in its extended code, e.g.
|
|
4
|
+
// `SQLITE_CONSTRAINT_UNIQUE`, `sqlite3` only after `SQLITE_CONSTRAINT: `. knex
|
|
5
|
+
// puts the SQL with its values in front, so the last match is SQLite's own.
|
|
6
|
+
export function getConstraintMessage(err, kind) {
|
|
7
|
+
const text = `${kind} constraint failed`;
|
|
8
|
+
// A duplicate primary key is a unique violation, with its own code:
|
|
9
|
+
const codes = kind === 'UNIQUE' ? ['UNIQUE', 'PRIMARYKEY'] : [kind.replace(' ', '')];
|
|
10
|
+
const isKind =
|
|
11
|
+
codes.some((code) => err.code === `SQLITE_CONSTRAINT_${code}`) ||
|
|
12
|
+
(err.code === 'SQLITE_CONSTRAINT' &&
|
|
13
|
+
/.*SQLITE_CONSTRAINT: (.*)$/s.exec(err.message)?.[1].startsWith(text));
|
|
14
|
+
|
|
15
|
+
return isKind ? err.message.slice(err.message.lastIndexOf(text)) : null;
|
|
16
|
+
}
|
|
@@ -8,7 +8,8 @@ export const constraintViolationErrorParser = {
|
|
|
8
8
|
error: ConstraintViolationError,
|
|
9
9
|
|
|
10
10
|
parse: (err) => {
|
|
11
|
-
|
|
11
|
+
// `better-sqlite3` uses the extended codes, e.g. `SQLITE_CONSTRAINT_UNIQUE`:
|
|
12
|
+
if (/^SQLITE_CONSTRAINT(_|$)/.test(err.code)) {
|
|
12
13
|
return {};
|
|
13
14
|
} else {
|
|
14
15
|
return null;
|
|
@@ -5,10 +5,11 @@ export const dbErrorParser = {
|
|
|
5
5
|
error: DBError,
|
|
6
6
|
|
|
7
7
|
parse: (err) => {
|
|
8
|
+
// `sqlite3` gives its errors an `errno`, `better-sqlite3` names them:
|
|
8
9
|
if (
|
|
9
10
|
typeof err.code === 'string' &&
|
|
10
11
|
err.code.startsWith('SQLITE_') &&
|
|
11
|
-
typeof err.errno === 'number'
|
|
12
|
+
(typeof err.errno === 'number' || err.name === 'SqliteError')
|
|
12
13
|
) {
|
|
13
14
|
return {
|
|
14
15
|
nativeError: err,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { asArray, isObject, flatten, isString } from '../utils/objectUtils.js';
|
|
2
2
|
import { isQueryProp } from './modelQueryProps.js';
|
|
3
|
-
import { isJsonParsedByDriver } from '../utils/knexUtils.js';
|
|
3
|
+
import { isJsonParsedByDriver, isMsSql, isSqlite } from '../utils/knexUtils.js';
|
|
4
4
|
|
|
5
5
|
export function parseJsonAttributes(json, modelClass, knex) {
|
|
6
6
|
const jsonAttr = modelClass.getJsonAttributes();
|
|
7
7
|
const format = getJsonColumnFormat(modelClass, knex);
|
|
8
8
|
|
|
9
|
-
if (jsonAttr.length && format !== '
|
|
9
|
+
if (jsonAttr.length && format !== 'json') {
|
|
10
10
|
// JSON attributes may be returned as strings depending on the database and
|
|
11
11
|
// the database client. Convert them to objects here.
|
|
12
12
|
for (let i = 0, l = jsonAttr.length; i < l; ++i) {
|
|
@@ -85,14 +85,23 @@ function getPropertyTypes(prop) {
|
|
|
85
85
|
return types;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Resolve 'auto'
|
|
89
|
-
//
|
|
88
|
+
// Resolve 'auto' by the driver: the PostgreSQL drivers return JSON columns
|
|
89
|
+
// parsed, SQLite and SQL Server return JSON text, and MySQL's drivers and others
|
|
90
|
+
// may return JSON or plain text, e.g. from MariaDB, which stores JSON as text,
|
|
91
|
+
// and which knex can't tell apart from MySQL. Without a driver, rows passed by
|
|
92
|
+
// hand may hold either.
|
|
90
93
|
function getJsonColumnFormat(modelClass, knex) {
|
|
91
94
|
const { jsonColumnFormat } = modelClass;
|
|
92
95
|
|
|
93
96
|
if (jsonColumnFormat === 'auto') {
|
|
94
|
-
return knex
|
|
95
|
-
|
|
97
|
+
return !knex
|
|
98
|
+
? 'mixed'
|
|
99
|
+
: isJsonParsedByDriver(knex)
|
|
100
|
+
? 'json'
|
|
101
|
+
: isSqlite(knex) || isMsSql(knex)
|
|
102
|
+
? 'text'
|
|
103
|
+
: 'mixed';
|
|
104
|
+
} else if (['json', 'text', 'mixed'].includes(jsonColumnFormat)) {
|
|
96
105
|
return jsonColumnFormat;
|
|
97
106
|
} else {
|
|
98
107
|
throw new Error(
|
package/lib/objection.js
CHANGED
|
@@ -12,6 +12,7 @@ export {
|
|
|
12
12
|
ConstraintViolationError,
|
|
13
13
|
CheckViolationError,
|
|
14
14
|
DataError,
|
|
15
|
+
wrapError,
|
|
15
16
|
} from './dbErrors/index.js';
|
|
16
17
|
export { QueryBuilderBase } from './queryBuilder/QueryBuilderBase.js';
|
|
17
18
|
export { QueryBuilderOperation } from './queryBuilder/operations/QueryBuilderOperation.js';
|
|
@@ -6,6 +6,9 @@ export class InternalOptions {
|
|
|
6
6
|
this.skipRelationFilter = false;
|
|
7
7
|
// The join table extras that related updates write, all of them if null:
|
|
8
8
|
this.joinTableExtras = null;
|
|
9
|
+
// The path updates that `upsertGraph()` writes the keys in json columns of
|
|
10
|
+
// updated rows with, by column:
|
|
11
|
+
this.jsonKeyPatches = null;
|
|
9
12
|
this.debug = false;
|
|
10
13
|
this.schema = undefined;
|
|
11
14
|
}
|
|
@@ -18,6 +21,7 @@ export class InternalOptions {
|
|
|
18
21
|
copy.isInternalQuery = this.isInternalQuery;
|
|
19
22
|
copy.skipRelationFilter = this.skipRelationFilter;
|
|
20
23
|
copy.joinTableExtras = this.joinTableExtras;
|
|
24
|
+
copy.jsonKeyPatches = this.jsonKeyPatches;
|
|
21
25
|
copy.debug = this.debug;
|
|
22
26
|
copy.schema = this.schema;
|
|
23
27
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { asArray, isObject } from '../utils/objectUtils.js';
|
|
2
|
+
import { AsArrayOperation } from './operations/AsArrayOperation.js';
|
|
2
3
|
|
|
3
4
|
const BUILDER_SYMBOL = Symbol();
|
|
4
5
|
|
|
@@ -26,12 +27,13 @@ export class StaticHookArguments {
|
|
|
26
27
|
// ones installed by `throwIfNotFound()` and `traverse()`) are meant for
|
|
27
28
|
// its result, not for the result of this find query. The same goes for
|
|
28
29
|
// the value it was resolved with, e.g. the 0 of an update that only
|
|
29
|
-
// changes the join rows of a many-to-many relation.
|
|
30
|
+
// changes the join rows of a many-to-many relation. The query resolves to
|
|
31
|
+
// an array also for single-result writes, unless the hook adds `first()`:
|
|
30
32
|
return this[BUILDER_SYMBOL].toFindQuery()
|
|
31
33
|
.clear('runAfter')
|
|
32
34
|
.clearResolve()
|
|
33
35
|
.clearWithGraphFetched()
|
|
34
|
-
.
|
|
36
|
+
.addOperation(new AsArrayOperation('asArray'), []);
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -191,6 +191,14 @@ export class GraphNode {
|
|
|
191
191
|
|
|
192
192
|
for (const node of nodes) {
|
|
193
193
|
if (node.isReference) {
|
|
194
|
+
// A `#ref` places its model in a relation, which a root doesn't have:
|
|
195
|
+
if (!node.parent) {
|
|
196
|
+
throw node.modelClass.createValidationError({
|
|
197
|
+
type: ValidationErrorType.InvalidGraph,
|
|
198
|
+
message: `the reference at ${formatPath(node.dataPathKey)} has no relation to place its model in`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
194
202
|
node.referencedNode = getNode(node.reference);
|
|
195
203
|
}
|
|
196
204
|
|
|
@@ -91,6 +91,19 @@ export class GraphPlan {
|
|
|
91
91
|
);
|
|
92
92
|
}
|
|
93
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
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
94
107
|
// Return true if the model of the `#ref` model `node` writes what `node`
|
|
95
108
|
// would write to the row: The model is inserted, or it's written and its path
|
|
96
109
|
// allows any write that the path of `node` allows.
|
|
@@ -1,17 +1,23 @@
|
|
|
1
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 or the values that
|
|
19
|
+
// their `#ref{}` copies, as a foreign key may reference them (#428, #491).
|
|
20
|
+
// Copies of values that are no keys wait unless that closes a cycle.
|
|
15
21
|
// 4. Patch the changed rows, the other relates, and the keys of owners whose
|
|
16
22
|
// BelongsToOne rows were inserted, related or removed.
|
|
17
23
|
// 5. Unrelate the rows that their own to-many relation on their own key leaves
|
|
@@ -25,7 +31,9 @@ export class GraphSchedule {
|
|
|
25
31
|
this.plan = plan;
|
|
26
32
|
this.keys = keys;
|
|
27
33
|
// The nodes that each node waits for, through its keys, its references and
|
|
28
|
-
// the rows that move out of the slots that it goes into
|
|
34
|
+
// the rows that move out of the slots that it goes into. `isKeyChange`
|
|
35
|
+
// marks the patches and relates that change the keys it reads or the values
|
|
36
|
+
// that its `#ref{}` copies, and `isCopy` the latter:
|
|
29
37
|
this.dependencies = createDependencies(plan, this.keys);
|
|
30
38
|
// Dependencies that aren't waited for, to break cycles. Deferred keys are
|
|
31
39
|
// written after the inserts:
|
|
@@ -36,9 +44,8 @@ export class GraphSchedule {
|
|
|
36
44
|
? []
|
|
37
45
|
: plan.activeNodes.filter((node) => this._isMovedFirst(node)),
|
|
38
46
|
);
|
|
39
|
-
// Relates
|
|
40
|
-
|
|
41
|
-
this.slotMovedNodes = this._getSlotMovedNodes();
|
|
47
|
+
// Relates and patches that aren't moved first but written in the inserts:
|
|
48
|
+
this.earlyNodes = this._getEarlyNodes();
|
|
42
49
|
this.steps = this._planSteps();
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -91,10 +98,9 @@ export class GraphSchedule {
|
|
|
91
98
|
activeNodes.filter((node) => {
|
|
92
99
|
const action = actions.get(node);
|
|
93
100
|
return (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
!this.slotMovedNodes.has(node))) &&
|
|
101
|
+
['patch', 'relate'].includes(action) &&
|
|
102
|
+
!this.movedNodes.has(node) &&
|
|
103
|
+
!this.earlyNodes.has(node) &&
|
|
98
104
|
// The row of a `#ref` model is written once, by its model:
|
|
99
105
|
!this._isWrittenByModel(node)
|
|
100
106
|
);
|
|
@@ -140,6 +146,7 @@ export class GraphSchedule {
|
|
|
140
146
|
!handledNodes.has(dependency.node) &&
|
|
141
147
|
(dependency.isSlotMove ||
|
|
142
148
|
dependency.isKeyWait ||
|
|
149
|
+
dependency.isKeyChange ||
|
|
143
150
|
!hasRow(dependency.node) ||
|
|
144
151
|
this.readsFilledProps(dependency)),
|
|
145
152
|
);
|
|
@@ -160,12 +167,14 @@ export class GraphSchedule {
|
|
|
160
167
|
|
|
161
168
|
// Break a cycle by deferring one BelongsToOne key that points to a `#ref`
|
|
162
169
|
// model: Its owner is inserted without the key, which is patched in once
|
|
163
|
-
// the referenced model is inserted. Without `#ref
|
|
164
|
-
// and only
|
|
165
|
-
//
|
|
170
|
+
// the referenced model is inserted. Without `#ref` models, the graph is a
|
|
171
|
+
// tree, and only `#ref{}` copies of changed values and the waits for the
|
|
172
|
+
// rows that move out of slots can close a cycle, like rows that swap slots.
|
|
173
|
+
// One of these isn't waited for then, the copies first.
|
|
166
174
|
const breakCycle = () => {
|
|
167
175
|
for (const canDefer of [
|
|
168
176
|
(dependency, node) => this._canDefer(dependency, node),
|
|
177
|
+
(dependency) => dependency.isCopy,
|
|
169
178
|
(dependency) => dependency.isSlotMove,
|
|
170
179
|
]) {
|
|
171
180
|
for (const node of nodes) {
|
|
@@ -212,7 +221,25 @@ export class GraphSchedule {
|
|
|
212
221
|
}
|
|
213
222
|
}
|
|
214
223
|
} else if (!breakCycle()) {
|
|
215
|
-
|
|
224
|
+
// A patch that changes a key that the rows it waits for read can be
|
|
225
|
+
// written neither before nor after them:
|
|
226
|
+
const { node: changingNode } =
|
|
227
|
+
nodes
|
|
228
|
+
.filter((node) => !handledNodes.has(node))
|
|
229
|
+
.flatMap(getPendingDependencies)
|
|
230
|
+
.find(
|
|
231
|
+
({ node, dependent, isKeyChange }) => isKeyChange && isBlockedBy(node, dependent),
|
|
232
|
+
) || {};
|
|
233
|
+
|
|
234
|
+
if (changingNode) {
|
|
235
|
+
const { modelClass, dataPathKey } = changingNode;
|
|
236
|
+
throw modelClass.createValidationError({
|
|
237
|
+
type: ValidationErrorType.InvalidGraph,
|
|
238
|
+
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`,
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
throw new Error('the object graph contains cyclic references');
|
|
242
|
+
}
|
|
216
243
|
}
|
|
217
244
|
}
|
|
218
245
|
|
|
@@ -245,9 +272,10 @@ export class GraphSchedule {
|
|
|
245
272
|
|
|
246
273
|
// Return true if the HasMany or HasOne relate of `node` moves its row to an
|
|
247
274
|
// existing owner before the removals, see the steps above. `#ref` models may
|
|
248
|
-
// get their ids from the inserts, rows that wait for the inserts
|
|
249
|
-
//
|
|
250
|
-
// the removal of that row, or its move, as its key may be
|
|
275
|
+
// get their ids from the inserts, rows that wait for the inserts or for
|
|
276
|
+
// changed keys are written after them, and a relate into the slot of a HasOne
|
|
277
|
+
// row waits for the removal of that row, or its move, as its key may be
|
|
278
|
+
// unique.
|
|
251
279
|
_isMovedFirst(node, visitedNodes = new Set()) {
|
|
252
280
|
const { actions } = this.plan;
|
|
253
281
|
visitedNodes.add(node);
|
|
@@ -257,7 +285,7 @@ export class GraphSchedule {
|
|
|
257
285
|
!node.isReference &&
|
|
258
286
|
node.relation.keySide === 'related' &&
|
|
259
287
|
node.parent.existing &&
|
|
260
|
-
!this.
|
|
288
|
+
!this._waitsForWrites(node)
|
|
261
289
|
) {
|
|
262
290
|
const slotNode = getSlotNode(node);
|
|
263
291
|
const moverNode = getSlotMover(this.plan, node);
|
|
@@ -271,24 +299,32 @@ export class GraphSchedule {
|
|
|
271
299
|
}
|
|
272
300
|
}
|
|
273
301
|
|
|
274
|
-
// Find the relates
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
// as the foreign key may be unique
|
|
278
|
-
|
|
302
|
+
// Find the relates and patches that are written in the inserts, as the rows
|
|
303
|
+
// written there wait for them: The relates that move the rows out of the
|
|
304
|
+
// slots of HasOneRelations that new rows go into, along with the relates that
|
|
305
|
+
// move the rows out of their own slots, as the foreign key may be unique, and
|
|
306
|
+
// the patches and relates that change the keys that these rows or join rows
|
|
307
|
+
// read, as a foreign key may reference them (#428).
|
|
308
|
+
_getEarlyNodes() {
|
|
309
|
+
const { actions } = this.plan;
|
|
279
310
|
const nodes = new Set();
|
|
280
|
-
const
|
|
281
|
-
for (const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
311
|
+
const addEarlyNodes = (node) => {
|
|
312
|
+
for (const dependency of this.dependencies.get(node) || []) {
|
|
313
|
+
const { node: earlyNode, isSlotMove, isKeyChange } = dependency;
|
|
314
|
+
if (
|
|
315
|
+
(isSlotMove || isKeyChange) &&
|
|
316
|
+
!this.movedNodes.has(earlyNode) &&
|
|
317
|
+
!nodes.has(earlyNode)
|
|
318
|
+
) {
|
|
319
|
+
nodes.add(earlyNode);
|
|
320
|
+
addEarlyNodes(earlyNode);
|
|
285
321
|
}
|
|
286
322
|
}
|
|
287
323
|
};
|
|
288
324
|
|
|
289
325
|
for (const node of this.plan.activeNodes) {
|
|
290
|
-
if (
|
|
291
|
-
|
|
326
|
+
if (actions.get(node) === 'insert' || (node.relation?.keySide === 'join' && !node.current)) {
|
|
327
|
+
addEarlyNodes(node);
|
|
292
328
|
}
|
|
293
329
|
}
|
|
294
330
|
|
|
@@ -317,19 +353,21 @@ export class GraphSchedule {
|
|
|
317
353
|
}
|
|
318
354
|
|
|
319
355
|
// Return true if `node` waits for the keys or references of inserted models,
|
|
320
|
-
//
|
|
321
|
-
// that it waits for to get its
|
|
322
|
-
|
|
356
|
+
// or for keys that patches change (#428), also through the models that it
|
|
357
|
+
// references (#356), and through the models that it waits for to get its
|
|
358
|
+
// keys.
|
|
359
|
+
_waitsForWrites(node, visitedNodes = new Set()) {
|
|
323
360
|
const { actions } = this.plan;
|
|
324
361
|
visitedNodes.add(node);
|
|
325
362
|
return (this.dependencies.get(node) || []).some((dependency) => {
|
|
326
|
-
const { node: dependencyNode, relation } = dependency;
|
|
363
|
+
const { node: dependencyNode, relation, isKeyWait, isKeyChange } = dependency;
|
|
327
364
|
const referencedNode = dependencyNode.referencedNode || dependencyNode;
|
|
328
365
|
return (
|
|
329
366
|
actions.get(referencedNode) === 'insert' ||
|
|
330
|
-
|
|
367
|
+
isKeyChange ||
|
|
368
|
+
((!relation || isKeyWait) &&
|
|
331
369
|
!visitedNodes.has(referencedNode) &&
|
|
332
|
-
this.
|
|
370
|
+
this._waitsForWrites(referencedNode, visitedNodes))
|
|
333
371
|
);
|
|
334
372
|
});
|
|
335
373
|
}
|
|
@@ -458,18 +496,32 @@ function createDependencies(plan, keys) {
|
|
|
458
496
|
|
|
459
497
|
// A place waits for the inserts and `#ref{}` values that bind the slots of
|
|
460
498
|
// its keys. A dependency on the same row through the same key becomes such
|
|
461
|
-
// a wait, and stays deferrable to break cycles. The other waits aren't
|
|
462
|
-
for
|
|
499
|
+
// a wait, and stays deferrable to break cycles. The other waits aren't. A
|
|
500
|
+
// key change is waited for at the node that writes it, not at a `#ref`
|
|
501
|
+
// model of its row, which writes nothing:
|
|
502
|
+
for (const { node: waitedNode, prop, isKeyChange } of keys.waitsFor(node)) {
|
|
463
503
|
const dependency = (dependencies.get(node) || []).find(
|
|
464
504
|
(dependency) =>
|
|
465
|
-
|
|
505
|
+
(isKeyChange
|
|
506
|
+
? dependency.node === waitedNode
|
|
507
|
+
: getRow(dependency.node) === getRow(waitedNode)) &&
|
|
466
508
|
(!dependency.relation || getKeyProps(dependency, node).includes(prop)),
|
|
467
509
|
);
|
|
468
510
|
|
|
469
511
|
if (dependency) {
|
|
470
512
|
dependency.isKeyWait = true;
|
|
513
|
+
dependency.isKeyChange ||= isKeyChange;
|
|
471
514
|
} else {
|
|
472
|
-
addDependency(node, { node: waitedNode, isKeyWait: true });
|
|
515
|
+
addDependency(node, { node: waitedNode, isKeyWait: true, isKeyChange });
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// A `#ref{}` that copies a value that the graph changes waits for the
|
|
520
|
+
// change, as a foreign key may reference it, like the keys (#491). As the
|
|
521
|
+
// copied value may be no key, the wait is deferred first to break cycles:
|
|
522
|
+
for (const reference of node.propertyReferences) {
|
|
523
|
+
for (const changingNode of keys.getChangingNodes(node, reference)) {
|
|
524
|
+
addDependency(node, { node: changingNode, isKeyChange: true, isCopy: true });
|
|
473
525
|
}
|
|
474
526
|
}
|
|
475
527
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
checkChangedIds,
|
|
10
10
|
checkGraph,
|
|
11
11
|
checkJoinTables,
|
|
12
|
+
checkKeyRelates,
|
|
12
13
|
checkMissingModels,
|
|
13
14
|
checkPlacements,
|
|
14
15
|
checkReadKeys,
|
|
@@ -100,6 +101,7 @@ export class GraphUpsert {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
checkChangedIds(builder, plan);
|
|
104
|
+
checkKeyRelates(builder, plan);
|
|
103
105
|
checkReferences(builder, plan);
|
|
104
106
|
checkJoinTables(builder, plan);
|
|
105
107
|
await new GraphWriter(new GraphSchedule(plan, keys)).write(builder);
|