turbine-orm 0.49.0 → 0.50.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 (157) hide show
  1. package/README.md +122 -39
  2. package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
  3. package/dist/cjs/adapters/index.d.ts +110 -0
  4. package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
  5. package/dist/cjs/cli/config.d.ts +181 -0
  6. package/dist/cjs/cli/config.js +32 -6
  7. package/dist/cjs/cli/destructive.d.ts +38 -0
  8. package/dist/cjs/cli/index.d.ts +359 -0
  9. package/dist/cjs/cli/index.js +228 -56
  10. package/dist/cjs/cli/loader.d.ts +61 -0
  11. package/dist/cjs/cli/mcp.d.ts +42 -0
  12. package/dist/cjs/cli/migrate.d.ts +356 -0
  13. package/dist/cjs/cli/migrate.js +131 -40
  14. package/dist/cjs/cli/observe-ui.d.ts +1 -0
  15. package/dist/cjs/cli/observe-ui.js +14 -5
  16. package/dist/cjs/cli/observe.d.ts +25 -0
  17. package/dist/cjs/cli/observe.js +49 -12
  18. package/dist/cjs/cli/pii-tags.d.ts +53 -0
  19. package/dist/cjs/cli/prisma-report.d.ts +33 -0
  20. package/dist/cjs/cli/prisma-report.js +73 -0
  21. package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
  22. package/dist/cjs/cli/prisma-resolve.js +1 -0
  23. package/dist/cjs/cli/prisma-schema.d.ts +176 -0
  24. package/dist/cjs/cli/prisma-schema.js +82 -4
  25. package/dist/cjs/cli/rate-limit.d.ts +32 -0
  26. package/dist/cjs/cli/rate-limit.js +45 -0
  27. package/dist/cjs/cli/studio-demo.d.ts +43 -0
  28. package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
  29. package/dist/cjs/cli/studio.d.ts +207 -0
  30. package/dist/cjs/cli/studio.js +136 -71
  31. package/dist/cjs/cli/ui.d.ts +73 -0
  32. package/dist/cjs/cli/ui.js +51 -9
  33. package/dist/cjs/client.d.ts +837 -0
  34. package/dist/cjs/client.js +3 -0
  35. package/dist/cjs/dialect.d.ts +516 -0
  36. package/dist/cjs/dialect.js +37 -12
  37. package/dist/cjs/errors.d.ts +370 -0
  38. package/dist/cjs/generate.d.ts +137 -0
  39. package/dist/cjs/generate.js +39 -6
  40. package/dist/cjs/index-advisor.d.ts +153 -0
  41. package/dist/cjs/index-stats.d.ts +384 -0
  42. package/dist/cjs/index.d.ts +55 -0
  43. package/dist/cjs/index.js +7 -2
  44. package/dist/cjs/introspect.d.ts +269 -0
  45. package/dist/cjs/mssql.d.ts +232 -0
  46. package/dist/cjs/mssql.js +6 -0
  47. package/dist/cjs/mysql.d.ts +173 -0
  48. package/dist/cjs/mysql.js +16 -0
  49. package/dist/cjs/nested-write.d.ts +96 -0
  50. package/dist/cjs/nested-write.js +414 -24
  51. package/dist/cjs/observe.d.ts +115 -0
  52. package/dist/cjs/optional-peer-import.d.cts +72 -0
  53. package/dist/cjs/pipeline-submittable.d.ts +93 -0
  54. package/dist/cjs/pipeline.d.ts +71 -0
  55. package/dist/cjs/powdb-introspect.d.ts +84 -0
  56. package/dist/cjs/powdb.d.ts +931 -0
  57. package/dist/cjs/powdb.js +106 -21
  58. package/dist/cjs/powql.d.ts +592 -0
  59. package/dist/cjs/powql.js +42 -6
  60. package/dist/cjs/prisma-compat.d.ts +283 -0
  61. package/dist/cjs/prisma-compat.js +167 -9
  62. package/dist/cjs/query/aggregates.d.ts +92 -0
  63. package/dist/cjs/query/aggregates.js +7 -3
  64. package/dist/cjs/query/batched-loader.d.ts +193 -0
  65. package/dist/cjs/query/builder.d.ts +849 -0
  66. package/dist/cjs/query/builder.js +571 -65
  67. package/dist/cjs/query/compound-unique.d.ts +51 -0
  68. package/dist/cjs/query/deferred.d.ts +223 -0
  69. package/dist/cjs/query/filters.d.ts +201 -0
  70. package/dist/cjs/query/index.d.ts +14 -0
  71. package/dist/cjs/query/index.js +6 -1
  72. package/dist/cjs/query/relations.d.ts +609 -0
  73. package/dist/cjs/query/relations.js +693 -46
  74. package/dist/cjs/query/types.d.ts +1300 -0
  75. package/dist/cjs/query/utils.d.ts +209 -0
  76. package/dist/cjs/query/utils.js +208 -1
  77. package/dist/cjs/query/warn-registry.d.ts +68 -0
  78. package/dist/cjs/query/warn-registry.js +9 -0
  79. package/dist/cjs/query/where-compile.d.ts +139 -0
  80. package/dist/cjs/query/where.d.ts +548 -0
  81. package/dist/cjs/query/where.js +58 -22
  82. package/dist/cjs/query/writes.d.ts +172 -0
  83. package/dist/cjs/query/writes.js +105 -12
  84. package/dist/cjs/realtime.d.ts +70 -0
  85. package/dist/cjs/schema-builder.d.ts +354 -0
  86. package/dist/cjs/schema-metadata.d.ts +83 -0
  87. package/dist/cjs/schema-sql.d.ts +217 -0
  88. package/dist/cjs/schema-sql.js +23 -5
  89. package/dist/cjs/schema.d.ts +356 -0
  90. package/dist/cjs/schema.js +125 -0
  91. package/dist/cjs/seed.d.ts +15 -0
  92. package/dist/cjs/serverless.d.ts +142 -0
  93. package/dist/cjs/sqlite.d.ts +143 -0
  94. package/dist/cjs/sqlite.js +4 -0
  95. package/dist/cjs/typed-sql.d.ts +102 -0
  96. package/dist/cli/config.d.ts +18 -4
  97. package/dist/cli/config.js +31 -6
  98. package/dist/cli/index.d.ts +123 -0
  99. package/dist/cli/index.js +223 -58
  100. package/dist/cli/migrate.d.ts +59 -10
  101. package/dist/cli/migrate.js +128 -41
  102. package/dist/cli/observe-ui.d.ts +1 -1
  103. package/dist/cli/observe-ui.js +14 -5
  104. package/dist/cli/observe.d.ts +7 -1
  105. package/dist/cli/observe.js +48 -12
  106. package/dist/cli/prisma-report.d.ts +14 -0
  107. package/dist/cli/prisma-report.js +72 -0
  108. package/dist/cli/prisma-resolve.d.ts +6 -0
  109. package/dist/cli/prisma-resolve.js +1 -0
  110. package/dist/cli/prisma-schema.d.ts +62 -2
  111. package/dist/cli/prisma-schema.js +81 -4
  112. package/dist/cli/rate-limit.d.ts +32 -0
  113. package/dist/cli/rate-limit.js +40 -0
  114. package/dist/cli/studio.d.ts +5 -5
  115. package/dist/cli/studio.js +135 -70
  116. package/dist/cli/ui.d.ts +1 -1
  117. package/dist/cli/ui.js +51 -9
  118. package/dist/client.d.ts +40 -0
  119. package/dist/client.js +3 -0
  120. package/dist/dialect.d.ts +17 -1
  121. package/dist/dialect.js +37 -12
  122. package/dist/generate.js +40 -7
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/mssql.js +6 -0
  126. package/dist/mysql.js +16 -0
  127. package/dist/nested-write.d.ts +2 -0
  128. package/dist/nested-write.js +415 -25
  129. package/dist/powdb.d.ts +4 -2
  130. package/dist/powdb.js +106 -21
  131. package/dist/powql.d.ts +5 -0
  132. package/dist/powql.js +42 -6
  133. package/dist/prisma-compat.d.ts +2 -0
  134. package/dist/prisma-compat.js +166 -8
  135. package/dist/query/aggregates.js +7 -3
  136. package/dist/query/builder.d.ts +292 -21
  137. package/dist/query/builder.js +570 -64
  138. package/dist/query/deferred.d.ts +39 -0
  139. package/dist/query/index.d.ts +1 -1
  140. package/dist/query/index.js +1 -1
  141. package/dist/query/relations.d.ts +173 -5
  142. package/dist/query/relations.js +688 -47
  143. package/dist/query/types.d.ts +123 -39
  144. package/dist/query/utils.d.ts +116 -0
  145. package/dist/query/utils.js +198 -0
  146. package/dist/query/warn-registry.d.ts +9 -0
  147. package/dist/query/warn-registry.js +9 -0
  148. package/dist/query/where.d.ts +38 -1
  149. package/dist/query/where.js +58 -23
  150. package/dist/query/writes.d.ts +42 -1
  151. package/dist/query/writes.js +104 -13
  152. package/dist/schema-sql.d.ts +14 -0
  153. package/dist/schema-sql.js +23 -5
  154. package/dist/schema.d.ts +38 -0
  155. package/dist/schema.js +123 -0
  156. package/dist/sqlite.js +4 -0
  157. package/package.json +77 -28
package/dist/cli/index.js CHANGED
@@ -33,7 +33,7 @@ import { buildCreateIndexSql, buildDropIndexSql, collectDoctorProbeIndexNames, c
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 { DestructivePushRefusal, schemaDiff, schemaPush } from '../schema-sql.js';
36
- import { configTemplate, findConfigFile, loadConfigResult, looksLikeSchemaFilePath, resolveConfig, resolveSeedFile, unwrapModuleDefault, } from './config.js';
36
+ import { configTemplate, DEFAULT_INIT_SEED_FILE, findConfigFile, loadConfigResult, looksLikeSchemaFilePath, resolveConfig, resolveSeedFile, unwrapModuleDefault, } from './config.js';
37
37
  import { DESTRUCTIVE_KIND_LABEL } from './destructive.js';
38
38
  import { canResolveTsx, getTsLoaderError, needsTsLoader, registerTsLoader } from './loader.js';
39
39
  import { runMcpServer } from './mcp.js';
@@ -41,7 +41,7 @@ import { buildDiffMigrationBody, collectUpDestructive, createMigration, formatCh
41
41
  import { startObserve } from './observe.js';
42
42
  import { formatPrismaReport, summaryLines } from './prisma-report.js';
43
43
  import { DEFAULT_EXCLUDED_TABLES, resolvePrismaSchema } from './prisma-resolve.js';
44
- import { PrismaParseError, parsePrismaSchema } from './prisma-schema.js';
44
+ import { PrismaParseError, parsePrismaSchema, resolvePrismaDatasourceUrl, } from './prisma-schema.js';
45
45
  import { startStudio } from './studio.js';
46
46
  import { banner, blue, bold, box, cyan, dim, divider, elapsed, error, table as formatTable, gray, green, header, info, label, magenta, newline, red, redactUrl, Spinner, success, symbols, warn, yellow, } from './ui.js';
47
47
  export function parseArgs(argv = process.argv.slice(2)) {
@@ -267,9 +267,49 @@ function failMissingTsLoader(filePath, reason) {
267
267
  process.exit(1);
268
268
  }
269
269
  // ---------------------------------------------------------------------------
270
- // Helpers
270
+ // Config bootstrap
271
271
  // ---------------------------------------------------------------------------
272
- function requireUrl(config) {
272
+ /**
273
+ * Does this invocation need a `turbine.config.*` file?
274
+ *
275
+ * Everything does, with one deliberate exception: `turbine studio --demo` boots
276
+ * a seeded in-memory database, needs no `DATABASE_URL` and no config file, and
277
+ * is the very next command the quickstart suggests after `turbine init`.
278
+ * Resolving the config anyway means a freshly scaffolded directory (a
279
+ * `turbine.config.ts` with `tsx` not installed yet) dies with "Cannot load
280
+ * TypeScript file" before demo mode ever starts.
281
+ *
282
+ * @internal exported for tests.
283
+ */
284
+ export function usesProjectConfig(args) {
285
+ return !(args.command === 'studio' && args.demo === true);
286
+ }
287
+ /**
288
+ * Resolve the effective CLI config: register the tsx loader when the config file
289
+ * is TypeScript, import it, then merge it with env vars and CLI flags. Exits with
290
+ * the actionable "Cannot load TypeScript file" error when a `.ts` config cannot
291
+ * be loaded. Config-free invocations short-circuit without touching the disk.
292
+ *
293
+ * @internal exported for tests.
294
+ */
295
+ export async function bootstrapCliConfig(args, overrides) {
296
+ if (!usesProjectConfig(args)) {
297
+ return { config: resolveConfig({}, overrides), fileConfig: {}, skipped: true };
298
+ }
299
+ // If the user has a TypeScript config file, register the tsx ESM loader
300
+ // before we attempt to import it. Otherwise Node throws
301
+ // ERR_UNKNOWN_FILE_EXTENSION for `.ts`.
302
+ const configPath = findConfigFile();
303
+ if (needsTsLoader(configPath)) {
304
+ const status = await registerTsLoader();
305
+ if (status === 'missing' || status === 'unsupported' || status === 'failed') {
306
+ failMissingTsLoader(configPath ?? 'turbine.config.ts', status);
307
+ }
308
+ }
309
+ const { config: fileConfig, loadError } = await loadConfigResult();
310
+ return { config: resolveConfig(fileConfig, overrides), fileConfig, loadError, skipped: false };
311
+ }
312
+ function requireUrl(config, options = {}) {
273
313
  if (!config.url) {
274
314
  error('No database URL provided.');
275
315
  newline();
@@ -279,6 +319,12 @@ function requireUrl(config) {
279
319
  const envFileNote = typeof process.loadEnvFile === 'function' ? '(auto-loaded)' : '(needs Node 20.12+ to auto-load)';
280
320
  console.log(` ${dim('2.')} Set ${cyan('DATABASE_URL')} in your environment or a ${cyan('.env')} file ${dim(envFileNote)}`);
281
321
  console.log(` ${dim('3.')} Pass ${cyan('--url')} flag`);
322
+ const vars = options.datasourceVars ?? [];
323
+ if (vars.length > 0) {
324
+ const list = vars.map((v) => cyan(v)).join(', ');
325
+ const plural = vars.length > 1 ? 'these variables' : 'this variable';
326
+ console.log(` ${dim('4.')} Set ${list} ${dim(`(${plural}, declared by your schema.prisma datasource, ${vars.length > 1 ? 'are' : 'is'} unset)`)}`);
327
+ }
282
328
  newline();
283
329
  process.exit(1);
284
330
  }
@@ -430,6 +476,57 @@ export function dotEnvUrlConflictWarning(input) {
430
476
  `(${redactUrl(fileUrl)}). Using the .env value. Remove DATABASE_URL from .env, or unset the config url, ` +
431
477
  `to silence this.`);
432
478
  }
479
+ /**
480
+ * Detect the consumer's package manager from its lockfile, defaulting to npm.
481
+ * Used only to print an exact, copy-pasteable install command.
482
+ *
483
+ * @internal exported for tests.
484
+ */
485
+ export function detectPackageManager(cwd = process.cwd()) {
486
+ if (existsSync(join(cwd, 'pnpm-lock.yaml')))
487
+ return 'pnpm';
488
+ if (existsSync(join(cwd, 'yarn.lock')))
489
+ return 'yarn';
490
+ if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock')))
491
+ return 'bun';
492
+ return 'npm';
493
+ }
494
+ /**
495
+ * The exact "add tsx as a dev dependency" command for a package manager.
496
+ *
497
+ * @internal exported for tests.
498
+ */
499
+ export function tsxInstallCommand(pm) {
500
+ switch (pm) {
501
+ case 'pnpm':
502
+ return 'pnpm add -D tsx';
503
+ case 'yarn':
504
+ return 'yarn add -D tsx';
505
+ case 'bun':
506
+ return 'bun add -d tsx';
507
+ default:
508
+ return 'npm install --save-dev tsx';
509
+ }
510
+ }
511
+ /**
512
+ * The heads-up `turbine init` prints when it has just scaffolded TypeScript
513
+ * files and `tsx` is not resolvable: without it the CLI cannot load them, and
514
+ * the very next command the user runs dies on "Cannot load TypeScript file".
515
+ * Pure (returns the lines, prints nothing) so it can be asserted in tests.
516
+ *
517
+ * @internal exported for tests.
518
+ */
519
+ export function tsxRequiredNotice(tsFiles, installCommand) {
520
+ return [
521
+ `Turbine needs ${cyan('tsx')} to load the TypeScript files just created:`,
522
+ ...tsFiles.map((f) => ` ${dim(symbols.dot)} ${cyan(f)}`),
523
+ '',
524
+ ` ${dim('Install it as a dev dependency:')}`,
525
+ ` ${cyan(installCommand)}`,
526
+ '',
527
+ ` ${dim('Without it, the next Turbine command fails with')} ${dim('"Cannot load TypeScript file".')}`,
528
+ ];
529
+ }
433
530
  /**
434
531
  * Read the consumer's `package.json` `"type"` field. Returns `'module'` for an
435
532
  * ESM project, `'commonjs'` for an explicit or absent (defaulted) CommonJS
@@ -755,6 +852,48 @@ function initPromptQuestion(step, config, seedFilePath) {
755
852
  return `Run ${initStepLabel(step.id)}?`;
756
853
  }
757
854
  }
855
+ /**
856
+ * Decide which connection notice `turbine init` prints. Pure so the whole
857
+ * decision matrix is testable.
858
+ *
859
+ * "No DATABASE_URL found in environment" is reserved for the case where NO
860
+ * source supplied one: printing it while happily using `--url` (or a config
861
+ * `url`) reads like a failure the user then goes looking for.
862
+ *
863
+ * @internal exported for tests.
864
+ */
865
+ export function initEnvNotice(input) {
866
+ if (input.envUrl) {
867
+ return { kind: 'success', message: `Detected ${cyan('DATABASE_URL')} in the environment` };
868
+ }
869
+ if (input.hasEnvFile && !input.canAutoLoadEnv) {
870
+ return {
871
+ kind: 'info',
872
+ message: `Found ${cyan('.env')} ${dim('(this Node version cannot auto-load it. Upgrade to Node 20.12+ or export')} ${cyan('DATABASE_URL')}${dim(')')}`,
873
+ };
874
+ }
875
+ if (input.hasEnvFile) {
876
+ // .env exists but did not provide DATABASE_URL; if it had, the auto-load
877
+ // in main() would have populated envUrl above.
878
+ return {
879
+ kind: 'info',
880
+ message: `Found ${cyan('.env')} ${dim('(no')} ${cyan('DATABASE_URL')} ${dim('set in it yet)')}`,
881
+ };
882
+ }
883
+ if (input.hasEnvLocal) {
884
+ return {
885
+ kind: 'info',
886
+ message: `Found ${cyan('.env.local')} ${dim('(note: Turbine only auto-loads')} ${cyan('.env')}${dim(')')}`,
887
+ };
888
+ }
889
+ if (input.flagUrl) {
890
+ return { kind: 'success', message: `Using the connection string passed with ${cyan('--url')}` };
891
+ }
892
+ if (input.configUrl) {
893
+ return { kind: 'success', message: `Using the ${cyan('url')} from your config file` };
894
+ }
895
+ return { kind: 'info', message: `No ${cyan('DATABASE_URL')} found in environment` };
896
+ }
758
897
  async function cmdInit(args, config) {
759
898
  banner();
760
899
  header('Initializing Turbine project');
@@ -767,23 +906,18 @@ async function cmdInit(args, config) {
767
906
  // On Node < 20.12 (no process.loadEnvFile) main() could not auto-load .env, so
768
907
  // we cannot claim it "has no DATABASE_URL"; we simply could not read it.
769
908
  const canAutoLoadEnv = typeof process.loadEnvFile === 'function';
770
- if (envUrl) {
771
- success(`Detected ${cyan('DATABASE_URL')} in the environment`);
772
- }
773
- else if (hasEnvFile && !canAutoLoadEnv) {
774
- info(`Found ${cyan('.env')} ${dim('(this Node version cannot auto-load it. Upgrade to Node 20.12+ or export')} ${cyan('DATABASE_URL')}${dim(')')}`);
775
- }
776
- else if (hasEnvFile) {
777
- // .env exists but did not provide DATABASE_URL; if it had, the auto-load
778
- // in main() would have populated envUrl above.
779
- info(`Found ${cyan('.env')} ${dim('(no')} ${cyan('DATABASE_URL')} ${dim('set in it yet)')}`);
780
- }
781
- else if (hasEnvLocal) {
782
- info(`Found ${cyan('.env.local')} ${dim('(note: Turbine only auto-loads')} ${cyan('.env')}${dim(')')}`);
783
- }
784
- else {
785
- info(`No ${cyan('DATABASE_URL')} found in environment`);
786
- }
909
+ const envNotice = initEnvNotice({
910
+ envUrl,
911
+ hasEnvFile,
912
+ hasEnvLocal,
913
+ canAutoLoadEnv,
914
+ flagUrl: args.url,
915
+ configUrl: config.url,
916
+ });
917
+ if (envNotice.kind === 'success')
918
+ success(envNotice.message);
919
+ else
920
+ info(envNotice.message);
787
921
  newline();
788
922
  // Heads-up (not an edit) about the consumer's module system. A CommonJS
789
923
  // project (`npm init -y` default, or no "type" field) works fine now that the
@@ -800,7 +934,9 @@ async function cmdInit(args, config) {
800
934
  const hasUrl = Boolean(url);
801
935
  const interactive = Boolean(process.stdin.isTTY);
802
936
  const yes = args.yes === true;
803
- const seedFilePath = config.seedFile ?? './seed.ts';
937
+ // Where init scaffolds the seed file. A root-level ./seed.ts from an older
938
+ // init still counts, so a re-run never scaffolds a second seed file.
939
+ const seedFilePath = config.seedFile ?? (existsSync('./seed.ts') ? './seed.ts' : DEFAULT_INIT_SEED_FILE);
804
940
  const configPath = findConfigFile();
805
941
  const state = {
806
942
  configExists: Boolean(configPath),
@@ -834,6 +970,8 @@ async function cmdInit(args, config) {
834
970
  // Supporting dirs + .gitignore, regardless of prompts (unchanged behavior).
835
971
  ensureInitScaffoldDirs(config);
836
972
  newline();
973
+ // TypeScript files this run actually created: they drive the tsx heads-up below.
974
+ const tsFilesWritten = [];
837
975
  // Execute the plan in order. `run` proceeds; `prompt` asks; `skip` reports.
838
976
  for (const step of plan) {
839
977
  if (step.action === 'skip') {
@@ -849,12 +987,17 @@ async function cmdInit(args, config) {
849
987
  case 'config':
850
988
  writeFileSync('turbine.config.ts', configTemplate(args.url ?? undefined), 'utf-8');
851
989
  success(state.configExists ? `Overwrote ${cyan('turbine.config.ts')}` : `Created ${cyan('turbine.config.ts')}`);
990
+ tsFilesWritten.push('turbine.config.ts');
852
991
  break;
853
992
  case 'schema':
854
993
  writeInitSchemaTemplate(config);
994
+ if (needsTsLoader(config.schemaFile))
995
+ tsFilesWritten.push(config.schemaFile);
855
996
  break;
856
997
  case 'seed-file':
857
998
  writeInitSeedTemplate(seedFilePath);
999
+ if (needsTsLoader(seedFilePath))
1000
+ tsFilesWritten.push(seedFilePath);
858
1001
  break;
859
1002
  case 'push':
860
1003
  await runInitPush(config, url);
@@ -867,6 +1010,17 @@ async function cmdInit(args, config) {
867
1010
  break;
868
1011
  }
869
1012
  }
1013
+ // Scaffolding .ts files and staying silent about tsx is how a fresh project
1014
+ // hits "Cannot load TypeScript file" on its very next command. Warn now, name
1015
+ // the exact install command, and never install anything ourselves.
1016
+ const tsxMissing = tsFilesWritten.length > 0 && !canResolveTsx();
1017
+ if (tsxMissing) {
1018
+ newline();
1019
+ const [headline, ...rest] = tsxRequiredNotice(tsFilesWritten, tsxInstallCommand(detectPackageManager()));
1020
+ warn(headline ?? '');
1021
+ for (const line of rest)
1022
+ console.log(line);
1023
+ }
870
1024
  if (degraded) {
871
1025
  newline();
872
1026
  info('Non-interactive shell: interactive prompts were skipped.');
@@ -884,8 +1038,9 @@ async function cmdInit(args, config) {
884
1038
  console.log(` ${dim('or create a')} ${cyan('.env')} ${dim('file with')} ${cyan('DATABASE_URL=postgres://...')}`);
885
1039
  }
886
1040
  console.log(` ${dim('2.')} Run ${cyan('npx turbine generate')} to introspect your DB`);
887
- if (!canResolveTsx()) {
888
- console.log(` ${dim('Note: the TypeScript config requires')} ${cyan('tsx')} ${dim('—')} ${cyan('npm install --save-dev tsx')}`);
1041
+ // Only when the fuller heads-up above did not already run (nothing scaffolded).
1042
+ if (!tsxMissing && !canResolveTsx()) {
1043
+ console.log(` ${dim('Note: the TypeScript config requires')} ${cyan('tsx')}: ${cyan(tsxInstallCommand(detectPackageManager()))}`);
889
1044
  }
890
1045
  }
891
1046
  else {
@@ -1010,20 +1165,30 @@ async function cmdGenerate(args, config) {
1010
1165
  console.log(` ${dim('const user = await db.users.findUnique({ where: { id: 1 } });')}`);
1011
1166
  newline();
1012
1167
  }
1013
- // ---------------------------------------------------------------------------
1014
- // migrate-from-prisma
1015
- // ---------------------------------------------------------------------------
1016
1168
  /**
1017
- * `turbine migrate-from-prisma --schema prisma/schema.prisma` parses a Prisma
1018
- * schema, resolve its models/fields/relations/compound-uniques against the live
1019
- * database (unless `--no-db`), and emit (a) a Markdown resolution report and
1020
- * (b) a typed `prisma-map.ts` name map next to the generated client.
1169
+ * Pick the connection string for `migrate-from-prisma`.
1021
1170
  *
1022
- * NOTE: within THIS command `--schema` names the Prisma schema FILE (not the
1023
- * Postgres namespace, which the rest of the CLI's `--schema` means). The
1024
- * Postgres namespace is `public` here; multi-schema (`@@schema`) is unsupported
1025
- * in v1 and listed as a parser note in the report.
1171
+ * `configUrl` is what {@link resolveConfig} already produced (`--url`, then
1172
+ * `DATABASE_URL`, then `turbine.config.ts`) and always wins: an explicit flag
1173
+ * must never be overridden by a value declared in someone else's schema file.
1174
+ * Only when that is empty do we fall back to the `datasource` block, which
1175
+ * removes the flag a project with a non-standard variable name would otherwise
1176
+ * pass on every run.
1026
1177
  */
1178
+ export function resolveMigrateFromPrismaUrl(configUrl, ast, env) {
1179
+ const lookup = resolvePrismaDatasourceUrl(ast, env);
1180
+ if (configUrl)
1181
+ return { url: configUrl, source: 'config', missingVariables: lookup.missingVariables };
1182
+ if (lookup.resolved) {
1183
+ return {
1184
+ url: lookup.resolved.url,
1185
+ source: 'datasource',
1186
+ datasource: lookup.resolved,
1187
+ missingVariables: lookup.missingVariables,
1188
+ };
1189
+ }
1190
+ return { source: 'none', missingVariables: lookup.missingVariables };
1191
+ }
1027
1192
  async function cmdMigrateFromPrisma(args, config) {
1028
1193
  banner();
1029
1194
  // `--schema` is the Prisma schema file path in this command.
@@ -1061,7 +1226,15 @@ async function cmdMigrateFromPrisma(args, config) {
1061
1226
  info('Parse-only mode (--no-db): names will not be resolved.');
1062
1227
  }
1063
1228
  else {
1064
- url = requireUrl(config);
1229
+ // The schema.prisma datasource is the LAST resort for the connection string,
1230
+ // below `--url`, `DATABASE_URL`, and `turbine.config.ts`.
1231
+ const resolvedUrl = resolveMigrateFromPrismaUrl(config.url, ast, process.env);
1232
+ if (resolvedUrl.source === 'datasource' && resolvedUrl.datasource) {
1233
+ const { variable, datasource: dsName, key } = resolvedUrl.datasource;
1234
+ const origin = variable ? `${cyan(variable)} via datasource "${dsName}"` : `datasource "${dsName}" ${key}`;
1235
+ info(`Using the connection string declared by your Prisma schema (${origin}).`);
1236
+ }
1237
+ url = resolvedUrl.url ?? requireUrl(config, { datasourceVars: resolvedUrl.missingVariables });
1065
1238
  label('Database', redactUrl(url));
1066
1239
  const spinner = new Spinner('Introspecting database schema').start();
1067
1240
  schemaMeta = await introspect({
@@ -2752,6 +2925,8 @@ function showMigrateFromPrismaHelp() {
2752
2925
  console.log(` ${dim('•')} ${cyan('prisma-map.ts')} - typed PRISMA_MAP name map`);
2753
2926
  newline();
2754
2927
  console.log(` ${dim('Note:')} here ${cyan('--schema')} names the Prisma FILE (not the Postgres namespace).`);
2928
+ console.log(` ${dim('Note:')} with no ${cyan('--url')} / ${cyan('DATABASE_URL')} / config ${cyan('url')}, the connection string`);
2929
+ console.log(` declared by your ${cyan('datasource')} block is used (including its ${cyan('env("...")')}).`);
2755
2930
  newline();
2756
2931
  console.log(` ${bold('Options:')}`);
2757
2932
  console.log(` ${cyan('--schema')} ${dim('<file>')} Path to schema.prisma ${dim('(default: prisma/schema.prisma)')}`);
@@ -3012,27 +3187,6 @@ async function main() {
3012
3187
  warn(`Found ${cyan('.env')} but this Node version cannot auto-load it. ` +
3013
3188
  `Upgrade to Node 20.12+ or export ${cyan('DATABASE_URL')} yourself.`);
3014
3189
  }
3015
- // If the user has a TypeScript config file, register the tsx ESM loader
3016
- // before we attempt to import it. Otherwise Node throws
3017
- // ERR_UNKNOWN_FILE_EXTENSION for `.ts`.
3018
- const configPath = findConfigFile();
3019
- if (needsTsLoader(configPath)) {
3020
- const status = await registerTsLoader();
3021
- if (status === 'missing' || status === 'unsupported' || status === 'failed') {
3022
- failMissingTsLoader(configPath ?? 'turbine.config.ts', status);
3023
- }
3024
- }
3025
- // Load config file. A config that exists but fails to import is surfaced
3026
- // loudly (with a name + the underlying error) instead of being swallowed and
3027
- // later misreported as a missing database URL.
3028
- const { config: fileConfig, loadError } = await loadConfigResult();
3029
- if (loadError && args.command !== 'init') {
3030
- const underlying = loadError.error instanceof Error ? loadError.error.message : String(loadError.error);
3031
- warn(`Could not load ${cyan(loadError.filename)}: ${underlying}`);
3032
- if (loadError.error instanceof Error)
3033
- printCjsHintIfApplicable(loadError.error);
3034
- newline();
3035
- }
3036
3190
  const overrides = {
3037
3191
  url: args.url,
3038
3192
  out: args.out,
@@ -3043,7 +3197,18 @@ async function main() {
3043
3197
  keepColumnNames: args.keepColumnNames,
3044
3198
  legacyToManyUniques: args.legacyToManyUniques,
3045
3199
  };
3046
- const config = resolveConfig(fileConfig, overrides);
3200
+ // Resolve the config file (skipped entirely for config-free invocations such
3201
+ // as `studio --demo`). A config that exists but fails to import is surfaced
3202
+ // loudly (with a name + the underlying error) instead of being swallowed and
3203
+ // later misreported as a missing database URL.
3204
+ const { config, fileConfig, loadError } = await bootstrapCliConfig(args, overrides);
3205
+ if (loadError && args.command !== 'init') {
3206
+ const underlying = loadError.error instanceof Error ? loadError.error.message : String(loadError.error);
3207
+ warn(`Could not load ${cyan(loadError.filename)}: ${underlying}`);
3208
+ if (loadError.error instanceof Error)
3209
+ printCjsHintIfApplicable(loadError.error);
3210
+ newline();
3211
+ }
3047
3212
  // Warn (don't change precedence) when an .env-sourced DATABASE_URL is silently
3048
3213
  // overriding a differing, non-empty url in the config file (a wrong-database
3049
3214
  // hazard for push/migrate/seed). Shell-exported DATABASE_URL stays silent.
@@ -11,6 +11,7 @@
11
11
  * -- DOWN
12
12
  * DROP TABLE users;
13
13
  */
14
+ import pg from 'pg';
14
15
  import type { DatabaseAdapter } from '../adapters/index.js';
15
16
  import { type Dialect } from '../dialect.js';
16
17
  import { type DestructiveStatement } from './destructive.js';
@@ -133,6 +134,29 @@ export declare function splitSqlStatements(sql: string): string[];
133
134
  * Parse a migration file into UP and DOWN sections.
134
135
  */
135
136
  export declare function parseMigrationSQL(filePath: string): ParsedMigration;
137
+ /**
138
+ * Can a stored pre-v0.6 checksum be safely upgraded to SHA-256?
139
+ *
140
+ * Only when the LEGACY algorithm, run over the file's CURRENT content,
141
+ * reproduces the stored value: that is what proves the file has not changed
142
+ * since it was applied. Upgrading without this proof blesses whatever the file
143
+ * says today and permanently disables drift detection for that migration.
144
+ *
145
+ * @internal exported for tests.
146
+ */
147
+ export declare function canUpgradeLegacyChecksum(stored: string, content: string): boolean;
148
+ /**
149
+ * Is an applied migration's stored checksum still valid for the file on disk?
150
+ *
151
+ * A SHA-256 match is the normal case. A pre-v0.6 row that {@link
152
+ * canUpgradeLegacyChecksum} would upgrade counts as valid too: `migrate up`
153
+ * accepts and upgrades it, so reporting it as invalid in `migrate status` would
154
+ * have the two commands disagree about the same unchanged file. Genuine drift
155
+ * (a legacy hash the current content no longer reproduces) still reports false.
156
+ *
157
+ * @internal exported for tests.
158
+ */
159
+ export declare function isChecksumValid(stored: string, content: string): boolean;
136
160
  /**
137
161
  * A named migration scaffold. `build()` returns the commented-SQL UP/DOWN body
138
162
  * for the recipe; `createMigration({ recipe })` wraps it in the file header.
@@ -216,6 +240,28 @@ export declare function createMigration(migrationsDir: string, name: string, aut
216
240
  * without contending on a single hardcoded lock ID.
217
241
  */
218
242
  export declare function deriveLockId(databaseName: string): number;
243
+ /**
244
+ * The minimal query surface a transactional migration body needs.
245
+ * `pg.Client` satisfies it; tests supply a fake.
246
+ */
247
+ export interface MigrationTxClient {
248
+ query(sql: string, params?: unknown[]): Promise<unknown>;
249
+ }
250
+ /**
251
+ * Run one migration body (UP or DOWN) plus its tracking-table write inside a
252
+ * single transaction. Returns `null` on success, or the error message on
253
+ * failure, leaving the caller to record it and stop.
254
+ *
255
+ * The ROLLBACK is best-effort: if the connection died, ROLLBACK throws too, and
256
+ * letting that escape would replace the real migration failure with a
257
+ * connection error. Same guard as `client.ts`, `query/builder.ts`, `dialect.ts`.
258
+ *
259
+ * @internal exported for tests; not part of the CLI's public surface.
260
+ */
261
+ export declare function runMigrationInTransaction(client: MigrationTxClient, body: string, tracking: {
262
+ sql: string;
263
+ params: unknown[];
264
+ }): Promise<string | null>;
219
265
  export interface ChecksumMismatch {
220
266
  name: string;
221
267
  expected: string;
@@ -227,6 +273,13 @@ export interface MigrationDeployPlan {
227
273
  pending: MigrationFile[];
228
274
  mismatches: ChecksumMismatch[];
229
275
  }
276
+ /**
277
+ * Validate that applied migration files have not been modified or deleted since they were run.
278
+ * Returns an array of mismatched migrations (empty if all are clean).
279
+ *
280
+ * @internal exported for tests; not part of the CLI's public surface.
281
+ */
282
+ export declare function validateChecksums(client: pg.Client, migrationsDir: string, dialect?: Dialect): Promise<ChecksumMismatch[]>;
230
283
  export declare function formatChecksumMismatchError(mismatches: ChecksumMismatch[]): string;
231
284
  /**
232
285
  * Build a deploy plan from local migration files and applied migration rows.
@@ -237,9 +290,7 @@ export declare function planMigrationDeploy(migrationsDir: string, applied: Appl
237
290
  /**
238
291
  * Inspect deploy status without applying migrations.
239
292
  */
240
- export declare function inspectMigrationDeploy(connectionString: string, migrationsDir: string, options?: {
241
- dialect?: Dialect;
242
- }): Promise<MigrationDeployPlan>;
293
+ export declare function inspectMigrationDeploy(connectionString: string, migrationsDir: string): Promise<MigrationDeployPlan>;
243
294
  /**
244
295
  * Apply all pending migrations (UP).
245
296
  *
@@ -261,7 +312,6 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
261
312
  /** Run migrations even when they contain data-destroying statements. Default false. */
262
313
  allowDestructive?: boolean;
263
314
  adapter?: DatabaseAdapter;
264
- dialect?: Dialect;
265
315
  /**
266
316
  * Called right before a `-- turbine:no-transaction` migration runs, so the
267
317
  * CLI can print its loud pre-run notice (a concurrent index build can wait a
@@ -275,7 +325,6 @@ export declare function migrateUp(connectionString: string, migrationsDir: strin
275
325
  */
276
326
  export declare function migrateDeploy(connectionString: string, migrationsDir: string, options?: {
277
327
  adapter?: DatabaseAdapter;
278
- dialect?: Dialect;
279
328
  allowDrift?: boolean;
280
329
  }): Promise<MigrationRunResult>;
281
330
  /**
@@ -290,7 +339,6 @@ export declare function migrateDown(connectionString: string, migrationsDir: str
290
339
  step?: number;
291
340
  allowDestructive?: boolean;
292
341
  adapter?: DatabaseAdapter;
293
- dialect?: Dialect;
294
342
  }): Promise<{
295
343
  rolledBack: MigrationFile[];
296
344
  errors: Array<{
@@ -300,8 +348,9 @@ export declare function migrateDown(connectionString: string, migrationsDir: str
300
348
  }>;
301
349
  /**
302
350
  * Get the status of all migrations (applied vs pending).
303
- * Includes checksum validation for applied migrations.
351
+ *
352
+ * Applied rows carry `checksumValid`, decided by {@link isChecksumValid} so an
353
+ * unchanged pre-v0.6 row reports the same way `migrate up` treats it (valid,
354
+ * pending an in-place hash upgrade) rather than looking like drift.
304
355
  */
305
- export declare function migrateStatus(connectionString: string, migrationsDir: string, options?: {
306
- dialect?: Dialect;
307
- }): Promise<MigrationStatus[]>;
356
+ export declare function migrateStatus(connectionString: string, migrationsDir: string): Promise<MigrationStatus[]>;