Update E2E test timings with averaged data
Collected timing data from 7 days of test runs.
All specs with timing data were included (simple averages).
🤖 Generated automatically by update-e2e-timings workflow
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Actions <actions@github.com>
* comment out all occurances of bleeding edge to see which ones are actually needed
* default to pro-self-hosted
* enable transforms where needed
* final adjustments?
* BOT-1321 BOT-1230 Switch to AiUsageLog for token usage reporting in metabot-stats
The AiUsageLog table includes usage not only for chat conversations, but non-chat LLM usage like the example question
generator (a.k.a suggested prompts), so use it for reporting metabot-stats metering values to harbormaster.
This also has the advantage that we use the same table both for billing and ai usage limits enforcement.
* Add some test of example question generator token usage in metabot-stats
* Move metabot-stats tests into EE
lychee docs --config ./.lychee/config.toml --offline
Issues found in 1 input. Find details below.
[docs/embedding/guest-embedding.md]:
[ERROR] /metabase/docs/embedding/guest-embedding.md#to-turn-off-a-locked-parameter-pass-an-empty-array-as-its-value | Cannot find fragment: Fragment not found in document. Check if fragment exists or page structure
* correctly analyze native queries
need to include aliases in column names. consider `select f.name as
field_name, t.name as table_name ...`. The columns referenced are both
`name`, but because of the aliases (field_name, and table_name) they are
disambiguated. The bug is we were using name and not the selected name
* fix sql wrapper issues:
```
-- PASS before fix, PASS after
SELECT datum FROM (SELECT 1 AS datum) DQ
-- FAIL before fix (false positive: missing-column "datum"), PASS after
SELECT datum AS b FROM (SELECT 1 AS datum) DQ
-- FAIL before fix (same bug, multiple references), PASS after
SELECT datum AS b, datum - 1 AS d FROM (SELECT 1 AS datum) DQ
-- Real-world case: dim_dates transform had 1 false positive error, now 0
SELECT TO_CHAR(datum, 'yyyymmdd')::INT AS date_dim_id,
datum AS date_actual,
EXTRACT(EPOCH FROM datum) AS epoch,
datum - DATE_TRUNC('quarter', datum)::DATE + 1 AS day_of_quarter
FROM (SELECT '2000-01-01'::DATE + SEQUENCE.DAY AS datum
FROM GENERATE_SERIES(0, 20000) AS SEQUENCE (DAY)) DQ
-- Real-world case: self_hosted_daily_stats had 47 false positive errors, now 0
-- (CTE-defined columns like latest_month_above_goal_alerts were flagged
-- as missing on the source table)
WITH stats AS (
SELECT ..., SUM(CASE WHEN x THEN v END) AS latest_month_alerts, ...
FROM source_table
)
SELECT ls.latest_month_alerts -- was falsely flagged as missing
FROM stats ls
```
issues propagating out from ctes where there was an alias involved
* skip validation for queries with table-type template tags
Queries like `SELECT * FROM {{table_one}}` where `table_one` is a
table-type template tag can't be meaningfully validated — the actual
table (and therefore its columns) is dynamic and unknown at check time.
`native-result-metadata` already skipped these, but `validate-native-query`
did not, causing a syntax error when the unresolved table-id path vector
(e.g. `["DB" "public" "my_table"]`) was substituted into the SQL as a
placeholder name.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* move function before declaration
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add agent-lib module for structured MBQL program repair, validation, and evaluation
Introduces the `agent-lib` module (team: Metabot), which provides the
core pipeline for processing structured MBQL programs produced by the AI
agent: deterministic repair/normalization of malformed operator forms,
schema validation, and safe evaluation against a trusted runtime.
Key components:
- repair: idempotent normalization of operator forms, stage boundaries,
and top-level operations
- validate: schema and semantic validation with structured error reporting
- eval: sandboxed evaluation of validated programs against lib/MBQL
- runtime: trusted bindings exposing lib query-introspection helpers
- capabilities/catalog: enumerates available operators, sources, and
field types for the agent
- mbql_integration: bridges agent-lib's structured programs to lib's
MBQL query building API
The module is registered in clj-kondo config with `:uses #{lib util}`
and as a `:friend` of `lib` (for two internal functions not yet in the
public API: `lib.expression/maybe-resolve-expression` and
`lib.ref/expression-ref-name`).
* Wire up agent-lib for MBQL construction, remove synthetic field IDs
Replace the old construct_notebook_query approach (3 query types, synthetic
field IDs like `t42-3`, rigid filter schemas) with agent-lib's unified
program format (`{source, operations}`). Programs use real database field
IDs and support richer capabilities: expressions, joins, post-aggregation
filtering via `append-stage`, and custom calculations.
Key changes:
- construct.clj: rewritten to call `agent-lib/evaluate-program` instead of
the old filters.clj pipeline. Tool now accepts `{program: {source, operations}}`
instead of `{query: {query_type, source, filters, ...}}`
- agent-api construct-query endpoint: translates the existing REST request
format into programs and evaluates via agent-lib
- Entity details, field stats, table utils: all switched from synthetic
index-based field IDs (`t42-3`) to real database field IDs (integers)
- filters.clj: deleted — no longer needed by any caller
- Prompts: construct_notebook_query.md completely rewritten with program
format documentation, operations reference, and usage patterns
- System prompts: removed outdated limitation references (no joins, no
field calculations)
* Refine construct_notebook_query tool schema and fix metric breakouts
Tool schema now uses the source_entity/context-ref pattern: `source_entity`
at the top level selects the primary data source, while the program always
uses `{"type": "context", "ref": "source"}`. This separates entity selection
from program structure and enables `referenced_entities` for additional
context.
Strip default metric breakouts when the agent provides explicit ones.
Metrics include a default time dimension breakout in their query definition.
When the program contains explicit breakout operations, these now replace
(not augment) the metric defaults via `lib/remove-all-breakouts`.
Include `display` in question URL hash so the frontend renders the correct
visualization type (line, bar, pie, etc.) instead of defaulting to table.
Rewrite construct_notebook_query.md prompt with comprehensive rules:
context-ref pattern, single-transform-per-operation rule, temporal
extraction helpers, multi-value filter guidance, breakout ordering rules,
grouped ratio examples, join retry guidance, and field projection rules.
* Fix evaluation context and time-interval keyword coercion
Evaluation context improvements:
- Map "model" source type to "dataset" (matching agent-lib's
context-allowed-ids which checks for "card" or "dataset")
- Compute surrounding tables for all source types, not just tables,
enabling join context for models/questions/metrics
- Compute and pass measure-ids to the evaluation context, enabling
measure ID validation in agent-lib's operator validator
Keywordize time-interval amount values ("current", "last", "next")
before passing to lib/filter, which expects keyword enum values
(:current, :last, :next). Integer amounts pass through unchanged.
* Fix tests
* Fix Eastwood warnings, update tests for real field IDs and new tool schema
* Fix cljfmt indentation in entity_details.clj
* Temp fix for transform table issues
* Build Uberjar on push to branch
* Tighten metabot prompt guidance for MBQL queries
- Add MBQL intro sentence to the construct_notebook_query tool prompt
- Drop references to the retired metabot query DSL and the redundant
`relative-date` alias
- Correct the permission-error join guidance: implicit refs hit the same
permission check, so surface the error instead of retrying
- Add a heuristic for choosing pie-chart vs calculated-percentage expressions
in the percentage_question example
- Update the agent_api reference to describe field/segment/measure refs
using the actual request-body keys instead of internal MBQL forms
* Use shared ::field-id schema in agent_api
Defines `::field-id` reusing `::lib.schema.id/field` (pos-int? with API
decoding) and a string branch for expression/aggregation aliases, then
replaces the repeated `[:or :int :string]` field-id shapes across request
and response schemas with that single alias.
Also tightens the `body->program` helper by removing blank lines inside
the `let`/`cond->` per the no-blank-lines-in-def-forms style rule and
fixes the `construct-query*` docstring to say MBQL 5 rather than pMBQL.
* Use vars for agent-lib capability bindings
Converts every catalog `:binding lib/foo` to `:binding #'lib/foo` so that
REPL inspection of a capability entry shows the var name instead of an
opaque function object.
Also aligns all seven catalog files on the standard `(def capabilities
"docstring" …)` form instead of the `^{:doc …}` metadata shape, adds a
namespace docstring to `catalog/aggregation.clj` explaining that the
catalog is the runtime source of truth while `construct_notebook_query.md`
is hand-curated prompt guidance, and brings the join / top-level prompt
notes in line with the corrected permission-error fallback guidance.
* Document the unsupported/repair split in agent-lib
Adds a namespace docstring to `capabilities.unsupported` explaining that
this catalog is prompt-time guidance distinct from the runtime repair
layer: some entries (temporal extractions, `count-if`, `variance`, etc.)
are silently rewritten before validation so the agent never sees an
error, while others (`avg-where`) have no safe mechanical rewrite and
only surface as retry guidance when the model gets it wrong.
* Simplify agent-lib coercion helpers
Rewrites `quarter-label->number` as a plain `case` dispatch over the
trimmed, upper-cased input instead of threading through a regex, and
makes `direction-string?` actually return a boolean so it matches its
`?`-suffixed name contract. Adjusts the direction-string test to assert
`true?`/`false?` instead of `some?`/`nil?`.
* Derive agent-lib aggregation operator names from lib hierarchy
Replaces the hard-coded set of aggregation helper names in
`literals/aggregation-operator-names` with a derivation from
`(descendants @lib.hierarchy/hierarchy ::aggregation-clause-tag)` so new
aggregation clauses added to the lib schema are picked up automatically.
Filters out `:aggregation` (the abstract wrapper tag) and `:offset`
(a windowed aggregation that also appears as an expression — keeping it
out of this set preserves the existing `aggregation-form?` semantics so
the repair layer still defers offset-inside-expression to lib.core).
* Inline private structured-helper-symbols in agent-lib syntax
`structured-helper-symbols` was a private single-use alias for
`capabilities/helper-symbols`. Inlining it at its one call site in
`recognized-operator-symbols` removes the indirection without changing
behavior. `top-level-operation-symbols` is left as-is because it is
consumed by other namespaces.
* Increase max allowed count for modules triggering drivers tests
* Make agent-lib operator repair idempotent for degenerate args
Several binary/ternary nested operators (`in`, `not-in`, `=`, `!=`, `is`,
`is-not`, `between`, `with-temporal-bucket`, `percentile`,
`with-join-conditions`) used to unconditionally destructure their args
and rebuild the form at a fixed arity, so a degenerate input like
`["in"]` would be padded to `["in" nil nil]`. When that same operator
was embedded inside `case`, the case normalizer would then classify the
3-element form as a `[pred then else]` triple and rewrap it into a
branches vector on the next pass — adding another `nil` each time
`repair-program` was called.
The `repair-is-idempotent` fuzz property caught this intermittently in
CI (~20% flake rate); a 10k-iteration local run now passes cleanly.
Each affected operator now returns its args unchanged when it does not
have enough of them to form a canonical shape, leaving rejection to
downstream validation instead of synthesizing a malformed canonical
form. A deterministic regression test pins the `["case" [op]]` case
for every affected operator.
* Scope agent-lib runtime metadata to entities the program references
Walk the validated program for referenced table/field/card/metric/measure
ids and load only those tables (and their fields) into the runtime, instead
of materializing every table in the database on each evaluate-program call.
Field/card/measure ids are resolved via lib.metadata/bulk-metadata so each
class collapses to a single SQL query regardless of count.
* Change how docstrings are attached to defs
* Drop unused agent-lib unsupported-rewrite-catalog
* Expose agent-lib programs directly in agent_api v2 endpoints
Replace the legacy POST /v1/construct-query and /v1/query endpoints —
which spoke a flat filters/aggregations/group_by body shape that an
in-process adapter translated into an agent-lib program — with new
POST /v2/construct-query and /v2/query endpoints whose request body is
the agent-lib program itself. The flat shape only ever exposed the
lowest-common-denominator subset of agent-lib's capabilities; programs
unlock joins, expressions, multi-stage queries, and the full operator
catalog for any HTTP or MCP client. Repair and validation continue to
happen inside agent-lib, so the loose-schema benefit of accepting raw
programs is free.
The request schema reuses metabase.agent-lib.schema/program-schema
(now re-exported from metabase.agent-lib.core), with a thin restriction
that the top-level source must be table/card/dataset/metric — context
and nested program sources are reserved for in-process callers. The
new endpoints derive the source-entity for evaluation from the program
itself via a new program-source->source-entity helper on
metabase.metabot.tools.construct, leaving execute-program's signature
unchanged so the slackbot caller is untouched. MCP picks up the new
shape automatically because its tool manifest is generated from
defendpoint metadata.
* Keep agent_api v2 program request schema MCP-friendly
The :and+:fn wrapper around agent-lib/program-schema confused the MCP
tools manifest's flatten-root-schema pass — it couldn't extract a clean
object root for construct_query and query, so their inputSchemas lost
their "type": "object" field and the tools became unusable through MCP.
Drop the :fn predicate from ::program-request and instead enforce the
top-level source-type restriction imperatively inside
evaluate-program-for-execution. The schema is now just program-schema,
which the manifest flattens cleanly into a single object. Update the
MCP execute-query test to send the v2 program shape.
* Make agent responses less chatty during work
Strip step-by-step narration from system prompts so the agent works
silently through tool calls, then presents results with a brief
query-translation sentence explaining how the question mapped to the query.
* Clean up prompt contradictions, redundancy, and misplaced guidance
- Fix identity/tone contradictions with brevity style ("conversational" →
"concise, action-oriented", "expert data analyst" → "query builder")
- Remove banned narration pattern from example text
- Remove redundant "Data Source Types" section from NLQ prompt
- Scope proactive helpfulness to ambiguous/partial results only
- Move percentage question guidance from <why> blocks in examples to
proper query construction sections in both prompts
- Fix chatty entity linking examples in NLQ prompt
* Fix tests
* Fix modules config
* Restore base_type and related field metadata in tool result columns
Keep the real-field-ID redesign and new :type enum, but restore
:base_type, :effective_type, :semantic_type (qualified), and
:coercion_strategy.
Also update construct-notebook-query-tool test to use the new
{:source_entity, :program} schema after the rebase.
* Remove model/Transform cleanup hotfix from database.clj
* feat: Add goal line to waterfall chart
* adjusting e2e test
* add storybook test for static viz
* adding loki screenshot, finxing e2e
---------
Co-authored-by: Brad W <bradleo99@gmail.com>
* human and json output
* drop --format flag and JSON output
Removed for now — the human format is what we actually want, and we'll
add structured output (JSON or otherwise) when there's a real consumer
asking for it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* remove classloader/plugins calls
These belong to the driver-loading PR (#72732), not this output-formatting
branch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Preserve original cause in serdes load errors when app DB tx is aborted
path-error-data does a SELECT via serdes/load-find-local to populate
:local-id on the error map. When it is called from the catch block in
load-one!, the outer load transaction has often already been poisoned by
the real failure, so that SELECT throws "current transaction is aborted,
commands ignored until end of transaction block" and prevents ex-info
from being constructed. The original cause is lost and support engineers
see the aborted-tx symptom instead of the actual SQL error.
Wrap the lookup so a failure leaves :local-id nil and still lets us
chain the real cause.
* Update enterprise/backend/src/metabase_enterprise/serialization/v2/load.clj
Co-authored-by: Edward Paget <edpaget@users.noreply.github.com>
* Don't run starting from empty app DB
---------
Co-authored-by: Edward Paget <edpaget@users.noreply.github.com>
* concise format
shaves about a minutes off!
before:
```
clj -M:dev:drivers:ee -m metabase.core.bootstrap --mode checker --export ~/projects/work/stats-remote-sync --schema-dir /tmp/metadata/metadata/databases
took 1m55s
```
now with new format:
```
clj -M:dev:drivers:ee -m metabase.core.bootstrap --mode checker --export ~/projects/work/stats-remote-sync --schema-dir /Users/dan/projects/work/exports-root/metadata/new-format
took 58s
```
new format is one JSON file per database, loaded entirely into memory:
```javascript
{
"name": "Sample Database",
"engine": "h2",
"tables": [
{
"name": "PRODUCTS",
"display_name": "Products",
"database_schema": "PUBLIC",
"fields": [
{"name": "ID", "base_type": "type/BigInteger", "semantic_type": "type/PK", "database_type": "BIGINT"},
...
]
},
...
]
}
```
The CLI auto-detects the format: if --schema-dir contains .json files,
it uses concise; otherwise falls back to the serdes YAML format.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* document SchemaSource and AssetsSource contracts
Each resolve-* method now documents the exact map shape it should return,
keys marked as required vs optional. The ns docstring lays out the broader
contract: sources return snake_case maps matching the serialized form,
provider.clj does all conversion to lib metadata format (kebab-case,
keywords, defaults, ID resolution, MBQL normalization).
Documenting this now (alongside adding the concise format) so future source
authors — REST API, git, LLM, app DB via toucan2 — know exactly what shape
to produce.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* concise format: single file with three flat lists
The concise schema format is now a single JSON file with three flat lists
keyed by integer IDs:
{"databases": [{"id": 1, "name": "Sample Database", "engine": "h2"}, ...],
"tables": [{"id": 3, "db_id": 1, "name": "PRODUCTS", "schema": "PUBLIC"}, ...],
"fields": [{"id": 12, "table_id": 3, "name": "ID",
"base_type": "type/BigInteger", "semantic_type": "type/PK",
"database_type": "BIGINT"}, ...]}
At load time we join tables.db_id → databases.id and fields.table_id →
tables.id to build path-keyed indexes, so the rest of the checker still
sees portable references like [db-name schema table].
Format selection is now explicit via --schema-format (serdes|concise)
rather than guessed. Default remains serdes for backwards compatibility.
java -jar metabase.jar --mode checker \
--export /path/to/export \
--schema-dir /path/to/metadata.json \
--schema-format concise
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* loudly error when --schema-dir doesn't match --schema-format
Pointing --schema-format serdes (the default) at a single JSON file used
to silently produce a schema source with zero databases, which then
showed up as a flood of unresolved-reference errors. Same in reverse for
concise format pointed at a directory.
Now the CLI checks the path shape against the format and exits with a
helpful error suggesting the right --schema-format value.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* rename format/concise to format/concise-schema
Mirrors the serdes-schema vs serdes-assets split — the namespace name now
makes clear which side of the protocol divide it implements.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* use cheshire instead of clojure.data.json
clojure.data.json is available at dev time but not bundled into the
production jar, causing a FileNotFoundException when running the checker
from a built artifact. cheshire is a hard dep already on the classpath.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* use metabase.util.json instead of cheshire directly
Project convention is to go through metabase.util.json, which wraps
cheshire and adds custom encoders/decoders. Stream-decode via reader
to avoid slurping the whole file into a string before parsing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Updates the top-level pin from 1.82 to 1.84 and adds matching pins to
the Snowflake and Databricks driver deps.edn files so the bundled
driver JARs pick up the patched version during security scans.
* Downgrade all netty pins to 4.2.9.Final
Netty 4.2.12 introduced a breaking change in HttpObjectEncoder that
causes AWS SDK netty-nio-client to fail with "unexpected message type:
LastHttpContent" errors. Revert to 4.2.9.Final which is compatible
with the AWS SDK while still being in the 4.2.x line.
Closes: #72532
* Switch Athena driver from uber-jar to thin jar with pinned deps
Use athena-jdbc-thin instead of the bundled uber-jar so transitive
deps are visible to security scanners. Pin netty at 4.2.12.Final to
match root deps.edn. Add athena-streaming from our private maven
repo (not available on Maven Central).
Update athena.bb to download and publish both the existing
with-dependencies uber-jar (athena-jdbc) and the lean thin jar
(athena-jdbc-thin) from AWS's Athena downloads bucket. The thin
jar enables pinning transitive deps for security scanning.
Fixes QUE2-97.
Row charts were passing formatting strings like "(empty)" as the `value`
in the drill-thru context map, rather than the proper value `null`. That
breaks at least the `underlying-records` drill and perhaps others too,
when the formatted string is different from the value's usual
representation.
* rename serdes to serdes assets in prep of split
* split serdes source into serdes-schema and serdes-assets
The previous commit renamed serdes to serdes-assets. This commit
extracts the schema (database/table/field) resolution into its own
serdes-schema namespace and source type (SerdesSchemaSource), keeping
assets (cards, dashboards, segments, etc.) in serdes-assets.
This is a no-op refactor — the checker behaves identically. The split
prepares for a new schema format that uses a single file instead of
the 500k-file directory tree.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* kondo lints
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Move Question.alertType to utils/notifications as getAlertType
* Move metabase/utils/notifications to metabase/notifications
* Disable alert message in embedding sdk differently
* Fix type
* Add question instructions into read_resource template
* Extend fetch to question
* support question entity in tools/field-values
* extend get-table-details to work also with questions
* refactor get-table-details usage
* refactor get-table-details tests
* Adjust fetch-model-or-card-resource to work with questions
* drop old get-table-details
* Adjust tests
* Extend question->xml with fields support
* Update llm representations question template with fields
* whitespace
* Update /v1/table/:id
* Update src/metabase/metabot/tools/entity_details.clj
Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
* Adjust test
---------
Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
* Show tooltip on disabled databases when creating transforms
* Fix issues / Use better approach
* Add e2e tests / Remove unneeded prop
* Address review comments
* Fix test expectations to match validateDatabase messages
* Fix e2e test assertions to match validateDatabase messages
* Add installation of Astral and Python3 pip
If you build Metabase with Docker it will fail unless uv and pip are installed
* Update Dockerfile to remove python3-pip installation
Removed installation of python3-pip from Dockerfile.
Pin jackson-core 2.21.2 in snowflake, bigquery, and redshift drivers
Each driver pulls a vulnerable jackson-core transitively:
- snowflake: 2.18.4.1 via snowflake-jdbc-thin
- bigquery: 2.18.2 via google-cloud-bigquery
- redshift: 2.17.2 via aws-java-sdk-core
**Simplify `result_metadata` serialization for serdes export**
For non-models, drop `result_metadata` entirely on export (it is recomputed by the QP on import anyway). For models, export only `:name` and the keys in `lib/model-preserved-keys` (used to match and apply model column overrides), discarding internal `lib/` and `qp/` properties that are unused after import. This reduces export noise and avoids needing to generate full metadata when editing cards externally.
* Fix create chart links
* Fix chart extraction
* Use pseudo-card fn
* Adjust test
* Integration test
* Instruction formatting
* Adjust chart link resolution
* Adjust test
* Adjust test
* Update test
* Adjust test
* Set effective_type for sample database fields (GHY-3367)
Every field entry in resources/sample-content.edn was missing :effective_type,
so CreateSampleContentV2 inserted metabase_field rows with null effective_type
on fresh installs. Default effective_type to match base_type for all 71 fields.
* Test that sample-db fields have non-null effective_type (GHY-3367)
Dashboard subscriptions store Slack channels by name (e.g. "#alerts"). Slack channel IDs are immutable, but names are not. Renaming a channel breaks all subscriptions targeting it with just the name.
This PR fixes this by storing the immutable Slack channel ID alongside the display name at subscription creation time. At send time, we prefer the ID over the name - this maintains backwards compatibility with legacy subscriptions. The UI remains entirely name-based - users never see or interact with channel IDs.
it's internal but we don't need to use field access on the type. I don't
want to export the type, etc. Just make a debug protocol and this won't
be exported so it's not for anyone else.
Before:
```
serdes-test=>
Reflection warning, metabase_enterprise/checker/format/serdes_test.clj:286:20 - reference to field schema_model can't be resolved.
Reflection warning, metabase_enterprise/checker/format/serdes_test.clj:292:20 - reference to field schema_model can't be resolved.
Reflection warning, metabase_enterprise/checker/format/serdes_test.clj:298:20 - reference to field schema_model can't be resolved.
Reflection warning, metabase_enterprise/checker/format/serdes_test.clj:315:20 - reference to field schema_model can't be resolved.
nil
```
After:
```
serdes-test=> (clojure.test/run-tests)
Testing metabase-enterprise.checker.format.serdes-test
Ran 28 tests containing 87 assertions.
0 failures, 0 errors.
{:test 28, :pass 87, :fail 0, :error 0, :type :summary}
```
fetch-or-create-support-user! now sets is_superuser=true on all paths
(new user, reactivation, and existing active user). On grant revocation,
is_superuser is set back to false. This ensures the support user has
full admin access as promised by the grant access dialog.
Fixes: UXW-3725
* Checker module
```
❯ clj -X:dev:test:ee:ee-dev :module enterprise/checker
Finding tests took 80.0 ms.
Running 10 tests
Running before-run hooks...
10/10 100% [==================================================] ETA: 00:00
Ran 10 tests in 0.645 seconds
39 assertions, 0 failures, 0 errors.
{:test 10, :pass 39, :fail 0, :error 0, :type :summary, :duration 644.51825, :single-threaded 10}
Ran 0 tests in parallel, 10 single-threaded.
Finding and running tests took 762.0 ms.
```
The number of public functions here is too high but will come down soon.
```
❯ tree export-dir -d -L 2
export-dir
├── collections
│ ├── BFCwHpVZqKMKqth3wVbGF_automatically_generated_dashboards
│ ├── HyB3nRtqb7pBPhFG26evI_examples
│ ├── cards
│ ├── hFKj55TbZN8uDDTNPiZgf_repro1
│ ├── transforms
│ ├── uj8O1_VYi4rPa4Y8ZGb_S_demo_day
│ └── wUkEfoXgg3dBTNCN2egsh_native_queries
├── databases
│ ├── Internal Metabase Database
│ ├── Sample Database
│ └── pg sample-dataset
├── python-libraries
└── transforms
├── transform_jobs
└── transform_tags
17 directories
```
and
```
❯ time clj -X:dev:drivers:ee nocommit.ci/cli :export '"export-dir"' :output '"/tmp/from-cli-with-error.txt"'
Warning: environ value /Users/dan/.sdkman/candidates/java/current for key :java-home has been overwritten with /Users/dan/.sdkman/candidates/java/25.0.2-tem
2026-03-13 14:26:57,899 INFO metabase.util :: Maximum memory available to JVM: 8.0 GB
2026-03-13 14:27:04,139 INFO util.encryption :: Saved credentials encryption is DISABLED for this Metabase instance. 🔓
For more information, see https://metabase.com/docs/latest/operations-guide/encrypting-database-details-at-rest.html
clj -X:dev:drivers:ee nocommit.ci/cli :export '"export-dir"' :output 34.10s user 1.68s system 228% cpu 15.634 total
```
the output is a big edn map:
```clojure
{"fdkSuB8zWEbjEq_vWMaI-"
{:card-id 1,
:name "Heavy-Duty Silk Chair trend",
:entity-id "fdkSuB8zWEbjEq_vWMaI-",
:file
"export-dir/collections/HyB3nRtqb7pBPhFG26evI_examples/cards/fdkSuB8zWEbjEq_vWMaI-_heavy_duty_silk_chair_trend.yaml",
:refs
{:tables ["Sample Database.PUBLIC.ORDERS"], :fields ["Sample Database.PUBLIC.ORDERS.CREATED_AT"], :source-cards []},
:unresolved nil,
:bad-refs #{}}
...
;; card with unknown field
"rvxR7dn4wrKT9_7MHhy7i"
{:card-id 222,
:name "Distinct User ID",
:entity-id "rvxR7dn4wrKT9_7MHhy7i",
:file "export-dir/collections/uj8O1_VYi4rPa4Y8ZGb_S_demo_day/cards/rvxR7dn4wrKT9_7MHhy7i_distinct_user_id.yaml",
:refs
{:tables
["pg sample-dataset.public.products" "pg sample-dataset.public.people" "pg sample-dataset.public.demo_transform"],
:fields [],
:source-cards []},
:unresolved [{:type :field, :path ["pg sample-dataset" "public" "demo_transform" "user_iddddd"]}],
:bad-refs #{}}
;; card with unknown table:
"srEGaobVwDUI177CowuLy"
{:card-id 226,
:name "Orders + People + Products model for real by Total",
:entity-id "srEGaobVwDUI177CowuLy",
:file
"export-dir/collections/uj8O1_VYi4rPa4Y8ZGb_S_demo_day/cards/srEGaobVwDUI177CowuLy_orders___people___products_model_for_real_by_total.yaml",
:refs
{:tables ["pg sample-dataset.public.begone" "pg sample-dataset.public.products" "pg sample-dataset.public.people"],
:fields ["pg sample-dataset.public.begone.total"],
:source-cards []},
:unresolved [{:type :table, :path ["pg sample-dataset" "public" "begoneeee"]}],
:bad-refs #{}}
```
* handle foreign key refs in metadata, schemaless dbs like sqlite
checker=> (write-results! (check "export-dir-2") "/tmp/realized.txt")
Results written to: /tmp/realized.txt
```
❯ clj -X:dev:test:ee:ee-dev :module enterprise/checker
Ran 14 tests in 0.936 seconds
58 assertions, 0 failures, 0 errors.
{:test 14, :pass 58, :fail 0, :error 0, :type :summary, :duration 936.312333, :single-threaded 14}
Ran 0 tests in parallel, 14 single-threaded.
Finding and running tests took 1.1 s.
All tests passed.
```
* yaml structural checks:
```shell
❯ clj -X:ee metabase-enterprise.checker.structural/cli :export '"/Users/dan/projects/work/yaml-checked-files-v1/exports/sqlite-based"'
Warning: environ value /Users/dan/.sdkman/candidates/java/current for key :java-home has been overwritten with /Users/dan/.sdkman/candidates/java/25.0.2-tem
2026-03-17 17:16:18,363 INFO metabase.util :: Maximum memory available to JVM: 8.0 GB
Structural validation complete:
Valid files: 444
Invalid files: 1
Errors:
/Users/dan/projects/work/yaml-checked-files-v1/exports/sqlite-based/collections/HyB3nRtqb7pBPhFG26evI_examples/cards/-1mRtcrAiZoe-GjYrQFHX_order_value_distribution_by_category.yaml (card)
#ordered/map ([:name ["missing required key"]])
```
* report errors
```
❯ clj -X:ee metabase-enterprise.checker.structural/cli :export '"/Users/dan/projects/work/yaml-checked-files-v1/exports/sqlite-based"'
Structural validation complete:
Valid files: 444
Invalid files: 1
Errors:
/Users/dan/projects/work/yaml-checked-files-v1/exports/sqlite-based/collections/HyB3nRtqb7pBPhFG26evI_examples/cards/-1mRtcrAiZoe-GjYrQFHX_order_value_distribution_by_category.yaml (card)
- Missing required key 'name' - found 'nname' which may be a typo
```
```
❯ clj -X:dev:test:ee:ee-dev :module enterprise/checker
Running tests in ("enterprise/backend/test/metabase_enterprise/checker")
Looking for test namespaces in directory enterprise/backend/test/metabase_enterprise/checker
Finding tests took 208.0 ms.
Running 22 tests
Running before-run hooks...
22/22 100% [==================================================] ETA: 00:00
Ran 22 tests in 0.948 seconds
104 assertions, 0 failures, 0 errors.
{:test 22, :pass 104, :fail 0, :error 0, :type :summary, :duration 947.840541, :single-threaded 22}
Ran 0 tests in parallel, 22 single-threaded.
Finding and running tests took 1.2 s.
All tests passed.
Running after-run hooks...
```
* Start to extract format from checker; add readme
ideally the runner persists and we can change the backends that power
it. Now that things are largely working on serdes, i'm extracting the
specifics about the format from the checker stuff so we can begin to
swap them out to simpler formats
* Refactor checker to use MetadataSource protocol
Introduce protocol-based architecture for the checker module:
- Add `source.clj` with `MetadataSource` protocol for entity resolution
- Refactor `checker.clj` to use source protocol instead of global state
- Session state is now per-check, not global (no more reset-state!)
- Source and enumerators passed in, enabling different backends
- Update `format/serdes.clj` to implement MetadataSource
- Add `make-source`, `check`, `check-cards`, `make-enumerators`
- High-level API now lives here, not in checker.clj
- Update tests to use new API
- In-memory sources via `reify` for edge case testing
- No fixture cleanup needed between tests
- Add DESIGN.md documenting two-protocol architecture:
- EntityResolver: "does this reference exist?"
- FolderLayout: "where are files in this directory?" (future)
- Update README.md with current architecture and usage
This prepares for supporting multiple file formats (serdes, simple LLM-friendly)
and multiple backends (files, memory, app-db) with the same checker core.
```
❯ clj -X:dev:test:ee:ee-dev :module enterprise/checker
Running tests with options {:mode :cli/local, :namespace-pattern #"^(?:(?:metabase.*)|(?:hooks\..*))", :exclude-directories [".clj-kondo/src" "classes" "dev" "enterprise/backend/src" "local" "resources" "resources-ee" "src" "target" "test_config" "test_resources"], :test-warn-time 3000, :exclude-tags [:metabot-v3/e2e], :module enterprise/checker, :only ("enterprise/backend/test/metabase_enterprise/checker")}
Running tests in ("enterprise/backend/test/metabase_enterprise/checker")
Looking for test namespaces in directory enterprise/backend/test/metabase_enterprise/checker
Finding tests took 196.0 ms.
Running 22 tests
Running before-run hooks...
22/22 100% [==================================================] ETA: 00:00
Ran 22 tests in 0.976 seconds
110 assertions, 0 failures, 0 errors.
{:test 22, :pass 110, :fail 0, :error 0, :type :summary, :duration 976.301375, :single-threaded 22}
Ran 0 tests in parallel, 22 single-threaded.
Finding and running tests took 1.2 s.
All tests passed.
Running after-run hooks...
```
* proper cli entries
```
❯ clj -M:ee -m metabase.core.bootstrap --mode checker --checker cards --export /Users/dan/projects/work/stats-remote-sync --output /tmp/output.edn
Warning: environ value /Users/dan/.sdkman/candidates/java/current for key :java-home has been overwritten with /Users/dan/.sdkman/candidates/java/25.0.2-tem
2026-03-19 12:27:47,656 INFO metabase.util :: Maximum memory available to JVM: 8.0 GB
2026-03-19 12:27:52,987 INFO util.encryption :: Saved credentials encryption is DISABLED for this Metabase instance. 🔓
For more information, see https://metabase.com/docs/latest/operations-guide/encrypting-database-details-at-rest.html
Results written to: /tmp/output.edn
Card Check Results
==================
Total cards: 2
OK: 0
Errors: 2
Unresolved refs: 0
Issues: 0
Failures:
---------
=== Account [beKpAz4TM1cmVN_0HiAvi] ===
Card ID: 1
UNRESOLVED REFERENCES:
- database: Analytics Data Warehouse
...
```
and structural:
```
❯ clj -M:ee -m metabase.core.bootstrap --mode checker --checker cards --export /Users/dan/projects/work/yaml-checked-files-v1/exports/sqlite-based --output /tmp/output.edn
Results written to: /tmp/output.edn
Card Check Results
==================
Total cards: 83
OK: 83
Errors: 0
Unresolved refs: 0
Issues: 0
```
```
❯ clj -X:dev:test:ee:ee-dev :module enterprise/checker
Running tests with options {:mode :cli/local, :namespace-pattern #"^(?:(?:metabase.*)|(?:hooks\..*))", :exclude-directories [".clj-kondo/src" "classes" "dev" "enterprise/backend/src" "local" "resources" "resources-ee" "src" "target" "test_config" "test_resources"], :test-warn-time 3000, :exclude-tags [:metabot-v3/e2e], :module enterprise/checker, :only ("enterprise/backend/test/metabase_enterprise/checker")}
Running tests in ("enterprise/backend/test/metabase_enterprise/checker")
Looking for test namespaces in directory enterprise/backend/test/metabase_enterprise/checker
Finding tests took 228.0 ms.
Running 22 tests
Running before-run hooks...
22/22 100% [==================================================] ETA: 00:00
Ran 22 tests in 1.076 seconds
110 assertions, 0 failures, 0 errors.
{:test 22, :pass 110, :fail 0, :error 0, :type :summary, :duration 1075.697958, :single-threaded 22}
Ran 0 tests in parallel, 22 single-threaded.
Finding and running tests took 1.3 s.
All tests passed.
Running after-run hooks...
```
* whoops forgot to add new files
* hybrid format concise db schema, serdes assets
* clean up models.serialization
two things we are cleaning up:
1. protocol
there should be a real protocol that we can implement, rather than
knowing which five dynamic vars we need to override/bind a bunch of
dynamic vars, let's have a real protocol and a way to swap it out.
```
;; for yaml
(def ^:private checker-import-resolver
(reify resolve/SerdesImportResolver
(import-fk [_ eid model] (import-fk eid model))
(import-fk-keyed [_ portable model field] (import-fk-keyed portable model field))
(import-user [_ _email] nil) ; checker doesn't resolve users
(import-table-fk [_ path] (import-table-fk path))
(import-field-fk [_ path] (import-field-fk path))))
```
note: future work will put the file system state inside of this and not
as some dynamic var spread throughout the code. But one step at a time.
Now to convert a dataset query we have:
```clojure
(defn- convert-dataset-query
"Convert a dataset query from serdes format, resolving paths to IDs."
[query]
(when query
(let [db-name (:database query)
db-id (when (string? db-name) (resolve-db-name db-name))
query (if db-id (assoc query :database db-id) query)]
(binding [resolve/*import-resolver* checker-import-resolver]
(resolve/import-mbql query)))))
```
instead of this:
```clojure
(defn- convert-dataset-query
"Convert a dataset query from serdes format, resolving paths to IDs."
[query]
(when query
(let [db-name (:database query)
db-id (when (string? db-name) (resolve-db-name db-name))
query (if db-id (assoc query :database db-id) query)]
(binding [serdes/*import-field-fk* import-field-fk
serdes/*import-table-fk* import-table-fk
serdes/*import-fk* import-fk
serdes/*import-fk-keyed* import-fk-keyed]
(serdes/import-mbql query)))))
```
And in the future it will hopefully be something like this:
```clojure
(defn- convert-dataset-query
"Convert a dataset query from serdes format, resolving paths to IDs."
[yaml-store query]
(when query
(let [db-name (:database query)
db-id (when (string? db-name) (resolve-db-name db-name))
query (if db-id (assoc query :database db-id) query)]
(binding [resolve/*import-resolver* (checker-import-resolver yaml-store)]
(resolve/import-mbql query)))))
```
2. not require toucan in the checker.
Before this change, we had to require `metabase.models.serialization`
which brought in `[toucan2.core :as t2]`, models.interface, all kinds of
database specific stuff. We now have the protocol and can implement it
against the yaml store rather than the db. Hopefully startup is faster
now.
```
clj -M:ee -m metabase.core.bootstrap --mode checker --checker cards --export /Users/dan/projects/work/yaml-checked-files-v1/exports/sqlite-based-mixed-versions --output /tmp/mixed.output
Results written to: /tmp/mixed.output
Card Check Results
==================
Total cards: 83
OK: 83
Errors: 0
Unresolved refs: 0
Issues: 0
```
has gone from 11s to 9s. Small wins, but ~10% savings.
Also, it seems like the vast majority of time is spent in stuff we don't
care about which generates these logs:
```
Warning: environ value /Users/dan/.sdkman/candidates/java/current for key :java-home has been overwritten with /Users/dan/.sdkman/candidates/java/25.0.2-tem
2026-03-20 12:15:32,319 INFO metabase.util :: Maximum memory available to JVM: 8.0 GB
```
* clean up, build file interpreter rather than imperative
* lenient mode
writes out a manifest file of fields that are used
* lenient checker
* add serdes workflow and yaml-edit skill files, fix bootstrap fallthrough
Two new Claude skill files for the serdes demo workflow:
- serdes-workflow: export/validate/import pipeline with exact commands
- serdes-yaml-edit: editing YAML with portable refs, self-healing loop,
checker error interpretation, and reference lookup guide
Fix bootstrap.clj to return false when no --mode flag is present,
allowing normal Metabase startup (import/export commands) to proceed.
* allow for separate db schema directory
checker: --schema-dir, --errors-only, sentinel IDs for unresolved refs
--schema-dir lets database schemas live in a separate directory from the
export. The directory is treated as the databases dir directly (contains
db entries, not a databases/ subdirectory).
--errors-only outputs just errors to stdout in a concise format for LLM
consumption — no summary, no status lines.
Unresolved field/table refs now get sentinel IDs instead of nil, so
lib/query can construct the query and lib/find-bad-refs can flag the
missing refs. Previously these blew up with "should be a positive int,
got: nil".
Without --lenient, missing schemas now throw instead of silently falling
back to lenient mode.
Want me to go ahead and commit with that?
* validate native sql
* filter false-positive :missing-column errors from SQL validation
Schema-less databases (e.g., SQLite) store tables with nil schema, but
sql-tools/validate-query assumes the driver's default schema ("public")
for unqualified table references. This mismatch causes valid columns to
be reported as missing.
Cross-check :missing-column errors against the store and suppress any
where the column actually exists for that database.
* extract the notion of the store
* down to just a single format
* cleanup, rename some args
* rename to semantic
* dashboard checker, extract a bit of common semantic checks
* check dashboard references
* extra schemas to bespoke file so we can move to a public repo
* Remove schema checker
we are semantic only. Now no reason to use a mode param
* Simplify checker module: remove formats, add dashboard/document, norm
Major refactoring of the checker module:
- Remove format abstraction: Delete concise, lenient, and hybrid
formats. Serdes is the only format. Delete format/hybrid.clj,
format/concise.clj, format/lenient.clj and their tests.
- Remove structural checker: Schemas moved to separate repo. Delete
structural.clj, schemas.clj, structural_test.clj.
- Rename checker.clj → semantic.clj: The "cards" checker is now the
"semantic" checker. CLI no longer has a --checker flag.
- Simplify store: Replace enumerator thunks with direct file index. Add
fields-by-table reverse index for O(1) table→fields lookup.
- Remove requiring-resolve: checker.clj (now semantic.clj) directly
requires format.serdes. No circular dependency indirection.
- schema-dir is required: No auto-detection. Export dir and schema dir
are always separate arguments.
- Add CompositeSource: In source.clj, delegates db/table/field to one
source and cards to another.
- Index entities by model field: Collections tree indexed by reading
serdes/meta model from YAML content rather than path patterns. Supports
both traditional serdes layout and the new flat layout.
- Dashboard validation: Validate dashcard card_id refs, dashboard_tab_id refs, and grid bounds (24-column grid).
- Document validation: Walk ProseMirror content tree, validate cardEmbed card refs.
- collection_id and dashboard_id validation: Any entity with these fields is checked against the index.
- Duplicate entity_id detection: Detected at index build time, reported with file paths.
- MBQL normalization pipeline: keywordize string operators → resolve
portable refs → legacy normalize. Known temporal unit strings
keywordized. Malli enforcement disabled during checking so lib/query
accepts valid clauses the schema doesn't enumerate.
- Disambiguate internal naming: Accumulator atom renamed from unresolved
to !failures. Qualified keys renamed to ::resolution-failures and
::missing-database.
* segment and measure support
* compile sql, handle native query snippet
* implement transforms checks
* using binding to prevent running t2 middleware
* fixup merge failures
* directory agnostic schema dir; sql parsing fixes
realized we weren't keeping around db information. now we are so we
don't have to bind driver information
```diff
- compiled (binding [driver/*driver* driver
- qp.i/*skip-middleware-because-app-db-access* true]
+ compiled (binding [qp.i/*skip-middleware-because-app-db-access* true]
```
* use lib for native check
* parse transform sql, store tests, fixups
* rename check-cards → check-entities; add direct native validation tests
check-cards was misleading — it validates cards, dashboards,
collections, documents, transforms, and more. Renamed to check-entities
with an updated docstring that documents the three parameters.
Added 6 direct tests for native.clj functions (extract-sql-refs,
validate-native-sql) covering valid queries, missing columns, syntax
errors, unknown databases, and nil db-name.
* remove bespoke native checking, use deps analysis
* compute result metadata for processing:
change:
before:
```
=== Native Query - Card and Snippet References [HiBFSt0BNx5s5MxVDLLKB] ===
Card ID: 94
Tables: Sample Database.PUBLIC.ORDERS
Fields: Sample Database.PUBLIC.ORDERS.CREATED_AT
Source Cards: Basic Aggregations
NATIVE SQL ERRORS:
- {:type :missing-column, :name "total_revenue", :source-entity-type :card, :source-entity-id "h5F2EjHsRd73Dqqh8sAtd"}
- {:type :missing-column, :name "created_attt", :source-entity-type :card, :source-entity-id "h5F2EjHsRd73Dqqh8sAtd"}
- {:type :missing-column, :name "cooount", :source-entity-type :card, :source-entity-id "h5F2EjHsRd73Dqqh8sAtd"}
Status: NATIVE SQL ERRORS
```
after:
```
=== Native Query - Card and Snippet References [HiBFSt0BNx5s5MxVDLLKB] ===
Card ID: 94
Tables: Sample Database.PUBLIC.ORDERS
Fields: Sample Database.PUBLIC.ORDERS.CREATED_AT
Source Cards: Basic Aggregations
NATIVE SQL ERRORS:
- {:type :missing-column, :name "created_attt", :source-entity-type :card, :source-entity-id "h5F2EjHsRd73Dqqh8sAtd"}
- {:type :missing-column, :name "cooount", :source-entity-type :card, :source-entity-id "h5F2EjHsRd73Dqqh8sAtd"}
Status: NATIVE SQL ERRORS
```
The total_revenue issue is related to not knowing that the card's
aggregation creates this column. Before it didnt' recognize it. Now with
metadata it does recognize it
* cleanup a bit
- println -> output! to silence the warnings and suppress in one place
- -main -> entrypoint so it doesn't appear as a proper entrypoint for
the jar, we still go through bootstrap and dispatch
* some dumentation on checker shape
* cljfmt files
* fixup
* include resolver arg
* fix readme and docstrings
* clean things up, introduce provider
provider is what wraps a store into a metadata provider. it's very
qp. the store is what cares about how to load assets and schema from
disk (or anywhere else?)
* e2e tests, report all checked items, roadmap and readme
* fix lint errors
* Fix dependencies module dependency
was using deps.analysis for it's check-entity multimethod. I at first
put an entry in deps.core that passes through to this, but deps.core
brings in t2 which i don't want.
Future work should split this namespace so it has some protocol of how
to load up it's information for deps analysis and the checker can figure
out how to adapt it's store and index for these purposes. Then all the
t2 stuff goes away and we're golden
* sort the api namespaces
* fmt fixes
metabase on yaml-backed-metadata-provider [$?] via 🥟 v1.3.8 via ☕ v25.0.2 on ☁️ metabase-query
❯ bin/mage cljfmt-files enterprise/backend/test/metabase_enterprise/checker
Reformatted enterprise/backend/test/metabase_enterprise/checker/semantic_test.clj
metabase on yaml-backed-metadata-provider [$!?] via 🥟 v1.3.8 via ☕ v25.0.2 on ☁️ metabase-query
❯ bin/mage cljfmt-files enterprise/backend/src/metabase_enterprise/serialization/cmd.clj
Reformatted enterprise/backend/src/metabase_enterprise/serialization/cmd.clj
* Checker: unify entity checking, split protocols, extract provider
Major refactoring of the checker module for clarity and consistency:
- Split MetadataSource into SchemaSource (databases/tables/fields) and
AssetsSource (cards/snippets/transforms/segments/dashboards/collections/
documents/measures). All entity types now go through source protocols
and the store — no more direct YAML file loading fallbacks.
- Extract provider.clj from semantic.clj. The provider adapts the store
into a lib MetadataProvider: reference resolution, MBQL normalization,
data conversion. semantic.clj is now just validation + orchestration.
- Replace custom keywordize-mbql-operators (~40 lines) with lib/normalize.
- Unify entity checking: cards are no longer special-cased. check-entity
dispatches by kind for all entity types. check-one works for any entity,
not just cards. check-entities defaults to checking everything.
- Store sources are now private. Provider creates resolvers internally.
resolve-ref uses store load functions instead of reaching through to
source protocols directly.
- Add dashboard/collection/document/measure to source protocols, store
load functions, and test helpers. Tests use in-memory sources throughout
— no more temp YAML files.
- Add e2e tests against test_resources/serialization_baseline with shape
assertions for joins, snippets, measures/metrics/segments, and
transitive ref propagation.
- Add forward-declare wrapper for deps.analysis/check-entity.
- Track snippets, measures, metrics, segments in card :refs, propagating
transitively through source-card chains.
- All non-card entities now appear in results (not just failures).
* handle multiple databases
* cleanup entrypoint a bit
* Checker: lazy on-demand indexing for schema directories
Index only database YAMLs (~40 files) at startup instead of parsing all
563k YAML files. Tables, fields resolved on-demand from directory structure.
Segments/measures indexed only from export dir, not schema dir.
Key changes:
- SchemaSource protocol gains enumeration methods (fields-for-table,
tables-for-database, all-table-paths, etc.)
- db-name->dir mapping handles slugified directory names
- Provider scoped to current database (tables-for-database not all-table-paths)
- Store delegates to source instead of maintaining field index
Setup time: 64s → ~100ms for 40 databases, 22k tables, 541k fields.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Checker: handle table-type template tags, lightweight table listing
- Native queries with {{table_one}} (type: table) template tags now get
placeholder substitution instead of passing through as raw mustache
syntax that SQLGlot can't parse
- Filter out validation errors referencing table placeholder names
- Skip result-metadata for table-tag queries (columns unknown at check time)
- Provider uses ensure-table-id! for table enumeration (path-derived
metadata, no YAML parse) instead of load-table! (full YAML parse)
- Remove debug instrumentation from semantic.clj
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Checker: update docs to reflect on-demand indexing architecture
- DESIGN.md: rewrite to reflect current architecture (SchemaSource/AssetsSource
protocols, on-demand field/table resolution, db-name->dir mapping, parallelism
readiness, performance characteristics)
- CHECKER_RESULTS.md: add analysis of 177-entity run (17 true errors, 2 remaining
bugs, 1 fixed template tag bug)
- Fix stale docstrings in store.clj and serdes.clj
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Checker: consolidate docs into README, remove standalone md files
Move architecture, performance, and on-demand indexing docs into
the existing README. Remove DESIGN.md and CHECKER_RESULTS.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Checker: fix test helpers for new SchemaSource protocol methods
- Add fields-for-table, all-field-paths, all-database-names,
all-table-paths, tables-for-database to test helper reify
- Update index assertion tests to check via source protocol
instead of pre-indexed table/field counts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Merge branch 'master' into yaml-backed-metadata-provider
* remove bashing in place, unused function
* Checker: single-atom schema model with two-level lazy resolution
Replace the three separate cache atoms (indexed-schemas,
table-cache,field-cache) with a single schema-model atom that owns all
database, table, and field knowledge. Separate from the assets-index
which holds cards, dashboards, segments, etc.
Schema model shape:
{db-name {:db-file "path"
:schemas {schema ::not-indexed
| {table {:table-file "path"
:fields ::not-indexed | {field "path"}}}}}}
Two levels of laziness:
- Schema → tables: populated when any table in that schema is resolved,
or when tables-for-database is called. Walks the schema's tables/
directory and reads each table's name: from YAML via fast regex.
- Table → fields: populated when resolve-field or fields-for-table is
called for that specific table. Walks the table's fields/ directory
and reads each field's name: from YAML via fast regex.
Schema name resolution handles the case where directory names differ
from real entity names (e.g. H2: dir=public, real=PUBLIC). This is
isolated in resolve-schema-name as the single point to change when
schema resolution improves.
Also: store paths as strings not Files, fix exists? for databases (now
checked via schema source not assets index), remove dead
has-card-template-tags?.
* inline util lower-case-en
don't want to pull in all the stuff so i just use the definition inline
here
---------
Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix OIDC SSRF: validate all discovery-document URLs before HTTP requests
The token_endpoint extracted from the OIDC discovery document was used
for HTTP POST requests without validating the host against
oidc-allowed-networks. A compromised OIDC provider could return a
token_endpoint pointing at internal hosts, causing Metabase to POST
client credentials to an attacker-chosen URL.
Centralizes all OIDC HTTP requests through a new metabase.sso.oidc.http
namespace that validates URLs against oidc-allowed-networks before every
request. Also adds defense-in-depth validation in get-endpoint so URLs
are checked at extraction time from the discovery document.
* Log blocked OIDC requests at WARN level for easier debugging
Adds an explicit log/warnf in validate-url! so SSRF blocks are visible
at the point of origin rather than only as wrapped causes downstream.
Netty 4.2.12 introduced a breaking change in HttpObjectEncoder that
causes AWS SDK netty-nio-client to fail with "unexpected message type:
LastHttpContent" errors. Revert to 4.2.9.Final which is compatible
with the AWS SDK while still being in the 4.2.x line.
Closes: #72532
Fixes QUE2-34.
These are nested fields (`:parent_id` is set) but not JSON nesting, so
the mechanism is slightly different from `:nfc_path`. This change should
include the necessary metadata on `:field` refs inserted by field filters
to correctly handle struct fields.
* Align embedding profile with NLQ profile tools and system prompt
The embedding_next profile was a stripped-down version with only 3 tools
and a simpler prompt. This aligns it with the NLQ profile so embedded
metabot behaves identically: same system prompt content (kept as a
separate file for future divergence), same tool set (nlq-search,
read-resource, construct-notebook-query, navigate-user, create-chart,
edit-chart).
* Remove obsolete embedding_next tools test
This test was asserting the old tool set (including
list_available_data_sources). Coverage is now provided by
embedding-next-matches-nlq-tools-test in profiles_test.
* center buttons vertically in CreateOrEditQuestionAlertModal footer
* Fix alert modal footer button sizing mismatch (#71945)
- Replace ActionButton with a Mantine Button for the Done/Save changes button
- Wire loading state to isCreating and isUpdating from RTK Query mutations
- Swap the wrapping div for a Flex with align="center" and gap="sm"
- Add align="center" to the outer footer Flex so all three buttons stay consistently sized and vertically centered at all viewport widths
- Remove throw result.error which only existed to drive ActionButton's internal error display
* handle error states
---------
Co-authored-by: Alexander <aquezadawa@gmail.com>
The frontend dev port was already configurable via MB_FRONTEND_DEV_PORT,
but the CLJS REPL port (shadow-cljs) was still hardcoded to 9630. This
causes CSP violations when running multiple Metabase instances with
dynamic port allocation.
Fix ClickHouse driver security vulnerabilities
- Pin httpcore5-h2 to 5.3.5 to fix DoS vulnerability
- Replace discontinued org.lz4:lz4-java 1.8.0 with maintained fork
at.yawk.lz4:lz4-java 1.10.2 to fix out-of-bounds read and
sensitive data exposure vulnerabilities
Currently, stats has 5 "Data Analysts (converted)" groups. As far as I
can tell, this shouldn't happen in the happy path: after the database is
set up, we check for the necessary premium features before converting
the "special" Data Analysts group into a renamed "normal" group and
creating a new Data Analysts group.
But this is happening - intermittently - so I think we just need to be
more defensive here. If some transient network issue or something causes
us to temporarily be unsure what features are enabled, then normally we
just say "nope, no features here" and that's fine. In this case we need
to be sure, and should only do the Data Analysts swap if Metastore has
*told us* that the feature is not present.
So, this adds:
- a `:canonical? false` flag, when the result is not canonical
- and a function, `canonically-has-feature?` that returns `true` if the
feature is canonically present, `false` if the feature is canonically
absent, or `nil` if the feature's presence cannot be canonically
determined
Then, when we start up, we check the feature using this function.
If the feature is canonically present, we do nothing.
If the feature is canonically absent, we do the swap.
If neither is true (network blip or some other issue caused us to not
receive a valid response from the metastore), we fire off a future that
will wait for a canonical response and then do the swap if necessary.
* Skip cache auto-refresh attempts for routed dbs
Currently we pollute the logs here, because we can't do anonymous
queries against a routed DB.
Just skip cards that are against routed databases to avoid failing
deeper down.
* Import :model/DatabaseRouter into enterprise/cache module
* Bump Zookeeper 3.8.4→3.8.6 and Curator 5.7.1→5.9.0 in hive-like driver
Fixes CVE-2026-24308 (config data leaked to INFO logs) and
CVE-2026-24281 (hostname verification bypass via PTR spoofing).
* Pin Guava 33.5.0-jre in hive-like driver to fix transitive 22.0 vulnerabilities
hive-standalone-metastore-common pulls Guava 22.0 transitively, which has
three known Snyk advisories (info disclosure, deserialization, insecure
temp files). Exclude it from the metastore dep and pin 33.5.0-jre directly,
matching the root deps.edn version.
* Pin Guava 33.5.0-jre in hive-like driver to fix transitive 22.0 vulnerabilities
hive-standalone-metastore-common pulls Guava 22.0 transitively, which has
known security advisories (info disclosure, deserialization, insecure
temp files). Exclude it from the metastore dep and pin 33.5.0-jre directly,
matching the root deps.edn version.
* Revert "Fix serdes breaking on IDs for db/table/fields for non-existent entit…"
This reverts commit 8b051f242f.
* Revert "Fix serdes export perf with database metadata - id lookup (#72326)"
This reverts commit e90c0ea24e.
Replace the standalone uberjar (which bundles and relocates dependency
versions with published security advisories) with the regular
hive-jdbc artifact plus explicit sub-module and third-party dependency
declarations at safe versions.
The published hive-jdbc 4.2.0 POM is broken (HIVE-28455) so all Hive
sub-modules must be declared manually. Each has exclusions to prevent
pulling in the massive Hadoop/Hive transitive dependency tree (YARN,
Jetty, Jersey, Spring, Shibboleth, etc.).
Key version pins for security scanning:
- Netty 4.2.12.Final (was bundled at 4.1.127.Final)
- commons-io 2.18.0 (CVE-2024-47554)
- commons-codec 1.17.1, commons-logging 1.3.5
- httpclient 4.5.14, httpcore 4.4.16
Fixes: GHY-1756:
* feat: metric math - backend only
Squashed commit of lib_metric AST, compile, plan, filter, projection, and related backend changes.
Frontend and JS-export changes have been removed.
* support for constants
* fix tests
* kondo
* whitespace
* tests
* UXW-3264 Add metric math test UX
* Polishing, address edge cases
* FE changes to work with constants
Code may be bad but it seemed pretty simple to add support here.
* Add text view
(cherry picked from commit e7ad8536860ece87a84608b42a2e7a70ebc99ca0)
# Conflicts:
# frontend/src/metabase/metrics-viewer/components/MetricSearch/MetricSearchInput/MetricSearchInput.module.css
# frontend/src/metabase/metrics-viewer/components/MetricSearch/MetricSearchInput/MetricSearchInput.tsx
# frontend/src/metabase/metrics-viewer/pages/MetricsViewerPage/MetricsViewerPage.tsx
# frontend/src/metabase/metrics-viewer/utils/url-serialization.ts
* Add support for constants
* Add unit tests
* Small updates
* WIP - added syntax highlighting, validation
* Add syntax highlight and basic validation
* Metric viewer can use metrics based on cards
[UXW-3260: Metrics from models/table questions are not viewable in the Metrics viewer](https://linear.app/metabase/issue/UXW-3260/metrics-from-modelstable-questions-are-not-viewable-in-the-metrics)
* Add commit changes flow
* Allow using the same metric multiple times
* Temp
* Fixed reload after adding a new metric from dropdown
* Fix issue with single expression
* Polished version with metric text tokens
* scalar results tab
* show no compatible dimensions error rather than blank screen
* fix types
* Fix adding breakouts
* fix scalar and map breakouts
* rename to totals tab and allow adding back in
* scalar tab cleanup
* fix default tabs not reappearing
* fix error flashing
* UXW-3382 Fix metric names with commas and other symbols (#71318)
* UXW-3382 Fix metric names with commas and other symbols
* UXW-3382 Fix issue with searching metric with comma in it's name
* Self review
* allow adding tabs for expressions
* fix deserializing measures from URL
* add index to serialized metrics and expressions to preserve order on refresh
* fix tests
* UXW-3380 Fix metric dropdown to filter only self
* don't move cursor with arrow up/down when popover is open
* handle order of operations properly
* handle non-numeric metric error
* Allow metric math with non-exact temporal field matches (#71183)
* Allow metric math with non-exact temporal field matches
We can now do metric math between two metrics that both have temporal
dimensions, even if those temporal dimensions aren't identical! 🎉
* cljfmt
* fix clj tests
* UXW-3413 Support break out the same metric multiple ways (#71390)
* UXW-3413 Support break out the same metric multiple ways
* UXW-3413 Fix issues the last same metric instance overrides breakout, fix colors
* Fix removing breakout after changing formula
* Self review
* Review comments
* Remove rename that breaks IDE navigation
* Filtering specific instances of metrics (#71640)
* fix be tests
* llm review
* fix be test
* support negative numbers in metrics viewer expressions (#71790)
* support negative numbers
* review
* slight refac
* llm review - don't clear dimension filter on dimension change
* cleanup console.log
* revert accidental changes to pie files
* fix unit tests
* revert changes with invalid css variables
* remove unused CSS classes with bad CSS vars
* fix e2e tests
* Fix Metric Math URL serialization (#71769)
* reconile state on expressions change (#71795)
* add metric math e2e tests
* wip
* Revert "wip"
This reverts commit dadde5a90c.
* UXW-3292 Add dimension controls for every metric instance (#71791)
* UXW-3292 Add dimension controls for every metric instance
* Add expressions dimensions controls, but it doesn't really apply to the series
* Add fixes to pick default dimension, and support dimensions for expressions
* Add e2e tests
* Refactor
* Fix issue with not visible series
* Fix issue with missing MetricControls
* Fix issue selected metric dimension being preserve after reload
* Fix e2e
* Update dimension projection serialization for expressions
* Address some review comments
* Added more unit tests
* Self review
* Self review
* fix test, try to fix flaky metric identities
* revert failed attempt to fix flaky test
* Reuse reconciliation logic from MetricSearchInput (#72111)
---------
Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
* Ensure we can always parse formulas (#72112)
* ensure we can always parse formulas
* review comments
* Revert changes to e2e to wait for metrics load
* Fix after merge
* fix modifiedDefinitions indexing for expressions
* update invalid expression handling (#71934)
* fix types in tests after merge
* fix test
* Review fixes
* more test fixes
* try to fix flaky test again
* Series refactor (#72060)
* add dimension pill icon for expressions
* Fix styles
* Fix styles
* Fixes after merge
* Review fixes
* Review fixes
* Fix e2e tests
---------
Co-authored-by: Edward Paget <ed.paget@gmail.com>
Co-authored-by: John Swanson <john.swanson@metabase.com>
Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
Co-authored-by: Aleksandr Lesnenko <alxnddr@users.noreply.github.com>
* Update cheshire version to 6.2.0
https://github.com/dakrone/cheshire/commits/master/
patches the jackson-core dep issue
* Make JSON error test resilient to parser version changes
Use regex matching instead of exact string equality so the test
doesn't break when the JSON parser reports a different column offset.
---------
Co-authored-by: Edward Paget <ed.paget@gmail.com>
* Fix metrics on models with custom column dims
Just like with joins, a metric with a source-card needs to be compiled
to a two-stage query rather than querying the table directly, so that we
can query custom columns.
* Fix metrics on models with custom column dims
Just like with joins, a metric with a source-card needs to be compiled
to a two-stage query rather than querying the table directly, so that we
can query custom columns.
Send metering for today's usage under a new :metabot-rolling-usage key with corresponding :metabot-rolling-usage-date.
All other keys only send yesterday's values for backwards compat.
Keys are omitted if there is no usage for the corresponding day.
* Fix sorting of maps nested under double-layers of sequences
* Re-export after fixing nested map sorting
* Descend into all sequential nodes
* Re-export instance analytics collection
* Test against expected yaml string
Test that sorting is idempotent
* Make broken dependency analysis async
This change make the synchronous (in the API request) part very simple and fast: Just mark the current entity stale,
trigger an async task, and return.
The async task will do the actual analysis and mark its direct dependents stale. The async task loop will then pick
those up and continue from there. In this way, all transient dependents will be analyzed as well.
Updated tests also to make sure chains of dependents do get updated.
* Async dependency backfill analysis
* Fix async dependency backfill: test fixes and error handling
- Fix NPE in mark-stale! when no existing entries found
- Add error handling in mark-stale-and-trigger! for empty test DBs
- Let dependency computation errors propagate for retry handling
- Fix downstream transform tests to persist target changes to DB
- Fix delayed-retry test scope for with-redefs
- Add run-backfill! calls to dependency model integration tests
- Remove with-empty-h2-app-db! from filtered-graph-dependencies-test
* Add DependencyStatus to model->namespace resolution map
Fixes failing all-models-are-accounted-for-test.
* Add backfill calls to replacement api tests
Events no longer synchronously compute dependencies, so tests
need to run the backfill before asserting on dependency data.
* Revert "Add backfill calls to replacement api tests"
This reverts commit 68b2835beb.
* Fix CI failures: add backfill calls, serialization exclusions, shared test helper
- Only process entities WITH dependency_status entries (inner-join
instead of left-join) to avoid computing deps for entities that
were never event-triggered
- Add DependencyStatus to serialization excluded-models and entity-id
exclusion set
- Add deps.test/synchronously-run-backfill! shared helper in test_util
- Use shared helper consistently across all test files
- Add events/publish-event! calls in tests that create entities via
mt/with-temp without firing dependency events
- Add backfill calls in replacement, usages, and workspace tests
* Remove trailing blank line in backfill_test.clj
* Add DependencyStatus to copy-test models-to-exclude
* Fix workspace test failures: run backfill before graph persistence
The workspace graph is cached when ws-done! runs. With async dep
computation, the backfill must run before ws-done! so that upstream
dependency edges exist when the graph is persisted.
Also add backfill call in validation_test which creates transforms
via mt/with-temp outside of create-resources!.
* Guard segment-delete dependency row delition
* Fix TOCTOU races in dependency_status and missing feature gate on segment-delete
- Replace select-then-insert patterns in mark-stale! and upsert-status! with
app-db/update-or-insert! which handles cross-database conflict retry with
savepoints (H2, MySQL, PostgreSQL).
- Add missing (when (premium-features/has-feature? :dependencies) ...) guard
on ::segment-delete event handler, matching all other delete handlers.
* Use app-db.core instead of app-db.query to fix kondo module boundary warning
* Refactor backfill to use multimethods for per-entity-type behavior
- Move dashboard hydration from backfill case statement to hydrate-for-deps
multimethod in dependency_status.clj, using standard t2/hydrate [:dashcards :series]
- Convert upstream-deps:* public functions to calculate-deps multimethod in
calculation.clj with logic in defmethods, update all callers
- Extract post-deps-cleanup! multimethod in backfill.clj for transform-specific
downstream dep cleanup
- Remove custom :series-card-ids hydration; extract IDs from existing :series
hydration in calculate-deps :dashboard
* Move retry/broken state into dependency_status table, add Malli schema wrapper, migration fixes
- Add fail_count, next_retry_at, terminal columns to dependency_status table
- Remove in-memory ConcurrentHashMap retry tracking from backfill.clj
- record-failure! and clear-failure! persist state to DB, surviving restarts
- mark-stale! resets retry state so modified entities get a fresh chance
- instances-for-dependency-calculation filters terminal/not-yet-retryable in SQL
- Uses Java time (not DB time) for retry comparison so mt/with-clock works in tests
- Rename calculate-deps multimethod to calculate-deps*, add mu/defn wrapper with
Malli schema validation on return value
- Add comment in events.clj explaining two separate stale tracking systems
(dependency_status vs analysis_finding)
- Migrate ALL entities (not just version > 0), marking never-analyzed as stale
- Rename migration file to 20260330 for fresh DB compatibility
- Remove unused DashboardCard/DashboardCardSeries from module imports
- Add DependencyStatus to module exports
* Remove DependencyStatus from module exports — only used internally
* Add tests for error logging and graceful degradation in backfill
- backfill-error-logging-test: verifies that when calculate-deps throws,
the entity remains stale, no deps are created, and fail_count is recorded
- backfill-partial-batch-failure-test: verifies that a failure on one entity
doesn't prevent other entities in the batch from being processed
* Shorten comment separator lines to stay under 120 char limit
* Clean up dependency_status rows on entity delete, fold clear-failure into upsert-status
- All 8 delete handlers now also delete the dependency_status row to prevent
orphaned rows accumulating over time
- upsert-status! now resets fail_count and next_retry_at, eliminating the
need for a separate clear-failure! call and closing a gap where
has-pending-retries? could loop indefinitely
* Remove trailing blank line in dependency_status.clj
* Harden error handling: catch failures in record-failure! path, fold clear-failure into upsert-status
- Wrap record-failure! and logging in a nested try/catch so a DB error
while recording the failure doesn't abort the rest of the batch
- upsert-status! now clears fail_count and next_retry_at, eliminating
the separate clear-failure! function and closing a gap where
has-pending-retries? could loop on a processed entity
* kick ci
* Remove DependencyStatus from serialization models list
dependency_status is transient state (stale flags, retry counts) that
gets recomputed by the backfill job. No need to export/import it.
* Fix runner test: add synchronously-run-backfill! after card-create events
With async deps, the runner test needs to run backfill before attempting
source swaps that depend on the dependency graph being populated.
* kick ci
* Add DependencyStatus to serialization excluded-models list
The every-model-is-supported-test requires every Toucan model to be
accounted for in either exported, inlined, or excluded models.
DependencyStatus is transient state and should never be serialized.
* Fix replacement api test: add synchronously-run-backfill! after card-create events
* Improve docstring
Co-authored-by: Timothy S. Dean <timothy.dean@metabase.com>
* Improve docstring
* Use immediate dependents instead of transitive for all event handlers
- Transform run, sync complete, table/field metadata update handlers now
mark only immediate dependents as stale, letting the entity-check job
propagate in waves rather than computing the full transitive tree upfront
- Add mark-all-immediate-dependents-stale! for batch immediate-dependents
- Add missing trigger-entity-check-job! calls to table/field metadata handlers
* Add wave propagation test, fix check-entities! loop to drain all stale entities
- Add wave-propagation-via-entity-check-test: verifies that marking immediate
dependents stale propagates through the entity-check job loop in waves
(grandparent change → parent re-analyzed → child marked stale → child re-analyzed)
- Fix check-entities! loop condition: continue while has-stale-entities? is true,
not only when the batch was full. analyze-and-propagate! creates new stale
entities during processing that need to be picked up in subsequent iterations.
* Fix nested deftest: move query-transform-update-handles-downstream-dependencies-test to top level
Was accidentally nested inside python-transform-update-handles-downstream-dependencies-test
due to a missing closing paren. Now runs as its own test (18 tests, was 17).
* Add test for mark-stale! failure resilience in event handlers
Verifies that when mark-stale! throws (e.g., DB failure), the event
handler catches the error and doesn't propagate it to the API caller.
* Restore deleted analysis propagation tests adapted for async model
Adds back 15 tests that were removed when switching to async deps:
- Transaction rollback test: verify analyze-and-propagate! rolls back on failure
- Card update triggers native cards: wave propagation through entity-check job
- Card update stops on transforms: propagation boundary test with waves
- Transform update/run: with/without existing analyses, native transforms
- Segment update: with/without existing analyses, propagation to dependents
- Table metadata update: marks immediate dependents stale, no-analysis resilience
- Field metadata update: marks immediate dependents stale, no-analysis resilience
Tests adapted for async model: events mark stale, entity-check job propagates.
* Increase e2e dependency wait timeout from 10s to 30s
With async dependency computation, the backfill and entity-check jobs
need time to process entities. The 10s timeout was too short for e2e
tests that create multiple entities and wait for analysis findings.
* Move dependency_status migration from 060 to 061, update changeset IDs
The cross-branch migration test requires migrations to be in the correct
version directory. Since this PR targets v61, move the migration there
and update all changeset IDs from v60 to v61.
* Move dependency_status migration back to 060/, keep updated date
* Add preConditions with MARK_RAN to all dependency_status changesets
Databases that already ran the old v60.2026-03-17 changesets (from the
previous migration filename) will skip these changesets instead of
failing with checksum or duplicate errors.
* Revert preConditions — old changesets were never deployed
* Add preConditions onFail: MARK_RAN to all dependency_status changesets
* Revert "Add preConditions onFail: MARK_RAN to all dependency_status changesets"
This reverts commit f9f7ef1645.
* Add failing test: wave propagation through non-analyzable entities
Demonstrates that mark-immediate-dependents-stale! followed by
check-entities! doesn't reach cards that depend on non-analyzable
intermediaries (tables). The chain transform → table → card breaks
because the wave stops at the table (not analyzable).
* Fix wave propagation through non-analyzable entities, add 1s trigger delay
Two fixes:
- mark-supported-dependents-stale! now looks through non-analyzable entities
(e.g., tables) to reach analyzable dependents beyond them. Previously the
wave stopped at tables, so cards depending on a transform's output table
were never re-analyzed when the transform changed.
- trigger-backfill-job! and trigger-entity-check-job! now use a 1-second
delay instead of 0. Event handlers run inside Toucan2 after-update hooks
(within a transaction), and delay-0 triggers fired the Quartz job before
the transaction committed, so the job saw no stale entities.
* Remove trailing blank line in findings_test.clj
* Add waitForGraphDependencies helper for e2e tests
Polls /api/ee/dependencies/graph until the dependency data is available,
similar to waitForBreakingDependencies and waitForUnreferencedEntities.
Needed because dependency computation is now async.
* Add waitForGraphDependencies to visitGraphForEntity in e2e graph tests
Polls the graph API until dependency edges exist before navigating to
the graph page. This accounts for async dependency computation.
Also removes accidental .only() left from local testing.
* Remove test-mode delay-0 for backfill job init
The backfill job was starting with delay 0 in test mode, which caused
it to run during e2e snapshot creation. This monopolized the connection
pool and caused the data-studio-metrics e2e test to timeout when trying
to load the entity picker. Now uses the normal production initial delay.
Backend tests are unaffected — they call synchronously-run-backfill!
directly without relying on the Quartz scheduler.
* Appease the kondo
* Remove config from enterprise/dependencies module uses
The dependency module no longer uses the config module.
* Wait for async dependency computation in e2e tests
Dependencies are now computed asynchronously, so tests that check the
dependency graph need to poll until edges are populated before asserting.
* Wait for async dependency computation in unreferenced list e2e tests
Add waitForUnreferencedEntities calls before visiting the unreferenced
entities page to ensure async dependency computation has completed.
* Add backfill-status endpoint and use waitForBackfillComplete in e2e tests
Add GET /api/ee/dependencies/backfill-status endpoint that returns whether
dependency backfill has pending work. Replace per-entity graph polling in
e2e tests with a single waitForBackfillComplete() helper that polls this
endpoint, simplifying async dependency waits across all test files.
* Reload page before navigating to dependencies tab to refresh cache
---------
Co-authored-by: Timothy S. Dean <timothy.dean@metabase.com>
An upstream Snowflake JDBC driver issue causes an exception to be thrown
because the UUID column type is not present in an enum.
This adds logic to fall back to the more generic `SELECT *` sync mode if
this error occurs during a sync.
* Remove snippets sentinel
* Add appropriate feature to python transform tools
* Adjust oss transform tools sentinels
* Remove remnants of ee-feature-available?
* Drop features-transforms and adjust profiles code
* Use feature-transforms
* Adjust test
* Tests
* tests
* Ensure python tools not available when basic transforms are not available
* security center data model (#71660)
* add security_center model
* wip: migration
* kondo
* Advisory security endpoints (#71704)
* Security center admin page and advisory list (#71707)
* Use :admin-security-center feature flag and restore premium-handler on route
* Reuse notification system components for security center channel config (#71723)
* Security center settings (#71807)
* feat: add security-center recipients settings
* feat(FE): add security-center settings
* tests
* feat(FE): hook to settings
* Security center matching engine (#71747)
* upgrade banner (#71800)
* added banner (#71816)
* moved the security center into the top bar (#71821)
* fixed an e2e test
* Gate Security Center for non-trial subscriptions (#71799)
* Gate Security Center for non-trial subscriptions
Trial subscriptions should not have access to Security Center to
prevent abuse via easily-obtainable trial enterprise tokens.
Adds `is-trial?` and `assert-not-trial` to the premium features
token check, and applies a `+require-non-trial` middleware to the
security-center route that returns 402 for trial tokens.
* Handle check-token exceptions in is-trial?
Fail open (return false) if the token status check throws, matching
the same pattern used by *token-features*. Prevents a 500 when
MetaStore is unreachable.
* Use token-status setting instead of check-token directly
Follow the existing pattern used by billing and cloud-add-ons.
* Use -token-status instead of the setting getter
* Gate Security Center for non-trial subscriptions
Trial subscriptions should not have access to Security Center to
prevent abuse via easily-obtainable trial enterprise tokens.
Adds is-trial? and security-center-enabled? to premium features
token check, and a +check-not-trial middleware on the security
center API routes that returns 402 for trial tokens.
* Security center notification (#71814)
* feat: add last_notified_at column
* feat: add security-advisory-match event
* feat: security advisory email template
* feat: add slack renderer
* feat: notify
* email fixes
* kondo
* fix: hydrate
* chore: quartz
* test
* send notification sync
* fix: guard against no smtp config
* fix: bcc
* more tests
* only on has feature
* switch to util
* move url builder
* feat: use all-admin-recipients
* Security center: fetch and scheduled evaulation (#71830)
* addressed stas feedback
* all-admins and recipients are no longer exclusive (#71895)
* Fix read only connection for security center (#71898)
* added sync button (#71905)
* Security center unify tasks + CI green (#71894)
* pull notify task into sync task
* make CI green
* chore: exclude security-advisory
* fmt
* require settings in init
* kondo
* set wor
* fixes
* exclude is-hosted
* check-enabled with open-api wrap
* change error
* missing
* fix: emails
* fix use an user-id
* fixes
* init test-users
* enable on dev
* rollback to enforce no writes
* fix: add index
* index before fk
* may not error
* Security center test notification (#71915)
* test notifications
Closes GDGT-2174
* test
* fix
* addressed PR feedback (#71919)
* fix parsing published_at timestamp durign fetch
* fix fetch test to use string published_at matching real API response
The test was using #t tagged literals for published_at which bypassed
the string→timestamp coercion path, masking the bug we just fixed.
* added tests for recipients settings (#71963)
* make recipients non nullable (#71967)
* Treat error match_status as affected (fail-open) (#71961)
* treat error match_status as affected (fail-open)
Per the matching engine spec, when a matching query fails, the advisory
should be treated as potentially affected. The isAffected check was only
looking for "active" status, missing the "error" case.
* fix type errors in createAdvisory mock
Add return type annotation so string literals narrow to the Advisory
type instead of widening to string when spread with Partial<Advisory>.
* Security center: force sync endpoint (#71899)
* Security center cursor (#71969)
* feat: add updated_at
* feat: cursor on updated_at
* feat: since tests
* fix fetch tests
* appease the linter
* fix test
* remove seed
* small fixes
* fix
* feature token properly propagates (#71970)
* added polling for sync button (#71971)
* fix
* fix
* fix
* Add Snowplow tracking for Security Center (#71972)
* Add Snowplow tracking for Security Center
Track 3 events using the existing simple_event schema:
- security_center_page_viewed (FE, on page mount)
- security_advisory_acknowledged (BE, with severity)
- security_advisory_notification_sent (BE, with channel/result/trigger)
* Derive notification channels from handlers instead of config
Instead of re-checking configured-channels at tracking time, derive
the channel list from the notification's actual handlers.
* Move last_notified_at update inside try block
* add :admin-security-center to excluded features in stats test
* kondo
* update schema (#72031)
* hook full feature logic to premium features exposed to the FE (#72026)
* hook full feature logic to premium features exposed to the FE
* tests
* fix test
* chore: error in FE
* fix
* fix e2e test
* lint
* tighten schemas
* fix hydration
* chore: defensively check-superuser
* tighter schemas
* fix test
* chore: simpler schema as we'd need to duplicate with :api/regex
* e2e test should only run on ee
* fix: recipients default (#72035)
* Add testing endpoint for security advisories (#71977)
* fix model boundary violations for SecurityAdvisory
* relax schema
* move to 45 (#72073)
* chore: migrations in 001 file for backporting to 45
* mark-ran preconds
* reorder
* consolidate
* add set null
* Mobile improvements (#72071)
* move to 55
* move severity->repeat-days into an overridable setting
Closes GDGT-2208
* reformat
* typo
* deal with nits
* switch to select-fn->fn
* explicit map building
* more nits
* align
* nit
* add not-empty
* use custom executor instead of future
* use semver4j
* fix test
* restrict queries to selects (#72162)
* reorder
* Security center no h2 (#72215)
* disable security-center for h2 appdbs
* tests
* fix last-checked at, store in setting
* fix tests
* enable for e2e tests
* fix merge conflicts
* Revert "Security center no h2 (#72215)"
This reverts commit 41dc93794d.
* fix: persist last-synced-at in a setting
* fix test
* disable on h2 (enabled for tests) (#72263)
* UI feedback on Security Center (#72214)
* fixed imports
* chore: max 2 mins for advisory query
* kondo
* chore: use sql-jdbc.execute
* kondo
---------
Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>
Co-authored-by: Sébastien <sebastien@metabase.com>
* Enforce session timeout server-side with last_active_at tracking
Add a last_active_at column to core_session and use it to enforce
session inactivity timeouts on the server side (behind the
session-timeout-config premium feature). Activity updates are throttled
via an atomic compare-and-set cache in session.core to avoid a DB write
on every request. The session cleanup task prunes idle sessions and
expired cache entries.
* Move EE-dependent session timeout tests to enterprise test file
Session timeout tests that require the :session-timeout-config premium
feature fail in OSS mode because enable-session-timeout-config? checks
config/ee-available? which is always false without enterprise code.
* Remove trailing blank lines from test files
Fix nested WHERE clause in remove-unsynced! for TransformTag (UXW-3710)
Mixing a {:where ...} HoneySQL map with kv-pair conditions in t2/delete!
caused toucan2 to misinterpret the map as a condition key, producing invalid
SQL with a WHERE inside a WHERE. Convert other-conditions into the {:where}
clause directly and remove unreachable scoped-path code that had the same bug.
If you have a parameterized card with computed columns, there is no Field from which to pull fingerprints, and the query-processor runs a fingerprint process based on the results of the query. Because the query is parameterized, the result is specific to whatever random parameter was used. `record-metadata!` saw this difference and updated the result-metadata on the card.
The fix: we will no longer persist `result_metadata` when non-default parameterized values are present.
Extract shared `apply-card-permission-filters` helper used by both GET and
PUT paths to ensure consistent card visibility filtering. Also fix
`set-download-perms-on-dashcards` to handle empty dataset_query maps
gracefully.
Fixes: UXW-3571
* fix n+1s in db, table, group creation
this batches permissions changes to fix cluster lock timeouts with large numbers of groups, database, and tables.
When we created a new group, table, or database, we obtained O(NxM) fine-grained cluster locks, and did NxM separate inserts into data permissions.
This does batch operations instead. Obtains a single cluster-wide shared lock for batch operations (which actually might not be necessary at all for database and group creation - think it's still needed for creating a table though), then does the insert in batches.
Reduces the total number of queries dramatically:
- group creation: scaling with NxM -> ~10
- db creation: scaling with NxM -> ~10
- table creation: scaling with NxM -> ~8
* Fixup tests, improve perf more
* comment cleanup
* fix test
* remove dead code + unnecessary api epxansion
* Fix cluster lock contention
This is a fairly big change on its own, but it's fairly straightforward
and an additional performance improvement that should help with
concurrent processes trying to modify permissions at the same time.
Essentially, I realized that currently our cluster lock system provides
exactly one type of lock, an exclusive lock. But that's not the only
type of potential lock! Both Postgres and MariaDB/MySQL support locking
a row as *shared*. This conflicts with exclusive locks (`FOR UPDATE`)
but does not conflict with other shared locks.
This way, we can design a two-level lock system in which:
- most operations require:
- a *shared* lock on `::batch-permissions-update`, and
- an *exclusive* lock on `::batch-permissions-update-<db>`
- some operations require only an *exclusive* lock on
`::batch-permissions-update`.
As one example, modifying permissions with the graph API requires an
exclusive lock on `::batch-permissions-update`. We don't want sync
adding a table at the same time that someone is modifying permissions,
e.g. moving from granular -> coalesced or vice versa.
On the other hand, sync itself just requires a *shared* lock on
`::batch-permissions-update`, because it can happily coexist with other
syncs that are for different databases. But they'll all take exclusive
locks on `::batch-permissions-update-<db>`, so any permissions changes
they're making to the database will be serialized.
Allow moving dashboard questions to their parent dashboard's collection
The MoveModal's isSameDestination() was comparing the question's
collection_id to the target collection, blocking the move. But for
dashboard questions, the real "location" is the dashboard — moving to the
collection is a valid operation that detaches the question from the
dashboard. Added \!movingItem.dashboard_id to the collection check.
* Add keyboard shortcut (g s) to navigate to Data Studio
Closes UXW-3155
* adding permissions check, e2e test coverage
* e2e adjustment
---------
Co-authored-by: Nick Fitzpatrick <nickfitz.582@gmail.com>
* only render children if plugin is ready
* fix: wait for ee plugins to initialize before rendering children
* add unit tests for race condition
* fix: avoid module cache and always re-dispatch plugin ready
* fix: initialize EE plugins synchronously during render instead of useEffect
Moves plugin initialization out of useEffect into the render body,
eliminating the extra frame delay. Uses reduxStore.dispatch directly
following the same pattern as use-init-data-internal.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The /api/eid-translation routes were unauthenticated despite only being
used by frontend routes behind the IsAuthenticated guard. Wrap with +auth
and remove the stale "public facing" comment.
* Don't allow pausing JWT if it's on via env var
This separate the `jwt-enabled` setting into two parts:
- `jwt-enabled`, which is settable via an env var, and means "do I want
JWT to run or not"?
- `jwt-enabled-and-configured`, which is not settable at all, and simply
computes `(and (jwt-enabled) (jwt-configured))`. This one means "JWT is
running".
This way, if you set MB_JWT_ENABLED via the env, we don't give you the
option to pause or disable JWT anymore. If it's configured, it's on.
In some ways `MB_JWT_ENABLED` feels useless here, when set alone - it's
not really doing anything other than blocking the user from pausing JWT.
But I think in context it makes sense: you want to be able to configure
JWT solely via the env, but if you do that partially, it makes sense
that we just don't let you change the env-configured bits.
* kondo
* fix
* Implement table privileges support for SQLite.
This is based on code from https://github.com/metabase/metabase/pull/57000
but breaks it down into more manageable chunks.
* Set SQLite insert/update/delete privileges to true.
Unclear why the original PR hard-coded these to false; SQLite has no
permissions system which could prevent writes.
Getting this wrong doesn't affect anything in the backend; it's not
used anywhere outside the frontend, so I suppose it's somewhat
unrealistic to expect it to cause a test failure.
* Normalize query parameters in preprocessing
* Filter metadata results by user permissions
* Simplify permission checking logic
* Use server-controlled query context
* Validate query parameters at API boundaries
* Code review improvements
* Fixed failing test
* Fixed failing test
* Revert SQL driver to read persisted-info from query object
Instead of having the SQL driver load persisted-info from the metadata
provider, retain the old behavior where it reads :persisted-info/native
from the query map (set by fetch-source-query middleware).
---------
Co-authored-by: Edward Paget <ed.paget@gmail.com>
Co-authored-by: Nathan Voxland <nathan@metabase.com>
Documents the MCP server capabilities, endpoint, client setup
(Claude Code, Claude Desktop, Cursor), OAuth authentication,
scopes, available tools, JSON-RPC methods, and architecture.
When:
- a table was published,
- a user had data permissions for that table, and
- the user did NOT have permissions on the collection the table was
published to
Then the search endpoint would not return that table, even though it's
queryable and viewable via data permissions.
This fixes the filtering, so that the table is discoverable via search.
The issue reported that discard-setting-changes is broken for settings that use :init (e.g. query-analysis-native-disabled). The report predicted that calling the setting getter inside the macro would return nil, and that the value would be corrupted afterward.
do-with-discarded-setting-changes! used setting/read-setting to capture the "original" value before the thunk. read-setting binds *disable-init* to true, so for settings whose value comes from an :init function (and which haven't been initialized yet), it returns nil. This nil is then passed as both:
(outer capture) The "temporary" value to set during the thunk (via do-with-temporary-setting-value!)
(inner capture) The value to restore in the finally block (via the :skip-init? flag)
The key fix here is to use setting/get instead of setting/read-setting. We also remove the :skip-init? flag since it was there to match the semantics of read-setting and suppress the init call.
* Implement static UI for Usage controls page
* Added initial ai-controls tables
* Set sidenav menu items / Add placeholders for new pages
* Implement Metabot Customization page UI
* - Added metabot/permissions GET and PUT apis
- Added settings for metabot name/tone/icon
* Fixed migration ids
* fixup! bad merge
* Add :scope metadata to all Agent Loop tools and create metabase.metabot.scope namespace
- Create metabase.metabot.scope with *current-user-scope* (defaults to #{}),
scope-matches? with hierarchical wildcard support, and unrestricted set
- Add :scope to all 38 Agent Loop tool-fns with specific action scopes
(e.g. "agent:sql:create", "agent:viz:edit") — wildcards are grant-side only
- Propagate :scope through wrap-tools-with-state in tools.clj
- :capabilities remain unchanged (orthogonal data-permission gating)
- Add tests for scope-matches?, default scope, and stub
Done: tool-scope/phase-1-tool-scope-declarations
* Add scope-based authorization middleware for Agent Loop tools
- Filter tools by scope in get-tools-for-profile (profiles.clj)
- Check scope before tool invocation in wrap-tools-with-state (tools.clj),
returning a generic denial message (scope details logged, not leaked)
- Bind *current-user-scope* to unrestricted in run-agent-loop (temporary
until phase 4 wires real permission resolution)
- Add scope enforcement tests covering filtering, denial, wildcards,
no-scope passthrough, and default empty scope behavior
Done: tool-scope/phase-2-authorization-middleware
* Gate profile system prompts by metabot permission types
- Add *current-user-metabot-permissions* dynamic var to scope.clj
- Bind all-yes permissions in run-agent-loop (temporary, phase 4 wires real resolution)
- Derive has_sql_generation, has_nql, has_query_tools, has_other_tools
boolean flags from permissions and pass to Selmer template context
- Gate internal.selmer sections: critical constraints, verification protocol,
query building steps, SQL construction, SQL anti-patterns, SQL examples,
NLQ guidance, dashboard routing, and query-focused common scenarios
- General sections (identity, context grounding, communication style, search
example, entity linking) remain ungated for all permission levels
- 7 tests, 43 assertions covering all permission combinations
Done: tool-scope/phase-3-profile-prompt-gating
* Wire user-metabot-perms->scopes to metabot_permissions data model
- Implement resolve-user-permissions: looks up user's groups, queries
MetabotPermissions, resolves most-permissive value across groups
- Implement real user-metabot-perms->scopes: maps :yes permissions to
wildcard scope sets, always includes always-granted-scopes
- Replace temporary unrestricted/all-yes bindings in run-agent-loop with
real permission resolution from api/*current-user-id*
- Superusers get scope/unrestricted and all-yes-permissions
- Permission resolution done once at reify construction, not per-reduce
- Agent API scope binding deferred (tracked in rdm task agent-api-scope-binding)
- 38 new test assertions covering permission→scope mapping, single-group,
multi-group most-permissive-wins, and default scenarios
Done: tool-scope/phase-4-permissions-to-scopes
* Update permissions expected request format
* Include metabot and semantic-search tool options on the backend
* Connect access controls UI to the backend
* Wire up the backend for the Metabot Customize page
* Polish access controls page / Create MetabotSystemPromptsPage
* Added metabot-system-prompt
* Connect System prompt page to the backend
* Remove model selection + clean up
* Use custom metabot name
* Use metabot-icon in the UI + added metabot-show-illustrations toggle
* Removed metabase custom prompt
* Add defscope macro, scope registry, and api-scope module
Introduce a `defscope` macro that registers scopes with translatable
descriptions in a global registry, replacing inline scope strings across
all tool and endpoint metadata with typed var references.
Key changes:
- New `metabase.api-scope.core` module with defscope, scope registry,
scope-matches?, and public API (all-scopes, scope-description)
- All 25 agent scopes declared via defscope in metabase.metabot.scope
- 3 EE workspace scopes declared in metabase-enterprise.workspaces.scope
- validate-tool-var! rejects tools with unregistered scopes at startup
- defendpoint accepts var references for :scope metadata (not just strings)
- enforce-scope logs dev-time warnings for unregistered endpoint scopes
- MCP scope enforcement now handled by defendpoint middleware (removed
redundant pre-dispatch check); enforce-authentication preserves existing
token-scopes on synthetic requests
- Scope enforcement error messages no longer leak required scope names
- Consolidated duplicate scope-matching logic in mcp/tools.clj
Done: tool-scope/phase-5-defscope-registry
* Make semantic_queries optional in search tool schemas
When semantic search is not enabled, the feature gate strips
semantic_queries from the tool definition sent to the LLM, but Malli
validation still required it — causing an "Invalid input" error.
* Add missing agent:query scope declaration
The agent API query endpoint requires "agent:query" but it was never
registered via defscope, only its sub-scopes were.
* Block agent profiles by permission and add prompt gating to slackbot
Add profile-level permission checks in run-agent-loop that throw 403
when users lack required permissions for specialized profiles (:sql,
:nlq, :transforms_codegen, :document-generate-content). Add explicit
denial messages to internal.selmer and slackbot.selmer templates when
features are disabled, directing users toward available capabilities
like search instead.
* Enforce per-group metabot on/off switch and remove semantic-search permission
Add base permission/metabot check in check-metabot-access! that blocks
all agent profiles when the group-level metabot permission is :no.
Remove permission/metabot-semantic-search from the permissions model
and frontend. Fix permissions API test request body format and update
all-perm-types to match current permission set.
Done: task/enforce-metabot-group-onoff
* Integrate custom system prompts into internal, SQL, and NLQ profiles
Read the metabot-system-prompt admin setting in build-system-message-content
and pass it as a custom_instructions Selmer variable. Add {% if %} blocks
at the end of internal, sql-querying-only, and natural-language-querying-only
templates to render custom instructions when set. Use |safe filter to
avoid HTML-escaping admin-provided content.
Done: task/integrate-custom-system-prompts
* Added AI usage backend APIs and tables
* Add new system prompts pages
* Update the ai feature access page
* Update side nav for Usage controls menu item + Clean up
* Make the page headings margin top consistent
* Update Customization page / Remove metabot-tone
* Add default metabot permissions for magic user groups
Insert default metabot permission rows (all yes/medium) for the four
magic groups: Administrators, All Users, Data Analysts, and All Tenant
Users. Also fix missing remarks on group_id column in metabot_group_limit
table to pass migration linter.
* Add GET /api/metabot/permissions/user-permissions endpoint
Return the current user's resolved metabot permissions (most permissive
across all their groups). Superusers get all-yes permissions. Response
uses simplified key names without the permission/ namespace prefix.
Done: frontend-ai-permission-gating/phase-1-backend-user-permissions-endpoint
* Add useUserMetabotPermissions hook for granular AI permission checks
Add RTK Query endpoint for GET /api/metabot/permissions/user-permissions
and a useUserMetabotPermissions() hook that returns granular permission
booleans (canUseMetabot, canUseSqlGeneration, canUseNlq, canUseOtherTools)
combining the global metabot-enabled setting with per-user group permissions.
Done: frontend-ai-permission-gating/phase-2-frontend-permissions-hook
* Gate metabot UI entry points with granular user permissions
Replace useMetabotEnabledEmbeddingAware with useUserMetabotPermissions
in 6 primary metabot UI entry points: sidebar button (canUseMetabot),
AI exploration menu (canUseNlq), Fix SQL button and inline prompt
(canUseSqlGeneration), chart analysis and data studio buttons
(canUseMetabot). Update tests to mock the permissions API endpoint.
Done: frontend-ai-permission-gating/phase-3-gate-ui-entry-points
* Migrate remaining components from useMetabotEnabledEmbeddingAware to useUserMetabotPermissions
Replace useMetabotEnabledEmbeddingAware with useUserMetabotPermissions
in all remaining metabot UI components: Metabot shell, MetabotQueryBuilder
(canUseNlq), MetabotQuickLinks, AppBarLarge, CodeMirrorEditor
(canUseSqlGeneration), query builder context registration, MetabotEmbed,
and CommandSuggestion. Add permissions API mock to shared test utils and
all affected test files. SelectEmbedExperienceStep intentionally kept on
the old hook (uses forceEmbedding for embedding feature config check).
Done: frontend-ai-permission-gating/phase-4-migrate-remaining-embedding-aware-uses
* Remove usage limits menu item
* Fix migration to work with mysql
* Fix clj-kondo warnings on feat/ai-controls branch
- Add api-scope to mcp module and users to metabot module allowed deps
- Add response schema to /user-permissions defendpoint
- Suppress aliased-namespace-symbol warnings on potemkin import-vars
- Remove unused make-tool-var helper and prefix unused bindings with _
- Rename make-tool-var to make-tool-var! (uses alter-meta!)
* Address code review findings from clojure-review
- Add response schemas to GET / and PUT / permissions endpoints
- Add ^:parallel to 22 pure tests across 3 test files
- Move check-metabot-access! guard out of let binding for clarity
- Fix indentation, alignment, :refer :all, and stale kondo ignores
* Route agent API scope references through metabase.metabot.core
Re-export scope vars from metabase.metabot.core so external modules
use the public API namespace instead of metabot.scope directly.
* Address PR review comments
- Fix indent in api-scope module config
- Re-export workspace scopes from workspaces.core instead of using
workspaces.scope directly in agent_api/workspace.clj
- Extract wrap-with-scope-check into its own function in tools.clj
- Fix comment indentation in core_test.clj
* Add 'Settings and general limits' section on Usage limits page
* Add 'Group and tenant limits' section
* Tighten user-permissions response schema with exact permission fields
Use keyword keys and enum values matching the defined permission types
instead of loose [:map-of :string :string]. Also fix simplify-permissions
to produce keyword keys/values instead of strings.
* Separate foreign keys and indexes into distinct migration steps
Extract inline foreign key constraints from createTable into their own
changesets so FKs can be dropped independently of indexes/unique
constraints. Affects metabot_permissions, metabot_instance_limit, and
metabot_group_limit tables.
* Added api_usage_log tracking table and populate it through usage
* Add api-scope to driver-affecting-overrides
api-scope is a simple scope-registry utility that only depends on util.
It was triggering driver tests via its enterprise/workspaces dependent.
* Connect the backend API / Remove dollar limit type
* Initial llm limit checks
* Fix CI failures from AI controls + master merge
- Add MetabotPermissions/GroupLimit/InstanceLimit to copy entities
- Fix module config: sort api-scope, remove extraneous APIs/uses,
add PermissionsGroup to metabot model-imports
- Fix MCP scope-matches? returning false instead of nil on non-match
- Remove :capabilities gate from autogen dashboard tool
- Bind scope to unrestricted in profiles capability tests
- Update prompt gating tests to match master's template wording
- Update prompts test to match metabot_name template variable
- Update scope resolution tests for migration defaults (yes/medium)
* Remove usage limit code from AI controls branch
Usage limits (instance, tenant, and group) are being developed on a
separate branch. Remove all related backend code: models, API endpoints,
settings, migrations, and tests.
Done: ai-controls-to-enterprise/phase-1-remove-usage-limits
* Fix copy entities: deduplicate MetabotPrompt, move AI models to core section
* Move permission resolution and MetabotPermissions model to enterprise
Introduce the :ai-controls premium feature flag and restructure metabot
permission resolution for the OSS/EE split:
- Register enable-ai-controls? in premium_features/settings.clj
- Convert resolve-user-permissions to defenterprise (OSS returns defaults)
- Create EE implementation with full DB-backed group resolution
- Move MetabotPermissions Toucan2 model registration to EE namespace
- Keep pure permission type definitions and defaults in OSS
- Split tests: OSS verifies defaults, EE verifies group-based resolution
Done: ai-controls-to-enterprise/phase-2-defenterprise-permission-resolution
* Move permission admin APIs to enterprise with input validation
Move GET/PUT /api/metabot/permissions admin endpoints to
/api/ee/ai-controls/permissions, gated by :ai-controls feature.
Keep user-permissions endpoint in OSS. Add schema validation on
PUT perm_type/perm_value fields, fix 402 feature-gate test to use
correct assertion pattern, and add ^:parallel to all test defs.
* Move AI controls pages into new plugin
* Add error toast for instance limit update
* Remove unused permission/metabot-model (model size) feature
The metabot-model permission (large/medium/small) was defined
speculatively but never wired into model selection and had no UI.
Remove it from permission definitions, defaults, API schemas,
migration seed data, frontend types, and all tests.
Done: ai-controls-to-enterprise/phase-4-move-model-loading
* Gate metabot customization settings behind :ai-controls feature
Add :feature :ai-controls to branding settings (metabot-name,
metabot-icon, metabot-show-illustrations) and system prompt settings
(metabot-chat-system-prompt, metabot-nlq-system-prompt,
metabot-sql-system-prompt). Without the feature, reads return defaults
and writes are rejected.
Done: ai-controls-to-enterprise/phase-5-move-customization-settings
* Refactor MetabotPermissions: move model to OSS, replace deprecated APIs
- Move Toucan2 model registration from enterprise to
metabase.metabot.models.metabot-permissions (OSS)
- Replace mdb/update-or-insert! with t2/exists?+update!/insert!
- Replace user/group-ids with a subquery join through
PermissionsGroupMembership
- Update model resolution to point to OSS namespace
* Move MetabotPermissions model to EE, fix test failures for ai-controls feature
- Move MetabotPermissions Toucan2 model registration from OSS to
enterprise (metabase-enterprise.metabot.models.metabot-permissions)
- Move permission defs (metabot-permissions, perm-types, perm-type-defaults)
into metabase.metabot.scope alongside related permission/scope code
- Add :ai-controls to snowplow stats ping and session token features test
- Add MetabotPermissions to serialization excluded-models and entity-id
not-exported lists
- Update serialization baseline with new metabot settings
- Fix modules config: add api routes, model imports/exports for
enterprise/metabot; remove stale users dep from metabot module
* Fix kondo warnings in settings_test: use true?, remove ^:parallel
* Add usage limit models and API as enterprise-only endpoints
Restore the usage limit code (instance, tenant, and group limits) that
was removed in 2db9f363, now scoped under enterprise modules and the
/api/ee/ai-controls/usage/ route prefix with :ai-controls feature gate.
* Address clojure-review findings: per-type perm validation, privacy, metadata
- Validate perm_value per perm_type in PUT endpoint (not just flat enum)
- Make format-native-query private
- Add ^{:added "0.61.0"} to enable-ai-controls?
- Add clarifying comment on enforce-scope dev-time warning
* - Add AI usage limits enforcement, logging, and cleanup
- Rename metabot-limit-type to metabot-limit-unit
- Add scheduled task to trim ai_usage_log rows older than 3 months
* Move ai controls RTK endpoints to enterprise folder
* Fix input debounced state issue
* Clean up on Group and tenant limits tabs
* - Add AI usage limits enforcement, logging, and cleanup
- Rename metabot-limit-type to metabot-limit-unit
- Add scheduled task to trim ai_usage_log rows older than 3 months
* Cleaned up ai_controls migrations
* Merged from ai-controls
* Fix CI test failures caused by ai-controls permission system
The new check-metabot-access! gate in run-agent-loop rejects
non-superusers whose permissions default to :no. Honor pre-bound
*current-user-metabot-permissions* so tests can bind permissions
directly. Also fix scope error message expectation, gate custom
prompt settings behind :ai-controls feature, increase poll timeout
for closing-connection test, and split large deftests into smaller
parallel tests.
* Move EE-dependent permissions test to enterprise, ignore metabot_permissions in H2 dump comparison
The user-permissions test that creates :model/MetabotPermissions belongs
in EE tests since the model is EE-only. The metabot_permissions table is
created by migration in all editions but only included in H2 dumps when
EE is available, so add it to the ignored tables in the dump comparison
utility.
* Fixed namespace reference
* Make cache ttl be 10 seconds
* Fix FE test failures by mocking metabot user-permissions endpoint
The new useUserMetabotPermissions hook calls /api/metabot/permissions/user-permissions
in many component trees. Tests that render these components were failing due to
unmocked API calls. Added setupUserMetabotPermissionsEndpoint helper and called it
in each affected test's setup.
* Mark update-permissions-test as non-parallel to fix MariaDB CI failures
* Fix prettier formatting in use-user-metabot-permissions test
* Fix TypeScript errors: missing settings defaults, stale token feature refs, type mismatches
* Fix eslint: remove unused createMockTokenFeatures imports, fix import ordering
* Compute token max in millions of tokens
* If any groups have a null limit, return null as the limit
* Improve state handling on group and limits components
* Add unit tests
* Fix metabot/new quicklink / don't call user-permissions if metabot is globally disabled
* Remove test flakiness
* Address PR review comments: remove scope re-exports, use enums, split tests
- Remove potemkin re-exports from metabot.scope; consumers now use
api-scope directly for scope-matches?, unrestricted, registered-scope?,
all-scopes, and scope-description
- Use proper malli enums for perm_type/perm_value in EE permissions
response schema instead of bare :keyword
- Sort required-feature->message alphabetically in EE routes
- Fix api-scope indent in kondo config
- Delete unused keep-me comment in EE metabot routes
- Split large deftests into focused tests in permissions_test,
scope_resolution_test, and profiles_test
- Remove unnecessary blank lines between testing blocks in api_test
* Exclude new AI usage limit models from serialization and update module config
Add AiUsageLog, MetabotGroupLimit, MetabotInstanceLimit, and
MetabotPermissions to serialization excluded-models, entity-id
not-exported, and copy-test exclusion lists. Update enterprise/metabot
module config with routes API, app-db/users dependencies, and
PermissionsGroup model import. Add new metabot settings to
serialization baseline.
* Fix kondo warnings: sort namespace requires alphabetically
* Fix namespace require ordering in agent core
* Fix add missing init module
* Fix module config for AI usage limit models
Update enterprise/metabot module: remove stale app-db and users deps,
add AiUsageLog model import. Export AiUsageLog from metabot module.
Remove stale internal-stats.metabot from internal-stats API.
* Add e2e tests for basic ai controls
* Fix serialization baseline: rename metabot-limit-type to metabot-limit-unit
* Fix e2e tests
* Add e2e tests for ai usage limits
* Polish tests
* Fix e2e tests
* Fix bug on custom quota-reached message input
* Fix issues with auto-saving and debounced requests
* Address further review comments
* Set max limit number on inputs
* Fix some tests
* Fix flaky MetabotFeatureAccessPage test by awaiting permission state
* Fix unit test
* Add anthropic api mock to test group quota limit more properly
* Remove stale metabot dependency from testing-api module config
* Fix nql→nlq typo in permission type and OSS default permissions
The permission type `permission/metabot-nql` was misspelled throughout
the codebase (should be `nlq` for Natural Language Query). Also fixed
the prompt file reference `nql_search.md` → `nlq_search.md` which was
a runtime bug, and updated the OSS fallback for resolve-user-permissions
to return all-yes (permissive) instead of all-no defaults.
* Move DB query helpers from permissions API into MetabotPermissions model
Move permissions-for-group and all-permissions from the API namespace
into the model namespace where they belong as data-access functions.
* Fix flaky resolve-user-permissions-nil-test
Return all-yes-permissions for nil user-id in both OSS and EE paths
(no user means no restrictions). Wrap test with with-premium-features
to make it deterministic regardless of token presence.
* Fix defenterprise-schema nil return schema causing OSS test failures
defenterprise-schema for log-ai-usage! had no return schema, which
produced nil and crashed malli validation in OSS mode. This broke the
report-token-usage-xf transducer pipeline, preventing snowplow events
from firing in downstream transducers.
* Renamed "conversations" to "messages"
* Add tenant limits e2e test / Address a few other review comments
* Fix more review comments
* Clean up on other pages to reuse new hook
* Move translation maps outside their functions / Fix config.edn issue
* Do not reset limits when switching limit types
* Fix e2e test
---------
Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Edward Paget <edpaget@users.noreply.github.com>
Co-authored-by: Edward Paget <ed.paget@gmail.com>
* metabot: ability to make requests via ai proxy w/o having api key configured
* modernize logic: BYOK is more important than ai proxy
* dry error messages and tests
* metabot token reporting
* byok setting
* track that ai-proxied messages are marked in the db
* only count ai_proxied in tokens reported
* fix ci concerns
* metabase/ as indicator of requests going through ai-proxy
* Port resolve-auth and test fixes from #71655
* Add claude-list-models-auth-preferences-test similar to claude-auth-preferences
* Add conventional -test suffix to auth-preferences tests
* BOT-1262 Add :feature :metabase-ai-provider guard to llm-proxy-base-url defsetting
Rather than moving the defsetting into ee add a :feature :metabase-ai-provider guard and leave it in oss.
* Fix trailing whitespace
* Fix logic in llm-provider-configured?
Logic now matches metabase.metabot.self.core/resolve-auth, i.e. no proxy fallback if llm-metabot-provider is unconfigured.
* Remove unused func
* Add more tests for metabase proxied models
Add tests for:
- validate-metabot-provider!
- /list-models endpoint provider resolution for both direct and metabase/ prefix
- slackbot streaming ai-proxy? propagation to store-message!
* Add metabot.provider-util namespace for parsing model-and-provider strings
And use it instead of ad-hoc parsing with str/split in various places.
* Remove unused metabot.core/routes
Re-exported from metabot.api/routes but nothing references it.
* Prefer with-redefs over mt/with-temporary-setting-values in tests to override env vars too
Specifically for tests that set llm-anthropic-api-key to ignore any value set in the env.
* Fix token usage reporting for openai
OpenAI returns token usage in :input_tokens and :output_tokens, map those back to out internal :promptTokens and
:completionTokens, respectively and also drop any nested token usage *_details that get returned to prevent an
exception in accumulate-usage-xf when attempting to (merge-with +).
java.lang.ClassCastException: class clojure.lang.PersistentArrayMap cannot be cast to class java.lang.Number (clojure.lang.PersistentArrayMap is in unnamed module of loader 'app'; java.lang.Number is in module java.base of loader 'bootstrap')
at clojure.lang.Numbers.add(Numbers.java:155)
at clojure.core$_PLUS_.invokeStatic(core.clj:994)
at clojure.core$_PLUS_.invoke(core.clj:986)
at clojure.core$merge_with$merge_entry__6053.invoke(core.clj:3087)
at clojure.core$reduce1.invokeStatic(core.clj:946)
* Review feedback: use str/replace-first
* Review feedback: ensure metabot-provider-schema matches supported-metabot-providers
---------
Co-authored-by: Mike Appleby <86076+appleby@users.noreply.github.com>
* No "login from new device" emails for `to_session`
Prevent the /api/sso/to_session endpoint from sending "login from new
device" emails. This endpoint is used for programmatic JWT-to-session
token exchange, so new device notifications are noise. Login history is
still recorded for audit purposes.
* kondo fix
* fix tests
When a card with an alert is permanently deleted, the Notification record and its Quartz cron trigger survive.
The trigger keeps firing and crashing every time, clogging logs and task history indefinitely.
The root cause is that Card's before-delete hook cleans up ParameterCards, ModerationReviews, and Revisions — but not Notifications. The notification_card table has a deleteCascade FK to report_card, so the NotificationCard payload row gets cascade-deleted, but the Notification row itself survives with a dangling payload_id.
There's 2 fixes in this PR:
Add notification cleanup to Card's before-delete hook. Materializes notification IDs first because Notification's own before-delete deletes the NotificationCard, which would invalidate a delete that depends on a subquery by the time the actual DELETE executes.
For notifications orphaned before this fix is deployed, guard in send-notification-sync! checks if the NotificationCard payload still exists before hydration. If not, it deletes the notification and throws a clean error.
We fully delete rather than deactivate (setting :active false) because an orphaned notification with a deleted card has no value — its handlers, subscriptions, and recipients would just be dead rows. This matches what happens when users manually delete an alert via delete-alert-and-notify!.
* Implement static UI for Usage controls page
* Added initial ai-controls tables
* Set sidenav menu items / Add placeholders for new pages
* Implement Metabot Customization page UI
* - Added metabot/permissions GET and PUT apis
- Added settings for metabot name/tone/icon
* Fixed migration ids
* fixup! bad merge
* Add :scope metadata to all Agent Loop tools and create metabase.metabot.scope namespace
- Create metabase.metabot.scope with *current-user-scope* (defaults to #{}),
scope-matches? with hierarchical wildcard support, and unrestricted set
- Add :scope to all 38 Agent Loop tool-fns with specific action scopes
(e.g. "agent:sql:create", "agent:viz:edit") — wildcards are grant-side only
- Propagate :scope through wrap-tools-with-state in tools.clj
- :capabilities remain unchanged (orthogonal data-permission gating)
- Add tests for scope-matches?, default scope, and stub
Done: tool-scope/phase-1-tool-scope-declarations
* Add scope-based authorization middleware for Agent Loop tools
- Filter tools by scope in get-tools-for-profile (profiles.clj)
- Check scope before tool invocation in wrap-tools-with-state (tools.clj),
returning a generic denial message (scope details logged, not leaked)
- Bind *current-user-scope* to unrestricted in run-agent-loop (temporary
until phase 4 wires real permission resolution)
- Add scope enforcement tests covering filtering, denial, wildcards,
no-scope passthrough, and default empty scope behavior
Done: tool-scope/phase-2-authorization-middleware
* Gate profile system prompts by metabot permission types
- Add *current-user-metabot-permissions* dynamic var to scope.clj
- Bind all-yes permissions in run-agent-loop (temporary, phase 4 wires real resolution)
- Derive has_sql_generation, has_nql, has_query_tools, has_other_tools
boolean flags from permissions and pass to Selmer template context
- Gate internal.selmer sections: critical constraints, verification protocol,
query building steps, SQL construction, SQL anti-patterns, SQL examples,
NLQ guidance, dashboard routing, and query-focused common scenarios
- General sections (identity, context grounding, communication style, search
example, entity linking) remain ungated for all permission levels
- 7 tests, 43 assertions covering all permission combinations
Done: tool-scope/phase-3-profile-prompt-gating
* Wire user-metabot-perms->scopes to metabot_permissions data model
- Implement resolve-user-permissions: looks up user's groups, queries
MetabotPermissions, resolves most-permissive value across groups
- Implement real user-metabot-perms->scopes: maps :yes permissions to
wildcard scope sets, always includes always-granted-scopes
- Replace temporary unrestricted/all-yes bindings in run-agent-loop with
real permission resolution from api/*current-user-id*
- Superusers get scope/unrestricted and all-yes-permissions
- Permission resolution done once at reify construction, not per-reduce
- Agent API scope binding deferred (tracked in rdm task agent-api-scope-binding)
- 38 new test assertions covering permission→scope mapping, single-group,
multi-group most-permissive-wins, and default scenarios
Done: tool-scope/phase-4-permissions-to-scopes
* Update permissions expected request format
* Include metabot and semantic-search tool options on the backend
* Connect access controls UI to the backend
* Wire up the backend for the Metabot Customize page
* Polish access controls page / Create MetabotSystemPromptsPage
* Added metabot-system-prompt
* Connect System prompt page to the backend
* Remove model selection + clean up
* Use custom metabot name
* Use metabot-icon in the UI + added metabot-show-illustrations toggle
* Removed metabase custom prompt
* Add defscope macro, scope registry, and api-scope module
Introduce a `defscope` macro that registers scopes with translatable
descriptions in a global registry, replacing inline scope strings across
all tool and endpoint metadata with typed var references.
Key changes:
- New `metabase.api-scope.core` module with defscope, scope registry,
scope-matches?, and public API (all-scopes, scope-description)
- All 25 agent scopes declared via defscope in metabase.metabot.scope
- 3 EE workspace scopes declared in metabase-enterprise.workspaces.scope
- validate-tool-var! rejects tools with unregistered scopes at startup
- defendpoint accepts var references for :scope metadata (not just strings)
- enforce-scope logs dev-time warnings for unregistered endpoint scopes
- MCP scope enforcement now handled by defendpoint middleware (removed
redundant pre-dispatch check); enforce-authentication preserves existing
token-scopes on synthetic requests
- Scope enforcement error messages no longer leak required scope names
- Consolidated duplicate scope-matching logic in mcp/tools.clj
Done: tool-scope/phase-5-defscope-registry
* Make semantic_queries optional in search tool schemas
When semantic search is not enabled, the feature gate strips
semantic_queries from the tool definition sent to the LLM, but Malli
validation still required it — causing an "Invalid input" error.
* Add missing agent:query scope declaration
The agent API query endpoint requires "agent:query" but it was never
registered via defscope, only its sub-scopes were.
* Block agent profiles by permission and add prompt gating to slackbot
Add profile-level permission checks in run-agent-loop that throw 403
when users lack required permissions for specialized profiles (:sql,
:nlq, :transforms_codegen, :document-generate-content). Add explicit
denial messages to internal.selmer and slackbot.selmer templates when
features are disabled, directing users toward available capabilities
like search instead.
* Enforce per-group metabot on/off switch and remove semantic-search permission
Add base permission/metabot check in check-metabot-access! that blocks
all agent profiles when the group-level metabot permission is :no.
Remove permission/metabot-semantic-search from the permissions model
and frontend. Fix permissions API test request body format and update
all-perm-types to match current permission set.
Done: task/enforce-metabot-group-onoff
* Integrate custom system prompts into internal, SQL, and NLQ profiles
Read the metabot-system-prompt admin setting in build-system-message-content
and pass it as a custom_instructions Selmer variable. Add {% if %} blocks
at the end of internal, sql-querying-only, and natural-language-querying-only
templates to render custom instructions when set. Use |safe filter to
avoid HTML-escaping admin-provided content.
Done: task/integrate-custom-system-prompts
* Added AI usage backend APIs and tables
* Add new system prompts pages
* Update the ai feature access page
* Update side nav for Usage controls menu item + Clean up
* Make the page headings margin top consistent
* Update Customization page / Remove metabot-tone
* Add default metabot permissions for magic user groups
Insert default metabot permission rows (all yes/medium) for the four
magic groups: Administrators, All Users, Data Analysts, and All Tenant
Users. Also fix missing remarks on group_id column in metabot_group_limit
table to pass migration linter.
* Add GET /api/metabot/permissions/user-permissions endpoint
Return the current user's resolved metabot permissions (most permissive
across all their groups). Superusers get all-yes permissions. Response
uses simplified key names without the permission/ namespace prefix.
Done: frontend-ai-permission-gating/phase-1-backend-user-permissions-endpoint
* Add useUserMetabotPermissions hook for granular AI permission checks
Add RTK Query endpoint for GET /api/metabot/permissions/user-permissions
and a useUserMetabotPermissions() hook that returns granular permission
booleans (canUseMetabot, canUseSqlGeneration, canUseNlq, canUseOtherTools)
combining the global metabot-enabled setting with per-user group permissions.
Done: frontend-ai-permission-gating/phase-2-frontend-permissions-hook
* Gate metabot UI entry points with granular user permissions
Replace useMetabotEnabledEmbeddingAware with useUserMetabotPermissions
in 6 primary metabot UI entry points: sidebar button (canUseMetabot),
AI exploration menu (canUseNlq), Fix SQL button and inline prompt
(canUseSqlGeneration), chart analysis and data studio buttons
(canUseMetabot). Update tests to mock the permissions API endpoint.
Done: frontend-ai-permission-gating/phase-3-gate-ui-entry-points
* Migrate remaining components from useMetabotEnabledEmbeddingAware to useUserMetabotPermissions
Replace useMetabotEnabledEmbeddingAware with useUserMetabotPermissions
in all remaining metabot UI components: Metabot shell, MetabotQueryBuilder
(canUseNlq), MetabotQuickLinks, AppBarLarge, CodeMirrorEditor
(canUseSqlGeneration), query builder context registration, MetabotEmbed,
and CommandSuggestion. Add permissions API mock to shared test utils and
all affected test files. SelectEmbedExperienceStep intentionally kept on
the old hook (uses forceEmbedding for embedding feature config check).
Done: frontend-ai-permission-gating/phase-4-migrate-remaining-embedding-aware-uses
* Remove usage limits menu item
* Fix migration to work with mysql
* Fix clj-kondo warnings on feat/ai-controls branch
- Add api-scope to mcp module and users to metabot module allowed deps
- Add response schema to /user-permissions defendpoint
- Suppress aliased-namespace-symbol warnings on potemkin import-vars
- Remove unused make-tool-var helper and prefix unused bindings with _
- Rename make-tool-var to make-tool-var! (uses alter-meta!)
* Address code review findings from clojure-review
- Add response schemas to GET / and PUT / permissions endpoints
- Add ^:parallel to 22 pure tests across 3 test files
- Move check-metabot-access! guard out of let binding for clarity
- Fix indentation, alignment, :refer :all, and stale kondo ignores
* Route agent API scope references through metabase.metabot.core
Re-export scope vars from metabase.metabot.core so external modules
use the public API namespace instead of metabot.scope directly.
* Address PR review comments
- Fix indent in api-scope module config
- Re-export workspace scopes from workspaces.core instead of using
workspaces.scope directly in agent_api/workspace.clj
- Extract wrap-with-scope-check into its own function in tools.clj
- Fix comment indentation in core_test.clj
* Tighten user-permissions response schema with exact permission fields
Use keyword keys and enum values matching the defined permission types
instead of loose [:map-of :string :string]. Also fix simplify-permissions
to produce keyword keys/values instead of strings.
* Separate foreign keys and indexes into distinct migration steps
Extract inline foreign key constraints from createTable into their own
changesets so FKs can be dropped independently of indexes/unique
constraints. Affects metabot_permissions, metabot_instance_limit, and
metabot_group_limit tables.
* Add api-scope to driver-affecting-overrides
api-scope is a simple scope-registry utility that only depends on util.
It was triggering driver tests via its enterprise/workspaces dependent.
* Fix CI failures from AI controls + master merge
- Add MetabotPermissions/GroupLimit/InstanceLimit to copy entities
- Fix module config: sort api-scope, remove extraneous APIs/uses,
add PermissionsGroup to metabot model-imports
- Fix MCP scope-matches? returning false instead of nil on non-match
- Remove :capabilities gate from autogen dashboard tool
- Bind scope to unrestricted in profiles capability tests
- Update prompt gating tests to match master's template wording
- Update prompts test to match metabot_name template variable
- Update scope resolution tests for migration defaults (yes/medium)
* Remove usage limit code from AI controls branch
Usage limits (instance, tenant, and group) are being developed on a
separate branch. Remove all related backend code: models, API endpoints,
settings, migrations, and tests.
Done: ai-controls-to-enterprise/phase-1-remove-usage-limits
* Fix copy entities: deduplicate MetabotPrompt, move AI models to core section
* Move permission resolution and MetabotPermissions model to enterprise
Introduce the :ai-controls premium feature flag and restructure metabot
permission resolution for the OSS/EE split:
- Register enable-ai-controls? in premium_features/settings.clj
- Convert resolve-user-permissions to defenterprise (OSS returns defaults)
- Create EE implementation with full DB-backed group resolution
- Move MetabotPermissions Toucan2 model registration to EE namespace
- Keep pure permission type definitions and defaults in OSS
- Split tests: OSS verifies defaults, EE verifies group-based resolution
Done: ai-controls-to-enterprise/phase-2-defenterprise-permission-resolution
* Move permission admin APIs to enterprise with input validation
Move GET/PUT /api/metabot/permissions admin endpoints to
/api/ee/ai-controls/permissions, gated by :ai-controls feature.
Keep user-permissions endpoint in OSS. Add schema validation on
PUT perm_type/perm_value fields, fix 402 feature-gate test to use
correct assertion pattern, and add ^:parallel to all test defs.
* Move AI controls pages into new plugin
* Remove unused permission/metabot-model (model size) feature
The metabot-model permission (large/medium/small) was defined
speculatively but never wired into model selection and had no UI.
Remove it from permission definitions, defaults, API schemas,
migration seed data, frontend types, and all tests.
Done: ai-controls-to-enterprise/phase-4-move-model-loading
* Gate metabot customization settings behind :ai-controls feature
Add :feature :ai-controls to branding settings (metabot-name,
metabot-icon, metabot-show-illustrations) and system prompt settings
(metabot-chat-system-prompt, metabot-nlq-system-prompt,
metabot-sql-system-prompt). Without the feature, reads return defaults
and writes are rejected.
Done: ai-controls-to-enterprise/phase-5-move-customization-settings
* Refactor MetabotPermissions: move model to OSS, replace deprecated APIs
- Move Toucan2 model registration from enterprise to
metabase.metabot.models.metabot-permissions (OSS)
- Replace mdb/update-or-insert! with t2/exists?+update!/insert!
- Replace user/group-ids with a subquery join through
PermissionsGroupMembership
- Update model resolution to point to OSS namespace
* Move MetabotPermissions model to EE, fix test failures for ai-controls feature
- Move MetabotPermissions Toucan2 model registration from OSS to
enterprise (metabase-enterprise.metabot.models.metabot-permissions)
- Move permission defs (metabot-permissions, perm-types, perm-type-defaults)
into metabase.metabot.scope alongside related permission/scope code
- Add :ai-controls to snowplow stats ping and session token features test
- Add MetabotPermissions to serialization excluded-models and entity-id
not-exported lists
- Update serialization baseline with new metabot settings
- Fix modules config: add api routes, model imports/exports for
enterprise/metabot; remove stale users dep from metabot module
* Fix kondo warnings in settings_test: use true?, remove ^:parallel
* Address clojure-review findings: per-type perm validation, privacy, metadata
- Validate perm_value per perm_type in PUT endpoint (not just flat enum)
- Make format-native-query private
- Add ^{:added "0.61.0"} to enable-ai-controls?
- Add clarifying comment on enforce-scope dev-time warning
* Cleaned up ai_controls migrations
* Fix CI test failures caused by ai-controls permission system
The new check-metabot-access! gate in run-agent-loop rejects
non-superusers whose permissions default to :no. Honor pre-bound
*current-user-metabot-permissions* so tests can bind permissions
directly. Also fix scope error message expectation, gate custom
prompt settings behind :ai-controls feature, increase poll timeout
for closing-connection test, and split large deftests into smaller
parallel tests.
* Move EE-dependent permissions test to enterprise, ignore metabot_permissions in H2 dump comparison
The user-permissions test that creates :model/MetabotPermissions belongs
in EE tests since the model is EE-only. The metabot_permissions table is
created by migration in all editions but only included in H2 dumps when
EE is available, so add it to the ignored tables in the dump comparison
utility.
* Fix FE test failures by mocking metabot user-permissions endpoint
The new useUserMetabotPermissions hook calls /api/metabot/permissions/user-permissions
in many component trees. Tests that render these components were failing due to
unmocked API calls. Added setupUserMetabotPermissionsEndpoint helper and called it
in each affected test's setup.
* Mark update-permissions-test as non-parallel to fix MariaDB CI failures
* Fix prettier formatting in use-user-metabot-permissions test
* Fix TypeScript errors: missing settings defaults, stale token feature refs, type mismatches
* Fix eslint: remove unused createMockTokenFeatures imports, fix import ordering
* Add unit tests
* Fix metabot/new quicklink / don't call user-permissions if metabot is globally disabled
* Remove test flakiness
* Address PR review comments: remove scope re-exports, use enums, split tests
- Remove potemkin re-exports from metabot.scope; consumers now use
api-scope directly for scope-matches?, unrestricted, registered-scope?,
all-scopes, and scope-description
- Use proper malli enums for perm_type/perm_value in EE permissions
response schema instead of bare :keyword
- Sort required-feature->message alphabetically in EE routes
- Fix api-scope indent in kondo config
- Delete unused keep-me comment in EE metabot routes
- Split large deftests into focused tests in permissions_test,
scope_resolution_test, and profiles_test
- Remove unnecessary blank lines between testing blocks in api_test
* Fix kondo warnings: sort namespace requires alphabetically
* Fix namespace require ordering in agent core
* Add e2e tests for basic ai controls
* Fix e2e tests
* Fix issues with auto-saving and debounced requests
* Address further review comments
* Fix some tests
* Fix flaky MetabotFeatureAccessPage test by awaiting permission state
* Fix nql→nlq typo in permission type and OSS default permissions
The permission type `permission/metabot-nql` was misspelled throughout
the codebase (should be `nlq` for Natural Language Query). Also fixed
the prompt file reference `nql_search.md` → `nlq_search.md` which was
a runtime bug, and updated the OSS fallback for resolve-user-permissions
to return all-yes (permissive) instead of all-no defaults.
* Move DB query helpers from permissions API into MetabotPermissions model
Move permissions-for-group and all-permissions from the API namespace
into the model namespace where they belong as data-access functions.
* Fix flaky resolve-user-permissions-nil-test
Return all-yes-permissions for nil user-id in both OSS and EE paths
(no user means no restrictions). Wrap test with with-premium-features
to make it deterministic regardless of token presence.
---------
Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Edward Paget <edpaget@users.noreply.github.com>
Co-authored-by: Edward Paget <ed.paget@gmail.com>
* fix: extract 20 unsafe expressions from run blocks to env vars
Move ${{ }} expressions used in shell `run:` blocks into step-level
`env:` mappings to prevent script injection. Affected workflows:
athena, drivers, e2e-stress-test, frontend-unit-test-stress-test,
perf-test, pr-env, pr-env-destroy, release-embedding-sdk, repro-bot,
tag-for-release.
This is part 2 of a split from #71610 as requested.
* fix: use C-style for loop for burn_in variable expansion
Bash brace expansion ({1..N}) happens before variable expansion, so
{1..$VAR} doesn't expand as a range. Use a C-style for loop instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: extract inline PGPASSWORD secret to env var
The PR_ENV_DB_PASSWORD was left inline in run blocks while other
secrets in the same steps were already extracted to env mappings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: rename REF_NAME env var to IS_MASTER_OR_RELEASE in drivers.yml
The variable holds a boolean expression result, not a ref name. The
new name matches the CLI flag it's passed to (--is-master-or-release).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: extract remaining inline workflow_dispatch inputs to env vars
Extracts spec, grep, burn_in, version, and issue_number inputs that
were still interpolated directly in run blocks. Also simplifies env
var names by dropping the redundant INPUT_ prefix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* style: place env block before run block in athena.yml
Consistent with the convention used in all other steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Chris Nyhuis <cnyhuis@vigilantnow.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* An empty commit for the integration branch
* Extract JWT utilities into dedicated lib/jwt.ts module (#71515)
* Extract JWT utilities into dedicated lib/jwt.ts module
Moves `isJWT`, `decodeJwt`, `extractResourceIdFromJwtToken`, and `redactJwt`
from `lib/utils.ts` into a new `lib/jwt.ts`. Improves JWT decoding with safer
UTF-8 handling via `TextDecoder`. Updates all import sites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Rename utils.unit.spec.ts to jwt.unit.spec.ts after moving extractResourceIdFromJwtToken
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Remove redactJwt and show full error strings in JWT error messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix AI slop
* Fix unit test
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix SdkError too wide (#71569)
* Extract JWT utilities into dedicated lib/jwt.ts module
Moves `isJWT`, `decodeJwt`, `extractResourceIdFromJwtToken`, and `redactJwt`
from `lib/utils.ts` into a new `lib/jwt.ts`. Improves JWT decoding with safer
UTF-8 handling via `TextDecoder`. Updates all import sites.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Rename utils.unit.spec.ts to jwt.unit.spec.ts after moving extractResourceIdFromJwtToken
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Remove redactJwt and show full error strings in JWT error messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix AI slop
* Fix unit test
* Add Storybook stories for SdkError component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix mage error
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Add token renewal infrastructure for guest embeds (#71516)
* Add token renewal infrastructure for guest embeds
Implements the full token renewal mechanism across three layers:
- embed.ts: adds _callGuestTokenProvider(), _fetchInitialGuestToken(), and
_refreshGuestToken() to MetabaseEmbedElement; fetches tokens from
guestEmbedProviderUri and serves refresh requests via postMessage
- request-session-token.ts + Redux store (auth.ts, guest-embed.ts): SDK-side
postMessage protocol for requestGuestTokenRefresh / submitRefreshedGuestToken;
getOrRefreshGuestSession thunk checks JWT expiry and refreshes transparently
- Plugin hooks (api.ts, embedding-sdk.ts, api.js, override-requests-for-embeds):
wires refresh handler into the API request pipeline; adds x-metabase-guest-embed
header and :entityIdentifier URL substitution
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* clean up AI slop
* Fix another AI slop
* Fix more slop
* fix embed wizard parameter not affecting the preview for 1 render
* Fix string custom-context not working
* Address review feedback
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Wire token renewal into dashboard/question components and add E2E tests (#71517)
* Wire token renewal into dashboard/question components and add E2E tests
- SdkDashboard and SdkQuestionProvider now dispatch setInitialGuestToken on
mount, storing the raw JWT in Redux so the request interceptor can refresh it
transparently when it expires
- use-load-question and use-common-dashboard-params updated for guest embed mode
- SdkIframeEmbedRoute: new route component for the iframe embed SDK
- guest-token-refresh.cy.spec.ts: comprehensive E2E Cypress suite covering
token expiry, automatic refresh, error paths for both iframe and SDK modes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix _fetchInitialGuestToken to omit dashboardId/questionId from settings
In the guest embed token flow the JWT already encodes the resource, so
dashboardId and questionId should not be forwarded to the iframe via
setSettings. Pass them as undefined to override the values that
_updateSettings merges in from this.properties.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix Kelvin slop
* Make E2E tests consistent
* Make test more complete
* add custom context assertions
* address review feedback
* Address review
* Fix failed E2E tests
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Fix that one test that's always failing
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Pin all third-party GitHub Actions to immutable 40-character commit
SHAs to prevent supply chain attacks via tag rewriting. Version
comments added for readability.
Prevent enabling database routing on a database that already has transforms, since running transforms on databases with DB routing is not supported
Backend: added validation check in PUT /api/ee/database-routing/router-database/:id that returns 400 if transforms exist for the database
Frontend: disables the routing toggle in the UI when transforms exist, with a descriptive message
Also wired through the existing but unused database-id filter param in the transforms list API
* Add GitHub Action to auto-resolve backport conflicts with Claude
Adds a new workflow triggered by the `auto-resolve-conflicts` label (or
manual dispatch) that uses Claude Code to automatically resolve cherry-pick
conflicts on backport PRs. This eliminates the manual step of checking out
the branch, running backport.sh, and hand-resolving conflicts.
* Add kondo and typescript checks
* Add syntax validation and formatting to backport conflict resolver
- Prepare backend (JDK/Clojure) and frontend (Node/Bun) environments
- Run cljfmt and eslint --fix to auto-format resolved files
- Run clj-kondo and tsc to validate syntax before completing cherry-pick
- Claude loops on errors until files are clean before committing
* Make Claude max turns configurable in backport conflict resolver, default to 50
* Fix Bash permission pattern for cljfmt in backport conflict resolver
Use colon-star syntax (cljfmt:*) to match other Bash tool permission
patterns, fixing permission denial for ./bin/mage cljfmt-files.
* Detect yarn vs bun for older release branches in backport resolver
Release branches ≤58 use yarn, ≥59 use bun. Detect the package manager
by checking for bun.lock after checkout, then use it for the CLJS build
step, allowed_tools permissions, and Claude prompt instructions.
* Set NODE_VERSION=22 for claude-code-base-action step
The action internally runs setup-node with 18.x by default, overriding
the Node 22 installed by prepare-frontend. This caused yarn engine
check failures on older release branches. The action respects a
NODE_VERSION env var, so set it to 22.
* Improve backport resolver: detect max turns, add commit details to success message
- Bump default max turns from 50 to 75
- Detect when Claude exits due to max_turns exceeded and post a specific
comment explaining the issue with a link to re-run the workflow
- Include a commit link and collapsible changed files list in the success comment
* reject multi statement queries
* add dialect
* update pr
* use multimethod
* mark driver tests
* remove join for clickhouse
* kebab case
* fix e2e test
* feat: add MCP apps CORS settings page at /admin/metabot/mcp
Add a new settings page where admins can enable CORS for popular MCP
app clients (Claude, ChatGPT, Cursor/VS Code) via checkboxes, and
add custom domains for self-hosted clients like LibreChat.
Backend: new `common-mcp-apps-cors-origins` (CSV) and
`custom-mcp-apps-cors-origins` (string) settings in mcp/settings.clj.
Security middleware merges these with embedding SDK origins in
`access-control-headers`. Special handling for vscode-webview:// prefix.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address design review — use toggles, fix casing, remove sublines
- Switch from Checkbox to Switch toggles for MCP client selection
- "MCP Apps" → "MCP apps" in sidebar and page title
- Remove redundant description sublines from client options
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: extract MCP CORS tests to separate security_mcp_test file
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* add unit tests for mcp apps
* rename: common-mcp-apps-cors-origins → mcp-apps-cors-enabled-clients
The setting stores client keys (claude, vscode, chatgpt), not origins.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* rename: custom-mcp-apps-cors-origins → mcp-apps-cors-custom-origins
Consistent mcp-apps-cors- prefix for both settings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add mcp to driver-affecting-overrides
MCP settings are infrastructure, not driver logic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* add cors origins parsing improvements from @crisptrutski
Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
* move mcp-apps-sandbox-origin? from security middleware to mcp.settings
Encapsulates MCP sandbox origin logic in the mcp module, uses condp
for extensibility when more non-standard schemes are added.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* add oauth-server to mage modules list
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* add mcp.core as public API, keep mcp.settings internal
External consumers (e.g. security middleware) now go through mcp.core
instead of reaching into mcp.settings directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* move application logic from mcp.settings to mcp.core
mcp.settings now only contains pure config (defsettings + cors-origins).
vscode-webview-enabled? and sandbox-origin? live in mcp.core.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* remove useEffect state sync from MCP apps settings
Local state is kept for optimistic UI updates, but the useEffect sync
from savedValue is unnecessary since these settings won't change from
an external source concurrently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* rename "MCP app clients" to "MCP clients" in descriptions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* rename "vscode" client key to "cursor-vscode"
Aligns the key with the label "Cursor and VS Code" across frontend,
backend domain mapping, and tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* expand test to toggle all MCP clients
Use findByLabelText instead of testIds, and verify each toggle
accumulates the enabled clients list correctly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* add init for mcp
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
* hides overview tab of the metrics pages if no dimensions
* hide caching tab if no write access to metric
* update specs based on latest changes from master
* move the cors settings to oss
* add quick test for cors on oss
* test: update security middleware tests for OSS CORS behaviour
Add a dedicated test verifying CORS headers are sent when origins are
configured even without embedding features enabled (OSS path).
Update existing cases to reflect that removing the :embedding-sdk
feature flag from embedding-app-origins-sdk means the getter now
returns the actual stored value in all contexts (previously returned ""
without the premium feature active in tests).
Remove 16 redundant test cases where sdk=true with non-empty origins
produced identical results to sdk=false with non-empty origins — now
that CORS is enabled whenever origins are configured, the sdk flag is
only meaningful when origins are empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* restore tests
* fix: remove redundant enable flags from CORS check and add OSS CORS test
The enable-embedding-sdk and enable-embedding-simple flags are no longer
needed to gate CORS headers — having configured origins is sufficient,
and localhost is always allowed via the existing passthrough. This
addresses PR review feedback to simplify the logic and add missing
frontend test coverage for CORS settings in OSS.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: update e2e tests for OSS/Starter CORS settings visibility
Security tab and CORS settings are now available on all plans,
not just EE. Update assertions to expect them to exist.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* Clean up workspace schema and user-name code
1. Single source of truth for the prefix — "mb__isolation" is defined once in metabase.driver.util (private), not three places
2. Matching methods live next to the constant — workspace-isolated-schema? (in-memory) and workspace-isolated-schema-clause (HoneySQL) so callers don't need to know the prefix or construct patterns themselves
3. Removed the requiring-resolve hack in the redshift test
4. Removed duplicated name generation — enterprise workspaces/util had identical copies of isolation-namespace-name, isolation-user-name, and instance-uuid-slug; callers now use driver.u directly
Closes [GDGT-2157: redshift tests require enterprise alias](https://linear.app/metabase/issue/GDGT-2157/redshift-tests-require-enterprise-alias)
* Avoid dynamic string building
* Use escape characters since _ is a wildcard
* Tighten up a bit more
* Remove stale include
* Move DataSegregationStrategy type to metabase-types/api/permissions
This is a permissions concept, not an embedding one. Moving it to
metabase-types resolves a module boundary violation (basic/api importing
from shared/other) and breaks a circular dependency between metabase/api
and metabase/embedding/embedding-hub.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fixes `GET /api/database/:id/usage_info` failing when a database has more than 65K tables
- The segment count query was passing all table IDs as parameters to an `IN` clause, which exceeds PostgreSQL's 65,535 parameter limit
- Replaced the `IN` clause with a subselect (`SELECT id FROM metabase_table WHERE db_id = ?`), eliminating the parameter explosion
- Removed the now-unnecessary `select-pks-set` call and `table-ids` argument from `database-usage-query`
* BOT-1223 Fix zero-arity tool call schemas for anthropic provider
Anthropic is stricter than openrouter about schemas for tool calls with no arguments. A schema like `[:maybe :map]`
produces a JSON schema like `{"oneOf": [{"type": "object", ...}, {"type": "null"}]}` but Anthropic's API requires
input_schema to be `{"type": "object", ...}` at the top level. A oneOf with a null variant is invalid.
* Handle nil arguments in metabase.metabot.self.core/run-tool
* Add failing test reproducing GHY-3254: sync_schema silently drops when executor is busy
The quick-task executor uses a single thread. If any task gets stuck
(e.g., a hanging JDBC connection), all subsequent manual sync requests
queue forever while the endpoint returns 200.
* Add per-task timeout to quick-task executor
Each task submitted to the quick-task executor now runs on a child
thread with a configurable timeout (*task-timeout-ms*, default 2h).
If a task exceeds the timeout, it is cancelled and the executor moves
on to the next queued task. This prevents a single stuck task from
blocking all subsequent manual sync requests.
The stuck thread's stack trace is logged for diagnostics.
* Use MB_QUICK_TASK_TIMEOUT_MINUTES env var for task timeout
Replace the dynamic var with a real env var (default 120 minutes).
* Use config/config-int for MB_QUICK_TASK_TIMEOUT_MINUTES
Read the env var via metabase.config.core instead of System/getenv,
consistent with how other config values are read in the codebase.
currently it is just two middleware: impersonation and db routing
this is necessary as we want to move into a world where we can compile a
query from yaml files without an app db file. These middleware are not
impportant for our purposes here.
This is also temporary work. This is slated to be fixed up in
[QUE2-488: Introduce an app-db protocol for QP
middleware](https://linear.app/metabase/issue/QUE2-488/introduce-an-app-db-protocol-for-qp-middleware)
The metabot describe endpoints (card and dashboard autodescription) are
no longer needed. This removes the API routes, task implementations,
OpenAI client wrapper, tests, and the now-unused openai-clojure and
lambdaisland/uri dependencies.
* BOT-1226 Only run metabot internal tasks when the llm-metabot-provider-lite is configured
The agent loop was already checking llm-metabot-internal-tasks-enabled? before running internal background tasks like
user intent classification. Switch the default value for llm-metabot-internal-tasks-enabled? from true to false and
update the :getter to check that the corresponding llm-metabot-provider-lite is configured.
* Review feedback: ensure llm-provider-configured? returns boolean
* BOT-1199 Add table-specific link examples to the internal metabot prompt
* Fix resolve-table-link to resolve to valid ad-hoc question link
The docstring mentioned avoiding a db lookup for the database id, but a link like `/table/<id>` is not a valid
metabase link.
* Cache metabase.metabot.agent.links/resolve-table-link result for 10 minutes
* Remove regex guard around parse-long in resolve-table-link
parse-long already returns nil if the parse fails.
* Fix require for application-database-metadata-provider
* Fix resolve-entity-link-test for tables and split it out into a new test
Exports are always full, so any .yaml file in a managed directory that isn't
in the current write set should be removed. This fixes stale files persisting
after entity renames, moves between collections, or deletions.
The git source now accepts a set of managed top-level directories. During
write-files!, existing files in managed dirs not in the write set are cleaned
up automatically. This replaces the previous path-prefix-based collection
scoping and the explicit build-all-removal-paths machinery, which couldn't
handle renames or cross-collection moves.
* Fix support access grant password reset failing for deactivated users
When a support user was deactivated (e.g. after a previous grant expired),
creating a new grant would leave the user inactive. The password reset
token authentication would succeed, the :after method would consume the
token and change the password, but create-session! would then reject the
login because is_active was false. with-fallback would swallow this and
fall back to emailed-secret-password-reset which also failed, resulting
in an opaque "Invalid reset token" error with the token irreversibly
consumed.
Fix: fetch-or-create-support-user! now reactivates deactivated users.
Also adds tests for the deactivated user flow, pre-existing auth identity
cascade, with-fallback error swallowing behavior, and direct login! calls
for easier debugging.
* fixup! cljfmt
The test overrode :max-attempts but diehard uses :max-retries, so the
override was silently ignored and all 6 retries ran with exponential
backoff (~31s). With the correct key the test finishes in ~1s.
Done: task/fix-notification-retry-key
Add a new sync-fork workflow that automatically syncs metabase internal forks
with upstream metabase/metabase (master + 5 most recent release branches).
Force-syncs from upstream as source of truth, skips push when no diff to
avoid unnecessary CI runs. Only runs on forks, never on the public repo.
Guard all scheduled workflows with repository check so they only run on
schedule for metabase/metabase, not on forks (manual dispatch
still works on both).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Field filter dropdowns on native query cards show as text inputs ("Enter some text") for users who have view-data permission but lack create-queries permission on the underlying table.
**Root cause:** Two issues combined to break this:
1. **Frontend couldn't determine dropdown type** — The frontend needs `has_field_values` from field metadata to decide whether to show a dropdown vs text input. This metadata came from `GET /api/database/:id`, which returns 403 for users without `create-queries` permission. Without it, the widget falls back to a text input.
2. **Backend param values endpoint was too restrictive** — Even if the frontend showed a dropdown, the card param values endpoint (`GET /api/card/:id/params/:key/values`) would fail because `search-values-from-field-id` does `api/read-check` on the Field (which requires `create-queries`), and the QP permission check also required `create-queries`.
**Fix:**
- **Hydrate `param_fields` on Card API response** (matching existing Dashboard pattern) — includes `has_field_values` so the frontend can show the correct widget type without needing `GET /api/database/:id`
- **Populate metadata store with `param_fields`** on card load in the query builder
- **Bind `*param-values-query*`** in card param values endpoints (like dashboards already do) so the QP only checks `view-data` permission
- **Skip `api/read-check` on Field** in `search-values-from-field-id` when `*param-values-query*` is true
* Extract Dashboard-only visualizations
Moved 4 dashboard-only visualization types from visualizations/visualizations/ to dashboard/visualizations/:
- Heading/ — imports dashboard context, selectors, parameter components
- IFrameViz/ — imports dashboard selectors
- LinkViz/ — imports dashboard selectors
- DashCardPlaceholder.tsx — imports dashboard actions
Created dashboard/visualizations/register.js — registers these 4 viz types separately from the main visualizations/register.js
Updated app.js — calls both registerVisualizations() and registerDashboardVisualizations()
Updated 3 external imports to use the new paths:
- dashboard/actions/utils.ts — IFrameViz utils
- parameters/utils/mapping-options.ts — IFrameViz utils
- PublicOrEmbeddedDashboardView-card-filters.stories.tsx — Heading
Fixed 1 relative import in DashCardPlaceholder.tsx to use absolute path for VisualizationProps type.
Violation progression: 157 → 111 → 86 → 78
* List DashboardChartSettings up
* Fix test setup
* Register dashboared viz in embedding
2026-03-30 22:01:47 +13:00
5211 changed files with 660074 additions and 518335 deletions
description: Export content from a running Metabase instance, validate with checkers, edit YAML, and import back. Use when the user wants to export, import, or run the full serdes round-trip workflow.
---
# Serdes Workflow Skill
End-to-end workflow for exporting Metabase content as YAML, validating it, editing it, and importing it back.
## Prerequisites
All commands require the `:ee` alias since export, import, and checkers live in enterprise code.
You need `MB_DB_CONNECTION_URI` set to point at the target Metabase Postgres database when importing. The running dev instance's connection string is in the worktree environment (check the nREPL or startup output).
## Export
```bash
clojure -M:run:ee export /path/to/export-dir
```
Flags:
-`-c 123,456` - export only specific collection IDs
-`-f` - include field values
-`-e` - continue on error
This writes YAML files in the serdes directory layout:
The `-e` flag continues on error. The `MB_DB_CONNECTION_URI` must point at the same Postgres database the running Metabase instance uses - without it, import defaults to H2 which is wrong.
If either checker fails, fix the issue before making further edits.
## Portable References
Serdes YAML uses portable references instead of integer IDs. This is the most important concept for editing.
### Database references
String name: `"Sample Database"`
### Table references
Array of `[database, schema, table]`:
```yaml
table_id:
- Sample Database
- PUBLIC
- ACCOUNTS
```
### Field references
Array of `[database, schema, table, field]`:
```yaml
id:
- Sample Database
- PUBLIC
- ACCOUNTS
- EMAIL
```
### Field refs in queries
```yaml
field_ref:
- field
- - Sample Database
- PUBLIC
- ACCOUNTS
- EMAIL
- null
```
The outer array is `[field, <field-path>, <options>]`. The options are usually `null`.
### Card references
Entity ID string (21 characters): `"Qk5TgsNx4ubXIUtsQmT8G"`
### Source table in queries
```yaml
dataset_query:
database:Sample Database
query:
source-table:
- Sample Database
- PUBLIC
- ACCOUNTS
type:query
```
## Safe Edits (low risk)
These rarely break validation:
-`name` - card/dashboard display name
-`description` - card/dashboard description
-`display` - visualization type (table, bar, line, pie, etc.)
-`visualization_settings` - chart configuration
-`archived` - true/false
-`collection_id` - move to a different collection (use entity_id of target collection, or null for root)
## Structural Edits (must match schema)
These must use valid portable refs and will be caught by the cards checker if wrong:
-`dataset_query` - the query definition
-`result_metadata` - column metadata (must match the query's output columns)
-`table_id` - must reference a table that exists in the export
-`database_id` - must reference a database that exists in the export
## Editing result_metadata
Each entry in `result_metadata` describes an output column. When changing a query's source table or fields, you must update result_metadata to match. Each field entry needs at minimum:
-`name` - column name (e.g., `EMAIL`)
-`base_type` - Metabase type (e.g., `type/Text`, `type/Integer`, `type/DateTime`)
-`display_name` - human-readable name
-`field_ref` - portable field reference
-`id` - portable field path
-`table_id` - portable table path
-`source` - usually `fields`
## Common Operations
### Rename a card
Change `name:` at the top level. Safe, no ref changes needed.
### Change a card's source table
Update all of these consistently:
1.`table_id` - top-level
2.`dataset_query.query.source-table` - in the query
3.`result_metadata` - every field entry's `id`, `field_ref`, and `table_id`
The export directory IS the reference catalog. When you need to find the correct name for a database, table, or field, look it up directly from the export.
### List valid databases
```bash
ls databases/
```
Each entry is a database name (directory for serdes format, .yaml for concise format).
### List valid tables for a database
```bash
ls databases/<db-name>/schemas/<schema>/tables/
```
Example: `ls databases/Sample\ Database/schemas/PUBLIC/tables/` shows `ACCOUNTS`, `ORDERS`, `PRODUCTS`, etc.
### List valid fields for a table
```bash
ls databases/<db-name>/schemas/<schema>/tables/<table>/fields/
```
Example: `ls databases/Sample\ Database/schemas/PUBLIC/tables/PRODUCTS/fields/` shows `CATEGORY.yaml`, `TITLE.yaml`, `PRICE.yaml`, etc. The filename (minus `.yaml`) is the field name.
### Find card entity IDs
Card filenames encode the entity ID: `<entity-id>_<slug>.yaml`. The entity ID is the part before the first underscore (21 characters).
## Self-Healing Loop
When a checker reports errors, follow this loop:
1.**Read the error message** - it tells you what's wrong and often suggests the fix
2.**Look up the correct value** from the export directory (see "Looking Up Valid References" above)
3.**Fix the YAML**
4.**Re-run both checkers**
5.**Repeat until clean**
Do not guess at fixes. Always look up the correct value from the export.
## Understanding Checker Errors
### Structural checker errors
The structural checker validates YAML shape against Malli schemas. Common errors:
**Missing required key with typo suggestion:**
```
Missing required key 'name' - found 'nameee' which may be a typo
```
Fix: rename the typo'd key back to the correct name. The checker tells you what it expected and what it found.
**Wrong type:**
```
'archived' should be a boolean, got: "yes"
```
Fix: use `true`/`false`, not strings.
**Unknown key:**
```
Unknown key 'foobar' in card
```
Fix: remove the key, or check if it's a typo of a known key.
### Cards checker errors
The cards checker validates that queries resolve against exported metadata. Error types:
The filename slug should match the card name (lowercase, underscored). If you rename a card, consider renaming the file to match, though import works on entity_id not filename.
## What NOT to Edit
-`entity_id` - this is the identity of the object, changing it creates a new object on import
-`serdes/meta` - internal serdes metadata, leave it alone
allowed_tools:"Read,Edit,Write,Bash(git:*),Bash(grep:*),Bash(find:*),Bash(ls:*),Bash(cat:*),Bash(clojure:*),Bash(${{ steps.pkg.outputs.mgr }} run type-check-pure:*),Bash(${{ steps.pkg.outputs.mgr }} run lint-eslint:*),Bash(./bin/mage cljfmt:*),Glob,Grep"
prompt:|
You are resolving git cherry-pick merge conflicts for a Metabase backport PR.
## Context
A commit from the `master` branch is being cherry-picked onto the `${{ steps.pr.outputs.base_ref }}` release branch.
The cherry-pick has failed with conflicts in these files:
${{ steps.backport.outputs.conflicted_files }}
## How to read conflict markers
```
<<<<<<< HEAD
(code from the release branch - the current state)
=======
(code from master - the change being backported)
>>>>>>> <commit-sha>
```
## Common conflict patterns in Metabase backports
- **Module/namespace renames**: A Clojure namespace was renamed or moved between versions. Check what the correct namespace is on the release branch.
- **Test file divergence**: Tests may have been added or reordered differently. Keep the backported test changes while preserving existing release-branch tests.
- **Import path changes**: Frontend imports may reference different paths. Check actual file locations on this branch.
- **Translation files (.po)**: For translation conflicts, prefer the release branch version of existing translations and add new translations from the backported change.
- **Minor context differences**: Surrounding code changed slightly. Adapt the backported change to fit the release branch context.
## Instructions
1. Read each conflicted file to understand the conflict
2. For context, you may search the codebase to understand the current state of the release branch (e.g., check if a module exists at a certain path)
3. Resolve each conflict by editing the file to remove ALL conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) and leave only the correct resolved code
4. The goal is to apply the INTENT of the backported change while adapting it to the release branch's code structure
5. Verify no conflict markers remain by searching for them
6. Auto-format resolved files:
- **Clojure/ClojureScript** (.clj, .cljc, .cljs): run cljfmt to fix formatting:
```
./bin/mage cljfmt-files <file1> <file2> ...
```
- **TypeScript/JavaScript** (.ts, .tsx, .js, .jsx): run eslint with auto-fix:
```
${{ steps.pkg.outputs.mgr }} run lint-eslint --fix
```
7. Validate resolved files before committing:
- **Clojure/ClojureScript** (.clj, .cljc, .cljs): run clj-kondo to verify syntax:
```
clojure -M:kondo --lint <file1> <file2> ...
```
- **TypeScript/JavaScript** (.ts, .tsx, .js, .jsx): run the type checker:
```
${{ steps.pkg.outputs.mgr }} run type-check-pure
```
If either tool reports errors or warnings, fix them before proceeding. CI treats kondo warnings as errors.
Repeat steps 6-7 until all errors are resolved.
7. Once all files are clean, stage and complete the cherry-pick:
```
git add -A
git cherry-pick --continue
```
8. Push the result:
```
git push --force
```
IMPORTANT: Do not skip any conflicted files. Every conflict marker must be resolved.
- name:Check for max turns exceeded
if:always() && steps.claude.outcome != 'skipped'
id:check-failure
run:|
if [ -f "${{ steps.claude.outputs.execution_file }}" ]; then
'🤖 Auto-resolution of backport conflicts stopped because Claude reached the maximum number of turns (${{ github.event.inputs.max_turns || '75' }}).',
'',
'The conflicts may be too complex for automatic resolution, or Claude needed more turns to finish.',
'',
'You can try again with more turns by manually running the [resolve-backport-conflicts workflow](https://github.com/' + context.repo.owner + '/' + context.repo.repo + '/actions/workflows/resolve-backport-conflicts.yml) with a higher `max_turns` value, or resolve the conflicts manually by checking out the branch and running `./backport.sh`.',
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.