Oracle compatibility
Supported Oracle versions
| Version | Status | CI coverage |
|---|---|---|
| Oracle 19c | Supported (all editions: SE2, EE, Autonomous Database) | Validated pre-release against the same JDBC + metadata API surface CI exercises continuously |
| Oracle 21c | Supported (all editions: SE2, EE, Express, Autonomous) | Continuous integration on every commit |
| Oracle 23ai | Supported (all editions: SE2, EE, Free, Autonomous) | Continuous integration on every commit |
All three versions are exercised against the same code paths. Newer Oracle releases (24+) ride the same JDBC dialect and are expected to work the same day they ship.
Compatibility surface
The extractor uses standard Oracle JDBC (ojdbc11, 23.5 driver) and
the standard DBMS_* metadata packages — both have been stable since
Oracle 9i. The specific features the extractor depends on:
| API | Available since | Why we use it |
|---|---|---|
DBMS_FLASHBACK.GET_SYSTEM_CHANGE_NUMBER | 9i | Pins a consistent SCN per table or globally |
AS OF SCN ... flashback queries | 10g | Lets parallel readers see the same logical snapshot |
DBMS_METADATA.GET_DDL | 9i | Captures DDL for replay during restore |
DBMS_LOB (READ, GETLENGTH) | 8i | LOB-aware partitioned reads |
ORA_HASH(ROWID) | 10g | Partition pseudo-key for tables without a PK |
ALL_TABLES, ALL_CONSTRAINTS, ALL_TAB_COLUMNS | 8i | Catalog discovery |
Because every feature predates 19c, all editions of 19c+ are supported without modification — SE, SE2, EE, Express, and Autonomous all expose the same APIs.
Deployment topologies
The product talks to Oracle over standard JDBC. Any topology that exposes a JDBC listener works. Notes for the most common cases:
Standard single-instance Oracle (default)
Default topology. Connect via oracleHost:oraclePort (defaults to TCP
1521). All editions behave identically from the extractor’s
perspective.
Oracle RAC (Real Application Clusters)
Connect via the SCAN listener (<scan-name>:1521). The JDBC driver
handles its own load balancing across instances. For deterministic
single-instance affinity (e.g. when you need session affinity for a
specific UNDO segment during a long whole_db_scn extract), point
oracleHost at a specific node’s VIP. No RAC-specific
configuration is required — standard JDBC TAF and FCF work
transparently.
Oracle Exadata
From the JDBC client’s perspective Exadata is just a tuned Oracle instance — Smart Scan, HCC, and storage offload are server-side and transparent. HCC-compressed tables (BASIC, OLTP, QUERY HIGH/LOW, ARCHIVE HIGH/LOW) decompress transparently during JDBC fetch — no special handling needed.
Oracle Autonomous Database (ADW / ATP)
ADW and ATP enforce TLS connections (port 1522 with TLS or 2484 with mTLS) and prefer Oracle Wallet for authentication. Three connection strategies:
Option 1 — Wallet via env vars (v0.9.2+): the extractor honours
ORACLE_TNS_ADMIN and ORACLE_TNS_ALIAS. When both are set, the JDBC
URL switches from host:port/service to TNS-alias mode and the driver
reads cwallet.sso + tnsnames.ora from TNS_ADMIN:
# 1. Download the wallet bundle (e.g. Wallet_MYDB.zip) from the OCI
# console for your Autonomous Database.
# 2. Upload the bundle to Key Vault as a base64 secret.
KV=$(az resource list -g <rg> --resource-type Microsoft.KeyVault/vaults \
--query '[0].name' -o tsv)
base64 -w0 Wallet_MYDB.zip | az keyvault secret set \
--vault-name "$KV" --name oracle-wallet --file /dev/stdin
# 3. Add a startup script to the extractor Container Apps Job that
# pulls the secret, base64-decodes, unzips to /tmp/oracle-wallet,
# and exports:
# ORACLE_TNS_ADMIN=/tmp/oracle-wallet
# ORACLE_TNS_ALIAS=mydb_high
# (Aliases live in tnsnames.ora — typically
# <dbname>_low / _medium / _high / _tp / _tpurgent.)
The wallet bundle carries the user credentials, so oracleUser /
oraclePassword become advisory; Bicep still requires them as
template params — pass placeholder values
(oracleUser=wallet, oraclePassword=wallet).
Option 2 — Password auth via public endpoint: create a database
user with a password and connect via the public endpoint using
standard JDBC. ADW accepts password auth alongside wallet auth on the
same listener — set oracleHost, oraclePort=1521,
oracleService=<dbname>_low. No wallet needed; simpler operationally
but exposes the DB to public internet (mitigate with the ADW ACL
list).
Option 3 — Private endpoint to ADW: use Oracle’s Database Service Gateway to expose ADW on a private endpoint reachable from the CAE subnet. Then either option 1 or option 2 connects over the private path.
Transparent Data Encryption (TDE)
- Tablespace-level TDE: fully transparent to JDBC. Rows arrive at the client in cleartext (subject to the source user’s grants). No configuration needed on the extractor side.
- Column-level TDE (DBMS_CRYPTO-managed): also transparent to
JDBC. The extracted Parquet contains the cleartext values; if your
threat model requires those to stay encrypted at rest in ADLS, set
lifecycleProfile: 'compliance'(enables WORM + storage encryption at rest). Parquet Modular Encryption (column-level encryption inside the Parquet file itself) is on the v2.0 roadmap for customers needing key-managed isolation per archived column.
Pluggable database (PDB) extraction
Connect to a PDB by setting oracleService to the PDB service name
(e.g. ORCLPDB1), not the CDB root. Multi-PDB extraction in a
single job is not supported today — run one extract per PDB. The
whole_db_scn consistency mode is scoped to the PDB you’re connected
to.
Database links / synonyms pointing at remote DBs
The extractor reads DBA_TAB_COLUMNS to discover columns; tables
exposed via synonyms that point at a remote DB-link are not
automatically followed. To archive those, materialize them locally
(CTAS) before the extract, or add the remote schema to
defaultSchemas if your user has the right grants on the remote side.
Data-type and LOB handling
The extractor uses spark.read.jdbc for ordinary columns and a
separate LOB-aware partitioned read for tables that contain LOB
columns. Round-trip behavior for the Oracle types we’ve validated:
| Oracle type | Extract behavior | Restore behavior | Notes |
|---|---|---|---|
NUMBER (precision/scale set) | round-trips as Spark Decimal(p,s) | preserved | exact |
NUMBER (no precision/scale) | coerced to Decimal(38,10) by Spark | preserved | the JDBC type code 2 with prec=0 forces Spark’s decimal default — rounding at 10 fractional digits |
INTEGER, FLOAT, BINARY_FLOAT, BINARY_DOUBLE | Spark numeric types | preserved | exact |
VARCHAR2, NVARCHAR2, CHAR, NCHAR | Spark String (UTF-8) | preserved | NVARCHAR2 character set normalized to AL32UTF8 |
DATE | Spark Timestamp (Oracle DATE has time component) | preserved as DATE | day-level resolution preserved |
TIMESTAMP | Spark Timestamp | preserved | nanosecond precision is truncated to microsecond by Spark (Parquet INT96/INT64 limit) |
TIMESTAMP WITH TIME ZONE | Spark Timestamp | preserved as TIMESTAMP | zone info is lost — values normalized to session zone (oracle.jdbc.timezoneAsRegion=false is set to make this deterministic) |
TIMESTAMP WITH LOCAL TIME ZONE | Spark Timestamp | preserved | session-zone-relative on both ends |
INTERVAL YEAR TO MONTH, INTERVAL DAY TO SECOND | Spark CalendarIntervalType | restored as INTERVAL | round-trips |
CLOB, NCLOB | LOB-mode read, materialized as Spark String | restored via setBigStringTryClob=true | 2GB ceiling per row (Spark string limit). Larger CLOBs fail with OutOfMemoryError — rare in practice; for >2GB values, copy via a separate JDBC path. |
BLOB | LOB-mode read, Spark Binary | preserved | 2GB ceiling, same as CLOB |
RAW(n ≤ 2000) | Spark Binary | preserved | exact |
LONG, LONG RAW (deprecated) | Spark String / Binary | restored if target column is LONG or LOB | deprecated by Oracle since 8i — convert to CLOB/BLOB on source |
BFILE | NOT extracted (server-side file pointer) | n/a | ddl.sql preserves the column declaration; the pointer becomes a dangling reference on restore. Avoid archiving tables with active BFILE columns. |
XMLType (binary storage) | LOB-mode read, Spark String | preserved as XMLType | textual round-trip; schema validation is not re-applied |
XMLType (CLOB storage, deprecated) | same as CLOB | same as CLOB | 2GB ceiling |
| Object types / nested tables / VARRAY | NOT supported | n/a | JDBC returns these as STRUCT types Spark cannot serialize to Parquet. Flatten on source or exclude via TABLES_EXCLUDE. |
JSON (23ai native) | Spark String (textual JSON) | preserved as JSON column | restore requires Oracle 21c+ |
ROWID, UROWID | Spark String | preserved as VARCHAR2 | row IDs are server-instance-specific; restored values are not valid in the target DB |
When to plan around the surface
-
CLOB > 2GB: Spark’s
StringTypeis bounded at 2GB regardless of the underlying CLOB. If your tables have >2GB single values, the extract for that row will fail withOutOfMemoryErrororIllegalArgumentException(depending on the driver path). Workaround: exclude those tables and copy them viaexpdp+DBMS_LOB.READinstead. -
BFILE: We do NOT extract the file contents — only the metadata pointer. After restore, the BFILE column points at a directory that doesn’t exist in the target environment. Pre-stage the files via a separate channel or exclude these tables.
-
Object types / UDTs: Unsupported. Flatten or exclude.
-
Encrypted columns (TDE column encryption): Transparent to the driver — extracted in cleartext (subject to the source user’s grants). Restore preserves the cleartext; re-encrypt at the target if required.
Operational notes per version
Historical Oracle issues that could affect operations:
| Symptom | Workaround | Affected versions |
|---|---|---|
ORA-01555 snapshot too old on long-running extracts | Use per_table_scn (default) instead of whole_db_scn; bump UNDO_RETENTION on the source DB | All |
JDBC-XA driver crash with TIMESTAMP WITH LOCAL TIME ZONE | oracle.jdbc.timezoneAsRegion=false is set automatically in extract.py | 19c |
DBMS_METADATA.GET_DDL returns NULL for objects you don’t own | The extractor user needs SELECT_CATALOG_ROLE or per-object SELECT — see the Quickstart grant block | All |
Partitioned table DDL contains STORAGE clauses that the target tablespace cannot honor | restore.py strips top-level STORAGE clauses; nested partition specs may still need manual edit of ddl.sql before restore | 21c, 23ai |
DROP TABLE ... CASCADE CONSTRAINTS racing across parallel restore threads | restore.py auto-serializes when DROP_IF_EXISTS=true (concurrent=1). For faster reruns, drop the target schema yourself once, then restore with parallelism. | All |