SQL Pipelines
Purpose#
SQL Pipelines is where you write transformations by hand. A task reads from a source, applies SQL you control, and writes into a destination table with an explicit load mode.
Unlike CDC (which mirrors) or streams (which pull an API), SQL Pipelines makes no assumptions about your model. You supply the query; Abrq DIP supplies the loading, the incremental bookkeeping, the retries, the drift handling, and the run history.
The surface lives at /etl.
How the surface is organised#
Projects group tasks. A project is the unit of ownership and access control: membership, roles, and environments are all per-project.
When you create a project two things happen automatically:
- You become its Owner. The Owner role carries project administration — updating the project, deleting it, and managing its members.
- A default
devenvironment is created. It is unprotected, and it is the only environment created for you. Everything else you add deliberately.
Inside a project, tasks are defined once and run in an environment. The environment supplies the connectors. This split is what makes the same task runnable against a scratch database and a production database — see Environments and promotion.
Before you begin#
- Data connectors for the databases you will read from and write to. Create them first — see Data connectors.
- Connector bindings on the environment you intend to run in. A task names
a role (
source,destination); the environment maps that role to a real connector. An unbound role fails the run loudly. - A destination you own. Abrq DIP writes rows into your table. It never
runs
ALTERagainst an operator-owned table — see Schema drift.
Required role#
| Action | Global role | Project capability | Typical project role |
|---|---|---|---|
| View projects, tasks, runs, logs | viewer or higher |
project.view, task.view |
any member |
| Create / edit / delete a task | editor or higher |
task.create, task.update, task.delete |
Developer (unprotected environments only), Maintainer, Owner |
| Run a task, cancel a run, replay quarantine | editor or higher |
task.run |
Developer (unprotected environments only), Maintainer, Owner |
| Promote a task to another environment | editor or higher |
task.promote (checked against the target environment) |
Maintainer, Owner |
| Manage environments | editor or higher |
project.environment.manage |
Maintainer, Owner |
| Manage members, update or delete the project | editor or higher |
project.member.manage, project.update, project.delete |
Owner |
task.create, task.update, task.delete, task.run, connection.bind and
schedule.manage are environment-gated: a Developer holds them in
unprotected environments only, and is refused in a protected one with the reason
protected_environment. Maintainers and Owners hold them everywhere. The global
admin role is an implicit Owner on every project.
Note. The builder's validation probes (below) are gated on the global role ladder, not on project capabilities:
editoror higher for preview, render-preview, drift, quality and contract checks; any authenticated user for SQL validation. They are read-only and never write to your data.
Create a project#
- Go to /etl and choose New project.
- Give it a name and description.
- Save.
You are now its Owner and it has a dev environment. Bind connectors to dev
before you build your first task — see
Environments and promotion.
Create a task#
- Open the project and choose New task.
- Pick the task type (see Task types).
- Set the source role and destination role — the names the environment resolves to connectors.
- Supply the logic: SQL for the SQL types, or a source table for
table_copy. - Set the destination schema and table.
- Pick the load mode and fill in whatever that mode requires (see Load modes).
- Set operational behaviour: timeout, retries,
on_row_error,on_schema_drift. - Validate with the probes, then save.
Task types#
| Type | What it does | SQL | Notes |
|---|---|---|---|
sql_extract_load |
Runs your SELECT against the source connector and loads the result into the destination connector. | required | The cross-database workhorse: source and destination can be different servers or different engines. |
sql_transform |
Runs SQL to reshape data that is already in the destination. | required | Use for staging-to-mart steps within one database. |
table_copy |
Copies a named source table to a destination table with no SQL body. | none | The straight lift-and-shift. Fewest moving parts; also the fewest options (see the sample-run note below). |
Load modes#
| Mode | Behaviour | Requires |
|---|---|---|
full_refresh |
Replaces the destination contents each run. | — |
append |
Adds rows; never updates or deletes. | — |
upsert |
Updates rows matching the keys, inserts the rest. | unique/business keys |
incremental |
Reads only rows newer than the stored watermark, then loads them. | an incremental column |
historical_upsert |
SCD Type 2. Never overwrites a current row: it expires the existing version and inserts a new one, keyed on natural keys and change-detected by a tracked-column hash. | keys + history columns |
For the full semantics of each mode — including exactly what "newer than the watermark" means and how the SCD2 hash is computed — see Load modes.
History columns (historical_upsert only)
historical_upsert needs four bookkeeping columns. The history columns
mode decides who owns them:
| Mode | Meaning |
|---|---|
auto |
Abrq DIP creates and names them for you. |
custom |
You map existing destination columns to each of the four roles. |
Default names in auto mode:
| Role | Default column |
|---|---|
| Current-version flag | is_current |
| Version start | row_effective_dt |
| Version end | row_expired_dt |
| Change-detection hash | record_hash |
Pre- and post-SQL#
Two optional hooks run around the load, against the destination:
- Pre-SQL runs before the load. Typical uses: truncate a staging table, create an index-free landing area, set a session parameter.
- Post-SQL runs after a successful load. Typical uses: rebuild an index, refresh a materialized view, insert an audit row.
Keep both idempotent. A retried run runs them again.
Schema drift#
Drift is the source projection producing columns the destination table does not have.
The ETL vocabulary has exactly two values, and this is deliberate:
on_schema_drift |
Behaviour |
|---|---|
block_and_alert |
Default. The run halts when the source projection has columns the destination lacks, and a drift alert is dispatched. |
proceed |
The run projects to the column intersection — the drifted columns are dropped from the load — and warns. |
Warning. ETL never
ALTERs an operator-owned table. There is noauto_addoption here, unlike CDC and file ingestion, which manage their own destination tables. If you want a new column loaded, add it to the destination yourself and the next run will pick it up.
Columns present at the destination but absent from the source are tolerated: the writer simply omits them. See Schema drift policies.
Row errors and quarantine#
on_row_error decides what happens when the destination rejects an individual
row — a constraint violation, a type conflict, an over-long value.
on_row_error |
Behaviour |
|---|---|
fail |
Default. One bad row fails the run. |
quarantine |
Bad rows are set aside; the rest of the batch lands. Each quarantined row is stored with the error text the destination returned. |
Quarantine is the right default for messy upstream data where partial progress beats a nightly failure. It is the wrong choice when the load must be all-or- nothing.
Quarantined rows are read back per run:
curl -s \
-H "Authorization: Bearer <API_TOKEN>" \
"https://<ABRQ_DIP_HOST>/api/v1/etl/runs/<RUN_ID>/quarantine?limit=200"
limit defaults to 200 and caps at 1000; rows come back oldest-first.
Each record carries the row payload, the destination's error_text, a
replay_status (pending, replayed, failed), and timestamps.
Once you have fixed the cause — widened a column, added a missing lookup row —
replay the quarantined rows. Replay re-attempts either a list of specific
rows or every row still pending, and reports how many were replayed, failed
or skipped. Replaying requires task.run.
Data contracts#
A data contract is a list of columns the task promises the destination will have. It is enforcement, not documentation.
expected_columns— the promised column names.enforce— defaults to on. When on, the contract gates the run.
The gate runs before any data is loaded or copied. If the destination table exists and any promised column is missing, the run is refused with a data- contract-breach error naming the destination table and the missing columns. It does not warn and continue.
Columns present at the destination but not declared in the contract are
reported as extra and do not breach it. A contract asserts a floor, not an
exact shape. Turn enforce off to keep the contract as a check you run by hand
without gating runs.
Validate before you save#
The builder has six read-only probes. None of them writes to your data, and all of them can be run as often as you like.
| Probe | What it does | What it returns |
|---|---|---|
| Validate SQL | Parses the SQL for the source engine's dialect. No database connection. | valid, error, dialect, normalized_sql, referenced_tables |
| Preview | Executes the SELECT against the source and returns a bounded sample. | columns, rows, row_count, truncated, masked_columns |
| Render preview | Substitutes built-in template variables into the SQL so you can read what will actually run. | variables, rendered_sql, unresolved |
| Check drift | Compares the source projection against the destination table. | destination_exists, has_drift, missing_at_dest, extra_at_dest, matched, note |
| Check quality | Runs every enabled data-quality check (count queries only). | total, passed, failed, errored, and a per-check breakdown |
| Check contract | Compares the contract's promised columns against the destination. | has_contract, destination_exists, satisfied, missing, extra, matched |
Notes worth acting on:
- Preview honours data masking. Columns your masking policy hides for you
are listed in
masked_columnsand their raw values are never returned. A preview is not a way around masking. - Preview is bounded. The limit defaults to 50 rows and caps at 200;
truncatedtells you whether more rows existed. unresolvedin render preview is normal. Job parameters are supplied at job-run time, so they legitimately remain unsubstituted in the builder. What should worry you is a placeholder you expected to be a built-in showing up there — that is a typo.- Validate SQL always answers 200. Invalid SQL comes back as
valid: falsewith a message, not as an HTTP error. missing_at_destis the drift that matters.extra_at_destis tolerated.
Data-quality checks come in four types — not_null, unique, row_count,
accepted_values — each at severity warn or error. In a check-quality
result, errored counts failing checks at severity error.
Run a task and read the result#
- Open the task and choose Run.
- Pick the environment.
- Optionally switch on Sample run to bound the read — see Environments and promotion.
- Watch the run detail.
The run detail shows three things:
- Logs — the ordered narrative of the run, including which connector each role resolved to, the rendered SQL, drift decisions, quarantine counts, and the failure with its cause when there is one.
- Metrics — rows read, rows written, duration, and per-phase timings.
- Quarantined rows — present when
on_row_error = quarantineand the destination rejected rows. Each row shows its payload and the error text, and can be replayed from here.
Runs also appear in the cross-family monitor at Executions, alongside CDC, streams, file ingestions and exports.
Retries and timeouts#
- Timeout bounds a single run. A run that exceeds it is stopped and marked failed.
- Retry count and retry delay control automatic re-attempts after a failure. Retries repeat pre-SQL and post-SQL, which is why both should be idempotent.
Field reference#
| Field | Type | Default | Notes |
|---|---|---|---|
| Name | text | — | Identifies the task in lists, runs and logs. |
| Task type | sql_extract_load, sql_transform, table_copy |
— | Immutable in spirit: changing it changes what the task means. |
| Source role | source |
— | Resolved to a connector by the environment. |
| Destination role | destination |
— | Resolved to a connector by the environment. |
| SQL | text | — | Required for the two SQL types; absent for table_copy. |
| Destination schema | text | — | The schema the task writes into. |
| Destination table | text | — | The table the task writes into. You own it. |
| Load mode | see Load modes | — | Decides the write strategy. |
| Keys | column list | — | Required by upsert and historical_upsert. |
| Incremental column | column | — | Required by incremental; also required for time-window sample runs. |
| History columns mode | auto, custom |
auto |
historical_upsert only. |
| Pre-SQL | text | empty | Runs against the destination before the load. |
| Post-SQL | text | empty | Runs against the destination after a successful load. |
| Enabled | boolean | on | A disabled task is skipped by jobs and schedules. |
| Timeout | duration | — | Bounds a single run. |
| Retry count | integer | — | Automatic re-attempts after failure. |
| Retry delay | duration | — | Wait between re-attempts. |
on_row_error |
fail, quarantine |
fail |
See Row errors and quarantine. |
on_schema_drift |
block_and_alert, proceed |
block_and_alert |
Two values only. ETL never ALTERs your table. |
Failure modes#
| What you see | Cause | Fix |
|---|---|---|
| Run fails immediately naming an environment and a connector role | The environment has no connector bound to that role. Resolution is fail-loud and never falls back. | Bind the role in that environment — see Environments and promotion. |
| Run blocked with a schema-drift error | on_schema_drift = block_and_alert and the source projection has columns the destination lacks. |
Add the columns to your destination table, or switch to proceed to load the intersection. |
| Columns silently missing from loaded rows | on_schema_drift = proceed — the run projected to the column intersection and warned. |
Read the run's warnings; add the columns to the destination if you want them. |
| Run refused before loading: data contract breach | The contract has enforce on and the destination is missing a promised column. |
Add the column, or amend the contract's expected_columns. |
| Run fails on one bad row | on_row_error = fail. |
Switch to quarantine if partial progress is acceptable. |
| Rows land but counts are short | Rows were quarantined. | Read the run's quarantined rows, fix the cause, replay. |
Action denied with reason protected_environment |
You hold the Developer role and the target environment is protected. | Ask a Maintainer or Owner, or work in an unprotected environment and promote. |
| Preview returns fewer columns than expected | Data masking removed them for your identity; they are listed in masked_columns. |
Expected. Masking is not bypassable from the builder. |
Render preview shows placeholders under unresolved |
Job parameters are substituted at job-run time, not in the builder. | Expected — unless the placeholder was meant to be a built-in, in which case fix the spelling. |
| Watermark did not advance after a run | The run was a sample run. | Expected. Run normally when the load should count. |
403 license_expired on run |
The deployment's license has expired. | See Expiry and clock. |
See also#
- Load modes
- Schema drift policies
- Environments and promotion
- Jobs — orchestrating tasks into a DAG
- Schedules
- Executions
- RBAC