turbine-orm 0.65.0 → 0.66.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 (142) hide show
  1. package/README.md +34 -32
  2. package/dist/adapters/cockroachdb.js +21 -3
  3. package/dist/adapters/index.d.ts +15 -0
  4. package/dist/adapters/yugabytedb.js +20 -3
  5. package/dist/cjs/adapters/cockroachdb.js +21 -3
  6. package/dist/cjs/adapters/index.d.ts +15 -0
  7. package/dist/cjs/adapters/yugabytedb.js +20 -3
  8. package/dist/cjs/cli/destructive.d.ts +18 -4
  9. package/dist/cjs/cli/destructive.js +230 -122
  10. package/dist/cjs/cli/index.d.ts +21 -4
  11. package/dist/cjs/cli/index.js +119 -22
  12. package/dist/cjs/cli/mcp.d.ts +28 -8
  13. package/dist/cjs/cli/mcp.js +170 -127
  14. package/dist/cjs/cli/migrate.d.ts +134 -13
  15. package/dist/cjs/cli/migrate.js +349 -241
  16. package/dist/cjs/cli/pii-predicate-guard.d.ts +112 -0
  17. package/dist/cjs/cli/pii-predicate-guard.js +390 -0
  18. package/dist/cjs/cli/prisma-resolve.js +75 -4
  19. package/dist/cjs/cli/prisma-schema.d.ts +17 -1
  20. package/dist/cjs/cli/prisma-schema.js +83 -17
  21. package/dist/cjs/cli/sql-statements.d.ts +125 -0
  22. package/dist/cjs/cli/sql-statements.js +378 -0
  23. package/dist/cjs/cli/studio.js +49 -118
  24. package/dist/cjs/cli/ui.d.ts +1 -1
  25. package/dist/cjs/client.d.ts +43 -0
  26. package/dist/cjs/client.js +125 -6
  27. package/dist/cjs/dialect.d.ts +123 -0
  28. package/dist/cjs/dialect.js +33 -0
  29. package/dist/cjs/errors.d.ts +74 -1
  30. package/dist/cjs/errors.js +239 -25
  31. package/dist/cjs/index-advisor.d.ts +33 -1
  32. package/dist/cjs/index-advisor.js +32 -1
  33. package/dist/cjs/introspect.d.ts +48 -0
  34. package/dist/cjs/introspect.js +222 -91
  35. package/dist/cjs/mssql.js +43 -1
  36. package/dist/cjs/mysql.d.ts +5 -2
  37. package/dist/cjs/mysql.js +202 -17
  38. package/dist/cjs/nested-write.js +6 -1
  39. package/dist/cjs/pipeline-submittable.js +17 -3
  40. package/dist/cjs/pipeline.js +75 -9
  41. package/dist/cjs/powdb.d.ts +23 -0
  42. package/dist/cjs/powdb.js +33 -1
  43. package/dist/cjs/powql.d.ts +61 -9
  44. package/dist/cjs/powql.js +186 -49
  45. package/dist/cjs/prisma-compat.js +160 -41
  46. package/dist/cjs/query/aggregates.d.ts +1 -1
  47. package/dist/cjs/query/aggregates.js +80 -18
  48. package/dist/cjs/query/batched-loader.d.ts +10 -0
  49. package/dist/cjs/query/batched-loader.js +268 -7
  50. package/dist/cjs/query/builder.d.ts +73 -0
  51. package/dist/cjs/query/builder.js +225 -28
  52. package/dist/cjs/query/filters.d.ts +162 -0
  53. package/dist/cjs/query/filters.js +250 -1
  54. package/dist/cjs/query/relations.d.ts +10 -10
  55. package/dist/cjs/query/relations.js +93 -12
  56. package/dist/cjs/query/types.d.ts +14 -1
  57. package/dist/cjs/query/utils.d.ts +146 -2
  58. package/dist/cjs/query/utils.js +210 -4
  59. package/dist/cjs/query/warn-registry.d.ts +10 -0
  60. package/dist/cjs/query/warn-registry.js +10 -0
  61. package/dist/cjs/query/where-compile.d.ts +30 -0
  62. package/dist/cjs/query/where-compile.js +41 -0
  63. package/dist/cjs/query/where.d.ts +128 -13
  64. package/dist/cjs/query/where.js +215 -77
  65. package/dist/cjs/query/writes.d.ts +1 -1
  66. package/dist/cjs/query/writes.js +39 -15
  67. package/dist/cjs/schema-builder.d.ts +2 -1
  68. package/dist/cjs/schema-sql.d.ts +94 -4
  69. package/dist/cjs/schema-sql.js +506 -30
  70. package/dist/cjs/schema.d.ts +3 -1
  71. package/dist/cjs/sqlite.d.ts +6 -0
  72. package/dist/cjs/sqlite.js +151 -10
  73. package/dist/cjs/typed-sql.d.ts +29 -1
  74. package/dist/cjs/typed-sql.js +30 -12
  75. package/dist/cli/destructive.d.ts +18 -4
  76. package/dist/cli/destructive.js +229 -121
  77. package/dist/cli/index.d.ts +21 -4
  78. package/dist/cli/index.js +120 -24
  79. package/dist/cli/mcp.d.ts +28 -8
  80. package/dist/cli/mcp.js +172 -129
  81. package/dist/cli/migrate.d.ts +134 -13
  82. package/dist/cli/migrate.js +347 -238
  83. package/dist/cli/pii-predicate-guard.d.ts +112 -0
  84. package/dist/cli/pii-predicate-guard.js +386 -0
  85. package/dist/cli/prisma-resolve.js +75 -4
  86. package/dist/cli/prisma-schema.d.ts +17 -1
  87. package/dist/cli/prisma-schema.js +83 -17
  88. package/dist/cli/sql-statements.d.ts +125 -0
  89. package/dist/cli/sql-statements.js +373 -0
  90. package/dist/cli/studio.js +49 -118
  91. package/dist/cli/ui.d.ts +1 -1
  92. package/dist/client.d.ts +43 -0
  93. package/dist/client.js +126 -7
  94. package/dist/dialect.d.ts +123 -0
  95. package/dist/dialect.js +33 -0
  96. package/dist/errors.d.ts +74 -1
  97. package/dist/errors.js +228 -19
  98. package/dist/index-advisor.d.ts +33 -1
  99. package/dist/index-advisor.js +31 -1
  100. package/dist/introspect.d.ts +48 -0
  101. package/dist/introspect.js +221 -91
  102. package/dist/mssql.js +44 -2
  103. package/dist/mysql.d.ts +5 -2
  104. package/dist/mysql.js +203 -18
  105. package/dist/nested-write.js +7 -2
  106. package/dist/pipeline-submittable.js +18 -4
  107. package/dist/pipeline.js +76 -10
  108. package/dist/powdb.d.ts +23 -0
  109. package/dist/powdb.js +33 -2
  110. package/dist/powql.d.ts +61 -9
  111. package/dist/powql.js +187 -50
  112. package/dist/prisma-compat.js +160 -41
  113. package/dist/query/aggregates.d.ts +1 -1
  114. package/dist/query/aggregates.js +82 -20
  115. package/dist/query/batched-loader.d.ts +10 -0
  116. package/dist/query/batched-loader.js +270 -9
  117. package/dist/query/builder.d.ts +73 -0
  118. package/dist/query/builder.js +226 -30
  119. package/dist/query/filters.d.ts +162 -0
  120. package/dist/query/filters.js +246 -1
  121. package/dist/query/relations.d.ts +10 -10
  122. package/dist/query/relations.js +94 -14
  123. package/dist/query/types.d.ts +14 -1
  124. package/dist/query/utils.d.ts +146 -2
  125. package/dist/query/utils.js +204 -3
  126. package/dist/query/warn-registry.d.ts +10 -0
  127. package/dist/query/warn-registry.js +10 -0
  128. package/dist/query/where-compile.d.ts +30 -0
  129. package/dist/query/where-compile.js +40 -1
  130. package/dist/query/where.d.ts +128 -13
  131. package/dist/query/where.js +216 -80
  132. package/dist/query/writes.d.ts +1 -1
  133. package/dist/query/writes.js +40 -16
  134. package/dist/schema-builder.d.ts +2 -1
  135. package/dist/schema-sql.d.ts +94 -4
  136. package/dist/schema-sql.js +505 -30
  137. package/dist/schema.d.ts +3 -1
  138. package/dist/sqlite.d.ts +6 -0
  139. package/dist/sqlite.js +151 -10
  140. package/dist/typed-sql.d.ts +29 -1
  141. package/dist/typed-sql.js +30 -12
  142. package/package.json +6 -4
package/dist/cli/index.js CHANGED
@@ -24,12 +24,12 @@
24
24
  * npx turbine init --url postgres://...
25
25
  * npx turbine migrate create add_users_table
26
26
  */
27
- import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync, } from 'node:fs';
27
+ import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from 'node:fs';
28
28
  import { tmpdir } from 'node:os';
29
29
  import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path';
30
30
  import { pathToFileURL } from 'node:url';
31
31
  import { generate, generatePrismaMap } from '../generate.js';
32
- import { buildCreateIndexSql, buildDropIndexSql, collectDoctorProbeIndexNames, collectRelationProbeColumns, findMissingRelationIndexes, } from '../index-advisor.js';
32
+ import { buildCreateIndexSql, buildCreateIndexStatements, buildDropIndexSql, collectDoctorProbeIndexNames, collectRelationProbeColumns, findMissingRelationIndexes, } from '../index-advisor.js';
33
33
  import { auditDoctorIndexes, collectStatsSnapshot, collectTableHeat, findInvalidIndexes, findRedundantIndexes, findUnusedIndexes, formatBytes, isSnapshotUsable, STATS_THRESHOLDS, scoreMissingIndex, } from '../index-stats.js';
34
34
  import { introspect } from '../introspect.js';
35
35
  import { collectDivergenceCandidateColumns, collectDivergenceOrderColumns, findPlanDivergence, PLAN_DIVERGENCE_THRESHOLDS, } from '../plan-divergence.js';
@@ -911,15 +911,21 @@ async function runInitPush(config, url) {
911
911
  }
912
912
  const schemaDef = await loadSchemaFile(config.schemaFile);
913
913
  const spinner = new Spinner('Computing schema diff').start();
914
- const diff = await schemaDiff(schemaDef, url);
914
+ const diff = await schemaDiff(schemaDef, url, { schema: config.schema });
915
915
  if (diff.statements.length === 0) {
916
916
  spinner.succeed('Database already in sync: nothing to push');
917
917
  return;
918
918
  }
919
919
  spinner.succeed(`Found ${bold(String(diff.statements.length))} change(s) to apply`);
920
+ // Same as `push`: a type change carries a warning naming what it costs the
921
+ // existing rows, and it is worth no less here than on the standalone command.
922
+ if (diff.warnings && diff.warnings.length > 0) {
923
+ for (const w of diff.warnings)
924
+ warn(w);
925
+ }
920
926
  const pushSpinner = new Spinner('Applying schema').start();
921
927
  try {
922
- const result = await schemaPush(schemaDef, url, { precomputedDiff: diff });
928
+ const result = await schemaPush(schemaDef, url, { precomputedDiff: diff, schema: config.schema });
923
929
  pushSpinner.succeed(`Applied ${bold(String(result.statementsExecuted))} statement(s)`);
924
930
  }
925
931
  catch (err) {
@@ -930,7 +936,11 @@ async function runInitPush(config, url) {
930
936
  warn('Push aborted: no schema changes were applied.');
931
937
  return;
932
938
  }
933
- const result = await schemaPush(schemaDef, url, { allowDestructive: true, precomputedDiff: diff });
939
+ const result = await schemaPush(schemaDef, url, {
940
+ allowDestructive: true,
941
+ precomputedDiff: diff,
942
+ schema: config.schema,
943
+ });
934
944
  success(`Applied ${bold(String(result.statementsExecuted))} statement(s)`);
935
945
  }
936
946
  }
@@ -1405,19 +1415,57 @@ export function resolveMigrateFromPrismaUrl(configUrl, ast, env) {
1405
1415
  }
1406
1416
  return { source: 'none', missingVariables: lookup.missingVariables };
1407
1417
  }
1418
+ /**
1419
+ * Read a Prisma schema from a FILE or from a multi-file schema DIRECTORY
1420
+ * (Prisma >= 5.15, GA). A directory used to reach `readFileSync` unchanged and
1421
+ * die with a raw `EISDIR`, which is a stack trace rather than an answer.
1422
+ *
1423
+ * Prisma treats every `.prisma` file under the directory as one logical schema,
1424
+ * so they are concatenated in sorted order (stable across machines) and parsed
1425
+ * as a single source. Line numbers in any parse error then refer to that
1426
+ * concatenation, so each file is introduced by a comment naming it.
1427
+ */
1428
+ export function readPrismaSchemaSource(path) {
1429
+ if (!statSync(path).isDirectory())
1430
+ return readFileSync(path, 'utf-8');
1431
+ const files = readdirSync(path)
1432
+ .filter((f) => f.endsWith('.prisma'))
1433
+ .sort();
1434
+ if (files.length === 0) {
1435
+ throw new Error(`No .prisma files found in directory ${path}.`);
1436
+ }
1437
+ return files.map((f) => `// file: ${f}\n${readFileSync(join(path, f), 'utf-8')}`).join('\n');
1438
+ }
1408
1439
  async function cmdMigrateFromPrisma(args, config) {
1409
1440
  banner();
1410
- // `--schema` is the Prisma schema file path in this command.
1411
- const prismaPath = resolve(args.schema ?? 'prisma/schema.prisma');
1441
+ // `--schema` is the Prisma schema file path in this command. It may also name
1442
+ // a DIRECTORY (Prisma's multi-file schema, GA since 5.15), and the default
1443
+ // `prisma/schema.prisma` falls back to `prisma/schema/` for the same reason.
1444
+ const explicit = args.schema !== undefined;
1445
+ let prismaPath = resolve(args.schema ?? 'prisma/schema.prisma');
1446
+ if (!explicit && !existsSync(prismaPath) && existsSync(resolve('prisma/schema'))) {
1447
+ prismaPath = resolve('prisma/schema');
1448
+ }
1412
1449
  if (!existsSync(prismaPath)) {
1413
- error(`Prisma schema file not found: ${cyan(prismaPath)}`);
1450
+ error(`Prisma schema not found: ${cyan(prismaPath)}`);
1414
1451
  newline();
1415
- console.log(` ${dim('Point at it with')} ${cyan('--schema <path/to/schema.prisma>')} ${dim('(default: prisma/schema.prisma).')}`);
1452
+ console.log(` ${dim('Point at it with')} ${cyan('--schema <path>')} ${dim('(a .prisma file, or a directory of them;')}`);
1453
+ console.log(` ${dim('default: prisma/schema.prisma, then prisma/schema/).')}`);
1416
1454
  newline();
1417
1455
  process.exit(1);
1418
1456
  }
1419
1457
  // Parse (fatal only on a construct we must understand).
1420
- const source = readFileSync(prismaPath, 'utf-8');
1458
+ let source;
1459
+ try {
1460
+ source = readPrismaSchemaSource(prismaPath);
1461
+ }
1462
+ catch (err) {
1463
+ newline();
1464
+ error(`Could not read ${cyan(prismaPath)}`);
1465
+ console.log(` ${red(err instanceof Error ? err.message : String(err))}`);
1466
+ newline();
1467
+ process.exit(1);
1468
+ }
1421
1469
  let ast;
1422
1470
  try {
1423
1471
  ast = parsePrismaSchema(source);
@@ -1489,6 +1537,18 @@ async function cmdMigrateFromPrisma(args, config) {
1489
1537
  console.log(` ${marker} ${line}`);
1490
1538
  }
1491
1539
  newline();
1540
+ // Parser notes on the CONSOLE, not only in the Markdown report.
1541
+ //
1542
+ // `--no-db` is the first command anyone runs and the one place a parse
1543
+ // problem should surface, and it was the one place that could not: resolution
1544
+ // is skipped, `hasUnresolved` is false by construction, so the run printed a
1545
+ // clean summary and exited 0 no matter what the parser had thrown away.
1546
+ if (result.parseWarnings.length > 0) {
1547
+ header('Parser notes');
1548
+ for (const w of result.parseWarnings)
1549
+ console.log(` ${yellow(symbols.warning)} ${w}`);
1550
+ newline();
1551
+ }
1492
1552
  // Write outputs into the generate outDir.
1493
1553
  const outDir = resolve(config.out);
1494
1554
  const rel = relative(process.cwd(), outDir);
@@ -1536,6 +1596,15 @@ async function cmdMigrateFromPrisma(args, config) {
1536
1596
  newline();
1537
1597
  process.exit(1);
1538
1598
  }
1599
+ // `--no-db` has no resolution to fail on, so parser notes are the ONLY signal
1600
+ // it can carry. Treated like an unresolved item, and cleared by the same
1601
+ // `--allow-partial` opt-in, so "exit 0" means the same thing in both modes.
1602
+ if (args.noDb && result.parseWarnings.length > 0 && !args.allowPartial) {
1603
+ warn(`${result.parseWarnings.length} parser note(s) above (also in the report). ` +
1604
+ 'Re-run with --allow-partial to accept them.');
1605
+ newline();
1606
+ process.exit(1);
1607
+ }
1539
1608
  if (args.noDb) {
1540
1609
  info(`Parse-only report written. Re-run without ${cyan('--no-db')} against your database to resolve names.`);
1541
1610
  }
@@ -1558,7 +1627,7 @@ async function cmdPush(args, config) {
1558
1627
  info(`Schema defines ${bold(String(tableCount))} tables`);
1559
1628
  // Compute diff
1560
1629
  const diffSpinner = new Spinner('Computing schema diff').start();
1561
- const diff = await schemaDiff(schemaDef, url);
1630
+ const diff = await schemaDiff(schemaDef, url, { schema: config.schema });
1562
1631
  if (diff.statements.length === 0 && diff.drop.length === 0) {
1563
1632
  diffSpinner.succeed('Database is already in sync');
1564
1633
  newline();
@@ -1633,7 +1702,11 @@ async function cmdPush(args, config) {
1633
1702
  const pushSpinner = new Spinner('Applying changes').start();
1634
1703
  let result;
1635
1704
  try {
1636
- result = await schemaPush(schemaDef, url, { allowDestructive: args.allowDestructive, precomputedDiff: diff });
1705
+ result = await schemaPush(schemaDef, url, {
1706
+ allowDestructive: args.allowDestructive,
1707
+ precomputedDiff: diff,
1708
+ schema: config.schema,
1709
+ });
1637
1710
  }
1638
1711
  catch (err) {
1639
1712
  if (!(err instanceof DestructivePushRefusal))
@@ -1645,7 +1718,11 @@ async function cmdPush(args, config) {
1645
1718
  process.exit(1);
1646
1719
  }
1647
1720
  pushSpinner.start();
1648
- result = await schemaPush(schemaDef, url, { allowDestructive: true, precomputedDiff: diff });
1721
+ result = await schemaPush(schemaDef, url, {
1722
+ allowDestructive: true,
1723
+ precomputedDiff: diff,
1724
+ schema: config.schema,
1725
+ });
1649
1726
  }
1650
1727
  pushSpinner.succeed(`Applied ${bold(String(result.statementsExecuted))} statement(s)`);
1651
1728
  if (result.tablesCreated.length > 0) {
@@ -1759,7 +1836,7 @@ async function cmdMigrateCreate(args, config) {
1759
1836
  newline();
1760
1837
  const schemaDef = await loadSchemaFile(config.schemaFile);
1761
1838
  const diffSpinner = new Spinner('Computing schema diff').start();
1762
- const diff = await schemaDiff(schemaDef, url);
1839
+ const diff = await schemaDiff(schemaDef, url, { schema: config.schema });
1763
1840
  if (diff.statements.length === 0) {
1764
1841
  diffSpinner.succeed('Database is already in sync: nothing to migrate');
1765
1842
  newline();
@@ -1836,7 +1913,7 @@ async function cmdMigrateCreate(args, config) {
1836
1913
  newline();
1837
1914
  const schemaDef = await loadSchemaFile(config.schemaFile);
1838
1915
  const diffSpinner = new Spinner('Computing schema diff').start();
1839
- const diff = await schemaDiff(schemaDef, url);
1916
+ const diff = await schemaDiff(schemaDef, url, { schema: config.schema });
1840
1917
  if (diff.statements.length === 0) {
1841
1918
  diffSpinner.succeed('Database is already in sync: nothing to migrate');
1842
1919
  newline();
@@ -2496,9 +2573,24 @@ const TIER_LABEL = {
2496
2573
  'take-deliberately': 'TAKE DELIBERATELY',
2497
2574
  scrutinize: 'SCRUTINIZE',
2498
2575
  };
2499
- /** Build the `CREATE INDEX` for a finding, honoring the partial-null suggestion. */
2576
+ /**
2577
+ * The `CREATE INDEX` for a finding (display / JSON only), honoring the
2578
+ * partial-null suggestion. The migration writer uses
2579
+ * {@link buildCreateIndexStatements} instead, which adds the preceding
2580
+ * `DROP INDEX CONCURRENTLY IF EXISTS` that makes a rerun converge.
2581
+ */
2500
2582
  function doctorCreateSql(f, opts) {
2501
2583
  return buildCreateIndexSql(f.missing.table, f.missing.columns, f.missing.indexName, {
2584
+ concurrently: opts.concurrently,
2585
+ // The report line is advice a human may copy into psql, where the
2586
+ // guard is genuinely useful and there is no corpse to skip.
2587
+ ifNotExists: true,
2588
+ partialNotNull: f.score.partialNotNull,
2589
+ });
2590
+ }
2591
+ /** The statements the fix migration's UP body runs for one finding. */
2592
+ function doctorCreateStatements(f, opts) {
2593
+ return buildCreateIndexStatements(f.missing.table, f.missing.columns, f.missing.indexName, {
2502
2594
  concurrently: opts.concurrently,
2503
2595
  partialNotNull: f.score.partialNotNull,
2504
2596
  });
@@ -2511,13 +2603,17 @@ const CONCURRENTLY_RECIPE_COMMENT = [
2511
2603
  '--',
2512
2604
  '-- 1. Idempotency is required. This migration is recorded only after ALL',
2513
2605
  '-- statements succeed; a mid-file failure leaves earlier indexes built and',
2514
- '-- the migration unrecorded, so a rerun must be safe. IF NOT EXISTS keeps',
2515
- '-- each CREATE idempotent.',
2516
- '-- 2. The INVALID-index trap. A CREATE INDEX CONCURRENTLY that fails partway',
2517
- '-- leaves an INVALID index behind. On rerun, IF NOT EXISTS SKIPS that',
2518
- '-- corpse (the name already exists), so the index is never actually built.',
2519
- '-- Fix: DROP INDEX CONCURRENTLY the invalid index, then rerun. Run',
2520
- '-- "turbine doctor" to list invalid indexes.',
2606
+ '-- the migration unrecorded, so a rerun must be safe.',
2607
+ '-- 2. The INVALID-index trap, and why each CREATE is preceded by a DROP.',
2608
+ '-- A CREATE INDEX CONCURRENTLY that fails partway leaves an INVALID index',
2609
+ '-- behind, under the name it was asked for. IF NOT EXISTS matches on the',
2610
+ '-- NAME and not on validity, so with it a rerun SKIPS the corpse and the',
2611
+ '-- index is never actually built: the migration records as applied while',
2612
+ '-- the index doctor reported is still missing. The DROP INDEX',
2613
+ '-- CONCURRENTLY IF EXISTS line ahead of each CREATE is a no-op on the',
2614
+ '-- first run (the index does not exist yet, which is why it was',
2615
+ '-- proposed) and clears the corpse on a rerun, so rerunning converges on',
2616
+ '-- a VALID index. Run "turbine doctor" to list invalid indexes.',
2521
2617
  '-- 3. Locking. CREATE INDEX CONCURRENTLY waits for every transaction that can',
2522
2618
  '-- see the table to finish. A long-running transaction makes it wait and',
2523
2619
  '-- makes "migrate up" look hung. For bounded waits, SET lock_timeout /',
@@ -3231,7 +3327,7 @@ function renderPlanDivergence(divergence, attached) {
3231
3327
  /** Write the --fix migration (CONCURRENTLY + directive by default; plain with --no-concurrently). */
3232
3328
  function renderFixMigration(findings, config, args) {
3233
3329
  const concurrently = args.noConcurrently !== true;
3234
- const up = findings.map((f) => doctorCreateSql(f, { concurrently })).join('\n');
3330
+ const up = findings.flatMap((f) => doctorCreateStatements(f, { concurrently })).join('\n');
3235
3331
  const down = findings.map((f) => buildDropIndexSql(f.missing.indexName, { concurrently })).join('\n');
3236
3332
  let file;
3237
3333
  if (concurrently) {
package/dist/cli/mcp.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Readable, Writable } from 'node:stream';
2
2
  import pg from 'pg';
3
- import { type ColumnMetadata, type RelationDef } from '../schema.js';
3
+ import { type ColumnMetadata, type IndexMetadata, type RelationDef } from '../schema.js';
4
4
  export interface McpServerOptions {
5
5
  url: string;
6
6
  schema: string;
@@ -31,6 +31,14 @@ export interface McpServerHandle {
31
31
  }
32
32
  export declare function startMcpServer(options: McpServerOptions, transport?: McpTransport): McpServerHandle;
33
33
  interface ForeignKeyRow {
34
+ /**
35
+ * `pg_constraint.oid` as text. THE grouping key: a constraint NAME is unique
36
+ * only per table (conrelid, conname), so two tables in one schema may both
37
+ * have a `shared_fk` and grouping on the name alone merges them, which loses
38
+ * one table's relation entirely and points the other at a column its target
39
+ * does not have. The OID is unique catalog-wide.
40
+ */
41
+ constraint_oid: string;
34
42
  source_table: string;
35
43
  source_column: string;
36
44
  target_table: string;
@@ -38,13 +46,25 @@ interface ForeignKeyRow {
38
46
  constraint_name: string;
39
47
  }
40
48
  /**
41
- * Group raw FK rows into constraint-level entries and delegate relation
42
- * naming to the SHARED introspection builder (`buildRelationsFromForeignKeys`
43
- * + `addAutoManyToManyRelations` in ../introspect.ts). MCP previously carried
44
- * a stale copy of a retired naming scheme, so `turbine mcp` and `turbine
45
- * generate` derived DIFFERENT relation names from the same database.
46
- * Exported for the parity unit test.
49
+ * Group raw FK rows into constraint-level entries and delegate relation naming
50
+ * to `deriveCatalogRelations` in ../introspect.ts, the SAME entry point
51
+ * `turbine generate` goes through. MCP introspects for itself (it cannot assume
52
+ * generated metadata exists), so any divergence here is a divergence a live MCP
53
+ * client trips over: it reads a relation name out of the MCP schema tool, passes
54
+ * it back in a query, and the core builder rejects it.
55
+ *
56
+ * MCP used to assemble the pipeline by hand and drifted twice over, both times
57
+ * by OMITTING an optional argument, which is silently type-correct and changes
58
+ * the answer: without `uniqueSetsByTable` a UNIQUE foreign key came back as
59
+ * `users.profiles` (hasMany) where generate said `users.profile` (hasOne), and
60
+ * without `uniqueIndexColsByTable` every Prisma-style PK-less junction lost its
61
+ * auto-m2m relations. Passing the whole input set to one shared function is what
62
+ * makes those two failures impossible rather than merely fixed.
63
+ *
64
+ * `uniqueByTable` and `indexesByTable` are therefore REQUIRED parameters, not
65
+ * optional ones: an optional catalog input is exactly how the hasOne flip went
66
+ * missing here in the first place. Exported for the parity unit test.
47
67
  */
48
- export declare function buildRelations(tableNames: string[], columnsByTable: Map<string, ColumnMetadata[]>, pkByTable: Map<string, string[]>, rows: ForeignKeyRow[], enums?: Record<string, string[]>): Map<string, Record<string, RelationDef>>;
68
+ export declare function buildRelations(tableNames: string[], columnsByTable: Map<string, ColumnMetadata[]>, pkByTable: Map<string, string[]>, rows: ForeignKeyRow[], uniqueByTable: Map<string, string[][]>, indexesByTable: Map<string, IndexMetadata[]>, enums?: Record<string, string[]>): Map<string, Record<string, RelationDef>>;
49
69
  export declare function runMcpServer(options: McpServerOptions): Promise<void>;
50
70
  export {};