diff --git a/changelog.d/3715-oracle-insertall-boxlang.fixed.md b/changelog.d/3715-oracle-insertall-boxlang.fixed.md new file mode 100644 index 0000000000..f5515f9479 --- /dev/null +++ b/changelog.d/3715-oracle-insertall-boxlang.fixed.md @@ -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) diff --git a/vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc b/vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc index 632c5ebf90..d3e871ed1e 100755 --- a/vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc +++ b/vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc @@ -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. */ @@ -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++) { @@ -385,6 +392,7 @@ component extends="wheels.databaseAdapters.Base" output=false { } ArrayAppend(local.sql, " FROM dual"); } + ArrayAppend(local.sql, "; END;"); return local.sql; } diff --git a/vendor/wheels/tests/specs/model/bulkOperationsSpec.cfc b/vendor/wheels/tests/specs/model/bulkOperationsSpec.cfc index 02ce2feb06..fbe1c3cc76 100644 --- a/vendor/wheels/tests/specs/model/bulkOperationsSpec.cfc +++ b/vendor/wheels/tests/specs/model/bulkOperationsSpec.cfc @@ -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"; } diff --git a/vendor/wheels/tests/specs/model/oracleBulkInsertSqlSpec.cfc b/vendor/wheels/tests/specs/model/oracleBulkInsertSqlSpec.cfc index 3d146543fc..3cdf17e801 100644 --- a/vendor/wheels/tests/specs/model/oracleBulkInsertSqlSpec.cfc +++ b/vendor/wheels/tests/specs/model/oracleBulkInsertSqlSpec.cfc @@ -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""",