A Python orchestration tool that breaks large database initial loads into parallel slices, runs them as independent Striim applications simultaneously, manages the full app lifecycle unattended, and hands off a CDC watermark so your change-data pipeline can catch up cleanly.
- Why You Need This Tool
- Key Concepts
- Prerequisites
- End-to-End Walkthrough
- Configuration Reference
- Management CLI Reference
- Probe Deep-Dive
- Split Predicates Deep-Dive
- State Backends
- Failure Handling and Recovery
- Cleanup
- Troubleshooting
- TQL Template and Namespace Scheme
- File Inventory
- Source Engine Support
- Known Limitations
- Appendix — Manual ROWID Split
A standard Striim application uses one DatabaseReader per app. For a small or medium table, this is fine — one sequential scan, one Striim app, done. But for a large table (tens of millions to billions of rows), a single reader becomes a bottleneck:
- The scan takes days, not hours
- There is no way to speed it up by adding more Striim nodes — the bottleneck is the single sequential read
- If the app fails partway through, the entire load must restart
This tool divides the source table into many disjoint SQL slices — non-overlapping subsets of rows that together cover the full table — and runs each slice as a separate, independent Striim app. Instead of one app reading 500 million rows sequentially, you might have 64 apps each reading ~8 million rows in parallel.
Source table (500M rows)
│
┌────┴────┐
│ split │ → 64 slices
└────┬────┘
│
┌─────▼─────────────────────────┐
│ Orchestrator (this tool) │
│ Runs up to N apps at once │
│ App 1: rows 1–8M [DONE] │
│ App 2: rows 8–16M [DONE] │
│ App 3: rows 16–24M [RUNNING]│
│ App 4: rows 24–32M [RUNNING]│
│ App 5: rows 32–40M [RUNNING]│
│ App 6: rows 40–48M [NEW] │
│ ... │
└───────────────────────────────┘
│
Target table
The orchestrator manages the full lifecycle of every app: create → deploy → start → monitor → undeploy → drop → clean namespace. It keeps a configurable number of apps in flight at all times and starts the next slice automatically when one finishes. The load runs completely unattended.
The right split strategy and chunk count depend on your specific table, its indexes, its joins, and your source database. Guessing produces sub-optimal splits. The probe measures actual throughput on a bounded sample of your real data and recommends the strategy, chunk count, and concurrency. It takes a few minutes and saves hours of wasted run time.
When an initial load finishes, a CDC pipeline typically starts replaying changes from the point the snapshot began. To find that point, this tool captures the source database's CDC watermark at the start of the load — an Oracle SCN, a PostgreSQL WAL LSN, or a SQL Server LSN, depending on the engine (see CDC watermark). You give that watermark to your downstream CDC reader as its start position. The tool also writes it to a sidecar file so you have it in writing after the load completes.
A slice is one SQL query that reads a disjoint range of rows. The source table is divided into N slices; each slice runs as a separate Striim app. Slices are disjoint — no row appears in more than one slice — so they can run in parallel without conflicts.
queryfile.txt is the input to the loader: one line per slice, the SQL query on the left and the target table on the right, separated by |:
SELECT s.* FROM PAY.CM_FB_SUBMISSION s WHERE s.ENTITY_TYPE='CASE' AND s.ROWID BETWEEN 'AAA' AND 'BBB'|PAY.CM_FB_SUBMISSION_TGT
SELECT s.* FROM PAY.CM_FB_SUBMISSION s WHERE s.ENTITY_TYPE='CASE' AND s.ROWID BETWEEN 'BBB' AND 'CCC'|PAY.CM_FB_SUBMISSION_TGT
...
The split command generates this file for you. You can also write it by hand for simple cases.
You write your source query once with a ~SPLIT~ placeholder where the slice predicate should go. The splitter replaces ~SPLIT~ with the actual boundary condition for each slice:
-- Your query template:
SELECT s.* FROM PAY.CM_FB_SUBMISSION s WHERE s.ENTITY_TYPE = 'CASE' AND ~SPLIT~
-- Becomes 64 lines in queryfile.txt:
... WHERE s.ENTITY_TYPE = 'CASE' AND s.ROWID BETWEEN 'AAA...' AND 'BBB...'
... WHERE s.ENTITY_TYPE = 'CASE' AND s.ROWID BETWEEN 'BBB...' AND 'CCC...'
...Joins, hints, JSON unnesting, subqueries, and projections pass through verbatim — only the ~SPLIT~ placeholder is replaced.
CONCURRENT_APPS_MAX (default 5) controls how many slices run simultaneously. The orchestrator keeps exactly this many apps in flight at all times: when one finishes, it immediately starts the next. The right number depends on your Striim cluster capacity and source database load tolerance.
Three ways to divide the table. The physical strategy uses an engine-specific row address; column range and partition strategies are conceptually identical across engines.
| Strategy | Predicate (per engine) | Engine support | Best for |
|---|---|---|---|
| Physical | Oracle: ROWID BETWEEN 'lo' AND 'hi' · PostgreSQL: ctid range · SQL Server: clustered-key range |
All supported engines (predicate syntax varies) | Default — fastest, no index dependency, works on any table |
| Column range | col >= lo AND col < hi (half-open; syntax identical across engines) |
All supported engines | An indexed high-NDV column that lets the source engine use an index range scan or partition pruning — often wins on tables with joins or JSON unnesting |
| Partition | Oracle: TABLE PARTITION (P) · PostgreSQL: child-relation scan (FROM schema.child) · SQL Server: $PARTITION.fn(col) = N predicate |
Engines with partition support | Already-partitioned tables — deterministic, no measurement needed |
The probe races your exact query against candidate split strategies, measures actual MB/s on a bounded sample, and recommends the strategy, chunk count, and concurrency. It never full-scans the table. Run it before splitting — it turns a guessing game into a measurement.
At fresh-run start the tool captures the source database's CDC watermark — the "start here" position for your CDC pipeline. The mechanism is engine-specific:
| Source engine | Watermark captured | How |
|---|---|---|
| Oracle | System Change Number (SCN) | SELECT current_scn FROM v$database, falling back to DBMS_FLASHBACK.GET_SYSTEM_CHANGE_NUMBER |
| PostgreSQL | WAL Log Sequence Number (LSN) | pg_current_wal_lsn() + optional pg_export_snapshot() |
| SQL Server | LSN | sys.fn_cdc_get_max_lsn() / @@DBTS |
The watermark is:
- Printed as a prominent banner on the console
- Written to
logging/run_<id>_watermark.jsonwith the engine-supplied label and value
The initial load snapshot and CDC replay intentionally overlap — CDC starts before the load finishes and replays changes that occurred during the load. This overlap is only safe if your CDC pipeline applies changes as upserts or merges (not blind inserts).
This is the most important operational concept to understand.
Striim's DatabaseWriter commits rows to the target in batches as it reads. There is no distributed transaction — each committed batch is permanent. If a slice fails midway (Striim error, network drop, stall timeout), the rows already committed remain on the target. There is no automatic rollback.
When you reset and re-run the failed slice, Striim re-reads the full slice from scratch and re-inserts everything. The TQL template handles the resulting duplicates with IgnorableExceptionCode: 'DUPLICATE_ROW_EXISTS' — but this only works if the target table has a primary key or unique constraint.
Rule: always define a primary key on the target table before running the load. A heap target (no PK) silently accumulates duplicate rows on any re-run.
The orchestrator persists every slice's status in a configurable state backend. This means a run can be interrupted and resumed: keep UNIQUE_RUN_ID unchanged and re-run python main.py. Slices already COMPLETED are skipped; the remaining ones pick up where they left off.
- Python 3.9+ — runs on your local machine or a jump host; does not need to run on the Striim server
- Striim — any recent version with
DatabaseReaderandDatabaseWriteradapters, running and reachable over HTTP or HTTPS - A source database — Oracle, PostgreSQL, SQL Server, or any JDBC source (see Source Engine Support). You need a read-only account with
SELECTon the table(s) being loaded. Engine-specific extras:- Oracle:
SELECTonV$DATABASEorEXECUTEonDBMS_FLASHBACKfor SCN watermark capture (absence degrades gracefully to a warning); for ROWID splitting,EXECUTEonDBMS_PARALLEL_EXECUTE(preferred),SELECTonDBA_EXTENTS, or just a read account (falls back toNTILE) - PostgreSQL: a role that can read
pg_catalog/pg_statsand callpg_current_wal_lsn()for the LSN watermark - SQL Server: read access to the
sys.*catalog views;sys.fn_cdc_get_max_lsn()(or@@DBTS) for the LSN watermark
- Oracle:
- JDBC driver for your engine installed in Striim — needed by Striim's
DatabaseReader, not by this tool - Target table — must exist and must have a primary key or unique constraint on the natural key before the load starts
- Striim property variables — connection strings and credentials defined in Striim (see Step 4)
The probe and split commands connect directly to the source database from your machine to discover boundaries. The required driver depends on your source engine and is not included in requirements.txt — install only what you need:
| Source engine | Install command |
|---|---|
| Oracle | pip install python-oracledb |
| PostgreSQL | already in requirements.txt via psycopg2-binary |
| SQL Server | pip install pyodbc |
The status, clear, reset, logs, board, and setup commands do not need a source database driver.
The probe/split source engine is selected with SOURCE_DB_TYPE (config.py) or the --source-engine flag. Drivers are lazy/optional — install only the engine(s) you actually probe or split against:
| Source engine | pip install |
Required env vars | Notes |
|---|---|---|---|
oracle (default) |
python-oracledb |
ORACLE_DSN or ORACLE_HOST+ORACLE_SERVICE; ORACLE_USER; ORACLE_PASSWORD (ORACLE_PORT defaults 1521) |
Optional — thin mode, no Instant Client needed. |
postgres |
psycopg2-binary |
SOURCE_PG_HOST / SOURCE_PG_PORT / SOURCE_PG_DATABASE / SOURCE_PG_USER / SOURCE_PG_PASSWORD / SOURCE_PG_SSLMODE |
Already bundled in requirements.txt for the PG state backend; reused here. |
sqlserver |
pyodbc |
SQLSERVER_HOST / SQLSERVER_PORT / SQLSERVER_DATABASE / SQLSERVER_USER / SQLSERVER_PASSWORD / SQLSERVER_DRIVER |
Optional — also needs a system ODBC driver, e.g. ODBC Driver 18 for SQL Server. |
jdbc |
JayDeBeApi + JPype1 |
JDBC_DRIVER_CLASS / JDBC_URL / JDBC_JAR_PATH / JDBC_USER / JDBC_PASSWORD (optional JDBC_ROW_LIMIT_SYNTAX, JDBC_WATERMARK_SQL) |
Optional — also needs a JRE/JDK (Java on PATH) and the vendor JDBC .jar. Best-effort (see Source Engine Support). |
The machine running the loader must reach:
- Striim REST API —
STRIIM_NODEhost + port (default 9080 for HTTP) - Source database — for probe, split, and watermark capture (read-only account is sufficient)
- State backend — only if using PostgreSQL, BigQuery, or Oracle as the state store
The recommended path: install → configure → set up Striim → validate → write query → probe → split → run → monitor → reconcile.
- Guided (interactive): run
./manage.shwith no arguments to open the management console, then pressGfor the first-time guided walkthrough — or pick the numbered actions directly. Every wizard validates as you type and re-prompts on bad input (a non-numeric chunk count, a malformedOWNER.TABLE, an unknown engine or strategy, a bad port) rather than failing later, so you can't get far with a wrong value. This is the easiest way to start. - Scripted (CLI): run the
python manage.py <command>subcommands shown in each step below. These take the same inputs as flags and are what the wizards call under the hood; they're ideal for automation. Invalid flags are rejected immediately with a clear one-line message.
Windows: run the
./manage.shTUI under WSL or Git Bash. Thepython manage.py …subcommands work natively incmd/PowerShell.
The steps below show the scripted form; the guided menu walks you through the identical sequence.
# Mac / Linux
git clone <this-repo>
cd Striim-InitialLoad-ParallelLoader
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pip install python-oracledb # for probe and split# Windows (Command Prompt or PowerShell)
python -m venv .venv && .venv\Scripts\activate
pip install -r requirements.txt
pip install python-oracledb # for probe and splitCredentials and connection details are read from environment variables — never hardcoded. Export them in your shell or place them in a .env file loaded by your shell profile.
Striim (required):
export STRIIM_NODE="your-striim-host:9080"
export STRIIM_ADMIN_USER="admin"
export STRIIM_ADMIN_PWD="your-password"
# OR use a pre-issued API token instead of user/password:
# export STRIIM_API_TOKEN="your-api-token"Source database (needed for probe, split, and watermark capture). Set SOURCE_DB_TYPE to your engine (oracle is the default) and export that engine's variables. Oracle shown here; PostgreSQL, SQL Server, and JDBC have their own blocks — see the driver matrix and Configuration Reference.
export SOURCE_DB_TYPE="oracle" # or postgres | sqlserver | jdbc
export ORACLE_USER="readonly_user"
export ORACLE_PASSWORD="your-password"
export ORACLE_DSN="your-oracle-host:1521/SERVICE_NAME"
# OR separately:
# export ORACLE_HOST="your-oracle-host"
# export ORACLE_SERVICE="SERVICE_NAME"
# export ORACLE_PORT="1521" # defaultDEV vs PROD: set ENV=PROD to switch to HTTPS defaults and longer polling/deploy intervals.
export ENV="PROD"Open config.py and set the values for your run. The most important ones:
UNIQUE_RUN_ID = 100 # Change for each new load; keep the same to resume
CONCURRENT_APPS_MAX = 5 # Parallel Striim apps in flight simultaneously
STAGE_DB_LOCATION = "TinyDB" # Start here; see State Backends section for PG/BQ/OracleFor a first run, all other settings can stay at their defaults. See Configuration Reference for the full list.
The TQL template uses Striim property variables for all credentials. Run pvs.tql once per Striim environment to define them:
# Via Striim console CLI:
$STRIIM_HOME/bin/console.sh <<'EOF'
use admin;
create or replace propertyvariable SourceConnectionString="jdbc:oracle:thin:@YOUR_SOURCE_HOST:1521:YOUR_SID";
-- PostgreSQL source example: jdbc:postgresql://HOST:5432/DB
-- SQL Server source example: jdbc:sqlserver://HOST:1433;databaseName=DB
create or replace propertyvariable SourceUsername="YOUR_SOURCE_USERNAME";
create or replace propertyvariable SourcePassword="YOUR_SOURCE_PASSWORD";
create or replace propertyvariable TargetConnectionString="jdbc:oracle:thin:@YOUR_TARGET_HOST:1521:YOUR_SID";
-- PostgreSQL target example: jdbc:postgresql://HOST:5432/DB
-- SQL Server target example: jdbc:sqlserver://HOST:1433;databaseName=DB
create or replace propertyvariable TargetUsername="YOUR_TARGET_USERNAME";
create or replace propertyvariable TargetPassword="YOUR_TARGET_PASSWORD";
EOFOr paste the contents of pvs.tql (with your values) into the Striim web console. These are shared across all generated apps and only need to be defined once.
Security: property variables are stored encrypted in Striim. Never paste credentials directly into
admin.SW.tqlorqueryfile.txt.
Before running the load, verify connectivity and prepare the orchestration state table:
./manage.sh setup
# Windows:
python manage.py setupThis connects to Striim and your chosen state backend, creates the orchestration table if it doesn't exist, and prints a summary of what it found. Fix any errors before proceeding — this step catches mis-configured environment variables, unreachable hosts, and permission problems early.
The interactive wizard (setup --interactive, or option 12 in ./manage.sh) prompts for each credential and re-prompts on invalid input: the source engine must be one of the four supported values, a port must be a positive integer, and the use / reset / cancel choice only accepts a recognized answer. A blank keeps the existing value. Non-numeric or out-of-range environment values (e.g. PG_PORT=abc) are reported with a clear message such as "PG_PORT must be an integer" instead of a stack trace.
Create a SQL file with your source query. Include a ~SPLIT~ token where the slice predicate should be injected, and alias your driving table:
-- my_query.sql
SELECT s.COL_A, s.COL_B, s.COL_C
FROM SCHEMA.SOURCE_TABLE s
WHERE s.ENTITY_TYPE = 'CASE'
AND ~SPLIT~Rules:
~SPLIT~is required for reliable injection; if omitted, the splitter appendsAND <pred>/WHERE <pred>instead, but a marker is more reliable- The driving table must have an alias (e.g.
s) — the predicate is alias-qualified so it binds to the right table in a join - Joins, hints, JSON unnesting, subqueries, and projections pass through verbatim
- Do not append a
|TARGETsuffix to this template. The target is supplied separately withsplit --target OWNER.TGT. Thequery|targetline format belongs to the generatedqueryfile.txtand to pasted batch lines (option 15) — not to the--query-filetemplate. A stray|TARGEThere leaks into the SQL (… WHERE <pred>|TARGET) and every slice fails.
See queryfile.txt.example for a working example using the seeded PAY.CM_FB_SUBMISSION table.
Input is validated.
--table/--targetmust beOWNER.TABLEidentifiers,--aliasand--columnmust be plain identifiers, and numeric flags (--chunks, probe tunables) must be positive integers. Bad values are rejected with a clear one-line message (not a stack trace); in the interactive./manage.shwizards you are simply asked to enter the value again.
Run the probe to find the best split strategy and chunk count for your specific query:
./manage.sh probe \
--query-file my_query.sql \
--table SCHEMA.SOURCE_TABLE \
--depth bakeoff
# Windows:
python manage.py probe --query-file my_query.sql --table SCHEMA.SOURCE_TABLE --depth bakeoffThe probe races candidate strategies on a bounded sample of your real data and prints a recommendation panel:
================================================================
PROBE RECOMMENDATION
================================================================
strategy: column
key: CREATED_DT
chunk_count: 64
concurrency: 8
winner_MB_per_sec: 41.20
----------------------------------------------------------------
label kind MB/s access_path verdict
ROWID physical 28.10 TABLE ACCESS BY ROWID R… good
CREATED_DT column 41.20 INDEX RANGE SCAN good
ENTITY_ID column 3.40 TABLE ACCESS FULL AMPLIFYING
----------------------------------------------------------------
partition: table IS partitioned — consider --strategy partition
warnings:
- AMPLIFYING: candidate 'ENTITY_ID' full-scans PAY.SUBMSN_RESP (join amplification)
================================================================
The recommendation is also saved to logging/probe_recommendation.json.
The probe never full-scans the real table. Every timing query is capped at
--sample-rowsrows (default 100,000) and a wall-clock--time-budget-secondsbackstop (default 20s). Safe to run against trillion-row tables.
See Probe Deep-Dive for depth tiers and what each column means.
Apply the probe's recommendation to generate queryfile.txt:
./manage.sh split \
--query-file my_query.sql \
--table SCHEMA.SOURCE_TABLE \
--target SCHEMA.TARGET_TABLE \
--strategy column \
--column CREATED_DT \
--chunks 64
# Windows:
python manage.py split --query-file my_query.sql --table SCHEMA.SOURCE_TABLE \
--target SCHEMA.TARGET_TABLE --strategy column --column CREATED_DT --chunks 64This connects to the source database, discovers the actual value boundaries for CREATED_DT, and writes 64 lines to queryfile.txt — one SQL per slice, one target table per line.
Optional: interleave for better concurrency
By default, slices are written in boundary order. To avoid all concurrent apps hitting the same source partition or target rows at once, interleave the file:
./manage.sh split ... --assort # interleave at split time (recommended)
# or separately:
python make_assorted_queryfile.py --input queryfile.txt --output queryfile-assorted.txt--assort reorders lines so each wave of CONCURRENT_APPS_MAX apps is a varied mix, reducing hot-block contention and per-table write throttling.
python main.pyOn first run, the loader:
- Reads
queryfile.txtand registers all slices in the state backend asNEW - Captures the source CDC watermark (Oracle SCN / PostgreSQL WAL LSN / SQL Server LSN) — note this value before starting your CDC pipeline downstream
- Enters the orchestration loop: deploys up to
CONCURRENT_APPS_MAXStriim apps simultaneously, monitors for completion, and starts new slices as each finishes
The loop runs until every slice reaches COMPLETED or FAILED, then exits.
Resuming an interrupted run: keep UNIQUE_RUN_ID unchanged in config.py and re-run python main.py. The loader picks up where it left off — slices already COMPLETED are skipped.
Windows: use python main.py directly (no bash wrapper needed for main.py).
Open a second terminal and launch the live status board:
./manage.sh # interactive menu → option 11 (Live status board)The board auto-refreshes every 5 seconds and shows:
- Run progress: percentage complete, slice counts by status, progress bar
- In-flight apps: slice number, target table, namespace, elapsed time, live rows/sec (when Striim is reachable)
- Recent completions
- Stage-file watch: apps spinning up (
+ appeared) and finishing (- finished) in real time - Log tail
For a quick text check without the board:
./manage.sh status
./manage.sh status --failed # show only failed slicesWindows: the full-screen board may render with broken formatting in Command Prompt or PowerShell. Use WSL for the best experience, or use
python manage.py board --jsonfor the raw JSON feed.
When the run reports FINISHED, it means no slice reported FAILED — it trusts Striim's COMPLETED/quiesced signal. Run reconcile to confirm that rows actually landed:
./manage.sh reconcile
# Windows:
python manage.py reconcile --run-id 100Reconcile counts source rows AS OF SCN <watermark> per stored slice boundary. Any slice that quiesced on a partial read is caught here. The command also emits the corresponding SELECT COUNT(*) SQL for each target table — run those against your target to complete the row-count check.
All settings live in config.py. Credentials always come from environment variables.
| Setting | Default | Env var override | Description |
|---|---|---|---|
QUERY_FILE |
queryfile.txt |
— | Input slice file. Point at queryfile-assorted.txt for the interleaved version. |
UNIQUE_RUN_ID |
100 |
— | Identifies this load session. Keep unchanged to resume; change to start fresh (history preserved). |
CONCURRENT_APPS_MAX |
5 |
— | Maximum Striim apps in flight simultaneously. |
APP_MONITOR_INTERVAL_SECONDS |
15 (DEV) / 60 (PROD) |
— | How often to poll app status. Not below 15s. |
DEPLOY_WAIT_TIME_SECONDS |
20 (DEV) / 120 (PROD) |
— | Minimum pause between deploying successive apps. |
STAGE_DB_LOCATION |
TinyDB |
— | State backend: TinyDB, BQ, PG, or ORACLE. |
SLICE_MAX_RUNTIME_SECONDS |
0 (off) |
SLICE_MAX_RUNTIME_SECONDS |
Stall timeout. A slice RUNNING past this is force-FAILED. 0 = disabled. |
DEPLOYMENT_GROUP_TARGET |
default |
— | Striim deployment group for all generated apps. |
ILA_APP_NAME_BASE |
OracleInitialLoadApp |
— | App name inside each namespace — must match the TQL template. |
SOURCE_TQL_FILE |
admin.SW.tql |
— | TQL template filename. |
LOG_OUTPUT_PATH |
logging/striimautoloader.log |
— | Log file; sidecars (watermark, probe recommendation) land in the same directory. |
ENV |
DEV |
ENV |
DEV or PROD. PROD uses HTTPS defaults and longer timing values. |
SOURCE_DB_TYPE |
oracle |
SOURCE_DB_TYPE |
Source engine: oracle (default), postgres, sqlserver, or jdbc — all implemented. Override per run with --source-engine on probe/split. |
| Variable | Default | Description |
|---|---|---|
STRIIM_NODE |
localhost:9080 |
Striim host and port |
STRIIM_URL_PREFIX |
http:// (DEV) / https:// (PROD) |
Protocol prefix |
STRIIM_ADMIN_USER |
admin |
Striim admin username (use OR token, not both) |
STRIIM_ADMIN_PWD |
(empty) | Striim admin password |
STRIIM_API_TOKEN |
(empty) | Pre-issued API token; used instead of user/password if set |
| Variable | Default | Description |
|---|---|---|
ORACLE_DSN |
(empty) | Full DSN, e.g. host:1521/SERVICE (use OR host+service) |
ORACLE_HOST |
(empty) | Oracle hostname (alternative to DSN) |
ORACLE_SERVICE |
(empty) | Oracle service name (alternative to DSN) |
ORACLE_PORT |
1521 |
Oracle port |
ORACLE_USER |
(empty) | Oracle username |
ORACLE_PASSWORD |
(empty) | Oracle password |
| Setting | Default | Env var override | Description |
|---|---|---|---|
SOURCE_DB_TYPE |
oracle |
SOURCE_DB_TYPE |
Active source engine: oracle, postgres, sqlserver, or jdbc. Overridden per-invocation by --source-engine on probe/split. |
TARGET_DB_TYPE |
(= SOURCE_DB_TYPE) |
TARGET_DB_TYPE |
Target engine for the generated TQL; defaults to the source engine for homogeneous loads. |
The
probeandsplitcommands also accept--source-engine {oracle,postgres,sqlserver,jdbc}to overrideSOURCE_DB_TYPEfor a single run (default =config.SOURCE_DB_TYPE).
Distinct from the
PG_*state-backend vars. TheseSOURCE_PG_*variables configure the PostgreSQL database the probe/split reads from. ThePG_*block (below) configures the PostgreSQL state/orchestration backend — they are independent and may point at different servers.
| Variable | Default | Description |
|---|---|---|
SOURCE_PG_HOST |
(empty) | Source PostgreSQL hostname |
SOURCE_PG_PORT |
5432 |
Source PostgreSQL port |
SOURCE_PG_DATABASE |
(empty) | Source database name |
SOURCE_PG_USER |
(empty) | Source PostgreSQL username |
SOURCE_PG_PASSWORD |
(empty) | Source PostgreSQL password |
SOURCE_PG_SSLMODE |
prefer |
SSL mode; use require for managed/cloud PG |
| Variable | Default | Description |
|---|---|---|
SQLSERVER_HOST |
(empty) | SQL Server hostname |
SQLSERVER_PORT |
1433 |
SQL Server port |
SQLSERVER_DATABASE |
(empty) | Database name |
SQLSERVER_USER |
(empty) | SQL Server username |
SQLSERVER_PASSWORD |
(empty) | SQL Server password |
SQLSERVER_DRIVER |
ODBC Driver 18 for SQL Server |
ODBC driver name pyodbc connects through (must be installed on the host) |
| Variable | Default | Description |
|---|---|---|
JDBC_DRIVER_CLASS |
(empty) | Fully-qualified JDBC driver class, e.g. com.mysql.cj.jdbc.Driver |
JDBC_URL |
(empty) | Full JDBC URL, e.g. jdbc:mysql://host:3306/db |
JDBC_JAR_PATH |
(empty) | Path to the vendor JDBC .jar |
JDBC_USER |
(empty) | JDBC username |
JDBC_PASSWORD |
(empty) | JDBC password |
JDBC_ROW_LIMIT_SYNTAX |
rownum |
Row-limit dialect for bounded probe sampling (rownum, limit, top, fetch) |
JDBC_WATERMARK_SQL |
(empty) | Optional SQL returning a CDC watermark value; blank disables watermark capture |
The generated TQL sets DatabaseProviderType on Striim's DatabaseReader/DatabaseWriter from SOURCE_DB_TYPE/TARGET_DB_TYPE. Override the mapping when Striim expects a provider string that differs from the engine name:
| Variable | Default | Description |
|---|---|---|
STRIIM_SOURCE_PROVIDER_TYPE |
(derived from SOURCE_DB_TYPE) |
Forces the source DatabaseProviderType string in the TQL |
STRIIM_TARGET_PROVIDER_TYPE |
(derived from TARGET_DB_TYPE) |
Forces the target DatabaseProviderType string in the TQL |
| Variable | Default | Description |
|---|---|---|
PG_HOST |
localhost |
PostgreSQL hostname |
PG_DATABASE |
(empty) | Database name |
PG_USER |
(empty) | PostgreSQL username |
PG_PASSWORD |
(empty) | PostgreSQL password |
PG_PORT |
5432 |
PostgreSQL port |
PG_TABLE_ID |
striim_orchestration |
Orchestration table name |
PG_SSLMODE |
prefer |
SSL mode; use require for RDS, Cloud SQL, or managed PG |
The orchestration table is created automatically on first run. Reference DDL: PG_TableCreate.sql.
| Variable | Default | Description |
|---|---|---|
BQ_KEYFILE_LOCATION |
(empty) | Path to service account JSON keyfile |
BQ_PROJECT_ID |
(empty) | GCP project ID |
BQ_DATASET_ID |
(empty) | BigQuery dataset name |
BQ_TABLE_ID |
striim_orchestration |
BigQuery table name |
The orchestration table must be created manually before first use. Reference DDL: BQ_TableCreate.sql.
This is independent of the source Oracle connection above — it may point at the same Oracle instance but is configured separately.
| Variable | Default | Description |
|---|---|---|
ORCH_ORACLE_DSN |
(empty) | Full DSN (use OR host+service) |
ORCH_ORACLE_HOST |
(empty) | Oracle hostname |
ORCH_ORACLE_SERVICE |
(empty) | Oracle service name |
ORCH_ORACLE_PORT |
1521 |
Oracle port |
ORCH_ORACLE_USER |
(empty) | Oracle username |
ORCH_ORACLE_PASSWORD |
(empty) | Oracle password |
ORCH_ORACLE_TABLE_ID |
striim_orchestration |
Orchestration table name |
The orchestration table is auto-created on first run. Reference DDL: ORA_TableCreate.sql.
Two independent Oracle configs:
ORACLE_*is the read-only source account used by the splitter, probe, and watermark.ORCH_ORACLE_*is the state account used by the orchestration backend. Both must be set for a column-split run with Oracle state storage.
All of these also read from a matching environment variable of the same name.
| Setting | Default | Description |
|---|---|---|
PROBE_DEPTH_DEFAULT |
bakeoff |
Default probe depth: lightweight, bakeoff, or adaptive |
PROBE_TARGET_SLICE_SECONDS |
600 |
Desired per-slice runtime in seconds; drives chunk count math |
PROBE_SAMPLE_ROWS |
100000 |
Max rows fetched per timing probe (bounded-sample cap) |
PROBE_TIME_BUDGET_SECONDS |
20 |
Wall-clock cap per timing probe |
PROBE_MAX_CONCURRENCY |
32 |
Ceiling for the adaptive concurrency ramp |
| Setting | Default | Description |
|---|---|---|
BOARD_REFRESH_SECONDS |
5 |
Refresh interval for TinyDB and PG backends |
BOARD_REFRESH_SECONDS_BQ |
30 |
Refresh interval for BigQuery (slower to avoid billable-query spam) |
manage.sh is the operator console. Run with no arguments for the interactive menu, or call a subcommand directly:
./manage.sh <subcommand> [flags]
# Windows:
python manage.py <subcommand> [flags]All subcommands act on UNIQUE_RUN_ID from config.py unless --run-id N is provided.
The Python entry points are fully cross-platform — python manage.py <subcommand> (and python main.py) run natively in cmd or PowerShell, with no shell required. The Oracle driver uses thin mode (no Instant Client), and manage.py board emits plain JSON, so monitoring works anywhere.
The interactive manage.sh TUI is a bash script. On Windows, run it under WSL or Git Bash; native cmd/PowerShell cannot execute it. You lose nothing by skipping it — every menu action maps to a python manage.py … subcommand you can call directly.
Validate configuration, test connectivity, and create or verify the state table.
./manage.sh setup
./manage.sh setup --backend PG # test a specific backend without changing config.pyRun this before your first load. It catches mis-configured variables and permission problems before the load starts.
Show run progress: counts by status, in-flight slices, failed slices.
./manage.sh status
./manage.sh status --all-runs # every run in the state backend
./manage.sh status --failed # only FAILED slices
./manage.sh status --json # machine-readable JSON
./manage.sh status --rows # individual slice rowsRace candidate split predicates and print the recommendation. Does not modify data or write any files.
./manage.sh probe --query-file my_query.sql --table SCHEMA.TABLE
./manage.sh probe --query-file my_query.sql --table SCHEMA.TABLE --depth bakeoff
./manage.sh probe --query-file my_query.sql --table SCHEMA.TABLE --depth adaptive
./manage.sh probe --query-file my_query.sql --table SCHEMA.TABLE --source-engine postgres| Flag | Default | Description |
|---|---|---|
--query-file FILE / --query SQL |
— | The SQL containing ~SPLIT~ |
--table OWNER.TABLE |
required | Driving / splitting table |
--depth lightweight|bakeoff|adaptive |
bakeoff |
Probe depth tier |
--alias |
auto-detect | Driving-table alias for predicate qualification |
--target-slice-seconds N |
600 |
Desired per-slice runtime; drives chunk count |
--sample-rows N |
100000 |
Bounded-sample cap per timing probe |
--time-budget-seconds N |
20 |
Wall-clock cap per timing probe |
--max-concurrency N |
32 |
Ceiling for adaptive concurrency ramp |
--source-engine oracle|postgres|sqlserver|jdbc |
config.SOURCE_DB_TYPE |
Source engine to probe (overrides SOURCE_DB_TYPE for this run) |
Generate queryfile.txt by injecting disjoint slice predicates into your query.
./manage.sh split \
--query-file my_query.sql \
--table SCHEMA.SOURCE_TABLE \
--target SCHEMA.TARGET_TABLE \
--strategy column \
--column CREATED_DT \
--chunks 64| Flag | Default | Description |
|---|---|---|
--query-file FILE / --query SQL |
— | Source query |
--table OWNER.TABLE |
required | Driving table |
--target OWNER.TABLE |
required | Target table (written to each queryfile line) |
--strategy auto|rowid|column|partition |
auto |
Split strategy. auto uses partition if available, else ROWID. |
--column COL |
— | Required for --strategy column |
--chunks N |
— | Number of slices to generate |
--alias |
auto-detect | Driving-table alias |
--subpartitions |
off | One slice per subpartition (partition strategy only) |
--assort |
off | Interleave slices for better concurrency |
--explain |
off | Print the EXPLAIN access path for verification |
--source-engine oracle|postgres|sqlserver|jdbc |
config.SOURCE_DB_TYPE |
Source engine to split against (overrides SOURCE_DB_TYPE for this run) |
Engine picker. Both probe and split read from the engine named by SOURCE_DB_TYPE. Override it per run with --source-engine, or set it once for the whole session:
python manage.py probe --query-file my_query.sql --table OWNER.TABLE --source-engine postgres
python manage.py split --query-file my_query.sql --table OWNER.SRC --target OWNER.TGT \
--strategy column --column CREATED_DT --chunks 64 --source-engine sqlserver
# …or set it once (no flag needed thereafter):
export SOURCE_DB_TYPE=postgresThe interactive manage.sh split/probe wizards prompt for the engine up front (Oracle = default) and append --source-engine automatically when you pick a non-Oracle engine.
Re-queue FAILED slices to NEW so the next python main.py retries them.
./manage.sh reset
./manage.sh reset --include-faileddrop # also re-queue COMPLETED-FAILEDDROP slices
./manage.sh reset --with-striim-cleanup # stop/undeploy/drop leftover ILA_ apps firstRetire a run from the active view (iscurrentrow = FALSE). Run history is preserved.
./manage.sh clear
./manage.sh clear --hard --yes # hard-delete the run's rows
./manage.sh clear --with-striim-cleanup # also clean up leftover ILA_ appsTail the loader log file.
./manage.sh logs
./manage.sh logs --lines 200
./manage.sh logs --follow # stream new lines (like tail -f)
./manage.sh logs --errors # show only ERROR linesEmit the live-board JSON data feed (consumed by the interactive board in menu option 11).
./manage.sh board --jsonRun the SCN-anchored completeness check after the load finishes.
./manage.sh reconcile
./manage.sh reconcile --run-id 100Counts source rows AS OF SCN <watermark> per slice. Emits SELECT COUNT(*) SQL for each target table for you to run manually.
The probe answers: for this specific query on this specific table, what is the fastest disjoint split strategy, how many chunks should I use, and how many should run in parallel?
For each candidate strategy, the probe builds one representative slice (the first range) injected into your exact query, then:
- EXPLAIN — captures the driving access path and flags any large inner table that gets full-scanned (join or JSON amplification)
- Bounded timing fetch — executes that one slice and measures rows/sec and MB/s
The probe never full-scans the real table. Every timing query is wrapped with an engine-specific row limit:
- Oracle:
SELECT * FROM (<slice>) WHERE ROWNUM <= n - PostgreSQL:
SELECT * FROM (<slice>) LIMIT n - SQL Server:
SELECT TOP (n) * FROM (<slice>) - Plus a wall-clock
--time-budget-secondsbackstop (all engines)
No unbounded COUNT(*), no NTILE over the full table. Column range boundaries come from a small SAMPLE clause. Safe to run against trillion-row tables.
| Candidate | Engine | Discovery source | Notes |
|---|---|---|---|
| Physical | Oracle: ROWID · PostgreSQL: ctid · SQL Server: clustered-key range |
Always present | Cheapest disjoint access; no index dependency; stable boundaries. Predicate syntax is engine-specific. |
| Column range | All supported engines | Indexed NUMBER / FLOAT / DATE / TIMESTAMP columns with num_distinct > 1 |
Top 3 by (indexed flag, NDV); equi-depth boundaries from a sample |
| Partition | Engines with native partitioning | Reported when the table is partitioned | Deterministic — not raced; appears as a note only |
Candidates are ranked by measured MB/s, restricted to plans that are disjoint and non-amplifying. AMPLIFYING candidates — those that full-scan an inner table once per slice — are excluded from the ranking but printed with their verdict. Ties break toward physical (ROWID).
slice_bytes = target_slice_seconds × winner_MB_per_sec
chunk_count = ceil(segment_bytes / slice_bytes)
Each slice is sized to run for roughly --target-slice-seconds at the measured throughput. Segment size is engine-specific: Oracle uses dba_segments (falling back to all_tables.blocks × 8K); PostgreSQL uses pg_relation_size; SQL Server uses sys.dm_db_partition_stats.
| Depth | What it does |
|---|---|
lightweight |
Times one physical (ROWID) slice → chunk count only; keeps configured concurrency |
bakeoff (default) |
Races all candidates; picks the fastest disjoint strategy + chunk count |
adaptive |
bakeoff + concurrency-knee ramp (k = 1, 2, 4, … up to --max-concurrency; stops at the plateau) + full amplification audit |
Use bakeoff for most loads. Use adaptive when you want the probe to also recommend a concurrency value, or when your cluster's throughput plateaus at a specific parallelism level.
================================================================
PROBE RECOMMENDATION
================================================================
strategy: column
key: CREATED_DT
chunk_count: 64
concurrency: 8
winner_MB_per_sec: 41.20
----------------------------------------------------------------
label kind MB/s access_path verdict
ROWID physical 28.10 TABLE ACCESS BY ROWID R… good
CREATED_DT column 41.20 INDEX RANGE SCAN good
ENTITY_ID column 3.40 TABLE ACCESS FULL AMPLIFYING
----------------------------------------------------------------
partition: table IS partitioned — consider --strategy partition
warnings:
- AMPLIFYING: candidate 'ENTITY_ID' full-scans PAY.SUBMSN_RESP (join amplification)
================================================================
strategy/key/chunk_count/concurrency→ pass directly tosplitconcurrency: keep configured(bakeoff/lightweight) means leaveCONCURRENT_APPS_MAXas-is- AMPLIFYING candidates full-scan an inner table per slice — they scale the source database load linearly with chunk count and are excluded from ranking
- Partition note appears when the table is partitioned; that path is deterministic and needs no probe
- Full per-candidate report also in the log file, greppable and reproducible
The recommendation is written to logging/probe_recommendation.json:
{ "strategy": "column", "key": "CREATED_DT", "chunk_count": 64,
"concurrency": 8, "table": "SCHEMA.TABLE",
"query_file": "my_query.sql", "depth": "bakeoff" }The interactive wizard reads this file on "apply" to prefill the split step.
| Strategy | Predicate injected | Best for |
|---|---|---|
physical (rowid) |
alias.ROWID BETWEEN 'lo' AND 'hi' (first slice ROWID <= 'hi', last slice ROWID >= 'lo' — see below) |
Default — works on any table; no index needed; cheapest disjoint access path |
| column | alias.col >= lo AND alias.col < hi (half-open) |
An indexed high-NDV column that lets the source engine use an index range scan or partition pruning — often wins on tables with joins or JSON_TABLE |
| partition | Engine-specific, one slice per partition: Oracle rewrites the table reference to OWNER.TABLE PARTITION (P); PostgreSQL swaps the parent for the child relation (FROM schema.child); SQL Server injects a $PARTITION.fn(col) = N predicate |
Already-partitioned table — deterministic, no measurement needed; add --subpartitions for one slice per subpartition (Oracle) |
Write your query with ~SPLIT~ where the predicate belongs and alias the driving table:
SELECT s.A, s.B
FROM PAY.CM_FB_SUBMISSION s
JOIN PAY.CM_FB_SUBMSN_RESP r ON r.T = s.T
WHERE s.ENTITY_TYPE = 'CASE'
AND ~SPLIT~- Pass
--alias stosplitandprobe(or let it auto-detect from the query) - The predicate is alias-qualified (e.g. Oracle:
s.ROWID BETWEEN … AND …) — so it binds to the driving table, not the joined one - If
~SPLIT~is absent, the splitter appendsAND <pred>/WHERE <pred>instead; a marker is more reliable
- Column must be indexed, numeric/date/timestamp, and have
num_distinct > 1 - Boundaries are half-open (
>= lo AND < hi) — prevents double-counting rows that fall on a boundary value - The probe discovers and ranks the top-3 candidates automatically; pass the winner to
split --column
For Oracle, the splitter auto-picks the best available ROWID-chunking method in order of preference:
DBMS_PARALLEL_EXECUTE— Oracle's official block-range API; most precise; requiresEXECUTEprivilegedba_extents— extent-level ranges; requiresSELECTonDBA_EXTENTSNTILE— pure-SQL fallback using row-number bucketing; requires only a read account; slightly less efficient but functionally equivalent
All three produce disjoint, non-overlapping ROWID slices. Other engines use equivalent mechanisms (PostgreSQL: pg_class.relpages block ranges; SQL Server: clustered-index / partition stats).
Open tails (completeness). The first and last ROWID slices are rendered with an open bound — the first as ROWID <= 'hi' (no lower bound) and the last as ROWID >= 'lo' (no upper bound); a single-slice table becomes 1=1 (whole table). Middle slices keep the closed BETWEEN. This guarantees that any row in a block allocated below the minimum or above the maximum boundary — for example a row inserted between chunking and the read — is still captured by initial load rather than falling through a gap. Because the ranges are disjoint and contiguous, opening the tails adds no double-counting.
By default, slices are written in sequential order (first chunk, second chunk, …). If you have many slices and CONCURRENT_APPS_MAX apps in flight, the first wave will hit the same range of data simultaneously — which can create hot blocks or write contention on the target.
Use --assort (or make_assorted_queryfile.py) to reorder the slices so each concurrent wave is a spread across the full data range. The script prints a wave-by-wave summary so you can verify the result.
The orchestrator tracks every slice's status in a configurable backend. All backends use the same schema.
| Backend | STAGE_DB_LOCATION |
When to use |
|---|---|---|
| TinyDB | TinyDB (default) |
Development and single-machine runs; zero setup; stored as a local JSON file |
| PostgreSQL | PG |
Production and on-premises; durable, queryable, safe for concurrent access |
| BigQuery | BQ |
Cloud runs; shareable history; use a slower refresh rate to avoid billable-query costs |
| Oracle | ORACLE |
Production when Oracle is already the state store of record |
TinyDB note: TinyDB is a local JSON file. It is not safe for concurrent writes and is susceptible to partial-read corruption if the process is killed mid-write. For scale or production runs, use PostgreSQL or Oracle.
PostgreSQL:
-- Progress summary
SELECT status, COUNT(*) AS cnt
FROM striim_orchestration
WHERE iscurrentrow = TRUE AND uniquerunid = 100
GROUP BY status ORDER BY status;
-- In-flight slices
SELECT appname, targettbl, status, started_datetime, notes
FROM striim_orchestration
WHERE iscurrentrow = TRUE AND uniquerunid = 100
AND status NOT IN ('COMPLETED', 'FAILED', 'COMPLETED-FAILEDDROP')
ORDER BY roworder;BigQuery:
SELECT status, COUNT(*) AS cnt
FROM `your_project.your_dataset.striim_orchestration`
WHERE iscurrentrow = TRUE AND uniquerunid = 100
GROUP BY status ORDER BY status;Oracle:
SELECT status, COUNT(*) AS cnt
FROM striim_orchestration
WHERE iscurrentrow = 1 AND uniquerunid = 100
GROUP BY status ORDER BY status;iscurrentrow is set to FALSE when a run completes or is cleared. History from multiple UNIQUE_RUN_ID values accumulates in the same table without collision.
Striim's DatabaseWriter commits rows to the target in batches as it reads from the source. There is no distributed transaction between source read and target write: once a batch commits to the target, it is permanent.
If a slice fails midway — whether due to a Striim error, network drop, Oracle timeout, or stall timeout — the rows already committed to the target remain. There is no automatic rollback.
When you reset and re-run the failed slice, Striim re-reads the full slice from scratch. The TQL template handles the resulting duplicate inserts with:
IgnorableExceptionCode: 'DUPLICATE_ROW_EXISTS'
This suppression only works if the target table has a primary key or unique constraint. Without a PK:
DUPLICATE_ROW_EXISTSis never raised- The
DatabaseWriterinserts every row again - The target silently accumulates duplicate rows
- No error is reported; the re-run appears successful
Always define a primary key on the target table before running the load.
| Status | Meaning |
|---|---|
| (blank / NEW) | Registered; not yet dispatched |
RUNNING |
App created, deployed, and started in Striim |
COMPLETED |
App quiesced; undeploy and namespace drop succeeded |
COMPLETED-FAILEDDROP |
App completed (data transferred) but undeploy/drop failed; manual cleanup needed |
FAILED |
Error at create, deploy, start, or stall timeout; see notes column in state table |
Re-queue failed slices and retry:
./manage.sh reset
python main.pyInclude COMPLETED-FAILEDDROP (data transferred; re-run cleanup only):
./manage.sh reset --include-faileddrop
python main.pyWith Striim cleanup (stop/undeploy/drop leftover ILA_ apps before reset):
./manage.sh reset --with-striim-cleanup
python main.pyIf the state backend shows zero rows for a run ID that previously had rows (e.g. you wiped the state manually), the loader refuses to start — preventing an accidental duplicate full load. Override when you genuinely want a fresh start:
python main.py --force-fresh
# or:
FORCE_FRESH=1 python main.pyThe fresh-run marker is deleted automatically on clean completion and by manage.py clear.
A second concurrent python main.py on the same UNIQUE_RUN_ID is refused (PID + start-time lock file at logging/run_<id>.lock). If a prior run crashed without releasing the lock:
rm logging/run_100.lock # Mac / Linux
del logging\run_100.lock # WindowsSLICE_MAX_RUNTIME_SECONDS (default 0 = disabled) force-FAILs any slice that has been RUNNING longer than the configured number of seconds. Useful as a safety net:
export SLICE_MAX_RUNTIME_SECONDS=7200 # fail slices stuck longer than 2 hours
python main.pyStalled slices appear in ./manage.sh status --failed with a [stalled > Ns] note.
A green FINISHED at the end of the run means no slice reported FAILED. It does not prove every row landed — a slice that quiesces on a partial read still shows COMPLETED. manage.py reconcile is the completeness gate: it counts source rows AS OF SCN <watermark> per slice so a falsely-COMPLETED slice is caught.
The run is automatically cleared from the active view (iscurrentrow = FALSE) when all slices complete. Generated TQL files in stage/ are removed. Log files and sidecars remain.
Manual cleanup of logs and sidecars:
# Mac / Linux
rm logging/*.json logging/*.log
rm stage/*.tql
# Windows
del logging\*.json logging\*.log
del stage\*.tql./manage.sh clear # sets iscurrentrow = FALSE; history preservedDirect SQL:
-- PostgreSQL:
UPDATE striim_orchestration SET iscurrentrow = FALSE
WHERE iscurrentrow = TRUE AND uniquerunid = 100;
-- BigQuery:
UPDATE `your_project.your_dataset.striim_orchestration`
SET iscurrentrow = FALSE
WHERE iscurrentrow = TRUE AND uniquerunid = 100;
-- Oracle:
UPDATE striim_orchestration SET iscurrentrow = 0
WHERE iscurrentrow = 1 AND uniquerunid = 100;
COMMIT;./manage.sh clear --hard --yes
# or full reset:
# TRUNCATE TABLE striim_orchestration;If a run was interrupted and apps remain in Striim:
./manage.sh clear --with-striim-cleanup
# or:
./manage.sh reset --with-striim-cleanupTo clean up a specific app manually via TQL:
STOP APPLICATION ILA_100_1.OracleInitialLoadApp;
UNDEPLOY APPLICATION ILA_100_1.OracleInitialLoadApp;
DROP APPLICATION ILA_100_1.OracleInitialLoadApp CASCADE;
DROP NAMESPACE ILA_100_1 CASCADE;
| Symptom | Likely cause | Fix |
|---|---|---|
Authentication failed. Check STRIIM_NODE and credentials. |
Wrong STRIIM_NODE, STRIIM_ADMIN_USER, or STRIIM_ADMIN_PWD; Striim unreachable |
Verify env vars; confirm Striim is reachable on that host + port |
Slices fail at CREATE with failureMessage |
TQL template error or property variables not defined in Striim | Run ./manage.sh setup; paste pvs.tql contents into Striim console; check admin.SW.tql |
All slices stay RUNNING indefinitely, never COMPLETED |
QuiesceOnILCompletion: true missing from DatabaseReader |
Confirm admin.SW.tql has QuiesceOnILCompletion: true in the DatabaseReader block |
| Duplicate rows on target after reset + re-run | Target table has no primary key | Add a PK or unique constraint on the natural key before the next run |
DPI-1047 or python-oracledb not found |
Oracle driver not installed | pip install python-oracledb; verify Oracle Instant Client is present if required |
| Probe returns empty column candidates | No indexed high-NDV columns on driving table | Use --strategy rowid or add an appropriate index |
✗ --table must be OWNER.TABLE, got 'CM_CASES' |
--table/--target given without an owner, or with spaces/typos |
Pass the fully-qualified OWNER.TABLE (e.g. PAY.CM_CASES). In the wizard just re-enter it |
✗ invalid alias identifier / invalid column identifier |
--alias/--column contains characters outside A-Z 0-9 _ $ # (a typo, or an injection attempt) |
Use a plain identifier; a column may be alias.col |
✗ target must be non-empty and must not contain '|' |
--target was empty or contained the | queryfile delimiter |
Supply a plain OWNER.TABLE; don't put |TARGET in the --query-file template |
argument --chunks: must be a positive integer (>= 1) |
A count/duration flag (--chunks, --sample-rows, --target-slice-seconds, --max-concurrency, --lines) was 0, negative, or non-numeric |
Pass a positive integer. Prevents the divide-by-zero and degenerate single-slice results a bad value used to cause |
ValueError: PG_PORT must be an integer, got 'abc' at startup |
A numeric env var / .env value (port or probe tunable) is non-numeric or below its minimum |
Fix the value in your environment or .env |
| Every probe/split slice fails right after connecting | A |TARGET suffix was left on the --query-file template, so the injected SQL ends … WHERE <pred>|TARGET |
Remove the |TARGET from the template; pass the target with split --target (see Step 6) |
REFUSE: run had N rows — use --force-fresh |
State backend was cleared externally while a run existed | Run python main.py --force-fresh if you intend a fresh start |
| Lock file present after crash | Prior run died without releasing lock | Remove logging/run_<id>.lock manually |
| Live board renders garbled on Windows | ANSI escape codes not supported by the terminal | Use WSL or run python manage.py board --json for the raw feed |
BigQuery setup verified but status queries time out |
BQ billing project or credentials issue | Check BQ_KEYFILE_LOCATION, BQ_PROJECT_ID, BQ_DATASET_ID; verify the service account has BigQuery Data Editor |
COMPLETED-FAILEDDROP slices after run |
Striim undeploy/drop failed (503, network blip) | Run ./manage.sh reset --include-faileddrop --with-striim-cleanup then python main.py |
A new copy of the template is instantiated and deployed as a Striim app for every slice. It contains four placeholders, all substituted at generation time by main.py:
~QUERYTEXT~— replaced with the slice SQL (the left side of the|inqueryfile.txt)~TARGETTABLE~— replaced with the target table (the right side of the|)~PROVIDER_TYPE_SRC~— the sourceDatabaseProviderType, derived fromSOURCE_DB_TYPE(override withSTRIIM_SOURCE_PROVIDER_TYPE)~PROVIDER_TYPE_TGT~— the targetDatabaseProviderType, derived fromTARGET_DB_TYPE(override withSTRIIM_TARGET_PROVIDER_TYPE)
The template must reference Striim property variables for all credentials — defined in Step 4 of the walkthrough. Never hardcode credentials in the template or in queryfile.txt.
Key settings in the default template:
| Setting | Value | Purpose |
|---|---|---|
DatabaseProviderType |
~PROVIDER_TYPE_SRC~ / ~PROVIDER_TYPE_TGT~ |
Source/target engine, substituted from SOURCE_DB_TYPE / TARGET_DB_TYPE at generation time |
QuiesceOnILCompletion |
true |
DatabaseReader quiesces when the query is exhausted — required for slice completion detection |
IgnorableExceptionCode |
DUPLICATE_ROW_EXISTS |
Suppresses PK violations on re-run — requires a target PK to function |
BatchPolicy |
EventCount:100,Interval:10 |
Target write batch size |
CommitPolicy |
EventCount:100,Interval:10 |
Target commit frequency |
ConnectionRetryPolicy |
retryInterval=30,maxRetries=3 |
Retry policy for target connection drops |
The template is engine-neutral: DatabaseProviderType is filled from SOURCE_DB_TYPE / TARGET_DB_TYPE at generation time, so the same template serves Oracle, PostgreSQL, and SQL Server. You still supply the matching ConnectionURL format through the SourceConnectionString / TargetConnectionString property variables (see Step 4), and may need to adjust adapter-specific settings for heterogeneous (cross-engine) loads.
Each slice app is deployed into a dedicated Striim namespace:
ILA_{UNIQUE_RUN_ID}_{N}
For example: ILA_100_1, ILA_100_2, ILA_100_3, …
The namespace is:
- Created fresh before each app is deployed
- Dropped automatically after the app completes successfully
- Prefixed with
ILA_for easy identification in the Striim UI
Multiple UNIQUE_RUN_ID values can coexist without namespace collision. The namespace is also the unit of cleanup — all leftover apps from a run can be found and dropped by prefix.
At fresh-run start, the tool writes a watermark sidecar to logging/run_<id>_watermark.json. The label and value fields are supplied by the active source dialect:
{
"run_id": 100,
"label": "Oracle SCN", // Oracle: SCN | PostgreSQL: WAL LSN | SQL Server: LSN
"value": "0000000000123456789", // engine-specific format
"captured_at": "2026-06-27T14:03:11Z",
"source_dsn": "your-host:1521/SERVICE"
}Set your downstream CDC reader's start point to this value. The console banner also displays it:
================================================================
INITIAL LOAD START WATERMARK [Oracle SCN]: 0000000000123456789
captured 2026-06-27T14:03:11Z (run 100)
-> Set the downstream CDC reader's start point to this value.
================================================================
Watermark capture is best-effort: a missing source connection or privilege logs a warning and does not block the load. If the banner is absent, set the CDC start point manually.
| File | Purpose |
|---|---|
main.py |
Orchestrator — the only entry point you run to start or resume a load |
manage.sh |
Bash operator console: interactive menu and all CLI subcommands |
manage.py |
CLI engine backing manage.sh (status / clear / reset / logs / split / probe / board / setup / reconcile) |
config.py |
All tunable settings; credentials read from environment variables |
admin.SW.tql |
TQL application template; deployed as a new app for each slice |
pvs.tql |
Property variable definitions for Striim; run once per environment |
queryfile.txt |
Generated slice list (one line per slice: `SQL |
queryfile.txt.example |
Working example for the seeded PAY.CM_FB_SUBMISSION table |
data.py |
State-backend dispatch: routes to TinyDB / BigQuery / PostgreSQL / Oracle |
data_tinydb.py |
TinyDB backend implementation |
data_bq.py |
BigQuery backend implementation |
data_pg.py |
PostgreSQL backend implementation |
data_oracle.py |
Oracle state backend implementation |
models.py |
QueryResult data class shared across backends |
source_dialect.py |
SourceDialect interface + get_dialect() factory |
oracle_dialect.py |
Oracle provider: candidates, boundaries, EXPLAIN, sizing, SCN watermark |
oracle_boundaries.py |
ROWID and column boundary discovery |
oracle_client.py |
Read-only Oracle connection helper (lazy import) |
probe.py |
Engine-agnostic bakeoff: timing, ranking, chunk count math, recommendation panel |
query_split.py |
Predicate injection (~SPLIT~ substitution) |
split_runner.py |
Queryfile generation driver |
watermark.py |
CDC watermark banner and sidecar persistence |
run_safety.py |
Fresh-run guard (marker + decide_startup), run lock, stall detection |
reconcile.py |
final_verdict, reconcile_count_sql, is_snapshot_too_old helpers |
striim_monitor.py |
Best-effort Striim per-app throughput helper for the live board |
make_assorted_queryfile.py |
Reorders queryfile.txt for concurrency interleaving |
seed_data.py |
Seeds the example PAY.CM_FB_SUBMISSION table for testing |
BQ_TableCreate.sql |
BigQuery orchestration table DDL (must be run manually before first BQ use) |
PG_TableCreate.sql |
PostgreSQL orchestration table DDL (reference; auto-created on first use) |
ORA_TableCreate.sql |
Oracle orchestration table DDL (reference; auto-created on first use) |
oracle_rowsplit.sql |
Manual SQL*Plus ROWID-split helper (legacy; see Appendix) |
requirements.txt |
Python dependencies (pip install -r requirements.txt) |
requirements-dev.txt |
Dev and test dependencies |
SOURCE_DB_TYPE (default oracle) selects the active source engine; oracle, postgres, sqlserver, and jdbc are all implemented. Override per run with --source-engine on probe/split. Drivers are lazy/optional — see the driver matrix.
| Capability | oracle |
postgres |
sqlserver |
jdbc |
|---|---|---|---|---|
| Physical split (ROWID / ctid / clustered-index range) | yes | yes | yes | no |
| Column-range split | yes | yes | yes | yes |
| Partition split | yes | yes | yes | no |
Native EXPLAIN (--explain) |
yes | yes | yes | no |
| Probe chunk-count discovery | yes | yes | yes | pass explicit --chunks |
| Reconcile anchoring | snapshot (flashback SCN) | best-effort | live row counts | live row counts |
- Postgres and SQL Server support physical, column, and partition probing/splitting and native EXPLAIN — essentially feature-parity with Oracle.
- JDBC is best-effort: column-range splitting only — no physical/partition splitting and no native EXPLAIN. Because it cannot size itself from physical stats, pass an explicit
--chunks Ntosplit. - Reconcile completeness is snapshot-anchored only on Oracle (flashback SCN gives a stable point-in-time count). PostgreSQL reconcile is best-effort; SQL Server and JDBC reconcile against live row counts, which can drift on an append-heavy source.
The same parallel-extract design maps each engine onto the shared SourceDialect interface:
| Concern | Oracle | PostgreSQL | SQL Server |
|---|---|---|---|
| Physical split key | ROWID block ranges |
ctid ranges via pg_class.relpages |
Clustered-index key range / partition |
| Boundary discovery | DBMS_PARALLEL_EXECUTE / dba_extents / NTILE |
Block ranges from relpages |
sys.dm_db_partition_stats |
| Partitions | all_tab_partitions |
pg_inherits / declarative |
sys.partition_range_values |
| CDC watermark | current_scn from v$database |
pg_current_wal_lsn() |
sys.fn_cdc_get_max_lsn() / @@DBTS |
| EXPLAIN | EXPLAIN PLAN FOR |
EXPLAIN (FORMAT JSON) |
SET SHOWPLAN_XML ON |
| Size estimate | dba_segments.bytes |
pg_relation_size |
sys.dm_db_partition_stats |
| Row-limit cap (probe) | ROWNUM <= n wrap |
LIMIT n |
TOP (n) |
PostgreSQL maps almost 1:1 onto the Oracle design (ctid ≈ ROWID, WAL LSN ≈ SCN). Adding a new engine is additive — the orchestrator, probe math, CLI, logging, chunk sizing, watermark, and live board are all engine-agnostic.
- TinyDB: not safe for concurrent writes; susceptible to partial-read corruption if the process is killed mid-write. Use PostgreSQL or Oracle for scale and production runs.
- BigQuery state table: must be created manually before first use via
BQ_TableCreate.sql— auto-creation is not implemented for BigQuery. - Schema verification: PostgreSQL and BigQuery backends verify that the state table exists but do not check column-level schema. If you have a table with an outdated schema shape, compare the live table against the reference DDL manually.
- Partitioned-table ROWID (dba_extents path): may produce a cartesian join on some partitioned tables — narrow trigger, and avoided automatically when
DBMS_PARALLEL_EXECUTEis available. - CLOB columns: a read-after-fetchall limitation in some Oracle driver versions applies when the query returns CLOBs — verify before running at scale.
- JDBC source: best-effort only — column-range splitting, no physical/partition splitting, no native EXPLAIN, and no automatic chunk-count discovery (always pass an explicit
--chunks N). - Reconcile anchoring: snapshot-anchored (point-in-time) only on Oracle. PostgreSQL is best-effort; SQL Server and JDBC reconcile against live row counts, which can over- or under-count on an append-heavy source mid-load.
- Deferred: auto-retry/backoff for Striim API transient errors; persistent attempt counters per slice; poison-quarantine for repeatedly-failing slices; cluster backpressure; cross-backend schema migration utility.
Advanced / legacy. The
probe→splitflow above supersedes this for almost all cases. Use it only when you cannot run the Python splitter (no Oracle driver available in your environment) and need to paste ROWID slices by hand.
oracle_rowsplit.sql is a self-contained SQL*Plus / SQL Developer script that generates ROWID-range slice lines for any Oracle table. Run it against your table, copy the output, and paste the resulting query|target lines into queryfile.txt:
SELECT * FROM SCHEMA.TABLE WHERE ROWID BETWEEN 'AAAFxpAABAAALMsAAA' AND 'AAAFxpAABAAALMsAAH'|SCHEMA2.TABLE_TGT
SELECT * FROM SCHEMA.TABLE WHERE ROWID BETWEEN 'AAAFxpAABAAALMsAAI' AND 'AAAFxpAABAAALMsAAQ'|SCHEMA2.TABLE_TGT
These lines coexist with any other query groups in the same queryfile.txt. You can still interleave with make_assorted_queryfile.py before running python main.py.
This approach produces only physical ROWID slices with a fixed chunk count and no throughput measurement — the probe exists precisely to replace that guesswork.