turbine-orm 0.80.0 → 0.81.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/dist/cjs/powql.d.ts +16 -13
- package/dist/cjs/powql.js +50 -55
- package/dist/cjs/prisma-compat.d.ts +2 -0
- package/dist/cjs/prisma-compat.js +10 -64
- package/dist/cjs/query/builder.d.ts +29 -0
- package/dist/cjs/query/builder.js +68 -0
- package/dist/cjs/query/compound-unique.d.ts +24 -0
- package/dist/cjs/query/compound-unique.js +55 -0
- package/dist/cjs/query/writes.d.ts +23 -0
- package/dist/cjs/query/writes.js +35 -14
- package/dist/powql.d.ts +16 -13
- package/dist/powql.js +52 -57
- package/dist/prisma-compat.d.ts +2 -0
- package/dist/prisma-compat.js +10 -64
- package/dist/query/builder.d.ts +29 -0
- package/dist/query/builder.js +70 -2
- package/dist/query/compound-unique.d.ts +24 -0
- package/dist/query/compound-unique.js +54 -0
- package/dist/query/writes.d.ts +23 -0
- package/dist/query/writes.js +35 -15
- package/package.json +2 -2
- package/skills/turbine-orm/SKILL.md +6 -3
package/dist/cjs/powql.d.ts
CHANGED
|
@@ -889,19 +889,22 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
|
|
|
889
889
|
}>;
|
|
890
890
|
upsert(args: UpsertArgs<T>): Promise<T>;
|
|
891
891
|
/**
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
*
|
|
896
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
* find and the update run through the transaction's table
|
|
903
|
-
* configured global filter applies to both exactly as it does
|
|
904
|
-
* read or write, and `skipGlobalFilters` is forwarded to both.
|
|
892
|
+
* `upsert` as Prisma defines it, for every shape the native `upsert … on`
|
|
893
|
+
* statement cannot express: find the row `where` names, update it if it
|
|
894
|
+
* exists, else insert `create`, in one flat transaction. PowDB's single
|
|
895
|
+
* writer makes the read-then-write safe from concurrent writers; the
|
|
896
|
+
* transaction makes it atomic with the write.
|
|
897
|
+
*
|
|
898
|
+
* The lookup is by `where`'s own values. It used to be by `create`'s values
|
|
899
|
+
* for the conflict columns, which is what the native statement compares, and
|
|
900
|
+
* so answered a different question whenever the two disagreed: `where: { id }`
|
|
901
|
+
* with a `create` that leaves the key to the engine inserted a new row every
|
|
902
|
+
* time. The find and the update run through the transaction's table
|
|
903
|
+
* interface, so a configured global filter applies to both exactly as it does
|
|
904
|
+
* to any other read or write, and `skipGlobalFilters` is forwarded to both.
|
|
905
|
+
*
|
|
906
|
+
* Both halves are compiled before the lookup, so an unknown field in `update`
|
|
907
|
+
* is refused whether or not the row exists (the SQL engines do the same).
|
|
905
908
|
*/
|
|
906
909
|
private upsertLookupFirst;
|
|
907
910
|
count(args?: CountArgs<T>): Promise<number>;
|
package/dist/cjs/powql.js
CHANGED
|
@@ -2701,7 +2701,7 @@ class PowqlInterface {
|
|
|
2701
2701
|
if (value === undefined)
|
|
2702
2702
|
continue;
|
|
2703
2703
|
if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, field)) {
|
|
2704
|
-
throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not createMany()
|
|
2704
|
+
throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update()/upsert(), not createMany()`);
|
|
2705
2705
|
}
|
|
2706
2706
|
out.push({ col: this.column(field), value });
|
|
2707
2707
|
}
|
|
@@ -2900,7 +2900,7 @@ class PowqlInterface {
|
|
|
2900
2900
|
if (value === undefined)
|
|
2901
2901
|
continue;
|
|
2902
2902
|
if ((0, utils_js_1.resolveRelationDef)(this.meta.relations, field)) {
|
|
2903
|
-
throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not updateMany()
|
|
2903
|
+
throw new errors_js_1.UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update()/upsert(), not updateMany()`);
|
|
2904
2904
|
}
|
|
2905
2905
|
const colMeta = this.column(field);
|
|
2906
2906
|
const ref = this.ref(field);
|
|
@@ -3067,46 +3067,47 @@ class PowqlInterface {
|
|
|
3067
3067
|
const plan = this.writeReturnPlan(args);
|
|
3068
3068
|
const createData = this.applyPkDefault(args.create);
|
|
3069
3069
|
const pkCol = this.meta.primaryKey[0];
|
|
3070
|
-
// The conflict target is the columns `where` names, as on the SQL
|
|
3071
|
-
// engines (`ON CONFLICT (<where keys>)`), with the conflicting VALUES
|
|
3072
|
-
// taken from `create`. This path used to conflict on the primary key
|
|
3073
|
-
// whatever `where` named, so an upsert keyed on another unique column
|
|
3074
|
-
// (`where: { email }`) never found the existing row.
|
|
3075
3070
|
const conflictColumns = Object.keys(upsertWhere)
|
|
3076
3071
|
.filter((k) => upsertWhere[k] !== undefined)
|
|
3077
3072
|
.map((k) => this.column(k).name);
|
|
3073
|
+
const updateData = (args.update ?? {});
|
|
3078
3074
|
// PowQL's native `upsert … on .col` expresses exactly one shape: a single
|
|
3079
|
-
// conflict column that is the primary key,
|
|
3080
|
-
//
|
|
3081
|
-
//
|
|
3075
|
+
// conflict column that is the primary key, pinned to the value `create`
|
|
3076
|
+
// inserts, with a non-empty update and no predicate on its conflict
|
|
3077
|
+
// branch. Every other shape looks the row up by `where` first, in one
|
|
3078
|
+
// transaction, which is what the SQL engines do for the same shapes and
|
|
3079
|
+
// what the call means (query/compound-unique.ts upsertWhereMatchesCreate).
|
|
3082
3080
|
//
|
|
3083
3081
|
// That includes a table under a global filter. The native statement's
|
|
3084
3082
|
// `on conflict` branch carries no predicate, so a tenant-scoped client
|
|
3085
3083
|
// whose `create` key matched ANOTHER tenant's row updated that row, and
|
|
3086
3084
|
// the tenant-filtered reselect then reported the write it had just made
|
|
3087
|
-
// as a NotFoundError. The
|
|
3088
|
-
//
|
|
3089
|
-
//
|
|
3090
|
-
// tenant's row is invisible to it: the insert then collides on the key
|
|
3091
|
-
// and fails, as it does on the SQL engines, and nothing is overwritten.
|
|
3085
|
+
// as a NotFoundError. The lookup runs its find and its update through the
|
|
3086
|
+
// filtered interface, so another tenant's row is invisible to it: the
|
|
3087
|
+
// insert then collides on the key and fails, and nothing is overwritten.
|
|
3092
3088
|
const filtered = this.applyGlobalFilter('', [], args.skipGlobalFilters) !== '';
|
|
3093
3089
|
const native = this.meta.primaryKey.length === 1 &&
|
|
3094
3090
|
pkCol !== undefined &&
|
|
3095
3091
|
conflictColumns.length === 1 &&
|
|
3096
3092
|
conflictColumns[0] === pkCol &&
|
|
3093
|
+
(0, compound_unique_js_1.upsertWhereMatchesCreate)(this.meta, upsertWhere, (args.create ?? {})) &&
|
|
3094
|
+
!(0, nested_write_js_1.hasRelationFields)(args.create, this.meta) &&
|
|
3095
|
+
!(0, nested_write_js_1.hasRelationFields)(updateData, this.meta) &&
|
|
3096
|
+
Object.values(updateData).some((v) => v !== undefined) &&
|
|
3097
3097
|
!filtered;
|
|
3098
3098
|
if (!native) {
|
|
3099
|
-
return this.upsertLookupFirst(
|
|
3099
|
+
return this.upsertLookupFirst(upsertWhere, createData, updateData, {
|
|
3100
3100
|
select: args.select,
|
|
3101
3101
|
omit: args.omit,
|
|
3102
3102
|
skipGlobalFilters: args.skipGlobalFilters,
|
|
3103
|
+
timeout: args.timeout,
|
|
3103
3104
|
});
|
|
3104
3105
|
}
|
|
3105
3106
|
const params = [];
|
|
3106
3107
|
const createBody = this.scalarData(createData)
|
|
3107
3108
|
.map((a) => `${(0, powdb_shared_js_1.quotePowqlIdent)(a.col.name)} := ${this.writeRef(a.value, a.col, params)}`)
|
|
3108
3109
|
.join(', ');
|
|
3109
|
-
const updateBody = this.buildUpdateAssignments(
|
|
3110
|
+
const updateBody = this.buildUpdateAssignments(updateData, params);
|
|
3110
3111
|
// PowDB 0.7.0's `upsert` statement does NOT accept a trailing `returning`
|
|
3111
3112
|
// (verified: "unexpected trailing token … 'returning'"), because it is one
|
|
3112
3113
|
// atomic insert-or-update, not two branches. So upsert alone keeps the
|
|
@@ -3126,48 +3127,42 @@ class PowqlInterface {
|
|
|
3126
3127
|
});
|
|
3127
3128
|
}
|
|
3128
3129
|
/**
|
|
3129
|
-
*
|
|
3130
|
-
*
|
|
3131
|
-
*
|
|
3132
|
-
*
|
|
3133
|
-
*
|
|
3134
|
-
* safe from concurrent writers; the transaction makes it atomic with the
|
|
3135
|
-
* write.
|
|
3130
|
+
* `upsert` as Prisma defines it, for every shape the native `upsert … on`
|
|
3131
|
+
* statement cannot express: find the row `where` names, update it if it
|
|
3132
|
+
* exists, else insert `create`, in one flat transaction. PowDB's single
|
|
3133
|
+
* writer makes the read-then-write safe from concurrent writers; the
|
|
3134
|
+
* transaction makes it atomic with the write.
|
|
3136
3135
|
*
|
|
3137
|
-
* The
|
|
3138
|
-
*
|
|
3139
|
-
*
|
|
3140
|
-
*
|
|
3141
|
-
*
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3136
|
+
* The lookup is by `where`'s own values. It used to be by `create`'s values
|
|
3137
|
+
* for the conflict columns, which is what the native statement compares, and
|
|
3138
|
+
* so answered a different question whenever the two disagreed: `where: { id }`
|
|
3139
|
+
* with a `create` that leaves the key to the engine inserted a new row every
|
|
3140
|
+
* time. The find and the update run through the transaction's table
|
|
3141
|
+
* interface, so a configured global filter applies to both exactly as it does
|
|
3142
|
+
* to any other read or write, and `skipGlobalFilters` is forwarded to both.
|
|
3143
|
+
*
|
|
3144
|
+
* Both halves are compiled before the lookup, so an unknown field in `update`
|
|
3145
|
+
* is refused whether or not the row exists (the SQL engines do the same).
|
|
3146
|
+
*/
|
|
3147
|
+
async upsertLookupFirst(where, createData, updateData, shape) {
|
|
3148
|
+
const { skipGlobalFilters, timeout, ...projection } = shape;
|
|
3145
3149
|
const skip = skipGlobalFilters === undefined ? {} : { skipGlobalFilters };
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
const
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
const keyless = keyPairs.some((p) => p.value == null);
|
|
3153
|
-
const isCompositePk = keyPairs.length > 1 &&
|
|
3154
|
-
keyPairs.length === this.meta.primaryKey.length &&
|
|
3155
|
-
conflictColumns.every((c) => this.meta.primaryKey.includes(c));
|
|
3156
|
-
if (keyless && isCompositePk) {
|
|
3157
|
-
throw new errors_js_1.ValidationError(`upsert on "${this.table}" needs every composite-PK field in \`create\` (${keyPairs
|
|
3158
|
-
.map((p) => p.field)
|
|
3159
|
-
.join(', ')}).`);
|
|
3160
|
-
}
|
|
3161
|
-
const keyWhere = Object.fromEntries(keyPairs.map((p) => [p.field, p.value]));
|
|
3150
|
+
const time = timeout === undefined ? {} : { timeout };
|
|
3151
|
+
this.scalarData((0, nested_write_js_1.extractRelationFields)(createData, this.meta).scalars);
|
|
3152
|
+
const scalarUpdate = (0, nested_write_js_1.extractRelationFields)(updateData, this.meta).scalars;
|
|
3153
|
+
if (Object.values(scalarUpdate).some((v) => v !== undefined))
|
|
3154
|
+
this.buildUpdateAssignments(scalarUpdate, []);
|
|
3155
|
+
const updateIsEmpty = Object.values(updateData).every((v) => v === undefined);
|
|
3162
3156
|
const run = async (ctx) => {
|
|
3163
3157
|
const tbl = ctx.tx.table(this.table);
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3158
|
+
const existing = await tbl.findUnique({ where, ...projection, ...skip, ...time });
|
|
3159
|
+
if (existing) {
|
|
3160
|
+
// An empty `update` means "leave it as it is": the row found is the answer.
|
|
3161
|
+
if (updateIsEmpty)
|
|
3162
|
+
return existing;
|
|
3163
|
+
return (await tbl.update({ where, data: updateData, ...projection, ...skip, ...time }));
|
|
3164
|
+
}
|
|
3165
|
+
return (await tbl.create({ data: createData, ...projection, ...time }));
|
|
3171
3166
|
};
|
|
3172
3167
|
return this.isTxScoped() ? run(this.buildNestedCtx()) : this.runInImplicitTx(run);
|
|
3173
3168
|
}
|
|
@@ -159,6 +159,8 @@ export interface CompatQueryInterface {
|
|
|
159
159
|
count: number;
|
|
160
160
|
}>;
|
|
161
161
|
buildUpsert(args: Record<string, unknown>): DeferredQuery<unknown>;
|
|
162
|
+
/** Whether `upsert()` will look the row up first (and `buildUpsert` refuse). Absent on PowDB. */
|
|
163
|
+
upsertNeedsLookup?(args: Record<string, unknown>): boolean;
|
|
162
164
|
buildCount(args?: Record<string, unknown>): DeferredQuery<number>;
|
|
163
165
|
buildAggregate(args: Record<string, unknown>): DeferredQuery<unknown>;
|
|
164
166
|
buildGroupBy(args: Record<string, unknown>): DeferredQuery<unknown[]>;
|
|
@@ -1775,53 +1775,6 @@ function hasNestedKeys(ctx, mm, data) {
|
|
|
1775
1775
|
return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date);
|
|
1776
1776
|
});
|
|
1777
1777
|
}
|
|
1778
|
-
/**
|
|
1779
|
-
* Whether an upsert's translated `where` key values all equal the
|
|
1780
|
-
* corresponding `create` values. When they do (the common Prisma idiom), the
|
|
1781
|
-
* native single-statement ON CONFLICT upsert is semantically identical to
|
|
1782
|
-
* Prisma's lookup-first and stays atomic. When they differ, native upsert
|
|
1783
|
-
* would insert the `create` row even though the `where` row exists, so the
|
|
1784
|
-
* adapter must emulate lookup-first instead.
|
|
1785
|
-
*/
|
|
1786
|
-
function upsertKeysMatch(t) {
|
|
1787
|
-
const where = t.where;
|
|
1788
|
-
const create = t.create;
|
|
1789
|
-
if (!isPlainObject(where) || !isPlainObject(create))
|
|
1790
|
-
return false;
|
|
1791
|
-
const scalarEq = (a, b) => {
|
|
1792
|
-
if (a instanceof Date || b instanceof Date) {
|
|
1793
|
-
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
1794
|
-
}
|
|
1795
|
-
return a === b;
|
|
1796
|
-
};
|
|
1797
|
-
for (const [k, v] of Object.entries(where)) {
|
|
1798
|
-
if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
|
|
1799
|
-
// Compound-unique selector object: every member must scalar-match create.
|
|
1800
|
-
if (Array.isArray(v))
|
|
1801
|
-
return false;
|
|
1802
|
-
for (const [mk, mv] of Object.entries(v)) {
|
|
1803
|
-
if (mv !== null && typeof mv === 'object' && !(mv instanceof Date))
|
|
1804
|
-
return false;
|
|
1805
|
-
if (!scalarEq(mv, create[mk]))
|
|
1806
|
-
return false;
|
|
1807
|
-
}
|
|
1808
|
-
continue;
|
|
1809
|
-
}
|
|
1810
|
-
if (!scalarEq(v, create[k]))
|
|
1811
|
-
return false;
|
|
1812
|
-
}
|
|
1813
|
-
return true;
|
|
1814
|
-
}
|
|
1815
|
-
/** Prisma upsert semantics: look up by where; update the found row, else insert create. */
|
|
1816
|
-
async function upsertLookupFirst(qi, t) {
|
|
1817
|
-
const existing = await qi.findUnique({ where: t.where });
|
|
1818
|
-
// The projection rides along so both branches return what the native upsert
|
|
1819
|
-
// path returns (an explicitly selected PII field included).
|
|
1820
|
-
const shape = { select: t.select, omit: t.omit };
|
|
1821
|
-
if (existing)
|
|
1822
|
-
return qi.update({ where: t.where, data: t.update, ...shape });
|
|
1823
|
-
return qi.create({ data: t.create, ...shape });
|
|
1824
|
-
}
|
|
1825
1778
|
/**
|
|
1826
1779
|
* The row shapes a translated `createMany` has to insert, as contiguous runs
|
|
1827
1780
|
* that each name the same fields (see {@link createManyShapeRuns}).
|
|
@@ -1883,7 +1836,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1883
1836
|
reshape: batch.reshape,
|
|
1884
1837
|
nested: () => {
|
|
1885
1838
|
try {
|
|
1886
|
-
return batch.nested?.(translate()) ?? false;
|
|
1839
|
+
return batch.nested?.(translate(), getQI()) ?? false;
|
|
1887
1840
|
}
|
|
1888
1841
|
catch {
|
|
1889
1842
|
return false; // let the build path surface the translation error consistently
|
|
@@ -2045,24 +1998,17 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
2045
1998
|
};
|
|
2046
1999
|
(0, index_js_1.applyNativeOptions)(index_js_1.UPSERT_OPTIONS, a, t);
|
|
2047
2000
|
return t;
|
|
2048
|
-
},
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
return qi.upsert(t).then(shape);
|
|
2054
|
-
}
|
|
2055
|
-
return runInTx(async (table) => shape(await upsertLookupFirst(table(mm.table), t)));
|
|
2056
|
-
}, {
|
|
2001
|
+
},
|
|
2002
|
+
// Core `upsert` has Prisma's semantics itself (it looks the row up by
|
|
2003
|
+
// `where` whenever one conflict statement would mean something else,
|
|
2004
|
+
// and runs nested writes), so the adapter passes it straight through.
|
|
2005
|
+
(qi, t) => qi.upsert(t).then(shape), {
|
|
2057
2006
|
build: (qi, t) => qi.buildUpsert(t),
|
|
2058
2007
|
reshape: shape,
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
}
|
|
2064
|
-
return shape(await upsertLookupFirst(table(mm.table), t));
|
|
2065
|
-
},
|
|
2008
|
+
// Batchable only when core would run it as one statement. PowDB has
|
|
2009
|
+
// no deferred statements at all, so it always takes the tx path.
|
|
2010
|
+
nested: (t, qi) => qi.upsertNeedsLookup?.(t) ?? true,
|
|
2011
|
+
execInTx: async (table, t) => shape(await table(mm.table).upsert(t)),
|
|
2066
2012
|
});
|
|
2067
2013
|
},
|
|
2068
2014
|
count: (args = {}) => defer('count', args, () => {
|
|
@@ -1283,6 +1283,35 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
1283
1283
|
private makeTxProxy;
|
|
1284
1284
|
delete<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: DeleteArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
|
|
1285
1285
|
upsert<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpsertArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
|
|
1286
|
+
/**
|
|
1287
|
+
* @internal Whether `upsert()` looks the row up rather than running one
|
|
1288
|
+
* statement, which is also exactly when `buildUpsert` refuses these args.
|
|
1289
|
+
* prisma-compat reads it to decide whether an array `$transaction` can batch
|
|
1290
|
+
* an upsert or has to run the array inside a transaction.
|
|
1291
|
+
*/
|
|
1292
|
+
upsertNeedsLookup(args: UpsertArgs<T, R, Record<string, boolean> | undefined, Record<string, boolean> | undefined>): boolean;
|
|
1293
|
+
/**
|
|
1294
|
+
* `upsert` as Prisma defines it: find the row `where` names, update it if it
|
|
1295
|
+
* exists, else insert `create`, in one transaction (the caller's, when there
|
|
1296
|
+
* is one). Taken whenever the single conflict statement would mean something
|
|
1297
|
+
* else (`upsertLookupReason`: `where` not pinned to `create`'s values, an
|
|
1298
|
+
* empty `update`, a global filter the engine's statement cannot carry) and
|
|
1299
|
+
* whenever `create` or `update` holds nested relation writes, which only the
|
|
1300
|
+
* nested-write engine can run.
|
|
1301
|
+
*
|
|
1302
|
+
* Validation is DATA-INDEPENDENT, as it is for the batched relation loader:
|
|
1303
|
+
* both halves are compiled before the lookup runs, so an unknown field in
|
|
1304
|
+
* `update` is refused whether or not the row exists, and a call is never
|
|
1305
|
+
* valid on one row and invalid on the next. Only the scalar part of each half
|
|
1306
|
+
* is compiled here; nested relation writes are validated by the engine that
|
|
1307
|
+
* runs them.
|
|
1308
|
+
*
|
|
1309
|
+
* Not atomic against a concurrent insert of the same key, exactly as Prisma's
|
|
1310
|
+
* own lookup is not: two callers can both miss, and the second insert then
|
|
1311
|
+
* fails on the unique constraint (E008) when `create` carries the key. It
|
|
1312
|
+
* never writes two rows over one key the database enforces.
|
|
1313
|
+
*/
|
|
1314
|
+
private upsertLookupFirst;
|
|
1286
1315
|
updateMany(args: UpdateManyArgs<T, R>): Promise<{
|
|
1287
1316
|
count: number;
|
|
1288
1317
|
}>;
|
|
@@ -3638,10 +3638,78 @@ class QueryInterface {
|
|
|
3638
3638
|
// -------------------------------------------------------------------------
|
|
3639
3639
|
async upsert(args) {
|
|
3640
3640
|
return this.executeWithMiddleware('upsert', args, async () => {
|
|
3641
|
+
if (this.upsertNeedsLookup(args)) {
|
|
3642
|
+
return this.upsertLookupFirst(args);
|
|
3643
|
+
}
|
|
3641
3644
|
const deferred = this.buildUpsert(args);
|
|
3642
3645
|
return this.executeMutation(deferred, args.timeout);
|
|
3643
3646
|
});
|
|
3644
3647
|
}
|
|
3648
|
+
/**
|
|
3649
|
+
* @internal Whether `upsert()` looks the row up rather than running one
|
|
3650
|
+
* statement, which is also exactly when `buildUpsert` refuses these args.
|
|
3651
|
+
* prisma-compat reads it to decide whether an array `$transaction` can batch
|
|
3652
|
+
* an upsert or has to run the array inside a transaction.
|
|
3653
|
+
*/
|
|
3654
|
+
upsertNeedsLookup(args) {
|
|
3655
|
+
return ((0, nested_write_js_1.hasRelationFields)((args.create ?? {}), this.tableMeta) ||
|
|
3656
|
+
(0, nested_write_js_1.hasRelationFields)((args.update ?? {}), this.tableMeta) ||
|
|
3657
|
+
writesMod.upsertLookupReason(this.ctx, args) !== null);
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* `upsert` as Prisma defines it: find the row `where` names, update it if it
|
|
3661
|
+
* exists, else insert `create`, in one transaction (the caller's, when there
|
|
3662
|
+
* is one). Taken whenever the single conflict statement would mean something
|
|
3663
|
+
* else (`upsertLookupReason`: `where` not pinned to `create`'s values, an
|
|
3664
|
+
* empty `update`, a global filter the engine's statement cannot carry) and
|
|
3665
|
+
* whenever `create` or `update` holds nested relation writes, which only the
|
|
3666
|
+
* nested-write engine can run.
|
|
3667
|
+
*
|
|
3668
|
+
* Validation is DATA-INDEPENDENT, as it is for the batched relation loader:
|
|
3669
|
+
* both halves are compiled before the lookup runs, so an unknown field in
|
|
3670
|
+
* `update` is refused whether or not the row exists, and a call is never
|
|
3671
|
+
* valid on one row and invalid on the next. Only the scalar part of each half
|
|
3672
|
+
* is compiled here; nested relation writes are validated by the engine that
|
|
3673
|
+
* runs them.
|
|
3674
|
+
*
|
|
3675
|
+
* Not atomic against a concurrent insert of the same key, exactly as Prisma's
|
|
3676
|
+
* own lookup is not: two callers can both miss, and the second insert then
|
|
3677
|
+
* fails on the unique constraint (E008) when `create` carries the key. It
|
|
3678
|
+
* never writes two rows over one key the database enforces.
|
|
3679
|
+
*/
|
|
3680
|
+
async upsertLookupFirst(args) {
|
|
3681
|
+
const where = (args.where ?? {});
|
|
3682
|
+
const create = (args.create ?? {});
|
|
3683
|
+
const update = (args.update ?? {});
|
|
3684
|
+
(0, compound_unique_js_1.assertMutationWhereIdentifiesOneRow)(this.tableMeta, this.table, (0, compound_unique_js_1.expandCompoundUniqueWhere)(this.tableMeta, where), 'upsert');
|
|
3685
|
+
this.resolveWriteProjection(args);
|
|
3686
|
+
// An undefined option is an absent one on every call below.
|
|
3687
|
+
const skip = { skipGlobalFilters: args.skipGlobalFilters };
|
|
3688
|
+
const shape = { select: args.select, omit: args.omit, timeout: args.timeout };
|
|
3689
|
+
const scalarCreate = (0, nested_write_js_1.extractRelationFields)(create, this.tableMeta).scalars;
|
|
3690
|
+
const scalarUpdate = (0, nested_write_js_1.extractRelationFields)(update, this.tableMeta).scalars;
|
|
3691
|
+
const updateIsEmpty = Object.values(update).every((v) => v === undefined);
|
|
3692
|
+
if (Object.keys(scalarCreate).length > 0)
|
|
3693
|
+
this.buildCreate({ data: scalarCreate });
|
|
3694
|
+
if (Object.values(scalarUpdate).some((v) => v !== undefined)) {
|
|
3695
|
+
this.buildUpdate({ where, data: scalarUpdate, ...skip });
|
|
3696
|
+
}
|
|
3697
|
+
const run = async (table) => {
|
|
3698
|
+
const found = await table.findUnique({ where, ...shape, ...skip });
|
|
3699
|
+
if (found) {
|
|
3700
|
+
// Prisma reads an empty `update` as "leave it as it is": the row found
|
|
3701
|
+
// IS the answer, projected as the write would have projected it.
|
|
3702
|
+
if (updateIsEmpty)
|
|
3703
|
+
return found;
|
|
3704
|
+
return (await table.update({ where, data: update, ...shape, ...skip }));
|
|
3705
|
+
}
|
|
3706
|
+
return (await table.create({ data: create, ...shape }));
|
|
3707
|
+
};
|
|
3708
|
+
if (this.txScoped) {
|
|
3709
|
+
return run(this.buildNestedCtx().tx.table(this.table));
|
|
3710
|
+
}
|
|
3711
|
+
return this.runInImplicitTx((ctx) => run(ctx.tx.table(this.table)));
|
|
3712
|
+
}
|
|
3645
3713
|
// -------------------------------------------------------------------------
|
|
3646
3714
|
// updateMany, UPDATE ... WHERE ... returning count
|
|
3647
3715
|
// -------------------------------------------------------------------------
|
|
@@ -169,3 +169,27 @@ export declare function isInternalRowSelector(where: unknown): boolean;
|
|
|
169
169
|
* uniqueness source there.
|
|
170
170
|
*/
|
|
171
171
|
export declare function whereIdentifiesOneRow(meta: TableMetadata, where: Record<string, unknown>): boolean;
|
|
172
|
+
/**
|
|
173
|
+
* Whether an `upsert` means the same thing as ONE conflict statement.
|
|
174
|
+
*
|
|
175
|
+
* An upsert's contract is Prisma's: find the row `where` names; update it if
|
|
176
|
+
* it exists, else insert `create`. A single `INSERT ... ON CONFLICT (<where
|
|
177
|
+
* keys>)` (or PowDB's `upsert ... on`) compares the conflict columns against
|
|
178
|
+
* the values being INSERTED, which are `create`'s, and never reads `where`'s
|
|
179
|
+
* values at all. The two agree exactly when every `where` key is pinned to the
|
|
180
|
+
* value `create` carries for that column. Otherwise the statement answers a
|
|
181
|
+
* different question: `where: { id: 5 }` with a `create` that leaves `id` to
|
|
182
|
+
* its sequence inserted a new row every time and never touched row 5, and
|
|
183
|
+
* `where: { email: a }` with `create: { email: b }` updated the row holding
|
|
184
|
+
* `b`, silently.
|
|
185
|
+
*
|
|
186
|
+
* Shared by every engine, like the one-row rule above: each one decides here
|
|
187
|
+
* whether it may take its single-statement fast path, and looks the row up by
|
|
188
|
+
* `where` first when it may not. `false` is always a SAFE answer (the lookup
|
|
189
|
+
* path is correct for every shape, only slower), so anything this does not
|
|
190
|
+
* recognise as a plain matching value answers `false`: an operator other than a
|
|
191
|
+
* bare `equals`, a different object (two Dates compare by instant), a key that
|
|
192
|
+
* names no column, and NULL, which a unique index never treats as equal to
|
|
193
|
+
* anything.
|
|
194
|
+
*/
|
|
195
|
+
export declare function upsertWhereMatchesCreate(meta: TableMetadata, where: Record<string, unknown>, create: Record<string, unknown>): boolean;
|
|
@@ -48,6 +48,7 @@ exports.uniqueKeyNames = uniqueKeyNames;
|
|
|
48
48
|
exports.markInternalRowSelector = markInternalRowSelector;
|
|
49
49
|
exports.isInternalRowSelector = isInternalRowSelector;
|
|
50
50
|
exports.whereIdentifiesOneRow = whereIdentifiesOneRow;
|
|
51
|
+
exports.upsertWhereMatchesCreate = upsertWhereMatchesCreate;
|
|
51
52
|
const errors_js_1 = require("../errors.js");
|
|
52
53
|
const filters_js_1 = require("./filters.js");
|
|
53
54
|
const utils_js_1 = require("./utils.js");
|
|
@@ -430,6 +431,60 @@ function whereIdentifiesOneRow(meta, where) {
|
|
|
430
431
|
return false;
|
|
431
432
|
return uniqueColumnSets(meta).some((cols) => cols.every((c) => pinned.has(c)));
|
|
432
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Whether an `upsert` means the same thing as ONE conflict statement.
|
|
436
|
+
*
|
|
437
|
+
* An upsert's contract is Prisma's: find the row `where` names; update it if
|
|
438
|
+
* it exists, else insert `create`. A single `INSERT ... ON CONFLICT (<where
|
|
439
|
+
* keys>)` (or PowDB's `upsert ... on`) compares the conflict columns against
|
|
440
|
+
* the values being INSERTED, which are `create`'s, and never reads `where`'s
|
|
441
|
+
* values at all. The two agree exactly when every `where` key is pinned to the
|
|
442
|
+
* value `create` carries for that column. Otherwise the statement answers a
|
|
443
|
+
* different question: `where: { id: 5 }` with a `create` that leaves `id` to
|
|
444
|
+
* its sequence inserted a new row every time and never touched row 5, and
|
|
445
|
+
* `where: { email: a }` with `create: { email: b }` updated the row holding
|
|
446
|
+
* `b`, silently.
|
|
447
|
+
*
|
|
448
|
+
* Shared by every engine, like the one-row rule above: each one decides here
|
|
449
|
+
* whether it may take its single-statement fast path, and looks the row up by
|
|
450
|
+
* `where` first when it may not. `false` is always a SAFE answer (the lookup
|
|
451
|
+
* path is correct for every shape, only slower), so anything this does not
|
|
452
|
+
* recognise as a plain matching value answers `false`: an operator other than a
|
|
453
|
+
* bare `equals`, a different object (two Dates compare by instant), a key that
|
|
454
|
+
* names no column, and NULL, which a unique index never treats as equal to
|
|
455
|
+
* anything.
|
|
456
|
+
*/
|
|
457
|
+
function upsertWhereMatchesCreate(meta, where, create) {
|
|
458
|
+
const createByColumn = new Map();
|
|
459
|
+
for (const [key, value] of Object.entries(create)) {
|
|
460
|
+
if (value === undefined)
|
|
461
|
+
continue;
|
|
462
|
+
const column = (0, utils_js_1.resolveColumnName)(meta, key);
|
|
463
|
+
if (column !== undefined)
|
|
464
|
+
createByColumn.set(column, value);
|
|
465
|
+
}
|
|
466
|
+
let keys = 0;
|
|
467
|
+
for (const [key, raw] of Object.entries(expandCompoundUniqueWhere(meta, where))) {
|
|
468
|
+
if (raw === undefined)
|
|
469
|
+
continue;
|
|
470
|
+
const column = (0, utils_js_1.resolveColumnName)(meta, key);
|
|
471
|
+
if (column === undefined || !createByColumn.has(column))
|
|
472
|
+
return false;
|
|
473
|
+
const value = isBareEquals(raw) ? raw.equals : raw;
|
|
474
|
+
if (!sameKeyValue(value, createByColumn.get(column)))
|
|
475
|
+
return false;
|
|
476
|
+
keys++;
|
|
477
|
+
}
|
|
478
|
+
return keys > 0;
|
|
479
|
+
}
|
|
480
|
+
/** `{ equals: v }` and nothing else (a `mode` beside it changes the match). */
|
|
481
|
+
function isBareEquals(value) {
|
|
482
|
+
return isPlainObject(value) && Object.keys(value).length === 1 && 'equals' in value;
|
|
483
|
+
}
|
|
484
|
+
/** Equal non-null key values; two Dates compare by instant, other objects by identity. */
|
|
485
|
+
function sameKeyValue(a, b) {
|
|
486
|
+
return a != null && (a instanceof Date && b instanceof Date ? a.getTime() === b.getTime() : a === b);
|
|
487
|
+
}
|
|
433
488
|
/** A bare value, or an operator object whose `equals` is a value. */
|
|
434
489
|
function isPinnedToOneValue(value) {
|
|
435
490
|
if (value === undefined || value === null)
|
|
@@ -59,6 +59,29 @@ export declare function makeCreateReselect<T extends object>(qi: BuilderCtx, ins
|
|
|
59
59
|
export declare function buildCreateMany<T extends object>(qi: BuilderCtx, args: CreateManyArgs<T>): DeferredQuery<T[]>;
|
|
60
60
|
export declare function buildUpdate<T extends object>(qi: BuilderCtx, args: UpdateArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
|
|
61
61
|
export declare function buildDelete<T extends object>(qi: BuilderCtx, args: DeleteArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
|
|
62
|
+
/**
|
|
63
|
+
* Why an `upsert` cannot run as this engine's single conflict statement, or
|
|
64
|
+
* `null` when it can. The one authority for that decision: `upsert()` asks it
|
|
65
|
+
* to choose between the statement and a lookup by `where` inside a
|
|
66
|
+
* transaction, and `buildUpsert` asks it to refuse the shapes its one
|
|
67
|
+
* statement cannot express, since a `DeferredQuery` (a pipeline, an array
|
|
68
|
+
* `$transaction`) has no second round trip to look anything up with.
|
|
69
|
+
*
|
|
70
|
+
* - `where-differs`: `where` is not pinned to `create`'s values, so the
|
|
71
|
+
* conflict statement would compare the wrong row (upsertWhereMatchesCreate).
|
|
72
|
+
* - `empty-update`: an `update` with no fields has no `DO UPDATE SET` to emit;
|
|
73
|
+
* every engine rejected the statement as a syntax error. Prisma reads it as
|
|
74
|
+
* "create it if it is missing", which the lookup answers.
|
|
75
|
+
* - `global-filter`: MySQL's `ON DUPLICATE KEY UPDATE` has no predicate slot
|
|
76
|
+
* and SQL Server's MERGE cannot take the builder's column references there,
|
|
77
|
+
* so the table's global filter cannot guard the conflict update. The lookup
|
|
78
|
+
* and the update both carry the filter, so another tenant's row is invisible
|
|
79
|
+
* to them.
|
|
80
|
+
*
|
|
81
|
+
* Reads `currentSkip`, so it resolves `skipGlobalFilters` itself first.
|
|
82
|
+
*/
|
|
83
|
+
export type UpsertLookupReason = 'where-differs' | 'empty-update' | 'global-filter';
|
|
84
|
+
export declare function upsertLookupReason<T extends object>(qi: BuilderCtx, args: UpsertArgs<T>): UpsertLookupReason | null;
|
|
62
85
|
export declare function buildUpsert<T extends object>(qi: BuilderCtx, args: UpsertArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
|
|
63
86
|
export declare function buildUpdateMany<T extends object>(qi: BuilderCtx, args: UpdateManyArgs<T>): DeferredQuery<{
|
|
64
87
|
count: number;
|
package/dist/cjs/query/writes.js
CHANGED
|
@@ -52,6 +52,7 @@ exports.makeCreateReselect = makeCreateReselect;
|
|
|
52
52
|
exports.buildCreateMany = buildCreateMany;
|
|
53
53
|
exports.buildUpdate = buildUpdate;
|
|
54
54
|
exports.buildDelete = buildDelete;
|
|
55
|
+
exports.upsertLookupReason = upsertLookupReason;
|
|
55
56
|
exports.buildUpsert = buildUpsert;
|
|
56
57
|
exports.buildUpdateMany = buildUpdateMany;
|
|
57
58
|
exports.buildDeleteMany = buildDeleteMany;
|
|
@@ -599,6 +600,35 @@ function buildDelete(qi, args, projection) {
|
|
|
599
600
|
: undefined,
|
|
600
601
|
};
|
|
601
602
|
}
|
|
603
|
+
function upsertLookupReason(qi, args) {
|
|
604
|
+
const where = (args.where ?? {});
|
|
605
|
+
const create = (args.create ?? {});
|
|
606
|
+
if (!(0, compound_unique_js_1.upsertWhereMatchesCreate)(qi.tableMeta, where, create))
|
|
607
|
+
return 'where-differs';
|
|
608
|
+
if (writeEntries(qi, (args.update ?? {})).length === 0)
|
|
609
|
+
return 'empty-update';
|
|
610
|
+
if (!qi.dialect.supportsUpsertUpdateWhere) {
|
|
611
|
+
qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
|
|
612
|
+
const filter = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
613
|
+
if (filter && whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), filter, [])) {
|
|
614
|
+
return 'global-filter';
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
/** The refusal `buildUpsert` raises for each reason, naming the way forward. */
|
|
620
|
+
function upsertStatementRefusal(qi, reason, where) {
|
|
621
|
+
const call = 'Call `upsert()` unbatched: it looks the row up by `where` first.';
|
|
622
|
+
if (reason === 'global-filter') {
|
|
623
|
+
return new errors_js_1.UnsupportedFeatureError(`a batched upsert on "${qi.table}", which has a global filter,`, qi.dialect.name, `Its upsert statement cannot carry the filter. ${call} Or pass \`skipGlobalFilters: UNSAFE\`.`);
|
|
624
|
+
}
|
|
625
|
+
const keys = Object.keys(where).filter((k) => where[k] !== undefined);
|
|
626
|
+
return new errors_js_1.ValidationError(`A batched upsert on "${qi.table}" cannot be one statement: ` +
|
|
627
|
+
(reason === 'empty-update'
|
|
628
|
+
? 'its `update` is empty. '
|
|
629
|
+
: `\`where\` (${keys.join(', ')}) does not carry \`create\`'s values, which are what one statement matches on. `) +
|
|
630
|
+
call);
|
|
631
|
+
}
|
|
602
632
|
function buildUpsert(qi, args, projection) {
|
|
603
633
|
assertWritable(qi, 'upsert');
|
|
604
634
|
assertNoGeneratedColumns(qi, args.create, 'upsert');
|
|
@@ -623,6 +653,11 @@ function buildUpsert(qi, args, projection) {
|
|
|
623
653
|
// `allowFullTableScan: UNSAFE`, which is not an option on `UpsertArgs` at
|
|
624
654
|
// all, so borrowing it here would name a way out that does not exist.
|
|
625
655
|
(0, compound_unique_js_1.assertMutationWhereIdentifiesOneRow)(qi.tableMeta, qi.table, upsertWhere, 'upsert');
|
|
656
|
+
// `upsert()` never reaches here with a reason (it looks the row up instead),
|
|
657
|
+
// so this refuses only a batched upsert, which has no round trip to spare.
|
|
658
|
+
const lookupReason = upsertLookupReason(qi, args);
|
|
659
|
+
if (lookupReason)
|
|
660
|
+
throw upsertStatementRefusal(qi, lookupReason, upsertWhere);
|
|
626
661
|
// Build the INSERT part from create data
|
|
627
662
|
const createEntries = writeEntries(qi, args.create);
|
|
628
663
|
const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
|
|
@@ -656,20 +691,6 @@ function buildUpsert(qi, args, projection) {
|
|
|
656
691
|
// globally filtered table at parse time (42702), insert path included.
|
|
657
692
|
let updateWhere;
|
|
658
693
|
const upsertFilter = whereMod.resolveGlobalFilter(qi, qi.table);
|
|
659
|
-
if (upsertFilter && !qi.dialect.supportsUpsertUpdateWhere) {
|
|
660
|
-
// MySQL's `ON DUPLICATE KEY UPDATE` has no predicate slot and SQL Server's
|
|
661
|
-
// MERGE cannot take the builder's column references there, so on those
|
|
662
|
-
// engines the filter used to be DROPPED from the conflict update, silently.
|
|
663
|
-
// A tenant-scoped upsert whose key matched another tenant's row updated
|
|
664
|
-
// that row. Refused instead, when the filter would compile to anything:
|
|
665
|
-
// the global-filter contract is that it scopes every update, and an
|
|
666
|
-
// upsert that cannot honour it must not run as if it did.
|
|
667
|
-
if (whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), upsertFilter, [])) {
|
|
668
|
-
throw new errors_js_1.UnsupportedFeatureError(`upsert on "${qi.table}", which has a global filter,`, qi.dialect.name, "This engine's upsert statement cannot carry the filter on its conflict update, so it could update a row " +
|
|
669
|
-
'the filter hides. Use findUnique, then update or create, inside $transaction; or pass ' +
|
|
670
|
-
'`skipGlobalFilters: UNSAFE` if the upsert is meant to reach every row.');
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
694
|
if (qi.dialect.supportsUpsertUpdateWhere) {
|
|
674
695
|
const gf = upsertFilter;
|
|
675
696
|
if (gf) {
|