turbine-orm 0.29.0 → 0.30.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.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/cli/index.js +5 -0
  3. package/dist/cjs/cli/mcp.js +22 -92
  4. package/dist/cjs/client.js +33 -3
  5. package/dist/cjs/generate.js +71 -25
  6. package/dist/cjs/index.js +4 -1
  7. package/dist/cjs/introspect.js +350 -120
  8. package/dist/cjs/mssql.js +18 -133
  9. package/dist/cjs/mysql.js +16 -129
  10. package/dist/cjs/optional-peer-import.cjs +122 -0
  11. package/dist/cjs/powdb.js +424 -81
  12. package/dist/cjs/powql.js +49 -25
  13. package/dist/cjs/query/builder.js +290 -23
  14. package/dist/cjs/query/filters.js +32 -1
  15. package/dist/cjs/schema-metadata.js +316 -0
  16. package/dist/cjs/sqlite.js +8 -89
  17. package/dist/cli/index.d.ts +2 -0
  18. package/dist/cli/index.js +5 -0
  19. package/dist/cli/mcp.d.ts +18 -0
  20. package/dist/cli/mcp.js +22 -93
  21. package/dist/client.d.ts +6 -2
  22. package/dist/client.js +33 -3
  23. package/dist/generate.d.ts +16 -4
  24. package/dist/generate.js +71 -25
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +2 -0
  27. package/dist/introspect.d.ts +94 -1
  28. package/dist/introspect.js +345 -120
  29. package/dist/mssql.js +16 -101
  30. package/dist/mysql.js +14 -97
  31. package/dist/optional-peer-import.cjs +89 -0
  32. package/dist/optional-peer-import.d.cts +53 -0
  33. package/dist/powdb.d.ts +87 -25
  34. package/dist/powdb.js +419 -80
  35. package/dist/powql.d.ts +6 -0
  36. package/dist/powql.js +51 -27
  37. package/dist/query/builder.d.ts +60 -3
  38. package/dist/query/builder.js +291 -24
  39. package/dist/query/deferred.d.ts +7 -2
  40. package/dist/query/filters.d.ts +18 -0
  41. package/dist/query/filters.js +30 -0
  42. package/dist/query/types.d.ts +19 -0
  43. package/dist/schema-metadata.d.ts +77 -0
  44. package/dist/schema-metadata.js +313 -0
  45. package/dist/schema.d.ts +10 -0
  46. package/dist/sqlite.js +9 -90
  47. package/package.json +3 -3
package/dist/client.js CHANGED
@@ -763,14 +763,31 @@ export class TurbineClient {
763
763
  */
764
764
  async transaction(fn) {
765
765
  const client = await this.pool.connect();
766
+ /**
767
+ * Only true once BEGIN has actually succeeded. If BEGIN itself throws
768
+ * (e.g. a single-writer engine's transaction gate times out or rejects a
769
+ * re-entrant begin), issuing a "best-effort" ROLLBACK would be a stray
770
+ * statement from a context that never opened a transaction — on a driver
771
+ * with one shared engine handle (PowDB embedded) it would roll back a
772
+ * DIFFERENT caller's open transaction.
773
+ */
774
+ let began = false;
766
775
  try {
767
776
  await client.query(this.dialect.beginStatement());
777
+ began = true;
768
778
  const result = await fn(client);
769
779
  await client.query(this.dialect.commitStatement());
770
780
  return result;
771
781
  }
772
782
  catch (err) {
773
- await client.query(this.dialect.rollbackStatement());
783
+ if (began) {
784
+ try {
785
+ await client.query(this.dialect.rollbackStatement());
786
+ }
787
+ catch {
788
+ // Best-effort rollback — the connection may have died mid-query.
789
+ }
790
+ }
774
791
  throw err;
775
792
  }
776
793
  finally {
@@ -804,11 +821,21 @@ export class TurbineClient {
804
821
  }
805
822
  };
806
823
  let timedOut = false;
824
+ /**
825
+ * Only true once BEGIN has actually succeeded. If BEGIN itself throws —
826
+ * e.g. a single-writer engine's transaction gate times out in its FIFO
827
+ * queue or rejects a re-entrant begin (PowDB, E002/E017) — this context
828
+ * never opened a transaction, so the catch below must NOT issue its
829
+ * best-effort ROLLBACK: on a driver with one shared engine handle that
830
+ * stray ROLLBACK would tear down a DIFFERENT caller's open transaction.
831
+ */
832
+ let began = false;
807
833
  try {
808
834
  // BEGIN with optional isolation level — the dialect owns the keyword and
809
835
  // BEGIN+isolation composition (Postgres appends ` ISOLATION LEVEL …`).
810
836
  const isolationSql = options?.isolationLevel ? ISOLATION_LEVELS[options.isolationLevel] : undefined;
811
837
  await client.query(this.dialect.beginStatement(isolationSql));
838
+ began = true;
812
839
  // Apply transaction-local session context (RLS / multi-tenant GUCs).
813
840
  // Order matters: BEGIN -> isolation level (above) -> set_config loop ->
814
841
  // user fn. Any error here propagates to the catch below and rolls back
@@ -881,8 +908,11 @@ export class TurbineClient {
881
908
  // If the timeout fired we already destroyed the connection — issuing a
882
909
  // ROLLBACK on a released client would throw "Client has already been
883
910
  // released". Skip the rollback in that case (the backend rolled back
884
- // when its socket was closed).
885
- if (!timedOut && !released) {
911
+ // when its socket was closed). Likewise skip it when BEGIN never
912
+ // succeeded (`began` false) — there is no transaction to roll back and
913
+ // the stray statement could hit another caller's transaction on a
914
+ // shared-handle engine.
915
+ if (began && !timedOut && !released) {
886
916
  try {
887
917
  await client.query(this.dialect.rollbackStatement());
888
918
  }
@@ -23,6 +23,18 @@ export interface GenerateOptions {
23
23
  * library's dependency graph. Default: `false`.
24
24
  */
25
25
  zod?: boolean;
26
+ /**
27
+ * Omit the `Generated at: <ISO timestamp>` line from every generated file
28
+ * header (T-8b — reproducible codegen). With this set, byte-identical
29
+ * schemas regenerate to byte-identical output, so regens produce empty
30
+ * diffs. Default: `false` (timestamp included, unchanged behavior).
31
+ */
32
+ noTimestamp?: boolean;
33
+ }
34
+ /** Per-file generator options (subset of {@link GenerateOptions} the emitters need). */
35
+ export interface GenerateFileOptions {
36
+ /** Omit the `Generated at:` header line for reproducible output. */
37
+ noTimestamp?: boolean;
26
38
  }
27
39
  export declare function generate(options: GenerateOptions): {
28
40
  outDir: string;
@@ -33,7 +45,7 @@ export declare function generate(options: GenerateOptions): {
33
45
  * and *Relations brand-field interfaces). Exported so tests can pin the
34
46
  * generator output without writing files to disk.
35
47
  */
36
- export declare function generateTypes(schema: SchemaMetadata): string;
48
+ export declare function generateTypes(schema: SchemaMetadata, options?: GenerateFileOptions): string;
37
49
  /**
38
50
  * Generate the contents of `zod.ts`. Emits, per table, `XSchema` (the full
39
51
  * row), `XCreateSchema` (PK/defaulted/nullable columns optional, STORED
@@ -41,6 +53,6 @@ export declare function generateTypes(schema: SchemaMetadata): string;
41
53
  * columns omitted, every remaining column optional). Exported so tests can pin
42
54
  * the output without writing files.
43
55
  */
44
- export declare function generateZod(schema: SchemaMetadata): string;
45
- export declare function generateMetadata(schema: SchemaMetadata): string;
46
- export declare function generateIndex(schema: SchemaMetadata): string;
56
+ export declare function generateZod(schema: SchemaMetadata, options?: GenerateFileOptions): string;
57
+ export declare function generateMetadata(schema: SchemaMetadata, options?: GenerateFileOptions): string;
58
+ export declare function generateIndex(schema: SchemaMetadata, options?: GenerateFileOptions): string;
package/dist/generate.js CHANGED
@@ -50,21 +50,22 @@ export function generate(options) {
50
50
  }
51
51
  mkdirSync(outDir, { recursive: true });
52
52
  const files = [];
53
+ const fileOptions = { noTimestamp: options.noTimestamp };
53
54
  // Generate types.ts
54
- const typesContent = generateTypes(options.schema);
55
+ const typesContent = generateTypes(options.schema, fileOptions);
55
56
  writeFileSync(join(outDir, 'types.ts'), typesContent, 'utf-8');
56
57
  files.push('types.ts');
57
58
  // Generate metadata.ts
58
- const metadataContent = generateMetadata(options.schema);
59
+ const metadataContent = generateMetadata(options.schema, fileOptions);
59
60
  writeFileSync(join(outDir, 'metadata.ts'), metadataContent, 'utf-8');
60
61
  files.push('metadata.ts');
61
62
  // Generate index.ts (configured client)
62
- const indexContent = generateIndex(options.schema);
63
+ const indexContent = generateIndex(options.schema, fileOptions);
63
64
  writeFileSync(join(outDir, 'index.ts'), indexContent, 'utf-8');
64
65
  files.push('index.ts');
65
66
  // Generate zod.ts (optional — --zod flag)
66
67
  if (options.zod) {
67
- const zodContent = generateZod(options.schema);
68
+ const zodContent = generateZod(options.schema, fileOptions);
68
69
  writeFileSync(join(outDir, 'zod.ts'), zodContent, 'utf-8');
69
70
  files.push('zod.ts');
70
71
  }
@@ -73,24 +74,51 @@ export function generate(options) {
73
74
  // ---------------------------------------------------------------------------
74
75
  // types.ts generator
75
76
  // ---------------------------------------------------------------------------
76
- function generatedFileHeader() {
77
+ function generatedFileHeader(options) {
78
+ // `noTimestamp` omits the volatile line entirely (T-8b) so regenerating an
79
+ // unchanged schema produces byte-identical files.
77
80
  return [
78
81
  '/**',
79
82
  ' * Auto-generated by turbine-orm — DO NOT EDIT',
80
83
  ' *',
81
- ` * Generated at: ${new Date().toISOString()}`,
84
+ ...(options?.noTimestamp ? [] : [` * Generated at: ${new Date().toISOString()}`]),
82
85
  ' * @see https://turbineorm.dev',
83
86
  ' */',
84
87
  '',
85
88
  ];
86
89
  }
90
+ /**
91
+ * The relations of a table that are safe to surface in the generated TYPE
92
+ * layer. A relation whose name equals a scalar column field would shadow the
93
+ * column: `interface XWithY extends X` becomes TS2430, the `XCreate & { y?: … }`
94
+ * intersection collapses (TS2322), and neither the column nor the relation is
95
+ * targetable. Introspection no longer produces such names (they are
96
+ * disambiguated at the source), but hand-written or legacy metadata may —
97
+ * skip those relations here with a warning instead of emitting broken types.
98
+ * The runtime metadata (metadata.ts) still carries every relation.
99
+ */
100
+ function typeSafeRelations(table, warn = true) {
101
+ const columnFields = new Set(table.columns.map((c) => c.field));
102
+ const usable = [];
103
+ for (const [relName, rel] of Object.entries(table.relations)) {
104
+ if (columnFields.has(relName)) {
105
+ if (warn) {
106
+ console.warn(`[turbine] Relation "${relName}" on table "${table.name}" shadows a column field of the same name — ` +
107
+ `omitting it from the generated types. Rename the relation (or the column) to expose it.`);
108
+ }
109
+ continue;
110
+ }
111
+ usable.push([relName, rel]);
112
+ }
113
+ return usable;
114
+ }
87
115
  /**
88
116
  * Generate the contents of `types.ts` (entity interfaces, *Create / *Update,
89
117
  * and *Relations brand-field interfaces). Exported so tests can pin the
90
118
  * generator output without writing files to disk.
91
119
  */
92
- export function generateTypes(schema) {
93
- const lines = [...generatedFileHeader()];
120
+ export function generateTypes(schema, options) {
121
+ const lines = [...generatedFileHeader(options)];
94
122
  // We import UpdateOperatorInput so generated *Update types can express
95
123
  // atomic increment / decrement / multiply / divide / set operators on
96
124
  // numeric columns (TASK-3.4).
@@ -106,9 +134,15 @@ export function generateTypes(schema) {
106
134
  // `${TargetType}Relations` (for deep inference) or `{}` (the no-relations
107
135
  // default) into each `RelationDescriptor`. Built once up-front because
108
136
  // relations can point at tables we haven't iterated to yet.
137
+ // Relations that can be surfaced in the type layer, computed once per table
138
+ // (relations that would shadow a scalar column field are excluded + warned).
139
+ const safeRelationsByTable = new Map();
140
+ for (const t of Object.values(schema.tables)) {
141
+ safeRelationsByTable.set(t.name, typeSafeRelations(t));
142
+ }
109
143
  const tablesWithRelations = new Set();
110
144
  for (const t of Object.values(schema.tables)) {
111
- if (Object.keys(t.relations).length > 0)
145
+ if ((safeRelationsByTable.get(t.name) ?? []).length > 0)
112
146
  tablesWithRelations.add(t.name);
113
147
  }
114
148
  // Generate enum types
@@ -173,11 +207,12 @@ export function generateTypes(schema) {
173
207
  // any depth — `RelationRelations<R[K]>` reads the third type parameter
174
208
  // and threads it into the next recursion step. If the target table has
175
209
  // no relations of its own, the descriptor uses `{}` (the default).
176
- const hasRelations = Object.keys(table.relations).length > 0;
210
+ const safeRelations = safeRelationsByTable.get(table.name) ?? [];
211
+ const hasRelations = safeRelations.length > 0;
177
212
  if (hasRelations) {
178
213
  lines.push(`/** Available relations for the \`${table.name}\` table */`);
179
214
  lines.push(`export interface ${typeName}Relations {`);
180
- for (const [relName, rel] of Object.entries(table.relations)) {
215
+ for (const [relName, rel] of safeRelations) {
181
216
  const targetType = entityName(rel.to);
182
217
  // manyToMany is a collection too → 'many' cardinality (same as hasMany).
183
218
  const cardinality = rel.type === 'hasMany' || rel.type === 'manyToMany' ? "'many'" : "'one'";
@@ -187,7 +222,7 @@ export function generateTypes(schema) {
187
222
  lines.push('}');
188
223
  lines.push('');
189
224
  // --- Legacy per-relation interfaces (kept for backward compatibility) ---
190
- for (const [relName, rel] of Object.entries(table.relations)) {
225
+ for (const [relName, rel] of safeRelations) {
191
226
  const targetType = entityName(rel.to);
192
227
  if (rel.type === 'hasMany' || rel.type === 'manyToMany') {
193
228
  lines.push(`/** ${typeName} with \`${relName}\` relation loaded (${rel.type}: ${rel.to}) */`);
@@ -211,7 +246,8 @@ export function generateTypes(schema) {
211
246
  // ---------------------------------------------------------------------------
212
247
  for (const table of Object.values(schema.tables)) {
213
248
  const typeName = entityName(table.name);
214
- const hasRels = Object.keys(table.relations).length > 0;
249
+ const safeRelations = safeRelationsByTable.get(table.name) ?? [];
250
+ const hasRels = safeRelations.length > 0;
215
251
  // WhereUnique — union of unique constraint shapes, deduplicating PK
216
252
  const seen = new Set();
217
253
  const uniqueSets = [];
@@ -243,14 +279,14 @@ export function generateTypes(schema) {
243
279
  // CreateInput / UpdateInput — extends base type with optional relation fields
244
280
  if (hasRels) {
245
281
  lines.push(`export type ${typeName}CreateInput = ${typeName}Create & {`);
246
- for (const [relName, rel] of Object.entries(table.relations)) {
282
+ for (const [relName, rel] of safeRelations) {
247
283
  const targetType = entityName(rel.to);
248
284
  lines.push(` ${relName}?: ${targetType}NestedCreateInput;`);
249
285
  }
250
286
  lines.push('};');
251
287
  lines.push('');
252
288
  lines.push(`export type ${typeName}UpdateInput = ${typeName}Update & {`);
253
- for (const [relName, rel] of Object.entries(table.relations)) {
289
+ for (const [relName, rel] of safeRelations) {
254
290
  const targetType = entityName(rel.to);
255
291
  if (rel.type === 'hasMany') {
256
292
  lines.push(` ${relName}?: ${targetType}NestedUpdateInput;`);
@@ -266,7 +302,7 @@ export function generateTypes(schema) {
266
302
  // Emit NestedCreateInput, NestedUpdateInput, ConnectOrCreate for every table
267
303
  for (const table of Object.values(schema.tables)) {
268
304
  const typeName = entityName(table.name);
269
- const hasRels = Object.keys(table.relations).length > 0;
305
+ const hasRels = (safeRelationsByTable.get(table.name) ?? []).length > 0;
270
306
  // NestedCreateInput uses *CreateInput (which includes relation fields) when
271
307
  // the table has relations, otherwise falls back to the plain *Create type.
272
308
  const createRefType = hasRels ? `${typeName}CreateInput` : `${typeName}Create`;
@@ -351,8 +387,8 @@ function zodBaseType(col, enums) {
351
387
  * columns omitted, every remaining column optional). Exported so tests can pin
352
388
  * the output without writing files.
353
389
  */
354
- export function generateZod(schema) {
355
- const lines = [...generatedFileHeader()];
390
+ export function generateZod(schema, options) {
391
+ const lines = [...generatedFileHeader(options)];
356
392
  // `zod` is a USER dependency — this generated file imports it, but the Turbine
357
393
  // library runtime never does, so Zod stays out of the package's dep graph.
358
394
  lines.push("import { z } from 'zod';");
@@ -409,9 +445,9 @@ export function generateZod(schema) {
409
445
  // ---------------------------------------------------------------------------
410
446
  // metadata.ts generator
411
447
  // ---------------------------------------------------------------------------
412
- export function generateMetadata(schema) {
448
+ export function generateMetadata(schema, options) {
413
449
  const lines = [
414
- ...generatedFileHeader(),
450
+ ...generatedFileHeader(options),
415
451
  "import type { SchemaMetadata } from 'turbine-orm';",
416
452
  '',
417
453
  'export const SCHEMA: SchemaMetadata = {',
@@ -511,10 +547,15 @@ export function generateMetadata(schema) {
511
547
  // ---------------------------------------------------------------------------
512
548
  // index.ts generator (configured client with typed table accessors)
513
549
  // ---------------------------------------------------------------------------
514
- export function generateIndex(schema) {
550
+ export function generateIndex(schema, options) {
515
551
  const tableEntries = Object.values(schema.tables);
552
+ // Must mirror generateTypes: `XRelations` only exists in types.ts when the
553
+ // table has at least one type-safe (non-column-shadowing) relation.
554
+ const hasSafeRelations = new Map();
555
+ for (const t of tableEntries)
556
+ hasSafeRelations.set(t.name, typeSafeRelations(t, false).length > 0);
516
557
  const lines = [
517
- ...generatedFileHeader(),
558
+ ...generatedFileHeader(options),
518
559
  "import { TurbineClient as BaseTurbineClient, TransactionClient as BaseTransactionClient, QueryInterface } from 'turbine-orm';",
519
560
  "import type { TurbineConfig, TransactionOptions, DeferredQuery, PipelineResults } from 'turbine-orm';",
520
561
  "import { SCHEMA } from './metadata.js';",
@@ -523,7 +564,7 @@ export function generateIndex(schema) {
523
564
  const typeImports = [];
524
565
  for (const t of tableEntries) {
525
566
  typeImports.push(entityName(t.name));
526
- if (Object.keys(t.relations).length > 0) {
567
+ if (hasSafeRelations.get(t.name)) {
527
568
  typeImports.push(`${entityName(t.name)}Relations`);
528
569
  }
529
570
  }
@@ -545,7 +586,7 @@ export function generateIndex(schema) {
545
586
  for (const table of tableEntries) {
546
587
  const typeName = entityName(table.name);
547
588
  const accessor = snakeToCamelStr(table.name);
548
- const hasRelations = Object.keys(table.relations).length > 0;
589
+ const hasRelations = hasSafeRelations.get(table.name) === true;
549
590
  const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
550
591
  lines.push(` /** Query interface for the \`${table.name}\` table (transaction-scoped) */`);
551
592
  lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
@@ -587,7 +628,7 @@ export function generateIndex(schema) {
587
628
  for (const table of tableEntries) {
588
629
  const typeName = entityName(table.name);
589
630
  const accessor = snakeToCamelStr(table.name);
590
- const hasRelations = Object.keys(table.relations).length > 0;
631
+ const hasRelations = hasSafeRelations.get(table.name) === true;
591
632
  const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
592
633
  lines.push(` /** Query interface for the \`${table.name}\` table */`);
593
634
  lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
@@ -670,6 +711,11 @@ function serializeColumn(col) {
670
711
  `arrayType: '${escSQ(col.arrayType ?? col.pgArrayType)}'`,
671
712
  `pgArrayType: '${escSQ(col.pgArrayType)}'`,
672
713
  ];
714
+ // Cross-schema type marker — introspection records it only for types living
715
+ // outside the introspected schema; it must survive codegen or the runtime
716
+ // enum-cast guard in query/builder.ts loses the signal (N-5).
717
+ if (col.pgTypeSchema !== undefined)
718
+ parts.push(`pgTypeSchema: '${escSQ(col.pgTypeSchema)}'`);
673
719
  // Emit isGenerated only when set (server-generated serial/identity), so the
674
720
  // output stays byte-identical for the common client-default columns.
675
721
  if (col.isGenerated)
package/dist/index.d.ts CHANGED
@@ -48,6 +48,7 @@ export { type ActiveSubscription, type NotificationHandler, type Subscription, v
48
48
  export type { CheckMetadata, ColumnMetadata, IndexMetadata, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
49
49
  export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
50
50
  export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, defineSchema, type ManyToManyDef, type ReferenceDef, type SchemaDef, type TableDef, table, } from './schema-builder.js';
51
+ export { schemaDefToMetadata } from './schema-metadata.js';
51
52
  export { type AlterColumnDef, type AlterDef, type DiffResult, type PushResult, type SchemaSqlOptions, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
52
53
  export { type DefinedSeed, defineSeed, type SeedFunction } from './seed.js';
53
54
  export { type TurbineHttpOptions, turbineHttp } from './serverless.js';
package/dist/index.js CHANGED
@@ -56,6 +56,8 @@ export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs,
56
56
  export { applyManyToManyRelations, ColumnBuilder, column, defineSchema,
57
57
  // Legacy compat (deprecated — use object format with defineSchema)
58
58
  table, } from './schema-builder.js';
59
+ // Schema metadata bridge — defineSchema() → SchemaMetadata without a live DB
60
+ export { schemaDefToMetadata } from './schema-metadata.js';
59
61
  // Schema SQL — generate DDL, diff, and push
60
62
  export { schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
61
63
  // Seed helper
@@ -8,7 +8,7 @@
8
8
  * This is the foundation of `npx turbine generate`.
9
9
  */
10
10
  import { type Dialect } from './dialect.js';
11
- import { type ReferentialAction, type SchemaMetadata } from './schema.js';
11
+ import { type ColumnMetadata, type ReferentialAction, type RelationDef, type SchemaMetadata } from './schema.js';
12
12
  /**
13
13
  * Map a `pg_constraint.confdeltype` / `confupdtype` character to a
14
14
  * {@link ReferentialAction}. Postgres encodes: `a` = NO ACTION, `r` = RESTRICT,
@@ -56,3 +56,96 @@ export declare function introspectPostgresCatalog(options: IntrospectOptions): P
56
56
  * balanced outer paren pair; leaves anything unexpected untouched.
57
57
  */
58
58
  export declare function stripCheckWrapper(def: string): string;
59
+ /** A foreign-key constraint grouped by constraint name (composite FKs carry column arrays). */
60
+ export interface ForeignKeyEntry {
61
+ sourceTable: string;
62
+ sourceColumns: string[];
63
+ targetTable: string;
64
+ targetColumns: string[];
65
+ constraintName: string;
66
+ }
67
+ /**
68
+ * Derive a belongsTo relation name from its FK column. Strips a trailing
69
+ * `_id` (snake_case) or `Id` (camelCase column names — common in Prisma-ported
70
+ * schemas where columns are quoted camelCase identifiers), then camelCases:
71
+ * `current_version_id` and `currentVersionId` both yield `currentVersion`.
72
+ * Stripping is what keeps the scalar FK field (`currentVersionId`) targetable
73
+ * alongside the relation. A column literally named `id` (nothing left after
74
+ * stripping) keeps its own name.
75
+ */
76
+ export declare function relationNameFromColumn(column: string): string;
77
+ /**
78
+ * True for the tsType forms a json/jsonb column maps to (`unknown`, nullable
79
+ * `unknown | null`). A relation shadowing such a column is a HISTORICAL shadow
80
+ * that worked at runtime and compiled (`unknown` absorbs the relation
81
+ * payload), so the legacy-first naming keeps it instead of renaming.
82
+ */
83
+ export declare function isUnknownTsType(tsType: string): boolean;
84
+ /**
85
+ * Build the belongsTo/hasMany relation maps for every table from its foreign
86
+ * keys. Naming rules (LEGACY-FIRST — a relation name that previously worked at
87
+ * runtime must never change out from under a regenerating app):
88
+ *
89
+ * 1. First compute the historical derivation exactly as it shipped before
90
+ * the collision guard existed: belongsTo strips a case-SENSITIVE `_id`
91
+ * suffix (`snakeToCamel(col.replace(/_id$/, ''))` when several FKs point
92
+ * at the same target, else the singularized target table), and hasMany is
93
+ * `snakeToCamel(`${source}_by_${strippedColumn}`)` (else the source
94
+ * table). If that legacy name is free, KEEP IT — even when it looks odd
95
+ * (`blogPostsByAuthorId`, `postsBy_Author`): those names were collision-
96
+ * free and worked, so regenerating must not rename them.
97
+ * 2. If the legacy name collides ONLY with a scalar column whose tsType is
98
+ * `unknown` (json/jsonb), keep it anyway with a warning: the shadow is
99
+ * historical, ran fine at runtime, and compiled (`unknown` absorbs the
100
+ * relation payload; generate.ts's typeSafeRelations omits the relation
101
+ * from the type layer).
102
+ * 3. On a genuine collision (concrete-typed column shadow, or a previously
103
+ * assigned relation), fall back to the modern derivation — the `_id`/`Id`
104
+ * case-insensitive strip of {@link relationNameFromColumn} plus the
105
+ * `By`-composed reverse name — which fixes the camelCase-FK shadowing
106
+ * shapes that were actually BROKEN before (relation name === scalar FK
107
+ * field → unusable types).
108
+ * 4. Last resort: deterministic `Rel`/`Rel2` suffix + warning.
109
+ *
110
+ * @param columnFieldsByTable camelCase column *fields* per table — used to
111
+ * guarantee relations never shadow concrete-typed scalar columns.
112
+ * @param unknownTypedFieldsByTable subset of the column fields whose tsType is
113
+ * `unknown` (json/jsonb) — legacy shadows of these are preserved (rule 2).
114
+ */
115
+ export declare function buildRelationsFromForeignKeys(foreignKeys: ForeignKeyEntry[], columnFieldsByTable: Map<string, Set<string>>, fkActions?: Map<string, {
116
+ onDelete: ReferentialAction;
117
+ onUpdate: ReferentialAction;
118
+ }>, unknownTypedFieldsByTable?: Map<string, Set<string>>): Map<string, Record<string, RelationDef>>;
119
+ /**
120
+ * Conservative auto-`manyToMany` detection over pure junction tables, shared
121
+ * by the Postgres introspector, the engine introspectors (SQLite / MySQL /
122
+ * MSSQL), the MCP server, and `schemaDefToMetadata()` so all surfaces derive
123
+ * IDENTICAL relation names for the same logical schema.
124
+ *
125
+ * A table J is a PURE junction only when ALL of these hold:
126
+ * 1. J's primary key is exactly two columns.
127
+ * 2. J has exactly two FKs, each single-column.
128
+ * 3. Each FK's source column is one of J's two PK columns.
129
+ * 4. The two FKs target two DISTINCT tables (A and B).
130
+ * 5. J has no payload columns beyond the two FK/PK columns.
131
+ *
132
+ * For such a J linking A and B this ADDS a `manyToMany` on A → B and B → A
133
+ * routed `through` J. It never removes or renames an existing relation:
134
+ * - an already-assigned relation with the same name → SKIP (additive-only,
135
+ * unchanged historical behavior);
136
+ * - a shadowed json/jsonb (`unknown`-typed) column → keep the historical
137
+ * name + warn (it worked at runtime and compiled);
138
+ * - a shadowed concrete-typed column → deterministic `Rel` suffix + warn
139
+ * instead of silently dropping the relation.
140
+ */
141
+ export declare function addAutoManyToManyRelations(tableNames: Iterable<string>, foreignKeys: ForeignKeyEntry[], pkByTable: Map<string, string[]>, columnNamesByTable: Map<string, string[]>, relationsByTable: Map<string, Record<string, RelationDef>>, columnFieldsByTable?: Map<string, Set<string>>, unknownTypedFieldsByTable?: Map<string, Set<string>>): void;
142
+ /**
143
+ * One-stop relation derivation for the engine introspectors (SQLite / MySQL /
144
+ * MSSQL): filters the FK list to the introspected table set, seeds the
145
+ * taken-name / json-shadow maps from the engine's column metadata, and runs
146
+ * the SAME `buildRelationsFromForeignKeys` + `addAutoManyToManyRelations`
147
+ * pipeline as the Postgres introspector — so every engine derives identical
148
+ * relation names for the same logical schema (the engines previously carried
149
+ * stale copies of a retired naming scheme).
150
+ */
151
+ export declare function deriveEngineRelations(tableNames: string[], foreignKeys: ForeignKeyEntry[], pkByTable: Map<string, string[]>, columnsByTable: Map<string, Pick<ColumnMetadata, 'name' | 'field' | 'tsType'>[]>): Map<string, Record<string, RelationDef>>;