turbine-orm 0.40.1 → 0.41.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 (63) hide show
  1. package/README.md +22 -4
  2. package/dist/cjs/cli/config.js +3 -0
  3. package/dist/cjs/cli/index.js +179 -0
  4. package/dist/cjs/cli/prisma-report.js +216 -0
  5. package/dist/cjs/cli/prisma-resolve.js +335 -0
  6. package/dist/cjs/cli/prisma-schema.js +484 -0
  7. package/dist/cjs/client.js +1 -0
  8. package/dist/cjs/generate.js +279 -22
  9. package/dist/cjs/index.js +3 -2
  10. package/dist/cjs/introspect.js +203 -26
  11. package/dist/cjs/mssql.js +9 -10
  12. package/dist/cjs/mysql.js +3 -9
  13. package/dist/cjs/powdb-introspect.js +5 -10
  14. package/dist/cjs/powql.js +13 -0
  15. package/dist/cjs/prisma-compat.js +1147 -0
  16. package/dist/cjs/query/aggregates.js +67 -7
  17. package/dist/cjs/query/builder.js +388 -17
  18. package/dist/cjs/query/compound-unique.js +0 -0
  19. package/dist/cjs/query/relations.js +7 -5
  20. package/dist/cjs/query/warn-registry.js +98 -0
  21. package/dist/cjs/query/writes.js +13 -5
  22. package/dist/cjs/schema.js +47 -0
  23. package/dist/cjs/sqlite.js +4 -9
  24. package/dist/cli/config.d.ts +26 -0
  25. package/dist/cli/config.js +3 -0
  26. package/dist/cli/index.d.ts +11 -0
  27. package/dist/cli/index.js +180 -1
  28. package/dist/cli/prisma-report.d.ts +19 -0
  29. package/dist/cli/prisma-report.js +211 -0
  30. package/dist/cli/prisma-resolve.d.ts +87 -0
  31. package/dist/cli/prisma-resolve.js +330 -0
  32. package/dist/cli/prisma-schema.d.ts +116 -0
  33. package/dist/cli/prisma-schema.js +479 -0
  34. package/dist/cli/ui.d.ts +1 -1
  35. package/dist/client.d.ts +18 -2
  36. package/dist/client.js +1 -0
  37. package/dist/generate.d.ts +80 -1
  38. package/dist/generate.js +277 -25
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.js +1 -1
  41. package/dist/introspect.d.ts +92 -2
  42. package/dist/introspect.js +198 -26
  43. package/dist/mssql.js +10 -11
  44. package/dist/mysql.js +4 -10
  45. package/dist/powdb-introspect.js +5 -10
  46. package/dist/powql.js +13 -0
  47. package/dist/prisma-compat.d.ts +281 -0
  48. package/dist/prisma-compat.js +1143 -0
  49. package/dist/query/aggregates.js +67 -7
  50. package/dist/query/builder.d.ts +77 -4
  51. package/dist/query/builder.js +390 -19
  52. package/dist/query/compound-unique.d.ts +49 -0
  53. package/dist/query/compound-unique.js +0 -0
  54. package/dist/query/deferred.d.ts +18 -0
  55. package/dist/query/relations.js +7 -5
  56. package/dist/query/types.d.ts +70 -9
  57. package/dist/query/warn-registry.d.ts +57 -0
  58. package/dist/query/warn-registry.js +92 -0
  59. package/dist/query/writes.js +13 -5
  60. package/dist/schema.d.ts +75 -0
  61. package/dist/schema.js +46 -0
  62. package/dist/sqlite.js +5 -10
  63. package/package.json +6 -1
package/README.md CHANGED
@@ -360,10 +360,12 @@ A few client options tune how `with` relations are loaded and encoded. All are o
360
360
  ```typescript
361
361
  const db = turbine({
362
362
  connectionString: process.env.DATABASE_URL,
363
- // How with-clause relations resolve: 'join' (default, one correlated-subquery
364
- // statement) or 'batched' (base query + one flat follow-up per relation).
365
- // Override per query on findMany/findFirst/findUnique. See Relations.
366
- relationLoadStrategy: 'join',
363
+ // How with-clause relations resolve: 'auto' (default since 0.41.0: the
364
+ // single-statement join plan, with a per-relation batched fallback when the
365
+ // correlation column has no covering index), 'join' (always one correlated-
366
+ // subquery statement), or 'batched' (base query + one flat follow-up per
367
+ // relation). Override per query on findMany/findFirst/findUnique.
368
+ relationLoadStrategy: 'auto',
367
369
  // 'positional' (Postgres-only) drops repeated JSON keys from relation
368
370
  // subqueries — ~39% fewer wire bytes on wide relations, byte-identical output.
369
371
  // Default 'object'.
@@ -887,6 +889,22 @@ const db = await turbinePowDB({ embedded: './data', syncMode: 'normal' }, schema
887
889
  // const db = await turbinePowDB('powdb://127.0.0.1:7070', schema);
888
890
  ```
889
891
 
892
+ Migrating from Prisma? `turbine migrate-from-prisma` emits a typed `PRISMA_MAP`, and the
893
+ `turbine-orm/prisma-compat` subpath wraps a `TurbineClient` in Prisma's `db.Model.*`
894
+ surface (no new dependencies):
895
+
896
+ ```ts
897
+ import { TurbineClient } from 'turbine-orm';
898
+ import { createPrismaCompatClient } from 'turbine-orm/prisma-compat';
899
+ import { SCHEMA } from './generated/turbine/metadata.js';
900
+ import { PRISMA_MAP } from './generated/turbine/prisma-map.js';
901
+
902
+ const db = new TurbineClient({ connectionString: process.env.DATABASE_URL }, SCHEMA);
903
+ const prisma = createPrismaCompatClient(db, PRISMA_MAP);
904
+
905
+ const users = await prisma.User.findMany({ include: { posts: { take: 5 } } });
906
+ ```
907
+
890
908
  ### Capability matrix
891
909
 
892
910
  Everything is honest about what ports and what doesn't. Features marked **PG-only** throw a typed `UnsupportedFeatureError` (`TURBINE_E017`) on other engines rather than silently degrading.
@@ -185,6 +185,9 @@ function resolveConfig(fileConfig, overrides) {
185
185
  migrationsDir: fileConfig.migrationsDir ?? './turbine/migrations',
186
186
  seedFile: fileConfig.seed ?? fileConfig.seedFile,
187
187
  schemaFile: fileConfig.schemaFile ?? './turbine/schema.ts',
188
+ importExtension: overrides.importExtension ?? fileConfig.importExtension ?? 'auto',
189
+ keepColumnNames: overrides.keepColumnNames ?? fileConfig.keepColumnNames ?? false,
190
+ legacyToManyUniques: overrides.legacyToManyUniques ?? fileConfig.legacyToManyUniques ?? false,
188
191
  };
189
192
  }
190
193
  /**
@@ -6,6 +6,7 @@
6
6
  * Commands:
7
7
  * turbine init — Initialize a Turbine project
8
8
  * turbine generate | pull — Introspect database and generate TypeScript types
9
+ * turbine migrate-from-prisma - Parse a schema.prisma and emit a Prisma->Turbine name map + report
9
10
  * turbine push - Apply schema-builder definitions to database (destructive ops gated)
10
11
  * turbine migrate create <name> - Create a new SQL migration file (--auto | --from-diff | --recipe <name>)
11
12
  * turbine migrate up — Apply pending migrations
@@ -80,6 +81,9 @@ const loader_js_1 = require("./loader.js");
80
81
  const mcp_js_1 = require("./mcp.js");
81
82
  const migrate_js_1 = require("./migrate.js");
82
83
  const observe_js_1 = require("./observe.js");
84
+ const prisma_report_js_1 = require("./prisma-report.js");
85
+ const prisma_resolve_js_1 = require("./prisma-resolve.js");
86
+ const prisma_schema_js_1 = require("./prisma-schema.js");
83
87
  const studio_js_1 = require("./studio.js");
84
88
  const ui_js_1 = require("./ui.js");
85
89
  function parseArgs(argv = process.argv.slice(2)) {
@@ -169,6 +173,21 @@ function parseArgs(argv = process.argv.slice(2)) {
169
173
  case '--no-timestamp':
170
174
  result.noTimestamp = true;
171
175
  break;
176
+ case '--import-ext':
177
+ case '--import-extension':
178
+ if (next !== 'js' && next !== 'none' && next !== 'auto') {
179
+ console.error(`--import-ext requires one of: js, none, auto (got ${next ?? '(nothing)'})`);
180
+ process.exit(1);
181
+ }
182
+ result.importExtension = next;
183
+ i++;
184
+ break;
185
+ case '--keep-column-names':
186
+ result.keepColumnNames = true;
187
+ break;
188
+ case '--legacy-to-many-uniques':
189
+ result.legacyToManyUniques = true;
190
+ break;
172
191
  case '--allow-destructive':
173
192
  result.allowDestructive = true;
174
193
  break;
@@ -215,6 +234,12 @@ function parseArgs(argv = process.argv.slice(2)) {
215
234
  case '--demo':
216
235
  result.demo = true;
217
236
  break;
237
+ case '--allow-partial':
238
+ result.allowPartial = true;
239
+ break;
240
+ case '--no-db':
241
+ result.noDb = true;
242
+ break;
218
243
  default:
219
244
  if (!arg.startsWith('-')) {
220
245
  result.positional.push(arg);
@@ -930,17 +955,25 @@ async function cmdGenerate(args, config) {
930
955
  (0, ui_js_1.newline)();
931
956
  // Introspect
932
957
  const spinner = new ui_js_1.Spinner('Introspecting database schema').start();
958
+ const skippedInternalTables = [];
933
959
  const schema = await (0, introspect_js_1.introspect)({
934
960
  connectionString: url,
935
961
  schema: config.schema,
936
962
  include: config.include.length ? config.include : undefined,
937
963
  exclude: config.exclude.length ? config.exclude : undefined,
938
964
  includeViews: args.includeViews,
965
+ legacyToManyUniques: config.legacyToManyUniques,
966
+ onDefaultTableExclusion: (tables) => skippedInternalTables.push(...tables),
939
967
  });
940
968
  const tableNames = Object.keys(schema.tables);
941
969
  const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
942
970
  const totalRelations = Object.values(schema.tables).reduce((sum, t) => sum + Object.keys(t.relations).length, 0);
943
971
  spinner.succeed(`Found ${(0, ui_js_1.bold)(String(tableNames.length))} tables, ${(0, ui_js_1.bold)(String(totalColumns))} columns, ${(0, ui_js_1.bold)(String(totalRelations))} relations`);
972
+ // F12: make the default bookkeeping-table exclusions discoverable rather than
973
+ // silent. `include` re-adds any of them byte-for-byte.
974
+ for (const t of skippedInternalTables) {
975
+ console.log(` ${(0, ui_js_1.dim)(`${ui_js_1.symbols.teeEnd} skipped internal table ${t} (add it to include to keep it)`)}`);
976
+ }
944
977
  // Guard: zero tables means the generated client would be empty. That is almost
945
978
  // always a misconfiguration (wrong `schema`, an include/exclude that filtered
946
979
  // everything, or a database with no tables yet) rather than intent. Fail loudly
@@ -980,6 +1013,8 @@ async function cmdGenerate(args, config) {
980
1013
  connectionString: url,
981
1014
  zod: args.zod,
982
1015
  noTimestamp: args.noTimestamp,
1016
+ importExtension: config.importExtension,
1017
+ keepColumnNames: config.keepColumnNames,
983
1018
  });
984
1019
  genSpinner.succeed(`Generated ${(0, ui_js_1.bold)(String(result.files.length))} files in ${(0, ui_js_1.elapsed)(startTime)}`);
985
1020
  // List files
@@ -998,6 +1033,112 @@ async function cmdGenerate(args, config) {
998
1033
  (0, ui_js_1.newline)();
999
1034
  }
1000
1035
  // ---------------------------------------------------------------------------
1036
+ // migrate-from-prisma
1037
+ // ---------------------------------------------------------------------------
1038
+ /**
1039
+ * `turbine migrate-from-prisma --schema prisma/schema.prisma` parses a Prisma
1040
+ * schema, resolve its models/fields/relations/compound-uniques against the live
1041
+ * database (unless `--no-db`), and emit (a) a Markdown resolution report and
1042
+ * (b) a typed `prisma-map.ts` name map next to the generated client.
1043
+ *
1044
+ * NOTE: within THIS command `--schema` names the Prisma schema FILE (not the
1045
+ * Postgres namespace, which the rest of the CLI's `--schema` means). The
1046
+ * Postgres namespace is `public` here; multi-schema (`@@schema`) is unsupported
1047
+ * in v1 and listed as a parser note in the report.
1048
+ */
1049
+ async function cmdMigrateFromPrisma(args, config) {
1050
+ (0, ui_js_1.banner)();
1051
+ // `--schema` is the Prisma schema file path in this command.
1052
+ const prismaPath = (0, node_path_1.resolve)(args.schema ?? 'prisma/schema.prisma');
1053
+ if (!(0, node_fs_1.existsSync)(prismaPath)) {
1054
+ (0, ui_js_1.error)(`Prisma schema file not found: ${(0, ui_js_1.cyan)(prismaPath)}`);
1055
+ (0, ui_js_1.newline)();
1056
+ console.log(` ${(0, ui_js_1.dim)('Point at it with')} ${(0, ui_js_1.cyan)('--schema <path/to/schema.prisma>')} ${(0, ui_js_1.dim)('(default: prisma/schema.prisma).')}`);
1057
+ (0, ui_js_1.newline)();
1058
+ process.exit(1);
1059
+ }
1060
+ // Parse (fatal only on a construct we must understand).
1061
+ const source = (0, node_fs_1.readFileSync)(prismaPath, 'utf-8');
1062
+ let ast;
1063
+ try {
1064
+ ast = (0, prisma_schema_js_1.parsePrismaSchema)(source);
1065
+ }
1066
+ catch (err) {
1067
+ if (err instanceof prisma_schema_js_1.PrismaParseError) {
1068
+ (0, ui_js_1.newline)();
1069
+ (0, ui_js_1.error)(`Could not parse ${(0, ui_js_1.cyan)(prismaPath)}`);
1070
+ console.log(` ${(0, ui_js_1.red)(err.message)}`);
1071
+ (0, ui_js_1.newline)();
1072
+ process.exit(1);
1073
+ }
1074
+ throw err;
1075
+ }
1076
+ (0, ui_js_1.label)('Prisma schema', prismaPath);
1077
+ (0, ui_js_1.label)('Models', String(ast.models.length));
1078
+ (0, ui_js_1.label)('Enums', String(ast.enums.length));
1079
+ // Resolve against the live database, unless --no-db (parse-only).
1080
+ let schemaMeta = null;
1081
+ if (args.noDb) {
1082
+ (0, ui_js_1.info)('Parse-only mode (--no-db): names will not be resolved.');
1083
+ }
1084
+ else {
1085
+ const url = requireUrl(config);
1086
+ (0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
1087
+ const spinner = new ui_js_1.Spinner('Introspecting database schema').start();
1088
+ schemaMeta = await (0, introspect_js_1.introspect)({
1089
+ connectionString: url,
1090
+ // The Postgres NAMESPACE is fixed to `public` here (`--schema` names the
1091
+ // Prisma file, not the namespace).
1092
+ schema: 'public',
1093
+ // Prisma `view` models resolve against introspected views.
1094
+ includeViews: true,
1095
+ // Inherit the shared bookkeeping-table exclusions (Turbine + Prisma).
1096
+ exclude: [...new Set([...config.exclude, ...prisma_resolve_js_1.DEFAULT_EXCLUDED_TABLES])],
1097
+ });
1098
+ spinner.succeed(`Introspected ${(0, ui_js_1.bold)(String(Object.keys(schemaMeta.tables).length))} tables`);
1099
+ }
1100
+ (0, ui_js_1.newline)();
1101
+ const result = (0, prisma_resolve_js_1.resolvePrismaSchema)(ast, schemaMeta);
1102
+ // Console summary.
1103
+ (0, ui_js_1.header)('Resolution');
1104
+ for (const line of (0, prisma_report_js_1.summaryLines)(result)) {
1105
+ const marker = line.includes('[UNRESOLVED]') ? (0, ui_js_1.red)(ui_js_1.symbols.cross) : (0, ui_js_1.green)(ui_js_1.symbols.check);
1106
+ console.log(` ${marker} ${line}`);
1107
+ }
1108
+ (0, ui_js_1.newline)();
1109
+ // Write outputs into the generate outDir.
1110
+ const outDir = (0, node_path_1.resolve)(config.out);
1111
+ const rel = (0, node_path_1.relative)(process.cwd(), outDir);
1112
+ if (rel.startsWith('..') || (0, node_path_1.resolve)(rel) !== outDir) {
1113
+ (0, ui_js_1.error)(`Output directory must be within the project root. Got: ${config.out}`);
1114
+ (0, ui_js_1.newline)();
1115
+ process.exit(1);
1116
+ }
1117
+ (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
1118
+ const reportPath = (0, node_path_1.join)(outDir, 'prisma-migration-report.md');
1119
+ (0, node_fs_1.writeFileSync)(reportPath, (0, prisma_report_js_1.formatPrismaReport)(result, { schemaPath: prismaPath, noTimestamp: args.noTimestamp }), 'utf-8');
1120
+ 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) {
1122
+ const mapPath = (0, node_path_1.join)(outDir, 'prisma-map.ts');
1123
+ (0, node_fs_1.writeFileSync)(mapPath, (0, generate_js_1.generatePrismaMap)(result.map, { noTimestamp: args.noTimestamp }), 'utf-8');
1124
+ 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)')}`);
1125
+ }
1126
+ (0, ui_js_1.newline)();
1127
+ // Exit non-zero when anything is UNRESOLVED, unless --allow-partial.
1128
+ if (result.hasUnresolved && !args.allowPartial) {
1129
+ (0, ui_js_1.warn)('Some items could not be resolved (see the report). Re-run with --allow-partial to accept a partial map.');
1130
+ (0, ui_js_1.newline)();
1131
+ process.exit(1);
1132
+ }
1133
+ if (args.noDb) {
1134
+ (0, ui_js_1.info)(`Parse-only report written. Re-run without ${(0, ui_js_1.cyan)('--no-db')} against your database to resolve names.`);
1135
+ }
1136
+ else {
1137
+ (0, ui_js_1.success)('Prisma name map generated.');
1138
+ }
1139
+ (0, ui_js_1.newline)();
1140
+ }
1141
+ // ---------------------------------------------------------------------------
1001
1142
  // Command: push
1002
1143
  // ---------------------------------------------------------------------------
1003
1144
  async function cmdPush(args, config) {
@@ -2164,6 +2305,7 @@ function showSubcommandHelp(command) {
2164
2305
  init: showInitHelp,
2165
2306
  generate: showGenerateHelp,
2166
2307
  pull: showGenerateHelp,
2308
+ 'migrate-from-prisma': showMigrateFromPrismaHelp,
2167
2309
  push: showPushHelp,
2168
2310
  migrate: showMigrateHelp,
2169
2311
  migration: showMigrateHelp,
@@ -2225,9 +2367,39 @@ function showGenerateHelp() {
2225
2367
  console.log(` ${(0, ui_js_1.cyan)('--zod')} Also emit ${(0, ui_js_1.cyan)('zod.ts')} validation schemas ${(0, ui_js_1.dim)('(needs the zod dep)')}`);
2226
2368
  console.log(` ${(0, ui_js_1.cyan)('--include-views')} Include views + materialized views as read-only entities`);
2227
2369
  console.log(` ${(0, ui_js_1.cyan)('--no-timestamp')} Omit the ${(0, ui_js_1.dim)('Generated at:')} header line ${(0, ui_js_1.dim)('(reproducible, diff-stable output)')}`);
2370
+ console.log(` ${(0, ui_js_1.cyan)('--import-ext')} ${(0, ui_js_1.dim)('<mode>')} Sibling-import extension: ${(0, ui_js_1.cyan)('js')} / ${(0, ui_js_1.cyan)('none')} / ${(0, ui_js_1.cyan)('auto')} ${(0, ui_js_1.dim)('(default: auto)')}`);
2371
+ console.log(` ${(0, ui_js_1.cyan)('--keep-column-names')} Keep raw DB column names as field names ${(0, ui_js_1.dim)('(snake_case, not camelCase)')}`);
2372
+ console.log(` ${(0, ui_js_1.cyan)('--legacy-to-many-uniques')} Emit ${(0, ui_js_1.cyan)('hasMany')} for unique-FK children ${(0, ui_js_1.dim)('(pre-0.41 shape; default flips to hasOne)')}`);
2228
2373
  console.log(` ${(0, ui_js_1.cyan)('--allow-empty')} Generate even when introspection matches 0 tables`);
2229
2374
  (0, ui_js_1.newline)();
2230
2375
  }
2376
+ function showMigrateFromPrismaHelp() {
2377
+ (0, ui_js_1.banner)();
2378
+ console.log(` ${(0, ui_js_1.bold)('turbine migrate-from-prisma')} - Map a Prisma schema onto a Turbine client`);
2379
+ (0, ui_js_1.newline)();
2380
+ console.log(` ${(0, ui_js_1.bold)('Usage:')}`);
2381
+ console.log(` npx turbine migrate-from-prisma ${(0, ui_js_1.dim)('--schema prisma/schema.prisma [options]')}`);
2382
+ (0, ui_js_1.newline)();
2383
+ console.log(` Parses your ${(0, ui_js_1.cyan)('schema.prisma')}, resolves models/fields/relations/compound`);
2384
+ console.log(` uniques against the live database, and writes into the output directory:`);
2385
+ console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('prisma-migration-report.md')} - per-model resolution + unresolved items`);
2386
+ console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('prisma-map.ts')} - typed PRISMA_MAP name map`);
2387
+ (0, ui_js_1.newline)();
2388
+ console.log(` ${(0, ui_js_1.dim)('Note:')} here ${(0, ui_js_1.cyan)('--schema')} names the Prisma FILE (not the Postgres namespace).`);
2389
+ (0, ui_js_1.newline)();
2390
+ console.log(` ${(0, ui_js_1.bold)('Options:')}`);
2391
+ console.log(` ${(0, ui_js_1.cyan)('--schema')} ${(0, ui_js_1.dim)('<file>')} Path to schema.prisma ${(0, ui_js_1.dim)('(default: prisma/schema.prisma)')}`);
2392
+ console.log(` ${(0, ui_js_1.cyan)('--url, -u')} ${(0, ui_js_1.dim)('<url>')} Postgres connection string ${(0, ui_js_1.dim)('(unless --no-db)')}`);
2393
+ console.log(` ${(0, ui_js_1.cyan)('--out, -o')} ${(0, ui_js_1.dim)('<dir>')} Output directory ${(0, ui_js_1.dim)('(default: ./generated/turbine)')}`);
2394
+ console.log(` ${(0, ui_js_1.cyan)('--no-db')} Parse-only: write the report without resolving names`);
2395
+ console.log(` ${(0, ui_js_1.cyan)('--allow-partial')} Exit 0 even when some items are UNRESOLVED`);
2396
+ console.log(` ${(0, ui_js_1.cyan)('--no-timestamp')} Omit the ${(0, ui_js_1.dim)('Generated:')} lines ${(0, ui_js_1.dim)('(reproducible output)')}`);
2397
+ (0, ui_js_1.newline)();
2398
+ console.log(` ${(0, ui_js_1.bold)('Examples:')}`);
2399
+ console.log(` ${(0, ui_js_1.dim)('$')} DATABASE_URL=postgres://... npx turbine migrate-from-prisma --schema prisma/schema.prisma`);
2400
+ console.log(` ${(0, ui_js_1.dim)('$')} npx turbine migrate-from-prisma --schema prisma/schema.prisma --no-db`);
2401
+ (0, ui_js_1.newline)();
2402
+ }
2231
2403
  function showPushHelp() {
2232
2404
  (0, ui_js_1.banner)();
2233
2405
  console.log(` ${(0, ui_js_1.bold)('turbine push')} — Apply schema-builder definitions to database`);
@@ -2340,6 +2512,7 @@ function showHelp() {
2340
2512
  console.log(` ${(0, ui_js_1.bold)('Commands:')}`);
2341
2513
  console.log(` ${(0, ui_js_1.cyan)('init')} Initialize a Turbine project`);
2342
2514
  console.log(` ${(0, ui_js_1.cyan)('generate')} ${(0, ui_js_1.dim)('| pull')} Introspect database ${ui_js_1.symbols.arrow} generate types`);
2515
+ console.log(` ${(0, ui_js_1.cyan)('migrate-from-prisma')} Map a schema.prisma onto Turbine ${(0, ui_js_1.dim)('(report + typed name map)')}`);
2343
2516
  console.log(` ${(0, ui_js_1.cyan)('push')} Apply schema definitions to database`);
2344
2517
  console.log(` ${(0, ui_js_1.cyan)('migrate')} ${(0, ui_js_1.dim)('<sub>')} SQL migration management`);
2345
2518
  console.log(` ${(0, ui_js_1.dim)('create <name>')} Create a new migration file`);
@@ -2500,6 +2673,9 @@ async function main() {
2500
2673
  schema: args.schema,
2501
2674
  include: args.include,
2502
2675
  exclude: args.exclude,
2676
+ importExtension: args.importExtension,
2677
+ keepColumnNames: args.keepColumnNames,
2678
+ legacyToManyUniques: args.legacyToManyUniques,
2503
2679
  };
2504
2680
  const config = (0, config_js_1.resolveConfig)(fileConfig, overrides);
2505
2681
  // Warn (don't change precedence) when an .env-sourced DATABASE_URL is silently
@@ -2526,6 +2702,9 @@ async function main() {
2526
2702
  case 'pull':
2527
2703
  await cmdGenerate(args, config);
2528
2704
  break;
2705
+ case 'migrate-from-prisma':
2706
+ await cmdMigrateFromPrisma(args, config);
2707
+ break;
2529
2708
  case 'push':
2530
2709
  await cmdPush(args, config);
2531
2710
  break;
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ /**
3
+ * Render a {@link ResolutionResult} into the `prisma-migration-report.md`
4
+ * artifact and a short console summary. Pure leaf - string in, string out.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.formatPrismaReport = formatPrismaReport;
8
+ exports.collectUnresolved = collectUnresolved;
9
+ exports.summaryLines = summaryLines;
10
+ const CHECK = 'OK';
11
+ const CROSS = 'UNRESOLVED';
12
+ function modelDisplayStatus(m) {
13
+ if (m.status === 'parsed')
14
+ return 'parsed';
15
+ if (m.status === 'unresolved')
16
+ return CROSS;
17
+ return m.viaMap ? `${CHECK} (@@map)` : CHECK;
18
+ }
19
+ /**
20
+ * Build the full Markdown migration report.
21
+ */
22
+ function formatPrismaReport(result, options = {}) {
23
+ const L = [];
24
+ L.push('# Prisma to Turbine migration report');
25
+ L.push('');
26
+ if (options.schemaPath)
27
+ L.push(`Source: \`${options.schemaPath}\``);
28
+ if (!options.noTimestamp)
29
+ L.push(`Generated: ${new Date().toISOString()}`);
30
+ L.push(`Mode: ${result.noDb ? 'parse-only (--no-db, no database resolution)' : 'resolved against live database'}`);
31
+ L.push('');
32
+ // ---- Summary ----------------------------------------------------------
33
+ const modelCount = result.models.length;
34
+ const resolvedModels = result.models.filter((m) => m.status === 'resolved').length;
35
+ const unresolvedModels = result.models.filter((m) => m.status === 'unresolved').length;
36
+ L.push('## Summary');
37
+ L.push('');
38
+ if (result.noDb) {
39
+ L.push(`- Parsed ${modelCount} model(s), ${result.enums.length} enum(s).`);
40
+ L.push('- No database URL provided - names were not resolved. Re-run without `--no-db` to resolve.');
41
+ }
42
+ else {
43
+ L.push(`- Models: ${resolvedModels}/${modelCount} resolved${unresolvedModels ? `, ${unresolvedModels} UNRESOLVED` : ''}.`);
44
+ L.push(`- Enums: ${result.enums.filter((e) => e.status === 'resolved').length}/${result.enums.length} resolved.`);
45
+ L.push(`- Overall: ${result.hasUnresolved ? 'INCOMPLETE (some items unresolved)' : 'complete (all items resolved)'}.`);
46
+ }
47
+ L.push('');
48
+ // ---- Model resolution table ------------------------------------------
49
+ L.push('## Models');
50
+ L.push('');
51
+ L.push('| Prisma model | Turbine accessor | Table | Status |');
52
+ L.push('| --- | --- | --- | --- |');
53
+ for (const m of result.models) {
54
+ L.push(`| ${m.prismaName} | ${m.accessor ?? '-'} | ${m.table ?? '-'} | ${modelDisplayStatus(m)} |`);
55
+ }
56
+ L.push('');
57
+ // ---- Per-model detail -------------------------------------------------
58
+ for (const m of result.models) {
59
+ L.push(`### ${m.prismaName}`);
60
+ L.push('');
61
+ if (m.status === 'unresolved') {
62
+ L.push(`> UNRESOLVED: ${m.reason ?? 'no matching table'}`);
63
+ L.push('');
64
+ }
65
+ if (m.fields.length > 0) {
66
+ L.push('Fields:');
67
+ L.push('');
68
+ for (const f of m.fields) {
69
+ if (f.status === 'resolved') {
70
+ L.push(`- \`${f.prismaName}\` -> \`${f.turbineField}\` (column \`${f.column}\`)`);
71
+ }
72
+ else if (f.status === 'parsed') {
73
+ L.push(`- \`${f.prismaName}\` (parsed)`);
74
+ }
75
+ else {
76
+ L.push(`- \`${f.prismaName}\` UNRESOLVED: ${f.reason}`);
77
+ }
78
+ }
79
+ L.push('');
80
+ }
81
+ if (m.relations.length > 0) {
82
+ L.push('Relations:');
83
+ L.push('');
84
+ for (const r of m.relations) {
85
+ if (r.status === 'resolved') {
86
+ const j = r.junction ? `, junction \`${r.junction}\`` : '';
87
+ L.push(`- \`${r.prismaName}\` -> \`${r.turbineName}\` (${r.cardinality}${j})`);
88
+ }
89
+ else if (r.status === 'parsed') {
90
+ L.push(`- \`${r.prismaName}\` -> ${r.targetModel} (parsed)`);
91
+ }
92
+ else {
93
+ L.push(`- \`${r.prismaName}\` -> ${r.targetModel} UNRESOLVED: ${r.reason}`);
94
+ }
95
+ }
96
+ L.push('');
97
+ }
98
+ if (m.compoundUniques.length > 0) {
99
+ L.push('Compound unique / id selectors:');
100
+ L.push('');
101
+ for (const c of m.compoundUniques) {
102
+ if (c.status === 'resolved') {
103
+ L.push(`- \`${c.selector}\` (@@${c.kind}) -> [${c.turbineFields.join(', ')}]`);
104
+ }
105
+ else if (c.status === 'parsed') {
106
+ L.push(`- \`${c.selector}\` (@@${c.kind}, parsed): [${c.prismaFields.join(', ')}]`);
107
+ }
108
+ else {
109
+ L.push(`- \`${c.selector}\` (@@${c.kind}) UNRESOLVED: ${c.reason}`);
110
+ }
111
+ }
112
+ L.push('');
113
+ }
114
+ }
115
+ // ---- Junction tables --------------------------------------------------
116
+ const junctions = new Set();
117
+ for (const m of result.models) {
118
+ for (const r of m.relations)
119
+ if (r.junction)
120
+ junctions.add(r.junction);
121
+ }
122
+ if (junctions.size > 0) {
123
+ L.push('## Junction tables (implicit m2m)');
124
+ L.push('');
125
+ for (const j of [...junctions].sort())
126
+ L.push(`- \`${j}\``);
127
+ L.push('');
128
+ }
129
+ // ---- Enums ------------------------------------------------------------
130
+ if (result.enums.length > 0) {
131
+ L.push('## Enums');
132
+ L.push('');
133
+ for (const e of result.enums) {
134
+ if (e.status === 'resolved')
135
+ L.push(`- \`${e.prismaName}\` -> \`${e.turbineName}\``);
136
+ else if (e.status === 'parsed')
137
+ L.push(`- \`${e.prismaName}\` (parsed)`);
138
+ else
139
+ L.push(`- \`${e.prismaName}\` UNRESOLVED: ${e.reason}`);
140
+ }
141
+ L.push('');
142
+ }
143
+ // ---- Unresolved roll-up ----------------------------------------------
144
+ const unresolved = collectUnresolved(result);
145
+ if (unresolved.length > 0) {
146
+ L.push('## Unresolved items');
147
+ L.push('');
148
+ for (const u of unresolved)
149
+ L.push(`- ${u}`);
150
+ L.push('');
151
+ }
152
+ // ---- Parser warnings --------------------------------------------------
153
+ if (result.parseWarnings.length > 0) {
154
+ L.push('## Parser notes');
155
+ L.push('');
156
+ for (const w of result.parseWarnings)
157
+ L.push(`- ${w}`);
158
+ L.push('');
159
+ }
160
+ // ---- Fixed semantic-divergence section --------------------------------
161
+ L.push(SEMANTIC_DIVERGENCE);
162
+ return `${L.join('\n')}\n`;
163
+ }
164
+ /** Flat list of unresolved item descriptions across the whole result. */
165
+ function collectUnresolved(result) {
166
+ const out = [];
167
+ for (const m of result.models) {
168
+ if (m.status === 'unresolved')
169
+ out.push(`Model ${m.prismaName}: ${m.reason ?? 'no matching table'}`);
170
+ for (const f of m.fields) {
171
+ if (f.status === 'unresolved')
172
+ out.push(`${m.prismaName}.${f.prismaName} (field): ${f.reason}`);
173
+ }
174
+ for (const r of m.relations) {
175
+ if (r.status === 'unresolved')
176
+ out.push(`${m.prismaName}.${r.prismaName} (relation): ${r.reason}`);
177
+ }
178
+ for (const c of m.compoundUniques) {
179
+ if (c.status === 'unresolved')
180
+ out.push(`${m.prismaName}.${c.selector} (@@${c.kind}): ${c.reason}`);
181
+ }
182
+ }
183
+ for (const e of result.enums) {
184
+ if (e.status === 'unresolved')
185
+ out.push(`Enum ${e.prismaName}: ${e.reason}`);
186
+ }
187
+ return out;
188
+ }
189
+ /** Static section documenting known Prisma-vs-Turbine behavior differences. */
190
+ const SEMANTIC_DIVERGENCE = `## Behavior notes (Prisma vs Turbine)
191
+
192
+ These are deliberate semantic differences to keep in mind when porting queries.
193
+ Phase 1 ships no runtime; it produces this report plus a typed name map. The
194
+ phase-2 \`turbine-orm/prisma-compat\` adapter handles most of these translations.
195
+
196
+ - Cursor pagination. Turbine cursors are EXCLUSIVE and the comparison direction
197
+ follows the \`orderBy\` entry for the cursor field. Prisma cursors are
198
+ INCLUSIVE and idiomatically paired with \`skip: 1\`. Port \`{ cursor, skip: n }\`
199
+ (n >= 1) to a Turbine cursor plus \`offset: n - 1\`.
200
+ - Aggregate / groupBy \`_count\`. Prisma returns \`_count\` as a record
201
+ (\`{ _all: n }\` / per-field counts). Turbine's scalar \`_count: true\` returns a
202
+ number. Reshape as needed (the phase-2 adapter does this both directions).
203
+ - Relation-array order. Without an \`orderBy\` on a \`with\`/\`include\` clause, the
204
+ order of a to-many relation array is unspecified in Turbine (\`json_agg\` order).
205
+ Add an explicit \`orderBy\` where order matters.
206
+ - Connection URL. Prefer an explicit \`sslmode\` in the connection URL (or the
207
+ future-proof \`uselibpqcompat\` form) to avoid a per-boot pg SSL security
208
+ warning.`;
209
+ /** A one-line-per-model console summary for the CLI. */
210
+ function summaryLines(result) {
211
+ return result.models.map((m) => {
212
+ const status = modelDisplayStatus(m);
213
+ const target = m.accessor ? `${m.accessor} (${m.table})` : '-';
214
+ return `${m.prismaName} -> ${target} [${status}]`;
215
+ });
216
+ }