Repository navigation
feat(databricks): a build can be a Lakeflow pipeline whose expectations are the contract's DQ rules - #872
Merged
Conversation
engine: lakeflow is a Databricks Lakeflow Declarative Pipeline: pattern embedded-logic, properties.sql the SELECT of the dataset an expose binds. Stable 0.7.6 is unchanged, so a contract opts in with fluidVersion 0.7.7 (founder decision 2026-11-12). util/contract.py: is_lakeflow_build, and lakeflow joins EXTERNALLY_RUN_ENGINES, so no local provider, planner or runner ever runs its SQL; only the pipeline does.
The shared reading of contract dq rules (read_rules -> DqCheck) that the Databricks dbt-tests work is landing. Copied unchanged so the Lakeflow emitter can be built against it; this commit is dropped at the rebase onto main once that branch has merged, and main's copy is kept.
…s expectations A build with engine lakeflow is one serverless Lakeflow pipeline per build, publishing the expose's table into the product's own catalog and schema. forge-cli writes the pipeline's source around properties.sql: CREATE OR REFRESH MATERIALIZED VIEW with the expose's columns (types, NOT NULL, comments) and one expectation per row-level dq rule, so the pipeline graph's Data quality tab shows each rule's passed and failed counts. - Severity: info and warn are EXPECT (kept, counted), error is ON VIOLATION DROP ROW, critical is ON VIOLATION FAIL UPDATE. No quarantine table: the dropped rows would sit outside the contract's access policy. - uniqueness, freshness and a valid_values rule whose column carries labels.references need the whole table: one PRIVATE materialized view per check in the same pipeline (never published), whose one expectation is named after the rule. A reference resolves to the referenced product's table through the workspace (fluid.workspace.yaml, FLUID_UPSTREAM_CONTRACTS), as embedded-SQL builds resolve consumes[]. schema is the declared column list itself. - The pipeline owns its table (founder decision 2026-11-12): no databricks_sql_table, its readers get SELECT on the product's schema (exists before the first update), and its table and column tags are set after each update fluid apply runs (they survive refreshes, measured on Free Edition). Masks, row filters, write grants, partitions, expire, another writer and a shared schema are refused for now. - A materialized view, not a streaming table: the query reads tables a dbt CREATE OR REPLACE rewrites. Manual or continuous trigger; cron refused. The rule-to-check mapping follows DQX (databrickslabs/dqx); the whole-table checks follow Lakeflow's own expectation patterns (a validation view per primary-key and referential check); the upload layout follows Databricks Asset Bundles, like the dbt job's.
… reports its rules Under --mode amend-and-build (replace-and-build: a full refresh) a lakeflow build starts an update of the pipeline this apply's OpenTofu state holds for it (by the fluid_product / fluid_build tags, as the dbt job runner finds its job), waits for it, and prints each rule's passed, failed and dropped records from that update's event log: what the pipeline graph's Data quality tab shows. A failed update fails the build with the event log's errors. After a completed update the table's and its columns' tags are set as the contract declares them, and the keys the previous run recorded (table properties fluid.tags, fluid.column_tags) but the contract no longer has are removed; a tag that cannot be set fails the build. amend and create-only only provision, and a continuous pipeline is not started. Found live on Free Edition: Lakeflow retried a failed triggered update by itself (cause RETRY_ON_FAILURE), and the retries kept running after the apply had failed. A triggered pipeline now sets pipelines.numUpdateRetryAttempts to 0: the same rows fail the same rule. The polling, interrupt handling and printing reuse the dbt job runner's (_databricks_job_run.py, after databricks-sdk-py's waiters); the update states and event-log fields are the Pipelines API's, read live.
fluid verify called the Databricks inspector outside the contract file's contract_source block, so the emitter's pass it checks against could not resolve a Lakeflow reference check's product from the workspace: the pipeline dropped out, and verify expected 1 tag on a table that carries 10 (found live). It now compiles as fluid apply does.
The unreleased Added entry, the AGENTS.md paragraph beside the dbt job runs, and the unit, tofu validate and manual live (Free Edition, 2026-11-12) rows.
…op flat ripwire --quality-delta flagged what the Lakeflow commits grew: _compile (complexity 16 to 19), managed_objects (14 to 33), the build loop's second copy of the job branch, _merge_pipelines as a clone of _merge_jobs, and _rules at 42. The table loop is _emit_tables, verify's pipeline readers and tags are _pipeline_readers_and_tags, one _merge_part takes jobs and pipelines, a lakeflow build and a dbt job build share one branch of run_builds_from_args, and _rules is four small helpers. No behaviour change: 1166 Databricks tests pass as before.
…rsion 0.7.7 A 0.7.6 contract with engine: lakeflow got jsonschema's anyOf message, "'lakeflow' is not valid under any of the given schemas", which does not say that the engine is in the 0.7.7 preview. format_schema_error now names it and the fix.
Security review (both below the report's cutoff, fixed anyway): - A query that left a /* comment open swallowed what forge-cli wrote after it, up to a */ inside a rule's description, so the text after that ran as a second statement (a dataset anywhere the deploying identity can write) and a check view vanished. The check views now come first and the contract's query last in the pipeline file, and a query that leaves a comment, string or backtick name open is refused (databricks-lakeflow-query), lexed as Spark SQL lexes it. - Readers' SELECT on the product's schema also covered tables other teams keep in a schema the product adopted. Each such grant now has a precondition over data.databricks_tables: the plan fails, naming the other tables, unless the schema holds only the pipeline's table (databricks-lakeflow-schema-not-owned). Live: the pipeline's own materialized view passes it; a foreign table fails the plan with one clean line (bare names: the CLI's redactor masks a.b.c as a JWT). Delivery review (P2): - A dbt build naming the expose in its outputs beside a lakeflow build with none crashed _compile with a KeyError; now databricks-lakeflow- writers. - The tag sync read a recorded column the table no longer has, failing a completed update; such a column took its tags with it and is skipped. - fluid verify told a pipeline's table to re-run plain fluid apply for missing tags; it names --mode amend-and-build (ManagedTable. pipeline_written). - The data-loss gate counting a pipeline's removal (and not its file and folder) is pinned by a test. Also: a reference whose product consumes[] does not list resolves to the product's one Databricks expose with that column (dq_checks now reports no expose id for it), and the validate hint no longer writes a literal fluidVersion (a repo guard forbids it).
…reconciling Keys go on a table with ALTER TABLE ADD CONSTRAINT, which a Lakeflow pipeline's materialized view refuses, so every apply would print a failed key. Until the pipeline's definition carries them (Lakeflow takes PRIMARY KEY and FOREIGN KEY constraints there), fluid validate says they are not declared and post_apply leaves the table out.
A defect scan of the Lakeflow commits found two blocking defects and seven small ones. - Two dq rules with the same id gave the pipeline two constraints of one name, which Lakeflow refuses at the first update. The emitter refuses them (databricks-lakeflow-rule-id) and names the id. - The tag keys forge-cli manages were recorded in TBLPROPERTIES, and a refresh of a changed definition dropped that record, so a label the contract stopped declaring stayed on the table. The record is gone: a tag is removed when the contract no longer declares it and the deploying identity (SCIM Me) set it. A steward's or an inherited tag stays; an unreadable identity removes nothing and says so. - A continuous pipeline is not started by amend-and-build, so its tags were never set. They are now set once its table exists. - Ctrl-C during the start request left a running update behind: the interrupt guard now covers the request as well as the wait. - The lexer read r'...' as a string with backslash escapes; Spark reads a raw string without them. A malformed event-log payload no longer raises, a rule's own severity (rule_severity) wins over the check's, and a failing info/warn rule is marked as a warning, not an error. - The schema precondition counts views as well as tables, the USE CATALOG notice counts a pipeline schema's SELECT grant, and verify says a missing pipeline table is created by the pipeline's first update.
The reader is a snapshot of feat/databricks-dbt-dq-tests, which has moved on (fce21b0): a reference to a product consumes[] does not list is no longer refused, its expose_id is None. The pipeline's reference resolver already takes that case (the product's one Databricks table with the column); a test now pins it. Whichever branch merges second finds the same file.
Found at the rebase onto main: data quality monitoring (#864) turns on for a schema whose table has a freshness or completeness SLO or rule, and creates the monitor after the schema's databricks_sql_table. A pipeline's table has none (the pipeline creates it at its first update), so a lakeflow build with a completeness rule failed tofu validate with "Reference to undeclared resource". A pipeline's table is left out of monitoring. Its rules are the pipeline's expectations, so they say nothing; a freshness or completeness SLO on it is a fluid validate warning, which contract.dq.monitoring.enabled: false silences. Whether the schema-level scan covers a materialized view is not verified yet (Databricks' docs name only views and foreign tables as unsupported). The CHANGELOG also says now that a pipeline's keys are not declared.
…output
Security review (LOW after filtering, fixed anyway): a critical rule is
ON VIOLATION FAIL UPDATE, and Databricks' EXPECTATION_VIOLATION error
quotes the record that failed it ("Input data: '{...}'. Output record:
'{...}'"). fluid apply printed that error, so a row of the table, or of
what its query read, reached the terminal and the CI log, which more
people read than the table (and a pipeline's table takes no masks yet).
Each printed error is now cut where it starts quoting a record, and says
that the update's event log on the pipeline's page shows it. The flow
and the violated expectation's name stay, and the per-rule counts are
printed as before.
Found by the fix round's re-check: _tags_on read the first page of the entity-tag-assignments list only, while fluid verify follows next_page_token. A declared tag on a later page was set again, and a stale one there was never removed. It now reads every page the same way verify does, and a token seen twice (or more than 100 pages) fails the build as an unreadable tag list. The test stand-in pages its list and no longer serves the tag record the review round removed.
…mbing ripwire --quality-delta flagged the Lakeflow run path as a twin of the dbt job runner, which #861 has since landed on main: the state scan (191 tokens), the build selection, the Ctrl-C/SIGTERM guard and the two "no run was started" error paths. They are one implementation now, in _databricks_job_run.py: selected_builds, tagged_in_state (with a ref maker per resource type), single_ref, client_or_none and interrupt_guard. Both runners print what they printed before; the job runner's tests pass unchanged. What stays alike is the shape of run_job_build and run_pipeline_build (the signature and the first four lines): the flows after it differ (a job run, or a pipeline compiled, updated or only tagged).
…enie place yet Found at the rebase onto #865 and #869: fluid apply creates a metric view (semantics) and sets the Genie space up right after tofu apply, and a Lakeflow pipeline creates its table at its first update, in the build phase after that. CREATE VIEW over the missing table would fail the first apply of a lakeflow product with semantics. What is set up at or right after tofu apply now reads the tables that exist by then (_tables_before_build): the data quality monitors, the metric views and the Genie space leave a pipeline's table out, and fluid validate warns about its semantics and its place in the space. A product whose tables are all a pipeline's gets no space and no refusal (a space with no table is refused otherwise). Setting them up after the pipeline's first update is a follow-up.
The dbt-tests PR (#871, head c724ed7) reads a valid_values rule through quality_engine.value_domain, as fluid test does, drops parse_enum and parse_range, and adds DqRefusal.needs_history. The pipeline emitter only uses read_rules and DqCheck, whose shape is unchanged; its tests pass on this copy. Whichever PR merges second finds the same file.
…es stays Found by the re-check of the previous commit: for a product whose tables are all a Lakeflow pipeline's, _genie returns no space, and databricks_genie.converge reads a missing space as the block removed. It then trashed the space an earlier apply had made (a product converted to lakeflow) and said extensions.databricksGenie no longer asked for it, which was not so. converge_output_ports now leaves such a space as it is and says so; converge is not called. The condition is one helper, _genie_waits_for_pipelines, which _genie uses too.
…till checked Found by the re-check of the previous commit (LOW, fixed in the same pass): for a product whose tables are all a Lakeflow pipeline's, _genie returned no space before compile_space ran, so a malformed extensions.databricksGenie block (an unknown key, say) passed fluid validate until a plain table joined the product. compile_space now runs over all the tables to collect its refusals; the space itself is still none.
📄 Documentation ReminderThis PR appears to be missing a documentation reference. Our docs live in a separate repo. Please update the PR description with one of:
See the Contributing Guide for details. |
…lakeflow-pipeline
1 task done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Databricks product can now be built as a Lakeflow Declarative Pipeline (formerly Delta Live Tables), with the contract's data-quality rules as the pipeline's expectations. The pipeline graph's Data quality tab then shows each rule's passed and failed records, and
fluid apply --mode amend-and-buildprints the same counts.What changed
engine: lakeflowjoins the 0.7.7 preview schema (stable 0.7.6 is unchanged; a 0.7.6 contract that uses it is told to move to 0.7.7). The build ispattern: embedded-logicwithproperties.sql, the SELECT of the dataset; upstreams are named by their Unity Catalog names.iac/providers/databricks_lakeflow.py): one serverlessdatabricks_pipelineper build, publishing into the expose's catalog and schema, with its source as a workspace file under the deploying identity's home (beside a dbt job's upload). forge-cli writes the source:CREATE OR REFRESH MATERIALIZED VIEW <table>with the expose's columns (types,NOT NULL, comments) and oneCONSTRAINT <rule id> EXPECT (...)per row-level rule, then the contract's query, last in the file. A materialized view, not a streaming table: the query reads tables that a dbtCREATE OR REPLACErewrites.util/dq_checks.py::read_rules(feat(databricks): a dbt job runs its contract's DQ rules as dbt tests #871), as the dbt tests read them:col IS NOT NULL; valid_values → the column'svalidationRules(IN,RLIKE, range comparisons), NULL passing;labels.references: <product id>.<column>need the whole table: onePRIVATEmaterialized view per check in the same pipeline (never published), whose one expectation is named after the rule. A reference resolves the referenced product's table through the workspace (fluid.workspace.yaml,FLUID_UPSTREAM_CONTRACTS), as an embedded-SQL build resolvesconsumes[], whether or notconsumes[]lists that product;EXPECT(row kept, counted); error →ON VIOLATION DROP ROW(row never published, counted as dropped); critical →ON VIOLATION FAIL UPDATE(update fails, the table's write rolls back). No quarantine table: the rejected rows would sit outside the contract's access policy. A whole-table check has no row of the product to drop, soerroronly counts there; acriticalone fails the update after the table was written (Lakeflow runs a check view after the table it reads). Uniqueness is checked by a private view:SELECT <key>, count(*) ... GROUP BY <key>withEXPECT (_fluid_rows = 1), NULL keys left out asfluid testand dbt'suniqueleave them out.databricks_sql_table, its comments are the definition's, its readers getSELECTon the product's schema (exists before the first update; a precondition fails the plan if the schema holds any other table or view), and its table and column tags are set after each update forge-cli runs. A tag the contract no longer declares is removed only when the deploying identity set it (the tag'supdated_by), so a steward's tag and an inherited one stay. Refused for now: masks, row filters, write grants, partitions,lifecycle.expire, another writer, a schema shared with another table. Not set up on a pipeline's table yet, each with afluid validatewarning: Unity Catalog keys (a materialized view refusesALTER TABLE ADD CONSTRAINT), data quality monitoring (feat(databricks): data quality monitoring from a contract's freshness and completeness SLOs #864), its metric view (feat(databricks): Unity Catalog metric views from exposes[].semantics #865) and its place in the Genie space (feat(databricks): a Genie space for a data product (extensions.databricksGenie) #863, feat(databricks): one Genie space across deploying identities (parentPath); plan, diff and generate iac show it #869). The last three are set up at or right aftertofu apply, and the pipeline creates its table at its first update, after that; a product whose tables are all a pipeline's gets no Genie space and no refusal.fluid apply --mode amend-and-buildstarts an update of the pipeline this apply's OpenTofu state holds (by itsfluid_product/fluid_buildtags, as feat(databricks): amend-and-build runs the dbt job now and waits for it #861 finds a dbt job), waits, prints each rule's counts from the update's event log, and fails on a failed update with Lakeflow's error, less the row a failed expectation quotes (replace-and-build: a full refresh).amendandcreate-onlyonly provision; a continuous pipeline is not started, but its tags are set once its table exists. A triggered pipeline setspipelines.numUpdateRetryAttemptsto 0 (found live: Lakeflow retried a failed update by itself and kept spending compute after the apply had failed).fluid verifynow compiles a Databricks expose from its contract file, as apply does (found live: verify expected 1 tag on a pipeline table that carries 10).build_runners/_databricks_job_run.py, used by both runners (ripwire flagged the twins; the job runner prints what it printed before and its tests pass unchanged).Industry pack
Under the industry-repo rule: Agenticstiger/industry-models#4 (merged,
8d82512b) made the pack's rules readable as written: the 57 enum constraints wereA | B | C(forge-cli reads comma lists, as FLUID's own example writes them), and dp009'ssmart_meters_nonnegative_energynamed five columns with no allowed values (nowrange >= 0validationRules). The 24*_refrules stay as written: their columns carrylabels.references. (A sibling session fixed the pack's generator to write the same form in industry-models#6.)Borrow-before-build
engines/dbt/_test_mapping.pyandproviders/quality_engine.py(rule readers),databricks_jobs.py(workspace upload and run seam),build_runners/_embedded_sql_io.py(consumes resolution),build_runners/dlt/(dlthub ingestion, not Lakeflow: nothing to reuse). The shared readerutil/dq_checks.pycomes from the dbt-tests session; this branch uses it.databricks_pipelinedocs, the event-log docs. No project emits Lakeflow expectations from a data contract.Validation
ruff check fluid_build/ tests/, the dead-code gate,black==24.10.0 --check, the product guardrail (self-test, then the run), license headers,lint-imports(6 + 1 contracts kept),detect-secrets-hook1.5.0 on the changed files (clean after a line-number refresh of the baseline), andscripts/mypy_strict.py.tofu validatesuites: 1651 passed, 24 skipped on the head (one stand-in-workspace test lost its connection to the stand-in server under load and passes alone). The fake-workspace e2e files pass 59/59 at-n 2; two of them flaked under-n 6load and pass alone.-n 6 --dist loadscope --ignore=tests/perf, tofu off PATH, pytest-randomly seed 20261112), thentests/perfserially: 35753 passed on the head (35b9490). The 9 failures and 2 collection errors are the shared venv's known baselines (lazy-core ×2, dlt dotenv ×4, the twotests/test_ci_*ImportErrors), two that fail the same way on pristine main (a test that needsdbton PATH, and the v0.24.0 compat gate, which CI skips), and one load flake (a Snowflake MCP test whose local stand-in refused a connection; it passes alone 3/3).tests/perf: 41 passed.--quality-delta(the branch diff applied to a fresh origin/main worktree). What stays:run_builds_from_argscomplexity 110 → 114: the lakeflow branch shares the dbt job branch.verify_databricks_expose16 → 19: the pipeline's "created by its first update" wording.run_job_build/run_pipeline_build, and rows inside feat(databricks): a dbt job runs its contract's DQ rules as dbt tests #871'sdq_checks.py.fluid_iactest_ldp, dropped after each run; nothing left). The 2026-11-12 row in HONESTLY_TESTED has the first run: counts per rule as promised, tags,verify, a critical failure, and the no-retry setting. 2026-11-13, rebased on feat(databricks): data quality monitoring from a contract's freshness and completeness SLOs #864 and feat(test): Databricks live checks, a Unity Catalog results table and an AI/BI dashboard #867:verifypassed all 10 tags.site_id'ssensitivitywas removed from the contract, the second apply (plan +0 ~0 -0) removed the column's tag, which the deploying identity had set, and kept the 8 table tags.updated_bywas the SCIMuserNamethe build phase compares it with.Reviews
One gate round; each review's findings were fixed and re-checked.
criticalexpectation's error quotes the row that broke it ("Input data: '{...}'. Output record: '{...}'"), andfluid applyprinted it to the terminal and CI log. Filtered to LOW, 5/10 (a contract author controls the query and its grants anyway, and masked tables are refused), below the report's cutoff; fixed anyway (ca082e7f). A follow-up pass over the commits after it found nothing. An earlier pass, before the rebase, found two issues below the cutoff, both fixed: a query ending in an unclosed/*swallowed forge-written text up to a*/in a rule description, so the rest ran as a second statement (the query now comes last in the file, and an open comment, string or backtick name is refused, lexed as Spark lexes it); readers' schema-levelSELECTcovered other teams' tables in an adopted schema (a precondition now fails the plan unless the schema holds only the pipeline's table).KeyError(now refused as another writer); the tag sync read a column the table no longer had and failed a completed update;fluid verifytold a pipeline's table to re-run plainfluid applyfor its tags; the data-loss gate's count of a pipeline's removal had no test.TBLPROPERTIES, and a refresh of a changed definition dropped that record, so a label the contract stopped declaring stayed on the table forever (the record is gone; removal goes by the tag'supdated_by, live-checked). Seven Low, fixed (a continuous pipeline's tags never set; Ctrl-C during the start request left an update running; raw strings lexed with escapes; malformed event payloads raised; a rule's own severity ignored; failing warn rules marked as errors; the precondition missing views; verify's wording for a missing pipeline table; the USE CATALOG notice missing a pipeline schema's grant). The re-check (one auditor on the findings and the fix diff) found both Medium fixed and no new defect, plus one Low, fixed: the tag sync read only the first page of a tag list.tofu validatefailed for a lakeflow build with a completeness rule (the data quality monitor named adatabricks_sql_tablethat a pipeline's table does not have); fixed with a test that fails without it.Notes for the merge
databricks_sql_tablethat a pipeline's table does not have (tofu validate: "Reference to undeclared resource"), and feat(databricks): Unity Catalog metric views from exposes[].semantics #865's metric views and the Genie space are set up before a pipeline creates its table. Both fixed here, with tests that fail without the fix.fluid_build/util/dq_checks.pycame with feat(databricks): a dbt job runs its contract's DQ rules as dbt tests #871: this PR no longer changes it (its earlier commits here were snapshots of that branch).providers/quality_engine.py(feat(test): Databricks live checks, a Unity Catalog results table and an AI/BI dashboard #867,fluid test) andutil/dq_checks.pynow read avalid_valuesrule the same way (feat(databricks): a dbt job runs its contract's DQ rules as dbt tests #871 reads it throughquality_engine.value_domain).Follow-ups (not in this PR)
PRIMARY KEY/FOREIGN KEYconstraints there), and masks and row filters in the definition._fake_databricks.py) tier for pipelines; a cron trigger; quarantine as an opt-in.fluid genie apply(feat(databricks): fluid genie apply, one Genie space over several data products #870) puts a pipeline's table in an estate space (it runs after the products are built), but shares the space (CAN_RUN) only with principals holding a table grant, and a pipeline's readers holdSELECTon its schema: a reader of only a pipeline's table is not shared the estate space yet.CAN_RUNshares until a later apply converges it: metadata only, since Genie runs questions on the asker's own grants. A converted table's old metric view is not dropped.