Oracle Provider

August 5, 2026 · View on GitHub

Oracle Database support for LibreDB Studio, built on the oracledb driver in Thin mode (pure JavaScript — no Oracle Instant Client required). This document is the single reference point for the Oracle provider: design, architecture, usage, and tests. Oracle is a SQL-family provider sharing SQLBaseProvider; read the PostgreSQL doc first for the canonical SQL walkthrough, then this doc for the Oracle-specific deltas.

Status✅ Implemented & shipped
Database type idoracle
FamilySQL (relational)
DriveroracledbThin mode (no Instant Client)
Query languagesql
Default port1521
Connection poolingYes — oracledb pool (poolMin/poolMax/poolTimeout)
Connection stringSupported — EZConnect host:port/service or a TNS string (passed straight to the driver's connectString)
TransactionsYes — explicit begin/commit/rollback (no auto-rollback timeout)
Query cancellationYes — tracked connection + connection.break()
SSLNot configured by the provider (TLS via connect string / Oracle wallet)
Sourcesrc/lib/db/providers/sql/oracle.ts
Basesrc/lib/db/providers/sql/sql-base.ts
Teststests/integration/db/oracle-provider.test.ts

1. Overview

Oracle is a relational database that maps onto the DatabaseProvider interface like the other SQL providers, with several Oracle-isms that are worth knowing before reading the code. Read this as a diff against the PostgreSQL provider (the SQL reference implementation):

AspectPostgreSQLOracle
Driver modepgoracledb Thin by default (Thick opt-in via ORACLE_CLIENT_LIB_DIR)
PaginationLIMIT … OFFSETFETCH FIRST n ROWS ONLY / OFFSET m ROWS FETCH NEXT n
Schema scopeall non-system schemasthe connecting user's schema (OWNER = USER)
Schema queries1 MATERIALIZED-CTE round-trip5 bulk ALL_* queries grouped in memory
Maintenancevacuum / analyze / reindex / killanalyze (DBMS_STATS) / optimize (index rebuild) / kill
Transaction timeout5-minute auto-rollbacknone
Cancellationpg_cancel_backend(pid)connection.break() (tracked connection)
SSLbuildSSLConfig() + cloud auto-detectnot handled — TLS via connect string / wallet
Monitoring sourcepg_stat_*V$ views (privilege-gated, each guarded)
UI labelsdefault SQLoverridden (Gather Statistics / Rebuild Indexes)

Thin mode

The constructor (oracle.ts:186) uses pure-JS Thin mode by default, sets outFormat = OUT_FORMAT_OBJECT (rows as objects), and autoCommit = true globally. Thin mode means no native Oracle client has to be installed in the container — a real deployment win.

⚠️ Thin mode only supports Oracle Database 12.1 and later. Connecting to an older server (11.2 and earlier) fails with the driver's NJS-138 error. For those servers, opt into Thick mode via ORACLE_CLIENT_LIB_DIR — see §4.4.


2. Architecture

Same Strategy-Pattern hierarchy as the other SQL providers:

DatabaseProvider (interface) → BaseDatabaseProvider → SQLBaseProvider → OracleProvider

OracleProvider inherits the shared SQL helpers from sql-base.ts — see the PostgreSQL doc §2.2. It overrides three of them: getCapabilities(), getLabels(), and prepareQuery() (Oracle pagination). Note escapeIdentifier() from the base produces "ident" quoting, but Oracle maintenance largely uses inline-escaped literals instead (see §9).

Registration

Loaded on demand by the factory (factory.ts:77):

case 'oracle': {
  const { OracleProvider } = await import('./providers/sql/oracle');
  return new OracleProvider(connection, options);
}

3. Design decisions

3.1 EZConnect connect string (service name, not database)

Oracle connects to a service, not a database name. getConnectString() (oracle.ts:103) returns the raw connectionString if given, otherwise builds host:port/serviceName where serviceName = config.serviceName ?? config.database ?? 'ORCL'. Accordingly, validate() requires only host (not database) when no connection string is present.

3.2 FETCH FIRST instead of LIMIT

Oracle has no LIMIT, so prepareQuery() (oracle.ts:225) overrides the base and appends FETCH FIRST n ROWS ONLY (or OFFSET m ROWS FETCH NEXT n ROWS ONLY when an offset is set) to bare SELECTs that don't already have a limit. Default page size DEFAULT_QUERY_LIMIT = 500; unlimited caps at MAX_UNLIMITED_ROWS = 100000.

Both branches append at the end of the statement, which src/lib/sql/statement-end.ts delimits — before any trailing comment and before the terminating ;, both of which are then re-attached verbatim. Appending after them instead put the clause inside a trailing -- note while this method still reported wasLimited: true, so the statement reached Oracle unbounded and the UI called the result capped. A statement with no trailing comment is emitted exactly as it was before. The same reading answers whether the statement already carries a FETCH FIRST, so SELECT … FETCH FIRST 10 ROWS ONLY -- deliberate is still honoured and never gets a second clause. A statement whose end may not be cut is returned untouched with wasLimited: false rather than bounded on a guess. One shape reaches that on Oracle: a literal Oracle and MySQL would close in different places (a quote behind an odd backslash run). It is a deliberate loss of a bound — appending after the whole text, as this method used to, happened to be valid Oracle there, and is what puts the clause inside a trailing comment everywhere else — so that statement returns every row rather than being bounded on a guess. Since #297 the same unresolvable run also costs that statement a confirmation prompt, because the safety gate cannot read it either; the general rule and its accepted costs are in query-optimization.md.

# inside an identifier (ID#, common in legacy schemas) used to reach the same refusal and no longer does. prepareQuery() passes its own type to the shared readers (#292), and Oracle's grammar says # opens no comment: node-oracledb's own SQL tokenizer (node_modules/oracledb/lib/thin/statement.js) accepts # as an identifier character and starts comments on -- and /* … */ only. SELECT * FROM EMP WHERE ID# = 1 is therefore bounded, emitted as … ID# = 1 FETCH FIRST 500 ROWS ONLY. See Which dialect the readers are reading.

Alternate quoting (q'{it's}') is read as the literal it is — the second half of the same fix, and Oracle is the only dialect that has the form. The delimiter after the tag opens the body and its partner followed by ' closes it ([ ] { } ( ) < > pair up, any other character closes with itself, q or Q, and nq'…' / NQ'…' is the same form for NCHAR/NVARCHAR2), so the body carries apostrophes with nothing escaped. That is precisely what made reading it as code costly, and it cost two different things:

  • An apostrophe in the body opened a string, so everything after it was read one construct out of step: a ) inside the literal closed a CTE body early and the statement was typed by a keyword written inside the literal. WITH T AS (SELECT q'{it's}' AS S FROM DUAL) SELECT * FROM T lost its bound entirely.
  • A -- in the body made the rest of the literal look like a trailing comment, and the insert-before-trivia rule above then placed the clause inside the literal: SELECT q'[it's a -- note )]' AS S FROM DUAL was emitted as SELECT q'[it's a FETCH FIRST 500 ROWS ONLY -- note )]' AS S FROM DUAL with wasLimited: true — a statement Oracle rejects, reported as capped.

Both are gone for either spelling of the tag; the clause now lands after the literal. A body whose closing delimiter never arrives is undeterminable, so that statement is returned untouched rather than bounded on a guess. The tag must also start a word: in SELECT FREQ'{it's}' … the reader takes FREQ for a name and the apostrophe after it for an ordinary string, so that statement reaches the same refusal rather than a bound placed inside something that may not be a literal at all. That is deliberately stricter than node-oracledb's tokenizer, which opens a q-string at any ' preceded by q/Q whatever comes before it; the strict side is the one whose mistake costs a bound — and, since #297, a confirmation prompt on that statement — rather than a misplaced clause.

3.3 Owner-scoped, five-query schema introspection

getSchema() (oracle.ts:323) runs five bulk queries over the ALL_* data-dictionary views — tables, columns, primary keys, foreign keys, indexes — all filtered by OWNER = :1 (the connecting user, upper-cased) and then grouped in memory by table. This is neither the Postgres single-CTE approach nor MySQL's per-table N+1: it is a fixed 5 round-trips regardless of table count. There is no getSchemaList()/getSchemaRelations() (no two-phase split), and the returned TableSchema has no size field (only rowCount from NUM_ROWS, an optimizer estimate that can be stale/NULL).

3.4 No transaction auto-rollback timeout

Unlike the Postgres and MySQL providers (which arm a 5-minute auto-rollback timer), beginTransaction() (oracle.ts:258) simply checks out a connection and marks the transaction active — there is no timeout. An abandoned transaction holds its connection (and locks) until explicitly committed/rolled back or the connection is reclaimed by the pool.

3.5 No SSL config path

The Oracle provider does not read connection.ssl and has no buildSSLConfig() / cloud-auto-detect. Transport security is expected to be configured outside the provider — via a TLS (tcps) connect string or an Oracle wallet. (So, unlike Postgres/MySQL, there is no rejectUnauthorized: false auto-detect caveat here.)

3.6 Privilege-resilient monitoring

Oracle monitoring reads V$ dynamic-performance views, which require privileges a typical app user may lack. Every monitoring sub-query is wrapped in its own try/catch and degrades to a default (N/A, 0, or []) rather than failing the whole call — so the dashboard still renders for a low-privilege user, just with gaps.


4. Connection

4.1 Configuration

// Discrete fields — host required; service comes from serviceName ?? database ?? 'ORCL'
const a = { id: 'or-1', name: 'XE', type: 'oracle',
  host: 'localhost', port: 1521, serviceName: 'XEPDB1',
  user: 'app', password: 'secret', createdAt: new Date() };

// Connection string — EZConnect host:port/service (or a TNS string); passed
// straight to oracledb's connectString. (An `oracle://…` URL is NOT a valid
// driver connect string — it's only decomposed into discrete fields by the UI
// paste-parser before it ever reaches the provider.)
const b = { id: 'or-1', name: 'XE', type: 'oracle',
  connectionString: 'localhost:1521/XEPDB1',
  user: 'app', password: 'secret', createdAt: new Date() };

validate() (oracle.ts:89) requires host only when no connectionString is given; database is not required (Oracle uses the service name).

4.2 Connection pooling

connect() builds an oracledb pool (oracle.ts:121):

oracledb pool optionValueSource
poolMin2ProviderOptions.pool.min
poolMax10ProviderOptions.pool.max
poolTimeout30 (s)ProviderOptions.pool.idleTimeout ÷ 1000

⚠️ acquireTimeout (from DEFAULT_POOL_CONFIG) and queryTimeout (a separate ProviderOptions option, defaulting to DEFAULT_QUERY_TIMEOUT) are not mapped — there is no provider-driven server-side query timeout (cancellation is explicit, §5.2).

connect() is idempotent; getPoolStats() (oracle.ts:651) exposes { total: connectionsOpen, idle, active: connectionsInUse, waiting: 0 }.

4.3 SSL / TLS

Not handled by the provider — see §3.5. Use a tcps:// connect string or an Oracle wallet for encrypted transport.

4.4 Thick-mode opt-in (ORACLE_CLIENT_LIB_DIR)

Env varRequiredEffect
ORACLE_CLIENT_LIB_DIRNo (default: unset, Thin mode)Absolute path to an installed Oracle Instant Client lib directory. When set, the constructor calls oracledb.initOracleClient({ libDir }) and the driver runs in Thick mode instead of Thin.
# Only needed against a pre-12.1 Oracle server (see the Thin-mode caveat above).
# The version matters: use Instant Client 19c for an Oracle 11.2 server (see below).
ORACLE_CLIENT_LIB_DIR=/opt/oracle/instantclient_19_28

node-oracledb's Thin/Thick choice is a process-wide singletoninitOracleClient() throws if called more than once, or after any connection/pool already exists. This is why the setting is a process-level env var rather than a per-connection config field: every OracleProvider in the process shares one driver mode. The constructor guards the call with a module-level flag so it runs at most once regardless of how many OracleProvider instances (i.e. connections) are created. The Oracle Instant Client itself must already be installed at the given path — this provider does not download or bundle it. If the path is wrong (no client library there), the constructor fails fast with a DatabaseConfigError naming ORACLE_CLIENT_LIB_DIR, not a cryptic driver error.

Pick the right Instant Client version

Thick mode delegates to Oracle's native client, whose ability to reach an older server is bounded by Oracle client/server interoperability (My Oracle Support Doc ID 207303.1). For the common "connect to Oracle 11g" case:

Target serverInstant Client to install
Oracle 11.2 (11g)19c — the newest client that still reaches 11.2 (11.2.0.3 / 11.2.0.4). 21c and 23ai cannot connect to 11.2.
Oracle 12.1+Any current Instant Client (19c / 21c / 23ai). Thin mode already covers these, so Thick is rarely needed.

Connecting to a pre-12.1 server (build your own image)

The published image (ghcr.io/libredb/libredb-studio) ships Thin only — it does not bundle the Oracle Instant Client, because the native client is ~100 MB and only a minority of deployments need it. To reach an 11g server, build a derived image that layers Instant Client 19c on top and sets the env var. The runtime base is Debian 13 (node:*-trixie-slim), so install libaio1t64 (trixie's renamed libaio1) and unpack the Basic package:

FROM ghcr.io/libredb/libredb-studio:latest

USER root
# Instant Client 19c — reaches Oracle 11.2; 21c/23ai do not. Pin to a specific
# 19.x build; check https://www.oracle.com/database/technologies/instant-client/linux-x86-64-downloads.html
# for the current file name and update the version folder in ORACLE_CLIENT_LIB_DIR to match.
RUN apt-get update && apt-get install -y --no-install-recommends libaio1t64 unzip curl \
    && mkdir -p /opt/oracle && cd /opt/oracle \
    && curl -fsSLO https://download.oracle.com/otn_software/linux/instantclient/1928000/instantclient-basic-linux.x64-19.28.0.0.0dbru.zip \
    && unzip -q instantclient-basic-linux.x64-*.zip \
    && rm instantclient-basic-linux.x64-*.zip \
    && rm -rf /var/lib/apt/lists/*
ENV ORACLE_CLIENT_LIB_DIR=/opt/oracle/instantclient_19_28
USER nextjs

Instead of rebuilding, you can also mount an Instant Client directory from the host into the stock image and point ORACLE_CLIENT_LIB_DIR at the mount — whichever your deployment prefers. A first-class, separately-published Thick-mode image variant is a possible future addition; until then the derived image above is the supported path.


5. Query interface

5.1 Execution

query(sql, params?, queryId?) (oracle.ts:162) checks out a pooled connection, optionally stores the connection object under queryId for cancellation, runs conn.execute(sql, binds, { outFormat: OUT_FORMAT_OBJECT, autoCommit: true }), and returns:

{ rows, fields: metaData.map(m => m.name), rowCount: rows.length, executionTime }

rowCount is rows.length. Non-SELECT statements (INSERT/UPDATE/DELETE/DDL) return no rows array, so rows defaults to [] and rowCount is 0 — Oracle's rowsAffected is not surfaced. Bind parameters use Oracle's :1-style placeholders (getPlaceholder() from the base). Native errors are normalised through mapDatabaseError() (see §11).

5.2 Query cancellation

A query issued with a queryId stores its connection in a Map. cancelQuery(queryId) (oracle.ts:208) calls connection.break() on it — interrupting the in-flight OCI call — and returns true on success (it does not verify a query was actually running). Exposed via POST /api/db/cancel.

5.3 Data-type handling (LOBs & NUMBER) ⚠️

node-oracledb returns several Oracle types as non-primitive values, and the provider does not currently configure fetchAsString/fetchAsBuffer/fetchInfo:

  • CLOB/NCLOB/BLOB are returned as Lob stream objects, not strings/buffers — so a result row containing a LOB column does not serialize cleanly into the JSON grid. (Contrast the MySQL provider's sanitizeRow Buffer→hex conversion.) Oracle schemas commonly use LOBs, so this is a real gap — see Known limitations.
  • NUMBER is returned as a JavaScript number; values beyond 2532^{53} (e.g. NUMBER(38) ids or high-precision decimals) lose precision. Fetching such columns as strings would preserve them.

6. Transactions

Explicit lifecycle on a dedicated connection checked out from the pool (oracle.ts:258). Oracle starts a transaction implicitly on the first DML, so beginTransaction() just holds the connection. No auto-rollback timeout (see §3.4). Surfaced via POST /api/db/transaction.

MethodBehaviour
beginTransaction()Checks out a connection, marks active. Throws if one is active.
queryInTransaction(sql, params?)Runs on that connection with autoCommit: false. Throws if none active.
commitTransaction() / rollbackTransaction()commit()/rollback(), then closes the connection. Throws if none active.
isInTransaction()Current state.

7. Schema introspection

getSchema() returns one TableSchema per table owned by the connecting user. Five ALL_* queries (OWNER = :user), grouped client-side:

DataSource view(s)
Tables + row estimateALL_TABLES (NUM_ROWS)
ColumnsALL_TAB_COLUMNS (isPrimary derived from PK set; nullable = NULLABLE = 'Y')
Primary keysALL_CONSTRAINTS + ALL_CONS_COLUMNS (CONSTRAINT_TYPE = 'P')
Foreign keysALL_CONSTRAINTS (type 'R') joined to the referenced constraint's columns
IndexesALL_INDEXES + ALL_IND_COLUMNS (unique = UNIQUENESS = 'UNIQUE')

No getSchemaList()/getSchemaRelations(); no size on the returned tables (see §3.3).


8. Monitoring & health

All from V$/USER_* views; getMonitoringData() (inherited) fans them out in parallel. Each sub-query is independently privilege-guarded (§3.6).

MethodPrimary sourceNotes / degradation
getHealth()V$SESSION, USER_SEGMENTS, V$SYSSTAT, V$SQLeach block guarded → N/A/0/[] if no privilege
getOverview()V$VERSION, V$INSTANCE, V$SESSION, V$PARAMETER, USER_SEGMENTS, USER_TABLES/USER_INDEXESeach guarded
getPerformanceMetrics()V$SYSSTATonly cacheHitRatio + bufferPoolUsage (no QPS/deadlocks); defaults to 100 if denied
getSlowQueries()V$SQL (top-N by ELAPSED_TIME)sharedBlksHit=BUFFER_GETS, sharedBlksRead=DISK_READS; [] on failure
getActiveSessions()V$SESSIONV$SQLpid = "SID,SERIAL#"; wait class/event; [] on failure
getTableStats()ALL_TABLES + USER_SEGMENTSsizes + lastAnalyze; no live/dead tuples, no bloat; [] on failure
getIndexStats()ALL_INDEXES + USER_SEGMENTS + ALL_IND_COLUMNSscans always 0 (no usage counter exposed); isPrimary always false; [] on failure
getStorageStats()DBA_DATA_FILES → fallback USER_SEGMENTSper-tablespace size; DBA view falls back to user segments without privilege

9. Maintenance

runMaintenance(type, target?) (oracle.ts:586):

TypeWith targetWithout target
analyzeDBMS_STATS.GATHER_TABLE_STATS(USER, '<t>')DBMS_STATS.GATHER_SCHEMA_STATS(USER)
optimizeALTER INDEX "<t>" REBUILDrebuild every normal user index (USER_INDEXES, each in its own try/catch)
killALTER SYSTEM KILL SESSION '<SID,SERIAL#>'throws (SID,SERIAL# required)

getCapabilities().maintenanceOperations = ['analyze', 'optimize', 'kill']. Targets are inline-escaped (single quotes doubled for the PL/SQL string literal; double quotes doubled for the quoted index identifier) rather than routed through escapeIdentifier(), because they sit inside DBMS_STATS arguments / ALTER identifiers that can't take bind parameters.


10. Capabilities & labels

getCapabilities() (oracle.ts:61)

CapabilityValue
queryLanguagesql
supportsExplainfalse (intentionally disabled — see Known limitations)
supportsExternalQueryLimitingtrue (from base)
supportsCreateTabletrue (from base)
supportsInlineRowEdittrueUPDATE t SET c = v WHERE pk = v is core Oracle DML
supportsMaintenancetrue
maintenanceOperations['analyze', 'optimize', 'kill']
supportsConnectionStringtrue
defaultPort1521
schemaRefreshPattern(CREATE|DROP|ALTER|TRUNCATE)\b (from base)

Labels — overridden (oracle.ts:71)

Oracle overrides the default SQL labels so the UI uses Oracle vocabulary: analyzeAction"Gather Statistics", vacuumAction"Rebuild Indexes", and the matching global labels ("Gather Stats", "Rebuild All Indexes").


11. Error handling

mapDatabaseError() (errors.ts) has Oracle-specific branches:

SituationError
Missing host (no connection string)DatabaseConfigError
Operation before connect()DatabaseConfigError (via ensureConnected())
connect() failsConnectionError (carries host/port)
ORA-01017 / invalid username/passwordAuthenticationError
ORA-12541 / ORA-12154 / TNS:ConnectionError
ORA-00942 (table or view does not exist)QueryError
NJS-138 (server predates Oracle 12.1, Thin-mode incompatible)DatabaseConfigError, not retryable — see §4.4
Driver message contains timeout / timed outTimeoutError
connection.break()-interrupted querymaps via the generic path (the driver's ORA-01013 / "user requested cancel"); other ORA-* codes fall through to QueryError/DatabaseError with the original message

There is no provider-driven server-side query timeout (no queryTimeout wiring), so a TimeoutError only arises from a driver-level timeout message.


12. Testing

12.1 How the tests work

Integration tests live in tests/integration/db/oracle-provider.test.ts. The oracledb module is replaced with an in-process mock via mock.module('oracledb', …) before the provider is imported — there is no live Oracle in the suite. The mock pool/connection returns canned { rows, metaData } results, exercising the same code paths as the real driver.

⚠️ Mock isolation: bun's mock.module() is process-wide; files mocking different drivers cross-contaminate in a shared process. A single file is safe (one file = one process). The full bun run test script runs the core group in one process and is load-order flaky, so CI does not use it — the deterministic runner is bun run test:ci (per-file isolation via tests/run-core.sh); the coverage workflow uses bun run test:coverage. See CLAUDE.md.

12.2 Coverage

The suite covers: validation, connect/disconnect, query, capabilities, labels override, prepareQuery FETCH FIRST / OFFSET-FETCH, getSchema (columns/PKs/FKs/indexes grouping), health, maintenance (analyze/optimize/kill), pool stats, the transaction lifecycle, query cancellation (break()), overview, performance metrics, slow queries, active sessions, table/index/storage stats, and error mapping.

12.3 Run it

bun test tests/integration/db/oracle-provider.test.ts   # just this file (single process — safe)
bun run test:ci                                          # CI publish gate — per-file isolation (tests/run-core.sh)
bun run test:coverage                                    # CI coverage workflow — per-file core + components

12.4 Optional: verifying against a live Oracle

docker run --rm -e ORACLE_PASSWORD=secret -p 1521:1521 gvenzl/oracle-free:slim
# then connect to localhost:1521 / FREEPDB1 (user system, password secret) in the Studio UI

13. Usage examples

import { createDatabaseProvider } from '@/lib/db/factory';

const provider = await createDatabaseProvider({
  id: 'or1', name: 'XE', type: 'oracle',
  host: 'localhost', port: 1521, serviceName: 'XEPDB1',
  user: 'app', password: 'secret', createdAt: new Date(),
});

await provider.connect();
const res = await provider.query('SELECT id, email FROM users WHERE active = :1', [1]);
const schema = await provider.getSchema();   // 5 ALL_* queries, grouped in memory
await provider.disconnect();

Over the API: POST /api/db/query, POST /api/db/transaction, POST /api/db/cancel, POST /api/db/maintenance (admin), POST /api/db/schema/list (falls back to getSchema()).


14. Known limitations & future work

  • CLOB/BLOB columns don't render. No fetchAsString/fetchAsBuffer is configured, so LOB columns come back as Lob stream objects rather than text/bytes (§5.3). Future: set oracledb.fetchAsString = [oracledb.CLOB] / fetchAsBuffer = [oracledb.BLOB] (or per-query fetchInfo), and stream genuinely large LOBs instead of buffering.
  • Large NUMBER precision loss — returned as a JS number; NUMBER values beyond 2532^{53} should be fetched as strings to stay exact.
  • NJS-138 (pre-12.1 server) is a non-retryable configuration error, not a transient one. mapDatabaseError() maps it to DatabaseConfigError instead of the generic retryable ConnectionError every other connect() failure produces — see §4.4 and §11. The error message points the operator at ORACLE_CLIENT_LIB_DIR.
  • EXPLAIN is intentionally disabled for Oracle until a dialect wrapper exists. getCapabilities().supportsExplain is false, so the UI hides the Explain action. The UI's EXPLAIN builder only handles Postgres/MySQL; before the flag was flipped, the Explain action silently ran the unmodified query instead of producing a plan. Future: build EXPLAIN PLAN FOR … followed by SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY()), then re-enable the capability.
  • No server-side query timeout. queryTimeout is not wired into the pool; runaway queries must be cancelled explicitly via cancelQuery() (connection.break()). Future: set connection.callTimeout (node-oracledb's per-round-trip timeout) from queryTimeout.
  • kill and full monitoring require elevated privileges. ALTER SYSTEM KILL SESSION needs the ALTER SYSTEM privilege; the V$ monitoring views need SELECT on the V_$ views. A least-privilege application user can neither kill sessions nor read most monitoring (the queries degrade to N/A/0/[]).
  • Module-global driver settings. The constructor sets oracledb.outFormat/autoCommit on the shared oracledb module singleton (not per-pool/connection) — fine for a single embedding, but a process-wide side effect to be aware of if Oracle is ever used alongside another oracledb consumer.
  • No transaction auto-rollback timeout (unlike Postgres/MySQL) — an abandoned transaction holds its connection/locks until committed, rolled back, or pool-reclaimed.
  • Schema is owner-scoped to the connecting user (OWNER = USER); objects in other schemas the user can see are not listed, and tables carry no size field.
  • getIndexStats().scans is always 0 and isPrimary always false — Oracle index usage counters aren't read here.
  • Row counts (NUM_ROWS) are optimizer estimates populated by DBMS_STATS; they can be stale or NULL until stats are gathered.
  • Monitoring depends on V$ privileges. A low-privilege app user silently gets N/A/0/[] for the views it can't read. getPerformanceMetrics() reports only cache-hit ratio (no QPS/deadlocks).
  • No two-phase schema loading/api/db/schema/list falls back to the full getSchema().

15. References