objection 4.0.0-rc.4 → 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 -4
- package/lib/queryBuilder/StaticHookArguments.js +4 -2
- package/lib/queryBuilder/graph/GraphSchedule.js +20 -6
- package/lib/queryBuilder/graph/GraphWriter.js +92 -35
- package/lib/queryBuilder/graph/graphKeys.js +78 -59
- package/lib/queryBuilder/operations/AsArrayOperation.js +10 -0
- package/lib/queryBuilder/operations/UpdateOperation.js +20 -10
- package/lib/queryBuilder/operations/fieldExpressions.js +14 -6
- package/lib/relations/RelationProperty.js +5 -0
- package/package.json +9 -3
- package/typings/objection/index.d.ts +100 -94
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,9 +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
|
|
10
|
-
// updated rows with:
|
|
11
|
-
this.
|
|
9
|
+
// The path updates that `upsertGraph()` writes the keys in json columns of
|
|
10
|
+
// updated rows with, by column:
|
|
11
|
+
this.jsonKeyPatches = null;
|
|
12
12
|
this.debug = false;
|
|
13
13
|
this.schema = undefined;
|
|
14
14
|
}
|
|
@@ -21,7 +21,7 @@ export class InternalOptions {
|
|
|
21
21
|
copy.isInternalQuery = this.isInternalQuery;
|
|
22
22
|
copy.skipRelationFilter = this.skipRelationFilter;
|
|
23
23
|
copy.joinTableExtras = this.joinTableExtras;
|
|
24
|
-
copy.
|
|
24
|
+
copy.jsonKeyPatches = this.jsonKeyPatches;
|
|
25
25
|
copy.debug = this.debug;
|
|
26
26
|
copy.schema = this.schema;
|
|
27
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
|
|
|
@@ -15,8 +15,9 @@ import { formatPath } from './GraphNode.js';
|
|
|
15
15
|
// keys deferred to break cycles and the join rows. Rows that move out of
|
|
16
16
|
// the slots of HasOneRelations that new rows go into are related in the
|
|
17
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
|
|
19
|
-
// reference them (#428).
|
|
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.
|
|
20
21
|
// 4. Patch the changed rows, the other relates, and the keys of owners whose
|
|
21
22
|
// BelongsToOne rows were inserted, related or removed.
|
|
22
23
|
// 5. Unrelate the rows that their own to-many relation on their own key leaves
|
|
@@ -31,7 +32,8 @@ export class GraphSchedule {
|
|
|
31
32
|
this.keys = keys;
|
|
32
33
|
// The nodes that each node waits for, through its keys, its references and
|
|
33
34
|
// the rows that move out of the slots that it goes into. `isKeyChange`
|
|
34
|
-
// marks the patches and relates that change the keys it reads
|
|
35
|
+
// marks the patches and relates that change the keys it reads or the values
|
|
36
|
+
// that its `#ref{}` copies, and `isCopy` the latter:
|
|
35
37
|
this.dependencies = createDependencies(plan, this.keys);
|
|
36
38
|
// Dependencies that aren't waited for, to break cycles. Deferred keys are
|
|
37
39
|
// written after the inserts:
|
|
@@ -144,6 +146,7 @@ export class GraphSchedule {
|
|
|
144
146
|
!handledNodes.has(dependency.node) &&
|
|
145
147
|
(dependency.isSlotMove ||
|
|
146
148
|
dependency.isKeyWait ||
|
|
149
|
+
dependency.isKeyChange ||
|
|
147
150
|
!hasRow(dependency.node) ||
|
|
148
151
|
this.readsFilledProps(dependency)),
|
|
149
152
|
);
|
|
@@ -164,12 +167,14 @@ export class GraphSchedule {
|
|
|
164
167
|
|
|
165
168
|
// Break a cycle by deferring one BelongsToOne key that points to a `#ref`
|
|
166
169
|
// model: Its owner is inserted without the key, which is patched in once
|
|
167
|
-
// the referenced model is inserted. Without `#ref
|
|
168
|
-
// and only
|
|
169
|
-
//
|
|
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.
|
|
170
174
|
const breakCycle = () => {
|
|
171
175
|
for (const canDefer of [
|
|
172
176
|
(dependency, node) => this._canDefer(dependency, node),
|
|
177
|
+
(dependency) => dependency.isCopy,
|
|
173
178
|
(dependency) => dependency.isSlotMove,
|
|
174
179
|
]) {
|
|
175
180
|
for (const node of nodes) {
|
|
@@ -511,6 +516,15 @@ function createDependencies(plan, keys) {
|
|
|
511
516
|
}
|
|
512
517
|
}
|
|
513
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 });
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
514
528
|
// A row that goes into the slot of a HasOneRelation waits for the relate
|
|
515
529
|
// that moves the current row out of it, as the foreign key may be unique.
|
|
516
530
|
const moverNode = getSlotMover(plan, node);
|
|
@@ -2,7 +2,7 @@ import { getModel } from '../../model/getModel.js';
|
|
|
2
2
|
import { restoreOmitFromDatabaseJson } from '../../model/modelUtils.js';
|
|
3
3
|
import { valuesKey } from '../../model/modelValues.js';
|
|
4
4
|
import { UpdateOperation } from '../operations/UpdateOperation.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getJsonKeyPatch } from '../operations/fieldExpressions.js';
|
|
6
6
|
import { getMaxRows, hasBatchInsert } from '../../utils/knexUtils.js';
|
|
7
7
|
import { isInternalProp } from '../../utils/internalPropUtils.js';
|
|
8
8
|
import {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
difference,
|
|
11
11
|
get,
|
|
12
12
|
groupBy,
|
|
13
|
+
isEmpty,
|
|
13
14
|
isObject,
|
|
14
15
|
jsonEquals,
|
|
15
16
|
nonStrictEquals,
|
|
@@ -26,6 +27,10 @@ export class GraphWriter {
|
|
|
26
27
|
constructor(schedule) {
|
|
27
28
|
this.schedule = schedule;
|
|
28
29
|
this.plan = schedule.plan;
|
|
30
|
+
// The rows that are written, as the nodes of their models, and whether the
|
|
31
|
+
// key columns of relations may be null:
|
|
32
|
+
this.writtenRows = new Set();
|
|
33
|
+
this.nullableProps = new Map();
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
async write(builder) {
|
|
@@ -154,6 +159,7 @@ export class GraphWriter {
|
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
const insertNodes = nodes.filter((node) => actions.get(node) === 'insert');
|
|
162
|
+
await this._clearSlots(builder, insertNodes);
|
|
157
163
|
|
|
158
164
|
if (insertNodes.length > 0) {
|
|
159
165
|
const [{ modelClass }] = insertNodes;
|
|
@@ -179,6 +185,10 @@ export class GraphWriter {
|
|
|
179
185
|
await fetchGeneratedKeys(builder, modelClass, insertNodes);
|
|
180
186
|
this.schedule.keys.setInserted(insertNodes);
|
|
181
187
|
}
|
|
188
|
+
|
|
189
|
+
for (const node of insertNodes) {
|
|
190
|
+
this.writtenRows.add(node);
|
|
191
|
+
}
|
|
182
192
|
}
|
|
183
193
|
|
|
184
194
|
_writeDeferredKeys(builder, dependencies) {
|
|
@@ -253,7 +263,60 @@ export class GraphWriter {
|
|
|
253
263
|
});
|
|
254
264
|
}
|
|
255
265
|
|
|
266
|
+
// Rows that swap slots in a cycle can't take them while the other rows still
|
|
267
|
+
// hold their keys, if they're unique. Under `unrelate`, the rows that leave
|
|
268
|
+
// the slots clear their keys first, as 3.x does, if they may be null, and
|
|
269
|
+
// take their new keys with their own writes, once the other rows took their
|
|
270
|
+
// slots (#489). Keys that can't be null are swapped directly, and so are keys
|
|
271
|
+
// in json columns, which hold other values:
|
|
272
|
+
async _clearSlots(builder, nodes) {
|
|
273
|
+
for (const { dependent, node, isSlotMove } of this.schedule.deferredDependencies) {
|
|
274
|
+
// The row that moves out of the slot, with its key:
|
|
275
|
+
const { modelClass } = node;
|
|
276
|
+
const relatedProp = isSlotMove && node.relation.relatedProp;
|
|
277
|
+
if (
|
|
278
|
+
relatedProp &&
|
|
279
|
+
nodes.includes(dependent) &&
|
|
280
|
+
!this.writtenRows.has(node.referencedNode || node) &&
|
|
281
|
+
relatedProp.props.every((_, i) => relatedProp.isColumn(i)) &&
|
|
282
|
+
this.plan.graphOptions.has(node, 'unrelate') &&
|
|
283
|
+
(await this._isNullable(builder, relatedProp))
|
|
284
|
+
) {
|
|
285
|
+
// Without validation or hooks, as the row's own write follows:
|
|
286
|
+
const { schema } = builder.internalOptions();
|
|
287
|
+
const query = builder.unsafeKnex()(modelClass.getTableName());
|
|
288
|
+
const idColumns = modelClass.getIdColumnArray();
|
|
289
|
+
const ids = modelClass.getIdRelationProperty().getProps(node.obj);
|
|
290
|
+
await (schema ? query.withSchema(schema) : query)
|
|
291
|
+
.where(Object.fromEntries(idColumns.map((column, i) => [column, ids[i]])))
|
|
292
|
+
.update(Object.fromEntries(relatedProp.cols.map((column) => [column, null])));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Return true if the database allows null in the columns of `prop`:
|
|
298
|
+
_isNullable(builder, prop) {
|
|
299
|
+
let isNullable = this.nullableProps.get(prop);
|
|
300
|
+
|
|
301
|
+
if (!isNullable) {
|
|
302
|
+
isNullable = prop.modelClass
|
|
303
|
+
.query()
|
|
304
|
+
.childQueryOf(builder)
|
|
305
|
+
.columnInfo()
|
|
306
|
+
.then((columnInfo) => prop.cols.every((column) => columnInfo[column]?.nullable));
|
|
307
|
+
this.nullableProps.set(prop, isNullable);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return isNullable;
|
|
311
|
+
}
|
|
312
|
+
|
|
256
313
|
async _patch(builder, nodes) {
|
|
314
|
+
await this._clearSlots(builder, nodes);
|
|
315
|
+
|
|
316
|
+
for (const node of nodes) {
|
|
317
|
+
this.writtenRows.add(node.referencedNode || node);
|
|
318
|
+
}
|
|
319
|
+
|
|
257
320
|
// The patches leave out the props that don't change, but the models that
|
|
258
321
|
// the graph returns get them back once the step is written (#371):
|
|
259
322
|
const omittedProps = new Map(nodes.map((node) => [node, node.obj.$omitFromDatabaseJson()]));
|
|
@@ -277,8 +340,6 @@ export class GraphWriter {
|
|
|
277
340
|
} finally {
|
|
278
341
|
for (const [node, props] of omittedProps) {
|
|
279
342
|
restoreOmitFromDatabaseJson(node.obj, props);
|
|
280
|
-
// The keys of `setKeyPaths()` stay where a hook resolves the patch:
|
|
281
|
-
removeFieldExpressions(node.modelClass, node.obj);
|
|
282
343
|
}
|
|
283
344
|
}
|
|
284
345
|
}
|
|
@@ -314,24 +375,27 @@ export class GraphWriter {
|
|
|
314
375
|
? node.relation.relatedProp.props
|
|
315
376
|
: []),
|
|
316
377
|
];
|
|
317
|
-
// The keys in json
|
|
318
|
-
|
|
378
|
+
// The path updates of the keys in json columns that the write sets or
|
|
379
|
+
// clears, by column:
|
|
380
|
+
let jsonKeyPatches = new Map();
|
|
319
381
|
// Changing the id never makes sense with `upsertGraph()`. A many-to-many
|
|
320
382
|
// `#ref` model whose model writes its row only writes its join row:
|
|
321
383
|
const idProps = node.modelClass.getIdPropertyArray();
|
|
322
384
|
const isJoinRowOnly =
|
|
323
385
|
!!referencedNode && node.relation.keySide === 'join' && this.plan.isCoveredByModel(node);
|
|
324
|
-
const isWritable = (prop) =>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
386
|
+
const isWritable = (prop) => {
|
|
387
|
+
// A json column that its key paths write is unchanged if they are:
|
|
388
|
+
const jsonKeyPatch = getJsonKeyPatch(node.obj, prop, jsonKeyPatches);
|
|
389
|
+
return (
|
|
390
|
+
!idProps.includes(prop) &&
|
|
391
|
+
!(jsonKeyPatch && isEmpty(jsonKeyPatch)) &&
|
|
392
|
+
!this.schedule.keys.isOnlyRead(node, prop) &&
|
|
393
|
+
(!isJoinRowOnly || node.relation.joinTableExtras.some((extra) => extra.aliasProp === prop))
|
|
394
|
+
);
|
|
395
|
+
};
|
|
329
396
|
|
|
330
397
|
const getPropsToUpdate = (changedProps) =>
|
|
331
|
-
(shouldPatch || shouldUpdate
|
|
332
|
-
? changedProps
|
|
333
|
-
: keyProps.flatMap((prop) => pathKeys.get(prop) || prop)
|
|
334
|
-
).filter(isWritable);
|
|
398
|
+
(shouldPatch || shouldUpdate ? changedProps : keyProps).filter(isWritable);
|
|
335
399
|
|
|
336
400
|
// Leave out the props that don't change, and return true if any do.
|
|
337
401
|
const handleUpdate = ({ changedProps, unchangedProps } = this._findChanges(node)) => {
|
|
@@ -353,7 +417,7 @@ export class GraphWriter {
|
|
|
353
417
|
// relates are no updates (#2233, #2605).
|
|
354
418
|
const hasBeforeUpdate = hasHook(node.obj, 'beforeUpdate');
|
|
355
419
|
|
|
356
|
-
|
|
420
|
+
const changes = this._findChanges(node);
|
|
357
421
|
|
|
358
422
|
if (getPropsToUpdate(changes.changedProps).length === 0) {
|
|
359
423
|
return null;
|
|
@@ -365,9 +429,8 @@ export class GraphWriter {
|
|
|
365
429
|
});
|
|
366
430
|
|
|
367
431
|
// Write the keys in json columns as path updates where their column holds
|
|
368
|
-
// nothing else, which keeps its other values (#415)
|
|
369
|
-
|
|
370
|
-
pathKeys = setKeyPaths(node, [
|
|
432
|
+
// nothing else, which keeps its other values (#415):
|
|
433
|
+
jsonKeyPatches = getJsonKeyPatches(node, [
|
|
371
434
|
...this.schedule.keys.getSetCells(node),
|
|
372
435
|
...this.plan
|
|
373
436
|
.getRemovedOwnerRelations(node)
|
|
@@ -376,10 +439,6 @@ export class GraphWriter {
|
|
|
376
439
|
),
|
|
377
440
|
]);
|
|
378
441
|
|
|
379
|
-
if (pathKeys.size > 0) {
|
|
380
|
-
changes = this._findChanges(node);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
442
|
const propsToUpdate = getPropsToUpdate(changes.changedProps);
|
|
384
443
|
|
|
385
444
|
if (propsToUpdate.length === 0) {
|
|
@@ -402,7 +461,7 @@ export class GraphWriter {
|
|
|
402
461
|
hasBeforeUpdate,
|
|
403
462
|
handleUpdate,
|
|
404
463
|
propsToUpdate,
|
|
405
|
-
|
|
464
|
+
jsonKeyPatches,
|
|
406
465
|
isPatchedThroughRelation,
|
|
407
466
|
};
|
|
408
467
|
}
|
|
@@ -415,7 +474,7 @@ export class GraphWriter {
|
|
|
415
474
|
hasBeforeUpdate,
|
|
416
475
|
handleUpdate,
|
|
417
476
|
propsToUpdate,
|
|
418
|
-
|
|
477
|
+
jsonKeyPatches,
|
|
419
478
|
isPatchedThroughRelation,
|
|
420
479
|
},
|
|
421
480
|
) {
|
|
@@ -439,8 +498,7 @@ export class GraphWriter {
|
|
|
439
498
|
// Join rows are patched with the extras among the props to update. The
|
|
440
499
|
// extras of new join rows are written with them, see `_findChanges()`:
|
|
441
500
|
joinTableExtras: node.relation?.keySide === 'join' && !node.current ? [] : propsToUpdate,
|
|
442
|
-
|
|
443
|
-
fieldExpressionKeys,
|
|
501
|
+
jsonKeyPatches,
|
|
444
502
|
})
|
|
445
503
|
.copyFrom(builder, isReturningAll);
|
|
446
504
|
|
|
@@ -796,12 +854,12 @@ async function fetchGeneratedKeys(builder, modelClass, nodes) {
|
|
|
796
854
|
}
|
|
797
855
|
}
|
|
798
856
|
|
|
799
|
-
//
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
// are
|
|
803
|
-
function
|
|
804
|
-
const
|
|
857
|
+
// Return the path updates of the changed keys of `cells` at the top level of
|
|
858
|
+
// json columns, by the columns that hold nothing but these keys. PostgreSQL and
|
|
859
|
+
// MySQL can't write nested paths whose parents are missing, so their columns
|
|
860
|
+
// are written whole.
|
|
861
|
+
function getJsonKeyPatches(node, cells) {
|
|
862
|
+
const patchesByColumn = new Map();
|
|
805
863
|
const pathCells = cells.filter(({ prop, index }) => prop.isJsonKey(index));
|
|
806
864
|
|
|
807
865
|
for (const [column, columnCells] of groupBy(pathCells, ({ prop, index }) => prop.props[index])) {
|
|
@@ -822,12 +880,11 @@ function setKeyPaths(node, cells) {
|
|
|
822
880
|
}
|
|
823
881
|
}
|
|
824
882
|
|
|
825
|
-
|
|
826
|
-
keysByColumn.set(column, Object.keys(patch));
|
|
883
|
+
patchesByColumn.set(column, { keys: keys[column], patch });
|
|
827
884
|
}
|
|
828
885
|
}
|
|
829
886
|
|
|
830
|
-
return
|
|
887
|
+
return patchesByColumn;
|
|
831
888
|
}
|
|
832
889
|
|
|
833
890
|
function hasOwnerKeyArray(relation) {
|