Skip to content

fix(oracle): run bulk inserts as a PL/SQL block so BoxLang asks no keys - #3718

Merged
bpamiri merged 1 commit into
developfrom
fix/3715-oracle-insertall-boxlang
Sep 27, 2026
Merged

bpamiri merged 1 commit into
developfrom
fix/3715-oracle-insertall-boxlang

Conversation

@bpamiri

@bpamiri bpamiri commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

Fixes #3715. Based on develop at b313a44, which includes #3710, so the BoxLang + Oracle leg gets past fixture population.

Root cause

The failure reproduces locally on BoxLang + Oracle (isolated compose project) with the same 4 insertAll errors as CI. Probes then isolated the cause with plain queryExecute calls, no Wheels code involved:

statement outside a transaction inside a transaction
INSERT ... SELECT ... UNION ALL ORA-17009: Closed statement Error closing connection: Connection is closed
INSERT ... SELECT (single row) ORA-17009 Connection is closed
INSERT ... VALUES, UPDATE, SELECT ok ok
leading /* */ comment, WITH clause ORA-17009 —
BEGIN INSERT ... SELECT ...; END; (with bind params) ok (2 rows) ok

BoxLang requests generated keys on every INSERT, whether or not a result is captured. Oracle's driver cannot return keys for INSERT ... SELECT, which is the shape $bulkInsertSQL must emit (#3302 per-row identity). In a transaction, the resulting error is masked by the connection-close failure. This is the same class of bug as #2745 and #3653.

Change

  • OracleModel.$bulkInsertSQL wraps the statement: BEGIN INSERT ... SELECT ...; END;. It is still one statement with one source row per record, and all values are still bound. OracleMigrator already runs BEGIN ... END; blocks on every engine.
  • oracleBulkInsertSqlSpec pins the block shape. Existing assertions are unchanged.
  • The fix exposed one more masked error: bulkOperationsSpec measured Len() of a raw oracle.sql.TIMESTAMP (Cannot determine length of object). It now asserts that the column normalizes to a date via $normalizeDbTimestamp(), a stronger check.
  • Changelog fragment added.

Verification

  • BoxLang + Oracle, local, wheels.tests.specs.model: before, 1047 pass / 4 error; after, 1052 pass / 0 fail / 0 error / 14 skip.
  • BoxLang + SQLite model: 1051 / 0 / 0.
  • Lucee 7 + SQLite model (tools/test-local.sh): 1051 passed.
  • Lucee / Adobe + Oracle: compat-matrix dispatch on this branch; totals to follow.

🤖 Generated with Claude Code

On BoxLang + Oracle every insertAll() spec errored with "Error closing
connection: Connection is closed". Outside a transaction the real error
shows: ORA-17009 (Closed statement). BoxLang requests generated keys on
every INSERT, and Oracle's driver cannot return them for INSERT ...
SELECT, the shape $bulkInsertSQL emits (#3302). INSERT ... VALUES,
UPDATE and SELECT are unaffected; a leading comment or a WITH clause
does not help.

Wrapping the statement as BEGIN INSERT ... SELECT ...; END; makes it
not an INSERT to the engine, so no key request is made. Bind
parameters work inside the block, and nothing on this path reads a
generated key. OracleMigrator already runs BEGIN ... END; blocks the
same way on every engine.

The fix exposed one more masked error: bulkOperationsSpec measured
Len() of a raw oracle.sql.TIMESTAMP. It now asserts the column
normalizes to a date through $normalizeDbTimestamp().

Fixes #3715

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
@bpamiri

bpamiri commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Review: independent reviewer (rev1.r1), commit 77ce2cb6b. Verdict: approve. No MUST-FIX. Merge gate, pending when I posted: dispatch 36344707369 (on this exact head). Read the result JSON, not the job colour, because Oracle is soft-fail.

Root cause and fix. BoxLang requests generated keys on every INSERT, and the Oracle driver can't return them for INSERT … SELECT … FROM dual, hence ORA-17009. Wrapping the statement as BEGIN INSERT … ; END; means the engine no longer sees an INSERT, so it makes no key request. The per-row identity shape and bind parameters are unchanged, and OracleMigrator already relies on anonymous blocks.

Checked for side effects:

  • Return value. A JDBC update count for an anonymous block isn't the row count, so I checked where insertAll()'s number comes from. vendor/wheels/model/bulk.cfm computes insertedCount from the batch bounds (batchEnd - batchStart + 1), never from the driver, and doesn't read the result. So wrapping the statement can't change what callers get back.
  • Size. Batches stay at 1000 rows, so the bind count per statement is unchanged; the block adds only BEGIN … END;.
  • Transactions. The block has no COMMIT, so it takes part in the surrounding transaction as the bare INSERT did. Errors still surface, now as ORA errors with ORA-06512 context.
  • The spec pins ^BEGIN INSERT INTO "AUTHORS", FROM dual; END;$ and exactly one BEGIN. The bulkOperationsSpec change from Len() to IsDate($normalizeDbTimestamp(...)) is stricter than before and no longer measures a raw driver object. It's the same masked-diagnostic pattern as fix(oracle): BoxLang reads a cf_sql_timestamp back as an unreadable oracle.sql.TIMESTAMP #3714.

@bpamiri

bpamiri commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review of 77ce2cb: source review clean; Oracle matrix evidence pending.

Independently ran bash tools/test-local.sh model on Lucee 7 + SQLite: 1051 passed, zero failures/errors. Reviewed the full bulk-insert caller and SQL builder: the wrapper preserves ordered bind objects, UNION ALL row shape, identifier quoting, and batch boundaries; it adds no commit/rollback or exception handler. insertAll() computes its inserted count and does not consume a driver-generated key/result. OracleMigrator already uses anonymous blocks.

The timestamp assertion now verifies a normalized date instead of merely nonempty text; it retains the requirement that timestamps were populated.

Final clearance awaits actual Oracle results from dispatch 36344707369, whose head matches this PR. The change applies to all Oracle engines, so the BoxLang reproduction fix also needs Lucee/Adobe execution evidence. A soft-fail job colour alone will not suffice.

@bpamiri

bpamiri commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

APPROVED at 77ce2cb.

Independently downloaded the actual Oracle JSON artifacts from exact-head dispatch 36344707369. All insertAll and upsertAll specs passed on BoxLang, Lucee 6/7, and Adobe 2023. Lucee 6/7 and Adobe 2023 each report 5,792 pass / 0 fail / 0 error / 24 skip. BoxLang reports 5,791 pass / 0 fail / 1 error / 24 skip; its sole error is the separate TimestampRoundTripSpec diagnostic issue #3714, addressed by #3716. Adobe 2025 remains pending and is not claimed as verified here.

The Oracle anonymous block keeps bound parameters, batching and transaction ownership intact; it does not commit or suppress errors. The caller already computes inserted count from the batch and does not consume generated keys. Earlier independent Lucee 7+SQLite model suite: 1,051 passed, zero failures/errors. No blocking finding in #3715's scope.

@bpamiri
bpamiri merged commit ca929cb into develop Sep 27, 2026
52 checks passed
@bpamiri
bpamiri deleted the fix/3715-oracle-insertall-boxlang branch September 27, 2026 20:11
@bpamiri

bpamiri commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up from rev1.r1, for the record: the gate is met. I read the result JSON of dispatch 36344707369 (77ce2cb6b) myself for two legs, and dev1-design reported the rest:

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.

fix(oracle): insertAll inside a transaction fails with "Connection is closed" on BoxLang

1 participant