From e9482ff66983a68c2efdfb8f8f7918644a7121d9 Mon Sep 17 00:00:00 2001 From: orian Date: Fri, 4 Sep 2026 12:24:16 +0200 Subject: [PATCH] fix(hcl): support override for every managed object --- README.md | 12 ++- cmd/hclexp/load_override_e2e_test.go | 139 +++++++++++++++++++++++++ docs/FAQ.md | 9 +- docs/README.hcl.md | 22 ++-- internal/loader/hcl/dictionary_dump.go | 3 + internal/loader/hcl/dump.go | 12 +++ internal/loader/hcl/dump_test.go | 19 ++++ internal/loader/hcl/layers.go | 61 ++++++----- internal/loader/hcl/layers_test.go | 70 +++++++++++-- internal/loader/hcl/locate.go | 1 + internal/loader/hcl/locate_test.go | 34 ++++++ internal/loader/hcl/resolver.go | 22 +++- internal/loader/hcl/types.go | 9 +- 13 files changed, 362 insertions(+), 51 deletions(-) create mode 100644 cmd/hclexp/load_override_e2e_test.go diff --git a/README.md b/README.md index defe7a3..e555bb5 100644 --- a/README.md +++ b/README.md @@ -1522,9 +1522,10 @@ environment patches, but the desired schema is always reduced to one flat table by the same rule: Before resolving inheritance, hclexp merges the selected layers in their -declared order. A later `override = true` replaces the earlier declaration; -patch blocks are collected rather than applied during that merge. The resolver -then evaluates the table graph below. +declared order. A later `override = true` replaces the earlier declaration for +any managed object kind (`table`, `materialized_view`, `view`, `dictionary`, +`raw`, or `named_collection`); patch blocks are collected rather than applied +during that merge. The resolver then evaluates the table graph below. ```text resolved(table) = @@ -1665,8 +1666,9 @@ The resolver enforces several safety invariants around this model: one **replaces it wholesale** (a one-key `settings` map loses every inherited key). `primary_key`, `comment`, `cluster`, constraints, and projections never flow through `extend`. -- `override = true` — required for a later layer to replace a table or - materialized view that an earlier layer already defined +- `override = true` — required for a later layer to replace a table, + materialized view, view, dictionary, raw object, or named collection that an + earlier layer already defined Rule of thumb: `extend` is for *different tables sharing a shape* (`events_local` / `events_distributed`); `patch_table` (below) is for *the diff --git a/cmd/hclexp/load_override_e2e_test.go b/cmd/hclexp/load_override_e2e_test.go new file mode 100644 index 0000000..9b93e73 --- /dev/null +++ b/cmd/hclexp/load_override_e2e_test.go @@ -0,0 +1,139 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + hclload "github.com/posthog/chschema/internal/loader/hcl" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadOverrideCLIProcess(t *testing.T) { + if os.Getenv("HCLEXP_LOAD_OVERRIDE_HELPER") != "1" { + return + } + for i, arg := range os.Args { + if arg == "--" { + runLoad(os.Args[i+1:]) + return + } + } + t.Fatal("missing load CLI arguments") +} + +func TestLoadCLI_OverrideEveryManagedObjectKindEndToEnd(t *testing.T) { + root := t.TempDir() + base := writeLoadOverrideLayer(t, root, "base.hcl", ` +named_collection "warehouse" { + param "host" { value = "production.internal" } +} + +database "analytics" { + table "events" { + column "id" { type = "UInt64" } + engine "log" {} + } + + materialized_view "events_mv" { + to_table = "analytics.events" + query = "SELECT id FROM production_source" + column "id" { type = "UInt64" } + } + + view "environment" { + query = "SELECT 'production' AS name" + } + + dictionary "labels" { + primary_key = ["id"] + attribute "id" { type = "UInt64" } + attribute "label" { type = "String" } + source "null" {} + layout "flat" {} + } + + raw "view" "legacy_environment" { + sql = "CREATE VIEW analytics.legacy_environment AS SELECT 'production'" + } +}`) + override := writeLoadOverrideLayer(t, root, "override.hcl", ` +named_collection "warehouse" { + override = true + param "host" { value = "development.internal" } +} + +database "analytics" { + table "events" { + override = true + column "dev_id" { type = "UInt32" } + engine "log" {} + } + + materialized_view "events_mv" { + override = true + to_table = "analytics.events" + query = "SELECT dev_id FROM development_source" + column "dev_id" { type = "UInt32" } + } + + view "environment" { + override = true + query = "SELECT 'development' AS name" + } + + dictionary "labels" { + override = true + primary_key = ["key"] + attribute "key" { type = "String" } + attribute "label" { type = "String" } + source "null" {} + layout "flat" {} + } + + raw "view" "legacy_environment" { + override = true + sql = "CREATE VIEW analytics.legacy_environment AS SELECT 'development'" + } +}`) + out := filepath.Join(root, "resolved.hcl") + + cmd := exec.Command(os.Args[0], "-test.run=^TestLoadOverrideCLIProcess$", "--", + "-layer", base+","+override, + "-out", out, + ) + cmd.Env = append(os.Environ(), "HCLEXP_LOAD_OVERRIDE_HELPER=1") + output, err := cmd.CombinedOutput() + require.NoError(t, err, string(output)) + + schema, err := hclload.ParseFile(out) + require.NoError(t, err) + require.NoError(t, hclload.Resolve(schema)) + require.Len(t, schema.NamedCollections, 1) + require.Len(t, schema.Databases, 1) + db := schema.Databases[0] + require.Len(t, db.Tables, 1) + require.Len(t, db.MaterializedViews, 1) + require.Len(t, db.Views, 1) + require.Len(t, db.Dictionaries, 1) + require.Len(t, db.Raws, 1) + assert.Equal(t, "development.internal", schema.NamedCollections[0].Params[0].Value) + assert.Equal(t, "dev_id", db.Tables[0].Columns[0].Name) + assert.Contains(t, db.MaterializedViews[0].Query, "development_source") + assert.Contains(t, db.Views[0].Query, "development") + assert.Equal(t, []string{"key"}, db.Dictionaries[0].PrimaryKey) + assert.Contains(t, db.Raws[0].SQL, "development") + + resolved, err := os.ReadFile(out) + require.NoError(t, err) + assert.NotContains(t, string(resolved), "override = true", "layer control metadata must not leak into resolved output") +} + +func writeLoadOverrideLayer(t *testing.T, root, name, body string) string { + t.Helper() + path := filepath.Join(root, name) + require.NoError(t, os.WriteFile(path, []byte(body), 0o600)) + return path +} diff --git a/docs/FAQ.md b/docs/FAQ.md index 2cf3911..55af33a 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -239,9 +239,11 @@ it, put a `patch_table` in each. If the column is present in every environment, just add it to the base `table`. Don't try to be clever — the local-knowledge wins over global indirection. -## How do I replace a table entirely in one environment? +## How do I replace an object entirely in one environment? -Declare the table in the environment layer with `override = true`. +Redeclare it in the environment layer with `override = true`. The same rule +applies to tables, materialized views, views, dictionaries, raw objects, and +named collections. ```hcl # envs/dev/events.hcl @@ -255,7 +257,8 @@ database "posthog" { ``` Without `override = true`, the cross-layer collision is an error. With it, -the dev-layer definition wins. +the dev-layer definition wins. Raw objects collide by `(kind, name)`; the +other database objects and named collections collide by name. ## What's the difference between `extend` and `patch_table`? diff --git a/docs/README.hcl.md b/docs/README.hcl.md index 8035390..1f00759 100644 --- a/docs/README.hcl.md +++ b/docs/README.hcl.md @@ -98,8 +98,10 @@ below. `patch_column` is valid only when the table sets `extend`. - `abstract = true` — declares this table as inheritable-only; it is not emitted as a real ClickHouse table. - `override = true` — declares that this block replaces an earlier-layer - `table` of the same name. Without it, a cross-layer name collision is an - error. + object of the same identity. It is supported uniformly by `table`, + `materialized_view`, `view`, `dictionary`, `raw`, and `named_collection`; + without it, a cross-layer collision is an error. A raw object's identity is + its `(kind, name)` label pair. ## `column` @@ -636,9 +638,10 @@ A later layer may fully replace an MV by redeclaring it with When the loader processes a layered set, this pipeline runs: 1. **Parse** every `.hcl` file in every layer (ordered). -2. **Merge** databases by name. Tables and materialized views collide on name - unless the later declaration sets `override = true`. Patch blocks - accumulate. +2. **Merge** databases by name. Every managed object (`table`, + `materialized_view`, `view`, `dictionary`, `raw`, and `named_collection`) + collides on identity unless the later declaration sets `override = true`. + Patch blocks accumulate. 3. **Apply view/dictionary patches.** Table patches remain attached to their named targets while the inheritance graph resolves. 4. **Resolve each table parent-first**: inherit the resolved parent, apply @@ -661,8 +664,9 @@ which question you are asking: - **`patch_table`**: *"this is the **same table**, and one layer wants to adjust it."* The table stays declared exactly **once**; the env layer contributes a modification, not a declaration. -- **`override = true`**: *"in this environment the table is genuinely - different."* A full replacement declaration, sanctioned by the flag. +- **`override = true`**: *"in this environment the object is genuinely + different."* A full replacement declaration, sanctioned by the flag. It + follows the same rule for every managed object kind. The declaration count is not cosmetic: `hclexp locate -duplicates` (the once-only CI guard) counts definition sites, while treating patch, @@ -690,6 +694,8 @@ existing table, which stays authoritative except where patched. | A table differing beyond the patchable fields (`primary_key`, constraints, …) | `override = true` | | Replace a table entirely in one environment | `override = true` | | Replace a materialized view entirely in one environment | `override = true` | +| Replace a view or dictionary entirely in one environment | `override = true` | +| Replace a raw object or named collection entirely in one environment | `override = true` | ## Dependency validation — `hclexp validate` @@ -1334,6 +1340,7 @@ database "posthog" { | Attribute | Required | Meaning | |------------------|----------|---------| +| `override` | no | Set `true` to replace the same view from an earlier layer. | | `query` | yes | the `AS SELECT ...` body (verbatim text) | | `column_aliases` | no | `CREATE VIEW v (a, b, ...) AS ...` | | `sql_security` | no | `SQL SECURITY` clause: `definer`, `invoker`, or `none` (canonical lowercase; case-insensitive on parse) | @@ -1375,6 +1382,7 @@ SQL |--------|---------| | `kind` (1st label) | `table`, `materialized_view`, `view`, or `dictionary`. Drives the `DROP` form on a recreate. | | `name` (2nd label) | The object name. | +| `override` | Set `true` to replace an earlier raw block with the same `(kind, name)`. | | `sql` | The original `CREATE` statement, emitted verbatim on apply. | **Semantics.** Raw objects are opaque: diff --git a/internal/loader/hcl/dictionary_dump.go b/internal/loader/hcl/dictionary_dump.go index 083b3cc..413a8ba 100644 --- a/internal/loader/hcl/dictionary_dump.go +++ b/internal/loader/hcl/dictionary_dump.go @@ -8,6 +8,9 @@ import ( // writeDictionary emits a `dictionary` block body. Sub-blocks (lifetime, // range, attribute, source, layout) are emitted in a consistent order. func writeDictionary(body *hclwrite.Body, d DictionarySpec) { + if d.Override { + body.SetAttributeValue("override", cty.True) + } body.SetAttributeValue("primary_key", stringList(d.PrimaryKey)) if len(d.Settings) > 0 { body.SetAttributeValue("settings", stringMap(d.Settings)) diff --git a/internal/loader/hcl/dump.go b/internal/loader/hcl/dump.go index edd1b1b..151e419 100644 --- a/internal/loader/hcl/dump.go +++ b/internal/loader/hcl/dump.go @@ -299,6 +299,9 @@ func writeDatabase(body *hclwrite.Body, db DatabaseSpec) { // as a quoted string otherwise. Heredoc emission is exact: the re-parsed value // equals the stored SQL. func writeRaw(body *hclwrite.Body, r RawSpec) { + if r.Override { + body.SetAttributeValue("override", cty.True) + } setSQLAttribute(body, "sql", r.SQL) } @@ -333,6 +336,9 @@ func setQueryAttribute(body *hclwrite.Body, query string) { } func writeView(body *hclwrite.Body, v ViewSpec) { + if v.Override { + body.SetAttributeValue("override", cty.True) + } setQueryAttribute(body, v.Query) if len(v.ColumnAliases) > 0 { body.SetAttributeValue("column_aliases", stringList(v.ColumnAliases)) @@ -352,6 +358,9 @@ func writeView(body *hclwrite.Body, v ViewSpec) { } func writeMaterializedView(body *hclwrite.Body, mv MaterializedViewSpec) { + if mv.Override { + body.SetAttributeValue("override", cty.True) + } body.SetAttributeValue("to_table", cty.StringVal(mv.ToTable)) setQueryAttribute(body, mv.Query) if mv.Cluster != nil { @@ -366,6 +375,9 @@ func writeMaterializedView(body *hclwrite.Body, mv MaterializedViewSpec) { } func writeTable(body *hclwrite.Body, t TableSpec) { + if t.Override { + body.SetAttributeValue("override", cty.True) + } if t.Comment != nil { body.SetAttributeValue("comment", cty.StringVal(*t.Comment)) } diff --git a/internal/loader/hcl/dump_test.go b/internal/loader/hcl/dump_test.go index d23148b..1c40b55 100644 --- a/internal/loader/hcl/dump_test.go +++ b/internal/loader/hcl/dump_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "sort" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -178,6 +179,24 @@ func TestDumpSchema_RendersNodeBlock(t *testing.T) { assert.Equal(t, want, reparsed.Nodes[0]) } +func TestWriteLayerPreservesOverrideForEveryManagedObjectKind(t *testing.T) { + schema := &Schema{ + NamedCollections: []NamedCollectionSpec{{Name: "credentials", Override: true}}, + Databases: []DatabaseSpec{{ + Name: "posthog", + Tables: []TableSpec{{Name: "events", Override: true}}, + MaterializedViews: []MaterializedViewSpec{{Name: "events_mv", Override: true}}, + Views: []ViewSpec{{Name: "events_view", Override: true, Query: "SELECT 1"}}, + Dictionaries: []DictionarySpec{{Name: "geo", Override: true}}, + Raws: []RawSpec{{Kind: "view", Name: "legacy", Override: true, SQL: "CREATE VIEW posthog.legacy AS SELECT 1\n"}}, + }}, + } + + var buf bytes.Buffer + require.NoError(t, WriteLayer(&buf, schema)) + assert.Equal(t, 6, strings.Count(buf.String(), "= true"), buf.String()) +} + func TestDumpSchema_RendersViewBlocks(t *testing.T) { want := ViewSpec{ Name: "metrics", diff --git a/internal/loader/hcl/layers.go b/internal/loader/hcl/layers.go index 8cc46a9..a49b8ca 100644 --- a/internal/loader/hcl/layers.go +++ b/internal/loader/hcl/layers.go @@ -10,9 +10,9 @@ import ( // LoadLayers parses the .hcl files each layer path contributes, in the given // order, and merges them into a combined raw spec set. A layer path is either // a directory (every *.hcl inside it, in lexical filename order) or a single -// .hcl file. Across layers (and across files), a duplicate table or -// materialized-view name is an error unless the later declaration sets -// override = true. Patch blocks always accumulate. +// .hcl file. Across layers (and across files), duplicate managed-object names +// are errors unless the later declaration sets override = true. Raw objects +// are keyed by (kind, name). Patch blocks always accumulate. // // LoadLayers does NOT call Resolve; callers run that explicitly so they can // inspect the merged-but-unresolved input first. @@ -151,43 +151,56 @@ func mergeIntoDatabase(target *DatabaseSpec, incoming DatabaseSpec) error { } } - viewByName := make(map[string]bool, len(target.Views)) - for _, v := range target.Views { - viewByName[v.Name] = true + viewByName := make(map[string]int, len(target.Views)) + for i := range target.Views { + viewByName[target.Views[i].Name] = i } for _, v := range incoming.Views { - if viewByName[v.Name] { - return fmt.Errorf("view %q redeclared across layers", v.Name) + if idx, ok := viewByName[v.Name]; ok { + if !v.Override { + return fmt.Errorf("view %q redeclared without override = true", v.Name) + } + target.Views[idx] = v + } else { + target.Views = append(target.Views, v) + viewByName[v.Name] = len(target.Views) - 1 } - viewByName[v.Name] = true - target.Views = append(target.Views, v) } - dictByName := make(map[string]bool, len(target.Dictionaries)) - for _, d := range target.Dictionaries { - dictByName[d.Name] = true + dictByName := make(map[string]int, len(target.Dictionaries)) + for i := range target.Dictionaries { + dictByName[target.Dictionaries[i].Name] = i } for _, d := range incoming.Dictionaries { - if dictByName[d.Name] { - return fmt.Errorf("dictionary %q redeclared across layers", d.Name) + if idx, ok := dictByName[d.Name]; ok { + if !d.Override { + return fmt.Errorf("dictionary %q redeclared without override = true", d.Name) + } + target.Dictionaries[idx] = d + } else { + target.Dictionaries = append(target.Dictionaries, d) + dictByName[d.Name] = len(target.Dictionaries) - 1 } - dictByName[d.Name] = true - target.Dictionaries = append(target.Dictionaries, d) } // A raw object's identity is (kind, name), consistent with indexRaws in // diff.go — two kinds may legitimately share a name. rawKey := func(r RawSpec) string { return r.Kind + "\x00" + r.Name } - rawSeen := make(map[string]bool, len(target.Raws)) - for _, r := range target.Raws { - rawSeen[rawKey(r)] = true + rawByKey := make(map[string]int, len(target.Raws)) + for i := range target.Raws { + rawByKey[rawKey(target.Raws[i])] = i } for _, r := range incoming.Raws { - if rawSeen[rawKey(r)] { - return fmt.Errorf("raw %q (%s) redeclared across layers", r.Name, r.Kind) + key := rawKey(r) + if idx, ok := rawByKey[key]; ok { + if !r.Override { + return fmt.Errorf("raw %q (%s) redeclared without override = true", r.Name, r.Kind) + } + target.Raws[idx] = r + } else { + target.Raws = append(target.Raws, r) + rawByKey[key] = len(target.Raws) - 1 } - rawSeen[rawKey(r)] = true - target.Raws = append(target.Raws, r) } return nil } diff --git a/internal/loader/hcl/layers_test.go b/internal/loader/hcl/layers_test.go index d968a66..ab5540d 100644 --- a/internal/loader/hcl/layers_test.go +++ b/internal/loader/hcl/layers_test.go @@ -116,7 +116,65 @@ func TestLoadLayers_ViewRedeclareAcrossLayers(t *testing.T) { layerPath("view_redeclare", "env_dev"), }) require.Error(t, err) - assert.Contains(t, err.Error(), `view "v" redeclared across layers`) + assert.Contains(t, err.Error(), `view "v" redeclared without override = true`) +} + +func TestLoadLayers_OverrideViewDictionaryAndRawAcrossLayers(t *testing.T) { + root := t.TempDir() + base := writePatchLayer(t, root, "base/schema.hcl", ` +database "posthog" { + view "environment" { + query = "SELECT 'production' AS name" + } + + dictionary "labels" { + primary_key = ["id"] + attribute "id" { type = "UInt64" } + attribute "label" { type = "String" } + source "null" {} + layout "flat" {} + } + + raw "view" "legacy_environment" { + sql = "CREATE VIEW posthog.legacy_environment AS SELECT 'production'" + } +}`) + override := writePatchLayer(t, root, "dev/schema.hcl", ` +database "posthog" { + view "environment" { + override = true + query = "SELECT 'development' AS name" + } + + dictionary "labels" { + override = true + primary_key = ["key"] + attribute "key" { type = "String" } + attribute "label" { type = "String" } + source "null" {} + layout "flat" {} + } + + raw "view" "legacy_environment" { + override = true + sql = "CREATE VIEW posthog.legacy_environment AS SELECT 'development'" + } +}`) + + schema, err := LoadLayers([]string{base, override}) + require.NoError(t, err) + require.NoError(t, Resolve(schema)) + require.Len(t, schema.Databases, 1) + db := schema.Databases[0] + require.Len(t, db.Views, 1) + require.Len(t, db.Dictionaries, 1) + require.Len(t, db.Raws, 1) + assert.Contains(t, db.Views[0].Query, "development") + assert.Equal(t, []string{"key"}, db.Dictionaries[0].PrimaryKey) + assert.Contains(t, db.Raws[0].SQL, "development") + assert.False(t, db.Views[0].Override) + assert.False(t, db.Dictionaries[0].Override) + assert.False(t, db.Raws[0].Override) } func TestLoadLayers_PatchPropagatesThroughExtend(t *testing.T) { @@ -377,7 +435,7 @@ func TestLoadLayers_DictionaryLaterLayer(t *testing.T) { } // TestLoadLayers_DictionaryRedeclareAcrossLayers asserts the redeclare guard: -// the same dictionary name in two layers is an error (mirrors views/MVs). +// the same dictionary name requires an explicit override. func TestLoadLayers_DictionaryRedeclareAcrossLayers(t *testing.T) { const dictLayer = `database "posthog" { dictionary "d" { @@ -395,11 +453,11 @@ func TestLoadLayers_DictionaryRedeclareAcrossLayers(t *testing.T) { _, err := LoadLayers([]string{a, b}) require.Error(t, err) - assert.Contains(t, err.Error(), `dictionary "d" redeclared across layers`) + assert.Contains(t, err.Error(), `dictionary "d" redeclared without override = true`) } -// TestLoadLayers_RawRedeclareAcrossLayers asserts the redeclare guard for raw -// blocks, keyed by (kind, name) like the diff engine. +// TestLoadLayers_RawRedeclareAcrossLayers asserts the explicit-override guard +// for raw blocks, keyed by (kind, name) like the diff engine. func TestLoadLayers_RawRedeclareAcrossLayers(t *testing.T) { const rawLayer = `database "posthog" { raw "dictionary" "r" { @@ -414,7 +472,7 @@ func TestLoadLayers_RawRedeclareAcrossLayers(t *testing.T) { _, err := LoadLayers([]string{a, b}) require.Error(t, err) - assert.Contains(t, err.Error(), `raw "r" (dictionary) redeclared across layers`) + assert.Contains(t, err.Error(), `raw "r" (dictionary) redeclared without override = true`) } func TestResolve_PatchUnknownTarget(t *testing.T) { diff --git a/internal/loader/hcl/locate.go b/internal/loader/hcl/locate.go index 16f1719..d825285 100644 --- a/internal/loader/hcl/locate.go +++ b/internal/loader/hcl/locate.go @@ -124,6 +124,7 @@ func objectDeclaration(blk *hclsyntax.Block, database, path string) (Declaration d.ObjectType = KindRaw d.RawKind = blk.Labels[0] d.Name = blk.Labels[1] + d.Override = boolAttr(blk.Body, "override") return d, true default: return Declaration{}, false diff --git a/internal/loader/hcl/locate_test.go b/internal/loader/hcl/locate_test.go index 5aeadf1..36454ed 100644 --- a/internal/loader/hcl/locate_test.go +++ b/internal/loader/hcl/locate_test.go @@ -85,6 +85,40 @@ named_collection "kafka_creds" { assert.Equal(t, want, decls) } +func TestScanDeclarationsRecognizesOverrideOnEveryManagedObjectKind(t *testing.T) { + dir := t.TempDir() + path := writeHCL(t, dir, "overrides.hcl", ` +database "posthog" { + table "events" { + override = true + } + materialized_view "events_mv" { + override = true + } + view "events_view" { + override = true + } + dictionary "geo" { + override = true + } + raw "view" "legacy" { + override = true + sql = "CREATE VIEW posthog.legacy AS SELECT 1" + } +} +named_collection "credentials" { + override = true +} +`) + + decls, err := ScanDeclarations([]string{path}) + require.NoError(t, err) + require.Len(t, decls, 6) + for _, declaration := range decls { + assert.True(t, declaration.Override, "%s %s", declaration.ObjectType, declaration.Name) + } +} + func TestScanDeclarationsParseError(t *testing.T) { dir := t.TempDir() path := writeHCL(t, dir, "broken.hcl", `database "posthog" {`) diff --git a/internal/loader/hcl/resolver.go b/internal/loader/hcl/resolver.go index 4d9620c..3d8bb76 100644 --- a/internal/loader/hcl/resolver.go +++ b/internal/loader/hcl/resolver.go @@ -16,13 +16,29 @@ func Resolve(s *Schema) error { if err := validateNamedCollections(s); err != nil { return err } - // Override is layer-authoring control metadata, like table/view Override. - // Once the winning declaration has been selected it must not leak into a - // resolved schema or its canonical dump. + // Override is layer-authoring control metadata. Once each winning + // declaration has been selected it must not leak into a resolved schema or + // its canonical dump. for i := range s.NamedCollections { s.NamedCollections[i].Override = false } for di := range s.Databases { + db := &s.Databases[di] + for i := range db.Tables { + db.Tables[i].Override = false + } + for i := range db.MaterializedViews { + db.MaterializedViews[i].Override = false + } + for i := range db.Views { + db.Views[i].Override = false + } + for i := range db.Dictionaries { + db.Dictionaries[i].Override = false + } + for i := range db.Raws { + db.Raws[i].Override = false + } if err := applyViewPatches(&s.Databases[di]); err != nil { return err } diff --git a/internal/loader/hcl/types.go b/internal/loader/hcl/types.go index 808ac7f..93d755a 100644 --- a/internal/loader/hcl/types.go +++ b/internal/loader/hcl/types.go @@ -51,9 +51,10 @@ type DatabaseSpec struct { // Kind drives the DROP form on a recreate, Name is the object name, and SQL // is emitted verbatim on apply. type RawSpec struct { - Kind string `hcl:"kind,label"` // table | materialized_view | view | dictionary - Name string `hcl:"name,label"` - SQL string `hcl:"sql"` + Kind string `hcl:"kind,label"` // table | materialized_view | view | dictionary + Name string `hcl:"name,label"` + Override bool `hcl:"override,optional" diff:"-"` + SQL string `hcl:"sql"` } // rawKinds is the set of kinds a RawSpec label may take. @@ -120,6 +121,7 @@ type PatchMaterializedViewSpec struct { // named user. The literal "current_user" is accepted unquoted. type ViewSpec struct { Name string `hcl:"name,label"` + Override bool `hcl:"override,optional" diff:"-"` Query string `hcl:"query"` ColumnAliases []string `hcl:"column_aliases,optional"` SQLSecurity *string `hcl:"sql_security,optional"` @@ -339,6 +341,7 @@ type EngineSpec struct { // the same pattern engines use. type DictionarySpec struct { Name string `hcl:"name,label"` + Override bool `hcl:"override,optional" diff:"-"` PrimaryKey []string `hcl:"primary_key"` Attributes []DictionaryAttribute `hcl:"attribute,block"` Source *DictionarySourceSpec `hcl:"source,block"`