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/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/powql.js
CHANGED
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
import { randomUUID } from 'node:crypto';
|
|
38
38
|
import { guardCheckout } from './connection-guard.js';
|
|
39
39
|
import { NotFoundError, OptimisticLockError, ReadOnlyError, TimeoutError, UnsupportedFeatureError, ValidationError, } from './errors.js';
|
|
40
|
-
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
|
|
40
|
+
import { executeNestedCreate, executeNestedUpdate, extractRelationFields, hasRelationFields, } from './nested-write.js';
|
|
41
41
|
import { ALL_POWDB_CAPABILITIES, atLeastVersion, baseTsType, coerceNativeValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, parsePowdbSemver, powqlColumnType, quotePowqlDotted, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb-shared.js';
|
|
42
42
|
import { assertAggregatePiiOptIn } from './query/aggregates.js';
|
|
43
|
-
import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, } from './query/compound-unique.js';
|
|
43
|
+
import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, upsertWhereMatchesCreate, } from './query/compound-unique.js';
|
|
44
44
|
import { ARRAY_OPERATOR_KEYS, isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
|
|
45
45
|
import { warnUnknownQueryOptions } from './query/option-surface.js';
|
|
46
46
|
import { applyStableRelationOrderTo, normalizeWithClause } from './query/relation-names.js';
|
|
@@ -2665,7 +2665,7 @@ export class PowqlInterface {
|
|
|
2665
2665
|
if (value === undefined)
|
|
2666
2666
|
continue;
|
|
2667
2667
|
if (resolveRelationDef(this.meta.relations, field)) {
|
|
2668
|
-
throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not createMany()
|
|
2668
|
+
throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update()/upsert(), not createMany()`);
|
|
2669
2669
|
}
|
|
2670
2670
|
out.push({ col: this.column(field), value });
|
|
2671
2671
|
}
|
|
@@ -2864,7 +2864,7 @@ export class PowqlInterface {
|
|
|
2864
2864
|
if (value === undefined)
|
|
2865
2865
|
continue;
|
|
2866
2866
|
if (resolveRelationDef(this.meta.relations, field)) {
|
|
2867
|
-
throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update(), not updateMany()
|
|
2867
|
+
throw new UnsupportedFeatureError('nested writes', 'PowDB', `relation "${field}", nested writes need create()/update()/upsert(), not updateMany()`);
|
|
2868
2868
|
}
|
|
2869
2869
|
const colMeta = this.column(field);
|
|
2870
2870
|
const ref = this.ref(field);
|
|
@@ -3031,46 +3031,47 @@ export class PowqlInterface {
|
|
|
3031
3031
|
const plan = this.writeReturnPlan(args);
|
|
3032
3032
|
const createData = this.applyPkDefault(args.create);
|
|
3033
3033
|
const pkCol = this.meta.primaryKey[0];
|
|
3034
|
-
// The conflict target is the columns `where` names, as on the SQL
|
|
3035
|
-
// engines (`ON CONFLICT (<where keys>)`), with the conflicting VALUES
|
|
3036
|
-
// taken from `create`. This path used to conflict on the primary key
|
|
3037
|
-
// whatever `where` named, so an upsert keyed on another unique column
|
|
3038
|
-
// (`where: { email }`) never found the existing row.
|
|
3039
3034
|
const conflictColumns = Object.keys(upsertWhere)
|
|
3040
3035
|
.filter((k) => upsertWhere[k] !== undefined)
|
|
3041
3036
|
.map((k) => this.column(k).name);
|
|
3037
|
+
const updateData = (args.update ?? {});
|
|
3042
3038
|
// PowQL's native `upsert … on .col` expresses exactly one shape: a single
|
|
3043
|
-
// conflict column that is the primary key,
|
|
3044
|
-
//
|
|
3045
|
-
//
|
|
3039
|
+
// conflict column that is the primary key, pinned to the value `create`
|
|
3040
|
+
// inserts, with a non-empty update and no predicate on its conflict
|
|
3041
|
+
// branch. Every other shape looks the row up by `where` first, in one
|
|
3042
|
+
// transaction, which is what the SQL engines do for the same shapes and
|
|
3043
|
+
// what the call means (query/compound-unique.ts upsertWhereMatchesCreate).
|
|
3046
3044
|
//
|
|
3047
3045
|
// That includes a table under a global filter. The native statement's
|
|
3048
3046
|
// `on conflict` branch carries no predicate, so a tenant-scoped client
|
|
3049
3047
|
// whose `create` key matched ANOTHER tenant's row updated that row, and
|
|
3050
3048
|
// the tenant-filtered reselect then reported the write it had just made
|
|
3051
|
-
// as a NotFoundError. The
|
|
3052
|
-
//
|
|
3053
|
-
//
|
|
3054
|
-
// tenant's row is invisible to it: the insert then collides on the key
|
|
3055
|
-
// and fails, as it does on the SQL engines, and nothing is overwritten.
|
|
3049
|
+
// as a NotFoundError. The lookup runs its find and its update through the
|
|
3050
|
+
// filtered interface, so another tenant's row is invisible to it: the
|
|
3051
|
+
// insert then collides on the key and fails, and nothing is overwritten.
|
|
3056
3052
|
const filtered = this.applyGlobalFilter('', [], args.skipGlobalFilters) !== '';
|
|
3057
3053
|
const native = this.meta.primaryKey.length === 1 &&
|
|
3058
3054
|
pkCol !== undefined &&
|
|
3059
3055
|
conflictColumns.length === 1 &&
|
|
3060
3056
|
conflictColumns[0] === pkCol &&
|
|
3057
|
+
upsertWhereMatchesCreate(this.meta, upsertWhere, (args.create ?? {})) &&
|
|
3058
|
+
!hasRelationFields(args.create, this.meta) &&
|
|
3059
|
+
!hasRelationFields(updateData, this.meta) &&
|
|
3060
|
+
Object.values(updateData).some((v) => v !== undefined) &&
|
|
3061
3061
|
!filtered;
|
|
3062
3062
|
if (!native) {
|
|
3063
|
-
return this.upsertLookupFirst(
|
|
3063
|
+
return this.upsertLookupFirst(upsertWhere, createData, updateData, {
|
|
3064
3064
|
select: args.select,
|
|
3065
3065
|
omit: args.omit,
|
|
3066
3066
|
skipGlobalFilters: args.skipGlobalFilters,
|
|
3067
|
+
timeout: args.timeout,
|
|
3067
3068
|
});
|
|
3068
3069
|
}
|
|
3069
3070
|
const params = [];
|
|
3070
3071
|
const createBody = this.scalarData(createData)
|
|
3071
3072
|
.map((a) => `${quotePowqlIdent(a.col.name)} := ${this.writeRef(a.value, a.col, params)}`)
|
|
3072
3073
|
.join(', ');
|
|
3073
|
-
const updateBody = this.buildUpdateAssignments(
|
|
3074
|
+
const updateBody = this.buildUpdateAssignments(updateData, params);
|
|
3074
3075
|
// PowDB 0.7.0's `upsert` statement does NOT accept a trailing `returning`
|
|
3075
3076
|
// (verified: "unexpected trailing token … 'returning'"), because it is one
|
|
3076
3077
|
// atomic insert-or-update, not two branches. So upsert alone keeps the
|
|
@@ -3090,48 +3091,42 @@ export class PowqlInterface {
|
|
|
3090
3091
|
});
|
|
3091
3092
|
}
|
|
3092
3093
|
/**
|
|
3093
|
-
*
|
|
3094
|
-
*
|
|
3095
|
-
*
|
|
3096
|
-
*
|
|
3097
|
-
*
|
|
3098
|
-
* safe from concurrent writers; the transaction makes it atomic with the
|
|
3099
|
-
* write.
|
|
3094
|
+
* `upsert` as Prisma defines it, for every shape the native `upsert … on`
|
|
3095
|
+
* statement cannot express: find the row `where` names, update it if it
|
|
3096
|
+
* exists, else insert `create`, in one flat transaction. PowDB's single
|
|
3097
|
+
* writer makes the read-then-write safe from concurrent writers; the
|
|
3098
|
+
* transaction makes it atomic with the write.
|
|
3100
3099
|
*
|
|
3101
|
-
* The
|
|
3102
|
-
*
|
|
3103
|
-
*
|
|
3104
|
-
*
|
|
3105
|
-
*
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3100
|
+
* The lookup is by `where`'s own values. It used to be by `create`'s values
|
|
3101
|
+
* for the conflict columns, which is what the native statement compares, and
|
|
3102
|
+
* so answered a different question whenever the two disagreed: `where: { id }`
|
|
3103
|
+
* with a `create` that leaves the key to the engine inserted a new row every
|
|
3104
|
+
* time. The find and the update run through the transaction's table
|
|
3105
|
+
* interface, so a configured global filter applies to both exactly as it does
|
|
3106
|
+
* to any other read or write, and `skipGlobalFilters` is forwarded to both.
|
|
3107
|
+
*
|
|
3108
|
+
* Both halves are compiled before the lookup, so an unknown field in `update`
|
|
3109
|
+
* is refused whether or not the row exists (the SQL engines do the same).
|
|
3110
|
+
*/
|
|
3111
|
+
async upsertLookupFirst(where, createData, updateData, shape) {
|
|
3112
|
+
const { skipGlobalFilters, timeout, ...projection } = shape;
|
|
3109
3113
|
const skip = skipGlobalFilters === undefined ? {} : { skipGlobalFilters };
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
const
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
const keyless = keyPairs.some((p) => p.value == null);
|
|
3117
|
-
const isCompositePk = keyPairs.length > 1 &&
|
|
3118
|
-
keyPairs.length === this.meta.primaryKey.length &&
|
|
3119
|
-
conflictColumns.every((c) => this.meta.primaryKey.includes(c));
|
|
3120
|
-
if (keyless && isCompositePk) {
|
|
3121
|
-
throw new ValidationError(`upsert on "${this.table}" needs every composite-PK field in \`create\` (${keyPairs
|
|
3122
|
-
.map((p) => p.field)
|
|
3123
|
-
.join(', ')}).`);
|
|
3124
|
-
}
|
|
3125
|
-
const keyWhere = Object.fromEntries(keyPairs.map((p) => [p.field, p.value]));
|
|
3114
|
+
const time = timeout === undefined ? {} : { timeout };
|
|
3115
|
+
this.scalarData(extractRelationFields(createData, this.meta).scalars);
|
|
3116
|
+
const scalarUpdate = extractRelationFields(updateData, this.meta).scalars;
|
|
3117
|
+
if (Object.values(scalarUpdate).some((v) => v !== undefined))
|
|
3118
|
+
this.buildUpdateAssignments(scalarUpdate, []);
|
|
3119
|
+
const updateIsEmpty = Object.values(updateData).every((v) => v === undefined);
|
|
3126
3120
|
const run = async (ctx) => {
|
|
3127
3121
|
const tbl = ctx.tx.table(this.table);
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3122
|
+
const existing = await tbl.findUnique({ where, ...projection, ...skip, ...time });
|
|
3123
|
+
if (existing) {
|
|
3124
|
+
// An empty `update` means "leave it as it is": the row found is the answer.
|
|
3125
|
+
if (updateIsEmpty)
|
|
3126
|
+
return existing;
|
|
3127
|
+
return (await tbl.update({ where, data: updateData, ...projection, ...skip, ...time }));
|
|
3128
|
+
}
|
|
3129
|
+
return (await tbl.create({ data: createData, ...projection, ...time }));
|
|
3135
3130
|
};
|
|
3136
3131
|
return this.isTxScoped() ? run(this.buildNestedCtx()) : this.runInImplicitTx(run);
|
|
3137
3132
|
}
|
package/dist/prisma-compat.d.ts
CHANGED
|
@@ -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[]>;
|
package/dist/prisma-compat.js
CHANGED
|
@@ -1738,53 +1738,6 @@ function hasNestedKeys(ctx, mm, data) {
|
|
|
1738
1738
|
return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date);
|
|
1739
1739
|
});
|
|
1740
1740
|
}
|
|
1741
|
-
/**
|
|
1742
|
-
* Whether an upsert's translated `where` key values all equal the
|
|
1743
|
-
* corresponding `create` values. When they do (the common Prisma idiom), the
|
|
1744
|
-
* native single-statement ON CONFLICT upsert is semantically identical to
|
|
1745
|
-
* Prisma's lookup-first and stays atomic. When they differ, native upsert
|
|
1746
|
-
* would insert the `create` row even though the `where` row exists, so the
|
|
1747
|
-
* adapter must emulate lookup-first instead.
|
|
1748
|
-
*/
|
|
1749
|
-
function upsertKeysMatch(t) {
|
|
1750
|
-
const where = t.where;
|
|
1751
|
-
const create = t.create;
|
|
1752
|
-
if (!isPlainObject(where) || !isPlainObject(create))
|
|
1753
|
-
return false;
|
|
1754
|
-
const scalarEq = (a, b) => {
|
|
1755
|
-
if (a instanceof Date || b instanceof Date) {
|
|
1756
|
-
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
1757
|
-
}
|
|
1758
|
-
return a === b;
|
|
1759
|
-
};
|
|
1760
|
-
for (const [k, v] of Object.entries(where)) {
|
|
1761
|
-
if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
|
|
1762
|
-
// Compound-unique selector object: every member must scalar-match create.
|
|
1763
|
-
if (Array.isArray(v))
|
|
1764
|
-
return false;
|
|
1765
|
-
for (const [mk, mv] of Object.entries(v)) {
|
|
1766
|
-
if (mv !== null && typeof mv === 'object' && !(mv instanceof Date))
|
|
1767
|
-
return false;
|
|
1768
|
-
if (!scalarEq(mv, create[mk]))
|
|
1769
|
-
return false;
|
|
1770
|
-
}
|
|
1771
|
-
continue;
|
|
1772
|
-
}
|
|
1773
|
-
if (!scalarEq(v, create[k]))
|
|
1774
|
-
return false;
|
|
1775
|
-
}
|
|
1776
|
-
return true;
|
|
1777
|
-
}
|
|
1778
|
-
/** Prisma upsert semantics: look up by where; update the found row, else insert create. */
|
|
1779
|
-
async function upsertLookupFirst(qi, t) {
|
|
1780
|
-
const existing = await qi.findUnique({ where: t.where });
|
|
1781
|
-
// The projection rides along so both branches return what the native upsert
|
|
1782
|
-
// path returns (an explicitly selected PII field included).
|
|
1783
|
-
const shape = { select: t.select, omit: t.omit };
|
|
1784
|
-
if (existing)
|
|
1785
|
-
return qi.update({ where: t.where, data: t.update, ...shape });
|
|
1786
|
-
return qi.create({ data: t.create, ...shape });
|
|
1787
|
-
}
|
|
1788
1741
|
/**
|
|
1789
1742
|
* The row shapes a translated `createMany` has to insert, as contiguous runs
|
|
1790
1743
|
* that each name the same fields (see {@link createManyShapeRuns}).
|
|
@@ -1846,7 +1799,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
1846
1799
|
reshape: batch.reshape,
|
|
1847
1800
|
nested: () => {
|
|
1848
1801
|
try {
|
|
1849
|
-
return batch.nested?.(translate()) ?? false;
|
|
1802
|
+
return batch.nested?.(translate(), getQI()) ?? false;
|
|
1850
1803
|
}
|
|
1851
1804
|
catch {
|
|
1852
1805
|
return false; // let the build path surface the translation error consistently
|
|
@@ -2008,24 +1961,17 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
|
2008
1961
|
};
|
|
2009
1962
|
applyNativeOptions(UPSERT_OPTIONS, a, t);
|
|
2010
1963
|
return t;
|
|
2011
|
-
},
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
return qi.upsert(t).then(shape);
|
|
2017
|
-
}
|
|
2018
|
-
return runInTx(async (table) => shape(await upsertLookupFirst(table(mm.table), t)));
|
|
2019
|
-
}, {
|
|
1964
|
+
},
|
|
1965
|
+
// Core `upsert` has Prisma's semantics itself (it looks the row up by
|
|
1966
|
+
// `where` whenever one conflict statement would mean something else,
|
|
1967
|
+
// and runs nested writes), so the adapter passes it straight through.
|
|
1968
|
+
(qi, t) => qi.upsert(t).then(shape), {
|
|
2020
1969
|
build: (qi, t) => qi.buildUpsert(t),
|
|
2021
1970
|
reshape: shape,
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
}
|
|
2027
|
-
return shape(await upsertLookupFirst(table(mm.table), t));
|
|
2028
|
-
},
|
|
1971
|
+
// Batchable only when core would run it as one statement. PowDB has
|
|
1972
|
+
// no deferred statements at all, so it always takes the tx path.
|
|
1973
|
+
nested: (t, qi) => qi.upsertNeedsLookup?.(t) ?? true,
|
|
1974
|
+
execInTx: async (table, t) => shape(await table(mm.table).upsert(t)),
|
|
2029
1975
|
});
|
|
2030
1976
|
},
|
|
2031
1977
|
count: (args = {}) => defer('count', args, () => {
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -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
|
}>;
|
package/dist/query/builder.js
CHANGED
|
@@ -15,11 +15,11 @@ import { settleEventLoop } from '../connection-guard.js';
|
|
|
15
15
|
import { postgresDialect } from '../dialect.js';
|
|
16
16
|
import { explainConnectionLoss, isStaleConnectionError, NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from '../errors.js';
|
|
17
17
|
import { missingIndexForRelation, schemaHasIndexInfo } from '../index-advisor.js';
|
|
18
|
-
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from '../nested-write.js';
|
|
18
|
+
import { executeNestedCreate, executeNestedUpdate, extractRelationFields, hasRelationFields, } from '../nested-write.js';
|
|
19
19
|
import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
20
20
|
import * as aggMod from './aggregates.js';
|
|
21
21
|
import { assertProjectionShape, defaultProjectionFields, includeKeysForBatching, loadRelationsBatched, neededParentKeyFields, rejectNestedPickOrder, resolveCountRelations, stripFields, } from './batched-loader.js';
|
|
22
|
-
import { assertWhereIdentifiesOneRow, expandCompoundUniqueWhere } from './compound-unique.js';
|
|
22
|
+
import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, } from './compound-unique.js';
|
|
23
23
|
import { dedupeColumnList, dedupeOrderEntries, isEmptyOrderBy, isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
|
|
24
24
|
import { warnUnknownQueryOptions } from './option-surface.js';
|
|
25
25
|
import { applyStableRelationOrderTo, normalizeWithClause } from './relation-names.js';
|
|
@@ -3600,10 +3600,78 @@ export class QueryInterface {
|
|
|
3600
3600
|
// -------------------------------------------------------------------------
|
|
3601
3601
|
async upsert(args) {
|
|
3602
3602
|
return this.executeWithMiddleware('upsert', args, async () => {
|
|
3603
|
+
if (this.upsertNeedsLookup(args)) {
|
|
3604
|
+
return this.upsertLookupFirst(args);
|
|
3605
|
+
}
|
|
3603
3606
|
const deferred = this.buildUpsert(args);
|
|
3604
3607
|
return this.executeMutation(deferred, args.timeout);
|
|
3605
3608
|
});
|
|
3606
3609
|
}
|
|
3610
|
+
/**
|
|
3611
|
+
* @internal Whether `upsert()` looks the row up rather than running one
|
|
3612
|
+
* statement, which is also exactly when `buildUpsert` refuses these args.
|
|
3613
|
+
* prisma-compat reads it to decide whether an array `$transaction` can batch
|
|
3614
|
+
* an upsert or has to run the array inside a transaction.
|
|
3615
|
+
*/
|
|
3616
|
+
upsertNeedsLookup(args) {
|
|
3617
|
+
return (hasRelationFields((args.create ?? {}), this.tableMeta) ||
|
|
3618
|
+
hasRelationFields((args.update ?? {}), this.tableMeta) ||
|
|
3619
|
+
writesMod.upsertLookupReason(this.ctx, args) !== null);
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* `upsert` as Prisma defines it: find the row `where` names, update it if it
|
|
3623
|
+
* exists, else insert `create`, in one transaction (the caller's, when there
|
|
3624
|
+
* is one). Taken whenever the single conflict statement would mean something
|
|
3625
|
+
* else (`upsertLookupReason`: `where` not pinned to `create`'s values, an
|
|
3626
|
+
* empty `update`, a global filter the engine's statement cannot carry) and
|
|
3627
|
+
* whenever `create` or `update` holds nested relation writes, which only the
|
|
3628
|
+
* nested-write engine can run.
|
|
3629
|
+
*
|
|
3630
|
+
* Validation is DATA-INDEPENDENT, as it is for the batched relation loader:
|
|
3631
|
+
* both halves are compiled before the lookup runs, so an unknown field in
|
|
3632
|
+
* `update` is refused whether or not the row exists, and a call is never
|
|
3633
|
+
* valid on one row and invalid on the next. Only the scalar part of each half
|
|
3634
|
+
* is compiled here; nested relation writes are validated by the engine that
|
|
3635
|
+
* runs them.
|
|
3636
|
+
*
|
|
3637
|
+
* Not atomic against a concurrent insert of the same key, exactly as Prisma's
|
|
3638
|
+
* own lookup is not: two callers can both miss, and the second insert then
|
|
3639
|
+
* fails on the unique constraint (E008) when `create` carries the key. It
|
|
3640
|
+
* never writes two rows over one key the database enforces.
|
|
3641
|
+
*/
|
|
3642
|
+
async upsertLookupFirst(args) {
|
|
3643
|
+
const where = (args.where ?? {});
|
|
3644
|
+
const create = (args.create ?? {});
|
|
3645
|
+
const update = (args.update ?? {});
|
|
3646
|
+
assertMutationWhereIdentifiesOneRow(this.tableMeta, this.table, expandCompoundUniqueWhere(this.tableMeta, where), 'upsert');
|
|
3647
|
+
this.resolveWriteProjection(args);
|
|
3648
|
+
// An undefined option is an absent one on every call below.
|
|
3649
|
+
const skip = { skipGlobalFilters: args.skipGlobalFilters };
|
|
3650
|
+
const shape = { select: args.select, omit: args.omit, timeout: args.timeout };
|
|
3651
|
+
const scalarCreate = extractRelationFields(create, this.tableMeta).scalars;
|
|
3652
|
+
const scalarUpdate = extractRelationFields(update, this.tableMeta).scalars;
|
|
3653
|
+
const updateIsEmpty = Object.values(update).every((v) => v === undefined);
|
|
3654
|
+
if (Object.keys(scalarCreate).length > 0)
|
|
3655
|
+
this.buildCreate({ data: scalarCreate });
|
|
3656
|
+
if (Object.values(scalarUpdate).some((v) => v !== undefined)) {
|
|
3657
|
+
this.buildUpdate({ where, data: scalarUpdate, ...skip });
|
|
3658
|
+
}
|
|
3659
|
+
const run = async (table) => {
|
|
3660
|
+
const found = await table.findUnique({ where, ...shape, ...skip });
|
|
3661
|
+
if (found) {
|
|
3662
|
+
// Prisma reads an empty `update` as "leave it as it is": the row found
|
|
3663
|
+
// IS the answer, projected as the write would have projected it.
|
|
3664
|
+
if (updateIsEmpty)
|
|
3665
|
+
return found;
|
|
3666
|
+
return (await table.update({ where, data: update, ...shape, ...skip }));
|
|
3667
|
+
}
|
|
3668
|
+
return (await table.create({ data: create, ...shape }));
|
|
3669
|
+
};
|
|
3670
|
+
if (this.txScoped) {
|
|
3671
|
+
return run(this.buildNestedCtx().tx.table(this.table));
|
|
3672
|
+
}
|
|
3673
|
+
return this.runInImplicitTx((ctx) => run(ctx.tx.table(this.table)));
|
|
3674
|
+
}
|
|
3607
3675
|
// -------------------------------------------------------------------------
|
|
3608
3676
|
// updateMany, UPDATE ... WHERE ... returning count
|
|
3609
3677
|
// -------------------------------------------------------------------------
|
|
@@ -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;
|
|
@@ -420,6 +420,60 @@ export function whereIdentifiesOneRow(meta, where) {
|
|
|
420
420
|
return false;
|
|
421
421
|
return uniqueColumnSets(meta).some((cols) => cols.every((c) => pinned.has(c)));
|
|
422
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Whether an `upsert` means the same thing as ONE conflict statement.
|
|
425
|
+
*
|
|
426
|
+
* An upsert's contract is Prisma's: find the row `where` names; update it if
|
|
427
|
+
* it exists, else insert `create`. A single `INSERT ... ON CONFLICT (<where
|
|
428
|
+
* keys>)` (or PowDB's `upsert ... on`) compares the conflict columns against
|
|
429
|
+
* the values being INSERTED, which are `create`'s, and never reads `where`'s
|
|
430
|
+
* values at all. The two agree exactly when every `where` key is pinned to the
|
|
431
|
+
* value `create` carries for that column. Otherwise the statement answers a
|
|
432
|
+
* different question: `where: { id: 5 }` with a `create` that leaves `id` to
|
|
433
|
+
* its sequence inserted a new row every time and never touched row 5, and
|
|
434
|
+
* `where: { email: a }` with `create: { email: b }` updated the row holding
|
|
435
|
+
* `b`, silently.
|
|
436
|
+
*
|
|
437
|
+
* Shared by every engine, like the one-row rule above: each one decides here
|
|
438
|
+
* whether it may take its single-statement fast path, and looks the row up by
|
|
439
|
+
* `where` first when it may not. `false` is always a SAFE answer (the lookup
|
|
440
|
+
* path is correct for every shape, only slower), so anything this does not
|
|
441
|
+
* recognise as a plain matching value answers `false`: an operator other than a
|
|
442
|
+
* bare `equals`, a different object (two Dates compare by instant), a key that
|
|
443
|
+
* names no column, and NULL, which a unique index never treats as equal to
|
|
444
|
+
* anything.
|
|
445
|
+
*/
|
|
446
|
+
export function upsertWhereMatchesCreate(meta, where, create) {
|
|
447
|
+
const createByColumn = new Map();
|
|
448
|
+
for (const [key, value] of Object.entries(create)) {
|
|
449
|
+
if (value === undefined)
|
|
450
|
+
continue;
|
|
451
|
+
const column = resolveColumnName(meta, key);
|
|
452
|
+
if (column !== undefined)
|
|
453
|
+
createByColumn.set(column, value);
|
|
454
|
+
}
|
|
455
|
+
let keys = 0;
|
|
456
|
+
for (const [key, raw] of Object.entries(expandCompoundUniqueWhere(meta, where))) {
|
|
457
|
+
if (raw === undefined)
|
|
458
|
+
continue;
|
|
459
|
+
const column = resolveColumnName(meta, key);
|
|
460
|
+
if (column === undefined || !createByColumn.has(column))
|
|
461
|
+
return false;
|
|
462
|
+
const value = isBareEquals(raw) ? raw.equals : raw;
|
|
463
|
+
if (!sameKeyValue(value, createByColumn.get(column)))
|
|
464
|
+
return false;
|
|
465
|
+
keys++;
|
|
466
|
+
}
|
|
467
|
+
return keys > 0;
|
|
468
|
+
}
|
|
469
|
+
/** `{ equals: v }` and nothing else (a `mode` beside it changes the match). */
|
|
470
|
+
function isBareEquals(value) {
|
|
471
|
+
return isPlainObject(value) && Object.keys(value).length === 1 && 'equals' in value;
|
|
472
|
+
}
|
|
473
|
+
/** Equal non-null key values; two Dates compare by instant, other objects by identity. */
|
|
474
|
+
function sameKeyValue(a, b) {
|
|
475
|
+
return a != null && (a instanceof Date && b instanceof Date ? a.getTime() === b.getTime() : a === b);
|
|
476
|
+
}
|
|
423
477
|
/** A bare value, or an operator object whose `equals` is a value. */
|
|
424
478
|
function isPinnedToOneValue(value) {
|
|
425
479
|
if (value === undefined || value === null)
|
package/dist/query/writes.d.ts
CHANGED
|
@@ -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;
|