Skip to content

Row-level expressions in every column position: time dimensions, partition keys, ranking keys, parameters - #512

Open
ZmeiGorynych wants to merge 28 commits into
egor/dev-2101-mode-b-as-a-sql-compatible-grammar-custom-parser-sql-formsfrom
egor/dev-2119-row-level-expressions-in-every-column-position-time
Open

ZmeiGorynych wants to merge 28 commits into
egor/dev-2101-mode-b-as-a-sql-compatible-grammar-custom-parser-sql-formsfrom
egor/dev-2119-row-level-expressions-in-every-column-position-time

Conversation

@ZmeiGorynych

@ZmeiGorynych ZmeiGorynych commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Resolves DEV-2119. Stacked on #509 (DEV-2101, the Mode-B grammar); GitHub retargets this to main once that merges.

What and why

Several query positions took only a column, so bucketing, partitioning, ranking or weighting by a computed value needed a query-time ModelExtension column. DEV-2021 is going to remove those extension columns, so this PR first makes every column position take any row-level Mode-B expression of the right type. A column is just the one-leaf case: semantics.arc42.md Axiom 13 now says so, and Axiom 15 that an expression is never a spine axis.

What users can now write

Time dimensions over an expression, also in the functional form:

{"source_model": "orders",
 "time_dimensions": [{"dimension": "coalesce(shipped_at, created_at)", "granularity": "month",
                      "date_range": ["2025-01-01", "2025-03-31"], "name": "effective_at"}],
 "measures": ["count(*)", "sum(amount, window='3m')", "change(sum(amount))"]}

returns one row per month of the per-order coalesced timestamp, keyed orders.effective_at (unnamed: orders.coalesce_shipped_at_created_at). date_range, whole_periods_only (date bounds for a DATE expression, instant bounds for a TIMESTAMP one), window=, time-ordered transforms and first / last all read the per-row value, and the results equal those of a derived column holding the same expression. "month(CAST(created_txt AS DATE))" works in dimensions, time_dimensions and order, and a granularity call takes any row-level argument in a filter too.

Also accepted now:

  • sum(amount, partition_by=upper(city)) — a key equal to a query computed dimension or a time dimension's bucket is that dimension; any other aggregate-bearing key points at the computed-dimension remedy.
  • last(balance, coalesce(updated_at, created_at)) and first(amount - customers.discount) — expression ranking keys and values, mixed sources included, rendered by the ranked kernel on every Tier-1 dialect.
  • weighted_avg(price, weight=quantity * 2), corr(amount, quantity + 1), and custom-aggregation parameters as expressions.
  • {var} literals inside a time-dimension expression, e.g. month(coalesce(shipped_at, created_at + INTERVAL '{days}' DAY)); an unnamed one is keyed from the template, so the result key never depends on the value.
  • Two spellings of one time dimension (coalesce(shipped_at, created_at) and COALESCE(shipped_at,created_at)) that agree in name, date range and label project once.

Fails closed, with typed errors raised before any SQL is generated:

  • a non-temporal or undetermined expression type (the latter names a CAST(…) remedy);
  • an aggregate inside a time dimension;
  • a leaf across a fanning hop from the expression's anchor;
  • re-bucketing a bucketed leaf finer than its granularity;
  • a spine time dimension over an expression;
  • a time-dimension name, an unnamed expression's auto-name included, that collides with another dimension's (at construction).

Breaking and result changes

  • Python API / dump: TimeDimension.dimension is Mode-B text, so td.dimension.name / .model are gone. It dumps as a string, and the JSON schema advertises a string. The legacy {"name", "model"} object is still accepted on input, and stored queries, models and memories migrate on load (SlayerQuery v8, SlayerModel v17, Memory v6, stored-only).
  • Default ranking key: first / last without a ranking key now ranks by the query's first DATE/TIMESTAMP dimension, a temporal computed dimension included (previously only a column qualified). In a producer with a coarser partition_by= the default stays producer-local; DEV-2136 tracks reading the query's dimensions there.
  • A time dimension's response type is its checked type (DATE for a DATE column or expression; it used to always be TIMESTAMP).

Also fixed

The source-model prefix strip used to rewrite an inner hop: rooted at orders, count(customers.orders.id) silently became count(customers.id). It now strips only a leading qualifier, so the revisit is refused with the circular-path error.

The full spec surface is in openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/.

ZmeiGorynych and others added 24 commits October 21, 2026 11:24
…-parser-sql-forms' into egor/dev-2119-row-level-expressions-in-every-column-position-time
…-parser-sql-forms' into egor/dev-2119-row-level-expressions-in-every-column-position-time
…position

New tests: expression time dimensions (forms, names, keys, every time position,
per-leaf re-bucketing), granularity calls over expressions, the spine refusal,
expression partition keys, expression ranking keys / values / parameters, the
ranked kernel on Tier-1 dialects, and the stored object-to-text upgrade.

Retargeted tests pinning refusals this change lifts:
- test_time_points_exec.py: month(upper(code)) -> date-operand type error
- test_dev1883_errors.py: month(upper(x)) -> binding-time temporal error
- test_dev2015_time_dimensions.py: fiscal_year(upper(id)) -> temporal error
- test_dev1903_first_last_dispatch.py: first(expr) / first(1) bind to the ranked aggregate
- test_dev1832_cross_model_exec.py: first over an expression emits a ranked aggregate
- test_dev1832_transform_source.py, test_dev1859_plan_structure.py: first over a
  mixed source (explicit ranking key id) emits a ranked aggregate
- test_dev1953_partition_alias.py: expression partition key equal to an alias binds
  to it; aggregate keys name the computed-dimension remedy
Move the exact-repeat extension-join dedupe from the query-backed-only planner branch into apply_extension_overlay, keyed on a semantic edge identity (target, name, join type, cardinality, pair set), and mirror it in the schema-drift stage graph. Attach saved joins after the stage-model rename with an explicit key check, route the saved-joins lookup through one bundle helper, and shorten the models-guide addition to one sentence.
Replace design decision 5's saved-relation-only workaround with the one cross-model rule, and add the models/join-traversal delta for it.
DuckDB reports an ETag change across CDN edges as 'this likely means the remote file has changed'; classify it as a transient outage of the named remote host so the notebook skips instead of failing.
Preserve declared joins on saved query-backed models
…and aggregation parameters; stored time dimensions migrate to text; docs
…-custom-parser-sql-forms' into egor/dev-2119-row-level-expressions-in-every-column-position-time

# Conflicts:
#	slayer/core/refs.py
#	slayer/engine/binding.py
@linear

linear Bot commented Oct 23, 2026

Copy link
Copy Markdown

DEV-2119

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 222 files, which is 72 over the limit of 150.

To get a review, reduce the PR to 150 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to Team to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

Run configuration
Commits

Reviewing files that changed from the base of the PR and between 322a892 and 5a4e2d4.

Files ignored due to path filters: 1
  • poetry.lock is excluded by !**/*.lock
Files selected for processing: 222
  • .basedpyright/baseline.json
  • CONTRIBUTING.md
  • architecture/semantics.arc42.md
  • docs/concepts/formulas.md
  • docs/concepts/models.md
  • docs/concepts/queries.md
  • docs/concepts/time.md
  • docs/examples/09_lightning_talk/slayer_models/models/jaffle_shop/customers.yaml
  • docs/examples/09_lightning_talk/slayer_models/models/jaffle_shop/items.yaml
  • docs/examples/09_lightning_talk/slayer_models/models/jaffle_shop/orders.yaml
  • docs/examples/09_lightning_talk/slayer_models/models/jaffle_shop/products.yaml
  • docs/examples/09_lightning_talk/slayer_models/models/jaffle_shop/stores.yaml
  • docs/examples/09_lightning_talk/slayer_models/models/jaffle_shop/supplies.yaml
  • docs/examples/09_lightning_talk/slayer_models/models/jaffle_shop/tweets.yaml
  • openspec/changes/archive/2026-10-22-fix-query-backed-declared-joins/.openspec.yaml
  • openspec/changes/archive/2026-10-22-fix-query-backed-declared-joins/design.md
  • openspec/changes/archive/2026-10-22-fix-query-backed-declared-joins/proposal.md
  • openspec/changes/archive/2026-10-22-fix-query-backed-declared-joins/specs/models/join-traversal/spec.md
  • openspec/changes/archive/2026-10-22-fix-query-backed-declared-joins/specs/queries/query-backed-inline/spec.md
  • openspec/changes/archive/2026-10-22-fix-query-backed-declared-joins/tasks.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/.openspec.yaml
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/design.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/proposal.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/specs/aggregations/expression-aggregation/spec.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/specs/aggregations/functional-form/spec.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/specs/models/stored-upgrade/spec.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/specs/queries/partitioned-aggregates/spec.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/specs/queries/time-dimensions/spec.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/specs/queries/time-points/spec.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/specs/queries/time-spine/spec.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/specs/queries/variables/spec.md
  • openspec/changes/dev-2119-row-level-expressions-in-every-column-position-time/tasks.md
  • openspec/specs/models/join-traversal/spec.md
  • openspec/specs/queries/query-backed-inline/spec.md
  • pyproject.toml
  • slayer/core/errors.py
  • slayer/core/join_edges.py
  • slayer/core/keys.py
  • slayer/core/query.py
  • slayer/core/refs.py
  • slayer/core/time_spine.py
  • slayer/engine/bind_inputs.py
  • slayer/engine/binding.py
  • slayer/engine/bundle_builder.py
  • slayer/engine/compile/projection.py
  • slayer/engine/compile/spine.py
  • slayer/engine/compile/stages.py
  • slayer/engine/elaborate_env.py
  • slayer/engine/home.py
  • slayer/engine/join_safety.py
  • slayer/engine/key_metadata.py
  • slayer/engine/model_reads.py
  • slayer/engine/plan.py
  • slayer/engine/population.py
  • slayer/engine/ranked_planner.py
  • slayer/engine/reference_closure.py
  • slayer/engine/response_meta.py
  • slayer/engine/schema_drift.py
  • slayer/engine/unrelated.py
  • slayer/facade/translator.py
  • slayer/ir/bound.py
  • slayer/ir/source_bundle.py
  • slayer/ir/variables.py
  • slayer/memories/help_content/02_workflow.md
  • slayer/memories/help_content/03_aggregations.md
  • slayer/memories/help_content/05_time.md
  • slayer/memories/resolver.py
  • slayer/sql/generator.py
  • slayer/sql/naming.py
  • slayer/sql/render/row_expr.py
  • slayer/sql/render/value_expr.py
  • slayer/storage/migrations.py
  • slayer/storage/time_dimension_text_migration.py
  • tests/_dev1471_fixtures.py
  • tests/_dev1739_fixtures.py
  • tests/_dev1750_fixtures.py
  • tests/_dev1800_fixtures.py
  • tests/_dev1832_fixtures.py
  • tests/_dev1836_fixtures.py
  • tests/_dev1838_fixtures.py
  • tests/_dev1840_fixtures.py
  • tests/_dev1842_fixtures.py
  • tests/_dev1846_fixtures.py
  • tests/_dev1871_raise_ledger.py
  • tests/_dev1908_fixtures.py
  • tests/_dev1915_fixtures.py
  • tests/_dev1919_fixtures.py
  • tests/_dev1945_fixtures.py
  • tests/_dev2006_fixtures.py
  • tests/_dev2046_fixtures.py
  • tests/_dev2062_fixtures.py
  • tests/_dev2119_fixtures.py
  • tests/dialects/test_dev1879_alias_battery.py
  • tests/dialects/test_generator_delegation.py
  • tests/dialects/test_multi_dialect_generation.py
  • tests/dialects/test_mysql.py
  • tests/dialects/test_tsql.py
  • tests/facade/test_translator.py
  • tests/integration/test_integration.py
  • tests/integration/test_integration_clickhouse.py
  • tests/integration/test_integration_duckdb.py
  • tests/integration/test_integration_mysql.py
  • tests/integration/test_integration_postgres.py
  • tests/integration/test_integration_snowflake.py
  • tests/integration/test_integration_sqlserver.py
  • tests/integration/test_integration_trino.py
  • tests/integration/test_integration_windowed_measures.py
  • tests/integration/test_notebooks.py
  • tests/perf/test_bench.py
  • tests/test_carrier_scope_matrix.py
  • tests/test_cross_model_derived_columns.py
  • tests/test_dev1446_nested_transform_dedup.py
  • tests/test_dev1450fix_derived_time_dimension.py
  • tests/test_dev1471_stage_date_range.py
  • tests/test_dev1471_stage_time_axis.py
  • tests/test_dev1471_stage_time_dimensions.py
  • tests/test_dev1474_time_shift_cross_model_partition.py
  • tests/test_dev1476_first_last_explicit_time.py
  • tests/test_dev1645_invalid_postgres_sql.py
  • tests/test_dev1708_stage4_cte_scope.py
  • tests/test_dev1713_naming.py
  • tests/test_dev1726_cte_case_folding.py
  • tests/test_dev1728_derived_shared_grain.py
  • tests/test_dev1732_frame_bound_filters.py
  • tests/test_dev1737_date_functions_typing.py
  • tests/test_dev1739_guards.py
  • tests/test_dev1744_naming_allocator.py
  • tests/test_dev1744_result_key_contract.py
  • tests/test_dev1744_value_expr.py
  • tests/test_dev1746_cte_assembly.py
  • tests/test_dev1746_null_safe_grain.py
  • tests/test_dev1746_projection_order.py
  • tests/test_dev1747_local_with_chain.py
  • tests/test_dev1747_order_entry.py
  • tests/test_dev1747_order_resolver.py
  • tests/test_dev1747_reroot_visitor.py
  • tests/test_dev1750_execution.py
  • tests/test_dev1763_call_site_migration.py
  • tests/test_dev1800_execution.py
  • tests/test_dev1825_regroup_execution.py
  • tests/test_dev1825_substitute_keys.py
  • tests/test_dev1827_value_key_traversal.py
  • tests/test_dev1832_cross_model_exec.py
  • tests/test_dev1832_golden_sql.py
  • tests/test_dev1832_transform_source.py
  • tests/test_dev1835_semantic_pins.py
  • tests/test_dev1836_ranked_windowed.py
  • tests/test_dev1836_reverse_hop_reroot.py
  • tests/test_dev1838_kernels.py
  • tests/test_dev1841_association_errors.py
  • tests/test_dev1842_keys_prepend.py
  • tests/test_dev1853_named_edges.py
  • tests/test_dev1859_plan_structure.py
  • tests/test_dev1859_walker.py
  • tests/test_dev1868_series_shift_exec.py
  • tests/test_dev1871_time_inkey_traversal.py
  • tests/test_dev1879_formula_measure_naming.py
  • tests/test_dev1883_equivalence_guards.py
  • tests/test_dev1883_errors.py
  • tests/test_dev1883_functional_rewrite.py
  • tests/test_dev1894_query_type_errors.py
  • tests/test_dev1900_closure.py
  • tests/test_dev1902_join_key_spelling.py
  • tests/test_dev1903_first_last_dispatch.py
  • tests/test_dev1903_transform_inputs.py
  • tests/test_dev1909_population_pushdown.py
  • tests/test_dev1915_windowed_exec.py
  • tests/test_dev1922_source_model_union.py
  • tests/test_dev1929_column_granularity.py
  • tests/test_dev1934_formula_agg.py
  • tests/test_dev1934_save_time_formula.py
  • tests/test_dev1942_golden_sql.py
  • tests/test_dev1946_transform_parameter.py
  • tests/test_dev1953_partition_alias.py
  • tests/test_dev1954_canonical_paths.py
  • tests/test_dev1958_cte_dependencies.py
  • tests/test_dev1958_shifted_leaf_grain.py
  • tests/test_dev1958_stage_axis.py
  • tests/test_dev1964_dual_phase_consumption.py
  • tests/test_dev2006_kernel_operands.py
  • tests/test_dev2013_expression_attribution.py
  • tests/test_dev2015_time_dimensions.py
  • tests/test_dev2062_alias_reference_rule.py
  • tests/test_dev2073_join_edge_identity.py
  • tests/test_dev2074_frame_local_revisit.py
  • tests/test_dev2101_stored_upgrade.py
  • tests/test_dev2119_expression_time_dimensions.py
  • tests/test_dev2119_granularity_calls.py
  • tests/test_dev2119_partition_keys.py
  • tests/test_dev2119_ranked_expressions.py
  • tests/test_dev2119_ranked_kernel.py
  • tests/test_dev2119_stored_upgrade.py
  • tests/test_dev2119_time_dimension_forms.py
  • tests/test_dev2119_time_dimension_variables.py
  • tests/test_dev2119_time_spine.py
  • tests/test_distinct_dimension_values.py
  • tests/test_entity_resolution.py
  • tests/test_expression_aggregations.py
  • tests/test_filtered_local_isolation.py
  • tests/test_memories_storage.py
  • tests/test_memory_string_ids.py
  • tests/test_model_access_caches.py
  • tests/test_model_access_tags.py
  • tests/test_models.py
  • tests/test_nested_dag_cross_stage_refs.py
  • tests/test_notebook_network_guard.py
  • tests/test_projection_trim.py
  • tests/test_query_backed_declared_join_boundaries.py
  • tests/test_query_backed_declared_joins.py
  • tests/test_rank_direction_migration.py
  • tests/test_reagg_outer_grain_attribution.py
  • tests/test_saved_query_refinement.py
  • tests/test_schema_drift_typed.py
  • tests/test_sql_generator.py
  • tests/test_sqlite_memories_pk_migration.py
  • tests/test_stored_upgrade_backing_sql.py
  • tests/test_stored_upgrade_rank_orders.py
  • tests/test_time_dimensions_filters.py
  • tests/test_time_dimensions_planner.py
  • tests/test_time_points_exec.py
  • tests/test_time_trunc_key.py
  • tests/test_transforms_planner.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

…like a computed dimension's; unnamed ones are named from the template
…/ join-path helpers out of two over-complex functions (Sonar S3776)
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants