From 0befa55e942241e1a0622349807616e527c38037 Mon Sep 17 00:00:00 2001 From: Speculator55005 <50082482+fas89@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:08:27 +0100 Subject: [PATCH] docs(databricks): Unity Catalog comments, product tags and certification in 0.28.0 - providers/databricks.md: table and column comments from the contract (a dbt-written table keeps dbt's), the product's identity tags and the fluid_sensitivity column tags, certification through system.certification_status (certified from the label, deprecated from lifecycle.state), the limits and refusal codes, and the #835 changes: the landing's clean-up, the dry-run's superseded-function lines and the sovereignty reason. Live claims follow HONESTLY_TESTED.md's 2026-11-04 run; an identity holding only ASSIGN was not tried. - advanced/error-codes.md: the databricks-* kinds behind unsupported_binding, with what triggers each and the fix (three new in 0.28.0, two older with new triggers). --- docs/advanced/error-codes.md | 21 ++++++ docs/providers/databricks.md | 129 ++++++++++++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 8 deletions(-) diff --git a/docs/advanced/error-codes.md b/docs/advanced/error-codes.md index 67b910d..38a78a0 100644 --- a/docs/advanced/error-codes.md +++ b/docs/advanced/error-codes.md @@ -331,6 +331,27 @@ The state refusals (`state_shared_with_another_provider`, `state_migration_ambig - Pass the generated-artifacts path produced by 'fluid generate artifacts' +### unsupported_binding, Databricks kinds + +`ERR_UNSUPPORTED_BINDING`. Not catalogued, so it prints no curated suggestions: the raise site prints a `kind:` line, the finding and a `remediation:` line, and `fluid validate` prints the same finding as a validation error. A kind names the refusal. Nothing is applied and no module is written. `databricks-tag-value-too-long`, `databricks-comment-unsafe` and `databricks-certification-invalid` came with the Unity Catalog comments, tags and certification *(since 0.28.0, [forge-cli #831](https://github.com/Agenticstiger/forge-cli/pull/831))*. `databricks-tag-collision` and `databricks-tags-too-many` are older (0.27.0) and gained triggers in 0.28.0: a label that meets one of the product's own tag keys, and more than 1,000 column tags. All are raised by `fluid validate`, `fluid generate iac` and `fluid apply`. Each limit is the one Unity Catalog enforces or the provider's escaping needs. + +```text +❌ unsupported_binding [ERR_UNSUPPORTED_BINDING] + kind: databricks-certification-invalid + error: exposes[0] (orders): the label certification is 'gold', and Unity Catalog's system.certification_status takes 'certified' or 'deprecated'. + remediation: ['Set labels.certification: certified, or remove it; a deprecated product says so with lifecycle.state: deprecated.'] +``` + +| Kind | Triggered by | Fix | +|---|---|---| +| `databricks-tag-collision` | Two labels that become one tag key once `-` is read as `_` (`cost-center` and `cost_center`), or a label that becomes one of the product's own tag keys *(since 0.28.0, [forge-cli #831](https://github.com/Agenticstiger/forge-cli/pull/831))*. Only a key the contract states counts: with no `metadata.productType`, a label `fluid_product_type` is no collision | Rename the label so its key differs | +| `databricks-tag-value-too-long` | A tag value over 256 characters: the contract's `id`, `name`, `domain`, `metadata.layer`, `metadata.productType`, `metadata.owner.team`, `fluidVersion` or the expose's `version`, or a column's `sensitivity` | Shorten it to 256 characters or fewer. forge-cli refuses rather than cuts, since a cut id names another product | +| `databricks-tags-too-many` | More than 50 tags on a table or schema, counting the product's own tags with the governance labels, or more than 1,000 column tags on a table *(since 0.28.0, [forge-cli #831](https://github.com/Agenticstiger/forge-cli/pull/831))* | Remove labels until 50 or fewer remain, or classify 1,000 columns or fewer | +| `databricks-comment-unsafe` | A table or column description that holds a backslash (the provider escapes only quotes in `COMMENT '...'`, and Databricks reads a backslash in a literal as an escape, so a trailing one, or one before a quote, would break the literal) or `${` (Spark may substitute a variable there) | Remove the backslash, or write `$ {` for `${` | +| `databricks-certification-invalid` | The label `certification` has any value but `certified` (case ignored), on a product that is not `deprecated`. Unity Catalog's `system.certification_status` takes `certified` or `deprecated` | Set `labels.certification: certified` or remove it. Mark a deprecated product with `lifecycle.state: deprecated` | + +The `fluid generate iac` and `fluid apply` runs print the refusal and exit before any resource is created. See [Databricks, Comments and product tags](../providers/databricks.md#comments-and-product-tags) and [Certification](../providers/databricks.md#certification). The kinds that came earlier (`databricks-region-invalid`, `databricks-region-conflict`, `databricks-row-filter-invalid` and the others) are described on [Databricks](../providers/databricks.md). + ## AI and copilot ### copilot_missing_llm_api_key diff --git a/docs/providers/databricks.md b/docs/providers/databricks.md index 7def8df..1f8c79b 100644 --- a/docs/providers/databricks.md +++ b/docs/providers/databricks.md @@ -22,6 +22,7 @@ Deploy data products to Databricks Unity Catalog (a schema, a managed Delta tabl - ✅ **Column masks, row filters, denied columns**: Unity Catalog SQL functions that `fluid apply` binds on the table (see [Column masks and row filters](#column-masks-and-row-filters)) - ✅ **Retention**: `lifecycle.expire` is accepted for a table rebuilt in full on every run, and `fluid verify` checks the table's Delta history against it (see [Retention](#retention)) - ✅ **Landing from a DuckDB acquisition**: the acquisition runner uploads Parquet to a Unity Catalog volume and loads the table with `INSERT OVERWRITE`, applying `policy.privacy.masking` before the data leaves the machine (see [Landing a DuckDB acquisition](#landing-a-duckdb-acquisition)) +- ✅ **Described in Unity Catalog**: table and column comments from the contract, the product's identity as table tags, a column's `sensitivity` as a column tag, and Unity Catalog's certification (see [Comments and product tags](#comments-and-product-tags) and [Certification](#certification)) *(since 0.28.0, [forge-cli #831](https://github.com/Agenticstiger/forge-cli/pull/831))* - ✅ **Scoped grants**: an `accessPolicy` grant with `resources` is placed only on the tables it names (see [Scoping a grant to an expose](#scoping-a-grant-to-an-expose)) - ❌ **Iceberg, `packaging`, and the governance fields that are not mapped**: `fluid validate` refuses them rather than apply something that is not enforced (see [What validate refuses](#what-validate-refuses)) @@ -213,6 +214,7 @@ What a contract says about where its data lives and who may read it, and what fo | --- | --- | --- | | `binding.location.region` with `sovereignty` | Held by the policy at `fluid validate`, `fluid apply` and `generate iac`, and checked against the workspace's metastore at plan time (see [Residency](#residency)) | `residency` | | `labels`, and the classification, jurisdiction, regulation and residency a contract carries | Unity Catalog tags on the schema and the table (see [Governance tags](#governance-tags)) | `governance_tags` | +| `description`, the product's identity, `sensitivity` and the certification label | Table and column comments, product tags on the table, column tags and `system.certification_status` *(since 0.28.0, [forge-cli #831](https://github.com/Agenticstiger/forge-cli/pull/831))* (see [Comments and product tags](#comments-and-product-tags) and [Certification](#certification)) | `governance_tags` | | `lifecycle.expire` with `lifecycle.retention` | Accepted for a table rebuilt in full on every run, when the retention covers the table's Delta history (see [Retention](#retention)) | `retention` | | `policy.authz.columnRestrictions` and `rowFilters` | SQL functions bound on the table as column masks and a row filter, by `fluid apply` only (see [Column masks and row filters](#column-masks-and-row-filters)) | `row_filter_masks` | @@ -245,6 +247,22 @@ The rules: - **A declared region is held to the policy** (`allowedRegions`, `deniedRegions`, `jurisdiction`) like any cloud region. - **With a `sovereignty` block, no region is a finding.** An error under `enforcementMode: strict`, a warning under `advisory`. Where the data lives cannot be checked without one. - **The policy holds at apply, not only at validate.** `fluid apply` and `fluid generate iac` refuse a placement the policy forbids with `SovereigntyViolationError`, before any module is written, so before `tofu init`, an import or an adoption. +- **The refusal says why.** *(since 0.28.0, [forge-cli #835](https://github.com/Agenticstiger/forge-cli/pull/835))* `fluid generate iac` used to turn the refusal into `generate_iac_failed` and print only the finding. It now prints the error's panel, with the reason, the fix and the doc link, as `fluid apply` does. Captured with `jurisdiction: EU`, `allowedRegions: [eu-central-1]` and a `binding.location.region` of `us-west-2` (terminal width 100): + + ```text +╭────────────────────────────────── ✗ SovereigntyViolationError ───────────────────────────────────╮ +│ Databricks placement refused by the sovereignty policy: orders: Region 'us-west-2' not in │ +│ allowed regions list; orders: Region 'us-west-2' (jurisdiction: US) does not match required │ +│ jurisdiction: EU │ +│ why contract.sovereignty is enforced (strict) and allows eu-central-1; the tables would be │ +│ created in the workspace DATABRICKS_HOST names. │ +│ fix Set binding.location.region on every databricks expose to the region of the workspace │ +│ DATABRICKS_HOST names, one the policy allows, or change the policy. │ +│ doc https://agenticstiger.github.io/forge_docs/concepts/sovereignty.html │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ + ``` + + A refusal whose message is too long for the panel's title (one that lists every finding) is printed whole inside the panel, not cut at the border. - **The spelling is exact.** The region must be a plain lower-case name: lower-case letters, digits and hyphens, starting with a letter, 2 to 63 characters, no spaces (`eu-central-1`, `westeurope`, `europe-west3`). Anything else is refused as `databricks-region-invalid`. It is not lower-cased for you: the policy compares the text as written, so `US-WEST-2` would otherwise pass a `deniedRegions: [us-west-2]` check and still plan against a workspace in us-west-2. - **One region per contract.** The module applies to one workspace (`DATABRICKS_HOST`). Two Databricks exposes that declare different regions are refused as `databricks-region-conflict`. - **The declaration is checked against the workspace, at plan time.** With a region declared, the module reads the workspace's metastore (`data.databricks_current_metastore`), and every schema it owns carries a precondition that the metastore's region equals the declared one. `tofu plan` fails, `--mode dry-run` included, before anything is written, when `DATABRICKS_HOST` names a workspace elsewhere. The run stops with `opentofu_plan_failed` and kind `databricks-residency-region`, and prints one line with both regions and the fix (the wording from [forge-cli #801](https://github.com/Agenticstiger/forge-cli/pull/801), since 0.27.0): @@ -278,13 +296,13 @@ The tags a contract carries are its and the expose's `labels`, plus `fluid_class Two rules shape the keys: - **A `-` in a key becomes `_`.** A Unity Catalog tag key may not contain `. , - = / :`, and a governance label key is lower-case letters, digits, `_` and `-`. `data-classification` is the tag `data_classification`; `fluid_jurisdiction` stays as it is. The value is not changed further. -- **Nothing is dropped, nothing is overwritten.** Two labels that become one key (`cost-center` and `cost_center`) are refused as `databricks-tag-collision`. More than the 50 tags a Unity Catalog securable holds is refused as `databricks-tags-too-many`. `fluid validate` reports both. +- **Nothing is dropped, nothing is overwritten.** Two labels that become one key (`cost-center` and `cost_center`) are refused as `databricks-tag-collision`. More than the 50 tags a Unity Catalog securable holds is refused as `databricks-tags-too-many`. `fluid validate` reports both. *(since 0.28.0, [forge-cli #831](https://github.com/Agenticstiger/forge-cli/pull/831))* On a table, the product's own tags (see [Comments and product tags](#comments-and-product-tags)) count toward the 50 with the governance labels, and a label that becomes one of their keys is also `databricks-tag-collision`. -A contract with no labels gets the module it always did. +A contract with no labels gets the module it always did. *(since 0.28.0, [forge-cli #831](https://github.com/Agenticstiger/forge-cli/pull/831))* Since 0.28.0 that is no longer so for a table: a contract with no labels still gets the product's own tags on it. An assignment that already exists is imported before the first apply, best-effort as grants are, so a run after lost state adopts it instead of failing. Removing a label destroys only its assignment. That holds no data, so it does not need `--allow-data-loss`: the apply prints it, as it does a revoked grant. -`fluid verify` reads the table's and its schema's tags into a `governance_tags` dimension. Every tag the contract implies must be there with its value. A missing or changed tag is `CRITICAL`, with `fluid apply` as the fix. A tag someone else set is ignored, and a contract with no labels skips the dimension. The tag list is paginated: verify follows every page, and a repeated page token or more than 100 pages is an error, not a pass. +`fluid verify` reads the table's and its schema's tags into a `governance_tags` dimension. Every tag the contract implies must be there with its value. A missing or changed tag is `CRITICAL`, with `fluid apply` as the fix. A tag someone else set is ignored, and a module that sets no tag and no comment skips the dimension. The tag list is paginated: verify follows every page, and a repeated page token or more than 100 pages is an error, not a pass. ::: warning Tags on a table a dbt build writes Databricks does not document whether a table keeps its tags across the `CREATE OR REPLACE` a dbt `table` materialization runs, and forge-cli has not proven it. If tags go missing after a build, `fluid apply` sets them again. `fluid verify` is the gate. @@ -292,6 +310,92 @@ Databricks does not document whether a table keeps its tags across the `CREATE O Not run against a live workspace. +### Comments and product tags + +*(since 0.28.0, [forge-cli #831](https://github.com/Agenticstiger/forge-cli/pull/831))* + +`fluid apply` describes the table in Unity Catalog from the contract, so it looks like a native one. + +**Comments.** + +- The table's comment is the expose's `description`, else the product's. A product `description` written as an object contributes its `purpose`. +- A column's comment is its `contract.schema[].description`. +- A description with a backslash is refused as `databricks-comment-unsafe`: the provider escapes only quotes when it writes `COMMENT '...'`, and Databricks reads a backslash in a literal as an escape, so a trailing one, or one before a quote, would break the literal. One with `${` is refused as `databricks-comment-unsafe` too, because Spark may replace it with a variable, setting or environment value before the comment is stored. + +**Product tags.** Each table carries these tags, built from the contract. A field the contract does not state makes no tag. Only `fluid_domain`, `fluid_layer`, `fluid_product_type` and `fluid_version` are keys AWS Glue and Snowflake already use; `fluid_product_id`, `fluid_product_name`, `fluid_product_version` and `fluid_owner_team` are new. + +| Tag | From | +| --- | --- | +| `fluid_product_id` | `id` | +| `fluid_product_name` | `name` | +| `fluid_domain` | `domain` | +| `fluid_layer` | `metadata.layer` | +| `fluid_product_type` | `metadata.productType` | +| `fluid_product_version` | the expose's `version` | +| `fluid_version` | `fluidVersion` | +| `fluid_owner_team` | `metadata.owner.team` | + +These count toward Unity Catalog's 50 tags per securable, together with the governance labels, the classification tags and the certification tag. A contract with no labels still carries these. A label that becomes one of these keys is refused as `databricks-tag-collision`, but only for a key the contract states: with no `metadata.productType`, a label `fluid_product_type` is no collision. A tag value longer than 256 characters is refused as `databricks-tag-value-too-long`, rather than cut, since a cut id or name would name another product. + +The table's owner is unchanged. The deploy identity keeps owning it, which the masks, the row filter and the fail-safe lock depend on. The owner team is only a tag. + +**Column tags.** A column the contract classifies with `sensitivity` carries it as the column tag `fluid_sensitivity`. A table holds at most 1,000 column tags, and more is refused as `databricks-tags-too-many`. A `sensitivity` over 256 characters is refused as `databricks-tag-value-too-long`. + +A contract with one classified column and two labels, validated and compiled offline (`fluid generate iac`), gave these table tags, with the column tag on `email`: + +```text +cost_center cc42 +fluid_domain sales +fluid_layer Gold +fluid_owner_team data-engineering +fluid_product_id sales.orders_dbx +fluid_product_name Orders +fluid_product_version 1.0.0 +fluid_version 0.7.5 +system.certification_status certified +``` + +Read back on a live workspace on 2026-11-04 (comments, product tags, column tags and `certified`, with an admin token). Not tried live: a non-admin identity holding only ASSIGN. + +#### Comments on a dbt-written table + +On a table a dbt build writes, the comments stay dbt's. Its `CREATE OR REPLACE` sets the table and column comments from the model (with `persist_docs`), so forge-cli sets none there. The product tags, the column tags and the certification are still set. `fluid validate` passes and, when the contract describes the table or a column, warns. Captured: + +```text +✅ Valid FLUID contract (schema v0.7.5) +⚠️ 1 warning(s) +Validation completed in 0.021s + +Validation Warnings: +==================== + 1. exposes[0] (orders) is written by dbt, whose CREATE OR REPLACE sets the table and column comments: Unity Catalog shows the dbt model's descriptions (with persist_docs), not the contract's, so fluid apply sets none. +``` + +`fluid verify` checks no comment on such a table. + +### Certification + +*(since 0.28.0, [forge-cli #831](https://github.com/Agenticstiger/forge-cli/pull/831))* + +Unity Catalog marks an asset as certified or deprecated with the system governed tag `system.certification_status`. forge-cli sets it on each Databricks table from the contract: + +| Value | When | +| --- | --- | +| `deprecated` | `lifecycle.state` is `deprecated`, on the expose, else on the product. | +| `certified` | The label `certification: certified` is on the expose, else on the product, and the state is not `deprecated`. | + +- **A deprecated product is never shown as certified**, whatever its label. +- **The label is not also a plain tag.** It becomes the certification tag and no `certification` tag. +- **Any other value is refused** as `databricks-certification-invalid` (compared in lower case, so `Certified` passes as `certified`), unless the state is `deprecated`, which wins before the label is read. Unity Catalog's tag takes only `certified` and `deprecated`. +- **Scope.** *(since 0.28.0, [forge-cli #831](https://github.com/Agenticstiger/forge-cli/pull/831))* A `labels.certification` on the product certifies every Databricks table of the product. One on an expose certifies only that expose's table. +- **ASSIGN.** This tag is a system governed tag, so the deploy identity needs the ASSIGN permission on it, besides `APPLY TAG` on the table. That comes from Databricks' documentation and the SDK's `EntityTagAssignmentsAPI.create`. It was read back live with an admin token (2026-11-04); a non-admin identity holding only ASSIGN was not tried. The apply engine prints a note after the `tofu plan` summary, indented by two spaces, on a dry run too: + +```text + note: system.certification_status is set on main.sales.orders (certified): a system governed tag, so this identity needs the ASSIGN permission on it, which an account or workspace admin grants, or the tag assignment fails. +``` + +The text is the plugin's `plan_notices` output for the example contract, captured offline; the engine adds the `note: ` prefix and the indent. + ### Column masks and row filters *(since 0.27.0, [forge-cli #785](https://github.com/Agenticstiger/forge-cli/pull/785))* @@ -347,7 +451,7 @@ The module `fluid generate iac` writes holds no mask or filter, and a plain `tof #### What the functions are -Each function is created once, as `CREATE FUNCTION IF NOT EXISTS`, under a name that carries a hash of its definition: `fluid_rf_