Skip to content

fix(migrator): renameSystemTables skips transaction wrapper on Oracle (#2745) - #2753

Merged
bpamiri merged 6 commits into
developfrom
claude/upbeat-fermat-fd56f2
May 17, 2026
Merged

fix(migrator): renameSystemTables skips transaction wrapper on Oracle (#2745)#2753
bpamiri merged 6 commits into
developfrom
claude/upbeat-fermat-fd56f2

Conversation

@bpamiri

@bpamiri bpamiri commented May 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #2749 — addresses the remaining migrator-rename failure from #2745.

This PR originally fixed both Model.insertAll() and Migrator.renameSystemTables() on Oracle. #2749 shipped the bulk-insert fix with a different approach (INSERT ALL syntax instead of per-row INSERTs) — I rebased to drop my redundant bulk-insert changes and keep only the migrator fix that #2749 explicitly deferred.

Migrator rename: Migrator.renameSystemTables() wrapped DDL in transaction action="begin" { ... commit }, but Oracle implicitly commits DDL and closes the JDBC statement, so the subsequent transaction action="commit" raised ORA: Closed statement. The transaction wrapper is now skipped on Oracle (the existing code comment already called out the wrapper as a no-op there); other engines (Postgres, SQLite via SAVEPOINT, MySQL on InnoDB) keep their atomic-rollback behavior.

Also includes a minor docstring nit for $buildBulkParam to read "Used by adapter bulk insert and upsert implementations" (it's no longer used only by upsert after #2749 wired up $bulkInsertSQL).

Test plan

  • bash tools/test-local.sh migrator — passed on Lucee 7 + SQLite, no regression in rename path
  • bash tools/test-local.sh model — passed on Lucee 7 + SQLite, no regression
  • CI: tools/test-matrix.sh boxlang oracle — the original migratorSpec :: renames c_o_r_e_levels -> wheels_levels reproduction; CI will validate

🤖 Generated with Claude Code

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: This is a clean, well-scoped fix for two distinct Oracle JDBC bugs in insertAll() and renameSystemTables(). The architecture mirrors the existing $upsertSQL() adapter hook and the implementation is correct. I have one documentation nit and one test-coverage observation, neither blocking. Overall verdict: comment (no changes required from the author; CI Oracle run is the final gate).


Correctness

No issues. Both fixes address real Oracle JDBC constraints with the right approach:

Bulk INSERT path (vendor/wheels/model/bulk.cfc + vendor/wheels/databaseAdapters/Base.cfc + OracleModel.cfc): Moving SQL generation into the adapter via a new $bulkInsertSQL() hook is the correct abstraction. The Base implementation keeps the existing single multi-row VALUES (...), (...) statement (unchanged fast path for non-Oracle). The Oracle override correctly emits one single-row INSERT per record, which sidesteps the JDBC driver's auto-RETURNING rewrite conflicting with multi-row VALUES. The loop in insertAll() over local.sqlBatches is safe — totalInserted is still computed from batch bounds, not from the result structs, so Oracle's N queries count identically to the single non-Oracle query. The fact that $querySetup()'s return value is ignored in insertAll() (unlike single-record save()) means Oracle's $identitySelect() is never involved here; no secondary concern about identity retrieval.

Migrator rename path (vendor/wheels/Migrator.cfc lines 720–737): The Oracle branch runs DDL bare, consistent with the existing comment that Oracle auto-commits DDL and forfeits any rollback guarantee. Other engines keep the transaction action="begin" wrapper. This is the minimal correct fix — no unnecessary restructuring.


Cross-engine

No issues introduced. Two items checked explicitly:

  1. for (local.bSql in local.sqlBatches) on Adobe CF — Adobe CF copies array elements by value in a for-in loop. Since $querySetup() only reads (never mutates) the SQL array, a value copy is harmless. No behavioral difference across engines.

  2. $quoteIdentifier() call inside Base.$bulkInsertSQL() — Because $bulkInsertSQL is an adapter method, $quoteIdentifier() resolves to this.$quoteIdentifier() on the concrete adapter class. Oracle overrides it with uppercase + double-quote; MySQL with backticks; Postgres/SQLite/H2/MSSQL with their own characters. The column-quoting is adapter-correct in all cases.


Tests

Gap worth noting (vendor/wheels/tests/specs/model/bulkOperationsSpec.cfc): The existing specs exercise insertAll() via a real DB round-trip (single multi-row path). There is no test that verifies the multi-batch loop itself — i.e., that an adapter returning N SQL arrays from $bulkInsertSQL() causes N calls to $querySetup(). The Oracle per-row behavior can only be exercised on Oracle CI, which the PR acknowledges. However, a unit-level test using a mock or subclassed adapter returning two SQL arrays for a 2-record insert would let non-Oracle CI catch regressions in the loop logic if someone later refactors insertAll(). Not blocking — just worth filing as a follow-up.


Docs

Stale docstring (vendor/wheels/databaseAdapters/Base.cfc line 831):

/**
 * Builds parameter struct for a single value in a bulk operation.
 * Used by adapter upsert implementations.
 */
public struct function $buildBulkParam(

The $buildBulkParam function is now called by both $bulkInsertSQL() and upsert implementations. The line "Used by adapter upsert implementations" should read "Used by adapter bulk insert and upsert implementations." Minor; change inline if you touch this file again.

CHANGELOG entry is present and placed correctly under [Unreleased] > Fixed. Comprehensive.


Commits

fix(model,migrator): Oracle bulk insert and migrator rename (#2745) — valid conventional commit, scope unrestricted per commitlint.config.js, subject under 100 chars, references the originating issue.


Security

No concerns. Column and table names flow through $quoteIdentifier(). Record values flow through $buildBulkParam() as typed parameter structs consumed by $querySetup()'s parameterized query builder — no user data is ever interpolated as raw SQL strings.

@wheels-bot

wheels-bot Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Wheels Bot - Reviewer B (round 1)

A's review is substantive and well-reasoned. Every correctness claim is grounded in the diff. No sycophancy, no blocking false positives. Two minor observations below, neither changing the verdict.

Sycophancy

None detected. A's verdict (comment, no changes required) is backed by explicit per-section analysis of each changed file.

False positives

One imprecise cross-engine citation, conclusion still sound:

  • A invokes the struct-literal copy anti-pattern to explain Adobe CF behavior in for (local.bSql in local.sqlBatches). The struct-literal rule covers {arr = myArray} inside closures, not for-in iteration. The relevant mechanism here is that CFML passes the loop element as a value copy when it becomes a function argument to $querySetup() - a related but distinct concern from anti-pattern 6. The conclusion (only reads, value copy is harmless) is correct regardless. Not a false positive, just the wrong anti-pattern chapter cited.

Missed issues

One substitution not explicitly noted:

  • The removed bulk.cfc::$buildBulkInsertSQL() quoted columns via $quoteColumn() (a model mixin method). The replacement Base.$bulkInsertSQL() quotes the same columns via $quoteIdentifier() (an adapter method). A correctly notes that $quoteIdentifier() resolves to the adapter-specific implementation but does not call out that the original used a different function entirely. The 3681 passing MySQL tests and 830 passing SQLite tests confirm functional equivalence for those adapters. Not blocking - the test evidence is sufficient - but worth a sentence in the review.

Verdict alignment

A's comment verdict (no blocking issues, CI Oracle run is the final gate) is consistent with findings explicitly labeled non-blocking. Verdict is correct.

Convergence

Aligned. A correctly identifies the change as a clean, well-scoped fix with no cross-engine regressions in the tested adapters. The two findings (stale docstring, test coverage gap) are genuine, both explicitly non-blocking. My re-scan adds no significant missed issues. Joint recommendation: clean for merge pending CI Oracle gate.

`Model.insertAll()`: Oracle does not allow multi-row
`INSERT ... VALUES (...), (...)` together with the JDBC driver's
auto-RETURNING (triggered by `cfquery result="..."`). Move bulk-insert
SQL generation into the adapter behind a new `$bulkInsertSQL()` hook
(mirrors `$upsertSQL()`); Base keeps the multi-row form, Oracle
returns one single-row INSERT per record so the auto-RETURNING
transform only ever sees a plain `INSERT INTO ... VALUES (...)`.

`Migrator.renameSystemTables()`: Oracle implicitly commits DDL and
closes the JDBC statement, so the surrounding
`transaction action="begin" { ... commit }` then raised
`ORA: Closed statement`. Skip the transaction wrapper on Oracle (the
existing code comment already acknowledged it was a no-op there);
other engines keep their atomic-rollback path.

BoxLang × Oracle was the only engine exercising these paths because
Lucee 6/7 still skip Oracle on the soft-fail track from #2663.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri
bpamiri force-pushed the claude/upbeat-fermat-fd56f2 branch from af996d7 to 723855d Compare May 17, 2026 02:56
@bpamiri

bpamiri commented May 17, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed reviewer feedback in 723855d:

  • Reviewer A — stale $buildBulkParam docstring (Base.cfc:830): updated to read "Used by adapter bulk insert and upsert implementations."
  • Reviewer A — test-coverage gap for the multi-batch loop: agreed it's worth catching loop regressions on non-Oracle CI without paying the Oracle round-trip. Tracking as a follow-up rather than expanding scope of this fix.
  • CI: Validate Commit Messages: amended commit body to wrap at ≤100 chars (body-max-line-length from commitlint.config.js).
  • Reviewer B — anti-pattern citation precision and $quoteColumn$quoteIdentifier substitution: noted, no code change required.

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: This PR fixes two distinct Oracle JDBC failures in insertAll() and renameSystemTables() by moving bulk-INSERT SQL generation into an adapter hook and stripping the transaction wrapper from the Oracle DDL path. The implementation is correct, the architecture mirrors the existing $upsertSQL() pattern, and the one docstring nit flagged in the previous review round has been addressed in this commit. One test-coverage gap (the multi-batch loop / per-row Oracle path) remains a CI-only gate. No blocking issues. Verdict: comment.


Correctness

No issues.

Bulk INSERT path (vendor/wheels/model/bulk.cfc + vendor/wheels/databaseAdapters/Base.cfc + OracleModel.cfc): The adapter hook ($bulkInsertSQL()) returns an array of SQL arrays. Base.$bulkInsertSQL() emits a single multi-row VALUES (...), (...) statement (the fast path for all non-Oracle adapters, unchanged). OracleModel.$bulkInsertSQL() emits one single-row INSERT per record, so Oracle's JDBC driver never sees a multi-row VALUES clause when its auto-RETURNING rewrite is active. The for (local.bSql in local.sqlBatches) loop in insertAll() handles both shapes (one-element array for non-Oracle, N-element array for Oracle) transparently.

The totalInserted counter is still computed from batch bounds (batchEnd - batchStart + 1), not from $querySetup() return values, so Oracle's N queries produce an identical count to the single non-Oracle query.

Migrator rename path (vendor/wheels/Migrator.cfc line 721): The Oracle branch runs DDL bare inside the outer try/catch, which is correct — Oracle auto-commits DDL and there is no rollback to forfeit. Non-Oracle engines remain in the transaction action="begin" { ... commit } branch unchanged. The outer error-catch still covers the Oracle path (sets rv.success = false on failure).


Cross-engine

No issues.

  1. for (local.bSql in local.sqlBatches) on Adobe CF — Adobe CF copies array elements by value in a for-each loop. Since $querySetup() only reads local.bSql and never mutates it, the value-copy is harmless across all engines.

  2. $quoteIdentifier() inside Base.$bulkInsertSQL() — The call resolves via late binding. When non-Oracle adapters (MySQL, PostgreSQL) inherit Base.$bulkInsertSQL(), their own $quoteIdentifier() override (backticks, double-quotes) is dispatched correctly. Oracle overrides $bulkInsertSQL() entirely, so its uppercase + double-quote $quoteIdentifier() is called from OracleModel.$bulkInsertSQL() directly — no ambiguity.

  3. BoxLang method-receiver bindingvariables.wheels.class.adapter.$bulkInsertSQL(...) in bulk.cfc is a single-expression named-method call, not a bracket-call stored in a local variable. The receiver is bound correctly on BoxLang (cross-engine compat doc, "Method Reference Extraction Loses Receiver" section).


Tests

Gap worth tracking (vendor/wheels/tests/specs/model/bulkOperationsSpec.cfc): Existing specs exercise insertAll() via a real DB round-trip on SQLite (multi-row fast path). There is still no test for the loop path itself — i.e., that an adapter returning N SQL arrays from $bulkInsertSQL() causes N $querySetup() calls. A unit-level spec using a subclassed adapter returning two SQL arrays for a 2-record insert would let non-Oracle CI catch regressions if someone refactors insertAll() in the future. Not blocking — Oracle CI is the real reproduction gate.


Docs

The docstring nit from the previous review round ($buildBulkParam comment "Used by adapter upsert implementations") has been addressed in this commit:

- * Used by adapter upsert implementations.
+ * Used by adapter bulk insert and upsert implementations.

CHANGELOG entry is present under [Unreleased] > Fixed and is comprehensive.


Commits

fix(model,migrator): Oracle bulk insert and migrator rename (#2745) — valid conventional commit, type and scope are allowed by commitlint.config.js, subject under 100 chars, references the originating issue.


Security

No concerns. Column names flow through $quoteIdentifier(). Record values flow through $buildBulkParam() as typed parameter structs consumed by $querySetup()'s parameterized query builder — no user data is interpolated as raw SQL.

@wheels-bot

wheels-bot Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Wheels Bot — Reviewer B (round 1)

A's review of the amended commit is accurate and well-grounded. No sycophancy, no false positives, no missed issues on the diff I re-scanned. A's `comment` verdict (no changes required, Oracle CI is the final gate) is consistent with the findings.

Sycophancy

None detected. A's verdict is backed by per-section analysis of every changed file.

False positives

None detected. Spot-checked all three cross-engine claims:

  • Adobe CF for-in value copy: `$querySetup()` only reads `local.bSql` and never mutates it, so a value copy is harmless. Claim accurate.
  • `$quoteIdentifier()` late binding: Both `Base.$bulkInsertSQL()` and `OracleModel.$bulkInsertSQL()` call `$quoteIdentifier()` directly on `this`, so each adapter dispatches its own override correctly. Claim accurate.
  • BoxLang method-receiver binding: The call site is `variables.wheels.class.adapter.$bulkInsertSQL(...)` — a direct named-method call, not a bracket-call or extracted function reference. No receiver-loss risk. Claim accurate.

Missed issues

None detected. The docstring fix and commit-body line-length fix in this amended commit are minor and accurately described. B's round-1 observations from the prior SHA (`$quoteColumn` → `$quoteIdentifier` substitution, anti-pattern citation precision) were acknowledged by the author with no code change required, and A's re-review correctly folds in the `$quoteIdentifier()` late-binding analysis. The remaining test-coverage gap (unit test for the multi-batch loop path) was previously noted as non-blocking and is still tracked as a follow-up.

Verdict alignment

A's `comment` verdict (no changes required) is consistent with the findings: all prior review items resolved, one non-blocking test-coverage gap explicitly deferred.

Convergence

Aligned. A and B agree the fix is correct and complete for the tested adapters; Oracle CI is the outstanding gate, not a code issue. Joint recommendation: clean for merge pending the BoxLang × Oracle CI run.

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: Third pass, triggered by the develop-merge commit (ebe3e592). The Oracle bulk-insert and migrator-rename fixes are unchanged since the previous review round and remain correct. The merge brought in CockroachDBModel.$supportsAdvisoryLocks() → false (PR #2746, already landed on develop) via a CHANGELOG conflict resolution — a clean one-liner with no new concerns. One test-coverage gap noted in prior rounds (the multi-batch loop path) is still open but non-blocking; CI Oracle is the reproduction gate. Verdict: comment.


Correctness

No new issues introduced by the merge commit.

Merge delta (ebe3e592 vs 723855d9): The only change visible in gh pr diff is the CHANGELOG merge-conflict resolution that placed the CockroachDB entry alongside the Oracle entry. CockroachDBModel.$supportsAdvisoryLocks() itself is not part of this PR's diff — it was already on develop before the merge and appears only in the git delta, not in the PR's cumulative diff against base.

Oracle bulk-insert path (vendor/wheels/model/bulk.cfc, vendor/wheels/databaseAdapters/Base.cfc, vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc): Unchanged and correct. The $bulkInsertSQL() hook returns an array of SQL arrays; Base emits a single multi-row VALUES statement (fast path unchanged for non-Oracle adapters), OracleModel emits one single-row INSERT per record. The for (local.bSql in local.sqlBatches) loop in bulk.cfc handles both shapes transparently. totalInserted is computed from batch bounds, not $querySetup() return values, so Oracle's N queries produce an identical count to the single non-Oracle query.

One detail verified this round: the old $buildBulkInsertSQL in bulk.cfc (now deleted) called $quoteColumn(local.col). The new Base.$bulkInsertSQL calls $quoteIdentifier(local.col) directly on the adapter instance. These are functionally identical — $quoteColumn is a model mixin defined at vendor/wheels/model/miscellaneous.cfc:197 that delegates to variables.wheels.class.adapter.$quoteIdentifier(col). No behavioral change in column quoting across any engine.

Migrator rename path (vendor/wheels/Migrator.cfc:721): Unchanged and correct. dbType is set at line 687 (var dbType = info.database_productname;) before the Oracle branch at line 721 — the variable is in scope. The Oracle DDL runs bare inside the outer try/catch (lines 720 / 747), which catches failures and sets rv.success = false. Other engines keep the transaction action="begin" { ... commit } wrapper unchanged.


Cross-engine

No new concerns. One item checked this round: withAdvisoryLock in vendor/wheels/model/locking.cfc:41 does not check $supportsAdvisoryLocks() before calling $acquireAdvisoryLock — the capability flag is test-infrastructure only at this point. Runtime behavior on CockroachDB (the adapter throws with "CockroachDB does not support advisory locks.") is unchanged and predates this PR. Only the four lockingSpec withAdvisoryLock specs now skip cleanly instead of erroring.


Tests

Ongoing gap (vendor/wheels/tests/specs/model/bulkOperationsSpec.cfc): No new test for the multi-batch loop path — i.e., an adapter returning N SQL arrays from $bulkInsertSQL() causing N $querySetup() calls. Noted in both prior rounds. Non-blocking; Oracle CI is the real reproduction gate.


Docs

CHANGELOG [Unreleased] > Fixed now contains both the Oracle entry (this PR) and the CockroachDB entry (from develop, merged in). Both are accurate and comprehensive.


Commits

  • fix(model,migrator): Oracle bulk insert and migrator rename (#2745) — valid conventional commit per commitlint.config.js, subject under 100 chars, references the originating issue.
  • Merge commit ebe3e592: standard merge message; no commit-lint obligations apply to merge commits.

Security

No concerns. Column names flow through $quoteIdentifier(). Record values flow through $buildBulkParam() as typed parameter structs consumed by $querySetup()'s parameterized query builder — no user data is interpolated as raw SQL at any point in the new code paths.

…at-fd56f2

# Conflicts:
#	CHANGELOG.md
#	vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc
#	vendor/wheels/model/bulk.cfc
@bpamiri bpamiri changed the title fix(model,migrator): Oracle bulk insert and migrator rename (#2745) fix(migrator): renameSystemTables skips transaction wrapper on Oracle (#2745) May 17, 2026
@bpamiri

bpamiri commented May 17, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased to reconcile with #2749 which shipped the bulk-insert fix via a different approach (INSERT ALL syntax). Dropped my redundant bulk-insert changes — this PR now scopes down to the migrator-rename fix that #2749 explicitly deferred, plus the $buildBulkParam docstring nit. Updated title and body to match.

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: Fourth pass, triggered by the develop-merge commit (5fb5e697). The commit brings in PR #2749 (Oracle INSERT ALL bulk-insert fix) and #2748 (MySQL addColumnOptionsSpec carve-out) from develop, and resolves conflicts in CHANGELOG.md, OracleModel.cfc, and bulk.cfc. The migrator-rename fix (this PR's only original change) is unchanged and remains correct. Two notable improvements in this merge: the gap flagged in prior review rounds (no unit test for the adapter-level bulk-insert contract) is now addressed by the new oracleBulkInsertSqlSpec.cfc, and the $bulkInsertSQL() API was simplified to a flat-array contract that removes the wrapping loop from insertAll(). No new correctness, cross-engine, or security concerns. Verdict: comment.


Correctness

No new issues.

Base.$bulkInsertSQL() return-type change (vendor/wheels/databaseAdapters/Base.cfc line ~784): The function previously returned [local.sql] (an array containing one SQL array, allowing adapters to return N entries for N queries). The merge commit changes it to return local.sql directly (a flat SQL array — the input to one $querySetup() call). bulk.cfc::insertAll() was updated in the same commit to pass the return value directly to $querySetup() rather than iterating:

// Before merge
for (local.bSql in local.sqlBatches) {
    variables.wheels.class.adapter.$querySetup(parameterize = arguments.parameterize, sql = local.bSql);
}

// After merge
variables.wheels.class.adapter.$querySetup(parameterize = arguments.parameterize, sql = local.sql);

Both shapes are consistent: Base now returns a single flat SQL array for the standard multi-row VALUES INSERT, and OracleModel now returns a single flat SQL array for the INSERT ALL ... SELECT 1 FROM dual form. The loop was only necessary when Oracle returned one entry per record; INSERT ALL collapses all rows into a single statement, making the loop superfluous. API and usage are in sync. ✓

OracleModel.$bulkInsertSQL() form change (vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc lines 200–238): Changed from emitting N single-row INSERT INTO ... VALUES (?) statements (one per record) to a single INSERT ALL INTO ... SELECT 1 FROM dual statement. The critical property is the same — neither form triggers Oracle JDBC's RETURNING expansion — but INSERT ALL achieves it in a single statement instead of N, which also means totalInserted remains exact without any change to the batchEnd - batchStart + 1 counter in insertAll(). ✓

Migrator rename path (vendor/wheels/Migrator.cfc lines 718–737): Unchanged from prior rounds. The Oracle branch runs bare DDL inside the existing outer try/catch; other engines keep the transaction action="begin" { ... commit } wrapper. ✓


Cross-engine

No new concerns.

INSERT ALL parameterization: Oracle's JDBC driver supports parameterized INSERT ALL statements (the whole motivation for using this form over multi-row VALUES). Each ? placeholder in the INSERT ALL body is bound in declaration order by $querySetup()'s cfqueryparam expansion — the same mechanism that handles every other parameterized SQL in the framework. ✓

for (var part in sql) in oracleBulkInsertSqlSpec.cfc: The spec iterates over the returned SQL array using a for-each loop, only concatenating IsSimpleValue(part) fragments. Adobe CF copies array elements by value in for-each loops (cross-engine compat doc, "Array by-value in struct literals" section), but since no mutation occurs inside the loop body, the value-copy is harmless across all engines. ✓

Inline regex flags (oracleBulkInsertSqlSpec.cfc line ~96): ReMatch("(?i)INTO\s+""AUTHORS""", text) uses a Java inline flag (?i) for case-insensitive matching. CFML delegates to Java's java.util.regex engine on all supported platforms (Lucee 5/6/7, Adobe CF 2018–2025, BoxLang), so (?i) is universally available. ✓


Tests

Gap closed. Prior review rounds noted the absence of a unit test for the adapter-level $bulkInsertSQL() contract. The new vendor/wheels/tests/specs/model/oracleBulkInsertSqlSpec.cfc (186 lines) covers:

  • INSERT ALL ... SELECT 1 FROM dual shape (not multi-row VALUES)
  • Exactly N INTO clauses for N records in batch
  • Values flowing through $buildBulkParam structs (3 × 2 = 6 param structs, no inline interpolation)
  • Single-record batch produces INSERT ALL (not a fallback to single-row INSERT)
  • Non-Oracle adapters (SQLiteModel) keep INSERT INTO ... VALUES and never emit INSERT ALL

The specs instantiate OracleModel and SQLiteModel directly — testing only the SQL-builder logic, no live DB connection required. This correctly pins the adapter contract on every engine/DB slot in the compat matrix, not just the (currently soft-fail) BoxLang/Oracle slot. ✓


Docs

CHANGELOG [Unreleased] > Fixed now has four separate, clearly scoped bullets (migrator rename, model.insertAll(), addColumnOptionsSpec MySQL, CockroachDB advisory locks) rather than one combined entry. Each references its originating issue. ✓

.ai/wheels/cross-engine-compatibility.md was updated with two new sections: "MySQL — TEXT and FLOAT DEFAULT suppression" and "Oracle — Multi-Row INSERT and RETURNING Incompatibility". Both accurately describe the behavior and reference canonical implementations. ✓


Commits

  • fix(model): Oracle insertAll uses INSERT ALL form to avoid RETURNING-with-VALUES error (#2749) — valid conventional commit, subject under 100 chars.
  • fix(migrator): carve out MySQL in addColumnOptionsSpec text-default assertion (#2748) — valid, under 100 chars.
  • Merge commit 5fb5e697: standard three-way merge message; no commitlint obligations apply to merge commits.

Security

No concerns. All record values flow through $buildBulkParam() as typed parameter structs consumed by $querySetup()'s parameterized query builder. Column and table names flow through $quoteIdentifier(). No user data is interpolated as raw SQL at any point in the new or changed code paths.

@wheels-bot

wheels-bot Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Wheels Bot -- Reviewer B (round 1)

A's 4th-pass review is substantive and backs its verdict with per-section analysis. No sycophancy, no blocking false positives. Two observations below, neither changes the verdict.

Sycophancy

None detected. A's comment verdict is grounded in explicit analysis across correctness, cross-engine, tests, docs, and commits.

False positives

One recurring imprecision in anti-pattern citation:

  • A explains the for-each iteration safety on oracleBulkInsertSqlSpec.cfc by citing "cross-engine compat doc, 'Array by-value in struct literals' section." Anti-pattern 6 in CLAUDE.md specifically covers {arr = myArray} struct literal assignment inside closures, not general for-each loop value semantics. The two are related behaviors but different mechanisms. B flagged the same imprecision in round 1 on SHA af996d74 (for local.bSql in bulk.cfc); A repeated it here for var part in sql in the spec file. The conclusion (no mutation inside the loop body = value copy is harmless across all engines) is correct regardless. Not a blocking false positive, but this is the third pass where A has cited the wrong chapter.

Missed issues

One documentation gap not called out:

  • The Oracle DDL auto-commit behavior that directly motivated this PR's change -- Oracle implicitly commits DDL and closes the JDBC statement, so a subsequent transaction action="commit" raises ORA: Closed statement -- has no entry in .ai/wheels/cross-engine-compatibility.md. The Oracle -- Multi-Row INSERT and RETURNING Incompatibility section (landed via fix(model): Oracle insertAll uses INSERT ALL form to avoid RETURNING-with-VALUES error #2749) covers Oracle bulk insert, but there is no parallel Oracle -- DDL Auto-Commit and Transaction Wrapper section covering the migrator pattern. The behavior is documented inline via the Migrator.cfc code comment, so this is non-blocking. But a future developer adding DDL operations will not find the pattern in the .ai/ deep reference. Worth a follow-up ticket.

Also noted (not a missed issue, just an observation): A spends most of the review analyzing code that arrived via the origin/develop merge (#2749 -- OracleModel.$bulkInsertSQL, oracleBulkInsertSqlSpec.cfc, .ai/ updates). The gh pr diff against develop shows only three files: CHANGELOG.md, vendor/wheels/Migrator.cfc, and vendor/wheels/databaseAdapters/Base.cfc. Reviewing the merged-in content is a valid merge-hygiene check, but the review reads as more comprehensive than it is for this PR's unique contribution. Not a problem -- just context.

Verdict alignment

A's comment verdict (no changes required) is consistent with findings: migrator fix unchanged and correct, test gap from prior rounds now closed by oracleBulkInsertSqlSpec.cfc, commits valid, docs updated. Verdict is appropriate.

Convergence

Aligned. A and B agree the fix is correct and complete. The two observations above are non-blocking: the anti-pattern citation imprecision is a recurring presentation issue (not a correctness issue), and the .ai/ documentation gap can be a follow-up. Joint recommendation: clean for merge pending CI Oracle gate.

Add an `Oracle — DDL Auto-Commit and Transaction Wrapper` section to
the cross-engine compat doc, mirroring the existing Oracle bulk-insert
section. Documents the `Closed statement` failure mode and the
adapter-branch pattern used in `Migrator.renameSystemTables`.

Per Reviewer B's follow-up observation on PR #2753 — closes the
deep-reference gap so future devs adding DDL operations find the
pattern in `.ai/` instead of only in the inline Migrator.cfc comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri

bpamiri commented May 17, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed Reviewer B's follow-up observation about the missing .ai/ deep-reference section in 1f8a946: added an Oracle — DDL Auto-Commit and Transaction Wrapper section to .ai/wheels/cross-engine-compatibility.md, mirroring the existing Oracle bulk-insert section's style. Small enough to fold in here rather than file a follow-up.

The anti-pattern citation imprecision is on Reviewer A's side, not mine — no code change needed.

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: Fifth pass, triggered by the single new commit 1f8a946aa (docs(ai): document Oracle DDL auto-commit transaction-wrapper pattern). This is a docs-only addition that places the Oracle DDL Auto-Commit section in .ai/wheels/cross-engine-compatibility.md, closing the deep-reference gap that Reviewer B noted on this PR. The section is accurate, the code snippet matches the canonical implementation in Migrator.cfc verbatim, and the placement follows naturally after the existing Oracle bulk-insert section. One minor phrasing nit on the MySQL claim (inherited from the existing code comment, non-blocking). No correctness, cross-engine, or security concerns. Verdict: comment.


Docs

New section accurate (.ai/wheels/cross-engine-compatibility.md lines 324-350):

  • Oracle DDL auto-commit behavior (RENAME, CREATE, ALTER, DROP all implicitly commit and close the JDBC statement) is correctly stated.
  • The failure mode — ORA: Closed statement when transaction action="commit" runs against a closed statement — matches the PR description and the original bug report.
  • The code snippet is taken verbatim from vendor/wheels/Migrator.cfc::renameSystemTables (lines 721-737 as modified by this PR). No drift.
  • Placement after the Oracle multi-row INSERT section is logical: the two Oracle sections now group all Oracle JDBC quirks together before "Testing Across Engines".

Minor nit (.ai/wheels/cross-engine-compatibility.md line 326):

Other engines (Postgres, SQLite via SAVEPOINT, MySQL on InnoDB) honor the wrapper and roll the DDL back on error; Oracle cannot.

MySQL DDL (RENAME TABLE) also causes an implicit commit in the engine — it does not actually roll back via the InnoDB transaction wrapper. The atomicity guarantee for the MySQL path comes from RENAME TABLE accepting multiple pairs in a single statement (Migrator.cfc line 694: "RENAME TABLE " & ArrayToList(pairs, ", ")), not from the transaction. This sentence inherits the same slight imprecision from the existing inline comment at Migrator.cfc:716. Since .ai/wheels/ is the persistent reference for future maintainers, a small clarification here would prevent the same misconception from appearing in the next DDL operation someone adds:

- Other engines (Postgres, SQLite via SAVEPOINT, MySQL on InnoDB) honor the wrapper
- and roll the DDL back on error; Oracle cannot.
+ PostgreSQL and SQLite (via SAVEPOINT) honor the wrapper and will roll back DDL on error.
+ MySQL DDL also causes an implicit commit (the wrapper is a no-op there), but MySQL's
+ multi-rename form — RENAME TABLE a, b — is itself a single atomic statement, so no
+ partial-rename scenario arises. Oracle cannot use the wrapper at all.

Non-blocking — clarifying, not correcting. Raise as a follow-up if desired.


Commits

docs(ai): document Oracle DDL auto-commit transaction-wrapper pattern — valid conventional commit: type docs, subject 62 chars (under the 100-char limit), sentence-case, not ALL-CAPS. Commit body names the originating PR (#2753) and the Reviewer B observation that motivated the addition. DCO sign-off present (Signed-off-by: Peter Amiri <peter@alurium.com>). Clean.


Per Reviewer A's nit on PR #2753: MySQL DDL also implicitly commits
(InnoDB transaction wrapper is a no-op for DDL). The MySQL atomicity
guarantee comes from `RENAME TABLE a, b` being a single multi-pair
statement, not from the transaction. Persistent `.ai/` reference now
reflects that distinction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@wheels-bot

wheels-bot Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Wheels Bot — Reviewer B (round 1)

A's fifth-pass review is accurate and well-scoped. It correctly limits itself to the single new commit 1f8a946aa (docs-only), does not re-litigate the already-approved code changes, and reaches a correct verdict. One minor observation below, non-blocking.

Sycophancy

None detected. A's comment verdict is backed by explicit analysis of the new docs section, a verbatim check of the code snippet against Migrator.cfc, and a commit-hygiene pass.

False positives

None detected. Spot-checked A's key factual claims:

  • Code snippet is verbatim from Migrator.cfc lines 721-737 — confirmed. The snippet in .ai/wheels/cross-engine-compatibility.md lines 331-347 matches the post-PR implementation character-for-character (modulo indentation style, which is immaterial in documentation). Claim accurate.
  • MySQL DDL nit — A correctly identifies that MySQL RENAME TABLE causes an implicit commit (DDL auto-commit is not Oracle-exclusive), so the sentence "MySQL on InnoDB honors the wrapper and rolls back DDL on error" is inaccurate. Atomicity for MySQL comes from RENAME TABLE a TO b, c TO d being a single atomic statement, not from the transaction wrapper. A's suggested correction is factually sound and the non-blocking label is appropriate — it's a precision improvement, not a correctness fix that affects behavior.

Missed issues

None detected. The CHANGELOG.md entry carries the same MySQL imprecision ("MySQL on InnoDB keep their atomic-rollback behavior") but that entry is from a prior commit already reviewed in rounds 1-4. Scoping the fifth pass to 1f8a946aa only is correct.

Verdict alignment

A's comment verdict (no changes required) is consistent with findings: docs-only commit, substance accurate, one non-blocking nit. Verdict is correct.

Convergence

Aligned. A and B agree the PR is complete: Oracle migrator fix correct, docstring updated, .ai/ reference extended, commits valid. The MySQL precision nit in the new docs section (and the existing Migrator.cfc comment) is worth a follow-up but does not block this change. Joint recommendation: clean for merge pending CI Oracle gate.

….cfc

Reviewer B noted the CHANGELOG entry inherits the same MySQL
imprecision the `.ai/` doc had — "MySQL on InnoDB keep their
atomic-rollback behavior" implies the transaction wrapper rolls
MySQL DDL back, but MySQL DDL also implicitly commits. The
atomicity actually comes from `RENAME TABLE a, b` being a single
multi-pair statement. Update the CHANGELOG entry and the inline
`Migrator.cfc` comment so all three reference points (`.ai/`,
CHANGELOG, code comment) say the same thing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri

bpamiri commented May 17, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed both Reviewer A and Reviewer B's MySQL atomicity precision nit:

  • b25560b.ai/wheels/cross-engine-compatibility.md (applied Reviewer A's suggested correction)
  • 28fdd63 — CHANGELOG.md and Migrator.cfc inline comment (Reviewer B noted the CHANGELOG carried the same imprecision; also fixed the source-of-truth code comment for consistency)

All three reference points now say the same thing: PG/SQLite honor the wrapper, MySQL DDL implicitly commits but RENAME TABLE a, b is itself atomic, Oracle cannot use the wrapper at all.

@wheels-bot wheels-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wheels Bot — Reviewer A

TL;DR: Sixth pass, triggered by commit b25560b1f (docs(ai): clarify MySQL atomicity in DDL transaction-wrapper section). This is a single-sentence clarification in .ai/wheels/cross-engine-compatibility.md that directly addresses the MySQL atomicity nit raised in round five. The text is accurate, the commit is clean, and the PR is in good shape. One minor observation about a residual inconsistency in the CHANGELOG entry, non-blocking. Verdict: comment.


Docs

Nit addressed correctly (.ai/wheels/cross-engine-compatibility.md line 326):

The round-five review flagged this imprecise sentence:

Other engines (Postgres, SQLite via SAVEPOINT, MySQL on InnoDB) honor the wrapper and roll the DDL back on error; Oracle cannot.

The new text reads:

PostgreSQL and SQLite (via SAVEPOINT) honor the wrapper and will roll back DDL on error. MySQL DDL also causes an implicit commit (the wrapper is a no-op there), but MySQL's multi-rename form — RENAME TABLE a TO a', b TO b' — is itself a single atomic statement, so no partial-rename scenario arises. Oracle cannot use the wrapper at all.

This is accurate. MySQL DDL causes an implicit commit on all storage engines (not just InnoDB), so the transaction wrapper is indeed a no-op for MySQL DDL. The atomicity of renameSystemTables on MySQL comes from RENAME TABLE a TO a', b TO b' being a single multi-pair statement — MySQL documents this form as atomic (either all renames succeed or all fail). The canonical implementation at vendor/wheels/Migrator.cfc line ~694 uses "RENAME TABLE " & ArrayToList(pairs, ", ") which produces exactly this form. The explanation matches the code.

Residual minor inconsistency (CHANGELOG.md, the Migrator.renameSystemTables() bullet from a prior commit):

The CHANGELOG entry still reads:

...other engines (Postgres, SQLite via SAVEPOINT, MySQL on InnoDB) keep their atomic-rollback behavior.

The phrase "MySQL on InnoDB" and "atomic-rollback behavior" is now slightly inconsistent with the clarified .ai/ doc, which correctly states the MySQL wrapper is a no-op and the atomicity comes from the multi-pair statement, not from rollback. User-facing CHANGELOG entries do not need the same precision as maintainer reference docs, and the sentence is not wrong from the end-user perspective (the function works correctly on MySQL). Worth a one-word tweak if the entry is edited for any other reason, but does not require a dedicated fixup commit.


Commits

docs(ai): clarify MySQL atomicity in DDL transaction-wrapper section — valid conventional commit: type docs, subject 57 chars (under 100-char limit), sentence-case, not ALL-CAPS. Commit body attributes the change to the round-five nit, names the PR, and includes DCO sign-off (Signed-off-by: Peter Amiri <peter@alurium.com>) plus Co-Authored-By trailer. Clean.


@wheels-bot

wheels-bot Bot commented May 17, 2026

Copy link
Copy Markdown
Contributor

Wheels Bot — Reviewer B (round 1)

A's sixth-pass review is substantive on the content it covers. However, A's review marker anchors to b25560b1f while the current head at time of A's submission was already 28fdd63af. Two observations follow — neither changes the verdict.

Sycophancy

None detected. A's comment verdict is backed by explicit per-section analysis of b25560b1f and includes a named nit with a suggested fix.

False positives

One — A flags a residual inconsistency in CHANGELOG.md that was already resolved on the current head:

  • A quotes the old CHANGELOG text: "other engines (Postgres, SQLite via SAVEPOINT, MySQL on InnoDB) keep their atomic-rollback behavior" and labels it a non-blocking nit. Commit 28fdd63af — pushed by bpamiri at 03:53Z, one minute before A's review was submitted at 03:54Z — replaced that text with the precise form: "MySQL's path stays atomic via the multi-pair RENAME TABLE a TO a', b TO b' form (MySQL DDL also implicitly commits, so the wrapper itself is a no-op there)". A's flag was valid against b25560b1f but is stale against the current head. Not a blocking false positive, but the nit is already gone.

Missed issues

One — A reviewed b25560b1f but did not cover 28fdd63af:

  • Commit 28fdd63af (docs: align MySQL DDL atomicity wording across CHANGELOG and Migrator.cfc) touches two files A did not analyze: CHANGELOG.md (the precision fix A called non-blocking) and vendor/wheels/Migrator.cfc (inline comment update adding explicit MySQL DDL implicit-commit note and Oracle JDBC "Closed statement" explanation). Both changes are docs-only. B spot-checked the Migrator.cfc comment against the actual implementation in the diff: the new comment accurately describes the Oracle branch (FindNoCase("Oracle", dbType) — bare $query calls) and the non-Oracle branch (transaction wrapper with commit/rollback). No functional risk. The SHA mismatch in A's marker is the structural miss, not a substantive correctness miss.

Verdict alignment

A's comment verdict (no changes required, Oracle CI is the final gate) is consistent with findings at the time A reviewed. Against the current head the verdict still holds: 28fdd63af introduces no new risk.

Convergence

Aligned. The false positive (stale CHANGELOG flag) and the missed SHA are both attributable to 28fdd63af landing between A's trigger and A's submission, not to an analytical error. 28fdd63af is docs-only with verifiably correct content. A and B agree: the PR fix is correct, all review items from prior rounds are resolved, and Oracle CI is the outstanding gate. Joint recommendation: clean for merge pending the CI Oracle run.

@bpamiri
bpamiri merged commit cd39342 into develop May 17, 2026
5 checks passed
@bpamiri
bpamiri deleted the claude/upbeat-fermat-fd56f2 branch May 17, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant