diff --git a/vendor/wheels/migrator/AutoMigrator.cfc b/vendor/wheels/migrator/AutoMigrator.cfc index 5aa6f53c7..35fb7c6e6 100644 --- a/vendor/wheels/migrator/AutoMigrator.cfc +++ b/vendor/wheels/migrator/AutoMigrator.cfc @@ -128,10 +128,8 @@ component extends="wheels.migrator.Base" { // "text" where the model declared "string" (and similarly for // the INTEGER/REAL affinities). Normalize the affinity-equivalent // pairs so `migrate diff` doesn't emit spurious `text -> string` - // changes on a schema that is already in sync. - if ($getDBType() == "sqlite" && local.actualMigType == "text" && ListFindNoCase("string,text,datetime,date,time,boolean", local.expectedMigType)) { - local.actualMigType = local.expectedMigType; - } + // changes on a schema that is already in sync (#3565). + local.actualMigType = $normalizeMigTypeForDiff(local.actualMigType, local.expectedMigType); local.typeChanged = (local.expectedMigType != local.actualMigType && local.actualMigType != "unknown"); local.sizeChanged = ( @@ -609,4 +607,29 @@ component extends="wheels.migrator.Base" { return "unknown"; } + /** + * Normalize an actual (DB-probed) migration type against the expected + * (model-introspected) type before the diff comparison, so SQLite's + * type-affinity collapse doesn't read as a spurious type change (#3565). + * + * SQLite stores VARCHAR/TEXT/DATETIME/DATE/TIME under a single TEXT + * storage class. The model layer maps that TEXT back to "string" + * (cf_sql_varchar), while the DB probe maps it to "text", so every stable + * string-like column looked like a `text -> string` change. Return the + * expected type in that case; otherwise return the actual type unchanged. + * + * Public ONLY so autoMigratorSpec can unit-test the affinity families + * (same carve-out as $cfSqlTypeToMigrationType / $dbTypeToMigrationType). + */ + public string function $normalizeMigTypeForDiff(required string actualMigType, required string expectedMigType) { + if ( + $getDBType() == "sqlite" + && arguments.actualMigType == "text" + && ListFindNoCase("string,text,datetime,date,time,boolean", arguments.expectedMigType) + ) { + return arguments.expectedMigType; + } + return arguments.actualMigType; + } + } diff --git a/vendor/wheels/tests/specs/migrator/autoMigratorSpec.cfc b/vendor/wheels/tests/specs/migrator/autoMigratorSpec.cfc index 984a49324..7e3324702 100644 --- a/vendor/wheels/tests/specs/migrator/autoMigratorSpec.cfc +++ b/vendor/wheels/tests/specs/migrator/autoMigratorSpec.cfc @@ -129,6 +129,43 @@ component extends="wheels.WheelsTest" { }); + describe("$normalizeMigTypeForDiff (SQLite text affinity, issue 3565)", () => { + + it("treats text vs string as equivalent", () => { + expect(autoMigrator.$normalizeMigTypeForDiff("text", "string")).toBe("string"); + }); + + it("treats text vs text as a no-op", () => { + expect(autoMigrator.$normalizeMigTypeForDiff("text", "text")).toBe("text"); + }); + + it("treats text vs datetime as equivalent", () => { + expect(autoMigrator.$normalizeMigTypeForDiff("text", "datetime")).toBe("datetime"); + }); + + it("treats text vs date as equivalent", () => { + expect(autoMigrator.$normalizeMigTypeForDiff("text", "date")).toBe("date"); + }); + + it("treats text vs time as equivalent", () => { + expect(autoMigrator.$normalizeMigTypeForDiff("text", "time")).toBe("time"); + }); + + it("treats text vs boolean as equivalent", () => { + expect(autoMigrator.$normalizeMigTypeForDiff("text", "boolean")).toBe("boolean"); + }); + + it("does NOT normalize text vs integer (not a text-affinity pair)", () => { + expect(autoMigrator.$normalizeMigTypeForDiff("text", "integer")).toBe("text"); + }); + + it("does NOT normalize a non-text actual type", () => { + expect(autoMigrator.$normalizeMigTypeForDiff("integer", "string")).toBe("integer"); + expect(autoMigrator.$normalizeMigTypeForDiff("real", "float")).toBe("real"); + }); + + }); + describe("diff()", () => { it("returns a struct with required keys", () => {