Created
May 14, 2026 19:31
-
-
Save camsaul/cb22c306332a8e81e342efce353da026 to your computer and use it in GitHub Desktop.
Models in Metabase
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| How Models Work in Metabase: QP & Lib | |
| What is a Model? | |
| A Model is a Card with :type :model (legacy: dataset = true), stored in the report_card table. The key distinguishing fields beyond :dataset-query are: | |
| - :result_metadata — an array of column metadata maps (user-editable: display names, semantic types, FK targets, visibility, etc.) | |
| - :type :model — distinguishes from :question and :metric | |
| --- | |
| Result Metadata: Storage and Merging | |
| result_metadata is the core mechanism that makes Models distinct from plain saved questions. It stores per-column annotations that the user can edit in the Model editor. | |
| When a Model's query runs, results_metadata middleware (src/metabase/query_processor/middleware/results_metadata.clj) records fresh metadata via record-metadata!. For Models specifically, this is merged with existing user edits rather than overwriting them — | |
| implemented in infer-metadata-with-model-overrides in the card metadata namespace. | |
| Lib-side, lib.card/merge-model-metadata (src/metabase/lib/card.cljc:224) controls which Model-authored keys survive a re-inference: | |
| ;; Keys preserved from the Model's result_metadata when merging | |
| (def model-preserved-keys | |
| [:description :display-name :semantic-type :fk-target-field-id :settings :visibility-type]) | |
| The :native-model? flag also preserves :id for native SQL models, enabling FK remapping even on native queries. | |
| --- | |
| Using a Model as a Source: Source Card Resolution | |
| When a query stage has :source-card <id>, the fetch-source-query middleware (src/metabase/query_processor/middleware/fetch_source_query.clj) resolves it: | |
| 1. Fetches the Card — retrieves it from the metadata provider, validates it's the same database | |
| 2. Splices the Card's query stages into the parent query (resolve-source-cards-in-stage) | |
| 3. Attaches :lib/stage-metadata — the Model's result_metadata becomes the column metadata for the spliced-in stage, so downstream stages see the Model's curated column types/names rather than raw DB types | |
| 4. Sets flags on the stage: | |
| - :source-query/model? true | |
| - :source-query/native-model? true (if the Model's underlying query is native SQL) | |
| 5. Recursively resolves nested Models (Models using other Models as sources), with circular dependency detection via weavejester.dependency and a max depth of 50 | |
| The net effect: a query using a Model as its source treats the Model's result_metadata as the schema, not the underlying table schema. | |
| --- | |
| Lib (MLv2) Handling | |
| Lib handles Models in a few places: | |
| lib.card/card-returned-columns is the main entry point for getting what columns a Card exposes. For Models, it calls merge-model-metadata to layer the user-curated metadata on top of the query-computed columns. | |
| lib.metadata.result_metadata/merge-model-metadata (src/metabase/lib/metadata/result_metadata.cljc:367) is the QP-side counterpart, used during result annotation. When :metadata/model-metadata is present in the query info, it merges the Model's column metadata into the | |
| returned columns. | |
| Metadata providers (src/metabase/lib/metadata.cljc) cache Card metadata and expose it to Lib query building. Models are retrieved the same way as Questions here — the :type :model distinction matters more at the annotation and source-card-resolution layers. | |
| --- | |
| Running a Model's Own Query | |
| When executing a Model directly (not as a source for another query), query-for-card (src/metabase/query_processor/card.clj:90) does Model-specific work: | |
| - Parameter insertion: Parameters added by the frontend are pointed to the last stage of the Model's query (via point-parameters-to-last-stage). This prevents parameters from leaking into intermediate stages of a multi-stage Model query. Questions and Metrics pass | |
| parameters through transparently. | |
| - :metadata/model-metadata attachment: process-query-for-card attaches the Card's result_metadata to the query's :info map, along with :metadata/own-model-query? true — this flag tells the annotate middleware that it is processing the Model's own output, so | |
| user-edited column names/types should be preserved. | |
| --- | |
| Model Persistence (Materialized Views) | |
| Models can optionally be materialized into physical tables. The persisted_cache utility (src/metabase/query_processor/util/persisted_cache.clj) checks whether a persisted version is usable: | |
| - persisted-info.active == true and state == "persisted" | |
| - The stored query hash matches the current query (no changes since last materialization) | |
| - Metadata definition matches (no user edits to result_metadata since last run) | |
| If valid, fetch-source-query inserts :persisted-info/native into the stage metadata, and the downstream persistence middleware substitutes the Model's query with a SELECT * FROM <persisted_table> native query. This is transparent to the rest of the pipeline. | |
| --- | |
| QP Middleware Order (Relevant Pieces) | |
| fetch-source-query ; resolves :source-card → splices stages + attaches Model metadata | |
| └─ persistence ; substitutes persisted table if valid | |
| parameters ; inserts parameters (Model-aware: targets last stage) | |
| annotate ; expected-cols → merges :metadata/model-metadata | |
| results-metadata ; records metadata back; blends with user edits for Models | |
| --- | |
| Key Files | |
| ┌──────────────────────────────┬────────────────────────────────────────────────────────────────┐ | |
| │ Purpose │ File │ | |
| ├──────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ Source card resolution │ src/metabase/query_processor/middleware/fetch_source_query.clj │ | |
| ├──────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ Model metadata merging (Lib) │ src/metabase/lib/card.cljc │ | |
| ├──────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ Result metadata recording │ src/metabase/query_processor/middleware/results_metadata.clj │ | |
| ├──────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ Card QP entry point │ src/metabase/query_processor/card.clj │ | |
| ├──────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ Result annotation │ src/metabase/query_processor/middleware/annotate.clj │ | |
| ├──────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ Persistence check │ src/metabase/query_processor/util/persisted_cache.clj │ | |
| ├──────────────────────────────┼────────────────────────────────────────────────────────────────┤ | |
| │ QP-side metadata merging │ src/metabase/lib/metadata/result_metadata.cljc │ | |
| └──────────────────────────────┴────────────────────────────────────────────────────────────────┘ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment