Skip to content

Instantly share code, notes, and snippets.

@camsaul
Last active May 7, 2026 21:14
Show Gist options
  • Select an option

  • Save camsaul/ecc6cd3a9372bc969341df4764de3f04 to your computer and use it in GitHub Desktop.

Select an option

Save camsaul/ecc6cd3a9372bc969341df4764de3f04 to your computer and use it in GitHub Desktop.
Metabase Parameters Summary

Parameters in MBQL Queries: Backend Technical Summary

1. Two Distinct Concepts Called "Parameters"

Template Tags (:template-tags on a native stage)

These are declarations — placeholders the SQL author defines. Schema lives in metabase.lib.schema.template-tag. Each tag has a :type:

:type Purpose
:text, :number, :date, :boolean Scalar variable substitution: WHERE x = {{var}}
:dimension Field filter — has :dimension [:field ...] and :widget-type; expands to a full WHERE clause
:snippet {{snippet: foo}} — resolves to a NativeQuerySnippet row (verbatim SQL)
:card {{#123}} — resolves to another card's compiled SQL as a subquery
:table References a Table by id, optionally with :source-filters and :emit-alias
:temporal-unit Injects a temporal bucketing unit into a field ref

Parameter Values (:parameters on the query)

These are values passed at query time from the API. Schema in metabase.lib.schema.parameter. A ::parameter is {:id :type [:value] [:target] ...}.

The :target routes the value to the right tag or field — four shapes:

[:dimension [:template-tag "my_tag"]]          ; Field filter tag (by name)
[:dimension [:template-tag {:id <param-id>}]]  ; Field filter tag (by id, preferred since v44)
[:variable  [:template-tag "my_tag"]]          ; Raw-value tag
[:dimension [:field 100 nil]]                  ; MBQL field directly

metabase.lib.parameters provides helpers like parameter-target-template-tag-name, parameter-target-field-id, etc. to pick these apart.


2. QP Middleware Pipeline Position

Defined in metabase.query-processor.preprocess:

resolve-referenced-card-resources   ← pre-fetches cards/snippets transitively
→ parameters/substitute-parameters  ← THE parameter middleware
resolve-source-tables
auto-bucket-datetimes
... (joins, remaps, implicit clauses, sandboxing, etc.)

resolve-referenced-card-resources (middleware/resolve_referenced.clj) runs first: it walks template tags, finds every :card and :snippet tag transitively, builds a dependency graph (via weavejester.dependency), throws on circular references, and warms the metadata provider. It runs before parameter substitution because substitution needs those cards/snippets loaded.


3. Native Template Tag Substitution ({{var}} and [[…]])

Four phases:

Phase 1: Parse the SQL string

metabase.lib.parse/parse tokenizes the SQL string, recognizing [[, ]], {{, }}, and SQL comment boundaries. metabase.lib.parameters.parse/parse wraps this and emits typed records:

  • ::param{:lib/type ::param :k "tag-name"}
  • ::optional{:lib/type ::optional :args [...]} — the [[…]] block
  • ::function-param — for special function invocations

Tag names are normalized: {{snippet: foo}} and {{#123}} get recognized and stripped.

Phase 2: Build a param-name → value map

metabase.driver.common.parameters.values/query->params-map iterates :template-tags and dispatches per :type:

  • :dimensionFieldFilter record with resolved column metadata + matched parameter values
  • :card → compiles the card via qp.compile/compile, wraps in ReferencedCardQuery (with its JDBC args)
  • :snippet → loads from metadata provider → ReferencedQuerySnippet {:snippet-id :content}
  • :temporal-unitTemporalUnit record with the field and new unit
  • Raw value types → bare value (with default/required handling)

Phase 3: Substitute into the parsed token sequence

metabase.driver.sql.parameters.substitute/substitute reduces the parsed fragments:

  • String fragments → verbatim
  • Param tokens → look up in param map, dispatch by record type
  • Optional blocks ([[…]]) → if any key is missing (no value), the entire block is discarded; otherwise it expands normally
  • A FieldFilter with no value substitutes "1 = 1" outside optionals, signals missing inside them
  • ReferencedQuerySnippet triggers recursive re-parsing of the snippet content (snippets can contain {{tags}})

Phase 4: Convert values to SQL

metabase.driver.sql.parameters.substitution/->replacement-snippet-info (multimethod on [driver class]):

Value type SQL output
Scalar ? + JDBC arg
DateRange/DateTimeRange BETWEEN ? AND ? or >= / <
IPersistentVector ?, ?, ? (IN list)
FieldFilter Builds MBQL filter via params.ops/to-clause, renders via HoneySQL
ReferencedCardQuery (SELECT ...) via make-nestable-sql + card's JDBC args
ReferencedQuerySnippet :content inserted verbatim (no JDBC args)
ReferencedTableQuery "schema"."table" with optional WHERE and alias

4. The Top-Level Middleware: substitute-parameters

In metabase.query-processor.middleware.parameters:

(defn substitute-parameters [query]
  (-> query
      hoist-database-for-snippet-tags   ; stamps :database id into every :snippet tag
      expand-parameters))

expand-parameters:

  1. move-top-level-params-to-stage — distributes top-level :parameters to the stage they target (using :stage-number from the target options, offset to account for source-card stages prepended during preprocessing). Renames top-level :parameters to :user-parameters. Sets :qp/skip-result-metadata-persistence true if non-default values were applied.

  2. expand-all — walks every stage via lib.walk/walk-stages, dispatching to:

    • MBQL stageqp.mbql/expand
    • Native stageqp.native/expand-stage

    Both strip :parameters and :template-tags from the stage after expansion.


5. MBQL-Stage Expansion

metabase.query-processor.middleware.parameters.mbql/expand reduces over the stage's :parameters:

  • :temporal-unit params → update-breakout-unit swaps the unit on the matching breakout
  • Operator params (:number/between, :string/contains, etc.) → params.ops/to-clauselib.filter/add-filter-to-stage
  • Date-typed params → params.dates/date-string->filter (parses relative ranges, exclusions, etc.) → filter
  • Single values → lib/= field-ref parsed-value
  • Multiple values → recursively built clauses glued with lib/or

6. :parameters vs :template-tags — Key Difference

Key Location Purpose Lifecycle
:template-tags Native stage Declarations — name, type, default, widget-type, dimension Created when SQL is parsed; stripped after substitute-parameters
:parameters (top-level) Query root Values from API (dashboard filters, etc.) Distributed to stages by move-top-level-params-to-stage, then consumed
:parameters (stage-level) Any stage Post-distribution values Stripped after expand-all

7. SQL Snippets vs Variable Substitution

Aspect {{my_var}} {{snippet: foo}}
Value source API :parameters matched by tag name NativeQuerySnippet row in app DB
SQL output ? placeholder + JDBC args (or field op ? for field filters) :content interpolated verbatim
Recursive expansion No Yes — snippet content is re-parsed, so it can contain {{tags}}
[[…]] interaction Empty value drops the block Almost always present
DB id requirement Not needed hoist-database-for-snippet-tags injects :database id before expansion

Cards ({{#123}}) are in between — they expand to (SELECT ...) via make-nestable-sql, but do contribute JDBC args (the inner card may itself be parameterized).


8. metabase.parameters.* vs QP Middleware

These are two separate systems:

Layer Namespaces Role
Application-side metabase.parameters.* Powers parameter widget UI — chain filtering, field value lookup, stored declarations
QP middleware metabase.query-processor.middleware.parameters.*, metabase.driver.sql.parameters.* Consumes the widget output (the :parameters list) and rewrites the query

Key metabase.parameters.* namespaces:

  • parameters.schema — stored Card/Dashboard parameter declarations and Toucan transforms
  • parameters.params — field ID resolution for parameter targets; Toucan hydration of :param_fields
  • parameters.chain-filter — builds MBQL queries to populate parameter dropdowns
  • parameters.field/field-values — values for parameter widgets (search, list, FK remapping)
  • parameters.custom-values — static-list or card-as-source-of-values
  • parameters.dashboard — bridges chain filtering to dashboard API endpoints

The bridge between them is the schema layer: metabase.lib.schema.parameter defines the wire format; metabase.parameters.schema defines the stored format. API endpoints normalize stored declarations + user input into the :parameters list before invoking the QP.


Key Files

File Purpose
src/metabase/lib/schema/template_tag.cljc Template tag Malli schemas
src/metabase/lib/schema/parameter.cljc Parameter value schemas, types registry, parameter-type-and-widget-type-allowed-together?
src/metabase/lib/parameters.cljc Target-parsing helpers
src/metabase/lib/parameters/parse.cljc SQL tokenizer/parser
src/metabase/query_processor/middleware/parameters.clj Top-level substitute-parameters middleware
src/metabase/query_processor/middleware/parameters/mbql.clj MBQL stage expansion
src/metabase/query_processor/middleware/parameters/native.clj Native stage expansion
src/metabase/query_processor/middleware/resolve_referenced.clj Pre-fetch cards/snippets
src/metabase/driver/common/parameters/values.clj query->params-map (legacy, still active)
src/metabase/driver/sql/parameters/substitute.clj Token-sequence substitution
src/metabase/driver/sql/parameters/substitution.clj ->replacement-snippet-info multimethod
src/metabase/parameters/chain_filter.clj Parameter dropdown query building

Note: metabase.driver.common.parameters.* namespaces are deprecated and flagged for migration to metabase.lib.parameters.* and metabase.query-processor.parameters.* as part of the pMBQL transition — but the SQL driver still uses the legacy form because expand-stage converts pMBQL stages to legacy-MBQL via lib/->legacy-MBQL before calling driver/substitute-native-parameters.

(ns metabase.parameters-demo
(:require
[metabase.lib.test-metadata :as meta]
[metabase.lib.core :as lib]
[metabase.driver :as driver]))
;; Simple demo
(comment
(driver/with-driver :h2
((requiring-resolve 'metabase.query-processor.middleware.parameters/substitute-parameters)
{:lib/type :mbql/query
:lib/metadata meta/metadata-provider
:database (meta/id)
:stages [{:lib/type :mbql.stage/native
:template-tags {"price" {:name "price"
:display-name "Price"
:type :number
:required true}}
:native "SELECT * FROM venues WHERE other_column = 1;"}]
:parameters [{:type :number
:target [:variable [:template-tag "price"]]
:value "1"}]})))
;;; Note that the actual saved queries just contain `:template-tags` (declaring which parameters this query takes), the
;;;
;;; Info about accepted parameters for a Dashboard lives in `report_dashboard.parameters` and mappings to specific
;;; Cards in `report_dashboardcard.parameter_mappings`
;;;
;;; For a Card in `report_card.parameters` and `report_card.parameter_mappings`
;; Code organization:
(comment
;; application-database level code, chain-filtering code; powers parameter widget UI
metabase.parameters.*
metabase.parameters.core
;; actual **values** (`:parameters`) are injected when executing the query in the Dashboard or Saved Question GUI
metabase.query-processor.card/enrich-parameters-from-card
metabase.query-processor.dashboard/resolve-params-for-query
;; QP middleware for parsing parameters
metabase.query-processor.middleware.parameters
metabase.query-processor.middleware.parameters/substitute-parameters
metabase.query-processor.middleware.parameters/expand-stage
metabase.query-processor.middleware.parameters.native/substitute-native-parameters*
;; (driver implementation)
metabase.driver.sql ; driver/substitute-native-parameters method
;; SQL implementation of driver methods for parameter substitution
metabase.driver.sql.parameters.substitute
;; (USES)
;; legacy MBQL code used to generate parameter substitutions (compiling snippets to inline into SQL) Deprecated
;; because it relies on drivers compiling MBQL 4.
metabase.driver.common.parameters.*
metabase.driver.common.parameters
metabase.driver.common.parameters.parse
metabase.driver.common.parameters.operators
;; (Replacement MBQL 5 namespaces)
;;
;; (Note we can't fully migrate to using these yet until drivers are moved to MBQL 5)
metabase.lib.parameters
metabase.lib.parameters.parse
;; can't go in Lib because they rely on JDBC-only stuff like `java.time`-based temporal types ... should they live
;; here or in `lib-be`??
metabase.query-processor.parameters.*
metabase.query-processor.parameters.operators)
;; Parameters can work in both native queries and MBQL queries
;; In a native query the syntax for a parameter is `{{x}}` or `[[WHERE {x}]]` for an OPTIONAL parameter
(comment
(metabase.driver.common.parameters.parse/parse "SELECT * FROM my_table WHERE x = {{x}};")
(metabase.driver.common.parameters.parse/parse "SELECT * FROM my_table[[ WHERE x = {{x}}]];")
(metabase.lib.parse/parse {} "SELECT * FROM my_table WHERE x = {{x}};")
(metabase.lib.parse/parse {} "SELECT * FROM my_table [[WHERE x = {{x}}]];"))
;;; Note [:stages 0 :native] is now de-templated
;;;
;;; `:parameters` is renamed to `:user-parameters` (why? for reference??)
;; Errors if parameter is missing
(comment
(driver/with-driver :h2
((requiring-resolve 'metabase.query-processor.middleware.parameters/substitute-parameters)
{:lib/type :mbql/query
:lib/metadata meta/metadata-provider
:database (meta/id)
:stages [{:lib/type :mbql.stage/native
:template-tags {"price" {:name "price"
:display-name "Price"
:type :number
:required true}}
:native "SELECT * FROM venues WHERE price = {{price}};"}]})))
;; Optional parameter -- not emitted since parameter is missing
(comment
(driver/with-driver :h2
((requiring-resolve 'metabase.query-processor.middleware.parameters/substitute-parameters)
{:lib/type :mbql/query
:lib/metadata meta/metadata-provider
:database (meta/id)
:stages [{:lib/type :mbql.stage/native
:template-tags {"price" {:name "price", :display-name "Price", :type :number}}
:native "SELECT * FROM venues[[ WHERE price = {{price}}]];"}]})))
;; with a default value
(comment
(driver/with-driver :h2
((requiring-resolve 'metabase.query-processor.middleware.parameters/substitute-parameters)
{:lib/type :mbql/query
:lib/metadata meta/metadata-provider
:database (meta/id)
:stages [{:lib/type :mbql.stage/native
:template-tags {"price" {:name "price"
:display-name "Price"
:type :number
:default 1000}}
:native "SELECT * FROM venues[[ WHERE price = {{price}}]];"}]})))
;; if specified, will override the default value
(comment
(driver/with-driver :h2
((requiring-resolve 'metabase.query-processor.middleware.parameters/substitute-parameters)
{:lib/type :mbql/query
:lib/metadata meta/metadata-provider
:database (meta/id)
:stages [{:lib/type :mbql.stage/native
:template-tags {"price" {:name "price"
:display-name "Price"
:type :number
:default 1000}}
:native "SELECT * FROM venues[[ WHERE price = {{price}}]];"}]
:parameters [{:type :number
:target [:variable [:template-tag "price"]]
:value 2000}]})))
;; Native field filter parameters
(comment
(#'metabase.driver.sql.parameters.substitute-test/expand*
{:native {:template-tags {"checkin_date" {:widget-type :date/all-options
:name "checkin_date"
:type :dimension
:dimension [:field (meta/id :checkins :date) nil]
:display-name "Checkin Date"}}
:query (str "SELECT count(*) AS \"count\", \"DATE\" "
"FROM CHECKINS "
"WHERE {{checkin_date}} "
"GROUP BY \"DATE\"")}
:parameters [{:value "past5days"
:type :date/range
:target [:dimension [:template-tag "checkin_date"]]}]}))
;;; - `:template-tags` contains `:dimension` with a LEGACY field ref.
;;;
;;; - Type is `:dimension` to signify a Field ref
;;;
;;; - `:parameters` includes the type of the value (`:date/range`)
;;;
;;; - `:parameters` `:target` is `:dimension`
;;; fancy parameter types
(comment
metabase.lib.schema.parameter/types ; see also `::parameter` schema
)
(comment
(metabase.driver/with-driver :h2
(metabase.query-processor.parameters.dates/date-str->datetime-range "past5days" :type/DateTimeWithTZ)))
;; MBQL parameters
;; basically the same as native parameters but doesn't require `:template-tags` to be specified
(comment
(#'metabase.query-processor.middleware.parameters.mbql-test/expand-parameters
{:type :query
:database 47001
:query {:source-table 47040, :breakout [[:field 47405 nil]]}
:parameters [{:hash "abc123"
:name "foo"
:value "9223372036854775808"
:type "id"
:target [:dimension [:field 47600 nil]]}]}))
;; SQL Snippets
;; Stored in the NativeQuerySnippet model
;; "named collection" snippet folders
(comment metabase.driver.sql.parameters.substitute-test/substitute-native-query-snippets-test)
;; More stuff worth discussing (?)
(comment metabase.parameters.chain-filter/chain-filter)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment