From 78abd1b9b3075943f1019b53fb0c0082e43b7c1a Mon Sep 17 00:00:00 2001 From: Andre Baaij Date: Wed, 12 Aug 2026 21:12:45 -0400 Subject: [PATCH 1/4] feat(dbt): emit Hex's semantic-model table binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hex's Semantic Model Sync parses dbt MetricFlow YAML straight from a git repo (not via an API), and binds each semantic model to a physical table through `config.meta.hex.table`. Without that key the sync imports models that resolve to nothing: the agent then answers about no data, which a benchmark grades as a wrong answer rather than the setup failure it is. That exact failure mode cost a full Lightdash run earlier. Adds two options, both off by default: table-prefix logical -> physical name (ClickHouse materialises fct_orders as marts__fct_orders) dbt-hex-meta emit the config.meta.hex.table binding `dbt` becomes Configurable to receive them. A plain dbt build is unchanged and carries no Hex key — asserted by a test, because the dbt target also writes the shared ground-truth reference every arm reads, and one vendor's meta must not leak into it. Also fixes a defaulting bug this surfaced. `schema` defaulted to MAIN unconditionally, so the binding came out as `ecomm.MAIN.marts__fct_orders`, which does not resolve on ClickHouse's two-part namespace. `schema` is now a pointer so an EXPLICIT empty value is distinguishable from an omitted one; omitted still defaults to MAIN. Changing the default globally was the wrong fix: ossie, nao-yaml, supersimple and the warehouse dialects all read Schema, and Snowflake targets genuinely need the qualified form. Verified against the pinned eval reference: 38 semantic models, 22 measures, 14 metrics, all 38 bound to ecomm.marts__. Co-Authored-By: Claude Opus 5 --- cmd/semglot/config.go | 23 +++++++++++++++--- cmd/semglot/main.go | 2 ++ dialect/dbt.go | 7 +++++- dialect/dbt_emit.go | 43 ++++++++++++++++++++++++++++++--- dialect/dbt_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++ dialect/dialect.go | 11 +++++++++ 6 files changed, 134 insertions(+), 7 deletions(-) diff --git a/cmd/semglot/config.go b/cmd/semglot/config.go index 0ab1bee..3e2a52b 100644 --- a/cmd/semglot/config.go +++ b/cmd/semglot/config.go @@ -33,11 +33,21 @@ type profile struct { TargetDialect string `yaml:"target-dialect"` Output string `yaml:"output"` Database string `yaml:"database"` - Schema string `yaml:"schema"` + // Schema is a pointer so an EXPLICIT empty value is distinguishable from an + // omitted one. Omitted defaults to MAIN (Snowflake-shaped targets need a + // qualified name); explicitly empty means a two-part namespace such as + // ClickHouse, where "db.MAIN.table" would not resolve. + Schema *string `yaml:"schema"` ViewSchema string `yaml:"view-schema"` ModelName string `yaml:"model-name"` Description string `yaml:"description"` DbtMetaKeyPath string `yaml:"dbt-meta-key-path"` + // TablePrefix maps a logical table name to its physical one (ClickHouse + // materialises fct_orders as marts__fct_orders). + TablePrefix string `yaml:"table-prefix"` + // DbtHexMeta emits Hex's config.meta.hex.table binding on each semantic + // model; Hex's Semantic Model Sync cannot resolve the physical table without it. + DbtHexMeta bool `yaml:"dbt-hex-meta"` } // configFile is the top-level shape of semglot.yaml. @@ -57,6 +67,8 @@ type buildSpec struct { ModelName string Description string DbtMetaKeyPath string + TablePrefix string + DbtHexMeta bool } // warehouseTargets emit into a physical warehouse (Snowflake, or a Databricks @@ -94,16 +106,21 @@ func loadProfile(configPath, name string) (buildSpec, error) { TargetDialect: p.TargetDialect, Output: p.Output, Database: p.Database, - Schema: p.Schema, + Schema: "", ViewSchema: p.ViewSchema, ModelName: p.ModelName, Description: p.Description, DbtMetaKeyPath: p.DbtMetaKeyPath, + TablePrefix: p.TablePrefix, + DbtHexMeta: p.DbtHexMeta, } if spec.SourceDialect == "" { spec.SourceDialect = "dbt" } - if spec.Schema == "" { + switch { + case p.Schema != nil: + spec.Schema = *p.Schema + default: spec.Schema = "MAIN" } if spec.ModelName == "" { diff --git a/cmd/semglot/main.go b/cmd/semglot/main.go index 4fa6193..2cf35b5 100644 --- a/cmd/semglot/main.go +++ b/cmd/semglot/main.go @@ -72,6 +72,8 @@ func buildCmd(args []string) int { Name: spec.ModelName, Description: spec.Description, DbtMetaKeyPath: spec.DbtMetaKeyPath, + TablePrefix: spec.TablePrefix, + DbtHexMeta: spec.DbtHexMeta, }) } diff --git a/dialect/dbt.go b/dialect/dbt.go index 1b3e747..5dffcb5 100644 --- a/dialect/dbt.go +++ b/dialect/dbt.go @@ -17,7 +17,12 @@ func init() { Register(dbt{}) } // model properties (`models:` — table/column descriptions, data types, key and // relationship constraints/tests) and the semantic layer (`semantic_models:` + // `metrics:` — measures, aggregations, metrics). Either may be present alone. -type dbt struct{} +type dbt struct{ opts Options } + +// WithOptions makes dbt Configurable so a consumer-specific binding (Hex's +// config.meta.hex.table) and the physical table prefix can be supplied per +// build rather than baked in. +func (d dbt) WithOptions(o Options) Emitter { d.opts = o; return d } func (dbt) Name() string { return "dbt" } diff --git a/dialect/dbt_emit.go b/dialect/dbt_emit.go index 0cb5e4f..4b53dff 100644 --- a/dialect/dbt_emit.go +++ b/dialect/dbt_emit.go @@ -72,12 +72,30 @@ type dbtEmitRelTest struct { type dbtEmitSemantic struct { Name string `yaml:"name"` Model string `yaml:"model"` + Config *dbtEmitSemConfig `yaml:"config,omitempty"` Defaults *dbtEmitDefaults `yaml:"defaults,omitempty"` Entities []dbtEmitEntity `yaml:"entities,omitempty"` Dimensions []dbtEmitDimension `yaml:"dimensions,omitempty"` Measures []dbtEmitMeasure `yaml:"measures,omitempty"` } +// dbtEmitSemConfig carries the `config: meta:` block on a semantic model. Only +// Hex's binding lives here today; the shape is nested rather than flat so other +// consumers' meta keys can sit alongside without moving Hex's. +type dbtEmitSemConfig struct { + Meta dbtEmitSemMeta `yaml:"meta"` +} + +type dbtEmitSemMeta struct { + Hex *dbtEmitHexMeta `yaml:"hex,omitempty"` +} + +// dbtEmitHexMeta is Hex's semantic-model binding: the fully qualified physical +// table the model reads. +type dbtEmitHexMeta struct { + Table string `yaml:"table"` +} + type dbtEmitDefaults struct { AggTimeDimension string `yaml:"agg_time_dimension,omitempty"` } @@ -145,7 +163,7 @@ type dbtEmitConversionParams struct { // inlined aggregates are emitted rather than reported; what remains is a // genuine dbt limit. It is applied before emitModel too, so a physical column // that only a synthesised measure references still gets a columns[] entry. -func (dbt) Emit(m *ir.Model, dir string) ([]string, error) { +func (d dbt) Emit(m *ir.Model, dir string) ([]string, error) { var f dbtEmitFile var warnings []string // A relationship reaches dbt as a `relationships` data test on the FK @@ -200,7 +218,7 @@ func (dbt) Emit(m *ir.Model, dir string) ([]string, error) { } f.Models = append(f.Models, emitModel(m, t, pk, fk)) - f.SemanticModels = append(f.SemanticModels, emitSemantic(t, pk, fk)) + f.SemanticModels = append(f.SemanticModels, emitSemantic(t, pk, fk, d.opts)) metrics, warn := emitMetrics(t) f.Metrics = append(f.Metrics, metrics...) warnings = append(warnings, warn...) @@ -338,12 +356,31 @@ func emitModel(m *ir.Model, t ir.Table, pk, fk map[string]bool) dbtEmitModel { return em } +// physicalTable renders the warehouse-qualified name a semantic model reads: +// [database.][schema.]prefix+name. The IR holds logical names only, so the +// prefix and container come from Options. +func physicalTable(name string, opts Options) string { + out := opts.TablePrefix + name + if opts.Schema != "" { + out = opts.Schema + "." + out + } + if opts.Database != "" { + out = opts.Database + "." + out + } + return out +} + // emitSemantic builds the semantic_models block: a primary entity per PK column, // every non-PK/non-FK dimension as a semantic dimension (FK columns round-trip // as plain model columns + the relationship test, so they are NOT re-emitted as // entities), and every measure. -func emitSemantic(t ir.Table, pk, fk map[string]bool) dbtEmitSemantic { +func emitSemantic(t ir.Table, pk, fk map[string]bool, opts Options) dbtEmitSemantic { sm := dbtEmitSemantic{Name: t.Name, Model: "ref('" + t.Name + "')"} + if opts.DbtHexMeta { + sm.Config = &dbtEmitSemConfig{Meta: dbtEmitSemMeta{ + Hex: &dbtEmitHexMeta{Table: physicalTable(t.Name, opts)}, + }} + } if t.Grain != "" { sm.Defaults = &dbtEmitDefaults{AggTimeDimension: t.Grain} } diff --git a/dialect/dbt_test.go b/dialect/dbt_test.go index 83e64b3..764e069 100644 --- a/dialect/dbt_test.go +++ b/dialect/dbt_test.go @@ -510,3 +510,58 @@ func TestDBTParseLabelAndGrain(t *testing.T) { t.Fatalf("Def = %#v, want %#v", m.Def, wantDef) } } + +// TestDbtEmitsHexBinding covers Hex's Semantic Model Sync requirement: it parses +// MetricFlow YAML straight from a repo and cannot resolve which physical table a +// semantic model reads without config.meta.hex.table. Without the binding the +// sync imports models that resolve to nothing — the agent then answers about no +// data, which grades as a wrong answer rather than a setup failure. +func TestDbtEmitsHexBinding(t *testing.T) { + m := &ir.Model{Tables: []ir.Table{{ + Name: "fct_orders", + PrimaryKey: []string{"order_id"}, + Dimensions: []ir.Field{{Name: "status", Expr: "status"}}, + Measures: []ir.Measure{{Field: ir.Field{Name: "gross", Expr: "gross"}, Agg: "sum"}}, + }}} + dir := t.TempDir() + d := dbt{}.WithOptions(Options{Database: "ecomm", TablePrefix: "marts__", DbtHexMeta: true}) + if _, err := d.Emit(m, dir); err != nil { + t.Fatalf("Emit: %v", err) + } + got := readFile(t, filepath.Join(dir, "ecommerce.yml")) + for _, want := range []string{"config:", "meta:", "hex:", "table: ecomm.marts__fct_orders"} { + if !strings.Contains(got, want) { + t.Errorf("emitted YAML missing %q:\n%s", want, got) + } + } +} + +// TestDbtOmitsHexBindingByDefault: the binding is one consumer's key, so a plain +// dbt build must stay clean. Emitting it unasked would put a vendor's meta into +// the shared ground-truth reference every arm reads. +func TestDbtOmitsHexBindingByDefault(t *testing.T) { + m := &ir.Model{Tables: []ir.Table{{Name: "fct_orders", PrimaryKey: []string{"order_id"}}}} + dir := t.TempDir() + if _, err := (dbt{}).Emit(m, dir); err != nil { + t.Fatalf("Emit: %v", err) + } + if got := readFile(t, filepath.Join(dir, "ecommerce.yml")); strings.Contains(got, "hex:") { + t.Errorf("default dbt emit must not carry Hex meta:\n%s", got) + } +} + +func TestPhysicalTable(t *testing.T) { + cases := []struct { + opts Options + want string + }{ + {Options{Database: "ecomm", TablePrefix: "marts__"}, "ecomm.marts__fct_orders"}, + {Options{Database: "EVAL_MARTS", Schema: "MAIN"}, "EVAL_MARTS.MAIN.fct_orders"}, + {Options{}, "fct_orders"}, + } + for _, c := range cases { + if got := physicalTable("fct_orders", c.opts); got != c.want { + t.Errorf("physicalTable(%+v) = %q, want %q", c.opts, got, c.want) + } + } +} diff --git a/dialect/dialect.go b/dialect/dialect.go index 3b639e1..7c97586 100644 --- a/dialect/dialect.go +++ b/dialect/dialect.go @@ -45,6 +45,17 @@ type Options struct { // DbtMetaKeyPath selects where Lightdash meta lives: "" / "meta" nests under // meta: (dbt <=1.9); "config.meta" nests under config.meta: (dbt 1.10+). DbtMetaKeyPath string + // TablePrefix is prepended to a table's IR name to form its PHYSICAL name. + // The IR carries logical names (fct_orders); a warehouse may materialise + // them under a prefix (ClickHouse marts__fct_orders). Consumers that bind a + // semantic model to a physical table need the latter. + TablePrefix string + // DbtHexMeta emits Hex's `config.meta.hex.table` binding on every semantic + // model. Hex's Semantic Model Sync parses dbt MetricFlow YAML straight from + // a repo and CANNOT resolve which physical table a semantic model reads + // without it, so without this the sync imports models that resolve to + // nothing. + DbtHexMeta bool } // Configurable is an Emitter that accepts model/view identity options. From aa1136a1abb637de8bf548a357197b87b75d96e7 Mon Sep 17 00:00:00 2001 From: Andre Baaij Date: Wed, 12 Aug 2026 21:23:39 -0400 Subject: [PATCH 2/4] style: gofmt config.go The Schema *string change altered the struct's field-alignment column, which gofmt reflows. CI checks gofmt -l and I did not run it before pushing. Co-Authored-By: Claude Opus 5 --- cmd/semglot/config.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/semglot/config.go b/cmd/semglot/config.go index 3e2a52b..ba8f21d 100644 --- a/cmd/semglot/config.go +++ b/cmd/semglot/config.go @@ -28,20 +28,20 @@ func (s *sourcePaths) UnmarshalYAML(node *yaml.Node) error { // profile is one named build in semglot.yaml. type profile struct { - Source sourcePaths `yaml:"source"` - SourceDialect string `yaml:"source-dialect"` - TargetDialect string `yaml:"target-dialect"` - Output string `yaml:"output"` - Database string `yaml:"database"` + Source sourcePaths `yaml:"source"` + SourceDialect string `yaml:"source-dialect"` + TargetDialect string `yaml:"target-dialect"` + Output string `yaml:"output"` + Database string `yaml:"database"` // Schema is a pointer so an EXPLICIT empty value is distinguishable from an // omitted one. Omitted defaults to MAIN (Snowflake-shaped targets need a // qualified name); explicitly empty means a two-part namespace such as // ClickHouse, where "db.MAIN.table" would not resolve. - Schema *string `yaml:"schema"` - ViewSchema string `yaml:"view-schema"` - ModelName string `yaml:"model-name"` - Description string `yaml:"description"` - DbtMetaKeyPath string `yaml:"dbt-meta-key-path"` + Schema *string `yaml:"schema"` + ViewSchema string `yaml:"view-schema"` + ModelName string `yaml:"model-name"` + Description string `yaml:"description"` + DbtMetaKeyPath string `yaml:"dbt-meta-key-path"` // TablePrefix maps a logical table name to its physical one (ClickHouse // materialises fct_orders as marts__fct_orders). TablePrefix string `yaml:"table-prefix"` From 2d21fbc9f9d50bd0ad072a388a11e4ad5ae449b6 Mon Sep 17 00:00:00 2001 From: Andre Baaij Date: Thu, 13 Aug 2026 19:47:37 -0400 Subject: [PATCH 3/4] fix(supersimple): two-sided join keys, table prefix, and case control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes needed to target a case-sensitive warehouse with prefixed tables. The first is a correctness bug that would have produced wrong numbers silently. TWO-SIDED JOIN KEYS. The emitter wrote `join_key: ` for every relation. That is Supersimple's shorthand for "both sides share this column name", which held for 20 of 23 relations and was WRONG for the rest: fct_orders.order_date -> dim_date.date_day was emitted as `join_key: date_day`, addressing a column the child does not have. `supersimple validate` caught three of these ("references unknown property 'date_day' on related model"), but the dangerous case is the one it could not catch — the golden encoded a role-playing relation joining BILLING_CUSTOMER_SK as `join_key: CUSTOMER_SK`, a column that DOES exist on both sides, so it would have joined on the wrong one and returned plausible wrong answers rather than erroring. The vendor's own discovery snapshot uses join_key_on_base/join_key_on_related; we now emit the shorthand only when both sides genuinely agree. Golden updated: it captured the bug. TablePrefix maps the IR's logical name to the physical one (ClickHouse materialises fct_orders as marts__fct_orders). LowerCaseIdentifiers keeps identifiers as spelled instead of upper-casing them: the upper-case default suits Snowflake, where unquoted identifiers fold up, but ClickHouse is case-SENSITIVE and MARTS__FCT_ORDERS does not exist. Both default off, so Snowflake output is byte-identical. Schema now honours an EXPLICIT empty value rather than always defaulting to MAIN — ClickHouse has a two-part namespace, so MAIN.marts__fct_orders resolves to nothing. Verified end to end: 38 models, 338 properties, 23 relations, 14 metrics (on_time_delivery_rate intact as a real operations expression, not degraded to prose), no NOTES sidecar, `supersimple validate` reports Configuration is valid. Co-Authored-By: Claude Opus 5 --- cmd/semglot/config.go | 53 ++++++----- cmd/semglot/main.go | 17 ++-- dialect/dialect.go | 5 ++ dialect/supersimple.go | 89 +++++++++++++++---- dialect/supersimple_test.go | 18 ++++ .../dbt/supersimple/DIM_CUSTOMER.yaml | 3 +- 6 files changed, 136 insertions(+), 49 deletions(-) diff --git a/cmd/semglot/config.go b/cmd/semglot/config.go index ba8f21d..f20ae1e 100644 --- a/cmd/semglot/config.go +++ b/cmd/semglot/config.go @@ -48,6 +48,9 @@ type profile struct { // DbtHexMeta emits Hex's config.meta.hex.table binding on each semantic // model; Hex's Semantic Model Sync cannot resolve the physical table without it. DbtHexMeta bool `yaml:"dbt-hex-meta"` + // LowerCaseIdentifiers keeps identifiers as spelled rather than upper-casing + // them; required for case-sensitive warehouses such as ClickHouse. + LowerCaseIdentifiers bool `yaml:"lowercase-identifiers"` } // configFile is the top-level shape of semglot.yaml. @@ -57,18 +60,19 @@ type configFile struct { // buildSpec is a fully-resolved build: a validated profile with defaults applied. type buildSpec struct { - Sources []string - SourceDialect string - TargetDialect string - Output string - Database string - Schema string - ViewSchema string - ModelName string - Description string - DbtMetaKeyPath string - TablePrefix string - DbtHexMeta bool + Sources []string + SourceDialect string + TargetDialect string + Output string + Database string + Schema string + ViewSchema string + ModelName string + Description string + DbtMetaKeyPath string + TablePrefix string + DbtHexMeta bool + LowerCaseIdentifiers bool } // warehouseTargets emit into a physical warehouse (Snowflake, or a Databricks @@ -101,18 +105,19 @@ func loadProfile(configPath, name string) (buildSpec, error) { return buildSpec{}, fmt.Errorf("profile %q: output is required", name) } spec := buildSpec{ - Sources: []string(p.Source), - SourceDialect: p.SourceDialect, - TargetDialect: p.TargetDialect, - Output: p.Output, - Database: p.Database, - Schema: "", - ViewSchema: p.ViewSchema, - ModelName: p.ModelName, - Description: p.Description, - DbtMetaKeyPath: p.DbtMetaKeyPath, - TablePrefix: p.TablePrefix, - DbtHexMeta: p.DbtHexMeta, + Sources: []string(p.Source), + SourceDialect: p.SourceDialect, + TargetDialect: p.TargetDialect, + Output: p.Output, + Database: p.Database, + Schema: "", + ViewSchema: p.ViewSchema, + ModelName: p.ModelName, + Description: p.Description, + DbtMetaKeyPath: p.DbtMetaKeyPath, + TablePrefix: p.TablePrefix, + DbtHexMeta: p.DbtHexMeta, + LowerCaseIdentifiers: p.LowerCaseIdentifiers, } if spec.SourceDialect == "" { spec.SourceDialect = "dbt" diff --git a/cmd/semglot/main.go b/cmd/semglot/main.go index 2cf35b5..f85f796 100644 --- a/cmd/semglot/main.go +++ b/cmd/semglot/main.go @@ -66,14 +66,15 @@ func buildCmd(args []string) int { } if c, ok := emitter.(dialect.Configurable); ok { emitter = c.WithOptions(dialect.Options{ - Database: spec.Database, - Schema: spec.Schema, - ViewSchema: spec.ViewSchema, - Name: spec.ModelName, - Description: spec.Description, - DbtMetaKeyPath: spec.DbtMetaKeyPath, - TablePrefix: spec.TablePrefix, - DbtHexMeta: spec.DbtHexMeta, + Database: spec.Database, + Schema: spec.Schema, + ViewSchema: spec.ViewSchema, + Name: spec.ModelName, + Description: spec.Description, + DbtMetaKeyPath: spec.DbtMetaKeyPath, + TablePrefix: spec.TablePrefix, + DbtHexMeta: spec.DbtHexMeta, + LowerCaseIdentifiers: spec.LowerCaseIdentifiers, }) } diff --git a/dialect/dialect.go b/dialect/dialect.go index 7c97586..7f73868 100644 --- a/dialect/dialect.go +++ b/dialect/dialect.go @@ -56,6 +56,11 @@ type Options struct { // without it, so without this the sync imports models that resolve to // nothing. DbtHexMeta bool + // LowerCaseIdentifiers keeps identifiers as the IR spells them instead of + // upper-casing. Snowflake folds unquoted identifiers up, so upper-case is + // the safe default there; ClickHouse is case-sensitive and would not + // resolve them. + LowerCaseIdentifiers bool } // Configurable is an Emitter that accepts model/view identity options. diff --git a/dialect/supersimple.go b/dialect/supersimple.go index 5e8468b..d9ca1d5 100644 --- a/dialect/supersimple.go +++ b/dialect/supersimple.go @@ -15,9 +15,18 @@ import ( func init() { Register(supersimple{}) } // supersimple emits one supersimple config YAML per model. Zero value usable; -// the build command sets Schema from the profile's schema field. +// the build command sets Schema and TablePrefix from the profile. type supersimple struct { Schema string + // TablePrefix maps a logical table name to its PHYSICAL one. The IR carries + // logical names (fct_orders); a warehouse may materialise them under a + // prefix (ClickHouse marts__fct_orders), and `table:` must address the + // physical object or every query fails to resolve. + TablePrefix string + // LowerCase emits identifiers as-is rather than upper-casing them. The + // upper-case default suits Snowflake, where unquoted identifiers fold up; + // ClickHouse is case-SENSITIVE, so MARTS__FCT_ORDERS does not exist. + LowerCase bool } func (supersimple) Name() string { return "supersimple" } @@ -40,7 +49,7 @@ func isRatioDef(def ir.Expr) bool { // WithOptions lets the CLI pass the profile's schema (other identity fields are unused). func (supersimple) WithOptions(o Options) Emitter { - return supersimple{Schema: o.Schema} + return supersimple{Schema: o.Schema, TablePrefix: o.TablePrefix, LowerCase: o.LowerCaseIdentifiers} } const ssHeader = "# yaml-language-server: $schema=https://assets.supersimple.io/configuration_schema/1.0.0/supersimple_configuration_schema.json\n" @@ -69,9 +78,31 @@ type ssRelation struct { ModelID string `yaml:"model_id"` JoinStrategy ssJoinStrategy `yaml:"join_strategy"` } + +// ssJoinStrategy addresses a join. `join_key` is Supersimple's shorthand for +// "both sides share this column name"; when the foreign key and the primary key +// are named differently the two-sided form is REQUIRED, and the shorthand +// silently addresses the wrong column. +// +// The vendor's own discovery emits the two-sided form (dim_date joins +// fct_orders on join_key_on_base: DATE_DAY / join_key_on_related: ORDER_DATE). +// Emitting only the shorthand made `supersimple validate` reject three +// relations with "references unknown property 'date_day' on related model". type ssJoinStrategy struct { - JoinKey string `yaml:"join_key"` + JoinKey string `yaml:"join_key,omitempty"` + JoinKeyOnBase string `yaml:"join_key_on_base,omitempty"` + JoinKeyOnRelated string `yaml:"join_key_on_related,omitempty"` } + +// joinStrategy renders the shorthand when both sides agree and the explicit +// two-sided form when they differ. +func joinStrategy(base, related string) ssJoinStrategy { + if base == related || related == "" { + return ssJoinStrategy{JoinKey: base} + } + return ssJoinStrategy{JoinKeyOnBase: base, JoinKeyOnRelated: related} +} + type ssMetric struct { Name string `yaml:"name"` ModelID string `yaml:"model_id"` @@ -122,11 +153,30 @@ type ssRelationRef struct { // Emit does not mutate m; it reads m.Notes and accumulates its own degrade // notes locally before writing the combined text to NOTES.md. +// fold renders an identifier in the casing the target warehouse resolves. +// Snowflake folds unquoted identifiers to upper case, so upper is the safe +// default; ClickHouse is case-sensitive and MARTS__FCT_ORDERS does not exist. +func (s supersimple) fold(v string) string { + if s.LowerCase { + return v + } + return strings.ToUpper(v) +} + +// foldAll is fold over a slice. +func (s supersimple) foldAll(vs []string) []string { + out := make([]string, len(vs)) + for i, v := range vs { + out[i] = s.fold(v) + } + return out +} + func (s supersimple) Emit(m *ir.Model, dir string) ([]string, error) { + // An EXPLICIT empty schema is honoured (ClickHouse has a two-part namespace, + // so "MAIN.marts__fct_orders" resolves to nothing); only an unset one + // defaults. schema := s.Schema - if schema == "" { - schema = "MAIN" - } // relationships grouped by parent (Right) table relsByParent := map[string][]ir.Relationship{} for _, r := range m.Relationships { @@ -182,7 +232,7 @@ func (s supersimple) Emit(m *ir.Model, dir string) ([]string, error) { // and relations) and register its simple metrics. for _, t := range m.Tables { t.Metrics = hoist.metricsFor(t) // t is the range's own copy; m is untouched - id := strings.ToUpper(t.Name) + id := s.fold(t.Name) // Prefer the source dialect's own declared physical address. `table:` // holds ONE opaque string, unlike cortexBaseTable's separate // Database/Schema/Table fields, so a genuine table reference is used @@ -192,7 +242,10 @@ func (s supersimple) Emit(m *ir.Model, dir string) ([]string, error) { // permits) can't go here as-is — that falls back to the profile // reconstruction with a warning rather than pasting a query into // `table:` as if it were an address. - table := schema + "." + id + table := s.TablePrefix + id + if schema != "" { + table = schema + "." + table + } if t.Source != "" { if looksLikeQuery(t.Source) { degradeNotes = append(degradeNotes, querySourceWarning("supersimple", t.Name, t.Source)) @@ -203,12 +256,12 @@ func (s supersimple) Emit(m *ir.Model, dir string) ([]string, error) { model := ssModel{ Name: prettify(t.Name), Table: table, - PrimaryKey: upperAll(t.PrimaryKey), + PrimaryKey: s.foldAll(t.PrimaryKey), Description: appendClause(t.Description, synonymClause(t.Synonyms)), Properties: map[string]ssProperty{}, } addProp := func(f ir.Field, typ string) { - col := strings.ToUpper(f.Expr) + col := s.fold(f.Expr) if _, ok := model.Properties[col]; ok { return } @@ -229,9 +282,13 @@ func (s supersimple) Emit(m *ir.Model, dir string) ([]string, error) { } for _, r := range relsByParent[t.Name] { child := r.Left - join := "" + // r.Right is the PARENT (this model, the base); r.Left is the child. + // Column pairs are (Left=child column, Right=parent column), so the + // base key is the parent's and the related key is the child's. + var baseKey, relatedKey string if len(r.Columns) > 0 { - join = strings.ToUpper(r.Columns[0].Right) + baseKey = s.fold(r.Columns[0].Right) + relatedKey = s.fold(r.Columns[0].Left) } if model.Relations == nil { model.Relations = map[string]ssRelation{} @@ -258,8 +315,8 @@ func (s supersimple) Emit(m *ir.Model, dir string) ([]string, error) { } } model.Relations[key] = ssRelation{ - Name: label, Type: "hasMany", ModelID: strings.ToUpper(child), - JoinStrategy: ssJoinStrategy{JoinKey: join}, + Name: label, Type: "hasMany", ModelID: s.fold(child), + JoinStrategy: joinStrategy(baseKey, relatedKey), } } @@ -271,12 +328,12 @@ func (s supersimple) Emit(m *ir.Model, dir string) ([]string, error) { var key string switch a := arg.(type) { case ir.Col: - key = strings.ToUpper(a.Name) + key = s.fold(a.Name) case ir.Raw: // raw.SQL is unqualified; wrap its columns and synthesize a // property keyed by the metric name, guarding against clobbering // a physical column that already owns that key. - key = strings.ToUpper(mt.Name) + key = s.fold(mt.Name) for { if _, taken := model.Properties[key]; !taken { break diff --git a/dialect/supersimple_test.go b/dialect/supersimple_test.go index ea77d88..9e7d31b 100644 --- a/dialect/supersimple_test.go +++ b/dialect/supersimple_test.go @@ -398,3 +398,21 @@ func TestSupersimpleCrossTableRatioEmit(t *testing.T) { t.Fatal("NOTES.md should not exist when nothing is deferred") } } + +// TestSupersimpleTwoSidedJoinKey pins the two-sided join form. `join_key` is +// Supersimple's shorthand for "both sides share this column name"; when the FK +// and PK differ it addresses the wrong column, and `supersimple validate` +// rejected three dim_date relations with "references unknown property +// 'date_day' on related model" because of exactly this. +func TestSupersimpleTwoSidedJoinKey(t *testing.T) { + if got := joinStrategy("date_day", "order_date"); got.JoinKeyOnBase != "date_day" || + got.JoinKeyOnRelated != "order_date" || got.JoinKey != "" { + t.Errorf("differing keys must use the two-sided form; got %+v", got) + } + // Matching keys keep the shorthand, which is what the vendor emits and what + // the other 20 relations already validated with. + if got := joinStrategy("order_id", "order_id"); got.JoinKey != "order_id" || + got.JoinKeyOnBase != "" { + t.Errorf("matching keys should use the shorthand; got %+v", got) + } +} diff --git a/test/models/ecommerce/dbt/supersimple/DIM_CUSTOMER.yaml b/test/models/ecommerce/dbt/supersimple/DIM_CUSTOMER.yaml index b7eb42e..501d6aa 100644 --- a/test/models/ecommerce/dbt/supersimple/DIM_CUSTOMER.yaml +++ b/test/models/ecommerce/dbt/supersimple/DIM_CUSTOMER.yaml @@ -25,7 +25,8 @@ models: type: hasMany model_id: FCT_ORDERS join_strategy: - join_key: CUSTOMER_SK + join_key_on_base: CUSTOMER_SK + join_key_on_related: BILLING_CUSTOMER_SK orders_customer_sk: name: Orders (Customer sk) type: hasMany From 047eb598a41d3c07a05799f10f5d06a19faf387c Mon Sep 17 00:00:00 2001 From: Andre Baaij Date: Fri, 21 Aug 2026 10:13:18 -0400 Subject: [PATCH 4/4] Add the gif tooling used to record the OSSIE dialect demo Lived in the working tree only; committed ahead of the machine move. Co-Authored-By: Claude Opus 5 (1M context) --- tools/gif/make_gif.py | 188 ++++++++++++++++++++++++++++++++ tools/gif/out/semglot-ossie.gif | Bin 0 -> 48630 bytes 2 files changed, 188 insertions(+) create mode 100644 tools/gif/make_gif.py create mode 100644 tools/gif/out/semglot-ossie.gif diff --git a/tools/gif/make_gif.py b/tools/gif/make_gif.py new file mode 100644 index 0000000..01f5b5d --- /dev/null +++ b/tools/gif/make_gif.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Regenerate the semglot announcement GIF. + +Renders N SVG frames, rasterises each with inkscape, assembles with ImageMagick. +Palette and mark geometry are taken from logo.svg: ink #111111, oxblood #A00000. + +Usage: python3 tools/gif/make_gif.py [outfile.gif] +""" +import os +import shutil +import subprocess +import sys + +W, H = 800, 450 +BG = "#FBF9F3" +INK = "#111111" +OXBLOOD = "#A00000" +MUTED = "#6B6560" +SERIF = "Georgia, 'Liberation Serif', serif" + +TITLE = "Nine Semantic Layer Translations" +SOURCES = ["dbt", "Ossie"] +TARGETS = [ + "Snowflake Cortex", + "Snowflake semantic view", + "supersimple", + "nao / yaml", + "nao / context rules", + "Databricks metric view", + "Lightdash", + "Ossie", +] +FOOTNOTE = "dbt and Ossie read and write" +FOOTER = "open source" +URL = "benchouse.ai" + +CX, CY = 400.0, 232.0 # logo centre +LOGO_SCALE = 1.45 # 64-unit viewBox -> ~93px +TRACK_Y = CY +SRC_X = 170.0 # right edge of the source boxes +TGT_X = 544.0 # left edge of the target list +SRC_GAP = 62.0 # vertical gap between source boxes +TGT_SPAN = 34.0 # vertical gap between targets +HIDE_R = 50.0 # radius of the mark; balls vanish inside it +FRAMES = 40 +DELAY_CS = 5 # centiseconds per frame + +# One ball per target. Sources alternate, so dbt and Ossie fire in turn. +# The jitter breaks the metronome without making it look random. +JITTER = (0.00, 0.06, -0.04, 0.03, -0.02, 0.05, -0.05, 0.02) + +MARK = """ + + + + + + + + + + + + +{arrows} + + +""".format( + ink=INK, + ox=OXBLOOD, + arrows="\n".join( + ' '.format(a=a) + for a in (0, 60, 120, 180, 240, 300) + ), +) + + +def source_xy(j: int) -> tuple: + n = len(SOURCES) + return SRC_X, TRACK_Y + (j - (n - 1) / 2) * SRC_GAP + + +def target_xy(j: int) -> tuple: + n = len(TARGETS) + return TGT_X, TRACK_Y - (n - 1) / 2 * TGT_SPAN + j * TGT_SPAN + + +def frame_svg(i: int) -> str: + phase = i / FRAMES + n = len(TARGETS) + dots = [] + for k in range(n): + p = (phase + k / n + JITTER[k % len(JITTER)]) % 1.0 + sx, sy = source_xy(k % len(SOURCES)) + tx, ty = target_xy(k) + + # first half converges on the mark, second half fans out to a target + if p < 0.5: + t = p / 0.5 + x, y = sx + (CX - sx) * t, sy + (CY - sy) * t + else: + t = (p - 0.5) / 0.5 + x, y = CX + (tx - CX) * t, CY + (ty - CY) * t + + if (x - CX) ** 2 + (y - CY) ** 2 < HIDE_R**2: + continue + + # fade in leaving the source, fade out arriving at the target + op = min(1.0, p / 0.10, (1.0 - p) / 0.10) + dots.append( + f'' + ) + + # source boxes, vertically centred on the track + boxes = [] + n = len(SOURCES) + for j, s in enumerate(SOURCES): + by = TRACK_Y + (j - (n - 1) / 2) * 62 + boxes.append( + f'' + f'{s}' + ) + + # target list + items = [] + n = len(TARGETS) + span = 34 + top = TRACK_Y - (n - 1) / 2 * span + for j, t in enumerate(TARGETS): + items.append( + f'{t}' + ) + + return f""" + +{TITLE} +{''.join(boxes)} +{''.join(dots)} + +{MARK} + +SEMGLOT +{''.join(items)} +{FOOTNOTE} +{FOOTER} +{URL} +""" + + +def main() -> int: + out = sys.argv[1] if len(sys.argv) > 1 else "semglot-ossie.gif" + tmp = "/tmp/semglot-gif" + shutil.rmtree(tmp, ignore_errors=True) + os.makedirs(tmp) + + for i in range(FRAMES): + with open(f"{tmp}/f{i:03d}.svg", "w") as fh: + fh.write(frame_svg(i)) + + subprocess.run( + ["inkscape", "--export-type=png", f"--export-width={W}", + *[f"{tmp}/f{i:03d}.svg" for i in range(FRAMES)]], + check=True, capture_output=True, + ) + subprocess.run( + ["convert", "-delay", str(DELAY_CS), "-loop", "0", + f"{tmp}/f%03d.png[0-{FRAMES-1}]", "-layers", "Optimize", out], + check=True, + ) + print(f"wrote {out} ({os.path.getsize(out)/1024:.0f} KB, {FRAMES} frames)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/gif/out/semglot-ossie.gif b/tools/gif/out/semglot-ossie.gif new file mode 100644 index 0000000000000000000000000000000000000000..3518aecf41bcb073e727925530f19e5d7f1e25b2 GIT binary patch literal 48630 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

3T1qV4j}>Gj-_zIhf>FOWG46Jvn{>owO1+ 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