From 7087c47fa2e025e22784c8c12856e09bf50f6469 Mon Sep 17 00:00:00 2001 From: Peter Amiri Date: Fri, 11 Sep 2026 03:58:31 -0700 Subject: [PATCH] test(migrator): pin SQLite text-affinity normalization for migrate diff Issue #3565's spurious `text -> string` changes were already fixed by #3584 (inline normalization in AutoMigrator.diff()), but the behavior had no test coverage and the normalization was inlined. Extract it into $normalizeMigTypeForDiff() and unit-test the affinity families so the fix can't regress. The method returns the expected type when SQLite's TEXT storage class was probed ("text") but the model introspected a string-like type (string, text, datetime, date, time, boolean), and otherwise returns the actual type unchanged. diff() now calls it in place of the inline conditional. Signed-off-by: Peter Amiri --- vendor/wheels/migrator/AutoMigrator.cfc | 31 ++++++++++++++-- .../tests/specs/migrator/autoMigratorSpec.cfc | 37 +++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/vendor/wheels/migrator/AutoMigrator.cfc b/vendor/wheels/migrator/AutoMigrator.cfc index 5aa6f53c7e..35fb7c6e69 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 984a493241..7e3324702f 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", () => {