@vibeorm/migrate 1.2.0 → 1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeorm/migrate",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Migration, introspection, and schema diff toolkit for VibeORM",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -37,6 +37,6 @@
37
37
  "bun": ">=1.1.0"
38
38
  },
39
39
  "dependencies": {
40
- "@vibeorm/parser": "1.2.0"
40
+ "@vibeorm/parser": "1.3.0"
41
41
  }
42
42
  }
@@ -447,7 +447,7 @@ export async function introspect(params: { executor: SqlExecutor }): Promise<Sch
447
447
  const isUnique = uniqueColumns.has(`${tableName}.${col.columnName}`);
448
448
  const parsedDefault = parseDefault({ columnDefault: col.columnDefault, dataType: col.dataType });
449
449
 
450
- // Check if this is an enum column
450
+ // Branch 1: scalar enum column (data_type=USER-DEFINED, udt_name=EnumName).
451
451
  if (col.dataType === "USER-DEFINED" && enumNames.has(col.udtName)) {
452
452
  const enumField: EnumField = {
453
453
  kind: "enum",
@@ -462,8 +462,33 @@ export async function introspect(params: { executor: SqlExecutor }): Promise<Sch
462
462
  documentation: undefined,
463
463
  };
464
464
  fields.push(enumField);
465
- } else {
466
- // Map PG type to Prisma type
465
+ }
466
+ // Branch 2: enum-array column. Postgres reports the underlying array type
467
+ // as `_EnumName` (the leading underscore is the array-type naming convention),
468
+ // and `data_type` is `ARRAY`. Without this branch the field would be parsed
469
+ // as `String[]` and the round-trip drift differ would always want to ALTER it.
470
+ // Bug 2 — see .ai/bug-report-2026-05-24.md.
471
+ else if (
472
+ col.dataType === "ARRAY" &&
473
+ col.udtName.startsWith("_") &&
474
+ enumNames.has(col.udtName.slice(1))
475
+ ) {
476
+ const enumField: EnumField = {
477
+ kind: "enum",
478
+ name: col.columnName,
479
+ dbName: col.columnName,
480
+ enumName: col.udtName.slice(1),
481
+ isRequired: !col.isNullable,
482
+ isList: true,
483
+ isId,
484
+ isUnique,
485
+ default: parsedDefault,
486
+ documentation: undefined,
487
+ };
488
+ fields.push(enumField);
489
+ }
490
+ // Branch 3: scalar / scalar-array fallback.
491
+ else {
467
492
  const pgType = col.dataType === "ARRAY" ? col.udtName.replace(/^_/, "") : col.udtName;
468
493
  const prismaType = PG_TO_PRISMA[pgType] ?? PG_TO_PRISMA[col.dataType] ?? "String";
469
494
  const isArray = col.dataType === "ARRAY";
@@ -72,8 +72,11 @@ function getRelationFields(params: { model: Model }): RelationField[] {
72
72
  function resolveColumnType(params: { field: ScalarField | EnumField; enums: Enum[] }): string {
73
73
  const { field, enums } = params;
74
74
  if (field.kind === "enum") {
75
+ // Enum lists must emit `EnumName[]`, not the scalar enum type. Mirrors
76
+ // `ddl-builder.ts:135-141`. Bug 2 — see .ai/bug-report-2026-05-24.md.
75
77
  const enumDef = enums.find((e) => e.name === field.enumName);
76
- return q({ name: enumDef?.dbName ?? field.enumName });
78
+ const enumType = q({ name: enumDef?.dbName ?? field.enumName });
79
+ return field.isList ? `${enumType}[]` : enumType;
77
80
  }
78
81
  if (field.default?.kind === "autoincrement") {
79
82
  return field.prismaType === "BigInt" ? "BIGSERIAL" : "SERIAL";
@@ -87,8 +90,10 @@ function resolveColumnType(params: { field: ScalarField | EnumField; enums: Enum
87
90
  function resolveBaseColumnType(params: { field: ScalarField | EnumField; enums: Enum[] }): string {
88
91
  const { field, enums } = params;
89
92
  if (field.kind === "enum") {
93
+ // Same enum-list handling as resolveColumnType (Bug 2).
90
94
  const enumDef = enums.find((e) => e.name === field.enumName);
91
- return q({ name: enumDef?.dbName ?? field.enumName });
95
+ const enumType = q({ name: enumDef?.dbName ?? field.enumName });
96
+ return field.isList ? `${enumType}[]` : enumType;
92
97
  }
93
98
  if (field.nativeType) return field.nativeType.toLowerCase();
94
99
  if (field.isList) return `${PRISMA_TO_PG[field.prismaType]}[]`;
@@ -556,11 +561,25 @@ export function diffSchemas(params: { previous: Schema; current: Schema }): Diff
556
561
 
557
562
  // Type change — reverse is setting back to the previous type
558
563
  if (prevType !== currType) {
564
+ // Detect scalar -> array transition of same base type. PG can't implicit-cast,
565
+ // so we add a USING clause that wraps the existing value in a 1-element array
566
+ // (or NULL if the column is NULL). This heals databases broken by an earlier
567
+ // enum-array bug (Bug 2) where the column was created as the scalar type.
568
+ // We intentionally do NOT handle the reverse direction (array -> scalar) — that
569
+ // is lossy and the user must decide which element to keep.
570
+ const isScalarToArray =
571
+ currType.endsWith("[]") && !prevType.endsWith("[]") &&
572
+ currType.slice(0, -2) === prevType;
573
+
574
+ const usingClause = isScalarToArray
575
+ ? ` USING CASE WHEN ${q({ name: colName })} IS NULL THEN NULL ELSE ARRAY[${q({ name: colName })}] END`
576
+ : "";
577
+
559
578
  ops.push({
560
579
  type: "alterColumnType",
561
- sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DATA TYPE ${currType};`,
580
+ sql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DATA TYPE ${currType}${usingClause};`,
562
581
  downSql: `ALTER TABLE ${q({ name: tableName })} ALTER COLUMN ${q({ name: colName })} SET DATA TYPE ${prevType};`,
563
- isDestructive: true,
582
+ isDestructive: !isScalarToArray, // scalar -> array is non-destructive (data preserved as 1-element arrays)
564
583
  isReversible: true,
565
584
  description: `Change type of "${fieldName}" in "${name}" from ${prevType} to ${currType}`,
566
585
  });