fix(model): Oracle insertAll uses INSERT ALL form to avoid RETURNING-with-VALUES error#2749
Conversation
…with-VALUES error
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 ("ORA: returning clause is not allowed
with INSERT and Table Value Constructor", plus a follow-on "ORA: no
statement parsed" for related lifecycle reasons).
Moves bulk-insert SQL building off the model mixin and onto the database
adapter (`$bulkInsertSQL` on Base.cfc, mirroring the existing `$upsertSQL`
pattern). OracleModel overrides it with `INSERT ALL INTO t (cols) VALUES
(...) ... SELECT 1 FROM dual` — Oracle's idiomatic multi-row insert form,
which neither uses the table value constructor nor triggers the RETURNING
expansion. Non-Oracle adapters keep the standard multi-row VALUES shape.
The migrator-rename "Closed statement" error in the same compat-matrix
run is a separate Oracle JDBC lifecycle issue and is left for a follow-up.
Fixes #2745
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
…ine guide Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Wheels Bot — Docs updatedAdded a doc commit to this PR:
No MDX user-guide page needed (no public API change — |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: This PR correctly fixes a real Oracle 23 × JDBC incompatibility in insertAll() by moving bulk-insert SQL generation onto the adapter layer (mirroring $upsertSQL) and overriding it in OracleModel to use Oracle's idiomatic INSERT ALL ... SELECT 1 FROM dual form. The implementation is clean, the design is architecturally consistent, tests cover the SQL-builder contract on every engine, and the commit history is tidy. Verdict: approve (blocked from self-approval by GitHub — human reviewer may merge).
Correctness
No issues found.
- SQL shape is valid Oracle.
INSERT ALL INTO t (cols) VALUES (...) INTO t (cols) VALUES (...) SELECT 1 FROM dualis the correct Oracle multitable-insert form and does not trigger the JDBC driver'sRETURN_GENERATED_KEYS → RETURNINGexpansion that Oracle 23 rejects with multi-rowVALUES. - Single-row batch is covered. The Oracle form degenerates correctly to one
INTOclause — the oldINSERT INTO t VALUES (?)path was also broken (ORA: no statement parsed), and the new path avoids it. insertAlldoes not consume the generated-key result.bulk.cfc:60computestotalInsertedfrom the batch bounds, so the absence ofRETURN_GENERATED_KEYSwithINSERT ALLhas no user-visible effect.$quoteColumn→$quoteIdentifiersubstitution is safe. The removed$buildBulkInsertSQLcalled$quoteColumn(local.col);miscellaneous.cfc:198shows$quoteColumnis a thin wrapper aroundvariables.wheels.class.adapter.$quoteIdentifier(). The newBase.cfc:761calls$quoteIdentifier(local.col)directly on the adapter — identical behavior.
Cross-engine
No issues found.
- Oracle's
$quoteIdentifier(OracleModel.cfc:173) uppercases and double-quote-wraps identifiers — both the column list ($quoteIdentifierinside$bulkInsertSQL) and the table name ($quotedTableName()at the call site inbulk.cfc:46) go through the correct engine-specific path. - Non-Oracle adapters inherit the
Base.cfcdefault, which keeps the existing multi-rowVALUESshape unchanged. - No
obj["key"]()bracket-call anti-pattern, no inline-closure-as-constructor-arg, noLeft(str, 0)risk.
Tests
No issues found.
oracleBulkInsertSqlSpec.cfcextendswheels.WheelsTestand uses BDD syntax.- 5 cases cover:
INSERT ALLshape, oneINTOper record, parameterized values (confirms no inline interpolation of user data), single-row batch, and the non-Oracle base-adapter shape. - Instantiating
OracleModeldirectly (no datasource required for SQL-builder methods) is the right approach for adapter-level unit tests; the live Oracle end-to-end path remains inbulkOperationsSpec.cfc. - No unescaped
#in string literals in the new spec.
Commits
Clean. Single commit fix(model): Oracle insertAll uses INSERT ALL form to avoid RETURNING-with-VALUES error — type fix, scope model, subject under 100 chars, body explains the why (JDBC RETURN_GENERATED_KEYS to RETURNING expansion), DCO Signed-off-by trailer present.
Docs
CHANGELOG.md[Unreleased]entry present.- Framework/AI/CLAUDE.md docs deferred to
bot-update-docs.yml— noted in checklist.
Wheels Bot — Reviewer B (round 1)A's review is accurate and well-evidenced. No sycophancy, no false positives. Two minor observations below, neither of which affect the correctness verdict. SycophancyNone detected. A verifies every claim against specific diff lines and explains the reasoning. False positivesNone detected.
Missed issuesTwo items A didn't raise, both minor:
Verdict alignmentA's ConvergenceAligned. The two observations above are not actionable blockers — one is a stale description in an otherwise correct review, and the other is a pipeline meta-issue. The joint recommendation is approve. The PR is review-clean on this SHA; a human reviewer may merge. |
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: The previous review (at a4c819b4b) covered the core fix in full. The only change since is commit 97d638063, a clean docs-only addition to .ai/wheels/cross-engine-compatibility.md documenting the Oracle INSERT ALL constraint for future adapter authors. No correctness, cross-engine, security, or test findings on the full PR. Verdict: approve (blocked from self-approval by GitHub — human reviewer may merge).
Correctness
No issues.
- SQL shape is valid Oracle.
INSERT ALL INTO t (cols) VALUES (...) SELECT 1 FROM dualis the correct Oracle multi-table-insert form. It avoids both the table value constructor and the JDBCRETURN_GENERATED_KEYS→RETURNINGexpansion that Oracle 23 rejects. $identitySelectdoes not fire forINSERT ALL. BothBase.cfc:179andOracleModel.cfc:124guard withLeft(local.sql, 11) == "INSERT INTO". The OracleINSERT ALLoutput starts with"INSERT ALL"(11 chars =INSERT ALL≠INSERT INTO), so the identity-select branch is correctly skipped for bulk inserts — consistent withinsertAll()computingtotalInsertedfrom batch bounds, not from generated keys.- Values are parameterized.
OracleModel.cfc:224andBase.cfc:778both route every value through$buildBulkParam(), which returns a typed parameter struct. The table name (arguments.tableName) comes from$quotedTableName()at the call site (bulk.cfc:46), not user input. - Docs commit is accurate. The new section in
.ai/wheels/cross-engine-compatibility.mdcorrectly describes the JDBCRETURN_GENERATED_KEYS→ OracleRETURNINGexpansion, points toOracleModel.cfc::$bulkInsertSQLas the canonical reference, and gives actionable guidance for future adapter authors.
Cross-engine
No issues.
- Non-Oracle adapters inherit the unchanged
Base.cfcmulti-row VALUES path. The Oracle override is isolated toOracleModel.cfc. $quoteIdentifieris called on the adapter instance in bothBase.cfc:761andOracleModel.cfc:210, so identifier casing (Oracle uppercases) is handled correctly per engine.- No
obj["key"]()bracket-call pattern, no inline-closure-as-constructor-arg, noLeft(str, 0)risk, no private mixin issue ($bulkInsertSQLis correctly declaredpublic).
Tests
No issues.
oracleBulkInsertSqlSpec.cfcextendswheels.WheelsTestand uses BDD syntax (5 cases: INSERT ALL shape, one INTO per record, parameterized values, single-row batch, non-Oracle base shape).- Direct adapter instantiation without
$init()is safe:$buildBulkParamis a pure function that only reads the passed-inpropertyInfostruct — no datasource access. - No unescaped
#in string literals. Double-quoted Oracle identifiers are correctly represented as""AUTHORS""in CFML string literals, yielding"AUTHORS"in the actual value. - The
expect(collapsed).notToMatch(...)regex correctly targets the multi-row tuple-list shape that triggers the Oracle JDBC bug.
Commits
Clean.
fix(model): Oracle insertAll uses INSERT ALL form to avoid RETURNING-with-VALUES error— typefix, scopemodel, body explains the JDBCRETURN_GENERATED_KEYS→RETURNINGexpansion, DCOSigned-off-bypresent.docs: note Oracle INSERT ALL requirement for bulk insert in cross-engine guide— typedocs, no scope (valid), 66-char subject, DCOSigned-off-bypresent.- Both sign-offs match the configured git author (
claude[bot]).
…sspec-insert-returning
There was a problem hiding this comment.
Wheels Bot — Reviewer A
TL;DR: The two prior reviews (at a4c819b4b and 97d638063) covered the substantive Oracle insertAll fix in full. The only change since is commit bc75bc5b1, a pre-merge sync that merges develop into the branch — bringing in three other already-merged PRs (#2748 MySQL text-default carve-out, #2743 CockroachDB advisory-locks flag, and the cross-engine-compat docs from 97d638063). The merge is clean: no conflict resolutions, only parallel additions to CHANGELOG.md and .ai/wheels/cross-engine-compatibility.md. No correctness, cross-engine, security, or test issues on the full PR. Verdict: approve — PR is already merged; this review records the final-state assessment.
Correctness
No issues.
- Merge is additive-only.
bc75bc5b1merges97d638063(PR branch) +76f3b8f9c(develop tip). The combined diff is a clean union: the OracleINSERT ALLCHANGELOG entry from the PR and the MySQL/CockroachDB entries from develop land in adjacent lines with no conflicts resolved by overwriting. - No functional code changed in the merge commit. The five files in
git show bc75bc5b1 --statareCHANGELOG.md,.ai/wheels/cross-engine-compatibility.md,CockroachDBModel.cfc,MySQLMigrator.cfc, andaddColumnOptionsSpec.cfc— none of which are part of the Oracle bulk-insert fix itself.CockroachDBModel.cfcandMySQLMigrator.cfcarrive unchanged from develop (already reviewed in #2743 / #2748). - Prior analysis still holds.
OracleModel.$bulkInsertSQLemits the correctINSERT ALL INTO ... SELECT 1 FROM dualshape, avoiding the Oracle 23 JDBCRETURN_GENERATED_KEYS->RETURNINGexpansion.Base.cfc.$bulkInsertSQLkeeps the standard multi-row VALUES path for all other adapters.bulk.cfcdelegates to the adapter instance (variables.wheels.class.adapter.$bulkInsertSQL(...)), correctly passingtableNameandpropertyInfothat the old mixin-level$buildBulkInsertSQLread fromthisscope.
Commits
Clean. Two substantive commits (fix(model), docs) plus the pre-merge sync. All three have DCO Signed-off-by trailers. Subject lines are under 100 characters and not ALL-CAPS.
Wheels Bot — Reviewer B (round 1)A's review of the merge-sync commit is accurate and terse. No sycophancy, no false positives, no missed issues. Aligned on approve. SycophancyNone detected. A's approval cites specific commit analysis — the merge is additive-only, no conflicts resolved by overwriting, no functional code changed in the Oracle bulk-insert fix itself. False positivesNone detected. A's file list for the merge commit (CHANGELOG.md, .ai/wheels/cross-engine-compatibility.md, CockroachDBModel.cfc, MySQLMigrator.cfc, addColumnOptionsSpec.cfc) is consistent with what #2748 and #2743 contributed to develop. The Oracle fix files (Base.cfc, OracleModel.cfc, bulk.cfc) were already on the feature branch prior to the sync and correctly do not appear in the merge commit's stat. Missed issuesNone detected. Verdict alignmentA submitted a COMMENTED-type review (not APPROVED) because GitHub disallows APPROVE on a merged PR — A explains this explicitly. The approve verdict in the body is consistent with the findings. ConvergenceAligned. The prior B round on SHA |
On BoxLang × Oracle,
model.insertAll()was erroring withORA: returning clause is not allowed with INSERT and Table Value Constructorfor multi-row batches andORA: no statement parsedfor single-row batches. 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 implicitRETURN_GENERATED_KEYShandling: the driver expandsRETURN_GENERATED_KEYSinto aRETURNINGclause, and Oracle 23 disallowsRETURNINGpaired with multi-row VALUES.This PR moves bulk-insert SQL building off the model mixin (
vendor/wheels/model/bulk.cfc::$buildBulkInsertSQL, removed) onto the database adapter ($bulkInsertSQLondatabaseAdapters/Base.cfc), mirroring the existing$upsertSQLadapter-override pattern.OracleModeloverrides it withINSERT ALL INTO t (cols) VALUES (...) ... SELECT 1 FROM dual— Oracle's idiomatic multi-row insert form, 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 statementerror in the same compat-matrix run is a separate Oracle JDBC lifecycle issue and is intentionally left for a follow-up — it touchesvendor/wheels/migrator/**, which falls under the bot's safety-net policy requiring human-in-the-loop review before code is written. The parent issue tracks both.Fixes #2745
Checklist
vendor/wheels/tests/specs/model/oracleBulkInsertSqlSpec.cfc(5 cases). Failing pre-fix:Component [wheels.databaseAdapters.Oracle.OracleModel] has no function with name [$bulkInsertSQL]× 5. Passing post-fix: 5/5. End-to-end coverage on the live Oracle datasource continues viabulkOperationsSpec.cfc(the original 5 failing cases this issue reports).bot-update-docs.ymlbot-update-docs.ymlbot-update-docs.ymlbash tools/test-local.sh model→835 passed, 0 failed, 0 errors(was830 pass, 0 fail, 5 errorpre-fix; the 5 errors were the new spec demonstrating the missing adapter method)