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
1 change: 1 addition & 0 deletions changelog.d/3715-oracle-insertall-boxlang.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `insertAll()` works on BoxLang with Oracle. BoxLang asks the driver for generated keys on every INSERT, and for the `INSERT ... SELECT` statement that Oracle bulk inserts use, the driver failed with `ORA-17009: Closed statement` (inside a transaction, only `Error closing connection: Connection is closed`). The statement now runs as an anonymous PL/SQL block, so no key request is made (#3715)
10 changes: 9 additions & 1 deletion vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,13 @@ component extends="wheels.databaseAdapters.Base" output=false {
* runs it with `$captureResult=false`, which drops the cfquery `result` attribute —
* the thing that makes Lucee request generated keys (#3653).
*
* BoxLang requests generated keys on every INSERT regardless of `result`, and
* for `INSERT ... SELECT` the Oracle driver then fails with ORA-17009 (Closed
* statement), which inside a transaction surfaces only as "Connection is closed"
* (#3715). The statement is therefore wrapped in an anonymous PL/SQL block:
* `BEGIN INSERT ... SELECT ...; END;` is not an INSERT to the engine, so no
* engine asks for keys, and bind parameters work inside the block.
*
* Uses parameterized values via `$buildBulkParam` — never interpolates user data
* into SQL.
*/
Expand All @@ -358,7 +365,7 @@ component extends="wheels.databaseAdapters.Base" output=false {
local.colList &= $quoteIdentifier(local.col);
}

ArrayAppend(local.sql, "INSERT INTO #arguments.tableName# (#local.colList#) ");
ArrayAppend(local.sql, "BEGIN INSERT INTO #arguments.tableName# (#local.colList#) ");

local.propCount = ArrayLen(arguments.validProperties);
for (local.r = arguments.batchStart; local.r <= arguments.batchEnd; local.r++) {
Expand All @@ -385,6 +392,7 @@ component extends="wheels.databaseAdapters.Base" output=false {
}
ArrayAppend(local.sql, " FROM dual");
}
ArrayAppend(local.sql, "; END;");

return local.sql;
}
Expand Down
8 changes: 5 additions & 3 deletions vendor/wheels/tests/specs/model/bulkOperationsSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,11 @@ component extends="wheels.WheelsTest" {

var found = g.model("bulkItem").findAll(where="code LIKE 'BULK-TS-%'", order="code");
expect(found.recordCount).toBe(2);
// createdAt should have been auto-populated.
expect(Len(found.createdAt[1])).toBeGT(0);
expect(Len(found.updatedAt[1])).toBeGT(0);
// createdAt should have been auto-populated. Read it through the
// framework normalizer: BoxLang + Oracle hands the column back as a
// raw oracle.sql.TIMESTAMP, which Len() cannot measure (#3715).
expect(IsDate(g.$normalizeDbTimestamp(found.createdAt[1]))).toBeTrue();
expect(IsDate(g.$normalizeDbTimestamp(found.updatedAt[1]))).toBeTrue();

transaction action="rollback";
}
Expand Down
30 changes: 30 additions & 0 deletions vendor/wheels/tests/specs/model/oracleBulkInsertSqlSpec.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,36 @@ component extends="wheels.WheelsTest" {
expect(text).notToInclude("Anderson");
});

it("wraps the INSERT ... SELECT in an anonymous PL/SQL block (##3715)", () => {
// BoxLang asks the driver for generated keys on every INSERT, and
// Oracle's driver cannot return them for INSERT ... SELECT: the
// statement fails with ORA-17009 (Closed statement), which inside a
// transaction surfaces only as "Connection is closed". A statement
// that starts with BEGIN is not an INSERT to the engine, so no key
// request is made; nothing here reads a generated key anyway.
var sql = oracle.$bulkInsertSQL(
tableName = """AUTHORS""",
columns = ["firstName", "lastName"],
validProperties = ["firstName", "lastName"],
records = records,
batchStart = 1,
batchEnd = 3,
propertyInfo = propertyInfo
);

var text = "";
for (var part in sql) {
if (IsSimpleValue(part)) {
text &= part;
}
}
var collapsed = Trim(ReReplace(text, "[[:space:]]+", " ", "all"));

expect(collapsed).toMatch("^BEGIN INSERT INTO ""AUTHORS""");
expect(collapsed).toMatch("FROM dual; END;$");
expect(ArrayLen(ReMatch("(?i)\bBEGIN\b", text))).toBe(1);
});

it("handles a single-row batch without falling back to multi-row VALUES", () => {
var sql = oracle.$bulkInsertSQL(
tableName = """AUTHORS""",
Expand Down
Loading