turbine-orm 0.41.0 → 0.42.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.
@@ -1078,11 +1078,12 @@ async function cmdMigrateFromPrisma(args, config) {
1078
1078
  (0, ui_js_1.label)('Enums', String(ast.enums.length));
1079
1079
  // Resolve against the live database, unless --no-db (parse-only).
1080
1080
  let schemaMeta = null;
1081
+ let url;
1081
1082
  if (args.noDb) {
1082
1083
  (0, ui_js_1.info)('Parse-only mode (--no-db): names will not be resolved.');
1083
1084
  }
1084
1085
  else {
1085
- const url = requireUrl(config);
1086
+ url = requireUrl(config);
1086
1087
  (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
1087
1088
  const spinner = new ui_js_1.Spinner('Introspecting database schema').start();
1088
1089
  schemaMeta = await (0, introspect_js_1.introspect)({
@@ -1098,7 +1099,10 @@ async function cmdMigrateFromPrisma(args, config) {
1098
1099
  spinner.succeed(`Introspected ${(0, ui_js_1.bold)(String(Object.keys(schemaMeta.tables).length))} tables`);
1099
1100
  }
1100
1101
  (0, ui_js_1.newline)();
1101
- const result = (0, prisma_resolve_js_1.resolvePrismaSchema)(ast, schemaMeta);
1102
+ // `--keep-column-names` makes the generated client key fields by raw DB column
1103
+ // names; resolve the name map against the same transformed schema so the
1104
+ // emitted PRISMA_MAP field values agree with the client (D).
1105
+ const result = (0, prisma_resolve_js_1.resolvePrismaSchema)(ast, schemaMeta, { keepColumnNames: config.keepColumnNames });
1102
1106
  // Console summary.
1103
1107
  (0, ui_js_1.header)('Resolution');
1104
1108
  for (const line of (0, prisma_report_js_1.summaryLines)(result)) {
@@ -1118,10 +1122,26 @@ async function cmdMigrateFromPrisma(args, config) {
1118
1122
  const reportPath = (0, node_path_1.join)(outDir, 'prisma-migration-report.md');
1119
1123
  (0, node_fs_1.writeFileSync)(reportPath, (0, prisma_report_js_1.formatPrismaReport)(result, { schemaPath: prismaPath, noTimestamp: args.noTimestamp }), 'utf-8');
1120
1124
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.cyan)(reportPath)} ${(0, ui_js_1.dim)('(report)')}`);
1121
- if (!args.noDb) {
1125
+ if (!args.noDb && schemaMeta) {
1122
1126
  const mapPath = (0, node_path_1.join)(outDir, 'prisma-map.ts');
1123
1127
  (0, node_fs_1.writeFileSync)(mapPath, (0, generate_js_1.generatePrismaMap)(result.map, { noTimestamp: args.noTimestamp }), 'utf-8');
1124
1128
  console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.cyan)(mapPath)} ${(0, ui_js_1.dim)('(typed name map)')}`);
1129
+ // Always emit the standard generated client alongside the report + name map.
1130
+ // It is built from the live introspected metadata, so unresolved Prisma
1131
+ // items never block it; a partially resolved run (--allow-partial) still
1132
+ // gets a working client (C). `--keep-column-names` flows through so the
1133
+ // client's field names match the name map (D).
1134
+ const gen = (0, generate_js_1.generate)({
1135
+ schema: schemaMeta,
1136
+ outDir: config.out,
1137
+ connectionString: url,
1138
+ noTimestamp: args.noTimestamp,
1139
+ importExtension: config.importExtension,
1140
+ keepColumnNames: config.keepColumnNames,
1141
+ });
1142
+ for (const file of gen.files) {
1143
+ console.log(` ${(0, ui_js_1.dim)(ui_js_1.symbols.teeEnd)} ${(0, ui_js_1.cyan)((0, node_path_1.join)(outDir, file))} ${(0, ui_js_1.dim)('(client)')}`);
1144
+ }
1125
1145
  }
1126
1146
  (0, ui_js_1.newline)();
1127
1147
  // Exit non-zero when anything is UNRESOLVED, unless --allow-partial.
@@ -75,16 +75,18 @@ function fieldColumn(model, fieldName) {
75
75
  function isRelationField(typeName, modelNames) {
76
76
  return modelNames.has(typeName);
77
77
  }
78
- // ---------------------------------------------------------------------------
79
- // Main entry
80
- // ---------------------------------------------------------------------------
81
78
  /**
82
79
  * Resolve `ast` against introspected `schema` (or `null` for parse-only).
83
80
  */
84
- function resolvePrismaSchema(ast, schema) {
85
- const noDb = schema === null;
81
+ function resolvePrismaSchema(ast, schema, options = {}) {
82
+ // Under keep-column-names the generated client keys fields by raw DB column
83
+ // names; resolve against the same transformed metadata so the name map's
84
+ // field values match the client (D).
85
+ const resolvedSchema = schema && options.keepColumnNames ? (0, schema_js_1.withDbFieldNames)(schema) : schema;
86
+ const noDb = resolvedSchema === null;
86
87
  const modelNames = new Set(ast.models.map((m) => m.name));
87
- const tableNames = schema ? new Set(Object.keys(schema.tables)) : new Set();
88
+ const modelsByName = new Map(ast.models.map((m) => [m.name, m]));
89
+ const tableNames = resolvedSchema ? new Set(Object.keys(resolvedSchema.tables)) : new Set();
88
90
  // Pass 1 - resolve each model to a table so relation targets are known.
89
91
  const modelTable = new Map();
90
92
  for (const model of ast.models) {
@@ -102,7 +104,7 @@ function resolvePrismaSchema(ast, schema) {
102
104
  for (const model of ast.models) {
103
105
  const rt = modelTable.get(model.name);
104
106
  const table = rt.table;
105
- const tableMeta = table && schema ? schema.tables[table] : undefined;
107
+ const tableMeta = table && resolvedSchema ? resolvedSchema.tables[table] : undefined;
106
108
  const accessor = table ? (0, schema_js_1.snakeToCamel)(table) : null;
107
109
  const status = noDb ? 'parsed' : table ? 'resolved' : 'unresolved';
108
110
  const resolved = {
@@ -119,7 +121,7 @@ function resolvePrismaSchema(ast, schema) {
119
121
  };
120
122
  for (const field of model.fields) {
121
123
  if (isRelationField(field.type, modelNames)) {
122
- resolved.relations.push(resolveRelation(model, field.name, field.type, field.isList, modelTable, schema, tableMeta, noDb));
124
+ resolved.relations.push(resolveRelation(model, field.name, field.type, field.isList, modelTable, modelsByName, resolvedSchema, tableMeta, noDb));
123
125
  }
124
126
  else {
125
127
  resolved.fields.push(resolveScalarField(model, field.name, tableMeta, noDb));
@@ -156,7 +158,7 @@ function resolvePrismaSchema(ast, schema) {
156
158
  }
157
159
  // Enums.
158
160
  for (const en of ast.enums) {
159
- const r = resolveEnum(en.name, en.map, schema, noDb);
161
+ const r = resolveEnum(en.name, en.map, resolvedSchema, noDb);
160
162
  result.enums.push(r);
161
163
  if (!noDb && r.status === 'resolved' && r.turbineName)
162
164
  result.map.enums[en.name] = r.turbineName;
@@ -213,7 +215,28 @@ function resolveScalarField(model, fieldName, tableMeta, noDb) {
213
215
  reason: `column "${column}" not found on table "${tableMeta.name}"`,
214
216
  };
215
217
  }
216
- function resolveRelation(model, fieldName, targetModelName, isList, modelTable, schema, tableMeta, noDb) {
218
+ /**
219
+ * The `@relation("Name")` name on a field, from the `name:` argument or the
220
+ * first positional string argument. Absent when the field has no `@relation`
221
+ * attribute or the attribute carries no name.
222
+ */
223
+ function relationNameOf(field) {
224
+ const relAttr = field?.attrs.find((a) => a.name === 'relation');
225
+ if (!relAttr)
226
+ return undefined;
227
+ const named = relAttr.args.find((a) => a.key === 'name' && a.kind === 'string');
228
+ if (named?.value)
229
+ return named.value;
230
+ const positional = relAttr.args.find((a) => a.key === undefined && a.kind === 'string');
231
+ return positional?.value;
232
+ }
233
+ /** The FK column list a field pins via `@relation(fields: [...])`, resolved to columns. */
234
+ function relationFkColumns(model, field) {
235
+ const relAttr = field?.attrs.find((a) => a.name === 'relation');
236
+ const fieldsArg = relAttr?.args.find((a) => a.key === 'fields' && a.kind === 'array');
237
+ return fieldsArg?.items?.map((pf) => fieldColumn(model, pf)) ?? null;
238
+ }
239
+ function resolveRelation(model, fieldName, targetModelName, isList, modelTable, modelsByName, schema, tableMeta, noDb) {
217
240
  const cardinality = isList ? 'many' : 'one';
218
241
  const base = {
219
242
  prismaName: fieldName,
@@ -230,9 +253,24 @@ function resolveRelation(model, fieldName, targetModelName, isList, modelTable,
230
253
  const targetTable = modelTable.get(targetModelName)?.table ?? null;
231
254
  // Explicit @relation(fields: [...]) names the FK columns on THIS side.
232
255
  const field = model.fields.find((f) => f.name === fieldName);
233
- const relAttr = field?.attrs.find((a) => a.name === 'relation');
234
- const fieldsArg = relAttr?.args.find((a) => a.key === 'fields' && a.kind === 'array');
235
- const fkColumns = fieldsArg?.items?.map((pf) => fieldColumn(model, pf)) ?? null;
256
+ let fkColumns = relationFkColumns(model, field);
257
+ // Inverse side (no fields on this side) with a @relation("Name"): pair by that
258
+ // name FIRST. Find the opposing model's field carrying the same relation name
259
+ // AND the FK (fields: [...]), and resolve through ITS foreign key. This is how
260
+ // Prisma disambiguates two or more relations to the same target model. Only
261
+ // fall back to the ambiguity handling below when there is no relation name or
262
+ // the named pair cannot be found.
263
+ if (!fkColumns || fkColumns.length === 0) {
264
+ const relName = relationNameOf(field);
265
+ const targetModel = modelsByName.get(targetModelName);
266
+ if (relName && targetModel) {
267
+ const opposing = targetModel.fields.find((f) => f.type === model.name &&
268
+ relationNameOf(f) === relName &&
269
+ (relationFkColumns(targetModel, f)?.length ?? 0) > 0);
270
+ if (opposing)
271
+ fkColumns = relationFkColumns(targetModel, opposing);
272
+ }
273
+ }
236
274
  const candidates = Object.values(tableMeta.relations).filter((def) => {
237
275
  if (targetTable && def.to !== targetTable)
238
276
  return false;
@@ -293,10 +331,25 @@ function resolveCompoundUnique(model, key, tableMeta, noDb) {
293
331
  return { ...base, turbineFields, status: 'resolved' };
294
332
  return { ...base, reason: `no compound primary key on "${tableMeta.name}" matches (${columns.join(', ')})` };
295
333
  }
296
- // Introspected metadata carries composite unique constraints in uniqueColumns.
297
- if (matches(tableMeta.uniqueColumns))
334
+ // A composite unique can surface EITHER as a unique constraint (uniqueColumns)
335
+ // OR as a plain UNIQUE INDEX (Prisma creates unique indexes, not table
336
+ // constraints), so accept a matching unique index too. A partial unique index
337
+ // does not enforce uniqueness across the whole table, so it never satisfies a
338
+ // @@unique; skip it defensively (the marker may be added to IndexMetadata).
339
+ const uniqueIndexMatches = tableMeta.indexes.some((idx) => {
340
+ if (!idx.unique)
341
+ return false;
342
+ if (idx.partial)
343
+ return false;
344
+ return [...idx.columns].sort().join(',') === want;
345
+ });
346
+ if (matches(tableMeta.uniqueColumns) || uniqueIndexMatches) {
298
347
  return { ...base, turbineFields, status: 'resolved' };
299
- return { ...base, reason: `no unique constraint on "${tableMeta.name}" matches (${columns.join(', ')})` };
348
+ }
349
+ return {
350
+ ...base,
351
+ reason: `no unique constraint or unique index on "${tableMeta.name}" matches (${columns.join(', ')})`,
352
+ };
300
353
  }
301
354
  function resolveEnum(name, map, schema, noDb) {
302
355
  if (noDb || !schema)
@@ -460,13 +460,23 @@ function generateTypes(schema, options) {
460
460
  for (const uc of table.uniqueColumns)
461
461
  addCompound(uc);
462
462
  for (const idx of table.indexes) {
463
- if (idx.unique && !idx.docPath)
463
+ // A partial unique index does not guarantee table-wide row uniqueness,
464
+ // so it must not become a compound-unique selector (matches the runtime
465
+ // exclusion in query/compound-unique.ts).
466
+ if (idx.unique && !idx.docPath && !idx.partial)
464
467
  addCompound(idx.columns);
465
468
  }
466
469
  const selectorEntries = compoundSets.map((cols) => {
467
470
  const members = cols.map(memberType);
468
471
  return {
469
- selectorName: members.map((m) => m.field).join('_'),
472
+ // The selector NAME is the underscore-join of the member FIELD names.
473
+ // It is normally a valid identifier (`orgId_userId`), but a
474
+ // junction-style column that is not a valid identifier (e.g. a quoted
475
+ // uppercase `"A"` / `"B"`) would join into a broken object key. Emit
476
+ // any non-identifier name as ONE quoted string-literal key so the
477
+ // generated types.ts always parses; the runtime selector map in
478
+ // query/compound-unique.ts registers the same name spelling.
479
+ selectorName: quoteIfNeeded(members.map((m) => m.field).join('_')),
470
480
  memberType: `{ ${members.map((m) => `${quoteIfNeeded(m.field)}: ${m.tsType}`).join('; ')} }`,
471
481
  };
472
482
  });
@@ -725,7 +735,11 @@ function generateMetadata(schema, options) {
725
735
  // indexes
726
736
  lines.push(' indexes: [');
727
737
  for (const idx of table.indexes) {
728
- lines.push(` { name: '${escSQ(idx.name)}', columns: [${idx.columns.map((c) => `'${escSQ(c)}'`).join(', ')}], unique: ${idx.unique}, definition: ${JSON.stringify(idx.definition)} },`);
738
+ // `partial` must round-trip: the runtime compound-unique derivation reads
739
+ // the GENERATED metadata, so dropping the flag here would re-arm a
740
+ // partial-unique selector that types.ts correctly excludes.
741
+ const partialSeg = idx.partial ? ', partial: true' : '';
742
+ lines.push(` { name: '${escSQ(idx.name)}', columns: [${idx.columns.map((c) => `'${escSQ(c)}'`).join(', ')}], unique: ${idx.unique}${partialSeg}, definition: ${JSON.stringify(idx.definition)} },`);
729
743
  }
730
744
  lines.push(' ],');
731
745
  // checks: introspected named CHECK constraints. Emitted only when present
@@ -18,6 +18,8 @@ exports.applyTableFilters = applyTableFilters;
18
18
  exports.defaultExcludedTablesPresent = defaultExcludedTablesPresent;
19
19
  exports.introspect = introspect;
20
20
  exports.introspectPostgresCatalog = introspectPostgresCatalog;
21
+ exports.parseIndexColumns = parseIndexColumns;
22
+ exports.indexHasWhere = indexHasWhere;
21
23
  exports.stripCheckWrapper = stripCheckWrapper;
22
24
  exports.relationNameFromColumn = relationNameFromColumn;
23
25
  exports.isUnknownTsType = isUnknownTsType;
@@ -414,15 +416,15 @@ async function introspectPostgresCatalog(options) {
414
416
  continue;
415
417
  if (!indexesByTable.has(row.tablename))
416
418
  indexesByTable.set(row.tablename, []);
417
- const isUnique = row.indexdef.includes('UNIQUE');
418
- // Extract column names from indexdef (e.g. "CREATE INDEX idx ON tbl USING btree (col1, col2)")
419
- const colMatch = row.indexdef.match(/\((.+)\)/);
420
- const columns = colMatch ? colMatch[1].split(',').map((c) => c.trim().replace(/ (ASC|DESC)/i, '')) : [];
419
+ const indexdef = row.indexdef;
420
+ const isUnique = indexdef.includes('UNIQUE');
421
+ const isPartial = indexHasWhere(indexdef);
421
422
  indexesByTable.get(row.tablename).push({
422
423
  name: row.indexname,
423
- columns,
424
+ columns: parseIndexColumns(indexdef),
424
425
  unique: isUnique,
425
- definition: row.indexdef,
426
+ definition: indexdef,
427
+ ...(isPartial ? { partial: true } : {}),
426
428
  });
427
429
  }
428
430
  // ----- Group check constraints by table -----
@@ -511,7 +513,17 @@ async function introspectPostgresCatalog(options) {
511
513
  // hasMany relations derived from J's FKs are left untouched — this block
512
514
  // never removes or renames anything. Naming/collision handling lives in the
513
515
  // shared addAutoManyToManyRelations helper.
514
- addAutoManyToManyRelations(tableNames, foreignKeys, pkByTable, new Map(Array.from(columnsByTable, ([tbl, cols]) => [tbl, cols.map((c) => c.name)])), relationsByTable, columnFieldsByTable, unknownTypedFieldsByTable);
516
+ //
517
+ // Prisma's implicit m2m junctions have no primary key (just a two-column
518
+ // UNIQUE index over the FK columns), so pass the introspected two-column
519
+ // unique indexes as the fallback junction-key source.
520
+ const uniqueIndexColsByTable = new Map();
521
+ for (const [tbl, idxs] of indexesByTable) {
522
+ const twoColUniques = idxs.filter((idx) => idx.unique && idx.columns.length === 2).map((idx) => idx.columns);
523
+ if (twoColUniques.length > 0)
524
+ uniqueIndexColsByTable.set(tbl, twoColUniques);
525
+ }
526
+ addAutoManyToManyRelations(tableNames, foreignKeys, pkByTable, new Map(Array.from(columnsByTable, ([tbl, cols]) => [tbl, cols.map((c) => c.name)])), relationsByTable, columnFieldsByTable, unknownTypedFieldsByTable, uniqueIndexColsByTable);
515
527
  // ----- Assemble TableMetadata for each table -----
516
528
  const tables = {};
517
529
  for (const tableName of tableNames) {
@@ -556,6 +568,50 @@ async function introspectPostgresCatalog(options) {
556
568
  await pool.end();
557
569
  }
558
570
  }
571
+ /**
572
+ * Parse the indexed column names out of a `pg_indexes.indexdef` string.
573
+ *
574
+ * `indexdef` always reads `CREATE [UNIQUE] INDEX name ON tbl USING method
575
+ * (col, ...) [WHERE predicate]`. We anchor on the `USING` clause's parenthesised
576
+ * column list (the same precedent as `describeIndexDefMismatch` in
577
+ * schema-sql.ts) so a PARTIAL index's trailing `WHERE (...)` parentheses are
578
+ * never mistaken for the column list. The older greedy `/\((.+)\)/` swallowed
579
+ * `) WHERE (` and spliced a raw predicate fragment into the column names, which
580
+ * then leaked into generated compound-unique selector names.
581
+ *
582
+ * Each column is de-quoted (Postgres quotes non-lowercase identifiers such as a
583
+ * Prisma implicit m2m junction's `"A"` / `"B"`), so the names match the
584
+ * unquoted column names carried elsewhere in the metadata. Expression columns
585
+ * (anything containing a parenthesis) are dropped conservatively: a functional
586
+ * index does not name a plain column.
587
+ */
588
+ function parseIndexColumns(indexdef) {
589
+ const m = indexdef.match(/USING\s+\w+\s*\(([^)]*)\)/i) ?? indexdef.match(/\(([^)]*)\)/);
590
+ if (!m)
591
+ return [];
592
+ return m[1]
593
+ .split(',')
594
+ .map((c) => unquoteIndexIdent(c
595
+ .trim()
596
+ .replace(/\s+(ASC|DESC)$/i, '')
597
+ .trim()))
598
+ .filter((c) => c.length > 0 && !c.includes('(') && !c.includes(')'));
599
+ }
600
+ /** Strip one pair of surrounding double quotes and unescape doubled `""`. */
601
+ function unquoteIndexIdent(col) {
602
+ if (col.length >= 2 && col.startsWith('"') && col.endsWith('"')) {
603
+ return col.slice(1, -1).replace(/""/g, '"');
604
+ }
605
+ return col;
606
+ }
607
+ /**
608
+ * Whether an `indexdef` carries a top-level `WHERE` predicate (a PARTIAL index).
609
+ * pg_indexes only ever emits `WHERE` as the partial predicate, so a keyword
610
+ * match is sufficient (matches the `describeIndexDefMismatch` precedent).
611
+ */
612
+ function indexHasWhere(indexdef) {
613
+ return /\bWHERE\b/i.test(indexdef);
614
+ }
559
615
  /**
560
616
  * Recover the raw check expression from `pg_get_constraintdef` output, which
561
617
  * wraps it as `CHECK ((expr))`. Strips the leading `CHECK ` keyword and one
@@ -900,11 +956,15 @@ function buildRelationsFromForeignKeys(foreignKeys, columnFieldsByTable, fkActio
900
956
  * IDENTICAL relation names for the same logical schema.
901
957
  *
902
958
  * A table J is a PURE junction only when ALL of these hold:
903
- * 1. J's primary key is exactly two columns.
959
+ * 1. J's junction KEY is exactly two columns: either a two-column primary
960
+ * key, OR (Prisma implicit m2m junctions have NO primary key) a two-column
961
+ * UNIQUE index over exactly the two FK columns, supplied via the optional
962
+ * `uniqueIndexColsByTable`. When that map is absent the behavior is
963
+ * unchanged: only a two-column PK qualifies.
904
964
  * 2. J has exactly two FKs, each single-column.
905
- * 3. Each FK's source column is one of J's two PK columns.
965
+ * 3. Each FK's source column is one of J's two key columns.
906
966
  * 4. The two FKs target two DISTINCT tables (A and B).
907
- * 5. J has no payload columns beyond the two FK/PK columns.
967
+ * 5. J has no payload columns beyond the two FK/key columns.
908
968
  *
909
969
  * For such a J linking A and B this ADDS a `manyToMany` on A → B and B → A
910
970
  * routed `through` J. It never removes or renames an existing relation:
@@ -915,11 +975,8 @@ function buildRelationsFromForeignKeys(foreignKeys, columnFieldsByTable, fkActio
915
975
  * - a shadowed concrete-typed column → deterministic `Rel` suffix + warn
916
976
  * instead of silently dropping the relation.
917
977
  */
918
- function addAutoManyToManyRelations(tableNames, foreignKeys, pkByTable, columnNamesByTable, relationsByTable, columnFieldsByTable, unknownTypedFieldsByTable) {
978
+ function addAutoManyToManyRelations(tableNames, foreignKeys, pkByTable, columnNamesByTable, relationsByTable, columnFieldsByTable, unknownTypedFieldsByTable, uniqueIndexColsByTable) {
919
979
  for (const tableName of tableNames) {
920
- const pk = pkByTable.get(tableName) ?? [];
921
- if (pk.length !== 2)
922
- continue;
923
980
  // FKs whose source is this table — both must be single-column.
924
981
  const tableFks = foreignKeys.filter((fk) => fk.sourceTable === tableName);
925
982
  if (tableFks.length !== 2)
@@ -927,20 +984,35 @@ function addAutoManyToManyRelations(tableNames, foreignKeys, pkByTable, columnNa
927
984
  if (tableFks.some((fk) => fk.sourceColumns.length !== 1))
928
985
  continue;
929
986
  const fkCols = tableFks.map((fk) => fk.sourceColumns[0]);
930
- const pkSet = new Set(pk);
931
- // Both FK columns must be the PK columns (and vice-versa).
932
- if (!fkCols.every((c) => pkSet.has(c)))
933
- continue;
934
987
  if (new Set(fkCols).size !== 2)
935
988
  continue;
989
+ const fkSet = new Set(fkCols);
990
+ // The junction KEY is normally the two-column PK. Prisma's implicit m2m
991
+ // junctions have NO primary key, so accept instead a two-column UNIQUE
992
+ // index that covers exactly the two FK columns. Only a PK-less table is
993
+ // eligible for the unique-index fallback, so a real entity that happens to
994
+ // carry a two-column unique index is never mistaken for a junction.
995
+ const pk = pkByTable.get(tableName) ?? [];
996
+ let keyCols;
997
+ if (pk.length === 2 && pk.every((c) => fkSet.has(c))) {
998
+ keyCols = pk;
999
+ }
1000
+ else if (pk.length === 0) {
1001
+ const uniques = uniqueIndexColsByTable?.get(tableName) ?? [];
1002
+ keyCols = uniques.find((u) => u.length === 2 && u.every((c) => fkSet.has(c)));
1003
+ }
1004
+ if (!keyCols)
1005
+ continue;
936
1006
  // Two DISTINCT target tables.
937
1007
  const [fkA, fkB] = tableFks;
938
1008
  if (fkA.targetTable === fkB.targetTable)
939
1009
  continue;
940
- // No payload columns: J's columns are exactly the two FK/PK columns.
1010
+ // No payload columns: J's columns are exactly the two FK/key columns.
941
1011
  const jCols = columnNamesByTable.get(tableName) ?? [];
942
1012
  if (jCols.length !== 2)
943
1013
  continue;
1014
+ if (!jCols.every((c) => fkSet.has(c)))
1015
+ continue;
944
1016
  // For each direction, the m2m `referenceKey` is the *targeted* table's
945
1017
  // referenced column(s); the junction's sourceKey is the FK column pointing
946
1018
  // to that table; the targetKey is the FK column pointing to the OTHER table.
@@ -848,14 +848,28 @@ function prismaPropertyAlias(model) {
848
848
  */
849
849
  function makeDelegate(ctx, mm, getQI) {
850
850
  const pe = ctx.options.prismaErrorCodes;
851
- const lift = (run, batchable) => new CompatPromise(async () => {
852
- try {
853
- return await run();
854
- }
855
- catch (err) {
856
- throw decorate(err, pe);
857
- }
858
- }, batchable);
851
+ // Build a lazy Prisma-style promise for one delegate call. Crucially, the
852
+ // Prisma-arg `translate` step runs INSIDE the deferred paths (the run closure
853
+ // and the batchable build closure), never eagerly at call time: a
854
+ // translation/validation error (unknown relation in `include`, unknown
855
+ // compound selector, negative take, ...) must surface as a REJECTED promise
856
+ // so a Prisma-shaped `.catch()` fires, not as a synchronous throw. The
857
+ // async run wrapper also converts any synchronous throw from the underlying
858
+ // `qi.*` build into a rejection; the array `$transaction([...])` batch path
859
+ // catches the same throw from `batch.build`.
860
+ const defer = (translate, run, batch) => {
861
+ const batchable = batch
862
+ ? { build: () => batch.build(getQI(), translate()), reshape: batch.reshape }
863
+ : undefined;
864
+ return new CompatPromise(async () => {
865
+ try {
866
+ return await run(getQI(), translate());
867
+ }
868
+ catch (err) {
869
+ throw decorate(err, pe);
870
+ }
871
+ }, batchable);
872
+ };
859
873
  const requireWhere = (args, op) => {
860
874
  if (!args || args.where === undefined) {
861
875
  throw new errors_js_1.ValidationError(`[turbine] prisma-compat: ${op} on "${modelName(ctx, mm)}" requires a \`where\`.`);
@@ -863,125 +877,68 @@ function makeDelegate(ctx, mm, getQI) {
863
877
  return args;
864
878
  };
865
879
  return {
866
- findMany: (args = {}) => {
867
- const t = translateReadArgs(ctx, mm, args);
868
- return lift(() => getQI()
869
- .findMany(t)
870
- .then((r) => reshapeRows(ctx, mm, r)), { build: () => getQI().buildFindMany(t), reshape: (raw) => reshapeRows(ctx, mm, raw) });
871
- },
872
- findFirst: (args = {}) => {
873
- const t = translateReadArgs(ctx, mm, args);
874
- return lift(() => getQI()
875
- .findFirst(t)
876
- .then((r) => reshapeRowOrNull(ctx, mm, r)), { build: () => getQI().buildFindFirst(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) });
877
- },
878
- findUnique: (args) => {
879
- const t = translateReadArgs(ctx, mm, requireWhere(args, 'findUnique'));
880
- return lift(() => getQI()
881
- .findUnique(t)
882
- .then((r) => reshapeRowOrNull(ctx, mm, r)), { build: () => getQI().buildFindUnique(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) });
883
- },
884
- findFirstOrThrow: (args = {}) => {
885
- const t = translateReadArgs(ctx, mm, args);
886
- return lift(() => getQI()
887
- .findFirstOrThrow(t)
888
- .then((r) => reshapeRow(ctx, mm, r)), { build: () => getQI().buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) });
889
- },
890
- findUniqueOrThrow: (args) => {
891
- const t = translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow'));
892
- return lift(() => getQI()
893
- .findUniqueOrThrow(t)
894
- .then((r) => reshapeRow(ctx, mm, r)), { build: () => getQI().buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) });
895
- },
896
- create: (args) => {
880
+ findMany: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findMany(t).then((r) => reshapeRows(ctx, mm, r)), { build: (qi, t) => qi.buildFindMany(t), reshape: (raw) => reshapeRows(ctx, mm, raw) }),
881
+ findFirst: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findFirst(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirst(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
882
+ findUnique: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUnique')), (qi, t) => qi.findUnique(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindUnique(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
883
+ 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) }),
884
+ 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) }),
885
+ create: (args) => defer(() => {
897
886
  const t = { data: translateWriteData(ctx, mm, args.data) };
898
887
  if (typeof args.timeout === 'number')
899
888
  t.timeout = args.timeout;
900
- return lift(() => getQI()
901
- .create(t)
902
- .then((r) => reshapeRow(ctx, mm, r)), { build: () => getQI().buildCreate(t), reshape: (raw) => reshapeRow(ctx, mm, raw) });
903
- },
904
- createMany: (args) => {
889
+ return t;
890
+ }, (qi, t) => qi.create(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildCreate(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
891
+ createMany: (args) => defer(() => {
905
892
  const data = args.data;
906
893
  const rows = Array.isArray(data) ? data.map((d) => translateWriteData(ctx, mm, d)) : [];
907
894
  const t = { data: rows };
908
895
  if (args.skipDuplicates)
909
896
  t.skipDuplicates = true;
910
- return lift(() => getQI()
911
- .createMany(t)
912
- .then((r) => ({ count: r.length })), { build: () => getQI().buildCreateMany(t), reshape: (raw) => ({ count: raw.length }) });
913
- },
914
- update: (args) => {
897
+ return t;
898
+ }, (qi, t) => qi.createMany(t).then((r) => ({ count: r.length })), { build: (qi, t) => qi.buildCreateMany(t), reshape: (raw) => ({ count: raw.length }) }),
899
+ update: (args) => defer(() => {
915
900
  const a = requireWhere(args, 'update');
916
- const t = { where: translateWhere(ctx, mm, a.where), data: translateWriteData(ctx, mm, a.data) };
917
- return lift(() => getQI()
918
- .update(t)
919
- .then((r) => reshapeRow(ctx, mm, r)), { build: () => getQI().buildUpdate(t), reshape: (raw) => reshapeRow(ctx, mm, raw) });
920
- },
921
- updateMany: (args) => {
901
+ return { where: translateWhere(ctx, mm, a.where), data: translateWriteData(ctx, mm, a.data) };
902
+ }, (qi, t) => qi.update(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildUpdate(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
903
+ updateMany: (args) => defer(() => {
922
904
  const a = args;
923
905
  const t = { where: translateWhere(ctx, mm, a.where ?? {}), data: translateWriteData(ctx, mm, a.data) };
924
906
  if (a.where === undefined)
925
907
  t.allowFullTableScan = true;
926
- return lift(() => getQI().updateMany(t), {
927
- build: () => getQI().buildUpdateMany(t),
928
- reshape: (raw) => raw,
929
- });
930
- },
931
- delete: (args) => {
908
+ return t;
909
+ }, (qi, t) => qi.updateMany(t), { build: (qi, t) => qi.buildUpdateMany(t), reshape: (raw) => raw }),
910
+ delete: (args) => defer(() => {
932
911
  const a = requireWhere(args, 'delete');
933
- const t = { where: translateWhere(ctx, mm, a.where) };
934
- return lift(() => getQI()
935
- .delete(t)
936
- .then((r) => reshapeRow(ctx, mm, r)), { build: () => getQI().buildDelete(t), reshape: (raw) => reshapeRow(ctx, mm, raw) });
937
- },
938
- deleteMany: (args = {}) => {
912
+ return { where: translateWhere(ctx, mm, a.where) };
913
+ }, (qi, t) => qi.delete(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildDelete(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
914
+ deleteMany: (args = {}) => defer(() => {
939
915
  const a = args;
940
916
  const t = { where: translateWhere(ctx, mm, a.where ?? {}) };
941
917
  if (a.where === undefined)
942
918
  t.allowFullTableScan = true;
943
- return lift(() => getQI().deleteMany(t), {
944
- build: () => getQI().buildDeleteMany(t),
945
- reshape: (raw) => raw,
946
- });
947
- },
948
- upsert: (args) => {
919
+ return t;
920
+ }, (qi, t) => qi.deleteMany(t), { build: (qi, t) => qi.buildDeleteMany(t), reshape: (raw) => raw }),
921
+ upsert: (args) => defer(() => {
949
922
  const a = requireWhere(args, 'upsert');
950
- const t = {
923
+ return {
951
924
  where: translateWhere(ctx, mm, a.where),
952
925
  create: translateWriteData(ctx, mm, a.create),
953
926
  update: translateWriteData(ctx, mm, a.update),
954
927
  };
955
- return lift(() => getQI()
956
- .upsert(t)
957
- .then((r) => reshapeRow(ctx, mm, r)), { build: () => getQI().buildUpsert(t), reshape: (raw) => reshapeRow(ctx, mm, raw) });
958
- },
959
- count: (args = {}) => {
928
+ }, (qi, t) => qi.upsert(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildUpsert(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
929
+ count: (args = {}) => defer(() => {
960
930
  const t = {};
961
931
  if (args.where !== undefined)
962
932
  t.where = translateWhere(ctx, mm, args.where);
963
933
  if (typeof args.timeout === 'number')
964
934
  t.timeout = args.timeout;
965
- return lift(() => getQI().count(t), {
966
- build: () => getQI().buildCount(t),
967
- reshape: (raw) => raw,
968
- });
969
- },
970
- aggregate: (args) => {
971
- const t = translateAggregateArgs(ctx, mm, args, false);
972
- return lift(() => getQI()
973
- .aggregate(t)
974
- .then((r) => reshapeAggregate(ctx, mm, r)), { build: () => getQI().buildAggregate(t), reshape: (raw) => reshapeAggregate(ctx, mm, raw) });
975
- },
976
- groupBy: (args) => {
977
- const t = translateAggregateArgs(ctx, mm, args, true);
978
- return lift(() => getQI()
979
- .groupBy(t)
980
- .then((rows) => rows.map((r) => reshapeGroupRow(ctx, mm, r))), {
981
- build: () => getQI().buildGroupBy(t),
982
- reshape: (raw) => raw.map((r) => reshapeGroupRow(ctx, mm, r)),
983
- });
984
- },
935
+ return t;
936
+ }, (qi, t) => qi.count(t), { build: (qi, t) => qi.buildCount(t), reshape: (raw) => raw }),
937
+ aggregate: (args) => defer(() => translateAggregateArgs(ctx, mm, args, false), (qi, t) => qi.aggregate(t).then((r) => reshapeAggregate(ctx, mm, r)), { build: (qi, t) => qi.buildAggregate(t), reshape: (raw) => reshapeAggregate(ctx, mm, raw) }),
938
+ groupBy: (args) => defer(() => translateAggregateArgs(ctx, mm, args, true), (qi, t) => qi.groupBy(t).then((rows) => rows.map((r) => reshapeGroupRow(ctx, mm, r))), {
939
+ build: (qi, t) => qi.buildGroupBy(t),
940
+ reshape: (raw) => raw.map((r) => reshapeGroupRow(ctx, mm, r)),
941
+ }),
985
942
  };
986
943
  }
987
944
  function poolOf(db) {
Binary file