Skip to content

DEV1260: Variables in Filters#34

Merged
AivanF merged 1 commit intomainfrom
aivan/dev-1260-variables-in-filters
Apr 16, 2026
Merged

DEV1260: Variables in Filters#34
AivanF merged 1 commit intomainfrom
aivan/dev-1260-variables-in-filters

Conversation

@AivanF
Copy link
Copy Markdown
Collaborator

@AivanF AivanF commented Apr 16, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Filters now support {variable} placeholders for dynamic values sourced from a new variables query parameter.
    • String and number values are inserted directly; escape literal braces with {{ and }}.
    • Undefined variables raise errors during processing.
  • Documentation

    • Updated query documentation with filter variable details and placeholder syntax.

@AivanF AivanF requested a review from ZmeiGorynych April 16, 2026 13:12
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

This pull request adds support for variable placeholder substitution in filter expressions. Filter strings can now reference {variable_name} placeholders sourced from query.variables: Dict[str, Any], with support for escaping literal braces via {{/}}. Missing or invalid variables raise errors during substitution.

Changes

Cohort / File(s) Summary
Documentation
CLAUDE.md, docs/concepts/queries.md
Updated documentation to describe the new {variable_name} placeholder syntax in filters, the new optional variables query parameter, and placeholder substitution behavior including escaping rules and error handling.
Core Implementation
slayer/core/query.py
Added substitute_variables() utility function with regex-based placeholder detection and validation; added optional variables: Optional[Dict[str, Any]] field to SlayerQuery model for providing substitution values. Validates that variable names match allowed identifier pattern, values are string/int/float, and referenced variables are defined.
Engine Integration
slayer/engine/enrichment.py
Integrated variable substitution into query enrichment pipeline; filters are substituted when query.variables is present, with substituted filters replacing original values in downstream processing (filter transformation extraction, column resolution, WHERE/HAVING classification).
Tests
tests/test_models.py
Added comprehensive TestSubstituteVariables test suite validating placeholder substitution for various data types, multiple placeholders per filter, escaped brace handling, and error cases for undefined variables, invalid names, and invalid value types. Includes test confirming SlayerQuery.variables field is accepted and retained.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Filter strings now bloom with variables true,
{placeholders} dance in queries new,
Escaped braces double, like our twitching nose,
From source to enrichment, the substitution flows!
No more hardcoding—flexibility grows!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'DEV1260: Variables in Filters' directly and accurately summarizes the main change: adding support for variable placeholders in filter expressions across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aivan/dev-1260-variables-in-filters

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
slayer/engine/enrichment.py (1)

441-450: Consider moving the import to the top of the file.

The substitute_variables import is placed inside the conditional block, but slayer.core.query is already imported at line 27 (from slayer.core.query import SlayerQuery). Moving this import to the top with the other imports from the same module would be more consistent with the coding guidelines and wouldn't risk circular import issues.

As per coding guidelines: "Place imports at the top of files"

Suggested change

At line 27, extend the existing import:

-from slayer.core.query import SlayerQuery
+from slayer.core.query import SlayerQuery, substitute_variables

Then simplify lines 443-448:

     if query.variables and query_filters:
-        from slayer.core.query import substitute_variables
-
         query_filters = [
             substitute_variables(filter_str=f, variables=query.variables) for f in query_filters
         ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@slayer/engine/enrichment.py` around lines 441 - 450, Move the from
slayer.core.query import substitute_variables import to the top of
slayer/engine/enrichment.py alongside the existing from slayer.core.query import
SlayerQuery import so all imports from slayer.core.query are co-located; then
remove the in-block import and keep the conditional logic that calls
substitute_variables (used with query.filters and query.variables) unchanged,
ensuring you only reference substitute_variables in the query-level filter
substitution block.
tests/test_models.py (1)

571-579: Consider adding an integration test for the full enrichment flow.

The unit tests thoroughly cover substitute_variables in isolation, but test_variable_in_slayer_query only verifies that the variables field is accepted and stored on SlayerQuery. There's no test verifying that variables are actually substituted during query enrichment and affect the final SQL output.

An integration test in tests/integration/ that runs a query with filters containing placeholders and variables providing values would catch regressions in the enrichment.py integration point (lines 443-448).

Would you like me to draft an integration test case that verifies the end-to-end flow?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_models.py` around lines 571 - 579, Add an integration test that
verifies variables are substituted through the full enrichment flow: create a
SlayerQuery with a filters string containing a placeholder like "{val}" and
variables={"val":"completed"}, pass that SlayerQuery into the enrichment
entrypoint that uses substitute_variables (the function in enrichment.py that
consumes SlayerQuery and returns the enriched SQL), then assert the
returned/enriched SQL contains the substituted value (e.g., "status =
'completed'") to ensure end-to-end substitution works.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@slayer/engine/enrichment.py`:
- Around line 441-450: Move the from slayer.core.query import
substitute_variables import to the top of slayer/engine/enrichment.py alongside
the existing from slayer.core.query import SlayerQuery import so all imports
from slayer.core.query are co-located; then remove the in-block import and keep
the conditional logic that calls substitute_variables (used with query.filters
and query.variables) unchanged, ensuring you only reference substitute_variables
in the query-level filter substitution block.

In `@tests/test_models.py`:
- Around line 571-579: Add an integration test that verifies variables are
substituted through the full enrichment flow: create a SlayerQuery with a
filters string containing a placeholder like "{val}" and
variables={"val":"completed"}, pass that SlayerQuery into the enrichment
entrypoint that uses substitute_variables (the function in enrichment.py that
consumes SlayerQuery and returns the enriched SQL), then assert the
returned/enriched SQL contains the substituted value (e.g., "status =
'completed'") to ensure end-to-end substitution works.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6469f151-9e61-4595-b5a7-4d69c4edf782

📥 Commits

Reviewing files that changed from the base of the PR and between bdce4d8 and c06d069.

📒 Files selected for processing (5)
  • CLAUDE.md
  • docs/concepts/queries.md
  • slayer/core/query.py
  • slayer/engine/enrichment.py
  • tests/test_models.py

@AivanF AivanF merged commit 984bf5c into main Apr 16, 2026
3 checks passed
ZmeiGorynych added a commit that referenced this pull request Apr 16, 2026
Five new CodeRabbit comments after the previous batches landed:

* slayer/cli.py: bridge async storage.get_datasource and storage.save_model
  with run_sync (B6-1, Critical). Every other storage.* call site in this
  file already does so; import-dbt was the lone exception, with the result
  that --include-hidden-models always failed the datasource lookup
  (coroutine != None) and even plain `slayer import-dbt` printed
  "Imported model: ..." without ever persisting anything.
* slayer/dbt/parser.py: add keyword-only `include_regular_models: bool =
  False` to parse_dbt_project (B6-2, Major). Previously the manifest was
  always loaded, which can invoke `dbt parse` (slow) or fail noisily when
  dbt-core isn't installed — making the "optional" hidden-model import
  path non-optional. CLI passes through args.include_hidden_models.
* slayer/engine/enrichment.py: propagate model_name_str through every
  Enriched* construction site (B6-3, Major). Batch 1 fixed filter
  resolution to use model_name_str but EnrichedMeasure.model_name,
  EnrichedQuery.model_name, and the Dimension/TimeDimension helpers
  still used model.name — mismatched aliases would render invalid SQL
  in any divergent path. In production today the two are always equal
  so this was latent; my Batch 1 regression test exposes the case.
* slayer/sql/generator.py: project a per-measure boolean match-flag
  column from the ranked subquery (B6-4, Major). _build_agg() previously
  re-emitted measure.filter_sql in the OUTER MAX(CASE WHEN ...). Batch 2
  injected joins inside the subquery so the inner ROW_NUMBER OVER
  resolved, but the outer aggregate had no scope on the joined tables.
  Now _build_last_ranked_from also projects `_match_f<idx>` columns and
  the outer MAX(CASE) tests `_match_f<idx> = 1` instead.
* docs/examples/01_dynamic/dynamic.md: subject-verb typo "audience are"
  → "audience is" (B6-5).

Plus regression tests for every real-bug fix:
* TestImportDbtCli::test_models_are_persisted_to_storage (B6-1)
* TestRegularModelsOptIn::{test_default_does_not_load_manifest,
  test_opt_in_loads_manifest} (B6-2)
* TestFilteredMeasures::test_filtered_measure_source_alias_propagates_to_generated_sql (B6-3)
* TestFilteredMeasures::test_filtered_last_outer_aggregate_uses_match_flag_not_joined_filter (B6-4)

Plus also merges in origin/main (PR #34 "Variables in filters") cleanly.

Full pytest: 681 passed (was 676 baseline post-merge). ruff clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

1 participant