@tailor-platform/sdk 2.2.0 → 2.3.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 (55) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/dist/application-BZfk4HKm.mjs +3 -0
  3. package/dist/{application-BpLeawBg.mjs → application-F-nl107y.mjs} +118 -24
  4. package/dist/application-F-nl107y.mjs.map +1 -0
  5. package/dist/cli/commands/tailordb/migrate/diff-calculator.d.mts +43 -20
  6. package/dist/cli/commands/tailordb/migrate/generate.d.mts +6 -0
  7. package/dist/cli/commands/tailordb/migrate/rename-detection.d.mts +22 -0
  8. package/dist/cli/commands/tailordb/migrate/snapshot-types.d.mts +1 -1
  9. package/dist/cli/commands/tailordb/migrate/snapshot.d.mts +26 -7
  10. package/dist/cli/lib.mjs +2 -2
  11. package/dist/cli/main.mjs +203 -42
  12. package/dist/cli/main.mjs.map +1 -1
  13. package/dist/cli/ts-hook.mjs +52 -7
  14. package/dist/completion/zsh-worker.zsh +23 -3
  15. package/dist/configure/index.d.mts +2 -2
  16. package/dist/configure/index.mjs +90 -23
  17. package/dist/configure/index.mjs.map +1 -1
  18. package/dist/configure/services/index.d.mts +2 -2
  19. package/dist/configure/services/workflow/index.d.mts +2 -2
  20. package/dist/configure/services/workflow/wait-point.d.mts +72 -13
  21. package/dist/{errors-CWj21238.mjs → errors-BVb6vYGy.mjs} +16 -2
  22. package/dist/{errors-CWj21238.mjs.map → errors-BVb6vYGy.mjs.map} +1 -1
  23. package/dist/kysely/index.d.mts +2 -1
  24. package/dist/kysely/index.mjs.map +1 -1
  25. package/dist/{register-ts-hook-CTth1eqj.mjs → register-ts-hook-ClI226n2.mjs} +2034 -511
  26. package/dist/register-ts-hook-ClI226n2.mjs.map +1 -0
  27. package/dist/{service-goqlJJgA.mjs → service-C_WpbKHu.mjs} +38 -4
  28. package/dist/service-C_WpbKHu.mjs.map +1 -0
  29. package/dist/service-D1RCdzIL.mjs +3 -0
  30. package/dist/{service-CCwl3Avt.mjs → service-hZskxZmg.mjs} +2 -2
  31. package/dist/{service-CCwl3Avt.mjs.map → service-hZskxZmg.mjs.map} +1 -1
  32. package/dist/vitest/index.mjs +83 -1
  33. package/dist/vitest/index.mjs.map +1 -1
  34. package/dist/vitest/mocks/file.d.mts +1 -1
  35. package/dist/vitest/mocks/workflow.d.mts +13 -1
  36. package/dist/wait-point-invoker-__oE88_P.mjs +148 -0
  37. package/dist/wait-point-invoker-__oE88_P.mjs.map +1 -0
  38. package/dist/wait-point-registry-TL99zotw.mjs +47 -0
  39. package/dist/wait-point-registry-TL99zotw.mjs.map +1 -0
  40. package/docs/cli/setup.md +14 -1
  41. package/docs/cli/tailordb.md +9 -6
  42. package/docs/cli-reference.md +2 -1
  43. package/docs/github-actions.md +73 -28
  44. package/docs/migration/v2.md +54 -0
  45. package/docs/services/tailordb-migration.md +137 -35
  46. package/docs/services/workflow.md +52 -2
  47. package/docs/testing.md +12 -0
  48. package/package.json +4 -4
  49. package/dist/application-BpLeawBg.mjs.map +0 -1
  50. package/dist/application-BxLLiLsr.mjs +0 -3
  51. package/dist/register-ts-hook-CTth1eqj.mjs.map +0 -1
  52. package/dist/service-_XmjSEGr.mjs +0 -3
  53. package/dist/service-goqlJJgA.mjs.map +0 -1
  54. package/dist/test-env-key-D7UkZp99.mjs +0 -75
  55. package/dist/test-env-key-D7UkZp99.mjs.map +0 -1
@@ -1,13 +1,14 @@
1
1
  import { t as db } from "./schema--xYWRGfe.mjs";
2
+ import { t as beginWaitPointScope } from "./wait-point-registry-TL99zotw.mjs";
2
3
  import { $ as CreateAuthHookRequestSchema, B as IdPLang, Bt as ApplicationSchemaUpdateAttemptStatus, C as CreateStaticWebsiteRequestSchema, Ct as AuthInvokerSchema, D as CreateSecretManagerSecretRequestSchema, Dt as AuthSCIMAttribute_Type, Et as AuthSCIMAttribute_Mutability, H as IdPPermissionPermit, Ht as CreateAIGatewayRequestSchema, I as PipelineResolver_OperationType, It as CreateApplicationRequestSchema, Jt as Condition_Operator, K as CreateExecutorExecutorRequestSchema, L as CreateIdPServiceRequestSchema, Lt as GetApplicationSchemaHealthResponse_ApplicationSchemaHealthStatus, M as CreatePipelineServiceRequestSchema, Mt as UserProfileProviderConfig_UserProfileProviderType, N as UpdatePipelineResolverRequestSchema, O as CreateSecretManagerVaultRequestSchema, Ot as AuthSCIMAttribute_Uniqueness, P as UpdatePipelineServiceRequestSchema, Q as CreateAuthConnectionRequestSchema, R as UpdateIdPServiceRequestSchema, Rt as UpdateApplicationRequestSchema, S as AddCustomDomainRequestSchema, St as AuthIDPConfig_AuthType, Tt as AuthOAuth2Client_GrantType, U as FunctionExecution_Status, Ut as UpdateAIGatewayRequestSchema, V as IdPPermissionOperator, Vt as Subgraph_ServiceType, X as ExecutorTargetType, Xt as PageDirection, Y as ExecutorJobStatus, Yt as FilterSchema, Z as ExecutorTriggerType, _ as TailorDBGQLPermission_Permit, _t as UpdateUserProfileConfigRequestSchema, a as CreateWorkflowRequestSchema, at as CreateAuthServiceRequestSchema, b as TailorDBType_Permission_Permit, bt as AuthConnection_Type, ct as UpdateAuthConnectionRequestSchema, d as CreateTailorDBServiceRequestSchema, dt as UpdateAuthMachineUserRequestSchema, et as CreateAuthIDPConfigRequestSchema, f as CreateTailorDBTypeRequestSchema, ft as UpdateAuthOAuth2ClientRequestSchema, g as TailorDBGQLPermission_Operator, gt as UpdateTenantConfigRequestSchema, h as TailorDBGQLPermission_Action, ht as UpdateAuthServiceRequestSchema, i as CreateWorkflowJobFunctionRequestSchema, it as CreateAuthSCIMResourceRequestSchema, j as CreatePipelineResolverRequestSchema, jt as TenantProviderConfig_TenantProviderType, k as UpdateSecretManagerSecretRequestSchema, kt as AuthSCIMConfig_AuthorizationType, l as WorkflowExecution_Status, lt as UpdateAuthHookRequestSchema, mt as UpdateAuthSCIMResourceRequestSchema, nt as CreateAuthOAuth2ClientRequestSchema, o as UpdateWorkflowJobFunctionExecutionPolicyRequestSchema, ot as CreateTenantConfigRequestSchema, p as UpdateTailorDBTypeRequestSchema, pt as UpdateAuthSCIMConfigRequestSchema, q as UpdateExecutorExecutorRequestSchema, qt as ConditionSchema, r as CreateWorkflowJobFunctionExecutionPolicyRequestSchema, rt as CreateAuthSCIMConfigRequestSchema, s as UpdateWorkflowRequestSchema, st as CreateUserProfileConfigRequestSchema, t as WorkspacePlatformUserRole, tt as CreateAuthMachineUserRequestSchema, u as WorkflowJobExecution_Status, ut as UpdateAuthIDPConfigRequestSchema, v as TailorDBTypeSchema, w as UpdateStaticWebsiteRequestSchema, wt as AuthOAuth2Client_ClientType, x as TailorDBType_PermitAction, xt as AuthHookPoint, y as TailorDBType_Permission_Operator, yt as AuthConnection_Status } from "./workspace_resource_pb-HNIL_Vfj.mjs";
3
4
  import { t as assertDefined } from "./assert-DBxo8jPo.mjs";
4
5
  import { a as parseBoolean, i as symbols, n as logger, o as renderTable, r as styles, s as renderFor, t as CIPromptError } from "./logger-DSdExfor.mjs";
5
- import { $ as createApplyLimiter, A as getDistDir, B as fetchUserInfo, D as getPluginGenerationDependencies, E as createBundleLog, F as fetchAll, G as initOAuth2Client, H as getOAuth2ClientId, I as fetchAllTolerant, J as isNotFoundError, K as initOperatorClient, L as fetchMachineUserToken, M as hashFile, O as hasGenerationHooks, P as defaultPlatformBaseUrl, Q as byName, R as fetchPaged, S as platformBundleDefinePlugin, U as getOrNull, V as getConsoleBaseUrl, W as getPlatformBaseUrl, X as rememberPlatformConfigForToken, Y as normalizeBaseUrl, Z as resolveStaticWebsiteUrls, _ as assertUniqueTailorDBTypeNamesWithExternal, b as createTsconfigPathsPlugin, d as HTTP_METHODS, et as LOG_LEVELS, g as assertUniqueLocalTailorDBTypeNames, i as resolverBundleKey, j as hashContent, k as createBundleCache, m as buildResolverOperationHookExpr, n as generatePluginFilesIfNeeded, o as getApplicationAuthNamespace, p as buildExecutorArgsExpr, q as isDefaultPlatform, r as loadApplication, s as WorkflowJobFunctionExecutionPolicySchema, t as defineApplication, v as stringifyFunction, x as resolveTSConfigWithFallback, y as createGeneratedEntryResolverPlugin } from "./application-BpLeawBg.mjs";
6
- import { c as stripTailorDBTypeBuilderHelpers, g as functionSchema, h as loadFilesWithIgnores, t as createExecutorService, u as TailorDBTypeSchema$1 } from "./service-goqlJJgA.mjs";
6
+ import { $ as resolveStaticWebsiteUrls, A as hasGenerationHooks, B as fetchPaged, C as resolveTSConfigWithFallback, G as getOrNull, H as fetchUserInfo, I as defaultPlatformBaseUrl, J as initOperatorClient, K as getPlatformBaseUrl, L as fetchAll, M as getDistDir, N as hashContent, O as createBundleLog, P as hashFile, Q as rememberPlatformConfigForToken, R as fetchAllTolerant, S as createTsconfigPathsPlugin, U as getConsoleBaseUrl, W as getOAuth2ClientId, X as isNotFoundError, Y as isDefaultPlatform, Z as normalizeBaseUrl, _ as assertUniqueTailorDBTypeNamesWithExternal, b as tailorPrincipalMap, d as HTTP_METHODS, et as byName, g as assertUniqueLocalTailorDBTypeNames, i as resolverBundleKey, j as createBundleCache, k as getPluginGenerationDependencies, m as buildResolverOperationHookExpr, n as generatePluginFilesIfNeeded, nt as LOG_LEVELS, o as getApplicationAuthNamespace, p as buildExecutorArgsExpr, q as initOAuth2Client, r as loadApplication, s as WorkflowJobFunctionExecutionPolicySchema, t as defineApplication, tt as createApplyLimiter, v as PRINCIPAL_VAR, w as platformBundleDefinePlugin, x as createGeneratedEntryResolverPlugin, y as stringifyFunction, z as fetchMachineUserToken } from "./application-F-nl107y.mjs";
7
+ import { _ as beginUserModuleRun, b as functionSchema, c as stripTailorDBTypeBuilderHelpers, g as IMPORT_NONCE_PARAM, h as loadFilesWithIgnores, t as createExecutorService, u as TailorDBTypeSchema$1, v as currentImportNonce } from "./service-C_WpbKHu.mjs";
7
8
  import { t as multiline } from "./multiline-sfHpTZZK.mjs";
8
9
  import { t as readPackageJson } from "./package-json-8b0O9TlX.mjs";
9
10
  import { t as userAgent } from "./user-agent-Bgsszb5I.mjs";
10
- import { i as isCLIError, n as formatCopyableCommand, t as createCLIError } from "./errors-CWj21238.mjs";
11
+ import { a as typeOnlyImportHint, i as isCLIError, n as formatCopyableCommand, t as createCLIError } from "./errors-BVb6vYGy.mjs";
11
12
  import { n as tightenSecretFilePermissions, r as writeSecretFile } from "./secret-file-C3J4Gy8r.mjs";
12
13
  import { r as withSpan } from "./telemetry-BRVdwh14.mjs";
13
14
  import { a as generateIdpTruncateScriptCode, i as generateIdpSeedScriptCode, o as processIdpUser, r as processLinesDb, t as SeedGeneratorID } from "./seed-xqM9WIHP.mjs";
@@ -1375,7 +1376,8 @@ async function loadConfig(configPath, options = {}) {
1375
1376
  const resolvedPath = path.resolve(process.cwd(), foundPath);
1376
1377
  if (!fs$2.existsSync(resolvedPath)) throw new Error(`Configuration file not found: ${configPath}`);
1377
1378
  const configUrl = pathToFileURL(resolvedPath);
1378
- if (options.importNonce) configUrl.searchParams.set("tailorImportNonce", options.importNonce);
1379
+ const importNonce = options.importNonce ?? currentImportNonce();
1380
+ if (importNonce) configUrl.searchParams.set(IMPORT_NONCE_PARAM, importNonce);
1379
1381
  const configModule = await import(configUrl.href);
1380
1382
  if (!configModule || !configModule.default) throw new Error("Invalid Tailor config module: default export not found");
1381
1383
  const validated = AppConfigSchema.safeParse(configModule.default);
@@ -2421,9 +2423,9 @@ function toComparableProtoJson(schema, value) {
2421
2423
  * @param value - Value to serialize
2422
2424
  * @returns Stable serialized string
2423
2425
  */
2424
- function stableStringify(value) {
2425
- if (Array.isArray(value)) return `[${value.map((item) => item === void 0 ? "null" : stableStringify(item)).join(",")}]`;
2426
- if (value && typeof value === "object") return `{${Object.entries(value).filter(([key, entryValue]) => key !== "$typeName" && entryValue !== void 0).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(",")}}`;
2426
+ function stableStringify$1(value) {
2427
+ if (Array.isArray(value)) return `[${value.map((item) => item === void 0 ? "null" : stableStringify$1(item)).join(",")}]`;
2428
+ if (value && typeof value === "object") return `{${Object.entries(value).filter(([key, entryValue]) => key !== "$typeName" && entryValue !== void 0).toSorted(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableStringify$1(entryValue)}`).join(",")}}`;
2427
2429
  if (typeof value === "bigint") return JSON.stringify(value.toString());
2428
2430
  return JSON.stringify(value);
2429
2431
  }
@@ -2434,7 +2436,7 @@ function stableStringify(value) {
2434
2436
  */
2435
2437
  function normalizeProtoConfig(value) {
2436
2438
  if (value === void 0 || value === null) return value;
2437
- return JSON.parse(stableStringify(value));
2439
+ return JSON.parse(stableStringify$1(value));
2438
2440
  }
2439
2441
  /**
2440
2442
  * Sort a string array for order-insensitive comparison.
@@ -2451,7 +2453,7 @@ function normalizeStringArray(values) {
2451
2453
  * @returns True when normalized values are equal
2452
2454
  */
2453
2455
  function areNormalizedEqual(left, right) {
2454
- return stableStringify(normalizeProtoConfig(left)) === stableStringify(normalizeProtoConfig(right));
2456
+ return stableStringify$1(normalizeProtoConfig(left)) === stableStringify$1(normalizeProtoConfig(right));
2455
2457
  }
2456
2458
 
2457
2459
  //#endregion
@@ -7435,13 +7437,17 @@ function buildValidateStatements(fields, accessExpr, keyPrefix) {
7435
7437
  }
7436
7438
  return statements;
7437
7439
  }
7440
+ function principalDeclIfReferenced(...exprs) {
7441
+ return exprs.some((expr) => expr?.includes("_principal")) ? ` const ${PRINCIPAL_VAR} = (${tailorPrincipalMap});` : "";
7442
+ }
7438
7443
  function wrapHook(objectExpr) {
7439
- return `((_invoker) => { const ${NOW} = new Date(); return ${objectExpr}; })(typeof _invoker !== "undefined" ? _invoker : undefined)`;
7444
+ return `((_invoker) => { const ${NOW} = new Date();${principalDeclIfReferenced(objectExpr)} return ${objectExpr}; })(typeof _invoker !== "undefined" ? _invoker : undefined)`;
7440
7445
  }
7441
7446
  function wrapValidate(statements, typeValidateExpr) {
7442
7447
  const issuesFn = typeValidateExpr ? " const __issues = (f, m) => { __errs[f] = m; };" : "";
7448
+ const principalDecl = principalDeclIfReferenced(typeValidateExpr, ...statements);
7443
7449
  const typeValidateStmt = typeValidateExpr ? ` ${typeValidateExpr};` : "";
7444
- return `((_invoker) => { const __errs = {};${issuesFn}\n${statements.join("\n")}${typeValidateStmt}\nreturn __errs; })(typeof _invoker !== "undefined" ? _invoker : undefined)`;
7450
+ return `((_invoker) => { const __errs = {};${issuesFn}${principalDecl}\n${statements.join("\n")}${typeValidateStmt}\nreturn __errs; })(typeof _invoker !== "undefined" ? _invoker : undefined)`;
7445
7451
  }
7446
7452
  /**
7447
7453
  * Aggregate every field's create/update hook, default, and validate into
@@ -7464,8 +7470,10 @@ function buildTypeScripts(fields, options) {
7464
7470
  const perFieldExpr = buildHookObject(fields, INPUT, OLD_RECORD, operation);
7465
7471
  const typeLevelExpr = typeHookExpr?.[operation];
7466
7472
  let expr;
7467
- if (perFieldExpr !== null && typeLevelExpr) expr = `((_invoker) => { const ${NOW} = new Date(); const __fl = ${perFieldExpr}; return Object.assign({}, __fl, ((${INPUT}) => ${typeLevelExpr})(Object.assign({}, ${INPUT}, __fl))); })(typeof _invoker !== "undefined" ? _invoker : undefined)`;
7468
- else if (typeLevelExpr) expr = wrapHook(typeLevelExpr);
7473
+ if (perFieldExpr !== null && typeLevelExpr) {
7474
+ const principalDecl = principalDeclIfReferenced(perFieldExpr, typeLevelExpr);
7475
+ expr = `((_invoker) => { const ${NOW} = new Date();${principalDecl} const __fl = ${perFieldExpr}; return Object.assign({}, __fl, ((${INPUT}) => ${typeLevelExpr})(Object.assign({}, ${INPUT}, __fl))); })(typeof _invoker !== "undefined" ? _invoker : undefined)`;
7476
+ } else if (typeLevelExpr) expr = wrapHook(typeLevelExpr);
7469
7477
  else if (perFieldExpr !== null) expr = wrapHook(perFieldExpr);
7470
7478
  if (expr) hook[operation] = { expr: expr + hashSuffix };
7471
7479
  }
@@ -7501,12 +7509,12 @@ function formatMigrationDiff(diff) {
7501
7509
  const lines = [];
7502
7510
  const changesByType = /* @__PURE__ */ new Map();
7503
7511
  for (const change of diff.changes) {
7504
- const existing = changesByType.get(change.typeName) ?? [];
7512
+ const existing = changesByType.get(change.tableName) ?? [];
7505
7513
  existing.push(change);
7506
- changesByType.set(change.typeName, existing);
7514
+ changesByType.set(change.tableName, existing);
7507
7515
  }
7508
- for (const [typeName, changes] of changesByType) {
7509
- lines.push(`${diff.namespace}.${typeName}:`);
7516
+ for (const [tableName, changes] of changesByType) {
7517
+ lines.push(`${diff.namespace}.${tableName}:`);
7510
7518
  for (const change of changes) lines.push(formatDiffChange(change));
7511
7519
  }
7512
7520
  return lines.join("\n");
@@ -7518,10 +7526,11 @@ function formatMigrationDiff(diff) {
7518
7526
  */
7519
7527
  function formatDiffChange(change) {
7520
7528
  switch (change.kind) {
7521
- case "type_added": return ` + [Type] ${change.typeName} (new type)`;
7522
- case "type_removed": return ` - [Type] ${change.typeName} (removed)`;
7523
- case "type_modified": return ` ~ [Type] ${change.typeName}: ${change.reason}`;
7524
- case "type_settings_modified": return ` ~ [Type Settings] ${change.typeName}: ${change.reason ?? "settings changed"}`;
7529
+ case "table_added": return ` + [Table] ${change.tableName} (new table)`;
7530
+ case "table_removed": return ` - [Table] ${change.tableName} (removed)`;
7531
+ case "table_renamed": return ` ~ [Table] ${change.previousTableName} ${change.tableName} (renamed)`;
7532
+ case "table_modified": return ` ~ [Table] ${change.tableName}: ${change.reason}`;
7533
+ case "table_settings_modified": return ` ~ [Table Settings] ${change.tableName}: ${change.reason ?? "settings changed"}`;
7525
7534
  case "field_added": {
7526
7535
  const typeStr = formatFieldType(change.after);
7527
7536
  return ` + ${change.fieldName}: ${typeStr}`;
@@ -7529,6 +7538,7 @@ function formatDiffChange(change) {
7529
7538
  case "field_removed": return ` - ${change.fieldName}: ${change.before.type}`;
7530
7539
  case "field_modified":
7531
7540
  case "field_type_modified": return ` ~ ${change.fieldName}: ${formatFieldModification(change.before, change.after)}`;
7541
+ case "field_renamed": return ` ~ ${change.previousFieldName} → ${change.fieldName}: ${formatFieldType(change.after)} (renamed)`;
7532
7542
  case "index_added": return ` + [Index] ${change.indexName}`;
7533
7543
  case "index_removed": return ` - [Index] ${change.indexName}`;
7534
7544
  case "index_modified": return ` ~ [Index] ${change.indexName}: ${change.reason ?? "modified"}`;
@@ -7539,10 +7549,10 @@ function formatDiffChange(change) {
7539
7549
  case "relationship_removed": return ` - [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}`;
7540
7550
  case "relationship_modified": return ` ~ [Relationship${change.relationshipType ? ` (${change.relationshipType})` : ""}] ${change.relationshipName}: ${change.reason ?? "modified"}`;
7541
7551
  case "permission_modified": return ` ~ [Permission] ${change.reason ?? "modified"}`;
7542
- case "type_scripts_modified": return ` ~ [Type Scripts] ${change.typeName}: ${change.reason ?? "type-level hooks/validate changed"}`;
7552
+ case "table_scripts_modified": return ` ~ [Table Scripts] ${change.tableName}: ${change.reason ?? "table-level hooks/validate changed"}`;
7543
7553
  default: {
7544
7554
  const unknown = change;
7545
- return ` ? ${unknown.typeName}.${unknown.fieldName ?? ""}`;
7555
+ return ` ? ${unknown.tableName}.${unknown.fieldName ?? ""}`;
7546
7556
  }
7547
7557
  }
7548
7558
  }
@@ -7598,7 +7608,7 @@ function formatBreakingChanges(breakingChanges) {
7598
7608
  if (breakingChanges.length === 0) return "";
7599
7609
  const lines = ["Breaking changes detected:", ""];
7600
7610
  for (const bc of breakingChanges) {
7601
- const location = bc.fieldName ? `${bc.typeName}.${bc.fieldName}` : bc.typeName;
7611
+ const location = bc.fieldName ? `${bc.tableName}.${bc.fieldName}` : bc.tableName;
7602
7612
  lines.push(` - ${location}: ${bc.reason}`);
7603
7613
  }
7604
7614
  return lines.join("\n");
@@ -7612,19 +7622,21 @@ function formatWarnings(warnings) {
7612
7622
  if (warnings.length === 0) return "";
7613
7623
  const lines = ["Warning: data loss possible:", ""];
7614
7624
  for (const w of warnings) {
7615
- const location = w.fieldName ? `${w.typeName}.${w.fieldName}` : w.typeName;
7625
+ const location = w.fieldName ? `${w.tableName}.${w.fieldName}` : w.tableName;
7616
7626
  lines.push(` - ${location}: ${w.reason}`);
7617
7627
  }
7618
7628
  return lines.join("\n");
7619
7629
  }
7620
7630
  const DIFF_CHANGE_LABELS = {
7621
- type_added: "type(s) added",
7622
- type_removed: "type(s) removed",
7623
- type_modified: "type(s) modified",
7624
- type_settings_modified: "type setting(s) modified",
7631
+ table_added: "table(s) added",
7632
+ table_removed: "table(s) removed",
7633
+ table_renamed: "table(s) renamed",
7634
+ table_modified: "table(s) modified",
7635
+ table_settings_modified: "table setting(s) modified",
7625
7636
  field_added: "field(s) added",
7626
7637
  field_removed: "field(s) removed",
7627
7638
  field_modified: "field(s) modified",
7639
+ field_renamed: "field(s) renamed",
7628
7640
  field_type_modified: "field type(s) modified",
7629
7641
  index_added: "index(es) added",
7630
7642
  index_removed: "index(es) removed",
@@ -7636,7 +7648,7 @@ const DIFF_CHANGE_LABELS = {
7636
7648
  relationship_removed: "relationship(s) removed",
7637
7649
  relationship_modified: "relationship(s) modified",
7638
7650
  permission_modified: "permission(s) modified",
7639
- type_scripts_modified: "type script(s) modified"
7651
+ table_scripts_modified: "table script(s) modified"
7640
7652
  };
7641
7653
  /**
7642
7654
  * Format a summary of the migration diff
@@ -7656,13 +7668,32 @@ function formatDiffSummary(diff) {
7656
7668
  * Scalar type changes whose complete source domain is accepted by the target
7657
7669
  * representation without relying on per-row index recreation.
7658
7670
  *
7671
+ * The whole source domain has to cast, not just the values present when the
7672
+ * script runs: the field keeps its previous type until the post-migration
7673
+ * phase, so an application can still write any value the source type allows
7674
+ * after the script has passed over the table. A pair that needs the script to
7675
+ * rewrite values first — `string` to `integer`, say, where `"abc"` never
7676
+ * casts — would fail that late write at the post-migration phase.
7677
+ *
7678
+ * Values must also read back with their meaning intact. Date, datetime, and
7679
+ * time values are not stored in the textual form they were written in, so
7680
+ * converting them to or from another type reads back as a different instant;
7681
+ * those pairs stay off this list even though the platform accepts the schema
7682
+ * change.
7683
+ *
7659
7684
  * Keep this list deliberately narrow. Pair-specific platform experiments can
7660
7685
  * extend it once both indexed and unindexed read paths have been verified.
7661
7686
  */
7662
7687
  const IN_PLACE_TYPE_CHANGES = /* @__PURE__ */ new Set([
7688
+ "boolean:string",
7689
+ "decimal:float",
7663
7690
  "decimal:string",
7664
7691
  "enum:string",
7692
+ "float:decimal",
7693
+ "float:string",
7694
+ "integer:decimal",
7665
7695
  "integer:float",
7696
+ "integer:string",
7666
7697
  "uuid:string"
7667
7698
  ]);
7668
7699
  /**
@@ -7678,7 +7709,448 @@ function supportsInPlaceFieldTypeChange(before, after) {
7678
7709
  if (before.serial || after.serial) return false;
7679
7710
  if (before.vector || after.vector) return false;
7680
7711
  if (before.foreignKey || after.foreignKey) return false;
7681
- return IN_PLACE_TYPE_CHANGES.has(`${before.type}:${after.type}`);
7712
+ if (!IN_PLACE_TYPE_CHANGES.has(`${before.type}:${after.type}`)) return false;
7713
+ return !(before.unique ?? false) || after.type !== "decimal" || before.type !== "float";
7714
+ }
7715
+ /**
7716
+ * Explain whether a field type change can be carried by a pair of migrations
7717
+ * that move values through a temporary field.
7718
+ *
7719
+ * The copy is a whole-value overwrite, so it can only carry a field whose value
7720
+ * stands on its own: a serial number belongs to a sequence the copy cannot
7721
+ * reproduce, a foreign key would dangle while both fields exist, a vector
7722
+ * belongs to an index built from it, and a nested value would need its members
7723
+ * converted individually. Arrays are excluded because collapsing one into a
7724
+ * single value has no answer the generated script could choose, and unique
7725
+ * fields because the duplicate-resolution scaffold the rename half would emit
7726
+ * only produces string values.
7727
+ * @param before - Previous field configuration
7728
+ * @param after - Target field configuration
7729
+ * @returns Eligibility and, when ineligible, the reason
7730
+ */
7731
+ function getExpandContractFieldChangeEligibility(before, after) {
7732
+ if (before.type === after.type) return {
7733
+ eligible: false,
7734
+ reason: "the field type did not change"
7735
+ };
7736
+ if (supportsInPlaceFieldTypeChange(before, after)) return {
7737
+ eligible: false,
7738
+ reason: `the ${before.type} to ${after.type} change is already supported in place`
7739
+ };
7740
+ if (before.unique || after.unique) return {
7741
+ eligible: false,
7742
+ reason: "the field is unique"
7743
+ };
7744
+ if (before.array || after.array) return {
7745
+ eligible: false,
7746
+ reason: "the field is an array"
7747
+ };
7748
+ if (before.type === "nested" || after.type === "nested") return {
7749
+ eligible: false,
7750
+ reason: "the field is nested"
7751
+ };
7752
+ if (before.serial || after.serial) return {
7753
+ eligible: false,
7754
+ reason: "the field uses a serial sequence"
7755
+ };
7756
+ if (before.vector || after.vector) return {
7757
+ eligible: false,
7758
+ reason: "the field is a vector"
7759
+ };
7760
+ if (before.foreignKey || after.foreignKey) return {
7761
+ eligible: false,
7762
+ reason: "the field is a foreign key"
7763
+ };
7764
+ return { eligible: true };
7765
+ }
7766
+
7767
+ //#endregion
7768
+ //#region src/cli/commands/tailordb/migrate/snapshot-types.ts
7769
+ /**
7770
+ * Optional boolean properties of {@link SnapshotFieldConfig}, absent meaning
7771
+ * `false`. Every comparator over snapshot fields iterates this single list so
7772
+ * a newly added property cannot be silently skipped by one of them; a
7773
+ * comparator that ignores some of these must declare its exclusions
7774
+ * explicitly.
7775
+ */
7776
+ const SNAPSHOT_FIELD_BOOLEAN_PROPS = [
7777
+ "array",
7778
+ "index",
7779
+ "unique",
7780
+ "foreignKey",
7781
+ "vector"
7782
+ ];
7783
+ /**
7784
+ * Type guard: is the operand a field-reference (object) operand?
7785
+ * @param {SnapshotPermissionOperand} operand - Operand to test
7786
+ * @returns {boolean} True if operand is a field-ref (not a value operand)
7787
+ */
7788
+ function isSnapshotFieldRefOperand(operand) {
7789
+ return typeof operand === "object" && operand !== null && !Array.isArray(operand);
7790
+ }
7791
+
7792
+ //#endregion
7793
+ //#region src/cli/commands/tailordb/migrate/rename-detection.ts
7794
+ /**
7795
+ * Field and type rename detection for TailorDB migrations
7796
+ *
7797
+ * A rename has no dedicated platform API, so the diff decomposes it into
7798
+ * `field_removed` + `field_added` (`table_removed` + `table_added` for types).
7799
+ * This module detects removed/added pairs that are compatible enough to be a
7800
+ * rename so that `migration generate` can ask the user (or accept
7801
+ * `--rename Type.old:new` / `--rename OldType:NewType`) and record a single
7802
+ * `field_renamed` / `table_renamed` change instead.
7803
+ */
7804
+ /** Modifiers a rename may change, since the Pre-phase relaxes them. */
7805
+ const RENAME_TOLERATED_BOOLEAN_PROPS = /* @__PURE__ */ new Set([
7806
+ "index",
7807
+ "unique",
7808
+ "vector"
7809
+ ]);
7810
+ /**
7811
+ * Whether copying values from `before` into `after` preserves their meaning,
7812
+ * i.e. the removed + added pair can be treated as a rename.
7813
+ *
7814
+ * Serial fields are excluded because their values are platform-generated and
7815
+ * cannot be written by a migration script.
7816
+ * @param {SnapshotFieldConfig} before - Removed field's configuration
7817
+ * @param {SnapshotFieldConfig} after - Added field's configuration
7818
+ * @returns {boolean} True if the pair is rename-compatible
7819
+ */
7820
+ function isRenameCompatible(before, after) {
7821
+ if (before.type !== after.type) return false;
7822
+ for (const prop of SNAPSHOT_FIELD_BOOLEAN_PROPS) {
7823
+ if (RENAME_TOLERATED_BOOLEAN_PROPS.has(prop)) continue;
7824
+ if ((before[prop] ?? false) !== (after[prop] ?? false)) return false;
7825
+ }
7826
+ if ((before.foreignKeyType ?? "") !== (after.foreignKeyType ?? "")) return false;
7827
+ if ((before.foreignKeyField ?? "") !== (after.foreignKeyField ?? "")) return false;
7828
+ if (before.serial || after.serial) return false;
7829
+ if (before.type === "enum") {
7830
+ const afterValues = new Set((after.allowedValues ?? []).map((v) => v.value));
7831
+ if ((before.allowedValues ?? []).some((v) => !afterValues.has(v.value))) return false;
7832
+ }
7833
+ const beforeNested = before.fields ?? {};
7834
+ const afterNested = after.fields ?? {};
7835
+ const beforeNames = Object.keys(beforeNested);
7836
+ const afterNames = Object.keys(afterNested);
7837
+ if (beforeNames.length !== afterNames.length) return false;
7838
+ for (const name of beforeNames) {
7839
+ const beforeMember = beforeNested[name];
7840
+ const afterMember = afterNested[name];
7841
+ if (!beforeMember || !afterMember) return false;
7842
+ if (beforeMember.required !== afterMember.required) return false;
7843
+ if (!isRenameCompatible(beforeMember, afterMember)) return false;
7844
+ }
7845
+ return true;
7846
+ }
7847
+ /**
7848
+ * Find removed + added field pairs in a diff that could be renames.
7849
+ * @param {MigrationDiff} diff - Migration diff to scan
7850
+ * @returns {FieldRenameCandidate[]} Candidates in diff order (may be empty)
7851
+ */
7852
+ function findRenameCandidates(diff) {
7853
+ const removedByType = /* @__PURE__ */ new Map();
7854
+ const addedByType = /* @__PURE__ */ new Map();
7855
+ for (const change of diff.changes) if (change.kind === "field_removed") {
7856
+ const list = removedByType.get(change.tableName) ?? [];
7857
+ list.push(change);
7858
+ removedByType.set(change.tableName, list);
7859
+ } else if (change.kind === "field_added") {
7860
+ const list = addedByType.get(change.tableName) ?? [];
7861
+ list.push(change);
7862
+ addedByType.set(change.tableName, list);
7863
+ }
7864
+ const candidates = [];
7865
+ for (const [tableName, removedChanges] of removedByType) {
7866
+ const addedChanges = addedByType.get(tableName);
7867
+ if (!addedChanges) continue;
7868
+ for (const removed of removedChanges) {
7869
+ const added = addedChanges.filter((a) => isRenameCompatible(removed.before, a.after));
7870
+ if (added.length > 0) candidates.push({
7871
+ tableName,
7872
+ removed,
7873
+ added
7874
+ });
7875
+ }
7876
+ }
7877
+ return candidates;
7878
+ }
7879
+ /**
7880
+ * Whether a rename spec matches a removed + added field pair between two
7881
+ * snapshots: the old field existed before and is gone now, and the new field
7882
+ * exists now but did not before. Field compatibility is checked separately
7883
+ * when the diff is recomputed with the spec.
7884
+ * @param {FieldRenameSpec} spec - Rename spec to test
7885
+ * @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot
7886
+ * @param {SchemaSnapshot} currentSnapshot - Current schema snapshot
7887
+ * @returns {boolean} True if the spec matches a removed + added pair
7888
+ */
7889
+ function renameSpecApplies(spec, previousSnapshot, currentSnapshot) {
7890
+ const prevFields = previousSnapshot.tables[spec.tableName]?.fields;
7891
+ const currFields = currentSnapshot.tables[spec.tableName]?.fields;
7892
+ return Boolean(prevFields?.[spec.previousFieldName] && !currFields?.[spec.previousFieldName] && currFields?.[spec.fieldName] && !prevFields[spec.fieldName]);
7893
+ }
7894
+ /**
7895
+ * Assert that every rename spec matches a compatible removed + added field
7896
+ * pair between the two normalized snapshots.
7897
+ * @param {NormalizedSchemaSnapshot} previous - Previous normalized snapshot
7898
+ * @param {NormalizedSchemaSnapshot} current - Current normalized snapshot
7899
+ * @param {readonly FieldRenameSpec[]} fieldRenames - Rename specs to validate
7900
+ */
7901
+ function assertValidFieldRenames(previous, current, fieldRenames) {
7902
+ const seen = /* @__PURE__ */ new Set();
7903
+ for (const rename of fieldRenames) {
7904
+ const { tableName, previousFieldName, fieldName } = rename;
7905
+ const label = `${tableName}.${previousFieldName}:${fieldName}`;
7906
+ for (const key of [`${tableName}.${previousFieldName}`, `${tableName}.${fieldName}`]) {
7907
+ if (seen.has(key)) throw new Error(`Field "${key}" appears in more than one rename.`);
7908
+ seen.add(key);
7909
+ }
7910
+ const prevType = previous.tables[tableName];
7911
+ const currType = current.tables[tableName];
7912
+ if (!prevType || !currType) throw new Error(`Cannot rename ${label}: type "${tableName}" must exist in both the previous and the current schema.`);
7913
+ const prevField = prevType.fields[previousFieldName];
7914
+ if (!prevField) throw new Error(`Cannot rename ${label}: field "${previousFieldName}" does not exist in the previous schema.`);
7915
+ if (currType.fields[previousFieldName]) throw new Error(`Cannot rename ${label}: field "${previousFieldName}" still exists in the current schema.`);
7916
+ const currField = currType.fields[fieldName];
7917
+ if (!currField) throw new Error(`Cannot rename ${label}: field "${fieldName}" does not exist in the current schema.`);
7918
+ if (prevType.fields[fieldName]) throw new Error(`Cannot rename ${label}: field "${fieldName}" already exists in the previous schema.`);
7919
+ if (!isRenameCompatible(prevField, currField)) throw new Error(`Cannot rename ${label}: the fields are not rename-compatible (the field type, array-ness, and foreign key target must match, enum values must not be removed, nested member names, requiredness, and types must match recursively, and serial fields cannot be renamed).`);
7920
+ }
7921
+ }
7922
+ const RENAME_OPTION_PATTERN = /^([^.:\s]+)\.([^.:\s]+):([^.:\s]+)$/;
7923
+ /**
7924
+ * Parse a `--rename` option value of the form `Type.old:new`.
7925
+ * @param {string} value - Raw option value
7926
+ * @returns {FieldRenameSpec} Parsed rename spec
7927
+ */
7928
+ function parseRenameOption(value) {
7929
+ const [, tableName, previousFieldName, fieldName] = value.match(RENAME_OPTION_PATTERN) ?? [];
7930
+ if (!tableName || !previousFieldName || !fieldName) throw new Error(`Invalid --rename value "${value}". Expected format: "Type.oldField:newField".`);
7931
+ if (previousFieldName === fieldName) throw new Error(`Invalid --rename value "${value}": old and new field names are identical.`);
7932
+ return {
7933
+ tableName,
7934
+ previousFieldName,
7935
+ fieldName
7936
+ };
7937
+ }
7938
+ const DROP_OPTION_PATTERN = /^([^.:\s]+)\.([^.:\s]+)$/;
7939
+ /**
7940
+ * Parse a `--drop` option value of the form `Type.field`.
7941
+ * @param {string} value - Raw option value
7942
+ * @returns {FieldDropSpec} Parsed drop spec
7943
+ */
7944
+ function parseDropOption(value) {
7945
+ const [, tableName, fieldName] = value.match(DROP_OPTION_PATTERN) ?? [];
7946
+ if (!tableName || !fieldName) throw new Error(`Invalid --drop value "${value}". Expected format: "Type.field".`);
7947
+ return {
7948
+ tableName,
7949
+ fieldName
7950
+ };
7951
+ }
7952
+ /**
7953
+ * Whether a drop spec matches a field that was removed between two snapshots:
7954
+ * the field existed before and is gone now.
7955
+ * @param {FieldDropSpec} spec - Drop spec to test
7956
+ * @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot
7957
+ * @param {SchemaSnapshot} currentSnapshot - Current schema snapshot
7958
+ * @returns {boolean} True if the spec matches a removed field
7959
+ */
7960
+ function dropSpecApplies(spec, previousSnapshot, currentSnapshot) {
7961
+ const prevFields = previousSnapshot.tables[spec.tableName]?.fields;
7962
+ const currFields = currentSnapshot.tables[spec.tableName]?.fields;
7963
+ return Boolean(prevFields?.[spec.fieldName] && !currFields?.[spec.fieldName]);
7964
+ }
7965
+ /**
7966
+ * Parse an `--expand-contract` option value of the form `Type.field`.
7967
+ * @param {string} value - Raw option value
7968
+ * @returns {FieldExpandContractSpec} Parsed spec
7969
+ */
7970
+ function parseExpandContractOption(value) {
7971
+ const [, tableName, fieldName] = value.match(DROP_OPTION_PATTERN) ?? [];
7972
+ if (!tableName || !fieldName) throw new Error(`Invalid --expand-contract value "${value}". Expected format: "Type.field".`);
7973
+ return {
7974
+ tableName,
7975
+ fieldName
7976
+ };
7977
+ }
7978
+ /**
7979
+ * Return a copy of a field config with foreign key references to the old type
7980
+ * name retargeted at the new name, so a self-referential type compares equal
7981
+ * to its renamed shape.
7982
+ * @param {SnapshotFieldConfig} field - Field configuration to retarget
7983
+ * @param {string} previousTableName - Type name before the rename
7984
+ * @param {string} tableName - Type name after the rename
7985
+ * @returns {SnapshotFieldConfig} Retargeted copy of the field
7986
+ */
7987
+ function retargetSelfReferences(field, previousTableName, tableName) {
7988
+ const nested = field.fields ? Object.fromEntries(Object.entries(field.fields).map(([name, member]) => [name, retargetSelfReferences(member, previousTableName, tableName)])) : void 0;
7989
+ return {
7990
+ ...field,
7991
+ ...field.foreignKeyType === previousTableName && { foreignKeyType: tableName },
7992
+ ...nested && { fields: nested }
7993
+ };
7994
+ }
7995
+ /**
7996
+ * JSON serialization with recursively sorted object keys, for deep equality.
7997
+ * @param {unknown} value - Value to serialize
7998
+ * @returns {string} Canonical JSON representation
7999
+ */
8000
+ function stableStringify(value) {
8001
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
8002
+ if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, v]) => v !== void 0).toSorted(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
8003
+ return JSON.stringify(value);
8004
+ }
8005
+ function isTypeRenameFieldCompatible(before, after) {
8006
+ if (!isRenameCompatible(before, after)) return false;
8007
+ if (before.required !== after.required) return false;
8008
+ if ((before.unique ?? false) !== (after.unique ?? false)) return false;
8009
+ if ((before.scale ?? null) !== (after.scale ?? null)) return false;
8010
+ for (const [name, beforeMember] of Object.entries(before.fields ?? {})) {
8011
+ const afterMember = after.fields?.[name];
8012
+ if (!afterMember || !isTypeRenameFieldCompatible(beforeMember, afterMember)) return false;
8013
+ }
8014
+ return true;
8015
+ }
8016
+ /**
8017
+ * Whether copying every row of `before` into `after` preserves the data, i.e.
8018
+ * the removed + added type pair can be treated as a rename.
8019
+ *
8020
+ * A renamed type is created with its full constraints in the Pre-phase (there
8021
+ * is no relaxation machinery for a fresh type), so the shape must match
8022
+ * strictly: same field names with the same type, array-ness, required/unique
8023
+ * constraints, foreign key target (self references compare against the new
8024
+ * name), and scale; enum values must not be removed; indexes must match.
8025
+ * Serial values cannot be written by a script and file contents are not
8026
+ * copied by SQL, so types with serial or file fields are never candidates.
8027
+ * Name-derived and data-independent surfaces (pluralForm, description,
8028
+ * settings, permissions, hooks, validations, relationships) may differ.
8029
+ * @param {TailorDBSnapshotType} before - Removed type's snapshot
8030
+ * @param {TailorDBSnapshotType} after - Added type's snapshot
8031
+ * @returns {boolean} True if the pair is rename-compatible
8032
+ */
8033
+ function isTypeRenameCompatible(before, after) {
8034
+ if (Object.keys(before.files ?? {}).length > 0 || Object.keys(after.files ?? {}).length > 0) return false;
8035
+ const beforeFieldNames = Object.keys(before.fields);
8036
+ const afterFieldNames = Object.keys(after.fields);
8037
+ if (beforeFieldNames.length !== afterFieldNames.length) return false;
8038
+ for (const fieldName of beforeFieldNames) {
8039
+ const beforeField = before.fields[fieldName];
8040
+ const afterField = after.fields[fieldName];
8041
+ if (!beforeField || !afterField) return false;
8042
+ if (beforeField.foreignKeyType === before.name && beforeField.required) return false;
8043
+ if (!isTypeRenameFieldCompatible(retargetSelfReferences(beforeField, before.name, after.name), afterField)) return false;
8044
+ }
8045
+ return stableStringify(before.indexes ?? {}) === stableStringify(after.indexes ?? {});
8046
+ }
8047
+ /**
8048
+ * Find removed + added type pairs in a diff that could be renames.
8049
+ * @param {MigrationDiff} diff - Migration diff to scan
8050
+ * @returns {TypeRenameCandidate[]} Candidates in diff order (may be empty)
8051
+ */
8052
+ function findTypeRenameCandidates(diff) {
8053
+ const removedChanges = diff.changes.filter((change) => change.kind === "table_removed");
8054
+ const addedChanges = diff.changes.filter((change) => change.kind === "table_added");
8055
+ const candidates = [];
8056
+ for (const removed of removedChanges) {
8057
+ const added = addedChanges.filter((a) => isTypeRenameCompatible(removed.before, a.after));
8058
+ if (added.length > 0) candidates.push({
8059
+ removed,
8060
+ added
8061
+ });
8062
+ }
8063
+ return candidates;
8064
+ }
8065
+ /**
8066
+ * Whether a type rename spec matches a removed + added type pair between two
8067
+ * snapshots: the old type existed before and is gone now, and the new type
8068
+ * exists now but did not before. Type compatibility is checked separately
8069
+ * when the diff is recomputed with the spec.
8070
+ * @param {TypeRenameSpec} spec - Rename spec to test
8071
+ * @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot
8072
+ * @param {SchemaSnapshot} currentSnapshot - Current schema snapshot
8073
+ * @returns {boolean} True if the spec matches a removed + added pair
8074
+ */
8075
+ function typeRenameSpecApplies(spec, previousSnapshot, currentSnapshot) {
8076
+ return Boolean(previousSnapshot.tables[spec.previousTableName] && !currentSnapshot.tables[spec.previousTableName] && currentSnapshot.tables[spec.tableName] && !previousSnapshot.tables[spec.tableName]);
8077
+ }
8078
+ /**
8079
+ * Assert that every type rename spec matches a compatible removed + added
8080
+ * type pair between the two normalized snapshots.
8081
+ * @param {NormalizedSchemaSnapshot} previous - Previous normalized snapshot
8082
+ * @param {NormalizedSchemaSnapshot} current - Current normalized snapshot
8083
+ * @param {readonly TypeRenameSpec[]} typeRenames - Rename specs to validate
8084
+ */
8085
+ function assertValidTypeRenames(previous, current, typeRenames) {
8086
+ const seen = /* @__PURE__ */ new Set();
8087
+ for (const rename of typeRenames) {
8088
+ const { previousTableName, tableName } = rename;
8089
+ const label = `${previousTableName}:${tableName}`;
8090
+ for (const key of [previousTableName, tableName]) {
8091
+ if (seen.has(key)) throw new Error(`Type "${key}" appears in more than one rename.`);
8092
+ seen.add(key);
8093
+ }
8094
+ const prevType = previous.tables[previousTableName];
8095
+ if (!prevType) throw new Error(`Cannot rename ${label}: type "${previousTableName}" does not exist in the previous schema.`);
8096
+ if (current.tables[previousTableName]) throw new Error(`Cannot rename ${label}: type "${previousTableName}" still exists in the current schema.`);
8097
+ const currType = current.tables[tableName];
8098
+ if (!currType) throw new Error(`Cannot rename ${label}: type "${tableName}" does not exist in the current schema.`);
8099
+ if (previous.tables[tableName]) throw new Error(`Cannot rename ${label}: type "${tableName}" already exists in the previous schema.`);
8100
+ if (!isTypeRenameCompatible(prevType, currType)) throw new Error(`Cannot rename ${label}: the types are not rename-compatible (every field must keep its name, type, array-ness, required/unique constraints, foreign key target, and scale, enum values must not be removed, indexes must match, self-referential foreign keys must be optional, and types with serial or file fields cannot be renamed).`);
8101
+ }
8102
+ }
8103
+ /**
8104
+ * Whether a field's foreign key target changed in a way that is not explained
8105
+ * by a confirmed type rename. Such a retarget is breaking (stored references
8106
+ * may become invalid) and needs a reference fixup script; a retarget that
8107
+ * follows a rename does not, because record ids are preserved by the copy —
8108
+ * provided the referenced field is unchanged, since the copy only guarantees
8109
+ * that the same ids exist under the new type name.
8110
+ * @param {SnapshotFieldConfig} before - Field configuration before the change
8111
+ * @param {SnapshotFieldConfig} after - Field configuration after the change
8112
+ * @param {ReadonlyMap<string, string>} [typeRenameTargets] - Confirmed type renames (old name → new name)
8113
+ * @returns {boolean} True if the retarget is breaking
8114
+ */
8115
+ function isBreakingForeignKeyRetarget(before, after, typeRenameTargets) {
8116
+ return Boolean(before.foreignKeyType && after.foreignKeyType && before.foreignKeyType !== after.foreignKeyType && (typeRenameTargets?.get(before.foreignKeyType) !== after.foreignKeyType || (before.foreignKeyField ?? "") !== (after.foreignKeyField ?? "")));
8117
+ }
8118
+ const TYPE_RENAME_OPTION_PATTERN = /^([^.:\s]+):([^.:\s]+)$/;
8119
+ /**
8120
+ * Parse a `--rename` option value of the form `OldType:NewType`.
8121
+ * @param {string} value - Raw option value
8122
+ * @returns {TypeRenameSpec} Parsed rename spec
8123
+ */
8124
+ function parseTypeRenameOption(value) {
8125
+ const [, previousTableName, tableName] = value.match(TYPE_RENAME_OPTION_PATTERN) ?? [];
8126
+ if (!previousTableName || !tableName) throw new Error(`Invalid --rename value "${value}". Expected format: "Type.oldField:newField" or "OldType:NewType".`);
8127
+ if (previousTableName === tableName) throw new Error(`Invalid --rename value "${value}": old and new type names are identical.`);
8128
+ return {
8129
+ previousTableName,
8130
+ tableName
8131
+ };
8132
+ }
8133
+ const TYPE_DROP_OPTION_PATTERN = /^([^.:\s]+)$/;
8134
+ /**
8135
+ * Parse a `--drop` option value of the form `Type`.
8136
+ * @param {string} value - Raw option value
8137
+ * @returns {TypeDropSpec} Parsed drop spec
8138
+ */
8139
+ function parseTypeDropOption(value) {
8140
+ const [, tableName] = value.match(TYPE_DROP_OPTION_PATTERN) ?? [];
8141
+ if (!tableName) throw new Error(`Invalid --drop value "${value}". Expected format: "Type.field" or "Type".`);
8142
+ return { tableName };
8143
+ }
8144
+ /**
8145
+ * Whether a type drop spec matches a type that was removed between two
8146
+ * snapshots: the type existed before and is gone now.
8147
+ * @param {TypeDropSpec} spec - Drop spec to test
8148
+ * @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot
8149
+ * @param {SchemaSnapshot} currentSnapshot - Current schema snapshot
8150
+ * @returns {boolean} True if the spec matches a removed type
8151
+ */
8152
+ function typeDropSpecApplies(spec, previousSnapshot, currentSnapshot) {
8153
+ return Boolean(previousSnapshot.tables[spec.tableName] && !currentSnapshot.tables[spec.tableName]);
7682
8154
  }
7683
8155
 
7684
8156
  //#endregion
@@ -7919,7 +8391,7 @@ const schemaSnapshotSchema = z.looseObject({
7919
8391
  version: z.number(),
7920
8392
  namespace: z.string(),
7921
8393
  createdAt: z.string(),
7922
- types: snapshotRecordSchema(tailorDBSnapshotTypeSchema),
8394
+ tables: snapshotRecordSchema(tailorDBSnapshotTypeSchema),
7923
8395
  rebaseline: rebaselineMarkerSchema.optional()
7924
8396
  });
7925
8397
  const typeSettingsPatchSchema = z.looseObject({
@@ -7946,56 +8418,73 @@ const snapshotPermissionStateSchema = z.looseObject({
7946
8418
  gqlPermission: snapshotGqlPermissionSchema.optional()
7947
8419
  });
7948
8420
  const typeAddedChangeSchema = z.looseObject({
7949
- kind: z.literal("type_added"),
7950
- typeName: z.string(),
8421
+ kind: z.literal("table_added"),
8422
+ tableName: z.string(),
7951
8423
  reason: z.string().optional(),
7952
8424
  after: tailorDBSnapshotTypeSchema
7953
8425
  });
7954
8426
  const typeRemovedChangeSchema = z.looseObject({
7955
- kind: z.literal("type_removed"),
7956
- typeName: z.string(),
8427
+ kind: z.literal("table_removed"),
8428
+ tableName: z.string(),
7957
8429
  reason: z.string().optional(),
7958
8430
  before: tailorDBSnapshotTypeSchema
7959
8431
  });
8432
+ const typeRenamedChangeSchema = z.looseObject({
8433
+ kind: z.literal("table_renamed"),
8434
+ tableName: z.string(),
8435
+ reason: z.string().optional(),
8436
+ previousTableName: z.string(),
8437
+ before: tailorDBSnapshotTypeSchema,
8438
+ after: tailorDBSnapshotTypeSchema
8439
+ });
7960
8440
  const typeModifiedChangeSchema = z.looseObject({
7961
- kind: z.literal("type_modified"),
7962
- typeName: z.string(),
8441
+ kind: z.literal("table_modified"),
8442
+ tableName: z.string(),
7963
8443
  reason: z.string().optional(),
7964
8444
  before: typeSettingsPatchSchema.optional(),
7965
8445
  after: typeSettingsPatchSchema.optional()
7966
8446
  });
7967
8447
  const typeSettingsModifiedChangeSchema = z.looseObject({
7968
- kind: z.literal("type_settings_modified"),
7969
- typeName: z.string(),
8448
+ kind: z.literal("table_settings_modified"),
8449
+ tableName: z.string(),
7970
8450
  reason: z.string().optional(),
7971
8451
  before: snapshotTypeSettingsStateSchema,
7972
8452
  after: snapshotTypeSettingsStateSchema
7973
8453
  });
7974
8454
  const fieldAddedChangeSchema = z.looseObject({
7975
8455
  kind: z.literal("field_added"),
7976
- typeName: z.string(),
8456
+ tableName: z.string(),
7977
8457
  reason: z.string().optional(),
7978
8458
  fieldName: z.string(),
7979
8459
  after: snapshotFieldConfigSchema
7980
8460
  });
7981
8461
  const fieldRemovedChangeSchema = z.looseObject({
7982
8462
  kind: z.literal("field_removed"),
7983
- typeName: z.string(),
8463
+ tableName: z.string(),
7984
8464
  reason: z.string().optional(),
7985
8465
  fieldName: z.string(),
7986
8466
  before: snapshotFieldConfigSchema
7987
8467
  });
7988
8468
  const fieldModifiedChangeSchema = z.looseObject({
7989
8469
  kind: z.literal("field_modified"),
7990
- typeName: z.string(),
8470
+ tableName: z.string(),
8471
+ reason: z.string().optional(),
8472
+ fieldName: z.string(),
8473
+ before: snapshotFieldConfigSchema,
8474
+ after: snapshotFieldConfigSchema
8475
+ });
8476
+ const fieldRenamedChangeSchema = z.looseObject({
8477
+ kind: z.literal("field_renamed"),
8478
+ tableName: z.string(),
7991
8479
  reason: z.string().optional(),
7992
8480
  fieldName: z.string(),
8481
+ previousFieldName: z.string(),
7993
8482
  before: snapshotFieldConfigSchema,
7994
8483
  after: snapshotFieldConfigSchema
7995
8484
  });
7996
8485
  const fieldTypeModifiedChangeSchema = z.looseObject({
7997
8486
  kind: z.literal("field_type_modified"),
7998
- typeName: z.string(),
8487
+ tableName: z.string(),
7999
8488
  reason: z.string().optional(),
8000
8489
  fieldName: z.string(),
8001
8490
  before: snapshotFieldConfigSchema,
@@ -8003,21 +8492,21 @@ const fieldTypeModifiedChangeSchema = z.looseObject({
8003
8492
  });
8004
8493
  const indexAddedChangeSchema = z.looseObject({
8005
8494
  kind: z.literal("index_added"),
8006
- typeName: z.string(),
8495
+ tableName: z.string(),
8007
8496
  reason: z.string().optional(),
8008
8497
  indexName: z.string(),
8009
8498
  after: snapshotIndexConfigSchema
8010
8499
  });
8011
8500
  const indexRemovedChangeSchema = z.looseObject({
8012
8501
  kind: z.literal("index_removed"),
8013
- typeName: z.string(),
8502
+ tableName: z.string(),
8014
8503
  reason: z.string().optional(),
8015
8504
  indexName: z.string(),
8016
8505
  before: snapshotIndexConfigSchema
8017
8506
  });
8018
8507
  const indexModifiedChangeSchema = z.looseObject({
8019
8508
  kind: z.literal("index_modified"),
8020
- typeName: z.string(),
8509
+ tableName: z.string(),
8021
8510
  reason: z.string().optional(),
8022
8511
  indexName: z.string(),
8023
8512
  before: snapshotIndexConfigSchema,
@@ -8025,21 +8514,21 @@ const indexModifiedChangeSchema = z.looseObject({
8025
8514
  });
8026
8515
  const fileAddedChangeSchema = z.looseObject({
8027
8516
  kind: z.literal("file_added"),
8028
- typeName: z.string(),
8517
+ tableName: z.string(),
8029
8518
  reason: z.string().optional(),
8030
8519
  fieldName: z.string(),
8031
8520
  after: z.string()
8032
8521
  });
8033
8522
  const fileRemovedChangeSchema = z.looseObject({
8034
8523
  kind: z.literal("file_removed"),
8035
- typeName: z.string(),
8524
+ tableName: z.string(),
8036
8525
  reason: z.string().optional(),
8037
8526
  fieldName: z.string(),
8038
8527
  before: z.string()
8039
8528
  });
8040
8529
  const fileModifiedChangeSchema = z.looseObject({
8041
8530
  kind: z.literal("file_modified"),
8042
- typeName: z.string(),
8531
+ tableName: z.string(),
8043
8532
  reason: z.string().optional(),
8044
8533
  fieldName: z.string(),
8045
8534
  before: z.string(),
@@ -8047,7 +8536,7 @@ const fileModifiedChangeSchema = z.looseObject({
8047
8536
  });
8048
8537
  const relationshipAddedChangeSchema = z.looseObject({
8049
8538
  kind: z.literal("relationship_added"),
8050
- typeName: z.string(),
8539
+ tableName: z.string(),
8051
8540
  reason: z.string().optional(),
8052
8541
  relationshipName: z.string(),
8053
8542
  relationshipType: z.enum(["forward", "backward"]).optional(),
@@ -8055,7 +8544,7 @@ const relationshipAddedChangeSchema = z.looseObject({
8055
8544
  });
8056
8545
  const relationshipRemovedChangeSchema = z.looseObject({
8057
8546
  kind: z.literal("relationship_removed"),
8058
- typeName: z.string(),
8547
+ tableName: z.string(),
8059
8548
  reason: z.string().optional(),
8060
8549
  relationshipName: z.string(),
8061
8550
  relationshipType: z.enum(["forward", "backward"]).optional(),
@@ -8063,7 +8552,7 @@ const relationshipRemovedChangeSchema = z.looseObject({
8063
8552
  });
8064
8553
  const relationshipModifiedChangeSchema = z.looseObject({
8065
8554
  kind: z.literal("relationship_modified"),
8066
- typeName: z.string(),
8555
+ tableName: z.string(),
8067
8556
  reason: z.string().optional(),
8068
8557
  relationshipName: z.string(),
8069
8558
  relationshipType: z.enum(["forward", "backward"]).optional(),
@@ -8072,7 +8561,7 @@ const relationshipModifiedChangeSchema = z.looseObject({
8072
8561
  });
8073
8562
  const permissionModifiedChangeSchema = z.looseObject({
8074
8563
  kind: z.literal("permission_modified"),
8075
- typeName: z.string(),
8564
+ tableName: z.string(),
8076
8565
  reason: z.string().optional(),
8077
8566
  before: snapshotPermissionStateSchema.optional(),
8078
8567
  after: snapshotPermissionStateSchema.optional()
@@ -8085,8 +8574,8 @@ const typeScriptsStateSchema = z.looseObject({
8085
8574
  typeValidateExpr: z.string().optional()
8086
8575
  });
8087
8576
  const typeScriptsModifiedChangeSchema = z.looseObject({
8088
- kind: z.literal("type_scripts_modified"),
8089
- typeName: z.string(),
8577
+ kind: z.literal("table_scripts_modified"),
8578
+ tableName: z.string(),
8090
8579
  reason: z.string().optional(),
8091
8580
  before: typeScriptsStateSchema,
8092
8581
  after: typeScriptsStateSchema
@@ -8094,11 +8583,13 @@ const typeScriptsModifiedChangeSchema = z.looseObject({
8094
8583
  const diffChangeSchema = z.discriminatedUnion("kind", [
8095
8584
  typeAddedChangeSchema,
8096
8585
  typeRemovedChangeSchema,
8586
+ typeRenamedChangeSchema,
8097
8587
  typeModifiedChangeSchema,
8098
8588
  typeSettingsModifiedChangeSchema,
8099
8589
  fieldAddedChangeSchema,
8100
8590
  fieldRemovedChangeSchema,
8101
8591
  fieldModifiedChangeSchema,
8592
+ fieldRenamedChangeSchema,
8102
8593
  fieldTypeModifiedChangeSchema,
8103
8594
  indexAddedChangeSchema,
8104
8595
  indexRemovedChangeSchema,
@@ -8113,14 +8604,14 @@ const diffChangeSchema = z.discriminatedUnion("kind", [
8113
8604
  typeScriptsModifiedChangeSchema
8114
8605
  ]);
8115
8606
  const breakingChangeInfoSchema = z.looseObject({
8116
- typeName: z.string(),
8607
+ tableName: z.string(),
8117
8608
  fieldName: z.string().optional(),
8118
8609
  reason: z.string(),
8119
8610
  unsupported: z.boolean().optional(),
8120
8611
  showThreeStepHint: z.boolean().optional()
8121
8612
  });
8122
8613
  const warningChangeInfoSchema = z.looseObject({
8123
- typeName: z.string(),
8614
+ tableName: z.string(),
8124
8615
  fieldName: z.string().optional(),
8125
8616
  reason: z.string()
8126
8617
  });
@@ -8142,17 +8633,6 @@ const migrationDiffSchema = z.looseObject({
8142
8633
  scriptSkipped: scriptSkippedInfoSchema.optional()
8143
8634
  });
8144
8635
 
8145
- //#endregion
8146
- //#region src/cli/commands/tailordb/migrate/snapshot-types.ts
8147
- /**
8148
- * Type guard: is the operand a field-reference (object) operand?
8149
- * @param {SnapshotPermissionOperand} operand - Operand to test
8150
- * @returns {boolean} True if operand is a field-ref (not a value operand)
8151
- */
8152
- function isSnapshotFieldRefOperand(operand) {
8153
- return typeof operand === "object" && operand !== null && !Array.isArray(operand);
8154
- }
8155
-
8156
8636
  //#endregion
8157
8637
  //#region src/cli/commands/tailordb/migrate/snapshot.ts
8158
8638
  /**
@@ -8179,6 +8659,8 @@ const DB_TYPES_FILE_NAME = "db.ts";
8179
8659
  * Examples: 0001, 0002, 0003, ...
8180
8660
  */
8181
8661
  const MIGRATION_NUMBER_PATTERN = /^\d{4}$/;
8662
+ /** Highest migration number the four-digit directory name can hold. */
8663
+ const MAX_MIGRATION_NUMBER = 9999;
8182
8664
  /**
8183
8665
  * Platform default scale for decimal fields when scale is not explicitly specified.
8184
8666
  * Must stay in sync with the platform's default decimal scale.
@@ -8189,14 +8671,111 @@ function assertSupportedMigrationFileVersion(filePath, raw) {
8189
8671
  if (typeof raw !== "object" || raw === null || !("version" in raw)) return;
8190
8672
  const version = raw.version;
8191
8673
  if (typeof version !== "number") return;
8192
- if (Number.isInteger(version) && version >= 1 && version <= 2) return;
8193
- const supportedRange = `${1}-${2}`;
8674
+ if (Number.isInteger(version) && version >= 1 && version <= 5) return;
8675
+ const supportedRange = `${1}-${5}`;
8194
8676
  let guidance;
8195
- if (version > 2) guidance = `Upgrade to an SDK that supports migration file format version ${version}.`;
8677
+ if (version > 5) guidance = `Upgrade to an SDK that supports migration file format version ${version}.`;
8196
8678
  else if (version < 1) guidance = "Re-baseline with an SDK that still supports this migration history, then upgrade the SDK.";
8197
8679
  else guidance = "Restore the migration file from version control or regenerate it.";
8198
8680
  throw new UnsupportedMigrationFileVersionError(`Unsupported migration file format version ${version} at ${filePath}. This SDK supports migration file format versions ${supportedRange}. ${guidance}`);
8199
8681
  }
8682
+ /**
8683
+ * Diff change kinds renamed when TailorDB table terminology replaced "type".
8684
+ * Migration histories written before the rename persist the old names, so
8685
+ * diff.json files keep being read through this mapping.
8686
+ */
8687
+ const LEGACY_CHANGE_KINDS = /* @__PURE__ */ new Map([
8688
+ ["type_added", "table_added"],
8689
+ ["type_removed", "table_removed"],
8690
+ ["type_renamed", "table_renamed"],
8691
+ ["type_modified", "table_modified"],
8692
+ ["type_settings_modified", "table_settings_modified"],
8693
+ ["type_scripts_modified", "table_scripts_modified"]
8694
+ ]);
8695
+ /**
8696
+ * Rewrite pre-rename `type_*` change kinds to their current `table_*` names
8697
+ * so that the diff schema, which only knows the current names, accepts them.
8698
+ * @param {unknown} raw - Parsed diff.json contents
8699
+ * @returns {unknown} Diff contents with legacy change kinds rewritten
8700
+ */
8701
+ function normalizeLegacyChangeKinds(raw) {
8702
+ if (typeof raw !== "object" || raw === null || !("changes" in raw)) return raw;
8703
+ const changes = raw.changes;
8704
+ if (!Array.isArray(changes)) return raw;
8705
+ const normalized = changes.map((change) => {
8706
+ if (typeof change !== "object" || change === null || !("kind" in change)) return change;
8707
+ const kind = change.kind;
8708
+ if (typeof kind !== "string") return change;
8709
+ const currentKind = LEGACY_CHANGE_KINDS.get(kind);
8710
+ return currentKind === void 0 ? change : {
8711
+ ...change,
8712
+ kind: currentKind
8713
+ };
8714
+ });
8715
+ return {
8716
+ ...raw,
8717
+ changes: normalized
8718
+ };
8719
+ }
8720
+ /**
8721
+ * Persisted field names renamed when TailorDB table terminology replaced
8722
+ * "type". Applied to every entry that carries a table name, so diff.json files
8723
+ * written before the rename keep validating against the current schema.
8724
+ */
8725
+ const LEGACY_ENTRY_FIELDS = /* @__PURE__ */ new Map([["typeName", "tableName"], ["previousTypeName", "previousTableName"]]);
8726
+ function renameLegacyEntryFields(entry) {
8727
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return entry;
8728
+ const source = entry;
8729
+ const renamed = {};
8730
+ let changed = false;
8731
+ for (const [key, value] of Object.entries(source)) {
8732
+ const currentKey = LEGACY_ENTRY_FIELDS.get(key);
8733
+ if (currentKey !== void 0 && !(currentKey in source)) {
8734
+ renamed[currentKey] = value;
8735
+ changed = true;
8736
+ } else renamed[key] = value;
8737
+ }
8738
+ return changed ? renamed : entry;
8739
+ }
8740
+ /** Persisted arrays whose entries carry a table name. */
8741
+ const LEGACY_FIELD_CARRIERS = [
8742
+ "changes",
8743
+ "breakingChanges",
8744
+ "warnings"
8745
+ ];
8746
+ /**
8747
+ * Move a pre-rename `types` record to `tables` so schema.json files written
8748
+ * before the rename keep validating against the current schema.
8749
+ * @param {unknown} raw - Parsed schema.json contents
8750
+ * @returns {unknown} Snapshot contents with the legacy key moved
8751
+ */
8752
+ function normalizeLegacyTablesKey(raw) {
8753
+ if (typeof raw !== "object" || raw === null) return raw;
8754
+ const source = raw;
8755
+ if (!("types" in source) || "tables" in source) return raw;
8756
+ const { types, ...rest } = source;
8757
+ return {
8758
+ ...rest,
8759
+ tables: types
8760
+ };
8761
+ }
8762
+ /**
8763
+ * Rewrite the pre-rename `typeName` / `previousTypeName` keys to their current
8764
+ * names across every persisted position that carries a table name.
8765
+ * @param {unknown} raw - Parsed diff.json contents
8766
+ * @returns {unknown} Diff contents with legacy field names rewritten
8767
+ */
8768
+ function normalizeLegacyFieldNames(raw) {
8769
+ if (typeof raw !== "object" || raw === null) return raw;
8770
+ const source = raw;
8771
+ const normalized = { ...source };
8772
+ for (const key of LEGACY_FIELD_CARRIERS) {
8773
+ const entries = source[key];
8774
+ if (!Array.isArray(entries)) continue;
8775
+ normalized[key] = entries.map(renameLegacyEntryFields);
8776
+ }
8777
+ return normalized;
8778
+ }
8200
8779
  function createSnapshotRecord() {
8201
8780
  return Object.create(null);
8202
8781
  }
@@ -8250,11 +8829,11 @@ function normalizeSnapshotType(type) {
8250
8829
  * @returns {NormalizedSchemaSnapshot} A new schema snapshot object branded as normalized
8251
8830
  */
8252
8831
  function normalizeSchemaSnapshot(snapshot) {
8253
- const types = createSnapshotRecord();
8254
- for (const [typeName, type] of Object.entries(snapshot.types)) types[typeName] = normalizeSnapshotType(type);
8832
+ const tables = createSnapshotRecord();
8833
+ for (const [tableName, type] of Object.entries(snapshot.tables)) tables[tableName] = normalizeSnapshotType(type);
8255
8834
  return {
8256
8835
  ...snapshot,
8257
- types
8836
+ tables
8258
8837
  };
8259
8838
  }
8260
8839
  /**
@@ -8436,12 +9015,12 @@ function convertActionPermission(permission) {
8436
9015
  */
8437
9016
  function createSnapshotFromLocalTypes(types, namespace) {
8438
9017
  const snapshotTypes = createSnapshotRecord();
8439
- for (const [typeName, type] of Object.entries(types)) snapshotTypes[typeName] = createSnapshotType(type);
9018
+ for (const [tableName, type] of Object.entries(types)) snapshotTypes[tableName] = createSnapshotType(type);
8440
9019
  return normalizeSchemaSnapshot({
8441
- version: 2,
9020
+ version: 5,
8442
9021
  namespace,
8443
9022
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8444
- types: snapshotTypes
9023
+ tables: snapshotTypes
8445
9024
  });
8446
9025
  }
8447
9026
  /**
@@ -8458,7 +9037,7 @@ function loadSnapshot(filePath) {
8458
9037
  throw new Error(`Invalid schema snapshot at ${filePath}: ${String(error)}`, { cause: error });
8459
9038
  }
8460
9039
  assertSupportedMigrationFileVersion(filePath, raw);
8461
- const result = schemaSnapshotSchema.safeParse(raw);
9040
+ const result = schemaSnapshotSchema.safeParse(normalizeLegacyTablesKey(raw));
8462
9041
  if (!result.success) throw new Error(`Invalid schema snapshot at ${filePath}: ${z.prettifyError(result.error)}`, { cause: result.error });
8463
9042
  const snapshot = result.data;
8464
9043
  return normalizeSchemaSnapshot(snapshot);
@@ -8477,7 +9056,7 @@ function loadDiff(filePath) {
8477
9056
  throw new Error(`Invalid migration diff at ${filePath}: ${String(error)}`, { cause: error });
8478
9057
  }
8479
9058
  assertSupportedMigrationFileVersion(filePath, raw);
8480
- const result = migrationDiffSchema.safeParse(raw);
9059
+ const result = migrationDiffSchema.safeParse(normalizeLegacyFieldNames(normalizeLegacyChangeKinds(raw)));
8481
9060
  if (!result.success) throw new Error(`Invalid migration diff at ${filePath}: ${z.prettifyError(result.error)}`, { cause: result.error });
8482
9061
  const parsed = result.data;
8483
9062
  const warnings = parsed.warnings ?? deriveWarningsFromChanges(parsed);
@@ -8488,7 +9067,7 @@ function loadDiff(filePath) {
8488
9067
  };
8489
9068
  }
8490
9069
  const FIELD_REMOVED_WARNING_REASON = "Field removed (existing data will no longer be accessible through the schema)";
8491
- const TYPE_REMOVED_WARNING_REASON = "Type removed (all records of this type will be deleted during post-migration cleanup)";
9070
+ const TABLE_REMOVED_WARNING_REASON = "Type removed (all records of this type will be deleted during post-migration cleanup)";
8492
9071
  /**
8493
9072
  * Reconstruct data-loss warnings from removal changes for diff.json files
8494
9073
  * written before the warning tier existed
@@ -8498,13 +9077,13 @@ const TYPE_REMOVED_WARNING_REASON = "Type removed (all records of this type will
8498
9077
  function deriveWarningsFromChanges(diff) {
8499
9078
  const warnings = [];
8500
9079
  for (const change of diff.changes) if (change.kind === "field_removed") warnings.push({
8501
- typeName: change.typeName,
9080
+ tableName: change.tableName,
8502
9081
  fieldName: change.fieldName,
8503
9082
  reason: FIELD_REMOVED_WARNING_REASON
8504
9083
  });
8505
- else if (change.kind === "type_removed") warnings.push({
8506
- typeName: change.typeName,
8507
- reason: TYPE_REMOVED_WARNING_REASON
9084
+ else if (change.kind === "table_removed") warnings.push({
9085
+ tableName: change.tableName,
9086
+ reason: TABLE_REMOVED_WARNING_REASON
8508
9087
  });
8509
9088
  return warnings;
8510
9089
  }
@@ -8555,19 +9134,19 @@ function getNextMigrationNumber(migrationsDir) {
8555
9134
  * @returns {NormalizedSchemaSnapshot} Normalized snapshot after applying diff
8556
9135
  */
8557
9136
  function applyDiffToSnapshot(snapshot, diff) {
8558
- const types = copySnapshotRecord(snapshot.types);
9137
+ const tables = copySnapshotRecord(snapshot.tables);
8559
9138
  for (const change of diff.changes) switch (change.kind) {
8560
- case "type_added":
8561
- types[change.typeName] = change.after;
9139
+ case "table_added":
9140
+ tables[change.tableName] = change.after;
8562
9141
  break;
8563
- case "type_removed":
8564
- delete types[change.typeName];
9142
+ case "table_removed":
9143
+ delete tables[change.tableName];
8565
9144
  break;
8566
- case "type_modified": {
8567
- const existing = types[change.typeName];
9145
+ case "table_modified": {
9146
+ const existing = tables[change.tableName];
8568
9147
  if (existing && change.after) {
8569
9148
  const after = change.after;
8570
- types[change.typeName] = {
9149
+ tables[change.tableName] = {
8571
9150
  ...existing,
8572
9151
  ...after.indexes !== void 0 && { indexes: after.indexes },
8573
9152
  ...after.files !== void 0 && { files: after.files }
@@ -8575,9 +9154,9 @@ function applyDiffToSnapshot(snapshot, diff) {
8575
9154
  }
8576
9155
  break;
8577
9156
  }
8578
- case "type_settings_modified": {
8579
- const existing = types[change.typeName];
8580
- if (existing) types[change.typeName] = {
9157
+ case "table_settings_modified": {
9158
+ const existing = tables[change.tableName];
9159
+ if (existing) tables[change.tableName] = {
8581
9160
  ...existing,
8582
9161
  description: change.after.description,
8583
9162
  pluralForm: change.after.pluralForm,
@@ -8585,11 +9164,11 @@ function applyDiffToSnapshot(snapshot, diff) {
8585
9164
  };
8586
9165
  break;
8587
9166
  }
8588
- case "type_scripts_modified": {
8589
- const existing = types[change.typeName];
9167
+ case "table_scripts_modified": {
9168
+ const existing = tables[change.tableName];
8590
9169
  if (existing) {
8591
9170
  const { typeHookExpr: _, typeValidateExpr: __, ...rest } = existing;
8592
- types[change.typeName] = {
9171
+ tables[change.tableName] = {
8593
9172
  ...rest,
8594
9173
  ...change.after.typeHookExpr && { typeHookExpr: change.after.typeHookExpr },
8595
9174
  ...change.after.typeValidateExpr !== void 0 && { typeValidateExpr: change.after.typeValidateExpr }
@@ -8600,11 +9179,11 @@ function applyDiffToSnapshot(snapshot, diff) {
8600
9179
  case "field_added":
8601
9180
  case "field_modified":
8602
9181
  case "field_type_modified": {
8603
- const existing = types[change.typeName];
9182
+ const existing = tables[change.tableName];
8604
9183
  if (existing) {
8605
9184
  const fields = copySnapshotRecord(existing.fields);
8606
9185
  fields[change.fieldName] = change.after;
8607
- types[change.typeName] = {
9186
+ tables[change.tableName] = {
8608
9187
  ...existing,
8609
9188
  fields
8610
9189
  };
@@ -8612,24 +9191,41 @@ function applyDiffToSnapshot(snapshot, diff) {
8612
9191
  break;
8613
9192
  }
8614
9193
  case "field_removed": {
8615
- const existing = types[change.typeName];
9194
+ const existing = tables[change.tableName];
8616
9195
  if (existing) {
8617
9196
  const remainingFields = copySnapshotRecord(existing.fields);
8618
9197
  delete remainingFields[change.fieldName];
8619
- types[change.typeName] = {
9198
+ tables[change.tableName] = {
8620
9199
  ...existing,
8621
9200
  fields: remainingFields
8622
9201
  };
8623
9202
  }
8624
9203
  break;
8625
9204
  }
9205
+ case "field_renamed": {
9206
+ const existing = tables[change.tableName];
9207
+ if (existing) {
9208
+ const fields = copySnapshotRecord(existing.fields);
9209
+ delete fields[change.previousFieldName];
9210
+ fields[change.fieldName] = change.after;
9211
+ tables[change.tableName] = {
9212
+ ...existing,
9213
+ fields
9214
+ };
9215
+ }
9216
+ break;
9217
+ }
9218
+ case "table_renamed":
9219
+ delete tables[change.previousTableName];
9220
+ tables[change.tableName] = change.after;
9221
+ break;
8626
9222
  case "index_added":
8627
9223
  case "index_modified": {
8628
- const existing = types[change.typeName];
9224
+ const existing = tables[change.tableName];
8629
9225
  if (existing) {
8630
9226
  const indexes = copySnapshotRecord(existing.indexes);
8631
9227
  indexes[change.indexName] = change.after;
8632
- types[change.typeName] = {
9228
+ tables[change.tableName] = {
8633
9229
  ...existing,
8634
9230
  indexes
8635
9231
  };
@@ -8637,11 +9233,11 @@ function applyDiffToSnapshot(snapshot, diff) {
8637
9233
  break;
8638
9234
  }
8639
9235
  case "index_removed": {
8640
- const existing = types[change.typeName];
9236
+ const existing = tables[change.tableName];
8641
9237
  if (existing && existing.indexes) {
8642
9238
  const remainingIndexes = copySnapshotRecord(existing.indexes);
8643
9239
  delete remainingIndexes[change.indexName];
8644
- types[change.typeName] = {
9240
+ tables[change.tableName] = {
8645
9241
  ...existing,
8646
9242
  indexes: Object.keys(remainingIndexes).length > 0 ? remainingIndexes : void 0
8647
9243
  };
@@ -8650,11 +9246,11 @@ function applyDiffToSnapshot(snapshot, diff) {
8650
9246
  }
8651
9247
  case "file_added":
8652
9248
  case "file_modified": {
8653
- const existing = types[change.typeName];
9249
+ const existing = tables[change.tableName];
8654
9250
  if (existing) {
8655
9251
  const files = copySnapshotRecord(existing.files);
8656
9252
  files[change.fieldName] = change.after;
8657
- types[change.typeName] = {
9253
+ tables[change.tableName] = {
8658
9254
  ...existing,
8659
9255
  files
8660
9256
  };
@@ -8662,11 +9258,11 @@ function applyDiffToSnapshot(snapshot, diff) {
8662
9258
  break;
8663
9259
  }
8664
9260
  case "file_removed": {
8665
- const existing = types[change.typeName];
9261
+ const existing = tables[change.tableName];
8666
9262
  if (existing && existing.files) {
8667
9263
  const remainingFiles = copySnapshotRecord(existing.files);
8668
9264
  delete remainingFiles[change.fieldName];
8669
- types[change.typeName] = {
9265
+ tables[change.tableName] = {
8670
9266
  ...existing,
8671
9267
  files: Object.keys(remainingFiles).length > 0 ? remainingFiles : void 0
8672
9268
  };
@@ -8675,20 +9271,20 @@ function applyDiffToSnapshot(snapshot, diff) {
8675
9271
  }
8676
9272
  case "relationship_added":
8677
9273
  case "relationship_modified": {
8678
- const existing = types[change.typeName];
9274
+ const existing = tables[change.tableName];
8679
9275
  if (existing) {
8680
9276
  const rel = change.after;
8681
9277
  if ((change.relationshipType ?? (existing.forwardRelationships?.[change.relationshipName] ? "forward" : existing.backwardRelationships?.[change.relationshipName] ? "backward" : "forward")) === "forward") {
8682
9278
  const forwardRelationships = copySnapshotRecord(existing.forwardRelationships);
8683
9279
  forwardRelationships[change.relationshipName] = rel;
8684
- types[change.typeName] = {
9280
+ tables[change.tableName] = {
8685
9281
  ...existing,
8686
9282
  forwardRelationships
8687
9283
  };
8688
9284
  } else {
8689
9285
  const backwardRelationships = copySnapshotRecord(existing.backwardRelationships);
8690
9286
  backwardRelationships[change.relationshipName] = rel;
8691
- types[change.typeName] = {
9287
+ tables[change.tableName] = {
8692
9288
  ...existing,
8693
9289
  backwardRelationships
8694
9290
  };
@@ -8697,20 +9293,20 @@ function applyDiffToSnapshot(snapshot, diff) {
8697
9293
  break;
8698
9294
  }
8699
9295
  case "relationship_removed": {
8700
- const type = types[change.typeName];
9296
+ const type = tables[change.tableName];
8701
9297
  if (type) {
8702
9298
  const targetType = change.relationshipType ?? (type.forwardRelationships?.[change.relationshipName] ? "forward" : type.backwardRelationships?.[change.relationshipName] ? "backward" : null);
8703
9299
  if (targetType === "forward" && type.forwardRelationships?.[change.relationshipName]) {
8704
9300
  const remaining = copySnapshotRecord(type.forwardRelationships);
8705
9301
  delete remaining[change.relationshipName];
8706
- types[change.typeName] = {
9302
+ tables[change.tableName] = {
8707
9303
  ...type,
8708
9304
  forwardRelationships: Object.keys(remaining).length > 0 ? remaining : void 0
8709
9305
  };
8710
9306
  } else if (targetType === "backward" && type.backwardRelationships?.[change.relationshipName]) {
8711
9307
  const remaining = copySnapshotRecord(type.backwardRelationships);
8712
9308
  delete remaining[change.relationshipName];
8713
- types[change.typeName] = {
9309
+ tables[change.tableName] = {
8714
9310
  ...type,
8715
9311
  backwardRelationships: Object.keys(remaining).length > 0 ? remaining : void 0
8716
9312
  };
@@ -8719,10 +9315,10 @@ function applyDiffToSnapshot(snapshot, diff) {
8719
9315
  break;
8720
9316
  }
8721
9317
  case "permission_modified": {
8722
- const existing = types[change.typeName];
9318
+ const existing = tables[change.tableName];
8723
9319
  if (existing && change.after) {
8724
9320
  const after = change.after;
8725
- types[change.typeName] = {
9321
+ tables[change.tableName] = {
8726
9322
  ...existing,
8727
9323
  permissions: {
8728
9324
  record: after.recordPermission,
@@ -8735,7 +9331,7 @@ function applyDiffToSnapshot(snapshot, diff) {
8735
9331
  }
8736
9332
  return normalizeSchemaSnapshot({
8737
9333
  ...snapshot,
8738
- types,
9334
+ tables,
8739
9335
  createdAt: diff.createdAt
8740
9336
  });
8741
9337
  }
@@ -8797,13 +9393,7 @@ function assertMigrationNumberExists(migrationsDir, migrationNumber) {
8797
9393
  function areFieldsDifferent(oldField, newField) {
8798
9394
  if (oldField.type !== newField.type) return true;
8799
9395
  if (oldField.required !== newField.required) return true;
8800
- for (const prop of [
8801
- "array",
8802
- "index",
8803
- "unique",
8804
- "foreignKey",
8805
- "vector"
8806
- ]) if ((oldField[prop] ?? false) !== (newField[prop] ?? false)) return true;
9396
+ for (const prop of SNAPSHOT_FIELD_BOOLEAN_PROPS) if ((oldField[prop] ?? false) !== (newField[prop] ?? false)) return true;
8807
9397
  if (oldField.foreignKeyType !== newField.foreignKeyType) return true;
8808
9398
  if (oldField.foreignKeyField !== newField.foreignKeyField) return true;
8809
9399
  if ((oldField.description ?? "") !== (newField.description ?? "")) return true;
@@ -8859,23 +9449,24 @@ function areFieldsDifferent(oldField, newField) {
8859
9449
  }
8860
9450
  /**
8861
9451
  * Collect breaking changes for a field change
8862
- * @param {string} typeName - Name of the type containing the field
9452
+ * @param {string} tableName - Name of the type containing the field
8863
9453
  * @param {string} fieldName - Name of the field being changed
8864
9454
  * @param {SnapshotFieldConfig | undefined} oldField - Old field configuration
8865
9455
  * @param {SnapshotFieldConfig | undefined} newField - New field configuration
9456
+ * @param {ReadonlyMap<string, string>} [typeRenameTargets] - Confirmed type renames (old name → new name)
8866
9457
  * @returns {BreakingChangeInfo[]} Breaking change information
8867
9458
  */
8868
- function getBreakingFieldChanges(typeName, fieldName, oldField, newField) {
9459
+ function getBreakingFieldChanges(tableName, fieldName, oldField, newField, typeRenameTargets) {
8869
9460
  const breakingChanges = [];
8870
9461
  if (!oldField && newField && newField.required) breakingChanges.push({
8871
- typeName,
9462
+ tableName,
8872
9463
  fieldName,
8873
9464
  reason: "Required field added"
8874
9465
  });
8875
9466
  if (oldField && newField && oldField.type !== newField.type) {
8876
9467
  const supported = supportsInPlaceFieldTypeChange(oldField, newField);
8877
9468
  breakingChanges.push({
8878
- typeName,
9469
+ tableName,
8879
9470
  fieldName,
8880
9471
  reason: `Field type changed from ${oldField.type} to ${newField.type}`,
8881
9472
  ...!supported && {
@@ -8885,36 +9476,32 @@ function getBreakingFieldChanges(typeName, fieldName, oldField, newField) {
8885
9476
  });
8886
9477
  }
8887
9478
  if (oldField && newField && !oldField.required && newField.required) breakingChanges.push({
8888
- typeName,
9479
+ tableName,
8889
9480
  fieldName,
8890
9481
  reason: "Field changed from optional to required"
8891
9482
  });
8892
9483
  if (oldField && newField && (oldField.array ?? false) !== (newField.array ?? false)) {
8893
9484
  const [fromType, toType] = oldField.array ? ["array", "single value"] : ["single value", "array"];
8894
9485
  breakingChanges.push({
8895
- typeName,
9486
+ tableName,
8896
9487
  fieldName,
8897
9488
  reason: `Field changed from ${fromType} to ${toType}`,
8898
9489
  unsupported: true,
8899
9490
  showThreeStepHint: true
8900
9491
  });
8901
9492
  }
8902
- if (oldField && newField) {
8903
- const oldForeignKeyType = oldField.foreignKeyType;
8904
- const newForeignKeyType = newField.foreignKeyType;
8905
- if (oldForeignKeyType && newForeignKeyType && oldForeignKeyType !== newForeignKeyType) breakingChanges.push({
8906
- typeName,
8907
- fieldName,
8908
- reason: `Foreign key target type changed from ${oldForeignKeyType} to ${newForeignKeyType}`
8909
- });
8910
- }
9493
+ if (oldField && newField && isBreakingForeignKeyRetarget(oldField, newField, typeRenameTargets)) breakingChanges.push({
9494
+ tableName,
9495
+ fieldName,
9496
+ reason: `Foreign key target type changed from ${oldField.foreignKeyType} to ${newField.foreignKeyType}`
9497
+ });
8911
9498
  if (oldField && newField && !(oldField.unique ?? false) && (newField.unique ?? false)) breakingChanges.push({
8912
- typeName,
9499
+ tableName,
8913
9500
  fieldName,
8914
9501
  reason: "Unique constraint added to field"
8915
9502
  });
8916
9503
  if (oldField?.type === "decimal" && newField?.type === "decimal" && oldField.scale !== newField.scale) breakingChanges.push({
8917
- typeName,
9504
+ tableName,
8918
9505
  fieldName,
8919
9506
  reason: `Decimal scale changed from ${oldField.scale} to ${newField.scale}`
8920
9507
  });
@@ -8925,7 +9512,7 @@ function getBreakingFieldChanges(typeName, fieldName, oldField, newField) {
8925
9512
  const newValuesSet = new Set(newAllowed.map((v) => v.value));
8926
9513
  const removedValues = oldValues.filter((v) => !newValuesSet.has(v));
8927
9514
  if (removedValues.length > 0) breakingChanges.push({
8928
- typeName,
9515
+ tableName,
8929
9516
  fieldName,
8930
9517
  reason: `Enum values removed: ${removedValues.join(", ")}`
8931
9518
  });
@@ -8935,37 +9522,62 @@ function getBreakingFieldChanges(typeName, fieldName, oldField, newField) {
8935
9522
  function addChange(ctx, change, oldField, newField) {
8936
9523
  ctx.changes.push(change);
8937
9524
  if (!change.fieldName) return;
8938
- const breakingChanges = getBreakingFieldChanges(change.typeName, change.fieldName, oldField, newField);
9525
+ const breakingChanges = getBreakingFieldChanges(change.tableName, change.fieldName, oldField, newField, ctx.typeRenameTargets);
8939
9526
  if (breakingChanges.length > 0) {
8940
9527
  ctx.breakingChanges.push(...breakingChanges);
8941
9528
  return;
8942
9529
  }
8943
9530
  if (change.kind === "field_removed") ctx.warnings.push({
8944
- typeName: change.typeName,
9531
+ tableName: change.tableName,
8945
9532
  fieldName: change.fieldName,
8946
9533
  reason: FIELD_REMOVED_WARNING_REASON
8947
9534
  });
8948
9535
  }
8949
- function compareTypeFields(ctx, typeName, prevType, currType) {
9536
+ function compareTypeFields(ctx, tableName, prevType, currType, fieldRenames = []) {
8950
9537
  const prevFieldNames = new Set(Object.keys(prevType.fields));
8951
9538
  const currFieldNames = new Set(Object.keys(currType.fields));
8952
- for (const fieldName of currFieldNames) if (!prevFieldNames.has(fieldName)) {
8953
- const currField = assertDefined(currType.fields[fieldName], `field "${fieldName}" missing from currType`);
8954
- addChange(ctx, {
8955
- kind: "field_added",
8956
- typeName,
8957
- fieldName,
9539
+ const renamedFromNames = new Set(fieldRenames.map((r) => r.previousFieldName));
9540
+ const renamedToNames = new Set(fieldRenames.map((r) => r.fieldName));
9541
+ for (const rename of fieldRenames) {
9542
+ const prevField = assertDefined(prevType.fields[rename.previousFieldName], `renamed field "${rename.previousFieldName}" missing from prevType`);
9543
+ const currField = assertDefined(currType.fields[rename.fieldName], `renamed field "${rename.fieldName}" missing from currType`);
9544
+ ctx.changes.push({
9545
+ kind: "field_renamed",
9546
+ tableName,
9547
+ fieldName: rename.fieldName,
9548
+ previousFieldName: rename.previousFieldName,
9549
+ before: prevField,
8958
9550
  after: currField
8959
- }, void 0, currField);
9551
+ });
9552
+ ctx.breakingChanges.push({
9553
+ tableName,
9554
+ fieldName: rename.fieldName,
9555
+ reason: `Field renamed from ${rename.previousFieldName} to ${rename.fieldName} (existing values must be copied by the migration script)`
9556
+ });
8960
9557
  }
8961
- for (const fieldName of prevFieldNames) if (!currFieldNames.has(fieldName)) {
8962
- const prevField = assertDefined(prevType.fields[fieldName], `field "${fieldName}" missing from prevType`);
8963
- addChange(ctx, {
8964
- kind: "field_removed",
8965
- typeName,
8966
- fieldName,
8967
- before: prevField
8968
- }, prevField, void 0);
9558
+ for (const fieldName of currFieldNames) {
9559
+ if (renamedToNames.has(fieldName)) continue;
9560
+ if (!prevFieldNames.has(fieldName)) {
9561
+ const currField = assertDefined(currType.fields[fieldName], `field "${fieldName}" missing from currType`);
9562
+ addChange(ctx, {
9563
+ kind: "field_added",
9564
+ tableName,
9565
+ fieldName,
9566
+ after: currField
9567
+ }, void 0, currField);
9568
+ }
9569
+ }
9570
+ for (const fieldName of prevFieldNames) {
9571
+ if (renamedFromNames.has(fieldName)) continue;
9572
+ if (!currFieldNames.has(fieldName)) {
9573
+ const prevField = assertDefined(prevType.fields[fieldName], `field "${fieldName}" missing from prevType`);
9574
+ addChange(ctx, {
9575
+ kind: "field_removed",
9576
+ tableName,
9577
+ fieldName,
9578
+ before: prevField
9579
+ }, prevField, void 0);
9580
+ }
8969
9581
  }
8970
9582
  for (const fieldName of currFieldNames) {
8971
9583
  if (!prevFieldNames.has(fieldName)) continue;
@@ -8973,7 +9585,7 @@ function compareTypeFields(ctx, typeName, prevType, currType) {
8973
9585
  const currField = assertDefined(currType.fields[fieldName], `field "${fieldName}" missing from currType`);
8974
9586
  if (areFieldsDifferent(prevField, currField)) addChange(ctx, {
8975
9587
  kind: prevField.type === currField.type ? "field_modified" : "field_type_modified",
8976
- typeName,
9588
+ tableName,
8977
9589
  fieldName,
8978
9590
  before: prevField,
8979
9591
  after: currField
@@ -8985,20 +9597,20 @@ function compareTypeFields(ctx, typeName, prevType, currType) {
8985
9597
  * unique reasoning: enforcing a unique constraint over existing rows can fail
8986
9598
  * on duplicates, so both adding a unique index and re-pointing an existing
8987
9599
  * unique index at a different field set require a data migration.
8988
- * @param {string} typeName - Name of the type containing the index
9600
+ * @param {string} tableName - Name of the type containing the index
8989
9601
  * @param {string} indexName - Name of the index being changed
8990
9602
  * @param {SnapshotIndexConfig | undefined} oldIndex - Old index configuration
8991
9603
  * @param {SnapshotIndexConfig | undefined} newIndex - New index configuration
8992
9604
  * @returns {BreakingChangeInfo | null} Breaking change info or null if not breaking
8993
9605
  */
8994
- function isBreakingIndexChange(typeName, indexName, oldIndex, newIndex) {
9606
+ function isBreakingIndexChange(tableName, indexName, oldIndex, newIndex) {
8995
9607
  if (!newIndex || !(newIndex.unique ?? false)) return null;
8996
9608
  if (!oldIndex || !(oldIndex.unique ?? false)) return {
8997
- typeName,
9609
+ tableName,
8998
9610
  reason: `Unique constraint added to index "${indexName}"`
8999
9611
  };
9000
9612
  if (JSON.stringify(oldIndex.fields.toSorted()) !== JSON.stringify(newIndex.fields.toSorted())) return {
9001
- typeName,
9613
+ tableName,
9002
9614
  reason: `Unique index fields changed on index "${indexName}"`
9003
9615
  };
9004
9616
  return null;
@@ -9006,27 +9618,27 @@ function isBreakingIndexChange(typeName, indexName, oldIndex, newIndex) {
9006
9618
  /**
9007
9619
  * Compare type-level indexes
9008
9620
  * @param {DiffContext} ctx - Diff context
9009
- * @param {string} typeName - Type name
9621
+ * @param {string} tableName - Type name
9010
9622
  * @param {Record<string, SnapshotIndexConfig> | undefined} oldIndexes - Previous indexes
9011
9623
  * @param {Record<string, SnapshotIndexConfig> | undefined} newIndexes - Current indexes
9012
9624
  * @returns {void}
9013
9625
  */
9014
- function compareIndexes(ctx, typeName, oldIndexes, newIndexes) {
9626
+ function compareIndexes(ctx, tableName, oldIndexes, newIndexes) {
9015
9627
  const oldKeys = new Set(Object.keys(oldIndexes || {}));
9016
9628
  const newKeys = new Set(Object.keys(newIndexes || {}));
9017
9629
  for (const [indexName, indexConfig] of Object.entries(newIndexes ?? {})) if (!oldKeys.has(indexName)) {
9018
9630
  ctx.changes.push({
9019
9631
  kind: "index_added",
9020
- typeName,
9632
+ tableName,
9021
9633
  indexName,
9022
9634
  after: indexConfig
9023
9635
  });
9024
- const breaking = isBreakingIndexChange(typeName, indexName, void 0, indexConfig);
9636
+ const breaking = isBreakingIndexChange(tableName, indexName, void 0, indexConfig);
9025
9637
  if (breaking) ctx.breakingChanges.push(breaking);
9026
9638
  }
9027
9639
  for (const [indexName, indexConfig] of Object.entries(oldIndexes ?? {})) if (!newKeys.has(indexName)) ctx.changes.push({
9028
9640
  kind: "index_removed",
9029
- typeName,
9641
+ tableName,
9030
9642
  indexName,
9031
9643
  before: indexConfig
9032
9644
  });
@@ -9040,13 +9652,13 @@ function compareIndexes(ctx, typeName, oldIndexes, newIndexes) {
9040
9652
  if ((oldIndex.unique ?? false) !== (newIndex.unique ?? false)) reasons.push("unique constraint changed");
9041
9653
  ctx.changes.push({
9042
9654
  kind: "index_modified",
9043
- typeName,
9655
+ tableName,
9044
9656
  indexName,
9045
9657
  reason: reasons.join(", "),
9046
9658
  before: oldIndex,
9047
9659
  after: newIndex
9048
9660
  });
9049
- const breaking = isBreakingIndexChange(typeName, indexName, oldIndex, newIndex);
9661
+ const breaking = isBreakingIndexChange(tableName, indexName, oldIndex, newIndex);
9050
9662
  if (breaking) ctx.breakingChanges.push(breaking);
9051
9663
  }
9052
9664
  }
@@ -9054,23 +9666,23 @@ function compareIndexes(ctx, typeName, oldIndexes, newIndexes) {
9054
9666
  /**
9055
9667
  * Compare type-level file fields
9056
9668
  * @param {DiffContext} ctx - Diff context
9057
- * @param {string} typeName - Type name
9669
+ * @param {string} tableName - Type name
9058
9670
  * @param {Record<string, string> | undefined} oldFiles - Previous file fields
9059
9671
  * @param {Record<string, string> | undefined} newFiles - Current file fields
9060
9672
  * @returns {void}
9061
9673
  */
9062
- function compareFiles(ctx, typeName, oldFiles, newFiles) {
9674
+ function compareFiles(ctx, tableName, oldFiles, newFiles) {
9063
9675
  const oldKeys = new Set(Object.keys(oldFiles || {}));
9064
9676
  const newKeys = new Set(Object.keys(newFiles || {}));
9065
9677
  for (const [fileName, fileDesc] of Object.entries(newFiles ?? {})) if (!oldKeys.has(fileName)) ctx.changes.push({
9066
9678
  kind: "file_added",
9067
- typeName,
9679
+ tableName,
9068
9680
  fieldName: fileName,
9069
9681
  after: fileDesc
9070
9682
  });
9071
9683
  for (const [fileName, fileDesc] of Object.entries(oldFiles ?? {})) if (!newKeys.has(fileName)) ctx.changes.push({
9072
9684
  kind: "file_removed",
9073
- typeName,
9685
+ tableName,
9074
9686
  fieldName: fileName,
9075
9687
  before: fileDesc
9076
9688
  });
@@ -9078,7 +9690,7 @@ function compareFiles(ctx, typeName, oldFiles, newFiles) {
9078
9690
  const oldDesc = assertDefined(assertDefined(oldFiles, "oldFiles is undefined when oldKeys has entry")[fileName], `file "${fileName}" missing from oldFiles`);
9079
9691
  if (oldDesc !== newDesc) ctx.changes.push({
9080
9692
  kind: "file_modified",
9081
- typeName,
9693
+ tableName,
9082
9694
  fieldName: fileName,
9083
9695
  reason: "description changed",
9084
9696
  before: oldDesc,
@@ -9089,25 +9701,25 @@ function compareFiles(ctx, typeName, oldFiles, newFiles) {
9089
9701
  /**
9090
9702
  * Compare type-level relationships
9091
9703
  * @param {DiffContext} ctx - Diff context
9092
- * @param {string} typeName - Type name
9704
+ * @param {string} tableName - Type name
9093
9705
  * @param {"forward" | "backward"} relationshipType - Relationship direction to compare
9094
9706
  * @param {Record<string, SnapshotRelationship> | undefined} oldRelationships - Previous relationships
9095
9707
  * @param {Record<string, SnapshotRelationship> | undefined} newRelationships - Current relationships
9096
9708
  * @returns {void}
9097
9709
  */
9098
- function compareRelationships(ctx, typeName, relationshipType, oldRelationships, newRelationships) {
9710
+ function compareRelationships(ctx, tableName, relationshipType, oldRelationships, newRelationships) {
9099
9711
  const oldKeys = new Set(Object.keys(oldRelationships || {}));
9100
9712
  const newKeys = new Set(Object.keys(newRelationships || {}));
9101
9713
  for (const [relName, rel] of Object.entries(newRelationships ?? {})) if (!oldKeys.has(relName)) ctx.changes.push({
9102
9714
  kind: "relationship_added",
9103
- typeName,
9715
+ tableName,
9104
9716
  relationshipName: relName,
9105
9717
  relationshipType,
9106
9718
  after: rel
9107
9719
  });
9108
9720
  for (const [relName, rel] of Object.entries(oldRelationships ?? {})) if (!newKeys.has(relName)) ctx.changes.push({
9109
9721
  kind: "relationship_removed",
9110
- typeName,
9722
+ tableName,
9111
9723
  relationshipName: relName,
9112
9724
  relationshipType,
9113
9725
  before: rel
@@ -9122,7 +9734,7 @@ function compareRelationships(ctx, typeName, relationshipType, oldRelationships,
9122
9734
  if (oldRel.description !== newRel.description) reasons.push("description changed");
9123
9735
  if (reasons.length > 0) ctx.changes.push({
9124
9736
  kind: "relationship_modified",
9125
- typeName,
9737
+ tableName,
9126
9738
  relationshipName: relName,
9127
9739
  relationshipType,
9128
9740
  reason: reasons.join(", "),
@@ -9134,14 +9746,14 @@ function compareRelationships(ctx, typeName, relationshipType, oldRelationships,
9134
9746
  /**
9135
9747
  * Compare type-level permissions
9136
9748
  * @param {DiffContext} ctx - Diff context
9137
- * @param {string} typeName - Type name
9749
+ * @param {string} tableName - Type name
9138
9750
  * @param {SnapshotRecordPermission | undefined} oldRecordPerm - Previous record permission
9139
9751
  * @param {SnapshotRecordPermission | undefined} newRecordPerm - Current record permission
9140
9752
  * @param {SnapshotGqlPermission | undefined} oldGqlPerm - Previous GQL permission
9141
9753
  * @param {SnapshotGqlPermission | undefined} newGqlPerm - Current GQL permission
9142
9754
  * @returns {void}
9143
9755
  */
9144
- function comparePermissions(ctx, typeName, oldRecordPerm, newRecordPerm, oldGqlPerm, newGqlPerm) {
9756
+ function comparePermissions(ctx, tableName, oldRecordPerm, newRecordPerm, oldGqlPerm, newGqlPerm) {
9145
9757
  const oldComparableRecordPerm = comparableRecordPermission(oldRecordPerm);
9146
9758
  const newComparableRecordPerm = comparableRecordPermission(newRecordPerm);
9147
9759
  const recordPermChanged = JSON.stringify(oldComparableRecordPerm ?? null) !== JSON.stringify(newComparableRecordPerm ?? null);
@@ -9154,7 +9766,7 @@ function comparePermissions(ctx, typeName, oldRecordPerm, newRecordPerm, oldGqlP
9154
9766
  if (gqlPermChanged) reasons.push("GQL permission");
9155
9767
  ctx.changes.push({
9156
9768
  kind: "permission_modified",
9157
- typeName,
9769
+ tableName,
9158
9770
  reason: `${reasons.join(" and ")} changed`,
9159
9771
  before: {
9160
9772
  recordPermission: oldComparableRecordPerm,
@@ -9234,13 +9846,13 @@ function comparableTypeSettings(type) {
9234
9846
  function snapshotTypeSettingsState(type) {
9235
9847
  return typeSettingsState(type.description, type.pluralForm, type.settings ?? {});
9236
9848
  }
9237
- function compareTypeSettings(ctx, typeName, previous, current) {
9849
+ function compareTypeSettings(ctx, tableName, previous, current) {
9238
9850
  const previousComparable = comparableTypeSettings(previous);
9239
9851
  const currentComparable = comparableTypeSettings(current);
9240
9852
  if (JSON.stringify(previousComparable) === JSON.stringify(currentComparable)) return;
9241
9853
  ctx.changes.push({
9242
- kind: "type_settings_modified",
9243
- typeName,
9854
+ kind: "table_settings_modified",
9855
+ tableName,
9244
9856
  reason: "settings changed",
9245
9857
  before: snapshotTypeSettingsState(previous),
9246
9858
  after: snapshotTypeSettingsState(current)
@@ -9252,66 +9864,190 @@ function typeScriptsState(type) {
9252
9864
  ...type.typeValidateExpr !== void 0 && { typeValidateExpr: type.typeValidateExpr }
9253
9865
  };
9254
9866
  }
9255
- function compareTypeScripts(ctx, typeName, previous, current) {
9867
+ function compareTypeScripts(ctx, tableName, previous, current) {
9256
9868
  const prevState = typeScriptsState(previous);
9257
9869
  const currState = typeScriptsState(current);
9258
9870
  if (JSON.stringify(prevState) === JSON.stringify(currState)) return;
9259
9871
  ctx.changes.push({
9260
- kind: "type_scripts_modified",
9261
- typeName,
9262
- reason: "type-level scripts changed",
9872
+ kind: "table_scripts_modified",
9873
+ tableName,
9874
+ reason: "table-level scripts changed",
9263
9875
  before: prevState,
9264
9876
  after: currState
9265
9877
  });
9266
9878
  }
9267
9879
  /**
9268
- * Compare two snapshots and generate a diff
9269
- * @param {SchemaSnapshot} previous - Previous schema snapshot
9270
- * @param {SchemaSnapshot} current - Current schema snapshot
9880
+ * Restate the schema an expand migration starts from, with each converted field
9881
+ * relaxed to optional.
9882
+ *
9883
+ * The expand script clears the original field once it has carried the value
9884
+ * across. That write reaches the field under the contract recorded on the
9885
+ * removal, which the deploy restores for the duration of the migration, so a
9886
+ * field left required would reject it.
9887
+ * @param previous - Snapshot the expand migration starts from
9888
+ * @param plans - Field changes carried through temporary fields
9889
+ * @returns Snapshot to compare the expand migration against
9890
+ */
9891
+ function buildExpandBaseSnapshot(previous, plans) {
9892
+ const tables = copySnapshotRecord(previous.tables);
9893
+ for (const plan of plans) {
9894
+ const type = tables[plan.tableName];
9895
+ const original = type?.fields[plan.fieldName];
9896
+ if (!type || !original) continue;
9897
+ const fields = copySnapshotRecord(type.fields);
9898
+ fields[plan.fieldName] = {
9899
+ ...original,
9900
+ required: false
9901
+ };
9902
+ tables[plan.tableName] = {
9903
+ ...type,
9904
+ fields
9905
+ };
9906
+ }
9907
+ return normalizeSchemaSnapshot({
9908
+ ...previous,
9909
+ tables
9910
+ });
9911
+ }
9912
+ /**
9913
+ * Build the diff for the migration that converts values into temporary fields.
9914
+ *
9915
+ * Adding an optional field and removing one are both non-breaking, so nothing
9916
+ * in the comparison marks the script as required — yet it is the only thing
9917
+ * carrying the values across before the original field is dropped.
9918
+ * @param previous - Snapshot the expand migration starts from
9919
+ * @param intermediate - Snapshot the expand migration produces
9920
+ * @param plans - Field changes carried through temporary fields
9921
+ * @returns Diff to write for the expand migration
9922
+ */
9923
+ function buildExpandDiff(previous, intermediate, plans) {
9924
+ return {
9925
+ ...compareSnapshots(buildExpandBaseSnapshot(previous, plans), intermediate),
9926
+ requiresMigrationScript: true
9927
+ };
9928
+ }
9929
+ /**
9930
+ * Build the schema state that sits between an expand and a contract migration:
9931
+ * each converted field is replaced by its temporary counterpart.
9932
+ *
9933
+ * The original field is dropped here rather than in the contract migration so
9934
+ * the contract can reuse its name. It stays readable while the expand script
9935
+ * runs, because a field removed by a migration is retained until that same
9936
+ * migration's post phase.
9937
+ *
9938
+ * The temporary field is optional and non-unique regardless of its final
9939
+ * contract, since the expand script fills it in batches.
9940
+ * @param previous - Snapshot the expand migration starts from
9941
+ * @param plans - Field changes carried through temporary fields
9942
+ * @returns Snapshot the contract migration compares against
9943
+ */
9944
+ function buildIntermediateSnapshot(previous, plans) {
9945
+ const tables = copySnapshotRecord(previous.tables);
9946
+ for (const plan of plans) {
9947
+ const type = tables[plan.tableName];
9948
+ if (!type) continue;
9949
+ const fields = copySnapshotRecord(type.fields);
9950
+ const { hooks: _hooks, validate: _validate, ...carried } = plan.after;
9951
+ fields[plan.tempFieldName] = {
9952
+ ...carried,
9953
+ required: false,
9954
+ unique: false
9955
+ };
9956
+ delete fields[plan.fieldName];
9957
+ tables[plan.tableName] = {
9958
+ ...type,
9959
+ fields
9960
+ };
9961
+ }
9962
+ return normalizeSchemaSnapshot({
9963
+ ...previous,
9964
+ tables
9965
+ });
9966
+ }
9967
+ /**
9968
+ * Compare two normalized snapshots and generate a diff
9969
+ * @param {NormalizedSchemaSnapshot} previous - Previous normalized snapshot
9970
+ * @param {NormalizedSchemaSnapshot} current - Current normalized snapshot
9971
+ * @param {CompareSnapshotsOptions} [options] - Comparison options
9271
9972
  * @returns {MigrationDiff} Migration diff between snapshots
9272
9973
  */
9273
- function compareSnapshots(previous, current) {
9274
- return compareNormalizedSnapshots(normalizeSchemaSnapshot(previous), normalizeSchemaSnapshot(current));
9275
- }
9276
- function compareNormalizedSnapshots(previous, current) {
9974
+ function compareSnapshots(previous, current, options) {
9975
+ const fieldRenames = options?.fieldRenames ?? [];
9976
+ assertValidFieldRenames(previous, current, fieldRenames);
9977
+ const renamesByType = /* @__PURE__ */ new Map();
9978
+ for (const rename of fieldRenames) {
9979
+ const list = renamesByType.get(rename.tableName) ?? [];
9980
+ list.push(rename);
9981
+ renamesByType.set(rename.tableName, list);
9982
+ }
9983
+ const typeRenames = options?.typeRenames ?? [];
9984
+ assertValidTypeRenames(previous, current, typeRenames);
9985
+ const typeRenameTargets = new Map(typeRenames.map((r) => [r.previousTableName, r.tableName]));
9986
+ const renamedToTypeNames = new Set(typeRenames.map((r) => r.tableName));
9277
9987
  const ctx = {
9278
9988
  changes: [],
9279
9989
  breakingChanges: [],
9280
- warnings: []
9281
- };
9282
- const previousTypeNames = new Set(Object.keys(previous.types));
9283
- const currentTypeNames = new Set(Object.keys(current.types));
9284
- for (const [typeName, type] of Object.entries(current.types)) if (!previousTypeNames.has(typeName)) ctx.changes.push({
9285
- kind: "type_added",
9286
- typeName,
9287
- after: type
9288
- });
9289
- for (const [typeName, type] of Object.entries(previous.types)) if (!currentTypeNames.has(typeName)) {
9990
+ warnings: [],
9991
+ typeRenameTargets
9992
+ };
9993
+ const previousTypeNames = new Set(Object.keys(previous.tables));
9994
+ const currentTypeNames = new Set(Object.keys(current.tables));
9995
+ for (const rename of typeRenames) {
9996
+ const prevType = assertDefined(previous.tables[rename.previousTableName], `renamed type "${rename.previousTableName}" missing from previous snapshot`);
9997
+ const currType = assertDefined(current.tables[rename.tableName], `renamed type "${rename.tableName}" missing from current snapshot`);
9290
9998
  ctx.changes.push({
9291
- kind: "type_removed",
9292
- typeName,
9293
- before: type
9294
- });
9295
- ctx.warnings.push({
9296
- typeName,
9297
- reason: TYPE_REMOVED_WARNING_REASON
9298
- });
9299
- }
9300
- for (const typeName of currentTypeNames) {
9301
- if (!previousTypeNames.has(typeName)) continue;
9302
- const prevType = assertDefined(previous.types[typeName], `type "${typeName}" missing from previous snapshot`);
9303
- const currType = assertDefined(current.types[typeName], `type "${typeName}" missing from current snapshot`);
9304
- compareTypeSettings(ctx, typeName, prevType, currType);
9305
- compareTypeScripts(ctx, typeName, prevType, currType);
9306
- compareTypeFields(ctx, typeName, prevType, currType);
9307
- compareIndexes(ctx, typeName, prevType.indexes, currType.indexes);
9308
- compareFiles(ctx, typeName, prevType.files, currType.files);
9309
- compareRelationships(ctx, typeName, "forward", prevType.forwardRelationships, currType.forwardRelationships);
9310
- compareRelationships(ctx, typeName, "backward", prevType.backwardRelationships, currType.backwardRelationships);
9311
- comparePermissions(ctx, typeName, prevType.permissions?.record, currType.permissions?.record, prevType.permissions?.gql, currType.permissions?.gql);
9999
+ kind: "table_renamed",
10000
+ tableName: rename.tableName,
10001
+ previousTableName: rename.previousTableName,
10002
+ before: prevType,
10003
+ after: currType
10004
+ });
10005
+ ctx.breakingChanges.push({
10006
+ tableName: rename.tableName,
10007
+ reason: `Type renamed from ${rename.previousTableName} to ${rename.tableName} (existing records must be copied by the migration script)`
10008
+ });
10009
+ ctx.breakingChanges.push({
10010
+ tableName: rename.tableName,
10011
+ reason: `GraphQL API names derived from ${rename.previousTableName}/${prevType.pluralForm} change to ${rename.tableName}/${currType.pluralForm} breaking for API clients`
10012
+ });
10013
+ }
10014
+ for (const [tableName, type] of Object.entries(current.tables)) {
10015
+ if (renamedToTypeNames.has(tableName)) continue;
10016
+ if (!previousTypeNames.has(tableName)) ctx.changes.push({
10017
+ kind: "table_added",
10018
+ tableName,
10019
+ after: type
10020
+ });
10021
+ }
10022
+ for (const [tableName, type] of Object.entries(previous.tables)) {
10023
+ if (typeRenameTargets.has(tableName)) continue;
10024
+ if (!currentTypeNames.has(tableName)) {
10025
+ ctx.changes.push({
10026
+ kind: "table_removed",
10027
+ tableName,
10028
+ before: type
10029
+ });
10030
+ ctx.warnings.push({
10031
+ tableName,
10032
+ reason: TABLE_REMOVED_WARNING_REASON
10033
+ });
10034
+ }
10035
+ }
10036
+ for (const tableName of currentTypeNames) {
10037
+ if (!previousTypeNames.has(tableName)) continue;
10038
+ const prevType = assertDefined(previous.tables[tableName], `type "${tableName}" missing from previous snapshot`);
10039
+ const currType = assertDefined(current.tables[tableName], `type "${tableName}" missing from current snapshot`);
10040
+ compareTypeSettings(ctx, tableName, prevType, currType);
10041
+ compareTypeScripts(ctx, tableName, prevType, currType);
10042
+ compareTypeFields(ctx, tableName, prevType, currType, renamesByType.get(tableName));
10043
+ compareIndexes(ctx, tableName, prevType.indexes, currType.indexes);
10044
+ compareFiles(ctx, tableName, prevType.files, currType.files);
10045
+ compareRelationships(ctx, tableName, "forward", prevType.forwardRelationships, currType.forwardRelationships);
10046
+ compareRelationships(ctx, tableName, "backward", prevType.backwardRelationships, currType.backwardRelationships);
10047
+ comparePermissions(ctx, tableName, prevType.permissions?.record, currType.permissions?.record, prevType.permissions?.gql, currType.permissions?.gql);
9312
10048
  }
9313
10049
  return {
9314
- version: 2,
10050
+ version: 5,
9315
10051
  namespace: current.namespace,
9316
10052
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9317
10053
  changes: ctx.changes,
@@ -9325,8 +10061,8 @@ function compareNormalizedSnapshots(previous, current) {
9325
10061
  /**
9326
10062
  * Compare a snapshot against canonical TailorDBSnapshotType-shaped local types.
9327
10063
  * Callers are expected to pre-convert TailorDBService.types to TailorDBSnapshotType via
9328
- * `createSnapshotType`. As a safety net, `compareSnapshots` re-runs idempotent
9329
- * normalization on both sides, so a caller that forgets will still get correct
10064
+ * `createSnapshotType`. As a safety net, both sides are re-run through idempotent
10065
+ * normalization here, so a caller that forgets will still get correct
9330
10066
  * comparisons (no silent false drift).
9331
10067
  * @param {SchemaSnapshot} snapshot - Schema snapshot to compare against
9332
10068
  * @param {Record<string, TailorDBSnapshotType>} localTypes - Local snapshot-shaped types
@@ -9334,12 +10070,13 @@ function compareNormalizedSnapshots(previous, current) {
9334
10070
  * @returns {MigrationDiff} Migration diff
9335
10071
  */
9336
10072
  function compareLocalTypesWithSnapshot(snapshot, localTypes, namespace) {
9337
- return compareSnapshots(snapshot, {
9338
- version: 2,
10073
+ const currentSnapshot = {
10074
+ version: 5,
9339
10075
  namespace,
9340
10076
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9341
- types: localTypes
9342
- });
10077
+ tables: localTypes
10078
+ };
10079
+ return compareSnapshots(normalizeSchemaSnapshot(snapshot), normalizeSchemaSnapshot(currentSnapshot));
9343
10080
  }
9344
10081
  /**
9345
10082
  * Validate migration files in a directory
@@ -9640,11 +10377,11 @@ function convertRemoteTypeToSnapshot(remoteType, expectedType) {
9640
10377
  * @returns {NormalizedSchemaSnapshot} Normalized snapshot-shaped remote state
9641
10378
  */
9642
10379
  function createSnapshotFromRemoteTypes(remoteTypes, namespace, remoteGqlPermissions = [], expectedSnapshot) {
9643
- const types = createSnapshotRecord();
9644
- for (const remoteType of remoteTypes) types[remoteType.name] = convertRemoteTypeToSnapshot(remoteType, expectedSnapshot?.types[remoteType.name]);
10380
+ const tables = createSnapshotRecord();
10381
+ for (const remoteType of remoteTypes) tables[remoteType.name] = convertRemoteTypeToSnapshot(remoteType, expectedSnapshot?.tables[remoteType.name]);
9645
10382
  for (const permission of remoteGqlPermissions) {
9646
- const { typeName } = permission;
9647
- const snapshotType = types[typeName];
10383
+ const { typeName: tableName } = permission;
10384
+ const snapshotType = tables[tableName];
9648
10385
  if (!snapshotType) continue;
9649
10386
  const gqlPermission = convertRemoteGqlPermissionToSnapshot(permission.permission);
9650
10387
  if (!gqlPermission) continue;
@@ -9654,10 +10391,10 @@ function createSnapshotFromRemoteTypes(remoteTypes, namespace, remoteGqlPermissi
9654
10391
  };
9655
10392
  }
9656
10393
  return normalizeSchemaSnapshot({
9657
- version: 2,
10394
+ version: 5,
9658
10395
  namespace,
9659
10396
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
9660
- types
10397
+ tables
9661
10398
  });
9662
10399
  }
9663
10400
  function fieldDifferenceValue(value) {
@@ -9744,13 +10481,7 @@ function addNestedFieldDifferences(differences, prefix, remoteField, snapshotFie
9744
10481
  function addFieldDifferences(differences, prefix, remoteField, snapshotField) {
9745
10482
  addFieldDifference(differences, prefix, "type", remoteField.type, snapshotField.type);
9746
10483
  addFieldDifference(differences, prefix, "required", remoteField.required, snapshotField.required);
9747
- for (const key of [
9748
- "array",
9749
- "index",
9750
- "unique",
9751
- "foreignKey",
9752
- "vector"
9753
- ]) addBooleanFieldDifference(differences, prefix, key, remoteField, snapshotField);
10484
+ for (const key of SNAPSHOT_FIELD_BOOLEAN_PROPS) addBooleanFieldDifference(differences, prefix, key, remoteField, snapshotField);
9754
10485
  addFieldDifference(differences, prefix, "foreignKeyType", remoteField.foreignKeyType, snapshotField.foreignKeyType);
9755
10486
  addFieldDifference(differences, prefix, "foreignKeyField", remoteField.foreignKeyField, snapshotField.foreignKeyField);
9756
10487
  addFieldDifference(differences, prefix, "description", remoteField.description ?? "", snapshotField.description ?? "");
@@ -9763,17 +10494,17 @@ function addFieldDifferences(differences, prefix, remoteField, snapshotField) {
9763
10494
  }
9764
10495
  /**
9765
10496
  * Compare a single field between remote and snapshot
9766
- * @param {string} typeName - Name of the type
10497
+ * @param {string} tableName - Name of the type
9767
10498
  * @param {string} fieldName - Name of the field
9768
10499
  * @param {SnapshotFieldConfig} remoteField - Remote field config
9769
10500
  * @param {SnapshotFieldConfig} snapshotField - Snapshot field config
9770
10501
  * @returns {SchemaDrift | null} Drift info or null if fields match
9771
10502
  */
9772
- function compareFields(typeName, fieldName, remoteField, snapshotField) {
10503
+ function compareFields(tableName, fieldName, remoteField, snapshotField) {
9773
10504
  const differences = [];
9774
10505
  addFieldDifferences(differences, "", remoteField, snapshotField);
9775
10506
  if (differences.length > 0) return {
9776
- typeName,
10507
+ tableName,
9777
10508
  kind: "field_mismatch",
9778
10509
  fieldName,
9779
10510
  details: differences.join("; ")
@@ -9819,24 +10550,24 @@ function remoteHasScripts(remoteType) {
9819
10550
  function compareScriptHashes(remoteTypes, snapshot) {
9820
10551
  const drifts = [];
9821
10552
  const remoteByName = new Map(remoteTypes.map((t) => [t.name, t]));
9822
- for (const [typeName, snapshotType] of Object.entries(snapshot.types)) {
10553
+ for (const [tableName, snapshotType] of Object.entries(snapshot.tables)) {
9823
10554
  const localHash = computeSourceScriptHash(snapshotType.fields, {
9824
10555
  typeHookExpr: snapshotType.typeHookExpr,
9825
10556
  typeValidateExpr: snapshotType.typeValidateExpr
9826
10557
  });
9827
- const remoteType = remoteByName.get(typeName);
10558
+ const remoteType = remoteByName.get(tableName);
9828
10559
  if (!remoteType) continue;
9829
10560
  if (localHash) {
9830
10561
  const remoteHash = extractRemoteScriptHash(remoteType);
9831
10562
  if (localHash !== remoteHash) drifts.push({
9832
- typeName,
10563
+ tableName,
9833
10564
  kind: "script_mismatch",
9834
- details: remoteHash ? `Type '${typeName}' scripts differ between remote and snapshot` : `Type '${typeName}' has no script hash on remote`
10565
+ details: remoteHash ? `Type '${tableName}' scripts differ between remote and snapshot` : `Type '${tableName}' has no script hash on remote`
9835
10566
  });
9836
10567
  } else if (remoteHasScripts(remoteType)) drifts.push({
9837
- typeName,
10568
+ tableName,
9838
10569
  kind: "script_mismatch",
9839
- details: `Type '${typeName}' has scripts on remote but not in snapshot`
10570
+ details: `Type '${tableName}' has scripts on remote but not in snapshot`
9840
10571
  });
9841
10572
  }
9842
10573
  return drifts;
@@ -9854,27 +10585,27 @@ function stripFieldScriptProps(field) {
9854
10585
  return rest;
9855
10586
  }
9856
10587
  function createRemoteComparableSnapshot(snapshot) {
9857
- const types = createSnapshotRecord();
9858
- for (const [typeName, type] of Object.entries(snapshot.types)) {
10588
+ const tables = createSnapshotRecord();
10589
+ for (const [tableName, type] of Object.entries(snapshot.tables)) {
9859
10590
  const fields = createSnapshotRecord();
9860
10591
  for (const [fieldName, field] of Object.entries(type.fields)) {
9861
10592
  if (SYSTEM_FIELDS.has(fieldName)) continue;
9862
10593
  fields[fieldName] = stripFieldScriptProps(field);
9863
10594
  }
9864
10595
  const { typeHookExpr: _, typeValidateExpr: __, ...typeRest } = type;
9865
- types[typeName] = {
10596
+ tables[tableName] = {
9866
10597
  ...typeRest,
9867
10598
  fields
9868
10599
  };
9869
10600
  }
9870
10601
  return normalizeSchemaSnapshot({
9871
10602
  ...snapshot,
9872
- types
10603
+ tables
9873
10604
  });
9874
10605
  }
9875
10606
  function fieldDriftFromChange(change) {
9876
- return compareFields(change.typeName, change.fieldName, change.before, change.after) ?? {
9877
- typeName: change.typeName,
10607
+ return compareFields(change.tableName, change.fieldName, change.before, change.after) ?? {
10608
+ tableName: change.tableName,
9878
10609
  kind: "field_mismatch",
9879
10610
  fieldName: change.fieldName,
9880
10611
  details: `Field '${change.fieldName}' differs between remote and snapshot`
@@ -9882,100 +10613,111 @@ function fieldDriftFromChange(change) {
9882
10613
  }
9883
10614
  function schemaDriftFromDiffChange(change) {
9884
10615
  switch (change.kind) {
9885
- case "type_added": return {
9886
- typeName: change.typeName,
10616
+ case "table_added": return {
10617
+ tableName: change.tableName,
9887
10618
  kind: "type_missing_remote",
9888
- details: `Type '${change.typeName}' exists in snapshot but not in remote`
10619
+ details: `Type '${change.tableName}' exists in snapshot but not in remote`
9889
10620
  };
9890
- case "type_removed": return {
9891
- typeName: change.typeName,
10621
+ case "table_removed": return {
10622
+ tableName: change.tableName,
9892
10623
  kind: "type_missing_local",
9893
- details: `Type '${change.typeName}' exists in remote but not in snapshot`
10624
+ details: `Type '${change.tableName}' exists in remote but not in snapshot`
10625
+ };
10626
+ case "table_renamed": return {
10627
+ tableName: change.tableName,
10628
+ kind: "type_settings_mismatch",
10629
+ details: `Type '${change.previousTableName}' was renamed to '${change.tableName}'`
9894
10630
  };
9895
- case "type_settings_modified":
9896
- case "type_modified": return {
9897
- typeName: change.typeName,
10631
+ case "table_settings_modified":
10632
+ case "table_modified": return {
10633
+ tableName: change.tableName,
9898
10634
  kind: "type_settings_mismatch",
9899
10635
  details: change.reason ?? "Type settings differ between remote and snapshot"
9900
10636
  };
9901
10637
  case "field_added": return {
9902
- typeName: change.typeName,
10638
+ tableName: change.tableName,
9903
10639
  kind: "field_missing_remote",
9904
10640
  fieldName: change.fieldName,
9905
10641
  details: `Field '${change.fieldName}' exists in snapshot but not in remote`
9906
10642
  };
9907
10643
  case "field_removed": return {
9908
- typeName: change.typeName,
10644
+ tableName: change.tableName,
9909
10645
  kind: "field_missing_local",
9910
10646
  fieldName: change.fieldName,
9911
10647
  details: `Field '${change.fieldName}' exists in remote but not in snapshot`
9912
10648
  };
9913
10649
  case "field_modified":
9914
10650
  case "field_type_modified": return fieldDriftFromChange(change);
10651
+ case "field_renamed": return {
10652
+ tableName: change.tableName,
10653
+ kind: "field_mismatch",
10654
+ fieldName: change.fieldName,
10655
+ details: `Field '${change.previousFieldName}' was renamed to '${change.fieldName}'`
10656
+ };
9915
10657
  case "index_added": return {
9916
- typeName: change.typeName,
10658
+ tableName: change.tableName,
9917
10659
  kind: "index_missing_remote",
9918
10660
  indexName: change.indexName,
9919
10661
  details: `Index '${change.indexName}' exists in snapshot but not in remote`
9920
10662
  };
9921
10663
  case "index_removed": return {
9922
- typeName: change.typeName,
10664
+ tableName: change.tableName,
9923
10665
  kind: "index_missing_local",
9924
10666
  indexName: change.indexName,
9925
10667
  details: `Index '${change.indexName}' exists in remote but not in snapshot`
9926
10668
  };
9927
10669
  case "index_modified": return {
9928
- typeName: change.typeName,
10670
+ tableName: change.tableName,
9929
10671
  kind: "index_mismatch",
9930
10672
  indexName: change.indexName,
9931
10673
  details: change.reason ?? `Index '${change.indexName}' differs between remote and snapshot`
9932
10674
  };
9933
10675
  case "file_added": return {
9934
- typeName: change.typeName,
10676
+ tableName: change.tableName,
9935
10677
  kind: "file_missing_remote",
9936
10678
  fileName: change.fieldName,
9937
10679
  details: `File '${change.fieldName}' exists in snapshot but not in remote`
9938
10680
  };
9939
10681
  case "file_removed": return {
9940
- typeName: change.typeName,
10682
+ tableName: change.tableName,
9941
10683
  kind: "file_missing_local",
9942
10684
  fileName: change.fieldName,
9943
10685
  details: `File '${change.fieldName}' exists in remote but not in snapshot`
9944
10686
  };
9945
10687
  case "file_modified": return {
9946
- typeName: change.typeName,
10688
+ tableName: change.tableName,
9947
10689
  kind: "file_mismatch",
9948
10690
  fileName: change.fieldName,
9949
10691
  details: change.reason ?? `File '${change.fieldName}' differs between remote and snapshot`
9950
10692
  };
9951
10693
  case "relationship_added": return {
9952
- typeName: change.typeName,
10694
+ tableName: change.tableName,
9953
10695
  kind: "relationship_missing_remote",
9954
10696
  relationshipName: change.relationshipName,
9955
10697
  relationshipType: change.relationshipType,
9956
10698
  details: `Relationship '${change.relationshipName}' exists in snapshot but not in remote`
9957
10699
  };
9958
10700
  case "relationship_removed": return {
9959
- typeName: change.typeName,
10701
+ tableName: change.tableName,
9960
10702
  kind: "relationship_missing_local",
9961
10703
  relationshipName: change.relationshipName,
9962
10704
  relationshipType: change.relationshipType,
9963
10705
  details: `Relationship '${change.relationshipName}' exists in remote but not in snapshot`
9964
10706
  };
9965
10707
  case "relationship_modified": return {
9966
- typeName: change.typeName,
10708
+ tableName: change.tableName,
9967
10709
  kind: "relationship_mismatch",
9968
10710
  relationshipName: change.relationshipName,
9969
10711
  relationshipType: change.relationshipType,
9970
10712
  details: change.reason ?? `Relationship '${change.relationshipName}' differs between remote and snapshot`
9971
10713
  };
9972
10714
  case "permission_modified": return {
9973
- typeName: change.typeName,
10715
+ tableName: change.tableName,
9974
10716
  kind: "permission_mismatch",
9975
10717
  details: change.reason ?? "Permissions differ between remote and snapshot"
9976
10718
  };
9977
- case "type_scripts_modified": return {
9978
- typeName: change.typeName,
10719
+ case "table_scripts_modified": return {
10720
+ tableName: change.tableName,
9979
10721
  kind: "script_mismatch",
9980
10722
  details: change.reason ?? "Type-level scripts differ between remote and snapshot"
9981
10723
  };
@@ -9983,7 +10725,7 @@ function schemaDriftFromDiffChange(change) {
9983
10725
  }
9984
10726
  }
9985
10727
  function compareNormalizedRemoteWithSnapshot(remoteSnapshot, snapshot) {
9986
- return compareNormalizedSnapshots(remoteSnapshot, snapshot).changes.map(schemaDriftFromDiffChange);
10728
+ return compareSnapshots(remoteSnapshot, snapshot).changes.map(schemaDriftFromDiffChange);
9987
10729
  }
9988
10730
  /**
9989
10731
  * Format schema drifts for display
@@ -9995,12 +10737,12 @@ function formatSchemaDrifts(drifts) {
9995
10737
  const lines = [];
9996
10738
  const driftsByType = /* @__PURE__ */ new Map();
9997
10739
  for (const drift of drifts) {
9998
- const existing = driftsByType.get(drift.typeName) ?? [];
10740
+ const existing = driftsByType.get(drift.tableName) ?? [];
9999
10741
  existing.push(drift);
10000
- driftsByType.set(drift.typeName, existing);
10742
+ driftsByType.set(drift.tableName, existing);
10001
10743
  }
10002
- for (const [typeName, typeDrifts] of driftsByType) {
10003
- lines.push(` Type '${typeName}':`);
10744
+ for (const [tableName, typeDrifts] of driftsByType) {
10745
+ lines.push(` Type '${tableName}':`);
10004
10746
  for (const drift of typeDrifts) if (drift.fieldName) lines.push(` - Field '${drift.fieldName}': ${drift.details}`);
10005
10747
  else if (drift.indexName) lines.push(` - Index '${drift.indexName}': ${drift.details}`);
10006
10748
  else if (drift.fileName) lines.push(` - File '${drift.fileName}': ${drift.details}`);
@@ -10014,12 +10756,7 @@ function formatSchemaDrifts(drifts) {
10014
10756
 
10015
10757
  //#endregion
10016
10758
  //#region src/cli/commands/tailordb/migrate/file-state.ts
10017
- const MIGRATION_FILE_KINDS = [
10018
- "schema",
10019
- "diff",
10020
- "migrate",
10021
- "db"
10022
- ];
10759
+ const MIGRATION_FILE_KINDS = Object.keys(MIGRATION_FILE_NAMES);
10023
10760
  function getMigrationArtifactNumbers(migrationsDir) {
10024
10761
  if (!fs$2.existsSync(migrationsDir)) return [];
10025
10762
  return fs$2.readdirSync(migrationsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && MIGRATION_NUMBER_PATTERN.test(entry.name)).map((entry) => Number.parseInt(entry.name, 10)).filter((migrationNumber) => MIGRATION_FILE_KINDS.some((kind) => fs$2.existsSync(getMigrationFilePath(migrationsDir, migrationNumber, kind)))).toSorted((a, b) => a - b);
@@ -10391,7 +11128,7 @@ function convertOperandToProto(operand) {
10391
11128
  function generateAllTypeManifestsFromSnapshot(snapshot, options = {}) {
10392
11129
  const manifests = /* @__PURE__ */ new Map();
10393
11130
  const { executorUsedTypes, ...baseOptions } = options;
10394
- for (const [typeName, snapshotType] of Object.entries(snapshot.types)) {
11131
+ for (const [typeName, snapshotType] of Object.entries(snapshot.tables)) {
10395
11132
  const typeOptions = {
10396
11133
  ...baseOptions,
10397
11134
  subscribed: executorUsedTypes?.has(typeName) ?? false
@@ -10407,7 +11144,7 @@ function generateAllTypeManifestsFromSnapshot(snapshot, options = {}) {
10407
11144
  * @returns {SnapshotTypeComparison} Comparison result
10408
11145
  */
10409
11146
  function compareSnapshotWithRemote(snapshot, existingTypeNames) {
10410
- const snapshotTypeNames = new Set(Object.keys(snapshot.types));
11147
+ const snapshotTypeNames = new Set(Object.keys(snapshot.tables));
10411
11148
  const creates = [];
10412
11149
  const updates = [];
10413
11150
  const deletes = [];
@@ -10531,6 +11268,8 @@ function protoGqlOperand(operand) {
10531
11268
  * - `field_added` with `required: true`: relax to `required: false`.
10532
11269
  * - `field_modified` optional→required, unique constraint added, enum
10533
11270
  * value removed: keep the looser side until Post-phase.
11271
+ * - `field_renamed`: keep the old field (readable by migrate.ts) and relax
11272
+ * the new field's required/unique constraints until Post-phase.
10534
11273
  * - `field_type_modified`: keep the complete previous field config until
10535
11274
  * Post-phase so migrate.ts runs against the previous type contract.
10536
11275
  *
@@ -10540,8 +11279,11 @@ function protoGqlOperand(operand) {
10540
11279
  * - `index_modified` that gains a unique constraint or re-points a unique
10541
11280
  * index at different fields: keep the previous definition until Post-phase.
10542
11281
  *
10543
- * Type-level deletions (`type_removed`) are handled by the deploy flow,
10544
- * which retains the type until Post-phase rather than via this module.
11282
+ * Type-level deletions (`table_removed`) and renames (`table_renamed`) are
11283
+ * handled by the deploy flow rather than via this module: the old type is
11284
+ * retained until Post-phase, and a renamed type's new type is created with
11285
+ * its full constraints in the Pre-phase (the copy script writes complete
11286
+ * rows, so nothing needs relaxing).
10545
11287
  *
10546
11288
  * Post-phase then sends the final schema, after migrate.ts has had a chance
10547
11289
  * to fix up data.
@@ -10553,7 +11295,8 @@ const PRE_MIGRATION_FIELD_KINDS = /* @__PURE__ */ new Set([
10553
11295
  "field_added",
10554
11296
  "field_modified",
10555
11297
  "field_type_modified",
10556
- "field_removed"
11298
+ "field_removed",
11299
+ "field_renamed"
10557
11300
  ]);
10558
11301
  /**
10559
11302
  * Type guard: is the change a field-level change that needs pre-migration
@@ -10599,16 +11342,16 @@ function createPreMigrationSnapshotType(snapshotType, typeChanges, typeScriptsCh
10599
11342
  /**
10600
11343
  * Build a map of field changes that require pre-migration schema adjustment.
10601
11344
  * @param {PendingMigration[]} pendingMigrations - Pending migrations to scan
10602
- * @returns {PreMigrationChangesMap} Map of changes keyed by typeName/fieldName
11345
+ * @returns {PreMigrationChangesMap} Map of changes keyed by tableName/fieldName
10603
11346
  */
10604
11347
  function buildPreMigrationChangesMap(pendingMigrations) {
10605
11348
  const map = /* @__PURE__ */ new Map();
10606
11349
  for (const migration of pendingMigrations) for (const change of migration.diff.changes) {
10607
11350
  if (!isPreMigrationFieldChange(change)) continue;
10608
11351
  if (!change.fieldName) continue;
10609
- const perType = map.get(change.typeName) ?? /* @__PURE__ */ new Map();
11352
+ const perType = map.get(change.tableName) ?? /* @__PURE__ */ new Map();
10610
11353
  perType.set(change.fieldName, change);
10611
- map.set(change.typeName, perType);
11354
+ map.set(change.tableName, perType);
10612
11355
  }
10613
11356
  return map;
10614
11357
  }
@@ -10631,6 +11374,15 @@ function applyPreMigrationFieldAdjustments(fields, typeChanges) {
10631
11374
  fields[fieldName] = convertFieldConfigToProto(change.before);
10632
11375
  continue;
10633
11376
  }
11377
+ if (change.kind === "field_renamed") {
11378
+ fields[change.previousFieldName] = convertFieldConfigToProto(change.before);
11379
+ const newField = fields[fieldName];
11380
+ if (newField) {
11381
+ if (change.after.required) newField.required = false;
11382
+ if (change.after.unique ?? false) newField.unique = false;
11383
+ }
11384
+ continue;
11385
+ }
10634
11386
  const field = fields[fieldName];
10635
11387
  if (!field) continue;
10636
11388
  if (change.kind === "field_added") {
@@ -10662,17 +11414,17 @@ function applyPreMigrationFieldAdjustments(fields, typeChanges) {
10662
11414
  * Build a map of type-level index changes that require pre-migration schema
10663
11415
  * adjustment (the breaking ones — see {@link isBreakingIndexChange}).
10664
11416
  * @param {PendingMigration[]} pendingMigrations - Pending migrations to scan
10665
- * @returns {PreMigrationIndexChangesMap} Map of changes keyed by typeName/indexName
11417
+ * @returns {PreMigrationIndexChangesMap} Map of changes keyed by tableName/indexName
10666
11418
  */
10667
11419
  function buildPreMigrationIndexChangesMap(pendingMigrations) {
10668
11420
  const map = /* @__PURE__ */ new Map();
10669
11421
  for (const migration of pendingMigrations) for (const change of migration.diff.changes) {
10670
11422
  if (change.kind !== "index_added" && change.kind !== "index_modified") continue;
10671
11423
  const before = change.kind === "index_modified" ? change.before : void 0;
10672
- if (!isBreakingIndexChange(change.typeName, change.indexName, before, change.after)) continue;
10673
- const perType = map.get(change.typeName) ?? /* @__PURE__ */ new Map();
11424
+ if (!isBreakingIndexChange(change.tableName, change.indexName, before, change.after)) continue;
11425
+ const perType = map.get(change.tableName) ?? /* @__PURE__ */ new Map();
10674
11426
  perType.set(change.indexName, change);
10675
- map.set(change.typeName, perType);
11427
+ map.set(change.tableName, perType);
10676
11428
  }
10677
11429
  return map;
10678
11430
  }
@@ -10841,8 +11593,8 @@ function appliedConfiguredDisabledGqlOperations(remoteDisabled, configuredDisabl
10841
11593
  function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
10842
11594
  const remoteTypesByName = new Map(remoteTypes.map((type) => [type.name, type]));
10843
11595
  const configuredDisabled = configuredDisabledGqlOperations(gqlOperations);
10844
- const types = {};
10845
- for (const [typeName, type] of Object.entries(snapshot.types)) {
11596
+ const tables = {};
11597
+ for (const [typeName, type] of Object.entries(snapshot.tables)) {
10846
11598
  const settings = { ...type.settings };
10847
11599
  const remoteSettings = remoteTypesByName.get(typeName)?.schema?.settings;
10848
11600
  if (type.settings?.publishEvents === void 0 && remoteSettings?.publishRecordEvents) settings.publishEvents = true;
@@ -10854,11 +11606,11 @@ function deployComparableSnapshot(snapshot, remoteTypes, gqlOperations) {
10854
11606
  const comparableSettings = definedWhenNotEmpty(settings);
10855
11607
  if (comparableSettings) comparableType.settings = comparableSettings;
10856
11608
  else delete comparableType.settings;
10857
- types[typeName] = comparableType;
11609
+ tables[typeName] = comparableType;
10858
11610
  }
10859
11611
  return {
10860
11612
  ...snapshot,
10861
- types
11613
+ tables
10862
11614
  };
10863
11615
  }
10864
11616
  /**
@@ -11431,8 +12183,12 @@ async function detectPendingMigrations(client, workspaceId, namespacesWithMigrat
11431
12183
  }
11432
12184
  if (diff.scriptSkipped) {
11433
12185
  const migrationLabel = `${namespace}/${formatMigrationNumber(file.number)}`;
11434
- if (hasScript) logger.warn(`Migration ${migrationLabel} has both a skip acknowledgment and migrate.ts; executing migrate.ts.`);
11435
- else logger.info(`Migration ${migrationLabel} runs without a script (skip acknowledged at ${diff.scriptSkipped.acknowledgedAt}: ${diff.scriptSkipped.reason})`);
12186
+ if (hasScript) throw new Error(`Migration ${migrationLabel} has both a --no-script skip acknowledgment and migrate.ts.\nTo resolve, either:\n - Keep the script and clear the stale acknowledgment: ${formatMigrationScriptCommand({
12187
+ migrationNumber: file.number,
12188
+ namespace,
12189
+ configPath
12190
+ })}\n - Or keep the skip: delete migrate.ts`);
12191
+ logger.info(`Migration ${migrationLabel} runs without a script (skip acknowledged at ${diff.scriptSkipped.acknowledgedAt}: ${diff.scriptSkipped.reason})`);
11436
12192
  }
11437
12193
  pendingMigrations.push({
11438
12194
  number: file.number,
@@ -11848,17 +12604,20 @@ async function applyTailorDB(client, result, phase = "create-update") {
11848
12604
  */
11849
12605
  function getAffectedTypeNames(migration) {
11850
12606
  const typeNames = /* @__PURE__ */ new Set();
11851
- for (const change of migration.diff.changes) typeNames.add(change.typeName);
12607
+ for (const change of migration.diff.changes) typeNames.add(change.tableName);
11852
12608
  return typeNames;
11853
12609
  }
11854
12610
  /**
11855
- * Get the set of type names to be deleted by a migration
12611
+ * Get the set of type names to be deleted by a migration. A renamed type's
12612
+ * old name is included: the old type survives the Pre-phase and the script
12613
+ * (which copies its rows into the new type), then is dropped here.
11856
12614
  * @param {PendingMigration} migration - Pending migration
11857
12615
  * @returns {Set<string>} Set of type names to delete
11858
12616
  */
11859
12617
  function getDeletedTypeNames(migration) {
11860
12618
  const typeNames = /* @__PURE__ */ new Set();
11861
- for (const change of migration.diff.changes) if (change.kind === "type_removed") typeNames.add(change.typeName);
12619
+ for (const change of migration.diff.changes) if (change.kind === "table_removed") typeNames.add(change.tableName);
12620
+ else if (change.kind === "table_renamed") typeNames.add(change.previousTableName);
11862
12621
  return typeNames;
11863
12622
  }
11864
12623
  /**
@@ -11911,11 +12670,11 @@ const migrationSnapshotCache = {
11911
12670
  return snapshot;
11912
12671
  }
11913
12672
  };
11914
- function buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes, typeChanges) {
11915
- const snapshotType = migrationSnapshotCache.load(migration).types[typeName];
12673
+ function buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes, typeChanges) {
12674
+ const snapshotType = migrationSnapshotCache.load(migration).tables[tableName];
11916
12675
  if (!snapshotType) return void 0;
11917
12676
  const input = tailorDBInputs.find((i) => i.namespace === migration.namespace);
11918
- const typeScriptsChange = migration.diff.changes.find((change) => change.kind === "type_scripts_modified" && change.typeName === typeName);
12677
+ const typeScriptsChange = migration.diff.changes.find((change) => change.kind === "table_scripts_modified" && change.tableName === tableName);
11919
12678
  return generateTailorDBTypeManifestFromSnapshot(typeChanges ? createPreMigrationSnapshotType(snapshotType, typeChanges, typeScriptsChange) : snapshotType, {
11920
12679
  subscribed: executorUsedTypes.has(snapshotType.name),
11921
12680
  namespaceGqlOperations: input?.config.gqlOperations
@@ -11947,30 +12706,30 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
11947
12706
  const affectedTypes = getAffectedTypeNames(migration);
11948
12707
  const createdBeforeMigration = new Set(processedTypes.created);
11949
12708
  for (const create of changeSet.type.creates) {
11950
- const typeName = create.request.tailordbType?.name;
11951
- if (!typeName || !affectedTypes.has(typeName) || createdBeforeMigration.has(typeName)) continue;
11952
- const typeChanges = preMigrationChanges.get(typeName);
11953
- const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes, typeChanges);
12709
+ const tableName = create.request.tailordbType?.name;
12710
+ if (!tableName || !affectedTypes.has(tableName) || createdBeforeMigration.has(tableName)) continue;
12711
+ const typeChanges = preMigrationChanges.get(tableName);
12712
+ const snapshotType = buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes, typeChanges);
11954
12713
  if (!snapshotType) continue;
11955
12714
  const clonedRequest = structuredClone(create.request);
11956
12715
  clonedRequest.tailordbType = snapshotType;
11957
12716
  if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
11958
- const indexChanges = preMigrationIndexChanges.get(typeName);
12717
+ const indexChanges = preMigrationIndexChanges.get(tableName);
11959
12718
  if (indexChanges && indexChanges.size > 0 && clonedRequest.tailordbType.schema?.indexes) applyPreMigrationIndexAdjustments(clonedRequest.tailordbType.schema.indexes, indexChanges);
11960
- processedTypes.created.add(typeName);
12719
+ processedTypes.created.add(tableName);
11961
12720
  await client.createTailorDBType(clonedRequest);
11962
12721
  }
11963
12722
  for (const create of changeSet.type.creates) {
11964
- const typeName = create.request.tailordbType?.name;
11965
- if (!typeName || !affectedTypes.has(typeName) || !createdBeforeMigration.has(typeName)) continue;
11966
- const typeChanges = preMigrationChanges.get(typeName);
11967
- const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes, typeChanges);
12723
+ const tableName = create.request.tailordbType?.name;
12724
+ if (!tableName || !affectedTypes.has(tableName) || !createdBeforeMigration.has(tableName)) continue;
12725
+ const typeChanges = preMigrationChanges.get(tableName);
12726
+ const snapshotType = buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes, typeChanges);
11968
12727
  if (!snapshotType) continue;
11969
12728
  const clonedTypeRequest = structuredClone(snapshotType);
11970
12729
  if (typeChanges && typeChanges.size > 0 && clonedTypeRequest.schema?.fields) applyPreMigrationFieldAdjustments(clonedTypeRequest.schema.fields, typeChanges);
11971
- const indexChanges = preMigrationIndexChanges.get(typeName);
12730
+ const indexChanges = preMigrationIndexChanges.get(tableName);
11972
12731
  if (indexChanges && indexChanges.size > 0 && clonedTypeRequest.schema?.indexes) applyPreMigrationIndexAdjustments(clonedTypeRequest.schema.indexes, indexChanges);
11973
- processedTypes.updated.add(typeName);
12732
+ processedTypes.updated.add(tableName);
11974
12733
  await client.updateTailorDBType({
11975
12734
  workspaceId: create.request.workspaceId,
11976
12735
  namespaceName: create.request.namespaceName,
@@ -11978,17 +12737,17 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
11978
12737
  });
11979
12738
  }
11980
12739
  for (const update of changeSet.type.updates) {
11981
- const typeName = update.request.tailordbType?.name;
11982
- if (!typeName || !affectedTypes.has(typeName)) continue;
11983
- const typeChanges = preMigrationChanges.get(typeName);
11984
- const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes, typeChanges);
12740
+ const tableName = update.request.tailordbType?.name;
12741
+ if (!tableName || !affectedTypes.has(tableName)) continue;
12742
+ const typeChanges = preMigrationChanges.get(tableName);
12743
+ const snapshotType = buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes, typeChanges);
11985
12744
  if (!snapshotType) continue;
11986
12745
  const clonedRequest = structuredClone(update.request);
11987
12746
  clonedRequest.tailordbType = snapshotType;
11988
12747
  if (typeChanges && typeChanges.size > 0 && clonedRequest.tailordbType.schema?.fields) applyPreMigrationFieldAdjustments(clonedRequest.tailordbType.schema.fields, typeChanges);
11989
- const indexChanges = preMigrationIndexChanges.get(typeName);
12748
+ const indexChanges = preMigrationIndexChanges.get(tableName);
11990
12749
  if (indexChanges && indexChanges.size > 0 && clonedRequest.tailordbType.schema?.indexes) applyPreMigrationIndexAdjustments(clonedRequest.tailordbType.schema.indexes, indexChanges);
11991
- processedTypes.updated.add(typeName);
12750
+ processedTypes.updated.add(tableName);
11992
12751
  await client.updateTailorDBType(clonedRequest);
11993
12752
  }
11994
12753
  if (!processedTypes.gqlPermissionsProcessed.has(migration.namespace)) {
@@ -11996,12 +12755,12 @@ async function executeSingleMigrationPrePhase(client, changeSet, migration, tail
11996
12755
  const gqlPermissionUpdatesForNamespace = changeSet.gqlPermission.updates.filter((update) => update.request.namespaceName === migration.namespace);
11997
12756
  const gqlPermissionTypeNames = new Set(gqlPermissionCreatesForNamespace.map((create) => create.name));
11998
12757
  const missingTypeCreates = changeSet.type.creates.filter((create) => {
11999
- const typeName = create.request.tailordbType?.name;
12000
- return create.request.namespaceName === migration.namespace && typeName && gqlPermissionTypeNames.has(typeName) && !processedTypes.created.has(typeName);
12758
+ const tableName = create.request.tailordbType?.name;
12759
+ return create.request.namespaceName === migration.namespace && tableName && gqlPermissionTypeNames.has(tableName) && !processedTypes.created.has(tableName);
12001
12760
  });
12002
12761
  if (missingTypeCreates.length > 0) for (const create of missingTypeCreates) {
12003
- const typeName = create.request.tailordbType?.name;
12004
- if (typeName) processedTypes.created.add(typeName);
12762
+ const tableName = create.request.tailordbType?.name;
12763
+ if (tableName) processedTypes.created.add(tableName);
12005
12764
  await client.createTailorDBType(create.request);
12006
12765
  }
12007
12766
  processedTypes.gqlPermissionsProcessed.add(migration.namespace);
@@ -12042,9 +12801,9 @@ async function executeSingleMigrationPostPhase(client, changeSet, migration, tai
12042
12801
  const affectedTypes = getAffectedTypeNames(migration);
12043
12802
  try {
12044
12803
  for (const create of changeSet.type.creates) {
12045
- const typeName = create.request.tailordbType?.name;
12046
- if (!typeName || !affectedTypes.has(typeName) || !adjustedTypes.has(typeName)) continue;
12047
- const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes);
12804
+ const tableName = create.request.tailordbType?.name;
12805
+ if (!tableName || !affectedTypes.has(tableName) || !adjustedTypes.has(tableName)) continue;
12806
+ const snapshotType = buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes);
12048
12807
  if (!snapshotType) continue;
12049
12808
  await client.updateTailorDBType({
12050
12809
  workspaceId: create.request.workspaceId,
@@ -12053,9 +12812,9 @@ async function executeSingleMigrationPostPhase(client, changeSet, migration, tai
12053
12812
  });
12054
12813
  }
12055
12814
  for (const update of changeSet.type.updates) {
12056
- const typeName = update.request.tailordbType?.name;
12057
- if (!typeName || !affectedTypes.has(typeName) || !adjustedTypes.has(typeName)) continue;
12058
- const snapshotType = buildSnapshotTypeManifest(migration, typeName, tailorDBInputs, executorUsedTypes);
12815
+ const tableName = update.request.tailordbType?.name;
12816
+ if (!tableName || !affectedTypes.has(tableName) || !adjustedTypes.has(tableName)) continue;
12817
+ const snapshotType = buildSnapshotTypeManifest(migration, tableName, tailorDBInputs, executorUsedTypes);
12059
12818
  if (!snapshotType) continue;
12060
12819
  await client.updateTailorDBType({
12061
12820
  workspaceId: update.request.workspaceId,
@@ -12073,17 +12832,17 @@ async function executeSingleMigrationPostPhaseDeletions(client, changeSet, migra
12073
12832
  const gqlPermissionsToDelete = changeSet.gqlPermission.deletes.filter((del) => {
12074
12833
  const permKey = `${del.request.namespaceName}/${del.name}`;
12075
12834
  if (deletedResources.gqlPermissions.has(permKey)) return false;
12076
- const typeName = del.name;
12077
- return deletedTypeNames.has(typeName);
12835
+ const tableName = del.name;
12836
+ return deletedTypeNames.has(tableName);
12078
12837
  });
12079
12838
  for (const del of gqlPermissionsToDelete) {
12080
12839
  await client.deleteTailorDBGQLPermission(del.request);
12081
12840
  deletedResources.gqlPermissions.add(`${del.request.namespaceName}/${del.name}`);
12082
12841
  }
12083
12842
  const typesToDelete = changeSet.type.deletes.filter((del) => {
12084
- const typeName = del.name;
12085
- if (!typeName || deletedResources.types.has(typeName)) return false;
12086
- return deletedTypeNames.has(typeName);
12843
+ const tableName = del.name;
12844
+ if (!tableName || deletedResources.types.has(tableName)) return false;
12845
+ return deletedTypeNames.has(tableName);
12087
12846
  });
12088
12847
  for (const del of typesToDelete) {
12089
12848
  await client.deleteTailorDBType(del.request);
@@ -12122,34 +12881,40 @@ async function rollbackSingleMigrationPrePhase(client, changeSet, migration, wor
12122
12881
  }
12123
12882
  const input = tailorDBInputs.find((i) => i.namespace === migration.namespace);
12124
12883
  logger.warn(`Migration ${migration.namespace}/${formatMigrationNumber(migration.number)} failed; rolling back its pre-migration schema changes.`);
12125
- for (const typeName of rollbackTypes) {
12126
- const priorType = priorSnapshot.types[typeName];
12127
- try {
12128
- if (priorType) {
12129
- const manifest = generateTailorDBTypeManifestFromSnapshot(priorType, {
12130
- subscribed: executorUsedTypes.has(priorType.name),
12131
- namespaceGqlOperations: input?.config.gqlOperations
12132
- });
12133
- await client.updateTailorDBType({
12134
- workspaceId,
12135
- namespaceName: migration.namespace,
12136
- tailordbType: manifest
12137
- });
12138
- } else {
12139
- await client.deleteTailorDBGQLPermission({
12140
- workspaceId,
12141
- namespaceName: migration.namespace,
12142
- typeName
12143
- }).catch(() => void 0);
12144
- await client.deleteTailorDBType({
12145
- workspaceId,
12146
- namespaceName: migration.namespace,
12147
- tailordbTypeName: typeName
12148
- });
12149
- }
12150
- } catch (rollbackError) {
12151
- logger.warn(`Failed to roll back type '${typeName}' in namespace '${migration.namespace}': ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
12152
- }
12884
+ const restoredTypes = [...rollbackTypes].flatMap((tableName) => {
12885
+ const priorType = priorSnapshot.tables[tableName];
12886
+ return priorType ? [{
12887
+ tableName,
12888
+ priorType
12889
+ }] : [];
12890
+ });
12891
+ const newTypes = [...rollbackTypes].filter((tableName) => !priorSnapshot.tables[tableName]);
12892
+ for (const { tableName, priorType } of restoredTypes) try {
12893
+ const manifest = generateTailorDBTypeManifestFromSnapshot(priorType, {
12894
+ subscribed: executorUsedTypes.has(priorType.name),
12895
+ namespaceGqlOperations: input?.config.gqlOperations
12896
+ });
12897
+ await client.updateTailorDBType({
12898
+ workspaceId,
12899
+ namespaceName: migration.namespace,
12900
+ tailordbType: manifest
12901
+ });
12902
+ } catch (rollbackError) {
12903
+ logger.warn(`Failed to roll back type '${tableName}' in namespace '${migration.namespace}': ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
12904
+ }
12905
+ for (const tableName of newTypes) try {
12906
+ await client.deleteTailorDBGQLPermission({
12907
+ workspaceId,
12908
+ namespaceName: migration.namespace,
12909
+ typeName: tableName
12910
+ }).catch(() => void 0);
12911
+ await client.deleteTailorDBType({
12912
+ workspaceId,
12913
+ namespaceName: migration.namespace,
12914
+ tailordbTypeName: tableName
12915
+ });
12916
+ } catch (rollbackError) {
12917
+ logger.warn(`Failed to roll back type '${tableName}' in namespace '${migration.namespace}': ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
12153
12918
  }
12154
12919
  }
12155
12920
  /**
@@ -12167,7 +12932,7 @@ async function planTailorDB(context) {
12167
12932
  const snapshot = migrationTestSnapshots?.get(tailordb.namespace);
12168
12933
  tailordbs.push(snapshot ? {
12169
12934
  ...input,
12170
- types: snapshot.types
12935
+ types: snapshot.tables
12171
12936
  } : input);
12172
12937
  }
12173
12938
  const executors = forRemoval ? [] : Object.values(await application.executorService?.loadExecutors() ?? {});
@@ -12179,10 +12944,10 @@ async function planTailorDB(context) {
12179
12944
  if (migrationTestSnapshots) {
12180
12945
  for (const namespace of migrationTestSnapshots.keys()) if (!tailordbs.some((tailordb) => tailordb.namespace === namespace)) throw new Error(`Migration test snapshot targets unknown TailorDB namespace "${namespace}".`);
12181
12946
  const namespaceByType = /* @__PURE__ */ new Map();
12182
- for (const tailordb of tailordbs) for (const typeName of Object.keys(tailordb.types)) {
12183
- const existingNamespace = namespaceByType.get(typeName);
12184
- if (existingNamespace) throw new Error(`Migration test snapshot has duplicate TailorDB type name "${typeName}" in namespaces "${existingNamespace}" and "${tailordb.namespace}".`);
12185
- namespaceByType.set(typeName, tailordb.namespace);
12947
+ for (const tailordb of tailordbs) for (const tableName of Object.keys(tailordb.types)) {
12948
+ const existingNamespace = namespaceByType.get(tableName);
12949
+ if (existingNamespace) throw new Error(`Migration test snapshot has duplicate TailorDB type name "${tableName}" in namespaces "${existingNamespace}" and "${tailordb.namespace}".`);
12950
+ namespaceByType.set(tableName, tailordb.namespace);
12186
12951
  }
12187
12952
  }
12188
12953
  const migrationConfig = getNamespacesWithMigrations(config, path.dirname(config.path));
@@ -12361,17 +13126,17 @@ async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, dele
12361
13126
  * Build one type's metadata write, carrying the dependency records that belong
12362
13127
  * to it. The type is what publishes record events, so the record lives there.
12363
13128
  * @param namespace - Namespace holding the type
12364
- * @param typeName - Type name
13129
+ * @param tableName - Type name
12365
13130
  * @param explicitPublishEvents - `publishEvents` declared on the type, if any
12366
13131
  * @returns The type's metadata write
12367
13132
  */
12368
- const typeMetaRequest = async (namespace, typeName, explicitPublishEvents) => {
13133
+ const typeMetaRequest = async (namespace, tableName, explicitPublishEvents) => {
12369
13134
  return addDependencyRecords(await buildMetaRequest({
12370
- trn: tailorDBTypeTrn(workspaceId, namespace, typeName),
13135
+ trn: tailorDBTypeTrn(workspaceId, namespace, tableName),
12371
13136
  appName: appName ?? "",
12372
13137
  appId
12373
13138
  }), {
12374
- key: eventSourceKey.tailorDBType(namespace, typeName),
13139
+ key: eventSourceKey.tailorDBType(namespace, tableName),
12375
13140
  dependentApps,
12376
13141
  runAppIds,
12377
13142
  pinned: explicitPublishEvents !== void 0
@@ -12390,46 +13155,46 @@ async function planTypes(client, workspaceId, tailordbs, executorUsedTypes, dele
12390
13155
  };
12391
13156
  for (const tailordb of tailordbs) {
12392
13157
  const types = filteredTypesByNamespace?.get(tailordb.namespace) ?? tailordb.types;
12393
- for (const [typeName, type] of Object.entries(types)) assertNoPublishEventsConflict({
13158
+ for (const [tableName, type] of Object.entries(types)) assertNoPublishEventsConflict({
12394
13159
  explicit: type.settings?.publishEvents,
12395
- subscribed: executorUsedTypes.has(typeName),
12396
- conflict: publishEventsConflict.tailorDBType(typeName)
13160
+ subscribed: executorUsedTypes.has(tableName),
13161
+ conflict: publishEventsConflict.tailorDBType(tableName)
12397
13162
  });
12398
13163
  }
12399
13164
  for (const tailordb of tailordbs) {
12400
13165
  const existingTypes = await fetchTypes(tailordb.namespace);
12401
13166
  const existingTypesMap = new Map(existingTypes.map((type) => [type.name, type]));
12402
13167
  const types = filteredTypesByNamespace?.get(tailordb.namespace) ?? tailordb.types;
12403
- const typeMeta = (typeName) => typeMetaRequest(tailordb.namespace, typeName, types[typeName]?.settings?.publishEvents);
12404
- for (const [typeName, tailordbTypeSnapshot] of Object.entries(types)) {
13168
+ const typeMeta = (tableName) => typeMetaRequest(tailordb.namespace, tableName, types[tableName]?.settings?.publishEvents);
13169
+ for (const [tableName, tailordbTypeSnapshot] of Object.entries(types)) {
12405
13170
  const tailordbType = generateTailorDBTypeManifestFromSnapshot(tailordbTypeSnapshot, {
12406
- subscribed: executorUsedTypes.has(typeName),
13171
+ subscribed: executorUsedTypes.has(tableName),
12407
13172
  namespaceGqlOperations: tailordb.config.gqlOperations
12408
13173
  });
12409
- const existingType = existingTypesMap.get(typeName);
13174
+ const existingType = existingTypesMap.get(tableName);
12410
13175
  if (existingType) {
12411
13176
  if (!forceApplyAll && areNormalizedEqual(normalizeComparableTailorDBType(existingType), normalizeComparableTailorDBType(tailordbType))) changeSet.unchanged.push({
12412
- name: typeName,
12413
- metaRequest: await typeMeta(typeName)
13177
+ name: tableName,
13178
+ metaRequest: await typeMeta(tableName)
12414
13179
  });
12415
13180
  else changeSet.updates.push({
12416
- name: typeName,
13181
+ name: tableName,
12417
13182
  request: {
12418
13183
  workspaceId,
12419
13184
  namespaceName: tailordb.namespace,
12420
13185
  tailordbType
12421
13186
  },
12422
- metaRequest: await typeMeta(typeName)
13187
+ metaRequest: await typeMeta(tableName)
12423
13188
  });
12424
- existingTypesMap.delete(typeName);
13189
+ existingTypesMap.delete(tableName);
12425
13190
  } else changeSet.creates.push({
12426
- name: typeName,
13191
+ name: tableName,
12427
13192
  request: {
12428
13193
  workspaceId,
12429
13194
  namespaceName: tailordb.namespace,
12430
13195
  tailordbType
12431
13196
  },
12432
- metaRequest: await typeMeta(typeName)
13197
+ metaRequest: await typeMeta(tableName)
12433
13198
  });
12434
13199
  }
12435
13200
  existingTypesMap.forEach((_type, name) => {
@@ -12561,29 +13326,29 @@ async function planGqlPermissions(client, workspaceId, tailordbs, deletedService
12561
13326
  existingNameSet.add(gqlPermission.typeName);
12562
13327
  });
12563
13328
  const types = tailordb.types;
12564
- for (const [typeName, typeEntry] of Object.entries(types)) {
13329
+ for (const [tableName, typeEntry] of Object.entries(types)) {
12565
13330
  const gqlPermission = typeEntry.permissions?.gql;
12566
13331
  if (!gqlPermission) continue;
12567
13332
  const desiredPermission = protoGqlPermission(gqlPermission);
12568
- const existingPermission = existingGqlPermissions.find((entry) => entry.typeName === typeName);
12569
- if (existingNameSet.has(typeName)) {
12570
- if (!forceApplyAll && existingPermission && areNormalizedEqual(normalizeComparableGqlPermission(existingPermission.permission), normalizeComparableGqlPermission(desiredPermission))) changeSet.unchanged.push({ name: typeName });
13333
+ const existingPermission = existingGqlPermissions.find((entry) => entry.typeName === tableName);
13334
+ if (existingNameSet.has(tableName)) {
13335
+ if (!forceApplyAll && existingPermission && areNormalizedEqual(normalizeComparableGqlPermission(existingPermission.permission), normalizeComparableGqlPermission(desiredPermission))) changeSet.unchanged.push({ name: tableName });
12571
13336
  else changeSet.updates.push({
12572
- name: typeName,
13337
+ name: tableName,
12573
13338
  request: {
12574
13339
  workspaceId,
12575
13340
  namespaceName: tailordb.namespace,
12576
- typeName,
13341
+ typeName: tableName,
12577
13342
  permission: desiredPermission
12578
13343
  }
12579
13344
  });
12580
- existingNameSet.delete(typeName);
13345
+ existingNameSet.delete(tableName);
12581
13346
  } else changeSet.creates.push({
12582
- name: typeName,
13347
+ name: tableName,
12583
13348
  request: {
12584
13349
  workspaceId,
12585
13350
  namespaceName: tailordb.namespace,
12586
- typeName,
13351
+ typeName: tableName,
12587
13352
  permission: desiredPermission
12588
13353
  }
12589
13354
  });
@@ -15827,7 +16592,7 @@ async function planDeploymentTarget(params) {
15827
16592
  const snapshot = migrationTestSnapshots?.get(service.namespace);
15828
16593
  return snapshot ? {
15829
16594
  ...service,
15830
- types: snapshot.types,
16595
+ types: snapshot.tables,
15831
16596
  typeSourceInfo: {}
15832
16597
  } : service;
15833
16598
  });
@@ -16364,6 +17129,8 @@ function adjustApplicationForMigrationTest(application, internalContext) {
16364
17129
  async function deployInternal(options, cliContext, internalContext) {
16365
17130
  return withSpan("deploy", async (rootSpan) => {
16366
17131
  rootSpan.setAttribute("deploy.dry_run", options?.dryRun ?? false);
17132
+ beginUserModuleRun();
17133
+ beginWaitPointScope();
16367
17134
  const configPaths = parseDeployConfigPaths(options?.configPath);
16368
17135
  const dryRun = options?.dryRun ?? false;
16369
17136
  const buildOnly = options?.buildOnly ?? parseBoolean(process.env.TAILOR_DEPLOY_BUILD_ONLY) === true;
@@ -20417,6 +21184,249 @@ async function openInConfiguredEditor(filePath) {
20417
21184
  return await openInEditor(filePath, editor);
20418
21185
  }
20419
21186
 
21187
+ //#endregion
21188
+ //#region src/cli/commands/tailordb/migrate/expand-contract.ts
21189
+ /**
21190
+ * Planning for field type changes that move values through a temporary field.
21191
+ *
21192
+ * A change that cannot be applied in place is split into two migrations: the
21193
+ * first adds a temporary field and converts values into it, the second renames
21194
+ * that field back over the original.
21195
+ */
21196
+ /** Longest field name the platform accepts. */
21197
+ const MAX_FIELD_NAME_LENGTH = 63;
21198
+ const TEMP_FIELD_SUFFIX = "Migrate";
21199
+ /**
21200
+ * Key identifying a field across snapshots and user-supplied options.
21201
+ * @param tableName - Name of the type holding the field
21202
+ * @param fieldName - Name of the field
21203
+ * @returns Key in `Type.field` form
21204
+ */
21205
+ function fieldKey(tableName, fieldName) {
21206
+ return `${tableName}.${fieldName}`;
21207
+ }
21208
+ /**
21209
+ * Derive the temporary field name to carry a field's converted values.
21210
+ *
21211
+ * Field names cannot hold an underscore, so the suffix is camelCase and a
21212
+ * collision is resolved by an ordinal rather than a separator.
21213
+ * @param fieldName - Field being converted
21214
+ * @param taken - Field names already in use for the same type
21215
+ * @returns Unused temporary field name
21216
+ * @throws {Error} When no candidate fits within the platform's length limit
21217
+ */
21218
+ function buildTempFieldName(fieldName, taken) {
21219
+ const base = `${fieldName}${TEMP_FIELD_SUFFIX}`;
21220
+ for (let ordinal = 1; ordinal <= taken.size + 1; ordinal++) {
21221
+ const candidate = ordinal === 1 ? base : `${base}${ordinal}`;
21222
+ if (candidate.length > MAX_FIELD_NAME_LENGTH) break;
21223
+ if (!taken.has(candidate)) return candidate;
21224
+ }
21225
+ throw new Error(`Cannot derive a temporary field name for "${fieldName}": every candidate is taken or exceeds ${MAX_FIELD_NAME_LENGTH} characters.`);
21226
+ }
21227
+ /**
21228
+ * Whether a field type change can be carried by a generated migration pair.
21229
+ *
21230
+ * The single answer behind the prompt, the flag hint, and planning, so a run
21231
+ * cannot recommend a conversion it would then reject.
21232
+ * @param options - Snapshots and the field to test
21233
+ * @returns Whether the conversion can be generated
21234
+ */
21235
+ function canConvertField(options) {
21236
+ return getExpandContractEligibility(options).eligible;
21237
+ }
21238
+ /**
21239
+ * Explain whether a field can be carried by a generated migration pair.
21240
+ * @param options - Snapshots and the field to test
21241
+ * @returns Eligibility and, when ineligible, the reason
21242
+ */
21243
+ function getExpandContractEligibility(options) {
21244
+ const { previous, current, tableName, fieldName } = options;
21245
+ const before = previous.tables[tableName]?.fields[fieldName];
21246
+ const after = current.tables[tableName]?.fields[fieldName];
21247
+ if (!before || !after) return {
21248
+ eligible: false,
21249
+ reason: "the field does not exist in both schemas"
21250
+ };
21251
+ const fieldEligibility = getExpandContractFieldChangeEligibility(before, after);
21252
+ if (!fieldEligibility.eligible) return fieldEligibility;
21253
+ if (isFieldReferenced(previous.tables[tableName], fieldName) || isFieldReferenced(current.tables[tableName], fieldName)) return {
21254
+ eligible: false,
21255
+ reason: "another schema feature references the field"
21256
+ };
21257
+ return { eligible: true };
21258
+ }
21259
+ /**
21260
+ * Names a type already exposes, which a temporary field cannot reuse.
21261
+ * @param type - Type to enumerate
21262
+ * @returns Field, file, and relationship names
21263
+ */
21264
+ function typeMemberNames(type) {
21265
+ if (!type) return [];
21266
+ return [
21267
+ ...Object.keys(type.fields),
21268
+ ...Object.keys(type.files ?? {}),
21269
+ ...Object.keys(type.forwardRelationships ?? {}),
21270
+ ...Object.keys(type.backwardRelationships ?? {})
21271
+ ];
21272
+ }
21273
+ function staticPropertyName(key, computed) {
21274
+ if (!computed && key.type === "Identifier") return key.name;
21275
+ if (key.type === "Literal" && typeof key.value === "string") return key.value;
21276
+ if (key.type === "TemplateLiteral" && key.expressions.length === 0) return key.quasis[0]?.value.cooked ?? void 0;
21277
+ }
21278
+ const SCRIPT_CONTEXT_ARGUMENTS = /* @__PURE__ */ new Set([
21279
+ "input",
21280
+ "newRecord",
21281
+ "oldRecord",
21282
+ "invoker",
21283
+ "now"
21284
+ ]);
21285
+ function walkScriptAst(node, visit, ancestors = []) {
21286
+ if (!node) return;
21287
+ visit(node, ancestors);
21288
+ const nestedAncestors = [...ancestors, node];
21289
+ const record = node;
21290
+ for (const [key, value] of Object.entries(record)) {
21291
+ if (key === "type" || key === "parent") continue;
21292
+ if (Array.isArray(value)) {
21293
+ for (const item of value) if (item && typeof item === "object" && "type" in item) walkScriptAst(item, visit, nestedAncestors);
21294
+ } else if (value && typeof value === "object" && "type" in value) walkScriptAst(value, visit, nestedAncestors);
21295
+ }
21296
+ }
21297
+ function unwrapFunction(node) {
21298
+ return node.type === "ParenthesizedExpression" ? unwrapFunction(node.expression) : node;
21299
+ }
21300
+ function collectIssueBindings(program) {
21301
+ const issueBindings = /* @__PURE__ */ new Set(["__issues"]);
21302
+ walkScriptAst(program, (node) => {
21303
+ if (node.type !== "CallExpression") return;
21304
+ const callee = unwrapFunction(node.callee);
21305
+ if (callee.type !== "ArrowFunctionExpression" && callee.type !== "FunctionExpression" || node.arguments[1]?.type !== "Identifier" || node.arguments[1].name !== "__issues") return;
21306
+ const issueParameter = callee.params[1];
21307
+ if (issueParameter?.type === "Identifier") issueBindings.add(issueParameter.name);
21308
+ });
21309
+ let addedBinding;
21310
+ do {
21311
+ const previousSize = issueBindings.size;
21312
+ walkScriptAst(program, (node) => {
21313
+ if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.init?.type === "Identifier" && issueBindings.has(node.init.name)) issueBindings.add(node.id.name);
21314
+ });
21315
+ addedBinding = issueBindings.size > previousSize;
21316
+ } while (addedBinding);
21317
+ return issueBindings;
21318
+ }
21319
+ function isWrapperArgumentProperty(node, ancestors) {
21320
+ if (node.type !== "Property") return false;
21321
+ const propertyName = staticPropertyName(node.key, node.computed);
21322
+ if (!propertyName || !SCRIPT_CONTEXT_ARGUMENTS.has(propertyName)) return false;
21323
+ const object = ancestors.at(-1);
21324
+ const call = ancestors.at(-2);
21325
+ if (object?.type !== "ObjectExpression" || call?.type !== "CallExpression") return false;
21326
+ return call.arguments[0] === object && ["ArrowFunctionExpression", "FunctionExpression"].includes(unwrapFunction(call.callee).type);
21327
+ }
21328
+ function isWrapperParameterProperty(node, ancestors) {
21329
+ if (node.type !== "Property") return false;
21330
+ const propertyName = staticPropertyName(node.key, node.computed);
21331
+ if (!propertyName || !SCRIPT_CONTEXT_ARGUMENTS.has(propertyName)) return false;
21332
+ const pattern = ancestors.at(-1);
21333
+ if (pattern?.type !== "ObjectPattern") return false;
21334
+ for (let index = ancestors.length - 2; index >= 0; index--) {
21335
+ const candidate = ancestors[index];
21336
+ if (candidate && (candidate.type === "ArrowFunctionExpression" || candidate.type === "FunctionExpression") && candidate.params[0] === pattern) return ancestors.slice(0, index).some((ancestor) => ancestor.type === "CallExpression" && unwrapFunction(ancestor.callee) === candidate && ancestor.arguments[0]?.type === "ObjectExpression" && ancestor.arguments[0].properties.some((property) => property.type === "Property" && staticPropertyName(property.key, property.computed) === propertyName));
21337
+ }
21338
+ return false;
21339
+ }
21340
+ function isIssueMessage(node, ancestors, issueBindings) {
21341
+ const call = ancestors.at(-1);
21342
+ return call?.type === "CallExpression" && call.callee.type === "Identifier" && issueBindings.has(call.callee.name) && call.arguments.findIndex((argument) => argument === node) > 0;
21343
+ }
21344
+ function scriptReferencesField(script, fieldName) {
21345
+ try {
21346
+ const { program, errors } = parseSync("expand-contract-reference.js", script, { sourceType: "module" });
21347
+ if (errors.length > 0) return true;
21348
+ const issueBindings = collectIssueBindings(program);
21349
+ let referenced = false;
21350
+ walkScriptAst(program, (node, ancestors) => {
21351
+ if (referenced) return;
21352
+ if (node.type === "MemberExpression" && (staticPropertyName(node.property, node.computed) === fieldName || node.computed && staticPropertyName(node.property, true) === void 0)) referenced = true;
21353
+ else if (node.type === "Property" && (staticPropertyName(node.key, node.computed) === fieldName || node.computed && staticPropertyName(node.key, true) === void 0) && (node.computed || !isWrapperArgumentProperty(node, ancestors) && !isWrapperParameterProperty(node, ancestors))) referenced = true;
21354
+ else if (node.type === "CallExpression" && node.callee.type === "Identifier" && issueBindings.has(node.callee.name) && node.arguments[0] !== void 0 && node.arguments[0].type !== "SpreadElement" && staticPropertyName(node.arguments[0], true) === fieldName) referenced = true;
21355
+ else if ((node.type === "Literal" || node.type === "TemplateLiteral") && staticPropertyName(node, true) === fieldName && !isIssueMessage(node, ancestors, issueBindings)) referenced = true;
21356
+ });
21357
+ return referenced;
21358
+ } catch {
21359
+ return true;
21360
+ }
21361
+ }
21362
+ function permissionsReferenceField(permissions, fieldName) {
21363
+ if (!permissions) return false;
21364
+ return [...permissions.record ? Object.values(permissions.record).flatMap((policies) => policies) : [], ...permissions.gql ?? []].some((policy) => policy.conditions.some((condition) => {
21365
+ const [left, , right] = condition;
21366
+ return [left, right].some((operand) => isSnapshotFieldRefOperand(operand) && ("record" in operand && operand.record === fieldName || "newRecord" in operand && operand.newRecord === fieldName || "oldRecord" in operand && operand.oldRecord === fieldName));
21367
+ }));
21368
+ }
21369
+ /**
21370
+ * Whether anything other than the field list names this field.
21371
+ *
21372
+ * The pair moves values through a differently named field, and only the field
21373
+ * list is rewritten. An index, relationship, permission, or script naming the
21374
+ * field would keep pointing at the name the pair drops.
21375
+ * @param type - Type holding the field
21376
+ * @param fieldName - Field being converted
21377
+ * @returns Whether another part of the type names the field
21378
+ */
21379
+ function isFieldReferenced(type, fieldName) {
21380
+ if (!type) return false;
21381
+ if (Object.values(type.indexes ?? {}).some((index) => index.fields.includes(fieldName))) return true;
21382
+ if ([...Object.values(type.forwardRelationships ?? {}), ...Object.values(type.backwardRelationships ?? {})].some((relationship) => relationship.sourceField === fieldName || relationship.targetField === fieldName)) return true;
21383
+ if (permissionsReferenceField(type.permissions, fieldName)) return true;
21384
+ return [
21385
+ type.typeHookExpr?.create,
21386
+ type.typeHookExpr?.update,
21387
+ type.typeValidateExpr
21388
+ ].filter((script) => script !== void 0).some((script) => scriptReferencesField(script, fieldName));
21389
+ }
21390
+ /**
21391
+ * Split unsupported field type changes into the ones a migration pair can carry
21392
+ * and the ones that must still fail.
21393
+ * @param options - Snapshots, diff, and the changes the user approved
21394
+ * @returns Plans to generate and breaking changes to reject
21395
+ */
21396
+ function planExpandContract(options) {
21397
+ const { previous, current, diff, confirmed } = options;
21398
+ const plans = [];
21399
+ const planned = /* @__PURE__ */ new Set();
21400
+ for (const change of diff.changes) {
21401
+ if (change.kind !== "field_type_modified") continue;
21402
+ const key = fieldKey(change.tableName, change.fieldName);
21403
+ if (!confirmed.has(key)) continue;
21404
+ if (!canConvertField({
21405
+ previous,
21406
+ current,
21407
+ tableName: change.tableName,
21408
+ fieldName: change.fieldName
21409
+ })) continue;
21410
+ const taken = /* @__PURE__ */ new Set([
21411
+ ...typeMemberNames(previous.tables[change.tableName]),
21412
+ ...typeMemberNames(current.tables[change.tableName]),
21413
+ ...plans.filter((plan) => plan.tableName === change.tableName).map((plan) => plan.tempFieldName)
21414
+ ]);
21415
+ plans.push({
21416
+ tableName: change.tableName,
21417
+ fieldName: change.fieldName,
21418
+ tempFieldName: buildTempFieldName(change.fieldName, taken),
21419
+ before: change.before,
21420
+ after: change.after
21421
+ });
21422
+ planned.add(key);
21423
+ }
21424
+ return {
21425
+ plans,
21426
+ blocked: diff.breakingChanges.filter((change) => change.unsupported && !(change.fieldName && planned.has(fieldKey(change.tableName, change.fieldName))))
21427
+ };
21428
+ }
21429
+
20420
21430
  //#endregion
20421
21431
  //#region src/cli/commands/tailordb/migrate/db-types-generator.ts
20422
21432
  /**
@@ -20434,11 +21444,13 @@ function extractBreakingChangeFields(diff) {
20434
21444
  const optionalToRequired = /* @__PURE__ */ new Map();
20435
21445
  const addedRequiredFields = /* @__PURE__ */ new Map();
20436
21446
  const enumValueChanges = /* @__PURE__ */ new Map();
21447
+ const renamedFields = /* @__PURE__ */ new Map();
21448
+ const renamedTypes = /* @__PURE__ */ new Map();
20437
21449
  for (const change of diff.changes) if (change.kind === "field_modified" || change.kind === "field_type_modified") {
20438
21450
  const { before, after } = change;
20439
21451
  if (!before.required && after.required) {
20440
- if (!optionalToRequired.has(change.typeName)) optionalToRequired.set(change.typeName, /* @__PURE__ */ new Set());
20441
- assertDefined(optionalToRequired.get(change.typeName), "optionalToRequired entry missing").add(change.fieldName);
21452
+ if (!optionalToRequired.has(change.tableName)) optionalToRequired.set(change.tableName, /* @__PURE__ */ new Set());
21453
+ assertDefined(optionalToRequired.get(change.tableName), "optionalToRequired entry missing").add(change.fieldName);
20442
21454
  }
20443
21455
  if (before.type === "enum" && after.type === "enum" && before.allowedValues && after.allowedValues) {
20444
21456
  const beforeValues = before.allowedValues.map((v) => v.value);
@@ -20446,8 +21458,8 @@ function extractBreakingChangeFields(diff) {
20446
21458
  const beforeSet = new Set(beforeValues);
20447
21459
  const afterSet = new Set(afterValues);
20448
21460
  if (beforeValues.some((v) => !afterSet.has(v)) || afterValues.some((v) => !beforeSet.has(v))) {
20449
- if (!enumValueChanges.has(change.typeName)) enumValueChanges.set(change.typeName, /* @__PURE__ */ new Map());
20450
- assertDefined(enumValueChanges.get(change.typeName), "enumValueChanges entry missing").set(change.fieldName, {
21461
+ if (!enumValueChanges.has(change.tableName)) enumValueChanges.set(change.tableName, /* @__PURE__ */ new Map());
21462
+ assertDefined(enumValueChanges.get(change.tableName), "enumValueChanges entry missing").set(change.fieldName, {
20451
21463
  beforeValues,
20452
21464
  afterValues
20453
21465
  });
@@ -20456,33 +21468,55 @@ function extractBreakingChangeFields(diff) {
20456
21468
  } else if (change.kind === "field_added") {
20457
21469
  const { after } = change;
20458
21470
  if (after.required) {
20459
- if (!addedRequiredFields.has(change.typeName)) addedRequiredFields.set(change.typeName, /* @__PURE__ */ new Map());
20460
- assertDefined(addedRequiredFields.get(change.typeName), "addedRequiredFields entry missing").set(change.fieldName, after);
21471
+ if (!addedRequiredFields.has(change.tableName)) addedRequiredFields.set(change.tableName, /* @__PURE__ */ new Map());
21472
+ assertDefined(addedRequiredFields.get(change.tableName), "addedRequiredFields entry missing").set(change.fieldName, after);
20461
21473
  }
20462
- }
21474
+ } else if (change.kind === "field_renamed") {
21475
+ if (!renamedFields.has(change.tableName)) renamedFields.set(change.tableName, /* @__PURE__ */ new Map());
21476
+ assertDefined(renamedFields.get(change.tableName), "renamedFields entry missing").set(change.fieldName, change.after);
21477
+ } else if (change.kind === "table_renamed") renamedTypes.set(change.tableName, change.after);
20463
21478
  return {
20464
21479
  optionalToRequired,
20465
21480
  addedRequiredFields,
20466
- enumValueChanges
21481
+ enumValueChanges,
21482
+ renamedFields,
21483
+ clearedFields: /* @__PURE__ */ new Map(),
21484
+ renamedTypes
20467
21485
  };
20468
21486
  }
20469
21487
  /**
20470
21488
  * Generate the complete db.ts file content from a schema snapshot
20471
21489
  * @param {SchemaSnapshot} snapshot - Schema snapshot to generate types from
20472
21490
  * @param {MigrationDiff} [diff] - Optional migration diff for breaking change info
21491
+ * @param expandPlans - Field changes carried through temporary fields
20473
21492
  * @returns {string} Generated db.ts file contents
20474
21493
  */
20475
- function generateDbTypesFromSnapshot(snapshot, diff) {
20476
- const types = Object.values(snapshot.types);
20477
- if (types.length === 0) return generateEmptyDbTypes(snapshot.namespace);
21494
+ function generateDbTypesFromSnapshot(snapshot, diff, expandPlans = []) {
20478
21495
  const breakingChangeFields = diff ? extractBreakingChangeFields(diff) : {
20479
21496
  optionalToRequired: /* @__PURE__ */ new Map(),
20480
21497
  addedRequiredFields: /* @__PURE__ */ new Map(),
20481
- enumValueChanges: /* @__PURE__ */ new Map()
20482
- };
21498
+ enumValueChanges: /* @__PURE__ */ new Map(),
21499
+ renamedFields: /* @__PURE__ */ new Map(),
21500
+ clearedFields: /* @__PURE__ */ new Map(),
21501
+ renamedTypes: /* @__PURE__ */ new Map()
21502
+ };
21503
+ for (const plan of expandPlans) {
21504
+ const injected = breakingChangeFields.renamedFields.get(plan.tableName) ?? /* @__PURE__ */ new Map();
21505
+ injected.set(plan.tempFieldName, {
21506
+ ...plan.after,
21507
+ required: false,
21508
+ unique: false
21509
+ });
21510
+ breakingChangeFields.renamedFields.set(plan.tableName, injected);
21511
+ const cleared = breakingChangeFields.clearedFields.get(plan.tableName) ?? /* @__PURE__ */ new Set();
21512
+ cleared.add(plan.fieldName);
21513
+ breakingChangeFields.clearedFields.set(plan.tableName, cleared);
21514
+ }
21515
+ const tables = [...Object.values(snapshot.tables), ...breakingChangeFields.renamedTypes.values()];
21516
+ if (tables.length === 0) return generateEmptyDbTypes(snapshot.namespace);
20483
21517
  const usedUtilityTypes = /* @__PURE__ */ new Set();
20484
21518
  const typeDefinitions = [];
20485
- for (const type of types) {
21519
+ for (const type of tables) {
20486
21520
  const result = generateTableType(type, breakingChangeFields);
20487
21521
  if (result.usedTimestamp) usedUtilityTypes.add("Timestamp");
20488
21522
  typeDefinitions.push(result.typeDef);
@@ -20559,12 +21593,14 @@ function generateTableType(type, breakingChangeFields) {
20559
21593
  const optionalToRequiredFields = breakingChangeFields.optionalToRequired.get(type.name) || /* @__PURE__ */ new Set();
20560
21594
  const addedRequiredFields = breakingChangeFields.addedRequiredFields.get(type.name) || /* @__PURE__ */ new Map();
20561
21595
  const enumValueChangesForType = breakingChangeFields.enumValueChanges.get(type.name) || /* @__PURE__ */ new Map();
21596
+ const clearedFieldsForType = breakingChangeFields.clearedFields.get(type.name) ?? /* @__PURE__ */ new Set();
20562
21597
  for (const [fieldName, fieldConfig] of Object.entries(type.fields)) {
20563
21598
  if (fieldName === "id") continue;
20564
21599
  const result = generateFieldType(fieldConfig, optionalToRequiredFields.has(fieldName), enumValueChangesForType.get(fieldName));
20565
- fieldLines.push(` ${fieldName}: ${result.type};`);
21600
+ const clearable = clearedFieldsForType.has(fieldName);
21601
+ fieldLines.push(` ${fieldName}: ${clearable ? generateClearableFieldType(fieldConfig) : result.type};`);
20566
21602
  usedTimestamp = usedTimestamp || result.usedTimestamp;
20567
- usedColumnType = usedColumnType || result.usedColumnType;
21603
+ usedColumnType = usedColumnType || result.usedColumnType || clearable;
20568
21604
  }
20569
21605
  for (const [fieldName, fieldConfig] of addedRequiredFields) {
20570
21606
  const result = generateFieldType(fieldConfig, true, void 0);
@@ -20572,6 +21608,13 @@ function generateTableType(type, breakingChangeFields) {
20572
21608
  usedTimestamp = usedTimestamp || result.usedTimestamp;
20573
21609
  usedColumnType = usedColumnType || result.usedColumnType;
20574
21610
  }
21611
+ const renamedFieldsForType = breakingChangeFields.renamedFields.get(type.name) || /* @__PURE__ */ new Map();
21612
+ for (const [fieldName, fieldConfig] of renamedFieldsForType) {
21613
+ const result = generateFieldType(fieldConfig, fieldConfig.required, void 0);
21614
+ fieldLines.push(` ${fieldName}: ${result.type};`);
21615
+ usedTimestamp = usedTimestamp || result.usedTimestamp;
21616
+ usedColumnType = usedColumnType || result.usedColumnType;
21617
+ }
20575
21618
  return {
20576
21619
  typeDef: ` ${type.name}: {\n${fieldLines.join("\n")}\n }`,
20577
21620
  usedTimestamp,
@@ -20622,6 +21665,19 @@ function generateEnumChangeColumnType(enumValueChange, config) {
20622
21665
  if (!config.required) return `ColumnType<(${selectType}) | null, (${afterType}) | null, (${afterType}) | null>`;
20623
21666
  return `ColumnType<${selectType}, ${afterType}, ${afterType}>`;
20624
21667
  }
21668
+ /**
21669
+ * Column type for a field the migration script both reads and clears.
21670
+ *
21671
+ * Kysely takes the select, insert, and update types from the three slots in
21672
+ * turn, so the update slot has to accept the null the script writes.
21673
+ * @param config - Field configuration in the pre-migration snapshot
21674
+ * @returns {string} Generated column type
21675
+ */
21676
+ function generateClearableFieldType(config) {
21677
+ const { type } = mapToTsType(config.type);
21678
+ const base = config.array ? `${type}[]` : type;
21679
+ return `ColumnType<${base} | null, ${base} | null, ${base} | null>`;
21680
+ }
20625
21681
  function generateOptionalToRequiredDateColumnType(config) {
20626
21682
  if (config.type !== "date" && config.type !== "datetime") return null;
20627
21683
  if (config.type === "date") {
@@ -20682,10 +21738,11 @@ function generateFieldType(config, isOptionalToRequired, enumValueChange) {
20682
21738
  * @param {string} migrationsDir - Migrations directory path
20683
21739
  * @param {number} migrationNumber - Migration number
20684
21740
  * @param {MigrationDiff} [diff] - Optional migration diff for breaking change info
21741
+ * @param expandPlans - Field changes carried through temporary fields
20685
21742
  * @returns {Promise<string>} Path to the written file
20686
21743
  */
20687
- async function writeDbTypesFile(snapshot, migrationsDir, migrationNumber, diff) {
20688
- const content = generateDbTypesFromSnapshot(snapshot, diff);
21744
+ async function writeDbTypesFile(snapshot, migrationsDir, migrationNumber, diff, expandPlans = []) {
21745
+ const content = generateDbTypesFromSnapshot(snapshot, diff, expandPlans);
20689
21746
  const filePath = getMigrationFilePath(migrationsDir, migrationNumber, "db");
20690
21747
  await fs.writeFile(filePath, content);
20691
21748
  return filePath;
@@ -20750,16 +21807,18 @@ async function generateSchemaFile(snapshot, migrationsDir, migrationNumber) {
20750
21807
  * @param {number} migrationNumber - Migration number
20751
21808
  * @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot (for db.ts generation)
20752
21809
  * @param {string} [description] - Optional description for the migration
21810
+ * @param expandPlans - Field changes carried through temporary fields
20753
21811
  * @returns {Promise<GenerateDiffResult>} Generated file info
20754
21812
  */
20755
- async function generateDiffFiles(diff, migrationsDir, migrationNumber, previousSnapshot, description) {
21813
+ async function generateDiffFiles(diff, migrationsDir, migrationNumber, previousSnapshot, description, expandPlans = []) {
20756
21814
  const migrationDir = getMigrationDirPath(migrationsDir, migrationNumber);
20757
21815
  await fs.mkdir(migrationDir, { recursive: true });
20758
21816
  const diffFilePath = getMigrationFilePath(migrationsDir, migrationNumber, "diff");
20759
21817
  const migrateFilePath = getMigrationFilePath(migrationsDir, migrationNumber, "migrate");
20760
21818
  const dbTypesFilePath = getMigrationFilePath(migrationsDir, migrationNumber, "db");
21819
+ const writeScript = diff.requiresMigrationScript;
20761
21820
  await ensureFileNotExists(diffFilePath);
20762
- if (diff.requiresMigrationScript) {
21821
+ if (writeScript) {
20763
21822
  await ensureFileNotExists(migrateFilePath);
20764
21823
  await ensureFileNotExists(dbTypesFilePath);
20765
21824
  }
@@ -20772,11 +21831,11 @@ async function generateDiffFiles(diff, migrationsDir, migrationNumber, previousS
20772
21831
  diffFilePath,
20773
21832
  migrationNumber
20774
21833
  };
20775
- if (diff.requiresMigrationScript) {
20776
- const scriptContent = generateMigrationScript(diff);
21834
+ if (writeScript) {
21835
+ const scriptContent = generateMigrationScript(diff, expandPlans);
20777
21836
  await fs.writeFile(migrateFilePath, scriptContent);
20778
21837
  result.migrateFilePath = migrateFilePath;
20779
- await writeDbTypesFile(previousSnapshot, migrationsDir, migrationNumber, diff);
21838
+ await writeDbTypesFile(previousSnapshot, migrationsDir, migrationNumber, diff, expandPlans);
20780
21839
  result.dbTypesFilePath = dbTypesFilePath;
20781
21840
  }
20782
21841
  return result;
@@ -20784,13 +21843,16 @@ async function generateDiffFiles(diff, migrationsDir, migrationNumber, previousS
20784
21843
  /**
20785
21844
  * Generate migration script content based on diff
20786
21845
  * @param {MigrationDiff} diff - Migration diff
21846
+ * @param expandPlans - Field changes carried through temporary fields
20787
21847
  * @returns {string} Migration script content
20788
21848
  */
20789
- function generateMigrationScript(diff) {
21849
+ function generateMigrationScript(diff, expandPlans = []) {
20790
21850
  const updates = [];
21851
+ const typeRenameTargets = new Map(diff.changes.filter((change) => change.kind === "table_renamed").map((change) => [change.previousTableName, change.tableName]));
21852
+ for (const plan of expandPlans) updates.push(generateExpandConversionScript(plan));
20791
21853
  for (const change of diff.changes) {
20792
21854
  const decimalScaleScript = generateDecimalScaleChangeScript(change);
20793
- updates.push(...generateChangeScripts(change, decimalScaleScript !== null));
21855
+ updates.push(...generateChangeScripts(change, decimalScaleScript !== null, typeRenameTargets));
20794
21856
  if (decimalScaleScript) {
20795
21857
  updates.push(decimalScaleScript);
20796
21858
  const uniqueConstraintScript = generateUniqueConstraintScript(change);
@@ -20863,33 +21925,34 @@ describe(${JSON.stringify(`${diff.namespace} migration`)}, () => {
20863
21925
  * Generate scripts for a single change
20864
21926
  * @param {DiffChange} change - Diff change to generate script for
20865
21927
  * @param {boolean} deferUniqueConstraint - Generate the unique check after decimal re-serialization
21928
+ * @param {ReadonlyMap<string, string>} [typeRenameTargets] - Confirmed type renames (old name → new name)
20866
21929
  * @returns {string[]} Script contents, or an empty array if no script is needed
20867
21930
  */
20868
- function generateChangeScripts(change, deferUniqueConstraint = false) {
21931
+ function generateChangeScripts(change, deferUniqueConstraint = false, typeRenameTargets) {
20869
21932
  if (change.kind === "index_added" || change.kind === "index_modified") {
20870
21933
  const before = change.kind === "index_modified" ? change.before : void 0;
20871
- if (!isBreakingIndexChange(change.typeName, change.indexName, before, change.after)) return [];
21934
+ if (!isBreakingIndexChange(change.tableName, change.indexName, before, change.after)) return [];
20872
21935
  const fields = change.after.fields;
20873
21936
  const fieldList = fields.map((f) => `"${f}"`).join(", ");
20874
21937
  const whereClauses = fields.map((f) => `.where("${f}", "=", dup.${f})`).join("\n ");
20875
21938
  return [` // Resolve duplicate (${fields.join(", ")}) combinations before unique index "${change.indexName}" is enforced
20876
21939
  {
20877
21940
  const duplicates = await trx
20878
- .selectFrom("${change.typeName}")
21941
+ .selectFrom("${change.tableName}")
20879
21942
  .select([${fieldList}])
20880
21943
  .groupBy([${fieldList}])
20881
21944
  .having((eb) => eb.fn.count("id"), ">", 1)
20882
21945
  .execute();
20883
21946
  for (const dup of duplicates) {
20884
21947
  const records = await trx
20885
- .selectFrom("${change.typeName}")
21948
+ .selectFrom("${change.tableName}")
20886
21949
  .select(["id"])
20887
21950
  ${whereClauses}
20888
21951
  .execute();
20889
21952
  // Keep the first record; update or delete the others so the combination becomes unique
20890
21953
  for (let i = 1; i < records.length; i++) {
20891
21954
  await trx
20892
- .updateTable("${change.typeName}")
21955
+ .updateTable("${change.tableName}")
20893
21956
  .set({ ${fields[0]}: null }) // TODO: Set appropriate unique value
20894
21957
  .where("id", "=", records[i].id)
20895
21958
  .execute();
@@ -20898,22 +21961,28 @@ function generateChangeScripts(change, deferUniqueConstraint = false) {
20898
21961
  }`];
20899
21962
  }
20900
21963
  if (change.kind === "field_added") {
20901
- if (change.after.required) return [` // Populate ${change.fieldName} for existing ${change.typeName} records
21964
+ if (change.after.required) return [` // Populate ${change.fieldName} for existing ${change.tableName} records
20902
21965
  await trx
20903
- .updateTable("${change.typeName}")
21966
+ .updateTable("${change.tableName}")
20904
21967
  .set({
20905
21968
  ${change.fieldName}: null, // TODO: Set appropriate default value
20906
21969
  })
20907
21970
  .execute();`];
20908
21971
  return [];
20909
21972
  }
21973
+ if (change.kind === "field_renamed") {
21974
+ const scripts = [generateFieldRenameCopyScript(change)];
21975
+ if ((change.after.unique ?? false) && (!(change.before.unique ?? false) || renameCopyCanCollapseValues(change))) scripts.push(generateUniqueDedupeScript(change.tableName, change.fieldName, "suffix"));
21976
+ return scripts;
21977
+ }
21978
+ if (change.kind === "table_renamed") return [generateTypeRenameCopyScript(change)];
20910
21979
  if (change.kind !== "field_modified" && change.kind !== "field_type_modified") return [];
20911
21980
  const { before, after } = change;
20912
21981
  const scripts = [];
20913
21982
  if (change.kind === "field_type_modified") scripts.push(generateFieldTypeChangeScript(change));
20914
- if (!before.required && after.required) scripts.push(` // Set ${change.fieldName} for ${change.typeName} records where it is null
21983
+ if (!before.required && after.required) scripts.push(` // Set ${change.fieldName} for ${change.tableName} records where it is null
20915
21984
  await trx
20916
- .updateTable("${change.typeName}")
21985
+ .updateTable("${change.tableName}")
20917
21986
  .set({
20918
21987
  ${change.fieldName}: null, // TODO: Set appropriate default value
20919
21988
  })
@@ -20931,25 +22000,25 @@ function generateChangeScripts(change, deferUniqueConstraint = false) {
20931
22000
  const defaultValue = afterValues[0] ?? "NEW_VALUE";
20932
22001
  scripts.push(` // Migrate records with removed enum values: ${removedValues.join(", ")}
20933
22002
  await trx
20934
- .updateTable("${change.typeName}")
22003
+ .updateTable("${change.tableName}")
20935
22004
  .set({ ${change.fieldName}: "${defaultValue}" }) // TODO: Set appropriate value
20936
22005
  .where("${change.fieldName}", "in", [${removedValues.map((v) => `"${v}"`).join(", ")}])
20937
22006
  .execute();`);
20938
22007
  }
20939
22008
  }
20940
- if (before.foreignKeyType && after.foreignKeyType && before.foreignKeyType !== after.foreignKeyType) scripts.push(` // Migrate ${change.fieldName} references from ${before.foreignKeyType} to ${after.foreignKeyType}
22009
+ if (isBreakingForeignKeyRetarget(before, after, typeRenameTargets)) scripts.push(` // Migrate ${change.fieldName} references from ${before.foreignKeyType} to ${after.foreignKeyType}
20941
22010
  // Find records that don't have a valid reference in the new target table
20942
22011
  {
20943
22012
  const orphanedRecords = await trx
20944
- .selectFrom("${change.typeName}")
20945
- .leftJoin("${after.foreignKeyType}", "${change.typeName}.${change.fieldName}", "${after.foreignKeyType}.id")
20946
- .select(["${change.typeName}.id", "${change.typeName}.${change.fieldName}"])
22013
+ .selectFrom("${change.tableName}")
22014
+ .leftJoin("${after.foreignKeyType}", "${change.tableName}.${change.fieldName}", "${after.foreignKeyType}.id")
22015
+ .select(["${change.tableName}.id", "${change.tableName}.${change.fieldName}"])
20947
22016
  .where("${after.foreignKeyType}.id", "is", null)
20948
- .where("${change.typeName}.${change.fieldName}", "is not", null)
22017
+ .where("${change.tableName}.${change.fieldName}", "is not", null)
20949
22018
  .execute();
20950
22019
  for (const record of orphanedRecords) {
20951
22020
  await trx
20952
- .updateTable("${change.typeName}")
22021
+ .updateTable("${change.tableName}")
20953
22022
  .set({ ${change.fieldName}: null }) // TODO: Set appropriate new reference
20954
22023
  .where("id", "=", record.id)
20955
22024
  .execute();
@@ -20957,13 +22026,70 @@ function generateChangeScripts(change, deferUniqueConstraint = false) {
20957
22026
  }`);
20958
22027
  return scripts;
20959
22028
  }
22029
+ function renameCopyCanCollapseValues(change) {
22030
+ const { before, after } = change;
22031
+ if (before.type !== "decimal" || after.type !== "decimal") return false;
22032
+ return (after.scale ?? 6) < (before.scale ?? 6);
22033
+ }
22034
+ function generateFieldRenameCopyScript(change) {
22035
+ const { tableName, fieldName, previousFieldName, before, after } = change;
22036
+ return ` // Copy ${tableName}.${previousFieldName} into ${fieldName} for every row.
22037
+ // Overwrite unconditionally: stored values of previously removed fields are
22038
+ // not pruned, so a stale value could otherwise resurface under ${fieldName}.${!before.required && after.required ? `
22039
+ // TODO: ${previousFieldName} is optional but ${fieldName} is required.
22040
+ // Resolve null values, or the post-migration phase will fail.` : ""}${renameCopyCanCollapseValues(change) ? `
22041
+ // WARNING: ${fieldName} has a smaller decimal scale than ${previousFieldName}, so
22042
+ // copied values that exceed it may be rounded half-up. Review the resulting
22043
+ // precision before deploying.` : ""}
22044
+ await trx
22045
+ .updateTable("${tableName}")
22046
+ .set((eb) => ({ ${fieldName}: eb.ref("${previousFieldName}") }))
22047
+ .execute();`;
22048
+ }
22049
+ function generateTypeRenameCopyScript(change) {
22050
+ const { tableName, previousTableName } = change;
22051
+ const columnList = ["id", ...Object.keys(change.before.fields).filter((name) => name !== "id")].map((name) => JSON.stringify(name)).join(", ");
22052
+ const selfRefColumns = Object.entries(change.after.fields).filter(([, field]) => field.foreignKeyType === tableName).map(([name]) => name);
22053
+ return ` // Copy every ${previousTableName} row into ${tableName}, preserving ids so that
22054
+ // stored foreign key references remain valid. ${previousTableName} stays readable
22055
+ // until the post-migration phase drops it.
22056
+ {
22057
+ let lastId: string | undefined;
22058
+ while (true) {
22059
+ let query = trx
22060
+ .selectFrom("${previousTableName}")
22061
+ .select([${columnList}])
22062
+ .orderBy("id", "asc")
22063
+ .limit(100);
22064
+ if (lastId) {
22065
+ query = query.where("id", ">", lastId);
22066
+ }
22067
+ const rows = await query.execute();
22068
+ if (rows.length === 0) break;
22069
+
22070
+ await trx.insertInto("${tableName}").values(${selfRefColumns.length > 0 ? `rows.map((row) => ({ ...row, ${selfRefColumns.map((name) => `${name}: null`).join(", ")} }))` : "rows"}).execute();
22071
+ lastId = rows[rows.length - 1]!.id;
22072
+ }
22073
+ }${selfRefColumns.length > 0 ? `
22074
+
22075
+ // Backfill the self-referential column(s) now that every row exists.
22076
+ await trx
22077
+ .updateTable("${tableName}")
22078
+ .set((eb) => ({
22079
+ ${selfRefColumns.map((name) => ` ${name}: eb
22080
+ .selectFrom("${previousTableName}")
22081
+ .select("${previousTableName}.${name}")
22082
+ .whereRef("${previousTableName}.id", "=", "${tableName}.id"),`).join("\n")}
22083
+ }))
22084
+ .execute();` : ""}`;
22085
+ }
20960
22086
  function generateFieldTypeChangeScript(change) {
20961
- return ` // Normalize ${change.typeName}.${change.fieldName} from ${change.before.type} to ${change.after.type} while the previous type is still active
22087
+ return ` // Normalize ${change.tableName}.${change.fieldName} from ${change.before.type} to ${change.after.type} while the previous type is still active
20962
22088
  {
20963
22089
  let lastId: string | undefined;
20964
22090
  while (true) {
20965
22091
  let query = trx
20966
- .selectFrom("${change.typeName}")
22092
+ .selectFrom("${change.tableName}")
20967
22093
  .select(["id", "${change.fieldName}"])
20968
22094
  .where("${change.fieldName}", "is not", null)
20969
22095
  .orderBy("id", "asc")
@@ -20982,7 +22108,7 @@ function generateFieldTypeChangeScript(change) {
20982
22108
  const normalizedValue: never = sourceValue;
20983
22109
  if (Object.is(normalizedValue, sourceValue)) continue;
20984
22110
  await trx
20985
- .updateTable("${change.typeName}")
22111
+ .updateTable("${change.tableName}")
20986
22112
  .set({ [${JSON.stringify(change.fieldName)}]: normalizedValue })
20987
22113
  .where("id", "=", row.id)
20988
22114
  .execute();
@@ -20991,38 +22117,76 @@ function generateFieldTypeChangeScript(change) {
20991
22117
  }
20992
22118
  }`;
20993
22119
  }
22120
+ function generateExpandConversionScript(plan) {
22121
+ return ` // Convert ${plan.tableName}.${plan.fieldName} into ${plan.tempFieldName}, which the next migration renames back to ${plan.fieldName}
22122
+ {
22123
+ let lastId: string | undefined;
22124
+ while (true) {
22125
+ let query = trx
22126
+ .selectFrom("${plan.tableName}")
22127
+ .select(["id", "${plan.fieldName}"])
22128
+ .where("${plan.fieldName}", "is not", null)
22129
+ .orderBy("id", "asc")
22130
+ .limit(100);
22131
+ if (lastId) {
22132
+ query = query.where("id", ">", lastId);
22133
+ }
22134
+ const rows = await query.execute();
22135
+ if (rows.length === 0) break;
22136
+
22137
+ for (const row of rows) {
22138
+ // ${MIGRATION_REVIEW_REQUIRED_MARKER}: Remove this marker and the \`never\` annotation after reviewing the conversion.
22139
+ // Produce a value accepted by the ${plan.after.type} type from the stored ${plan.before.type} value.
22140
+ const sourceValue = row.${plan.fieldName};
22141
+ const convertedValue: never = sourceValue;
22142
+ // Clearing ${plan.fieldName} keeps a re-run from converting the row twice.
22143
+ await trx
22144
+ .updateTable("${plan.tableName}")
22145
+ .set({
22146
+ [${JSON.stringify(plan.tempFieldName)}]: convertedValue,
22147
+ [${JSON.stringify(plan.fieldName)}]: null,
22148
+ })
22149
+ .where("id", "=", row.id)
22150
+ .execute();
22151
+ }
22152
+ lastId = rows[rows.length - 1]!.id;
22153
+ }
22154
+ }`;
22155
+ }
20994
22156
  function generateUniqueConstraintScript(change) {
20995
22157
  if (change.kind !== "field_modified" && change.kind !== "field_type_modified") return null;
20996
22158
  const { before, after } = change;
20997
22159
  if ((before.unique ?? false) || !(after.unique ?? false)) return null;
20998
- const duplicateResolution = change.kind === "field_type_modified" ? ` if (records.length > 1) {
20999
- throw new Error(
21000
- "TODO: Resolve duplicate ${change.typeName}.${change.fieldName} values before adding the unique constraint",
21001
- );
21002
- }` : ` // Keep first record, add suffix to others
21003
- for (let i = 1; i < records.length; i++) {
21004
- await trx
21005
- .updateTable("${change.typeName}")
21006
- .set({ ${change.fieldName}: \`\${records[i].${change.fieldName}}_\${i}\` }) // TODO: Set appropriate unique value
21007
- .where("id", "=", records[i].id)
21008
- .execute();
21009
- }`;
21010
- return ` // Ensure ${change.fieldName} values are unique before adding constraint
22160
+ return generateUniqueDedupeScript(change.tableName, change.fieldName, change.kind === "field_type_modified" ? "throw" : "suffix");
22161
+ }
22162
+ function generateUniqueDedupeScript(tableName, fieldName, resolution) {
22163
+ return ` // Ensure ${fieldName} values are unique before adding constraint
21011
22164
  {
21012
22165
  const duplicates = await trx
21013
- .selectFrom("${change.typeName}")
21014
- .select(["${change.fieldName}"])
21015
- .groupBy("${change.fieldName}")
22166
+ .selectFrom("${tableName}")
22167
+ .select(["${fieldName}"])
22168
+ .groupBy("${fieldName}")
21016
22169
  .having((eb) => eb.fn.count("id"), ">", 1)
21017
22170
  .execute();
21018
22171
  for (const dup of duplicates) {
21019
22172
  // Load every record in this duplicate group before resolving it
21020
22173
  const records = await trx
21021
- .selectFrom("${change.typeName}")
21022
- .select(["id", "${change.fieldName}"])
21023
- .where("${change.fieldName}", "=", dup.${change.fieldName})
22174
+ .selectFrom("${tableName}")
22175
+ .select(["id", "${fieldName}"])
22176
+ .where("${fieldName}", "=", dup.${fieldName})
21024
22177
  .execute();
21025
- ${duplicateResolution}
22178
+ ${resolution === "throw" ? ` if (records.length > 1) {
22179
+ throw new Error(
22180
+ "TODO: Resolve duplicate ${tableName}.${fieldName} values before adding the unique constraint",
22181
+ );
22182
+ }` : ` // Keep first record, add suffix to others
22183
+ for (let i = 1; i < records.length; i++) {
22184
+ await trx
22185
+ .updateTable("${tableName}")
22186
+ .set({ ${fieldName}: \`\${records[i].${fieldName}}_\${i}\` }) // TODO: Set appropriate unique value
22187
+ .where("id", "=", records[i].id)
22188
+ .execute();
22189
+ }`}
21026
22190
  }
21027
22191
  }`;
21028
22192
  }
@@ -21033,9 +22197,9 @@ function generateDecimalScaleChangeScript(change) {
21033
22197
  const valueExpression = !before.required && after.required ? `row.${change.fieldName}!` : `row.${change.fieldName}`;
21034
22198
  const beforeScale = before.scale ?? 6;
21035
22199
  const roundingWarning = (after.scale ?? 6) < beforeScale ? `
21036
- // Values that exceed the new scale may be rounded half-up, so review the
21037
- // resulting precision before deploying.` : "";
21038
- return ` // Re-save existing ${change.typeName} rows so ${change.fieldName} is stored under the new scale.
22200
+ // WARNING: Values that exceed the new scale may be rounded half-up, so
22201
+ // review the resulting precision before deploying.` : "";
22202
+ return ` // Re-save existing ${change.tableName} rows so ${change.fieldName} is stored under the new scale.
21039
22203
  // This is a workaround for a platform-side gap where rows written under the
21040
22204
  // previous scale could fail on later updates until re-saved. Keep it unless
21041
22205
  // your platform is confirmed to handle stored values across scale changes.${roundingWarning}
@@ -21043,7 +22207,7 @@ function generateDecimalScaleChangeScript(change) {
21043
22207
  let lastId: string | undefined;
21044
22208
  while (true) {
21045
22209
  let query = trx
21046
- .selectFrom("${change.typeName}")
22210
+ .selectFrom("${change.tableName}")
21047
22211
  .select(["id", "${change.fieldName}"])
21048
22212
  .where("${change.fieldName}", "is not", null)
21049
22213
  .orderBy("id", "asc")
@@ -21056,7 +22220,7 @@ function generateDecimalScaleChangeScript(change) {
21056
22220
 
21057
22221
  for (const row of rows) {
21058
22222
  await trx
21059
- .updateTable("${change.typeName}")
22223
+ .updateTable("${change.tableName}")
21060
22224
  .set({ ${change.fieldName}: ${valueExpression} })
21061
22225
  .where("id", "=", row.id)
21062
22226
  .where("${change.fieldName}", "=", ${valueExpression})
@@ -21344,14 +22508,46 @@ async function generate(options) {
21344
22508
  logger.info("Add \"migration: { directory: \\\"./migrations\\\" }\" to your db config to enable migrations.");
21345
22509
  return;
21346
22510
  }
22511
+ const renameFlags = [];
22512
+ const typeRenameFlags = [];
22513
+ for (const raw of options.renames ?? []) if (raw.includes(".")) renameFlags.push({
22514
+ raw,
22515
+ spec: parseRenameOption(raw)
22516
+ });
22517
+ else typeRenameFlags.push({
22518
+ raw,
22519
+ spec: parseTypeRenameOption(raw)
22520
+ });
22521
+ const dropFlags = [];
22522
+ const typeDropFlags = [];
22523
+ for (const raw of options.drops ?? []) if (raw.includes(".")) dropFlags.push({
22524
+ raw,
22525
+ spec: parseDropOption(raw)
22526
+ });
22527
+ else typeDropFlags.push({
22528
+ raw,
22529
+ spec: parseTypeDropOption(raw)
22530
+ });
22531
+ const expandContractFlags = (options.expandContracts ?? []).map((raw) => ({
22532
+ raw,
22533
+ spec: parseExpandContractOption(raw)
22534
+ }));
22535
+ if (options.init && (renameFlags.length > 0 || typeRenameFlags.length > 0 || dropFlags.length > 0 || typeDropFlags.length > 0 || expandContractFlags.length > 0)) throw new Error("--rename, --drop, and --expand-contract cannot be used together with --init.");
22536
+ const droppedFieldKeys = new Set(dropFlags.map(({ spec }) => `${spec.tableName}.${spec.fieldName}`));
22537
+ const conflictingFlags = renameFlags.filter(({ spec }) => droppedFieldKeys.has(`${spec.tableName}.${spec.previousFieldName}`));
22538
+ if (conflictingFlags.length > 0) throw new Error(`--rename and --drop conflict for: ${conflictingFlags.map(({ spec }) => `${spec.tableName}.${spec.previousFieldName}`).join(", ")}`);
22539
+ const droppedTypeNames = new Set(typeDropFlags.map(({ spec }) => spec.tableName));
22540
+ const conflictingTypeFlags = typeRenameFlags.filter(({ spec }) => droppedTypeNames.has(spec.previousTableName));
22541
+ if (conflictingTypeFlags.length > 0) throw new Error(`--rename and --drop conflict for: ${conflictingTypeFlags.map(({ spec }) => spec.previousTableName).join(", ")}`);
21347
22542
  if (options.init) await handleInitOption(namespacesWithMigrations, options.yes);
21348
22543
  let pluginManager;
21349
22544
  if (plugins.length > 0) pluginManager = new PluginManager(plugins);
21350
- const { defineApplication } = await import("./application-BxLLiLsr.mjs");
22545
+ const { defineApplication } = await import("./application-BZfk4HKm.mjs");
21351
22546
  const application = defineApplication({
21352
22547
  config,
21353
22548
  pluginManager
21354
22549
  });
22550
+ const generations = [];
21355
22551
  for (const { namespace, migrationsDir } of namespacesWithMigrations) {
21356
22552
  logger.info(`Processing namespace: ${styles.bold(namespace)}`);
21357
22553
  assertValidMigrationFiles(migrationsDir, namespace);
@@ -21363,11 +22559,110 @@ async function generate(options) {
21363
22559
  await tailordbService.loadTypes();
21364
22560
  await tailordbService.processNamespacePlugins();
21365
22561
  const localTypesObj = tailordbService.types;
21366
- const currentSnapshot = createSnapshotFromLocalTypes(localTypesObj, namespace);
21367
- const previousSnapshot = reconstructSnapshotFromMigrations(migrationsDir);
21368
- if (!previousSnapshot) await generateInitialSnapshot(currentSnapshot, migrationsDir);
21369
- else await generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migrationsDir, options);
22562
+ generations.push({
22563
+ namespace,
22564
+ migrationsDir,
22565
+ currentSnapshot: createSnapshotFromLocalTypes(localTypesObj, namespace),
22566
+ previousSnapshot: reconstructSnapshotFromMigrations(migrationsDir)
22567
+ });
21370
22568
  }
22569
+ const renameSpecsByNamespace = matchFlagsToNamespaces(renameFlags, generations, renameSpecApplies, "--rename does not match a removed + added field pair");
22570
+ const typeRenameSpecsByNamespace = matchFlagsToNamespaces(typeRenameFlags, generations, typeRenameSpecApplies, "--rename does not match a removed + added type pair");
22571
+ const dropSpecsByNamespace = matchFlagsToNamespaces(dropFlags, generations, dropSpecApplies, "--drop does not match a removed field");
22572
+ const typeDropSpecsByNamespace = matchFlagsToNamespaces(typeDropFlags, generations, typeDropSpecApplies, "--drop does not match a removed type");
22573
+ const expandContractKeysByNamespace = /* @__PURE__ */ new Map();
22574
+ const matchedExpandContractFlags = /* @__PURE__ */ new Set();
22575
+ const ineligibleExpandContracts = [];
22576
+ for (const { namespace, previousSnapshot, currentSnapshot } of generations) {
22577
+ if (!previousSnapshot) continue;
22578
+ const applicable = [];
22579
+ for (const flag of expandContractFlags) {
22580
+ const { spec } = flag;
22581
+ const before = previousSnapshot.tables[spec.tableName]?.fields[spec.fieldName];
22582
+ const after = currentSnapshot.tables[spec.tableName]?.fields[spec.fieldName];
22583
+ if (!before || !after || before.type === after.type) continue;
22584
+ matchedExpandContractFlags.add(flag);
22585
+ const eligibility = getExpandContractEligibility({
22586
+ previous: previousSnapshot,
22587
+ current: currentSnapshot,
22588
+ tableName: spec.tableName,
22589
+ fieldName: spec.fieldName
22590
+ });
22591
+ if (!eligibility.eligible) {
22592
+ ineligibleExpandContracts.push(`--expand-contract cannot convert ${flag.raw} (namespace: ${namespace}): ${eligibility.reason}`);
22593
+ continue;
22594
+ }
22595
+ applicable.push(flag);
22596
+ }
22597
+ expandContractKeysByNamespace.set(namespace, new Set(applicable.map(({ spec }) => fieldKey(spec.tableName, spec.fieldName))));
22598
+ }
22599
+ const unusedExpandContracts = expandContractFlags.filter((flag) => !matchedExpandContractFlags.has(flag));
22600
+ if (unusedExpandContracts.length > 0) throw new Error(`--expand-contract does not match a field whose type changed: ${unusedExpandContracts.map((flag) => flag.raw).join(", ")}`);
22601
+ if (ineligibleExpandContracts.length > 0) throw new Error(ineligibleExpandContracts.join("\n"));
22602
+ const unresolvedCandidates = [];
22603
+ for (const generation of generations) {
22604
+ const { namespace, previousSnapshot, currentSnapshot } = generation;
22605
+ if (!previousSnapshot) continue;
22606
+ const diff = compareSnapshots(previousSnapshot, currentSnapshot);
22607
+ if (!hasChanges(diff)) {
22608
+ generation.diff = diff;
22609
+ continue;
22610
+ }
22611
+ const resolution = await resolveRenames(previousSnapshot, currentSnapshot, diff, options, {
22612
+ fieldRenames: renameSpecsByNamespace.get(namespace) ?? [],
22613
+ typeRenames: typeRenameSpecsByNamespace.get(namespace) ?? [],
22614
+ fieldDrops: dropSpecsByNamespace.get(namespace) ?? [],
22615
+ typeDrops: typeDropSpecsByNamespace.get(namespace) ?? []
22616
+ });
22617
+ generation.diff = resolution.diff;
22618
+ unresolvedCandidates.push(...resolution.unresolved);
22619
+ generation.expandPlans = await resolveExpandContractPlans({
22620
+ previousSnapshot,
22621
+ currentSnapshot,
22622
+ diff: resolution.diff,
22623
+ options,
22624
+ confirmedKeys: expandContractKeysByNamespace.get(namespace) ?? /* @__PURE__ */ new Set()
22625
+ });
22626
+ }
22627
+ if (unresolvedCandidates.length > 0) {
22628
+ const details = unresolvedCandidates.map(({ namespace, label, targets }) => ` - ${label} → ${targets.join(", ")}? (namespace: ${namespace})`).join("\n");
22629
+ throw new Error(`Possible rename(s) detected:\n${details}\nRe-run with --rename "Type.oldField:newField" (field) or --rename "OldType:NewType" (type) to record a rename and scaffold a data copy script, or --drop "Type.field" / --drop "Type" to confirm the removal.`);
22630
+ }
22631
+ for (const { migrationsDir, currentSnapshot, previousSnapshot, diff, expandPlans } of generations) if (!previousSnapshot) await generateInitialSnapshot(currentSnapshot, migrationsDir);
22632
+ else await generateDiffFromSnapshot(previousSnapshot, assertDefined(diff, "Migration diff was not resolved during preflight"), migrationsDir, options, currentSnapshot, expandPlans ?? []);
22633
+ }
22634
+ /**
22635
+ * Decide which unsupported field type changes to carry through a migration
22636
+ * pair, asking about each one that was not already named by a flag.
22637
+ * @param input - Snapshots, diff, command options, and flag-approved fields
22638
+ * @returns Approved plans, empty when nothing was confirmed
22639
+ */
22640
+ async function resolveExpandContractPlans(input) {
22641
+ const { previousSnapshot, currentSnapshot, diff, options, confirmedKeys } = input;
22642
+ const confirmed = new Set(confirmedKeys);
22643
+ if (!options.yes && canPrompt()) for (const change of diff.changes) {
22644
+ if (change.kind !== "field_type_modified") continue;
22645
+ const key = fieldKey(change.tableName, change.fieldName);
22646
+ if (confirmed.has(key)) continue;
22647
+ if (!canConvertField({
22648
+ previous: previousSnapshot,
22649
+ current: currentSnapshot,
22650
+ tableName: change.tableName,
22651
+ fieldName: change.fieldName
22652
+ })) continue;
22653
+ logger.newline();
22654
+ logger.info(`${change.tableName}.${change.fieldName} changes from ${change.before.type} to ${change.after.type}, which cannot be applied in one step.`);
22655
+ if (await prompt.confirm({
22656
+ message: `Generate two migrations to convert ${change.tableName}.${change.fieldName} through a temporary field?`,
22657
+ default: true
22658
+ })) confirmed.add(key);
22659
+ }
22660
+ return planExpandContract({
22661
+ previous: previousSnapshot,
22662
+ current: currentSnapshot,
22663
+ diff,
22664
+ confirmed
22665
+ }).plans;
21371
22666
  }
21372
22667
  /**
21373
22668
  * Generate the initial schema snapshot
@@ -21379,19 +22674,174 @@ async function generateInitialSnapshot(snapshot, migrationsDir) {
21379
22674
  const result = await generateSchemaFile(snapshot, migrationsDir, 0);
21380
22675
  logger.success(`Generated initial schema snapshot`);
21381
22676
  logger.info(` File: ${result.filePath}`);
21382
- logger.info(` Types: ${Object.keys(snapshot.types).length}`);
22677
+ logger.info(` Types: ${Object.keys(snapshot.tables).length}`);
21383
22678
  logger.log("\nThis is the baseline schema. Future changes will be tracked as diffs.");
21384
22679
  }
21385
22680
  /**
21386
- * Generate diff from previous snapshot
22681
+ * Match `--rename` / `--drop` flags against every namespace's snapshots.
22682
+ * Throws when a flag applies to no namespace, so a typo cannot silently fall
22683
+ * back to remove + add.
22684
+ * @param {readonly { raw: string; spec: S }[]} flags - Parsed flags of one form
22685
+ * @param {readonly NamespaceGeneration[]} generations - Snapshots per namespace
22686
+ * @param {(spec: S, previous: SchemaSnapshot, current: SchemaSnapshot) => boolean} applies - Whether a spec matches a namespace's snapshot pair
22687
+ * @param {string} unmatchedError - Error prefix for flags that match no namespace
22688
+ * @returns {Map<string, S[]>} Applicable specs keyed by namespace
22689
+ */
22690
+ function matchFlagsToNamespaces(flags, generations, applies, unmatchedError) {
22691
+ const specsByNamespace = /* @__PURE__ */ new Map();
22692
+ const matched = /* @__PURE__ */ new Set();
22693
+ for (const { namespace, previousSnapshot, currentSnapshot } of generations) {
22694
+ if (!previousSnapshot) continue;
22695
+ const applicable = flags.filter(({ spec }) => applies(spec, previousSnapshot, currentSnapshot));
22696
+ for (const flag of applicable) matched.add(flag);
22697
+ specsByNamespace.set(namespace, applicable.map((flag) => flag.spec));
22698
+ }
22699
+ const unused = flags.filter((flag) => !matched.has(flag));
22700
+ if (unused.length > 0) throw new Error(`${unmatchedError}: ${unused.map((flag) => flag.raw).join(", ")}`);
22701
+ return specsByNamespace;
22702
+ }
22703
+ function availableRenameTargets(candidate, claimedFields) {
22704
+ return candidate.added.filter((added) => !claimedFields.has(`${candidate.tableName}.${added.fieldName}`)).map((added) => added.fieldName);
22705
+ }
22706
+ /**
22707
+ * Ask the user whether a removed field was renamed to one of the compatible
22708
+ * added fields. Returns the confirmed new field name, or undefined.
22709
+ * @param {FieldRenameCandidate} candidate - Candidate to confirm
22710
+ * @param {string[]} addedFieldNames - Added field names still available as rename targets
22711
+ * @returns {Promise<string | undefined>} Confirmed new field name, if any
22712
+ */
22713
+ async function promptRenameCandidate(candidate, addedFieldNames) {
22714
+ const oldLabel = `${candidate.tableName}.${candidate.removed.fieldName}`;
22715
+ const [firstFieldName] = addedFieldNames;
22716
+ if (addedFieldNames.length === 1 && firstFieldName) return await prompt.confirm({
22717
+ message: `${oldLabel} was removed and ${firstFieldName} was added with a compatible type. Was it renamed to ${firstFieldName}?`,
22718
+ default: true
22719
+ }) ? firstFieldName : void 0;
22720
+ return await prompt.select({
22721
+ message: `${oldLabel} was removed. Was it renamed to one of these added fields?`,
22722
+ choices: [...addedFieldNames.map((fieldName) => ({
22723
+ name: `Yes, renamed to ${fieldName}`,
22724
+ value: fieldName
22725
+ })), {
22726
+ name: `No, ${candidate.removed.fieldName} was removed`,
22727
+ value: null
22728
+ }]
22729
+ }) ?? void 0;
22730
+ }
22731
+ function availableTypeRenameTargets(candidate, claimedTypes) {
22732
+ return candidate.added.filter((added) => !claimedTypes.has(added.tableName)).map((added) => added.tableName);
22733
+ }
22734
+ /**
22735
+ * Ask the user whether a removed type was renamed to one of the compatible
22736
+ * added types. Returns the confirmed new type name, or undefined.
22737
+ * @param {TypeRenameCandidate} candidate - Candidate to confirm
22738
+ * @param {string[]} addedTypeNames - Added type names still available as rename targets
22739
+ * @returns {Promise<string | undefined>} Confirmed new type name, if any
22740
+ */
22741
+ async function promptTypeRenameCandidate(candidate, addedTypeNames) {
22742
+ const oldTypeName = candidate.removed.tableName;
22743
+ const [firstTypeName] = addedTypeNames;
22744
+ if (addedTypeNames.length === 1 && firstTypeName) return await prompt.confirm({
22745
+ message: `${oldTypeName} was removed and ${firstTypeName} was added with a compatible schema. Was it renamed to ${firstTypeName}?`,
22746
+ default: true
22747
+ }) ? firstTypeName : void 0;
22748
+ return await prompt.select({
22749
+ message: `${oldTypeName} was removed. Was it renamed to one of these added types?`,
22750
+ choices: [...addedTypeNames.map((tableName) => ({
22751
+ name: `Yes, renamed to ${tableName}`,
22752
+ value: tableName
22753
+ })), {
22754
+ name: `No, ${oldTypeName} was removed`,
22755
+ value: null
22756
+ }]
22757
+ }) ?? void 0;
22758
+ }
22759
+ /**
22760
+ * Resolve field and type renames for a diff: apply `--rename` flags, skip
22761
+ * candidates whose removal is confirmed by `--drop`, confirm the rest
22762
+ * interactively, and recompute the diff with the confirmed renames. When
22763
+ * prompting is unavailable (`--yes` or no TTY), the remaining candidates are
22764
+ * returned as unresolved for the caller to fail on.
22765
+ * @param {NormalizedSchemaSnapshot} previousSnapshot - Previous normalized schema snapshot
22766
+ * @param {NormalizedSchemaSnapshot} currentSnapshot - Current normalized schema snapshot
22767
+ * @param {MigrationDiff} diff - Diff computed without rename knowledge
22768
+ * @param {GenerateOptions} options - Generate options
22769
+ * @param {NamespaceRenameSpecs} specs - This namespace's `--rename` / `--drop` specs
22770
+ * @returns {Promise<RenameResolution>} Diff with confirmed renames and any unresolved candidates
22771
+ */
22772
+ async function resolveRenames(previousSnapshot, currentSnapshot, diff, options, specs) {
22773
+ const confirmed = [...specs.fieldRenames];
22774
+ const claimedFields = new Set(confirmed.flatMap((spec) => [`${spec.tableName}.${spec.previousFieldName}`, `${spec.tableName}.${spec.fieldName}`]));
22775
+ const droppedFields = new Set(specs.fieldDrops.map((spec) => `${spec.tableName}.${spec.fieldName}`));
22776
+ const confirmedTypes = [...specs.typeRenames];
22777
+ const claimedTypes = new Set(confirmedTypes.flatMap((spec) => [spec.previousTableName, spec.tableName]));
22778
+ const droppedTypes = new Set(specs.typeDrops.map((spec) => spec.tableName));
22779
+ const typeCandidates = findTypeRenameCandidates(diff).filter((candidate) => !droppedTypes.has(candidate.removed.tableName) && !claimedTypes.has(candidate.removed.tableName) && availableTypeRenameTargets(candidate, claimedTypes).length > 0);
22780
+ const candidates = findRenameCandidates(diff).filter((candidate) => !droppedFields.has(`${candidate.tableName}.${candidate.removed.fieldName}`) && !claimedFields.has(`${candidate.tableName}.${candidate.removed.fieldName}`) && availableRenameTargets(candidate, claimedFields).length > 0);
22781
+ const unresolved = [];
22782
+ if (options.yes || !canPrompt()) {
22783
+ for (const candidate of typeCandidates) unresolved.push({
22784
+ namespace: diff.namespace,
22785
+ label: candidate.removed.tableName,
22786
+ targets: availableTypeRenameTargets(candidate, claimedTypes)
22787
+ });
22788
+ for (const candidate of candidates) unresolved.push({
22789
+ namespace: diff.namespace,
22790
+ label: `${candidate.tableName}.${candidate.removed.fieldName}`,
22791
+ targets: availableRenameTargets(candidate, claimedFields)
22792
+ });
22793
+ } else {
22794
+ for (const candidate of typeCandidates) {
22795
+ const addedTypeNames = availableTypeRenameTargets(candidate, claimedTypes);
22796
+ if (addedTypeNames.length === 0) continue;
22797
+ const newTypeName = await promptTypeRenameCandidate(candidate, addedTypeNames);
22798
+ if (newTypeName) {
22799
+ confirmedTypes.push({
22800
+ previousTableName: candidate.removed.tableName,
22801
+ tableName: newTypeName
22802
+ });
22803
+ claimedTypes.add(candidate.removed.tableName);
22804
+ claimedTypes.add(newTypeName);
22805
+ }
22806
+ }
22807
+ for (const candidate of candidates) {
22808
+ const addedFieldNames = availableRenameTargets(candidate, claimedFields);
22809
+ if (addedFieldNames.length === 0) continue;
22810
+ const newFieldName = await promptRenameCandidate(candidate, addedFieldNames);
22811
+ if (newFieldName) {
22812
+ confirmed.push({
22813
+ tableName: candidate.tableName,
22814
+ previousFieldName: candidate.removed.fieldName,
22815
+ fieldName: newFieldName
22816
+ });
22817
+ claimedFields.add(`${candidate.tableName}.${candidate.removed.fieldName}`);
22818
+ claimedFields.add(`${candidate.tableName}.${newFieldName}`);
22819
+ }
22820
+ }
22821
+ }
22822
+ if (confirmed.length === 0 && confirmedTypes.length === 0) return {
22823
+ diff,
22824
+ unresolved
22825
+ };
22826
+ return {
22827
+ diff: compareSnapshots(previousSnapshot, currentSnapshot, {
22828
+ fieldRenames: confirmed,
22829
+ typeRenames: confirmedTypes
22830
+ }),
22831
+ unresolved
22832
+ };
22833
+ }
22834
+ /**
22835
+ * Generate migration files from a diff resolved during preflight
21387
22836
  * @param {SchemaSnapshot} previousSnapshot - Previous schema snapshot
21388
- * @param {SchemaSnapshot} currentSnapshot - Current schema snapshot
22837
+ * @param {MigrationDiff} diff - Diff with confirmed renames recorded
21389
22838
  * @param {string} migrationsDir - Migrations directory path
21390
22839
  * @param {GenerateOptions} options - Generate options
22840
+ * @param currentSnapshot - Schema the user now declares
22841
+ * @param expandPlans - Field changes confirmed for a migration pair
21391
22842
  * @returns {Promise<void>} Promise that resolves when diff is generated
21392
22843
  */
21393
- async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migrationsDir, options) {
21394
- const diff = compareSnapshots(previousSnapshot, currentSnapshot);
22844
+ async function generateDiffFromSnapshot(previousSnapshot, diff, migrationsDir, options, currentSnapshot, expandPlans = []) {
21395
22845
  if (!hasChanges(diff)) {
21396
22846
  logger.info("No schema differences detected.");
21397
22847
  return;
@@ -21400,18 +22850,30 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
21400
22850
  logger.log(formatMigrationDiff(diff));
21401
22851
  logger.newline();
21402
22852
  logger.info(`Summary: ${formatDiffSummary(diff)}`);
21403
- const unsupportedChanges = diff.breakingChanges.filter((change) => change.unsupported);
22853
+ const plannedKeys = new Set(expandPlans.map((plan) => fieldKey(plan.tableName, plan.fieldName)));
22854
+ const unsupportedChanges = diff.breakingChanges.filter((change) => change.unsupported && !(change.fieldName && plannedKeys.has(fieldKey(change.tableName, change.fieldName))));
21404
22855
  if (unsupportedChanges.length > 0) {
21405
22856
  for (const change of unsupportedChanges) {
21406
22857
  logger.newline();
21407
- logger.error(`Unsupported change: ${change.typeName}.${change.fieldName}`);
22858
+ logger.error(`Unsupported change: ${change.tableName}.${change.fieldName}`);
21408
22859
  logger.error(` ${change.reason}`);
21409
22860
  }
22861
+ const convertible = unsupportedChanges.filter(({ tableName, fieldName }) => fieldName !== void 0 && canConvertField({
22862
+ previous: previousSnapshot,
22863
+ current: currentSnapshot,
22864
+ tableName,
22865
+ fieldName
22866
+ }));
22867
+ if (convertible.length > 0) {
22868
+ logger.newline();
22869
+ logger.info("Convert these fields through a temporary field with:");
22870
+ for (const { tableName, fieldName } of convertible) logger.info(` --expand-contract "${tableName}.${fieldName}"`);
22871
+ }
21410
22872
  if (unsupportedChanges.some((change) => change.showThreeStepHint)) {
21411
22873
  logger.newline();
21412
22874
  for (const line of getUnsupportedMigrationHintLines()) logger.info(line);
21413
22875
  }
21414
- const details = unsupportedChanges.map((c) => ` - ${c.typeName}.${c.fieldName}: ${c.reason}`).join("\n");
22876
+ const details = unsupportedChanges.map((c) => ` - ${c.tableName}.${c.fieldName}: ${c.reason}`).join("\n");
21415
22877
  throw new Error(`Unsupported schema changes detected:\n${details}`);
21416
22878
  }
21417
22879
  if (diff.hasBreakingChanges) {
@@ -21432,6 +22894,17 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
21432
22894
  logger.newline();
21433
22895
  logger.warn(formatWarnings(diff.warnings));
21434
22896
  }
22897
+ if (expandPlans.length > 0) {
22898
+ await generateExpandContractMigrations({
22899
+ previousSnapshot,
22900
+ currentSnapshot,
22901
+ resolvedDiff: diff,
22902
+ plans: expandPlans,
22903
+ migrationsDir,
22904
+ description: options.name
22905
+ });
22906
+ return;
22907
+ }
21435
22908
  const result = await generateDiffFiles(diff, migrationsDir, getNextMigrationNumber(migrationsDir), previousSnapshot, options.name);
21436
22909
  logger.success(`Generated migration ${styles.bold(formatMigrationNumber(result.migrationNumber))}`);
21437
22910
  logger.info(` Diff file: ${result.diffFilePath}`);
@@ -21464,6 +22937,46 @@ async function generateDiffFromSnapshot(previousSnapshot, currentSnapshot, migra
21464
22937
  });
21465
22938
  }
21466
22939
  /**
22940
+ * Write the two migrations that carry a field type change: one that converts
22941
+ * values into a temporary field, and one that renames it back.
22942
+ * @param input - Snapshots, confirmed plans, and output location
22943
+ * @returns {Promise<void>} Promise that resolves when both migrations are written
22944
+ */
22945
+ async function generateExpandContractMigrations(input) {
22946
+ const { previousSnapshot, currentSnapshot, resolvedDiff, plans, migrationsDir, description } = input;
22947
+ const intermediateSnapshot = buildIntermediateSnapshot(previousSnapshot, plans);
22948
+ const expandDiff = buildExpandDiff(previousSnapshot, intermediateSnapshot, plans);
22949
+ const confirmedFieldRenames = resolvedDiff.changes.filter((change) => change.kind === "field_renamed").map(({ tableName, previousFieldName, fieldName }) => ({
22950
+ tableName,
22951
+ previousFieldName,
22952
+ fieldName
22953
+ }));
22954
+ const confirmedTypeRenames = resolvedDiff.changes.filter((change) => change.kind === "table_renamed").map(({ previousTableName, tableName }) => ({
22955
+ previousTableName,
22956
+ tableName
22957
+ }));
22958
+ const contractDiff = compareSnapshots(intermediateSnapshot, currentSnapshot, {
22959
+ fieldRenames: [...confirmedFieldRenames, ...plans.map((plan) => ({
22960
+ tableName: plan.tableName,
22961
+ previousFieldName: plan.tempFieldName,
22962
+ fieldName: plan.fieldName
22963
+ }))],
22964
+ typeRenames: confirmedTypeRenames
22965
+ });
22966
+ const expandNumber = getNextMigrationNumber(migrationsDir);
22967
+ if (expandNumber + 1 > 9999) throw new Error(`Converting a field type needs two migration numbers, and ${formatMigrationNumber(MAX_MIGRATION_NUMBER)} is the last one available. Re-baseline the history first.`);
22968
+ const expand = await generateDiffFiles(expandDiff, migrationsDir, expandNumber, previousSnapshot, description, plans);
22969
+ const contract = await generateDiffFiles(contractDiff, migrationsDir, expandNumber + 1, intermediateSnapshot, description);
22970
+ const fields = plans.map((plan) => `${plan.tableName}.${plan.fieldName}`).join(", ");
22971
+ logger.success(`Generated migrations ${styles.bold(formatMigrationNumber(expand.migrationNumber))} and ${styles.bold(formatMigrationNumber(contract.migrationNumber))} to convert ${fields}`);
22972
+ logger.info(` Diff files: ${expand.diffFilePath}, ${contract.diffFilePath}`);
22973
+ if (expand.migrateFilePath) logger.info(` Conversion script: ${expand.migrateFilePath}`);
22974
+ if (contract.migrateFilePath) logger.info(` Copy script: ${contract.migrateFilePath}`);
22975
+ logger.newline();
22976
+ logger.info(`Edit the conversion in ${formatMigrationNumber(expand.migrationNumber)} before deploying. The copy script in ${formatMigrationNumber(contract.migrationNumber)} is complete, though that migration also carries any other change in this run.`, { mode: "plain" });
22977
+ logger.info("Both migrations are applied by 'tailor deploy'.", { mode: "plain" });
22978
+ }
22979
+ /**
21467
22980
  * Offer to record a --no-script acknowledgment for a warning-only migration,
21468
22981
  * or print the follow-up commands when the session is non-interactive
21469
22982
  * @param {AcknowledgeWarningsOptions} options - Target migration and prompt behavior
@@ -21517,14 +23030,20 @@ const generateCommand = defineAppCommand({
21517
23030
  alias: "n",
21518
23031
  description: "Optional description for the migration"
21519
23032
  }),
21520
- init: arg(z.boolean().default(false), { description: "Delete existing migrations and start fresh" })
23033
+ init: arg(z.boolean().default(false), { description: "Delete existing migrations and start fresh" }),
23034
+ rename: arg(z.array(z.string()).optional(), { description: "Record a field or type rename instead of remove + add (format: \"Type.oldField:newField\" or \"OldType:NewType\"; repeatable). Renames require a migration script that copies the data." }),
23035
+ drop: arg(z.array(z.string()).optional(), { description: "Confirm that a removed field or type is a genuine removal, not a rename (format: \"Type.field\" or \"Type\"; repeatable). Required in non-interactive runs for a removal with rename candidates." }),
23036
+ "expand-contract": arg(z.array(z.string()).optional(), { description: "Convert a field type through a temporary field (format: \"Type.field\"; repeatable). Generates two migrations." })
21521
23037
  }),
21522
23038
  run: async (args) => {
21523
23039
  await generate({
21524
23040
  configPath: args.config,
21525
23041
  name: args.name,
21526
23042
  yes: args.yes,
21527
- init: args.init
23043
+ init: args.init,
23044
+ renames: args.rename,
23045
+ drops: args.drop,
23046
+ expandContracts: args["expand-contract"]
21528
23047
  });
21529
23048
  }
21530
23049
  });
@@ -23942,11 +25461,15 @@ function errorToJson(error, options) {
23942
25461
  ...options?.includeStack && error.stack ? { stack: error.stack } : {}
23943
25462
  } };
23944
25463
  }
23945
- if (error instanceof Error) return { error: {
23946
- code: error.name === "CIPromptError" ? "INTERACTIVE_PROMPT_REQUIRED" : "UNEXPECTED_ERROR",
23947
- message: error.message,
23948
- ...options?.includeStack && error.stack ? { stack: error.stack } : {}
23949
- } };
25464
+ if (error instanceof Error) {
25465
+ const suggestion = typeOnlyImportHint(error);
25466
+ return { error: {
25467
+ code: error.name === "CIPromptError" ? "INTERACTIVE_PROMPT_REQUIRED" : "UNEXPECTED_ERROR",
25468
+ message: error.message,
25469
+ ...suggestion ? { suggestion } : {},
25470
+ ...options?.includeStack && error.stack ? { stack: error.stack } : {}
25471
+ } };
25472
+ }
23950
25473
  return { error: {
23951
25474
  code: "UNKNOWN_ERROR",
23952
25475
  message: String(error)
@@ -24016,5 +25539,5 @@ async function registerTsHook(tsHookUrl) {
24016
25539
  }
24017
25540
 
24018
25541
  //#endregion
24019
- export { treeCommand as $, formatDiffSummary as $n, listWorkspaces as $t, truncate as A, MIGRATION_NUMBER_PATTERN as An, removeLegacyUserAlias as Ar, triggerExecutor as At, MIGRATION_REVIEW_REQUIRED_MARKER as B, getMigrationDirPath as Bn, isVerbose as Br, getCommand$5 as Bt, healthCommand as C, captureFileState as Cn, loadConsoleBaseUrl as Cr, listCommand$7 as Ct, resumeWorkflow as D, INITIAL_SCHEMA_NUMBER as Dn, loadWorkspaceId as Dr, listWebhookExecutors as Dt, resumeCommand as E, DIFF_FILE_NAME as En, loadStoredUserTokens as Er, getFunctionRegistry as Et, chunkSeedData as F, compareLocalTypesWithSnapshot as Fn, defineAppCommand as Fr, jobsCommand as Ft, showCommand as G, loadSnapshot as Gn, toPageDirection as Gr, functionExecutionStatusToString as Gt, deployCommand as H, getMigrationFiles as Hn, pagedLogArgs as Hr, executionsCommand as Ht, bundleSeedScript as I, compareSnapshots as In, commonArgs as Ir, listExecutorJobs as It, remove as J, MIGRATION_HISTORY_LABEL_KEY as Jn, deploy as Jt, extractOwnedNamespaces as K, normalizeSchemaSnapshot as Kn, workspaceArgs as Kr, getCommand$6 as Kt, generate as L, createSnapshotFromLocalTypes as Ln, configArg as Lr, watchExecutorJob as Lt, loadSeedContext as M, UnsupportedMigrationFileVersionError as Mn, resolveUserTokenKey as Mr, listExecutors as Mt, loadApplicationNamespaces as N, assertMigrationNumberExists as Nn, saveUserTokens as Nr, getExecutorJob as Nt, listCommand$2 as O, MIGRATE_FILE_NAME as On, platformConfigFromProfile as Or, webhookCommand as Ot, loadTailorDBNamespaces as P, assertValidMigrationFiles as Pn, writePlatformConfig as Pr, getExecutorWaitFailureMessage as Pt, organizationTree as Q, sanitizeMigrationLabel as Qn, listCommand$9 as Qt, generateCommand as R, formatSchemaDrifts as Rn, confirmationArgs as Rr, startCommand as Rt, getAppHealth as S, protoGqlPermission as Sn, loadConfigPath as Sr, generate$1 as St, waitWorkflowExecution as T, DB_TYPES_FILE_NAME as Tn, loadPlatformClientConfig as Tr, getCommand$4 as Tt, deployStaticWebsite as U, getNextMigrationNumber as Un, paginationArgs as Ur, getWorkflowExecution as Ut, generateSchemaFile as V, getMigrationFilePath as Vn, multiConfigArg as Vr, getWorkflow as Vt, show as W, loadDiff as Wn, resolveMachineUserInputSource as Wr, listWorkflowExecutions as Wt, updateCommand$1 as X, createMigrationHistoryId as Xn, deployMigrationTestBaseline as Xt, removeCommand$1 as Y, MIGRATION_LABEL_KEY as Yn, deployFromCLI as Yt, updateOrganization as Z, handleOptionalToRequiredError as Zn, deployMigrationTestTarget as Zt, getWorkspace as _, verifyRemoteSchema as _n, deleteUserTokens as _r, getOAuth2Client as _t, queryCommand as a, ensureConfigId as an, sdkAppIdLabelKey as ar, updateFolder as at, listApps as b, compareSnapshotWithRemote as bn, hasUserTokenEntry as br, listCommand$6 as bt, removeCommand as c, executeScript as cn, writeMetadataLabels as cr, getCommand$2 as ct, listUsers as d, bundleMigrationScript as dn, formatMigrationNumber as dr, deleteFolder as dt, createCommand$1 as en, formatMigrationDiff as er, listCommand$3 as et, inviteCommand as f, checkMigrationDiffs as fn, parseMigrationNumberArg as fr, createCommand as ft, getCommand as g, toTailorDBDeployInput as gn, loadConfig as gr, getCommand$3 as gt, restoreWorkspace as h, logRemoteDriftGuidance as hn, assertWritable as hr, listOAuth2Clients as ht, query as i, workspaceNameSchema as in, resourceTrn as ir, updateCommand$2 as it, truncateCommand as j, SCHEMA_FILE_NAME as jn, resolveConfigUser as jr, listCommand$8 as jt, listWorkflows as k, MIGRATE_TEST_FILE_NAME as kn, readPlatformConfig as kr, triggerCommand as kt, removeUser as l, waitForExecution as ln, PluginManager as lr, getFolder as lt, restoreCommand as m, formatRemoteVerificationResults as mn, apiCall as mr, listCommand$5 as mt, errorToJson as n, createWorkspace$1 as nn, getNamespacesWithMigrations as nr, getCommand$1 as nt, updateCommand as o, getMigrationMachineUser as on, sdkNameLabelKey as or, listCommand$4 as ot, inviteUser as p, fetchRemoteSchemaSnapshot as pn, prompt as pr, createFolder as pt, logBetaWarning as q, reconstructSnapshotFromMigrations as qn, getExecutor as qt, serializeError as r, formatKeyValueTable as rn, selectTargetNamespace as rr, getOrganization as rt, updateUser as s, updateMigrationLabel as sn, sdkVersionLabelKey as sr, listFolders as st, registerTsHook as t, createValidatedWorkspaceWithClient as tn, hasChanges as tr, listOrganizations as tt, listCommand as u, formatMigrationScriptCommand as un, generateUserTypes as ur, deleteCommand$1 as ut, deleteCommand as v, fetchRemoteMigrationNumber as vn, fetchLatestToken as vr, getMachineUserToken as vt, waitCommand as w, captureMigrationFileState as wn, loadMachineUserName as wr, listFunctionRegistries as wt, listCommand$1 as x, generateAllTypeManifestsFromSnapshot as xn, loadAccessToken as xr, listMachineUsers as xt, deleteWorkspace as y, fetchRemoteMigrationState as yn, hasAnyUserTokenEntry as yr, tokenCommand as yt, scriptCommand as z, getLatestMigrationNumber as zn, deploymentArgs as zr, startWorkflow as zt };
24020
- //# sourceMappingURL=register-ts-hook-CTth1eqj.mjs.map
25542
+ export { treeCommand as $, sanitizeMigrationLabel as $n, listWorkspaces as $t, truncate as A, MIGRATION_FILE_NAMES as An, readPlatformConfig as Ar, triggerExecutor as At, MIGRATION_REVIEW_REQUIRED_MARKER as B, getLatestMigrationNumber as Bn, deploymentArgs as Br, getCommand$5 as Bt, healthCommand as C, captureFileState as Cn, loadConfigPath as Cr, listCommand$7 as Ct, resumeWorkflow as D, INITIAL_SCHEMA_NUMBER as Dn, loadStoredUserTokens as Dr, listWebhookExecutors as Dt, resumeCommand as E, DIFF_FILE_NAME as En, loadPlatformClientConfig as Er, getFunctionRegistry as Et, chunkSeedData as F, assertValidMigrationFiles as Fn, writePlatformConfig as Fr, jobsCommand as Ft, showCommand as G, loadDiff as Gn, resolveMachineUserInputSource as Gr, functionExecutionStatusToString as Gt, deployCommand as H, getMigrationFilePath as Hn, multiConfigArg as Hr, executionsCommand as Ht, bundleSeedScript as I, compareLocalTypesWithSnapshot as In, defineAppCommand as Ir, listExecutorJobs as It, remove as J, reconstructSnapshotFromMigrations as Jn, deploy as Jt, extractOwnedNamespaces as K, loadSnapshot as Kn, toPageDirection as Kr, getCommand$6 as Kt, generate as L, compareSnapshots as Ln, commonArgs as Lr, watchExecutorJob as Lt, loadSeedContext as M, SCHEMA_FILE_NAME as Mn, resolveConfigUser as Mr, listExecutors as Mt, loadApplicationNamespaces as N, UnsupportedMigrationFileVersionError as Nn, resolveUserTokenKey as Nr, getExecutorJob as Nt, listCommand$2 as O, MIGRATE_FILE_NAME as On, loadWorkspaceId as Or, webhookCommand as Ot, loadTailorDBNamespaces as P, assertMigrationNumberExists as Pn, saveUserTokens as Pr, getExecutorWaitFailureMessage as Pt, organizationTree as Q, handleOptionalToRequiredError as Qn, listCommand$9 as Qt, generateCommand as R, createSnapshotFromLocalTypes as Rn, configArg as Rr, startCommand as Rt, getAppHealth as S, protoGqlPermission as Sn, loadAccessToken as Sr, generate$1 as St, waitWorkflowExecution as T, DB_TYPES_FILE_NAME as Tn, loadMachineUserName as Tr, getCommand$4 as Tt, deployStaticWebsite as U, getMigrationFiles as Un, pagedLogArgs as Ur, getWorkflowExecution as Ut, generateSchemaFile as V, getMigrationDirPath as Vn, isVerbose as Vr, getWorkflow as Vt, show as W, getNextMigrationNumber as Wn, paginationArgs as Wr, listWorkflowExecutions as Wt, updateCommand$1 as X, MIGRATION_LABEL_KEY as Xn, deployMigrationTestBaseline as Xt, removeCommand$1 as Y, MIGRATION_HISTORY_LABEL_KEY as Yn, deployFromCLI as Yt, updateOrganization as Z, createMigrationHistoryId as Zn, deployMigrationTestTarget as Zt, getWorkspace as _, verifyRemoteSchema as _n, loadConfig as _r, getOAuth2Client as _t, queryCommand as a, ensureConfigId as an, resourceTrn as ar, updateFolder as at, listApps as b, compareSnapshotWithRemote as bn, hasAnyUserTokenEntry as br, listCommand$6 as bt, removeCommand as c, executeScript as cn, sdkVersionLabelKey as cr, getCommand$2 as ct, listUsers as d, bundleMigrationScript as dn, generateUserTypes as dr, deleteFolder as dt, createCommand$1 as en, formatDiffSummary as er, listCommand$3 as et, inviteCommand as f, checkMigrationDiffs as fn, formatMigrationNumber as fr, createCommand as ft, getCommand as g, toTailorDBDeployInput as gn, assertWritable as gr, getCommand$3 as gt, restoreWorkspace as h, logRemoteDriftGuidance as hn, apiCall as hr, listOAuth2Clients as ht, query as i, workspaceNameSchema as in, selectTargetNamespace as ir, updateCommand$2 as it, truncateCommand as j, MIGRATION_NUMBER_PATTERN as jn, removeLegacyUserAlias as jr, listCommand$8 as jt, listWorkflows as k, MIGRATE_TEST_FILE_NAME as kn, platformConfigFromProfile as kr, triggerCommand as kt, removeUser as l, waitForExecution as ln, writeMetadataLabels as lr, getFolder as lt, restoreCommand as m, formatRemoteVerificationResults as mn, prompt as mr, listCommand$5 as mt, errorToJson as n, createWorkspace$1 as nn, hasChanges as nr, getCommand$1 as nt, updateCommand as o, getMigrationMachineUser as on, sdkAppIdLabelKey as or, listCommand$4 as ot, inviteUser as p, fetchRemoteSchemaSnapshot as pn, parseMigrationNumberArg as pr, createFolder as pt, logBetaWarning as q, normalizeSchemaSnapshot as qn, workspaceArgs as qr, getExecutor as qt, serializeError as r, formatKeyValueTable as rn, getNamespacesWithMigrations as rr, getOrganization as rt, updateUser as s, updateMigrationLabel as sn, sdkNameLabelKey as sr, listFolders as st, registerTsHook as t, createValidatedWorkspaceWithClient as tn, formatMigrationDiff as tr, listOrganizations as tt, listCommand as u, formatMigrationScriptCommand as un, PluginManager as ur, deleteCommand$1 as ut, deleteCommand as v, fetchRemoteMigrationNumber as vn, deleteUserTokens as vr, getMachineUserToken as vt, waitCommand as w, captureMigrationFileState as wn, loadConsoleBaseUrl as wr, listFunctionRegistries as wt, listCommand$1 as x, generateAllTypeManifestsFromSnapshot as xn, hasUserTokenEntry as xr, listMachineUsers as xt, deleteWorkspace as y, fetchRemoteMigrationState as yn, fetchLatestToken as yr, tokenCommand as yt, scriptCommand as z, formatSchemaDrifts as zn, confirmationArgs as zr, startWorkflow as zt };
25543
+ //# sourceMappingURL=register-ts-hook-ClI226n2.mjs.map