turbine-orm 0.43.0 → 0.44.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.
@@ -153,7 +153,15 @@ function resolvePrismaSchema(ast, schema, options = {}) {
153
153
  if (c.status === 'resolved' && c.turbineFields)
154
154
  compoundUniques[c.selector] = c.turbineFields;
155
155
  }
156
- result.map.models[model.name] = { table, accessor, fields, relations, compoundUniques };
156
+ const clientDefaults = collectClientDefaults(model, fields, resolvedSchema?.tables[table]);
157
+ result.map.models[model.name] = {
158
+ table,
159
+ accessor,
160
+ fields,
161
+ relations,
162
+ compoundUniques,
163
+ ...(clientDefaults ? { clientDefaults } : {}),
164
+ };
157
165
  }
158
166
  }
159
167
  // Enums.
@@ -236,6 +244,42 @@ function relationFkColumns(model, field) {
236
244
  const fieldsArg = relAttr?.args.find((a) => a.key === 'fields' && a.kind === 'array');
237
245
  return fieldsArg?.items?.map((pf) => fieldColumn(model, pf)) ?? null;
238
246
  }
247
+ /**
248
+ * Prisma CLIENT-side defaults for a resolved model: `@default(uuid())` /
249
+ * `@default(cuid())` / `@updatedAt` are filled by the Prisma client (the
250
+ * database column typically has NO default), so migrated call sites omit those
251
+ * fields and a plain insert violates NOT NULL. `@default(now())` is normally a
252
+ * database default, so it is carried only when the introspected column has no
253
+ * default. `@updatedAt` is always carried (Prisma touches it on every update
254
+ * regardless of any database default). Returns undefined when the model has
255
+ * none, keeping the emitted map byte-identical for unaffected schemas.
256
+ */
257
+ function collectClientDefaults(model, resolvedFields, tableMeta) {
258
+ if (!tableMeta)
259
+ return undefined;
260
+ const out = {};
261
+ for (const field of model.fields) {
262
+ const turbineField = resolvedFields[field.name];
263
+ if (!turbineField)
264
+ continue;
265
+ const col = tableMeta.columns.find((c) => c.field === turbineField || c.name === turbineField);
266
+ if (field.attrs.some((a) => a.name === 'updatedAt')) {
267
+ out[field.name] = 'updatedAt';
268
+ continue;
269
+ }
270
+ if (col?.hasDefault)
271
+ continue; // the database fills it; nothing to emulate
272
+ const def = field.attrs.find((a) => a.name === 'default');
273
+ const raw = def?.args.find((a) => a.key === undefined && a.kind === 'raw')?.value ?? '';
274
+ if (/^uuid\s*\(/.test(raw))
275
+ out[field.name] = 'uuid';
276
+ else if (/^cuid\s*\(/.test(raw))
277
+ out[field.name] = 'cuid';
278
+ else if (/^now\s*\(\s*\)$/.test(raw))
279
+ out[field.name] = 'now';
280
+ }
281
+ return Object.keys(out).length > 0 ? out : undefined;
282
+ }
239
283
  /**
240
284
  * FK columns for an INVERSE relation field (one carrying no `fields: [...]`),
241
285
  * derived by @relation("Name") pairing: the opposing model's same-named field
@@ -956,6 +956,9 @@ function generatePrismaMap(map, options) {
956
956
  }
957
957
  lines.push(' },');
958
958
  lines.push(` compoundUniques: ${serializeStringArrayRecord(model.compoundUniques)},`);
959
+ if (model.clientDefaults && Object.keys(model.clientDefaults).length > 0) {
960
+ lines.push(` clientDefaults: ${serializeStringRecord(model.clientDefaults)},`);
961
+ }
959
962
  lines.push(' },');
960
963
  }
961
964
  lines.push(' },');
@@ -872,7 +872,118 @@ function prismaPropertyAlias(model) {
872
872
  * QueryInterface for this model's table on the active connection (the base pool,
873
873
  * or a transaction's connection inside `$transaction(callback)`).
874
874
  */
875
- function makeDelegate(ctx, mm, getQI) {
875
+ // ---------------------------------------------------------------------------
876
+ // Prisma client-side defaults (@default(uuid()/cuid()) / @updatedAt / now())
877
+ // ---------------------------------------------------------------------------
878
+ let cuidCounter = Math.floor(Math.random() * 1296);
879
+ /**
880
+ * A cuid-shaped id ('c' + timestamp + counter + fingerprint + random, 25
881
+ * chars): collision-resistant and format-compatible with Prisma's
882
+ * `@default(cuid())` call sites. Not byte-identical to any specific cuid
883
+ * library; Prisma treats these as opaque unique strings.
884
+ */
885
+ function makeCuid() {
886
+ const ts = Date.now().toString(36);
887
+ const count = (cuidCounter++ % 1296).toString(36).padStart(2, '0');
888
+ const rand = () => Math.floor(Math.random() * 36 ** 4)
889
+ .toString(36)
890
+ .padStart(4, '0');
891
+ return `c${ts}${count}${rand()}${rand()}${rand()}`.slice(0, 25);
892
+ }
893
+ function clientDefaultValue(kind) {
894
+ if (kind === 'uuid')
895
+ return globalThis.crypto.randomUUID();
896
+ if (kind === 'cuid')
897
+ return makeCuid();
898
+ return new Date();
899
+ }
900
+ /** Fill missing create-side client defaults (uuid / cuid / now / updatedAt), by Prisma field name. */
901
+ function applyCreateDefaults(mm, data) {
902
+ const cd = mm.clientDefaults;
903
+ if (!cd || !isPlainObject(data))
904
+ return data;
905
+ let out = data;
906
+ for (const [field, kind] of Object.entries(cd)) {
907
+ if (out[field] === undefined) {
908
+ if (out === data)
909
+ out = { ...data };
910
+ out[field] = clientDefaultValue(kind);
911
+ }
912
+ }
913
+ return out;
914
+ }
915
+ /** Touch @updatedAt fields on the update side (Prisma sets them on every update). */
916
+ function applyUpdateTouch(mm, data) {
917
+ const cd = mm.clientDefaults;
918
+ if (!cd || !isPlainObject(data))
919
+ return data;
920
+ let out = data;
921
+ for (const [field, kind] of Object.entries(cd)) {
922
+ if (kind === 'updatedAt' && out[field] === undefined) {
923
+ if (out === data)
924
+ out = { ...data };
925
+ out[field] = new Date();
926
+ }
927
+ }
928
+ return out;
929
+ }
930
+ /** Whether TRANSLATED write data carries relation keys (nested-write shapes). */
931
+ function hasNestedKeys(ctx, mm, data) {
932
+ const rels = ctx.schema.tables[mm.table]?.relations;
933
+ if (!rels || !isPlainObject(data))
934
+ return false;
935
+ return Object.keys(data).some((k) => {
936
+ if (!Object.hasOwn(rels, k))
937
+ return false;
938
+ const v = data[k];
939
+ return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date);
940
+ });
941
+ }
942
+ /**
943
+ * Whether an upsert's translated `where` key values all equal the
944
+ * corresponding `create` values. When they do (the common Prisma idiom), the
945
+ * native single-statement ON CONFLICT upsert is semantically identical to
946
+ * Prisma's lookup-first and stays atomic. When they differ, native upsert
947
+ * would insert the `create` row even though the `where` row exists, so the
948
+ * adapter must emulate lookup-first instead.
949
+ */
950
+ function upsertKeysMatch(t) {
951
+ const where = t.where;
952
+ const create = t.create;
953
+ if (!isPlainObject(where) || !isPlainObject(create))
954
+ return false;
955
+ const scalarEq = (a, b) => {
956
+ if (a instanceof Date || b instanceof Date) {
957
+ return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
958
+ }
959
+ return a === b;
960
+ };
961
+ for (const [k, v] of Object.entries(where)) {
962
+ if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
963
+ // Compound-unique selector object: every member must scalar-match create.
964
+ if (Array.isArray(v))
965
+ return false;
966
+ for (const [mk, mv] of Object.entries(v)) {
967
+ if (mv !== null && typeof mv === 'object' && !(mv instanceof Date))
968
+ return false;
969
+ if (!scalarEq(mv, create[mk]))
970
+ return false;
971
+ }
972
+ continue;
973
+ }
974
+ if (!scalarEq(v, create[k]))
975
+ return false;
976
+ }
977
+ return true;
978
+ }
979
+ /** Prisma upsert semantics: look up by where; update the found row, else insert create. */
980
+ async function upsertLookupFirst(qi, t) {
981
+ const existing = await qi.findUnique({ where: t.where });
982
+ if (existing)
983
+ return qi.update({ where: t.where, data: t.update });
984
+ return qi.create({ data: t.create });
985
+ }
986
+ function makeDelegate(ctx, mm, getQI, runInTx) {
876
987
  const pe = ctx.options.prismaErrorCodes;
877
988
  // Build a lazy Prisma-style promise for one delegate call. Crucially, the
878
989
  // Prisma-arg `translate` step runs INSIDE the deferred paths (the run closure
@@ -885,7 +996,27 @@ function makeDelegate(ctx, mm, getQI) {
885
996
  // catches the same throw from `batch.build`.
886
997
  const defer = (translate, run, batch) => {
887
998
  const batchable = batch
888
- ? { build: () => batch.build(getQI(), translate()), reshape: batch.reshape }
999
+ ? {
1000
+ build: () => batch.build(getQI(), translate()),
1001
+ reshape: batch.reshape,
1002
+ nested: () => {
1003
+ try {
1004
+ return batch.nested?.(translate()) ?? false;
1005
+ }
1006
+ catch {
1007
+ return false; // let the build path surface the translation error consistently
1008
+ }
1009
+ },
1010
+ execInTx: async (table) => {
1011
+ try {
1012
+ const t = translate();
1013
+ return batch.execInTx ? await batch.execInTx(table, t) : await run(table(mm.table), t);
1014
+ }
1015
+ catch (err) {
1016
+ throw decorate(err, pe);
1017
+ }
1018
+ },
1019
+ }
889
1020
  : undefined;
890
1021
  return new CompatPromise(async () => {
891
1022
  try {
@@ -909,14 +1040,20 @@ function makeDelegate(ctx, mm, getQI) {
909
1040
  findFirstOrThrow: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
910
1041
  findUniqueOrThrow: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow')), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
911
1042
  create: (args) => defer(() => {
912
- const t = { data: translateWriteData(ctx, mm, args.data) };
1043
+ const t = { data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)) };
913
1044
  if (typeof args.timeout === 'number')
914
1045
  t.timeout = args.timeout;
915
1046
  return t;
916
- }, (qi, t) => qi.create(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildCreate(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1047
+ }, (qi, t) => qi.create(t).then((r) => reshapeRow(ctx, mm, r)), {
1048
+ build: (qi, t) => qi.buildCreate(t),
1049
+ reshape: (raw) => reshapeRow(ctx, mm, raw),
1050
+ nested: (t) => hasNestedKeys(ctx, mm, t.data),
1051
+ }),
917
1052
  createMany: (args) => defer(() => {
918
1053
  const data = args.data;
919
- const rows = Array.isArray(data) ? data.map((d) => translateWriteData(ctx, mm, d)) : [];
1054
+ const rows = Array.isArray(data)
1055
+ ? data.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)))
1056
+ : [];
920
1057
  const t = { data: rows };
921
1058
  if (args.skipDuplicates)
922
1059
  t.skipDuplicates = true;
@@ -924,11 +1061,21 @@ function makeDelegate(ctx, mm, getQI) {
924
1061
  }, (qi, t) => qi.createMany(t).then((r) => ({ count: r.length })), { build: (qi, t) => qi.buildCreateMany(t), reshape: (raw) => ({ count: raw.length }) }),
925
1062
  update: (args) => defer(() => {
926
1063
  const a = requireWhere(args, 'update');
927
- return { where: translateWhere(ctx, mm, a.where), data: translateWriteData(ctx, mm, a.data) };
928
- }, (qi, t) => qi.update(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildUpdate(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1064
+ return {
1065
+ where: translateWhere(ctx, mm, a.where),
1066
+ data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
1067
+ };
1068
+ }, (qi, t) => qi.update(t).then((r) => reshapeRow(ctx, mm, r)), {
1069
+ build: (qi, t) => qi.buildUpdate(t),
1070
+ reshape: (raw) => reshapeRow(ctx, mm, raw),
1071
+ nested: (t) => hasNestedKeys(ctx, mm, t.data),
1072
+ }),
929
1073
  updateMany: (args) => defer(() => {
930
1074
  const a = args;
931
- const t = { where: translateWhere(ctx, mm, a.where ?? {}), data: translateWriteData(ctx, mm, a.data) };
1075
+ const t = {
1076
+ where: translateWhere(ctx, mm, a.where ?? {}),
1077
+ data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
1078
+ };
932
1079
  if (a.where === undefined)
933
1080
  t.allowFullTableScan = true;
934
1081
  return t;
@@ -948,10 +1095,31 @@ function makeDelegate(ctx, mm, getQI) {
948
1095
  const a = requireWhere(args, 'upsert');
949
1096
  return {
950
1097
  where: translateWhere(ctx, mm, a.where),
951
- create: translateWriteData(ctx, mm, a.create),
952
- update: translateWriteData(ctx, mm, a.update),
1098
+ create: translateWriteData(ctx, mm, applyCreateDefaults(mm, a.create)),
1099
+ update: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.update)),
953
1100
  };
954
- }, (qi, t) => qi.upsert(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildUpsert(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1101
+ }, (qi, t) => {
1102
+ // Native ON CONFLICT upsert is only Prisma-equivalent when the where
1103
+ // key values equal the create values AND no nested write data is
1104
+ // present; otherwise emulate Prisma's lookup-first atomically.
1105
+ if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
1106
+ return qi.upsert(t).then((r) => reshapeRow(ctx, mm, r));
1107
+ }
1108
+ return runInTx(async (table) => {
1109
+ const row = await upsertLookupFirst(table(mm.table), t);
1110
+ return reshapeRow(ctx, mm, row);
1111
+ });
1112
+ }, {
1113
+ build: (qi, t) => qi.buildUpsert(t),
1114
+ reshape: (raw) => reshapeRow(ctx, mm, raw),
1115
+ nested: (t) => !upsertKeysMatch(t) || hasNestedKeys(ctx, mm, t.create) || hasNestedKeys(ctx, mm, t.update),
1116
+ execInTx: async (table, t) => {
1117
+ if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
1118
+ return reshapeRow(ctx, mm, await table(mm.table).upsert(t));
1119
+ }
1120
+ return reshapeRow(ctx, mm, await upsertLookupFirst(table(mm.table), t));
1121
+ },
1122
+ }),
955
1123
  count: (args = {}) => defer(() => {
956
1124
  const t = {};
957
1125
  if (args.where !== undefined)
@@ -1046,7 +1214,7 @@ function createPrismaCompatClient(client, map, options = {}) {
1046
1214
  // Delegates bound to the base client (each call reads db.table(...) lazily).
1047
1215
  const delegates = new Map();
1048
1216
  for (const [prismaModel, mm] of Object.entries(map.models)) {
1049
- delegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table)));
1217
+ delegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table), (fn) => db.$transaction((tx) => fn((n) => tx.table(n)))));
1050
1218
  }
1051
1219
  const ph = placeholderOf(db);
1052
1220
  const runRaw = async (text, params) => {
@@ -1073,6 +1241,18 @@ function createPrismaCompatClient(client, map, options = {}) {
1073
1241
  }
1074
1242
  return b;
1075
1243
  });
1244
+ // Nested write data (or a lookup-first upsert) cannot run as a
1245
+ // single deferred statement. Prisma's array form still supports
1246
+ // those, so fall back to running the WHOLE array sequentially
1247
+ // inside one transaction; ordering and atomicity are preserved.
1248
+ if (batchables.some((b) => b.nested())) {
1249
+ return await db.$transaction(async (tx) => {
1250
+ const out = [];
1251
+ for (const b of batchables)
1252
+ out.push(await b.execInTx((n) => tx.table(n)));
1253
+ return out;
1254
+ }, txOptions);
1255
+ }
1076
1256
  const deferreds = batchables.map((b) => b.build());
1077
1257
  const results = (await db.$transaction(deferreds));
1078
1258
  return results.map((raw, i) => batchables[i].reshape(raw));
@@ -1087,7 +1267,7 @@ function createPrismaCompatClient(client, map, options = {}) {
1087
1267
  return db.$transaction((tx) => {
1088
1268
  const txDelegates = {};
1089
1269
  for (const [prismaModel, mm] of Object.entries(map.models)) {
1090
- txDelegates[prismaModel] = makeDelegate(ctx, mm, () => tx.table(mm.table));
1270
+ txDelegates[prismaModel] = makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n)));
1091
1271
  const alias = prismaPropertyAlias(prismaModel);
1092
1272
  if (alias && !(alias in map.models) && !(alias in txDelegates)) {
1093
1273
  txDelegates[alias] = txDelegates[prismaModel];
@@ -148,7 +148,15 @@ export function resolvePrismaSchema(ast, schema, options = {}) {
148
148
  if (c.status === 'resolved' && c.turbineFields)
149
149
  compoundUniques[c.selector] = c.turbineFields;
150
150
  }
151
- result.map.models[model.name] = { table, accessor, fields, relations, compoundUniques };
151
+ const clientDefaults = collectClientDefaults(model, fields, resolvedSchema?.tables[table]);
152
+ result.map.models[model.name] = {
153
+ table,
154
+ accessor,
155
+ fields,
156
+ relations,
157
+ compoundUniques,
158
+ ...(clientDefaults ? { clientDefaults } : {}),
159
+ };
152
160
  }
153
161
  }
154
162
  // Enums.
@@ -231,6 +239,42 @@ function relationFkColumns(model, field) {
231
239
  const fieldsArg = relAttr?.args.find((a) => a.key === 'fields' && a.kind === 'array');
232
240
  return fieldsArg?.items?.map((pf) => fieldColumn(model, pf)) ?? null;
233
241
  }
242
+ /**
243
+ * Prisma CLIENT-side defaults for a resolved model: `@default(uuid())` /
244
+ * `@default(cuid())` / `@updatedAt` are filled by the Prisma client (the
245
+ * database column typically has NO default), so migrated call sites omit those
246
+ * fields and a plain insert violates NOT NULL. `@default(now())` is normally a
247
+ * database default, so it is carried only when the introspected column has no
248
+ * default. `@updatedAt` is always carried (Prisma touches it on every update
249
+ * regardless of any database default). Returns undefined when the model has
250
+ * none, keeping the emitted map byte-identical for unaffected schemas.
251
+ */
252
+ function collectClientDefaults(model, resolvedFields, tableMeta) {
253
+ if (!tableMeta)
254
+ return undefined;
255
+ const out = {};
256
+ for (const field of model.fields) {
257
+ const turbineField = resolvedFields[field.name];
258
+ if (!turbineField)
259
+ continue;
260
+ const col = tableMeta.columns.find((c) => c.field === turbineField || c.name === turbineField);
261
+ if (field.attrs.some((a) => a.name === 'updatedAt')) {
262
+ out[field.name] = 'updatedAt';
263
+ continue;
264
+ }
265
+ if (col?.hasDefault)
266
+ continue; // the database fills it; nothing to emulate
267
+ const def = field.attrs.find((a) => a.name === 'default');
268
+ const raw = def?.args.find((a) => a.key === undefined && a.kind === 'raw')?.value ?? '';
269
+ if (/^uuid\s*\(/.test(raw))
270
+ out[field.name] = 'uuid';
271
+ else if (/^cuid\s*\(/.test(raw))
272
+ out[field.name] = 'cuid';
273
+ else if (/^now\s*\(\s*\)$/.test(raw))
274
+ out[field.name] = 'now';
275
+ }
276
+ return Object.keys(out).length > 0 ? out : undefined;
277
+ }
234
278
  /**
235
279
  * FK columns for an INVERSE relation field (one carrying no `fields: [...]`),
236
280
  * derived by @relation("Name") pairing: the opposing model's same-named field
package/dist/generate.js CHANGED
@@ -944,6 +944,9 @@ export function generatePrismaMap(map, options) {
944
944
  }
945
945
  lines.push(' },');
946
946
  lines.push(` compoundUniques: ${serializeStringArrayRecord(model.compoundUniques)},`);
947
+ if (model.clientDefaults && Object.keys(model.clientDefaults).length > 0) {
948
+ lines.push(` clientDefaults: ${serializeStringRecord(model.clientDefaults)},`);
949
+ }
947
950
  lines.push(' },');
948
951
  }
949
952
  lines.push(' },');
@@ -868,7 +868,118 @@ function prismaPropertyAlias(model) {
868
868
  * QueryInterface for this model's table on the active connection (the base pool,
869
869
  * or a transaction's connection inside `$transaction(callback)`).
870
870
  */
871
- function makeDelegate(ctx, mm, getQI) {
871
+ // ---------------------------------------------------------------------------
872
+ // Prisma client-side defaults (@default(uuid()/cuid()) / @updatedAt / now())
873
+ // ---------------------------------------------------------------------------
874
+ let cuidCounter = Math.floor(Math.random() * 1296);
875
+ /**
876
+ * A cuid-shaped id ('c' + timestamp + counter + fingerprint + random, 25
877
+ * chars): collision-resistant and format-compatible with Prisma's
878
+ * `@default(cuid())` call sites. Not byte-identical to any specific cuid
879
+ * library; Prisma treats these as opaque unique strings.
880
+ */
881
+ function makeCuid() {
882
+ const ts = Date.now().toString(36);
883
+ const count = (cuidCounter++ % 1296).toString(36).padStart(2, '0');
884
+ const rand = () => Math.floor(Math.random() * 36 ** 4)
885
+ .toString(36)
886
+ .padStart(4, '0');
887
+ return `c${ts}${count}${rand()}${rand()}${rand()}`.slice(0, 25);
888
+ }
889
+ function clientDefaultValue(kind) {
890
+ if (kind === 'uuid')
891
+ return globalThis.crypto.randomUUID();
892
+ if (kind === 'cuid')
893
+ return makeCuid();
894
+ return new Date();
895
+ }
896
+ /** Fill missing create-side client defaults (uuid / cuid / now / updatedAt), by Prisma field name. */
897
+ function applyCreateDefaults(mm, data) {
898
+ const cd = mm.clientDefaults;
899
+ if (!cd || !isPlainObject(data))
900
+ return data;
901
+ let out = data;
902
+ for (const [field, kind] of Object.entries(cd)) {
903
+ if (out[field] === undefined) {
904
+ if (out === data)
905
+ out = { ...data };
906
+ out[field] = clientDefaultValue(kind);
907
+ }
908
+ }
909
+ return out;
910
+ }
911
+ /** Touch @updatedAt fields on the update side (Prisma sets them on every update). */
912
+ function applyUpdateTouch(mm, data) {
913
+ const cd = mm.clientDefaults;
914
+ if (!cd || !isPlainObject(data))
915
+ return data;
916
+ let out = data;
917
+ for (const [field, kind] of Object.entries(cd)) {
918
+ if (kind === 'updatedAt' && out[field] === undefined) {
919
+ if (out === data)
920
+ out = { ...data };
921
+ out[field] = new Date();
922
+ }
923
+ }
924
+ return out;
925
+ }
926
+ /** Whether TRANSLATED write data carries relation keys (nested-write shapes). */
927
+ function hasNestedKeys(ctx, mm, data) {
928
+ const rels = ctx.schema.tables[mm.table]?.relations;
929
+ if (!rels || !isPlainObject(data))
930
+ return false;
931
+ return Object.keys(data).some((k) => {
932
+ if (!Object.hasOwn(rels, k))
933
+ return false;
934
+ const v = data[k];
935
+ return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date);
936
+ });
937
+ }
938
+ /**
939
+ * Whether an upsert's translated `where` key values all equal the
940
+ * corresponding `create` values. When they do (the common Prisma idiom), the
941
+ * native single-statement ON CONFLICT upsert is semantically identical to
942
+ * Prisma's lookup-first and stays atomic. When they differ, native upsert
943
+ * would insert the `create` row even though the `where` row exists, so the
944
+ * adapter must emulate lookup-first instead.
945
+ */
946
+ function upsertKeysMatch(t) {
947
+ const where = t.where;
948
+ const create = t.create;
949
+ if (!isPlainObject(where) || !isPlainObject(create))
950
+ return false;
951
+ const scalarEq = (a, b) => {
952
+ if (a instanceof Date || b instanceof Date) {
953
+ return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
954
+ }
955
+ return a === b;
956
+ };
957
+ for (const [k, v] of Object.entries(where)) {
958
+ if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
959
+ // Compound-unique selector object: every member must scalar-match create.
960
+ if (Array.isArray(v))
961
+ return false;
962
+ for (const [mk, mv] of Object.entries(v)) {
963
+ if (mv !== null && typeof mv === 'object' && !(mv instanceof Date))
964
+ return false;
965
+ if (!scalarEq(mv, create[mk]))
966
+ return false;
967
+ }
968
+ continue;
969
+ }
970
+ if (!scalarEq(v, create[k]))
971
+ return false;
972
+ }
973
+ return true;
974
+ }
975
+ /** Prisma upsert semantics: look up by where; update the found row, else insert create. */
976
+ async function upsertLookupFirst(qi, t) {
977
+ const existing = await qi.findUnique({ where: t.where });
978
+ if (existing)
979
+ return qi.update({ where: t.where, data: t.update });
980
+ return qi.create({ data: t.create });
981
+ }
982
+ function makeDelegate(ctx, mm, getQI, runInTx) {
872
983
  const pe = ctx.options.prismaErrorCodes;
873
984
  // Build a lazy Prisma-style promise for one delegate call. Crucially, the
874
985
  // Prisma-arg `translate` step runs INSIDE the deferred paths (the run closure
@@ -881,7 +992,27 @@ function makeDelegate(ctx, mm, getQI) {
881
992
  // catches the same throw from `batch.build`.
882
993
  const defer = (translate, run, batch) => {
883
994
  const batchable = batch
884
- ? { build: () => batch.build(getQI(), translate()), reshape: batch.reshape }
995
+ ? {
996
+ build: () => batch.build(getQI(), translate()),
997
+ reshape: batch.reshape,
998
+ nested: () => {
999
+ try {
1000
+ return batch.nested?.(translate()) ?? false;
1001
+ }
1002
+ catch {
1003
+ return false; // let the build path surface the translation error consistently
1004
+ }
1005
+ },
1006
+ execInTx: async (table) => {
1007
+ try {
1008
+ const t = translate();
1009
+ return batch.execInTx ? await batch.execInTx(table, t) : await run(table(mm.table), t);
1010
+ }
1011
+ catch (err) {
1012
+ throw decorate(err, pe);
1013
+ }
1014
+ },
1015
+ }
885
1016
  : undefined;
886
1017
  return new CompatPromise(async () => {
887
1018
  try {
@@ -905,14 +1036,20 @@ function makeDelegate(ctx, mm, getQI) {
905
1036
  findFirstOrThrow: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
906
1037
  findUniqueOrThrow: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow')), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
907
1038
  create: (args) => defer(() => {
908
- const t = { data: translateWriteData(ctx, mm, args.data) };
1039
+ const t = { data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)) };
909
1040
  if (typeof args.timeout === 'number')
910
1041
  t.timeout = args.timeout;
911
1042
  return t;
912
- }, (qi, t) => qi.create(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildCreate(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1043
+ }, (qi, t) => qi.create(t).then((r) => reshapeRow(ctx, mm, r)), {
1044
+ build: (qi, t) => qi.buildCreate(t),
1045
+ reshape: (raw) => reshapeRow(ctx, mm, raw),
1046
+ nested: (t) => hasNestedKeys(ctx, mm, t.data),
1047
+ }),
913
1048
  createMany: (args) => defer(() => {
914
1049
  const data = args.data;
915
- const rows = Array.isArray(data) ? data.map((d) => translateWriteData(ctx, mm, d)) : [];
1050
+ const rows = Array.isArray(data)
1051
+ ? data.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)))
1052
+ : [];
916
1053
  const t = { data: rows };
917
1054
  if (args.skipDuplicates)
918
1055
  t.skipDuplicates = true;
@@ -920,11 +1057,21 @@ function makeDelegate(ctx, mm, getQI) {
920
1057
  }, (qi, t) => qi.createMany(t).then((r) => ({ count: r.length })), { build: (qi, t) => qi.buildCreateMany(t), reshape: (raw) => ({ count: raw.length }) }),
921
1058
  update: (args) => defer(() => {
922
1059
  const a = requireWhere(args, 'update');
923
- return { where: translateWhere(ctx, mm, a.where), data: translateWriteData(ctx, mm, a.data) };
924
- }, (qi, t) => qi.update(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildUpdate(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1060
+ return {
1061
+ where: translateWhere(ctx, mm, a.where),
1062
+ data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
1063
+ };
1064
+ }, (qi, t) => qi.update(t).then((r) => reshapeRow(ctx, mm, r)), {
1065
+ build: (qi, t) => qi.buildUpdate(t),
1066
+ reshape: (raw) => reshapeRow(ctx, mm, raw),
1067
+ nested: (t) => hasNestedKeys(ctx, mm, t.data),
1068
+ }),
925
1069
  updateMany: (args) => defer(() => {
926
1070
  const a = args;
927
- const t = { where: translateWhere(ctx, mm, a.where ?? {}), data: translateWriteData(ctx, mm, a.data) };
1071
+ const t = {
1072
+ where: translateWhere(ctx, mm, a.where ?? {}),
1073
+ data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
1074
+ };
928
1075
  if (a.where === undefined)
929
1076
  t.allowFullTableScan = true;
930
1077
  return t;
@@ -944,10 +1091,31 @@ function makeDelegate(ctx, mm, getQI) {
944
1091
  const a = requireWhere(args, 'upsert');
945
1092
  return {
946
1093
  where: translateWhere(ctx, mm, a.where),
947
- create: translateWriteData(ctx, mm, a.create),
948
- update: translateWriteData(ctx, mm, a.update),
1094
+ create: translateWriteData(ctx, mm, applyCreateDefaults(mm, a.create)),
1095
+ update: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.update)),
949
1096
  };
950
- }, (qi, t) => qi.upsert(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildUpsert(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1097
+ }, (qi, t) => {
1098
+ // Native ON CONFLICT upsert is only Prisma-equivalent when the where
1099
+ // key values equal the create values AND no nested write data is
1100
+ // present; otherwise emulate Prisma's lookup-first atomically.
1101
+ if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
1102
+ return qi.upsert(t).then((r) => reshapeRow(ctx, mm, r));
1103
+ }
1104
+ return runInTx(async (table) => {
1105
+ const row = await upsertLookupFirst(table(mm.table), t);
1106
+ return reshapeRow(ctx, mm, row);
1107
+ });
1108
+ }, {
1109
+ build: (qi, t) => qi.buildUpsert(t),
1110
+ reshape: (raw) => reshapeRow(ctx, mm, raw),
1111
+ nested: (t) => !upsertKeysMatch(t) || hasNestedKeys(ctx, mm, t.create) || hasNestedKeys(ctx, mm, t.update),
1112
+ execInTx: async (table, t) => {
1113
+ if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
1114
+ return reshapeRow(ctx, mm, await table(mm.table).upsert(t));
1115
+ }
1116
+ return reshapeRow(ctx, mm, await upsertLookupFirst(table(mm.table), t));
1117
+ },
1118
+ }),
951
1119
  count: (args = {}) => defer(() => {
952
1120
  const t = {};
953
1121
  if (args.where !== undefined)
@@ -1042,7 +1210,7 @@ export function createPrismaCompatClient(client, map, options = {}) {
1042
1210
  // Delegates bound to the base client (each call reads db.table(...) lazily).
1043
1211
  const delegates = new Map();
1044
1212
  for (const [prismaModel, mm] of Object.entries(map.models)) {
1045
- delegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table)));
1213
+ delegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table), (fn) => db.$transaction((tx) => fn((n) => tx.table(n)))));
1046
1214
  }
1047
1215
  const ph = placeholderOf(db);
1048
1216
  const runRaw = async (text, params) => {
@@ -1069,6 +1237,18 @@ export function createPrismaCompatClient(client, map, options = {}) {
1069
1237
  }
1070
1238
  return b;
1071
1239
  });
1240
+ // Nested write data (or a lookup-first upsert) cannot run as a
1241
+ // single deferred statement. Prisma's array form still supports
1242
+ // those, so fall back to running the WHOLE array sequentially
1243
+ // inside one transaction; ordering and atomicity are preserved.
1244
+ if (batchables.some((b) => b.nested())) {
1245
+ return await db.$transaction(async (tx) => {
1246
+ const out = [];
1247
+ for (const b of batchables)
1248
+ out.push(await b.execInTx((n) => tx.table(n)));
1249
+ return out;
1250
+ }, txOptions);
1251
+ }
1072
1252
  const deferreds = batchables.map((b) => b.build());
1073
1253
  const results = (await db.$transaction(deferreds));
1074
1254
  return results.map((raw, i) => batchables[i].reshape(raw));
@@ -1083,7 +1263,7 @@ export function createPrismaCompatClient(client, map, options = {}) {
1083
1263
  return db.$transaction((tx) => {
1084
1264
  const txDelegates = {};
1085
1265
  for (const [prismaModel, mm] of Object.entries(map.models)) {
1086
- txDelegates[prismaModel] = makeDelegate(ctx, mm, () => tx.table(mm.table));
1266
+ txDelegates[prismaModel] = makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n)));
1087
1267
  const alias = prismaPropertyAlias(prismaModel);
1088
1268
  if (alias && !(alias in map.models) && !(alias in txDelegates)) {
1089
1269
  txDelegates[alias] = txDelegates[prismaModel];
package/dist/schema.d.ts CHANGED
@@ -297,6 +297,17 @@ export interface PrismaModelMap {
297
297
  * the core `findUnique`-family derivation cannot know.
298
298
  */
299
299
  compoundUniques: Record<string, string[]>;
300
+ /**
301
+ * Prisma CLIENT-side default per Prisma field name. Prisma's
302
+ * `@default(uuid())` / `@default(cuid())` / `@updatedAt` (and `@default(now())`
303
+ * when the migration left no database default) are filled by the Prisma
304
+ * client, not the database, so the columns commonly have NO db default and a
305
+ * Prisma call site omits them. The compat adapter fills these on create
306
+ * (`uuid` / `cuid` / `now`) and touches `updatedAt` fields on
307
+ * update/updateMany/upsert, exactly like Prisma. Emitted by
308
+ * `migrate-from-prisma` only for columns the database does not default.
309
+ */
310
+ clientDefaults?: Record<string, 'uuid' | 'cuid' | 'now' | 'updatedAt'>;
300
311
  }
301
312
  /** A resolved Prisma relation field → Turbine relation. */
302
313
  export interface PrismaRelationMap {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
5
5
  "type": "module",
6
6
  "exports": {