Joins overhaul, dbt semantic model ingestion #42
ZmeiGorynych
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Join overhaul: query-time multi-hop resolution, INNER join support, per-measure CTE isolation
Motivation
The join system had fundamental limitations exposed by the ACME insurance benchmark (gpt-5.3-codex scored 1/11 correct). Baked-in multi-hop joins at ingestion created stale metadata when the graph changed. All joins were LEFT, causing fan-out on entity relationships that should be INNER (50-118x inflated sums on benchmark queries). Cross-model filtered measures sharing a single base query caused JOIN intersections that zeroed out results. This PR rewrites the join and SQL generation architecture to fix these issues.
What changed
1. Query-time multi-hop join resolution
Ingestion now emits only direct joins (one
ModelJoinper FK). Multi-hop paths likeorders ? customers ? regionsare resolved at query time by walking each intermediate model's own joins. This eliminates redundant storage and keeps join metadata consistent when the graph changes.Files:
ingestion.py,enrichment.py(_collect_needed_paths,_resolve_joins)2. LEFT/INNER join support
New
JoinTypeenum (LEFT,INNER). dbt entity-based joins (both foreign?primary and peer) now use INNER, matching MetricFlow behavior. Inner joins are automatically mirrored: if A?B is INNER, B?A is created too.Join synchronization is centralized in a new
JoinSyncStoragedecorator that wraps every storage backend, handling mirroring on save, orphan cleanup on delete, and one-time reconciliation of hand-edited YAML.Files:
enums.py,models.py,entities.py,converter.py,join_sync.py(new),base.py3. Per-measure CTE isolation
Major SQL generation refactor. The generator now follows a multi-stage pattern:
This fixes two classes of bugs: filtered measures needing different INNER JOINs no longer intersect in a single base query, and cross-model measures in arithmetic formulas (previously "not yet supported") now work.
CTE builders were also converted from f-string SQL to sqlglot AST building for dialect safety.
Files:
generator.py(major rewrite),enrichment.py4. Re-rooted subqueries for cross-model measures
Cross-model measure CTEs previously only joined source?target, so filters on tables reachable from the target's join graph were silently dropped. Now each cross-model measure gets a re-rooted
EnrichedQuerywhere the target model is the FROM table, giving it access to its full join graph for correct filter application. Unreachable dimensions/filters are dropped gracefully (scalar CROSS JOIN).Files:
query_engine.py(_build_rerooted_enriched),enriched.py,generator.py5. dbt entity system fixes
claimandclaim_coverageboth declaringclaim_identifier) now get bidirectional joins, enabling paths likeclaim ? claim_coverage ? policyloss_payment.has_loss_payment)get_primary_modeland peer dimension selection use lexicographic ordering instead of depending on registration order(model, local_expr, peer_expr)signature instead of model-name-onlyFiles:
entities.py,filters.py,converter.py6. LLM-friendliness improvements
fields, it's automatically moved todimensions(with validation: no aggregation, resolves as dimension, not a measure)SlayerQuery.strip_source_model_prefix()centralizes the removal of redundant prefixes likeorders.revenue:sumwhensource_model=orders, handling all reference types (dims, time dims, fields, filters, order, main_time_dimension)claim_identifier:count_distinctnow works ? dimensions are looked up when no measure matches, with type-safety checks (rejects sum/avg on strings)policies.id:count_distinctwork via a synthetic Measure wrappersql="1") no longer produce brokenpremium.1qualificationsFiles:
query_engine.py,query.py,enrichment.py,generator.py,server.py7. Runtime column type inference and measure profiling
get_column_types()probes measure types via the engine pipeline (properly joined SQL), supporting DuckDB, Postgres, MySQL, and SQLite with dialect-specific OID mapsinspect_modelnow shows type (number/string/time/boolean) and value range (min..max or "all NULL") columns for measures, helping LLMs avoid errors likeSUM(VARCHAR)Files:
client.py,query_engine.py,server.py8. Time shift rework
time_shiftnow shifts the time dimension column via SQLINTERVALinstead of computing shifted date range strings in Python. Eliminates_shift_date,_build_row_number_join, and the row-number vs calendar join branchingchange(x)andchange_pct(x)are desugared at enrichment time into a hiddentime_shift+ arithmetic expression, simplifying the generatorFiles:
enrichment.py,generator.py,enriched.py9. Additional bug fixes
field_name_aliasesmap consulted by the ORDER BY resolver_resolve_order_column()instead of manual f-string formatting_build_combined()returns structured CTE tuples instead of assembled SQLrevenue:sum+revenue:avg) gets distinct CTE names(target, src_col, tgt_col)signature, preserving multiple FKs to the same table_last_rncolumns from the baseTest coverage
~3,400 new test lines across 12 test files:
test_sql_generator.py: ~2,000 new lines covering CTE isolation, re-rooted subqueries, cross-model dimension aggregation, ORDER BY resolution, first/last isolation, SQL validity checks (_assert_valid_sqlvalidates every generated query with sqlglot)test_join_sync.py(new): 321 lines ? mirroring, save/delete sync, reconciliationtest_models.py(new): 303 lines ?strip_source_model_prefixexhaustive coveragetest_dbt_converter.py: 326 new lines ? peer joins, foreign entity, inner join mirroringtest_dbt_filters.py: 100 new lines ? peer dimension qualification, deterministic selectiontest_sql_client.py(new): 86 lines ? type code mapping across driverstest_mcp_server.py: 154 new lines ? show_sql gating, measure types, inner join mirroringDocumentation
Updated across ingestion, joins, formulas, transforms, queries, terminology, and help topics to reflect direct FK joins at ingestion, query-time traversal, INTERVAL-based time shifting, and dimension aggregation.
This discussion was created from the release Joins overhaul, dbt semantic model ingestion.
Beta Was this translation helpful? Give feedback.
All reactions