Chapter 27 · Application Integration: Drivers, Pools, Session State, Transactions, and Resilience
Bind Variables, Array/Bulk Operations, Type Mapping, LOB Streaming, and Injection Defense
Use binds and batch APIs to reduce parse and network work, map Oracle NUMBER/time/JSON/VECTOR/LOB types consciously, stream large LOBs, and prove why identifiers require allowlisting instead of bind placeholders.
Learning outcomes
ServiceHub fills the shared pool with thousands of SQL texts because IDs are interpolated into SQL. Another endpoint correctly binds values but tries to bind a table name. A bulk loader sends one INSERT per network round trip, while an attachment endpoint reads an unbounded CLOB into memory. Efficient integration separates SQL grammar from typed values and uses batch/streaming APIs deliberately.
Use bind variables/prepared statements for values and observe cursor reuse.
Use python-oracledb executemany and node-oracledb executeMany with explicit batch-error transaction semantics.
Map NUMBER, DATE/TIMESTAMP/TSTZ, JSON, VECTOR, and LOB values without silent precision/time-zone assumptions.
Stream LOBs instead of materializing arbitrary payloads in application memory.
Show that values can be bound but arbitrary identifiers require an allowlist plus identifier validation/quoting.
Mandatory database examples target Oracle AI Database Free 26ai, reviewed against RU 23.26.3, SQL Developer 26.2, SQLcl 26.2.1.222.1617, JDBC/UCP 23.26.3.0, ODP.NET 23.26.3, python-oracledb 4.0.2, node-oracledb 7.0.1, and Oracle Instant Client 23.26.3 where Thick mode is discussed. Free is limited to 2 foreground CPU cores, 2 GB database RAM, 12 GB user data, and one installation per logical environment, with no Release Update patching or Oracle Support SRs. The course baseline is CDB/instance FREE, application PDB/service FREEPDB1 on port 1521, owner SERVICEHUB_OWNER, and runtime user SERVICEHUB_APP. JDBC Thin is the preferred Java path; JDBC OCI/Type 2 is deprecated in 26ai. python-oracledb and node-oracledb default to Thin mode and use Oracle Client libraries only if Thick mode is explicitly initialized. Application Continuity and Transaction Guard are not licensed in Free. On EE/EE-ES, Application Continuity requires Active Data Guard, RAC One Node, or RAC. DRCP and RESET_STATE are separate mechanisms. TCPS examples require a configured TLS listener/server certificate and trusted client configuration. No real password, wallet, private key, or provider secret is embedded, no mandatory lab changes COMPATIBLE, and no remote GitHub file is modified.
1. Disposable typed table
BEGIN EXECUTE IMMEDIATE 'DROP TABLE sh27_bind_lab PURGE';EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;/CREATE TABLE sh27_bind_lab ( item_id NUMBER PRIMARY KEY, tenant_code VARCHAR2(20) NOT NULL, item_name VARCHAR2(100) NOT NULL, amount NUMBER(12,2) NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL, payload_json JSON, attachment CLOB, embedding VECTOR(4,FLOAT32));INSERT INTO sh27_bind_lab( item_id,tenant_code,item_name,amount,payload_json,embedding) VALUES( 1,'TENANT_A','Compressor guide',25.50, JSON('{"status":"ACTIVE"}'), VECTOR('[0.9,0.8,0.1,0.0]',4,FLOAT32));INSERT INTO sh27_bind_lab( item_id,tenant_code,item_name,amount,payload_json,embedding) VALUES( 2,'TENANT_B','Private electrical guide',75.00, JSON('{"status":"ACTIVE"}'), VECTOR('[0.1,0.1,0.9,0.2]',4,FLOAT32));COMMIT;
2. Deliberately unsafe string interpolation
attacker_input = "TENANT_A' OR '1'='1"sql = ( "select item_id, tenant_code, item_name " "from sh27_bind_lab " "where tenant_code = '" + attacker_input + "'")# Resulting SQL changes the predicate and can expose all rows.
The attack changes SQL grammar. Least privilege limits damage, but the correct application construction is still parameter binding.
3. Bind the value
tenant_code = "TENANT_A' OR '1'='1"cur.execute( """ select item_id, tenant_code, item_name from sh27_bind_lab where tenant_code = :tenant_code """, tenant_code=tenant_code)print(cur.fetchall())# Expected: no row for this literal tenant value.
The quote/OR tokens remain characters in a value. Binds are not authorization—VPD/grants still protect rows/objects.
4. Observe reusable SQL text
SELECT sql_id, executions, parse_calls, version_count, substr(sql_text,1,140) AS sql_textFROM v$sqlWHERE sql_text LIKE 'select item_id, tenant_code, item_name%sh27_bind_lab%'ORDER BY last_active_time DESC;
Repeated executions of one bind SQL text can reuse parsed cursors/statement caches instead of producing one SQL text per ID. Exact parse counts depend on session and statement-cache behavior—measure an interval rather than requiring a magic parse ratio.
5. Batch DML reduces network round trips
rows = [ (10, "TENANT_A", "Pump guide", 10.00), (11, "TENANT_A", "Valve guide", 20.00), (10, "TENANT_A", "Duplicate PK", 30.00)]cur.executemany( """ insert into sh27_bind_lab( item_id,tenant_code,item_name,amount ) values(:1,:2,:3,:4) """, rows, batcherrors=True)for err in cur.getbatcherrors(): print(err.offset, err.message)conn.rollback() # this lab chooses all-or-nothing
With batch errors, valid rows can be processed while bad rows are reported. The application must explicitly decide whether partial success is allowed; this lab rolls back the batch.
6. Node executeMany follows the same principle
const sql = ` insert into sh27_bind_lab( item_id, tenant_code, item_name, amount ) values (:id, :tenant, :name, :amount)`;const binds = [ {id: 20, tenant: "TENANT_A", name: "A", amount: 1.25}, {id: 21, tenant: "TENANT_A", name: "B", amount: 2.50}];const result = await connection.executeMany(sql, binds, { autoCommit: false, bindDefs: { id: {type: oracledb.NUMBER}, tenant: {type: oracledb.STRING, maxSize: 20}, name: {type: oracledb.STRING, maxSize: 100}, amount: {type: oracledb.NUMBER} }, batchErrors: true, dmlRowCounts: true});console.log(result.dmlRowCounts, result.batchErrors);await connection.rollback();
7. Type mapping is correctness
| Oracle type | Risk | Rule |
|---|---|---|
| NUMBER | Binary floating/JavaScript Number may lose exact large integer/decimal values | Use Decimal/BigInt/string/type handlers when exact semantics require it. |
| DATE | Has date+time but no timezone | Never infer UTC merely from DATE. |
| TIMESTAMP | No timezone | Define application timezone conversion explicitly. |
| TIMESTAMP WITH TIME ZONE | Offset/region representation can differ by runtime | Test round-trip as an instant plus required zone semantics. |
| JSON | Native JSON versus text/object conversion differs by driver/version | Use current JSON APIs and test numbers/null/date conventions. |
| VECTOR | Dimension/format/model mismatch | Pin embedding provenance/dimension as in Chapter 25. |
| CLOB/BLOB | Unbounded process-memory use | Stream/chunk with driver LOB APIs. |
8. LOB streaming keeps memory bounded
const result = await connection.execute( `select attachment from sh27_bind_lab where item_id = :id`, { id: 1 });const lob = result.rows[0][0];lob.setEncoding("utf8");lob.on("data", chunk => { // Process/write each chunk.});lob.on("end", () => console.log("done"));lob.on("error", console.error);// Do not release the connection before the LOB stream completes.
Python/JDBC/ODP.NET expose equivalent LOB reader/stream APIs. A locator/stream can depend on the originating session, so pool lifecycle is part of LOB correctness.
9. Bind placeholders cannot be identifiers
SELECT *FROM :table_name;-- Typical result:-- ORA-00903: invalid table name
Object and column names must be known at parse time; bind values arrive later. The repair is not arbitrary string concatenation.
10. Validate and allowlist identifiers
requested = user_inputif requested not in {"SH27_BIND_LAB"}: raise ValueError("object is not allowed")if not oracledb.is_simple_sql_name(requested): raise ValueError("invalid identifier")safe_name = oracledb.enquote_name(requested)sql = f"select count(*) from {safe_name}"
const allowed = new Set(["SH27_BIND_LAB"]);if (!allowed.has(userInput) || !oracledb.isSimpleSqlName(userInput)) { throw new Error("object is not allowed");}const safeName = oracledb.enquoteName(userInput);const sql = `select count(*) from ${safeName}`;
Syntactic validation is not a business authorization rule; the allowlist decides what the application may access.
11. Server-side dynamic SQL follows the same rule
DECLARE l_table VARCHAR2(128) := :requested_table;BEGIN IF l_table NOT IN ('SH27_BIND_LAB') THEN RAISE_APPLICATION_ERROR(-20001,'table not allowed'); END IF; EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM ' || DBMS_ASSERT.SIMPLE_SQL_NAME(l_table) INTO :row_count;END;/
12. Cleanup and production judgment
DROP TABLE sh27_bind_lab PURGE;
Bind every user-controlled value, batch with explicit atomicity/partial-success rules, predefine types/sizes where useful, stream large LOBs, and treat dynamic identifiers as controlled code—not user data. Test edge NUMBER/timezone/JSON/VECTOR mappings across every supported language/runtime.
No pack, restart, or new COMPATIBLE change is required beyond earlier type prerequisites such as VECTOR. Batch size is not “larger is always faster”; measure redo/undo/network/memory. Lesson 4 applies the same explicitness to commit/retry behavior.
Check your understanding
- What does a value bind protect?
- Why can :table_name not identify a table?
- What does batchErrors change?
- Why can JavaScript Number be unsafe for Oracle NUMBER?
- What must remain valid while streaming a LOB?
Review the answers
It prevents that input value from changing SQL grammar and promotes reusable SQL shape.
Object names are resolved during parse, before bind values are supplied.
Individual row failures can be reported while other batch rows are processed; the caller must choose commit/rollback semantics.
It cannot exactly represent every Oracle integer/decimal value.
The originating connection/session until the locator/stream operation is complete.
Authoritative references
- python-oracledb SQL Execution — bind execution and transaction behavior
- python-oracledb Batch Operations — executemany/batch errors
- node-oracledb Batch Statements — executeMany
- node-oracledb LOB Data — LOB streams/lifecycle
- JDBC Developer's Guide — prepared statements/type/LOB behavior