Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .ai/wheels/cross-engine-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,34 @@ Oracle 23 rejects `INSERT INTO t (cols) VALUES (?,?), (?,?), ...` (the SQL-stand

If you write code that generates raw bulk-insert SQL for Oracle (or adds a new adapter), use `INSERT ALL ... SELECT 1 FROM dual` rather than multi-row VALUES. The canonical implementation is `vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc::$bulkInsertSQL`.

### Oracle — DDL Auto-Commit and Transaction Wrapper

Oracle implicitly commits DDL statements (RENAME, CREATE, ALTER, DROP, …) and closes the JDBC statement as part of that commit. If the DDL is wrapped in `transaction action="begin" { ... commit }`, the subsequent `transaction action="commit"` runs against a closed statement and raises `ORA: Closed statement`. 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.

If you write code that runs DDL inside a transaction block, branch on the adapter and run the DDL bare on Oracle. The canonical implementation is `vendor/wheels/Migrator.cfc::renameSystemTables`:

```cfm
if (FindNoCase("Oracle", dbType)) {
for (var sql in rv.sql) {
$query(datasource = dsn, sql = sql);
}
} else {
transaction action="begin" {
try {
for (var sql in rv.sql) {
$query(datasource = dsn, sql = sql);
}
transaction action="commit";
} catch (any e) {
transaction action="rollback";
rethrow;
}
}
}
```

There is no rollback to forfeit on Oracle — the implicit commit makes each DDL atomic on its own.

## Testing Across Engines

### Local Test Procedure
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo

### Fixed

- `Migrator.renameSystemTables()` now works on Oracle. The function wrapped its 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 acknowledged it was a no-op there); PostgreSQL and SQLite (via SAVEPOINT) keep the wrapper and roll back on error, while 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 — but the multi-rename is a single atomic statement, so no partial-rename scenario arises). Follow-up to #2749 which fixed the companion `model.insertAll()` Oracle failure from the same compat-matrix run (#2745)
- `model.insertAll()` on Oracle no longer errors with `ORA: returning clause is not allowed with INSERT and Table Value Constructor` (and the related `ORA: no statement parsed` follow-on). The bulk-insert SQL was always emitted as the SQL-standard multi-row table value constructor — `INSERT INTO t (cols) VALUES (?,?), (?,?), ...` — which Oracle 23 rejects in combination with the JDBC driver's implicit `RETURN_GENERATED_KEYS` handling (the driver expands `RETURN_GENERATED_KEYS` into a `RETURNING ROWID` clause, and Oracle 23 disallows `RETURNING` paired with multi-row VALUES). Bulk-insert SQL generation moved off the model mixin (`vendor/wheels/model/bulk.cfc::$buildBulkInsertSQL`, removed) onto the database adapter (`$bulkInsertSQL` on `databaseAdapters/Base.cfc`, mirroring the existing `$upsertSQL` pattern), so adapters can override per-engine. `databaseAdapters/Oracle/OracleModel.cfc` overrides it to emit Oracle's idiomatic multi-row form — `INSERT ALL INTO t (cols) VALUES (...) INTO t (cols) VALUES (...) SELECT 1 FROM dual` — which neither uses the table value constructor nor triggers the RETURNING expansion. Non-Oracle adapters (MySQL, Postgres, SQLite, H2, SQL Server, CockroachDB) keep the standard multi-row VALUES shape unchanged. The migrator-rename "Closed statement" error in the same compat-matrix run is a separate Oracle JDBC lifecycle issue and remains tracked under the parent issue (#2745)
- `addColumnOptionsSpec` now branches on `adapter.adapterName() == "MySQL"` for the `text` + non-empty default assertion, matching the existing `isPostgresFamily` carve-out. MySQL's `MySQLMigrator.optionsIncludeDefault` returns false for `text` / `mediumtext` / `longtext` / `float`, so the Abstract `addColumnOptions` short-circuits the entire DEFAULT clause for those types — emitting `NULL` rather than `DEFAULT '<value>'`. The spec previously asserted `toInclude("DEFAULT")` unconditionally and failed on every MySQL leg of the compat matrix (lucee6/mysql, lucee7/mysql, boxlang/mysql). The MySQL adapter's `optionsIncludeDefault` doc-comment now also explains the legacy pre-8.0.13 TEXT/BLOB constraint that motivates the suppression and references the spec contract. Follow-up to #2661/#2669
- `CockroachDBModel` now overrides `$supportsAdvisoryLocks()` to return `false`, so the four `lockingSpec` `withAdvisoryLock` tests skip cleanly on CockroachDB instead of erroring with `CockroachDB does not support advisory locks.`. The PR that introduced the capability flag (#2670) claimed CockroachDB in its CHANGELOG entry but never added the override — CockroachDB inherits from `PostgreSQLModel`, which reports `true`, so the spec's `beforeEach` skip-guard never fired and the four specs proceeded to call `$acquireAdvisoryLock`, which the adapter throws from by design. Compat-matrix legs `lucee6/cockroachdb`, `lucee7/cockroachdb`, and `boxlang/cockroachdb` now report 4 skips where they previously reported 4 errors. No spec changes needed — the capability-flag layer added in #2670 already does the right thing once the flag is correct (#2743)
Expand Down
35 changes: 23 additions & 12 deletions vendor/wheels/Migrator.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -710,20 +710,31 @@ component output="false" extends="wheels.Global"{
}

// Execute. Wrap in a transaction so a partial failure rolls back
// rather than leaving a half-renamed schema. Note: DDL inside a
// transaction is a no-op on Oracle (auto-commits) and MSSQL has
// adapter-specific behavior, but on the engines that DO honor it
// (Postgres, SQLite via SAVEPOINT, MySQL on InnoDB) we get atomicity.
// rather than leaving a half-renamed schema. Postgres and SQLite
// (via SAVEPOINT) honor the wrapper and roll back DDL on error.
// MySQL DDL also implicitly commits (the wrapper is a no-op there),
// but MySQL's multi-pair `RENAME TABLE a TO a', b TO b'` is itself
// a single atomic statement, so no partial-rename arises. MSSQL has
// adapter-specific behavior. On Oracle the implicit DDL commit
// closes the JDBC statement, so a subsequent
// `transaction action="commit"` reports "Closed statement" — run
// the DDL bare on Oracle. There is no rollback to forfeit.
try {
transaction action="begin" {
try {
for (var sql in rv.sql) {
$query(datasource = dsn, sql = sql);
if (FindNoCase("Oracle", dbType)) {
for (var sql in rv.sql) {
$query(datasource = dsn, sql = sql);
}
} else {
transaction action="begin" {
try {
for (var sql in rv.sql) {
$query(datasource = dsn, sql = sql);
}
transaction action="commit";
} catch (any e) {
transaction action="rollback";
rethrow;
}
transaction action="commit";
} catch (any e) {
transaction action="rollback";
rethrow;
}
}

Expand Down
2 changes: 1 addition & 1 deletion vendor/wheels/databaseAdapters/Base.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,7 @@ component output=false extends="wheels.Global"{

/**
* Builds parameter struct for a single value in a bulk operation.
* Used by adapter upsert implementations.
* Used by adapter bulk insert and upsert implementations.
*/
public struct function $buildBulkParam(
required string value,
Expand Down
Loading