Architecture
The design rationale is in the Pipeline Engineering blog series. In short:
- Config loader (
pipetree.config) — parses and validates the YAML, failing fast with an error that names the offending key (silver.tables.orders.merge.delete_mode: ...). - Typed model (
pipetree.model) — defaults resolved, every table given a fully qualified name fromtable_schema(defaulting to its layer) + its block key. - Graph builder (
pipetree.graph) — derives the dependency tree (autoparses the logic file: Spark SQL with sqlglot, PySpark withast, and fails with a clear error for a table name only known at run time;depends_onlists are matched by fqn or, if unambiguous, by bare table name), sorts it topologically, and reports a cycle’s full path if it finds one.select.pyresolves--select/--with-dependentsto a set of fqns to run;render.pydraws the tree as text or Mermaid,html.pyas a self-contained HTML picture laid out in waves (waves.py). - Executor (
pipetree.executor) — walks the dependency tree (not the layers) with a thread pool and a ready queue: a table starts the instant its parents have succeeded. Retries transient errors only (throttling, timeouts, connection resets) with exponential backoff and jitter, marks every descendant of a failed tableupstream_failedinstead of stopping the run, and marks anything outside a--selectscopeskipped(skipped tables never affect the run’s overall success). - Adapters (
pipetree.adapters) sit behindrun_table(table)and know nothing about the engine.SparkAdapter(pipetree.adapters.spark) is the one real implementation - local Spark + Delta today, the same code path Fabric and Databricks run on - withscd1andscd2implemented as a Delta merge, andreplaceandappendas plain overwrite and append writes. With no active SparkSession it builds a local one; on a real cluster it reuses the one already running. It also translates the engine’s transient errors (Delta commit conflicts, throttled storage, socket timeouts and resets) into the three types the executor retries (pipetree.adapters.spark.transient). - Sources (
pipetree.sources) read asourcetable’s raw input, keyed onsystems.<name>.type:csv/json/parquet(local files, the example project’s stand-in for a real connector),sqlserver(JDBC),kusto,storage_stream(Structured Streaming withTrigger.AvailableNow, so an always-on stream becomes a bounded unit of work the executor can run once and finish -d365_exportandsynapse_linkare aliases of it) andcustom(a dotted-path class you bring yourself, e.g. for Microsoft Graph). - Schema drift (
pipetree.schema) — structure is inferred, never declared. Before an scd1/scd2 merge, the source’s schema is diffed against the target’s (added/removed/retyped/nullability), andschema_policy(evolveby default, orfail/ignoreper table) decides what happens:evolveapplies safe numeric widenings and lets Delta add a genuinely new column (withSchemaEvolution()), never drops a removed one;failraises before anything is written;ignorekeeps only the columns already in the target. Every non-empty diff is recorded in the run log (append/replacedon’t enforce a policy -replacealready means “whatever the source says, goes”). - Audit columns - every table gets
_inserted_at,_updated_at,_is_deleted,_execution_id,_source_system(plus_valid_from,_valid_to,_is_currentonscd2). They’re reserved: any of these an incoming source already carries (aSELECT *from an upstream pipetree table, say) are dropped before the merge, since pipetree stamps the table’s own. To use an upstream stamp downstream (e.g. indelete_whenorsequence_by), alias it in the logic file (_is_deleted AS upstream_is_deleted). - Unknown member -
unknown_member: trueseeds one row whose business key columns each hold a sentinel of their own type, for facts to resolve a missing foreign key to:-1for a numeric key, SQLNULLfor a string,1900-01-01for a date or timestamp (other key types are rejected; a composite key gets one sentinel per column). A fact reaches aNULL-keyed unknown member with a null-safe join (<=>/eqNullSafe); a plain=join behaves exactly as before for every matched row. Seeding is null-safe and idempotent. On such a table a source row with aNULLin any business key column is dropped (logged as a warning, counted asnull_keys_droppedin the run log) -NULLis reserved for the unknown member there. - NULL business keys - on an
scd1/scd2table withoutunknown_member, a source row whose key isNULL(a single-column key) orNULLin every key column (a composite key) cannot identify a row: it is dropped with a warning and counted asnull_keys_dropped. A composite key with aNULLin just one column is an ordinary value: keys are matched null-safely, so the same key finds the same row in every batch.replaceandappendtables withoutunknown_memberkeep every row as it comes (aNULLkey is data there). - Snapshot merge -
merge.snapshot: trueon anscd1/scd2table treats the batch as the complete current state of the source: an active key that is missing from it is deleted followingdelete_mode, guarded bysnapshot_max_missing_ratio(default 0.5) andsnapshot_allow_empty. See Snapshot merge. - Surrogate keys -
surrogate_key: <column>on anscd1/scd2dimension adds a Kimball surrogate key: a Delta identity column (BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1 INCREMENT BY 1)), first in the table, created with it. See Surrogate key. - Platforms (
pipetree.platform) answer “where am I running?” for secrets, table naming, storage paths and run metadata -LocalPlatform(env-var secrets),DatabricksPlatform(Key Vault read directly with a Unity Catalog service credential, or a pluggablesecret_resolver; Unity Catalog naming is handled by setting the session’s default catalog, not by rewriting every table reference),FabricPlatform(notebookutils.credentials, the attached lakehouse’sFiles/mount). Every platform also implementsacquire_token(resource)- a short-lived AAD token via the platform’s own identity, for thesqlserver/kustosources’auth.mode: aad_tokenpath: on Databricks, a named Unity Catalog service credential per resource (dbutils.credentials.getServiceCredentialsProvider(name), DBR 16.2+); on Fabric,notebookutils.credentials.getToken, which runs as the notebook’s executing identity (the user, the schedule owner, or the pipeline’s last modifier - the workspace identity only when a pipeline Notebook activity uses a Workspace Identity connection) and supports a limited set of audiences (storage,pbi,keyvault,kusto, plus Kusto cluster URIs), so custom app audiences are Databricks/local only;DefaultAzureCredentiallocally. Tokens are minted once on the driver and not refreshed, so a read must finish within the token’s lifetime (60-90 minutes by default). Secret-based auth stays the default for anyone without managed-identity infrastructure available; token-based auth is additive, not a replacement.detect()picks one from runtime markers. - Run log & notifier (
pipetree.runlog) — every table’s result is collected in memory and written once, at the end, to_meta.pipeline_run_log(areplaceWhereon_execution_id, so re-running the same execution id overwrites rather than duplicates); a pluggableNotifierreports the digest (ConsoleNotifierby default; anotifications.emailblock in the YAML also mails a failed run through Azure Communication Services, see the Notifications guide). - Progress (
pipetree.executor.events,pipetree.notebook) - an optionalRunObserversees the run while it happens (per-table running/retrying/finished events);LiveGraphViewandHtmlFileObserverturn that into the live picture. See Seeing the run. - Declarative pipelines (
pipetree.declarative.autocdc) — a pure translation of anscd1/scd2table into Databricks’ AUTO CDC flow syntax (KEYS,SEQUENCE BY,APPLY AS DELETE WHEN,STORED AS SCD TYPE,TRACK HISTORY ON * EXCEPT) - the comparison part 1 draws, made mechanical. Not an executor: AUTO CDC only runs inside a Lakeflow Declarative Pipeline, where the platform owns the DAG; this compiles to it, and doesn’t attempt to run anything itself. The flow readsFROM STREAM <source>, as Databricks requires a streaming source. Semantics differ from pipetree’s merge: scd2 history uses__START_AT/__END_AT(SEQUENCE BY values, no audit columns), duplicates of a key within one micro-batch each become history versions. A row with an older sequence is handled by sequence in the same micro-batch and in a later one (scd1 keeps the higher sequence, scd2 inserts an earlier version); pipetree follows the same rule whensequence_byis configured.delete_modeis only modeled forignore.