Repository navigation
fix(migrator): addIndex alias order, dbtype caching, helper dedup, diffAll rethrow - #2937
Conversation
…ffAll rethrow Addresses seven framework-review findings in the migrator (refs migrator:3, migrator:5, migrator:7, migrator:9, migrator:11, migrator:13, migrator:14): - Migration.addIndex(): compute the default indexName in the body after $combineArguments resolves the columnName alias — the parameter-default expression dereferenced arguments.columnNames and threw an undefined-key error on the documented columnName path (MG3). - Base.$getDBType(): memoize the resolved adapter name per datasource on the application scope; discovery instantiates every migration CFC and each init() previously triggered a fresh $dbinfo(version) round-trip (MG11). - Base.$execute()/$executeWithParams(): extract the duplicated terminator/SQL-file/debug-capture pipeline into $prepareMigrationSql(), replace the per-statement $dbinfo(version) round-trip with the memoized $getDBType(), and give $executeWithParams a dataSource override matching its sibling (MG5). - Base.$getColumns(): request-scoped cache keyed by datasource + table; $execute() drops the cache so same-request DDL is re-probed. addRecord() now probes table metadata once per record instead of twice (MG13). - Migration.dropTable(): use this.adapter.adapterName() instead of re-sniffing the engine; Base.$getForeignKeys() probes the single table with a zero-row SELECT instead of listing every table in the schema (MG14). - TableDefinition: dedupe the fourteen byte-identical typed column helpers into a shared $addTypedColumns() helper, preserving the float() and uniqueidentifier() outlier defaults and timestamp()'s columnType pass-through verbatim (MG7). - AutoMigrator.diffAll(): validate heuristicThreshold up front and rethrow deliberate validation throws (InvalidThreshold, InvalidRenameHint, DuplicateRenameHint, RenameHintTypeMismatch) instead of swallowing them in the per-model missing-table catch and reporting "no drift" (MG9). New specs (verified red on pre-fix code, green post-fix, Lucee 7 + SQLite): MigratorBaseCachingSpec (both caches), migrationCommandsSpec (addIndex columnName alias), autoMigratorSpec (diffAll threshold + hint rethrow). Full migrator area: 270 pass / 0 fail / 0 error / 6 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Peter Amiri <peter@alurium.com>
There was a problem hiding this comment.
Wheels Bot — Reviewer A
This PR addresses seven internal migrator findings (MG3, MG5, MG7, MG9, MG11, MG13, MG14) from a multi-agent framework audit: an addIndex crash on the columnName alias, two redundant DB-round-trip hotspots ($getDBType and $getColumns), duplicate SQL pre-execution pipeline, dropTable re-sniff, fourteen byte-identical typed-column helpers, and silent swallowing of validation throws in diffAll. The changes are well-scoped, the cross-engine annotations are accurate, and the three new BDD specs cover the three riskiest paths. Verdict: comment — one low-severity correctness nit and one docs gap; no blocking issues.
Correctness
$getColumns() cache key folds to lowercase but the $dbinfo probe does not — case-sensitive database mismatch.
vendor/wheels/migrator/Base.cfc, line 211:
local.cacheKey = LCase(application[local.appKey].dataSourceName & "|" & arguments.tableName);The cache key is case-folded, but the downstream $dbinfo(type="columns", table=arguments.tableName) call uses the original case. On a case-sensitive database (PostgreSQL with quoted identifiers, Oracle, case-sensitive SQLite), two callers using different case for the same table name — e.g., $getColumns("Authors") followed by $getColumns("authors") — share the same cache slot. The second call returns data from the first lookup even if the actual tables differ.
Low risk in practice (migration code usually passes consistent casing) but technically incorrect. Recommend keying on the original arguments.tableName without case folding, or documenting that the caller is responsible for consistent casing:
// consistent with $dbinfo(table=arguments.tableName)
local.cacheKey = application[local.appKey].dataSourceName & "|" & arguments.tableName;Cross-engine
No blocking issues found.
The $addTypedColumns refactor calls column(argumentCollection = arguments.args) where arguments.args is a plain struct parameter — not the raw arguments scope itself. Cross-engine invariant #10 (Adobe 2023/2025 rejecting the arguments scope as attributeCollection on built-in tags) does not apply here: column() is a CFC method, not a built-in CFML tag, and the value passed is a regular struct. The $addTypedColumns private helper lives on TableDefinition.cfc, a standalone CFC instantiated with CreateObject, not a mixin — invariant #7 (private mixin functions not integrated) does not apply either.
The new $getForeignKeys table-existence probe uses SELECT 1 FROM #local.quotedTable# WHERE 1=0. The fallback branch StructKeyExists(this, "adapter") ? ... : arguments.table passes an unquoted table name into the SQL when no adapter is present. In practice this branch is unreachable because Migration.init() always populates this.adapter and $getForeignKeys is only called from dropTable(). But the dead code reads like a safety net that silently breaks on table names with spaces or dashes. Consider a Throw instead of the silent fallback, or remove the ternary and always require the adapter.
The BoxLang catch-body invariant (#11 — local.X = ... inside catch is discarded) is correctly addressed in both $getForeignKeys (local.state = {tableExists = true}) and the new BDD specs (shared-struct pattern throughout). Well done.
Tests
MigratorBaseCachingSpec.cfc line 38 poisons the application-scope $migratorAdapterNames cache with "SentinelAdapter" during the $getDBType test. The finally block at line 42 correctly restores the real value, so the isolation is sound.
No test covers the $addTypedColumns deduplication directly (e.g., verifying that float()'s default=""/allowNull=true outlier defaults survive the refactor). The existing TableDefinition specs exercise the public helpers indirectly, which is sufficient, but a single spot-check would make the preservation of those defaults explicit.
Docs
vendor/wheels/migrator/CLAUDE.md does not mention the two new caches introduced by this PR:
application[appKey].$migratorAdapterNames(application-scoped, keyed by datasource name) — memoized adapter name per datasource, written by$getDBType()request.$wheelsMigratorColumns(request-scoped, keyed bydsName|tableName) — column list per table, written by$getColumns(), dropped wholesale by$execute()
A future agent debugging a stale-schema or wrong-adapter issue will grep for $migratorAdapterNames and find nothing in the reference doc. A short paragraph in the migrator CLAUDE.md under an "Internal caches" heading would make these discoverable.
No CHANGELOG entry — acknowledged in the PR body as deliberate (campaign consolidated). Acceptable.
Commits
fix(migrator): addIndex alias order, dbtype caching, helper dedup, diffAll rethrow — valid type, valid scope, header within 100 chars, sentence-case, reflects the why. Clean.
Security
No SQL injection vectors introduced. The $getForeignKeys probe routes the table name through this.adapter.quoteTableName() before interpolating it into SQL. The $getColumns cache key uses only the datasource name and table name from internal application config, not user input.
Wheels Bot — Reviewer B (round 1)A's review is well-calibrated. I verified every cited diff location, the cross-engine reasoning, and the test isolation claim. No sycophancy, no false positives. All findings are confirmed accurate and actionable. Verdict alignment is correct. Converging on changes needed. SycophancyNone detected. A issued a "comment" verdict (not "approve") and cited specific file/line evidence for each finding. False positivesNone detected.
Missed issuesNone of materiality. Two I checked and ruled out:
Verdict alignment"Comment" is consistent with findings that are all non-blocking (low-severity correctness nit, dead-code cleanup, minor test gap, docs gap). Correct call. ConvergenceAligned. Joint recommendation is to apply A's four concrete suggestions before merge: (1) remove |
Summary
Wave-2 remediation for the migration-helpers package of the internal framework review. Seven migrator findings fixed in a single commit: a crash on the documented
columnNamepath ofaddIndex(), two redundant-DB-round-trip hotspots (engine sniffing and table-metadata probes), a duplicated SQL pre-execution pipeline, an engine re-sniff indropTable()plus a whole-schema FK probe, fourteen byte-identical typed column helpers, anddiffAll()swallowing its own validation throws.Findings addressed
addIndex()defaultindexNamedereferencesarguments.columnNamesbefore$combineArgumentsresolves thecolumnNamealias (undefined-key throw) @vendor/wheels/migrator/Migration.cfc:418-432— default now computed in the body after$combineArguments.$execute()/$executeWithParams()duplicate the terminator / SQL-file / debug-capture pipeline, with a per-statement$dbinfo(version)round-trip @vendor/wheels/migrator/Base.cfc:177— extracted into$prepareMigrationSql()preserving the exact old pipeline order; Oracle-terminator check now uses memoized$getDBType();$executeWithParams()gains thedataSourceoverride its sibling already had (Base.cfc:159).TableDefinition@vendor/wheels/migrator/TableDefinition.cfc:133— deduped into shared$addTypedColumns(), preserving thefloat()/uniqueidentifier()outlier defaults andtimestamp()'scolumnTypepass-through verbatim.diffAll()swallows deliberate validation throws in the per-model missing-table catch and reports "no drift" @vendor/wheels/migrator/AutoMigrator.cfc:185-231—heuristicThresholdvalidated up front (mirroringRenameDetector's range check);Wheels.InvalidThreshold,Wheels.InvalidRenameHint,Wheels.DuplicateRenameHint,Wheels.RenameHintTypeMismatchare rethrown.init()triggers a fresh$dbinfo(version)round-trip during discovery @vendor/wheels/migrator/Base.cfc:15—$getDBType()memoizes the resolved adapter name per datasource on the application scope.addRecord()probes table metadata twice per record @vendor/wheels/migrator/Base.cfc:202—$getColumns()gets a request-scoped cache keyed by datasource + table;$execute()(DDL) invalidates the cache (Base.cfc:148-150) while$executeWithParams()(data-only) does not.dropTable()re-sniffs the engine and$getForeignKeys()lists every table in the schema @vendor/wheels/migrator/Migration.cfc:119+vendor/wheels/migrator/Base.cfc:103— usesthis.adapter.adapterName()(byte-identical to$getDBType()strings across all 7 adapters) and a zero-row single-table SELECT probe.Findings verified already-fixed
None — all seven findings reproduced against current
origin/develop(spot-confirmed by the reviewer for MG3 atMigration.cfc:416-420and MG13).Source
Internal multi-agent framework review 2026-06-09, wave 2, package migration-helpers.
Tests
vendor/wheels/tests/specs/migrator/(written red against pre-fix code, green post-fix):MigratorBaseCachingSpec.cfc— both caches; uses a poison-the-cache technique to distinguish cached reads from re-probes, and verifies$execute()invalidates the$getColumns()cache.migrationCommandsSpec.cfc—addIndex()via thecolumnNamealias (the MG3 crash path).autoMigratorSpec.cfc—diffAll()throws on invalid threshold and rethrows rename-hint validation errors.Cross-engine notes
local.Xincatchdoes not persist on BoxLang).argumentCollection = arguments.argsforwarding in$addTypedColumns()has in-tree precedent (Injector.cfc:174,controller/filters.cfm:66).quoteTableNameoverride;adapterName()returns are byte-identical to the old$getDBType()strings for all 7 adapters.databaseAdapters/Abstract.cfc:300(unreachable via the fixed Migration path);$getForeignKeys()now maps any probe failure to "no FKs", shifting failure to the subsequent DROP; two parallel application-scope engine caches could be unified later.Changelog
Entry deliberately omitted; consolidated at campaign end.
🤖 Generated with Claude Code