The file bundled two unrelated concerns. Search tracking now lives in
api/analytics/search.ts next to its only caller, and the export
mutation in api/analytics/export.ts. The analytics/ directory satisfies
the no-analytics-import-outside-analytics-files rule that pinned the
old filename.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Show pinned items in the collection contents list
* Update e2e specs for pinned items appearing in the contents list
* Simplify the collection empty state offset to a plain vh value
* Simplify bookmark state, skip the pinned query in trash, and drop the dead toolbar prop
* Use a real pointer click for the pinned item menu so hover cannot be raced away
* fix: run source-card field-type inference before cross-stage inference (BOT-1604)
The repair pipeline mini-resolves earlier stages to type a later stage's
cross-stage field refs, but ran that pass before stamping base-type onto a
source-card stage's own bare-name field refs. The mini-resolve of a
source-card-sourced prefix then always failed schema validation and was
silently skipped, leaving cross-stage refs untyped and the query permanently
unrunnable for any multi-stage construct_notebook_query built on a saved
question/model.
* docs: fix stale pass numbering in repair pipeline docstring (BOT-1604)
Number the new source-card field-type pass 4.5 and keep the pre-existing
cross-stage pass as 5, instead of bumping cross-stage to 5.5 - this leaves
the other in-file references to "Pass 5" correct and matches the file's
existing fractional-pass-number convention.
- metric-details surfaces the metric's FK-less join dimensions (which the
consumer-framed dimension list silently drops), each carrying the exact
portable join clause to paste into `joins:` and an alias-qualified field
reference. Fail closed: an entry is emitted only when its join clause exports
and targets a real table; join-key columns are excluded.
- the :no-fk-path repair error is now actionable, pointing the agent at the
metric's /dimensions resource for the join to add.
Also fix a pre-existing option-key typo in resources.clj
(:with-queryable-dimensions / :with-field-values -> the ?-suffixed forms the
readers expect) so the plain metric resource correctly skips dimensions and
field-value fetches.
* docs - host the model button image in the repo
The troubleshooting guide linked the model button screenshot from
https://www.metabase.com/learn/images/models/model-icon.png, which now
404s with no redirect. Learn images were moved out of the marketing repo
into a CDN bucket, and that key space has been renamed several times with
old keys hard-deleted rather than redirected.
This broke the nightly Update Docs job in metabase/docs.metabase.github.io
on Aug 8-10. It only goes green today because the link gate tolerates one
broken link and the other one got fixed upstream.
Commit the image here instead of chasing the CDN. It matches how every
other image in docs/ is referenced, it gets versioned along with the
release branch so older doc snapshots keep the image they shipped with,
and it is the only form our link checkers actually validate: lychee runs
--offline (external URLs skipped) and the docs site's htmlproofer runs
--disable-external.
The source file was a WebP served with a .png extension, so it has been
transcoded to a real PNG - pngcrush in optimize-images.yml would have
failed on the original bytes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Optimized 1 images, saved 1.8 KB (1.7% smaller)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Metabase Automation <github-automation@metabase.com>
fix(ui): align Alert icon with text when there is no title
Icon box was locked to the 24px title row, so title-less alerts (e.g.
incremental transform warning) hung the icon above a single line of text.
Without a title, icon and close button now center on the first message line.
* BOT-1953: Suppress the Metabot sidebar on the conversation route
The full-page Metabot surface disables the app-bar button and the Cmd+E
shortcut so the chat cannot open on top of itself. That check compared the
pathname against /question/ask exactly, but #77549 added a second route
rendering the same MetabotAsk, /metabot/conversation/:convoId, which
MetabotAsk itself redirects to after the first message. The sidebar came
back from there on.
useIsFullPageMetabot matches either route and drives both suppression
sites. useIsAskPage stays strict for MetabotAsk's redirect, which must not
re-fire once it is already on the conversation route. The route path now
has one spelling, Urls.CONVERSATION_BASE_PATH, so a rename cannot silently
turn the suppression off again.
The conversation matcher pins the route shape rather than a prefix, with an
optional trailing slash because react-router matches the route either way.
Cmd+E had no coverage because a note in the test harness claimed tinykeys
shortcuts do not fire under jsdom. They do, bound to Control rather than
Meta since jsdom reports an empty navigator.platform.
* BOT-1953: Match the Metabot route predicates with react-router
useIsAskPage compared the pathname to /question/ask by string equality,
so /question/ask/, which react-router serves as the same page, left the
app-bar button and Cmd/Ctrl+E active on a full-page Metabot surface.
Both predicates now go through matchPath, the matcher the router itself
uses: trailing slash and case tolerated, extra segments and an empty
conversation id rejected.
* BOT-1953: Keep the urls module to plain URL builders
The two route predicates move into the metabot hooks that use them, which
now call matchPath directly; urls/ goes back to exporting only functions
that build strings. Their url-level specs go with them, since the metabot
ui and app-bar button specs already pin the behavior.
* remove dead workflows and support code
# Conflicts:
# .github/workflows/build-for-release.yml
# .github/workflows/main.yml
# .github/workflows/presto-kerberos-integration-test.yml
* remove dead clojure files
* Stop autopinning models and metrics on creation
* Fix e2e assertions that expected new models to be pinned
* Remove negative-case pin tests per review
* Add lazyModalRoute for modals under a code-split page
* Split documents into its own chunk
* Prefetch the documents chunk from links into it
* Prefetch the comments sidesheet with the document page
* Fix two Metabot tool failures surfaced by the reliability data
get_transform_details passed the transform's query map straight into the XML
formatter. clojure.data.xml reads a map in second position as an attribute map
and then throws on MBQL 5's namespaced :lib/type key, so every query-source
transform failed and only Python ones (no :query key) got through. That is 72
errors in 75 calls over 90 days, across 15+ instances. The query now renders as
SQL text, matching how the line below already handles :target.
create_sql_query has two implementations sharing one tool name, and neither used
the shared agent-error contract. The :sql profile's code-edit variant had no
error handling at all, so the :agent-error? exceptions validate-database-access
raises escaped uncaught and were tracked as tool failures. That variant errored
on 26% of calls against 0.04% for the :internal one. Both now delegate to
handle-agent-error, as the other tools do: the intended validation message goes
back to the model, and anything unexpected is rethrown so real defects stay
visible in the telemetry rather than being reported as successes.
Neither tool had a test; both now do.
* Address review: transform rendering and 403 classification
Transform source queries now render verbatim (data.xml escaped <, >, &,
which breaks old_string matching against the real query), python bodies
render, and the source type attribute is no longer a keyword literal.
Notebook-built queries fall back to EDN with the attached metadata
provider stripped, shared with transform->xml, whose string guard always
dropped the normalized (map) query.
validate-database-access tags the plain 403 from read-check so both
create_sql_query variants classify permission failures as agent errors,
and the pre-classification ERROR logs are gone; run-tool already logs
both paths at the right levels.
* Tolerate string queries in transform-query->text
Legacy transforms stored raw SQL strings under :query, and orphaned sources
skip normalization, so the query reaching this helper is not always a map.
dissoc on a string threw ClassCastException; the search formatter's old
string guard had tolerated exactly this shape. Strings now pass through
verbatim, and the helper's namespace gets its first tests.
* Use a doc link for escape-xml in the escape-xml-content docstring
* Read orphaned sources' string keys in extract-sql-content
Orphaned transform sources skip normalization and keep their JSON string
keys, so the keyword probes missed them and the model got EDN of a
string-keyed map instead of the SQL. Multi-stage queries now fall through
to the EDN branch too: stage 0 alone is partial SQL and hid the rest.
Also tags the read-check 404 alongside the 403, closing the race where
the database vanishes between the exists? check and the read.
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>
We no longer upload JUnit XML to S3, so the action and all 47 call sites
go. Test results stay visible through the JUnit reporter and CI
Conductor.
`id-token: write` drops from minimal-tests and transforms-python-stress-test,
whose only OIDC consumer was the upload's AWS credential handshake. It
stays in run-tests, which must keep granting it to drivers.yml's ECR login.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings `actionlint` (v1.7.12, the version CI pins) to zero findings, down
from 186.
Real problems, not just lint noise:
- `pr-env-destroy-manual.yml` used an unquoted YAML scalar containing
` #`, so YAML truncated the command at the comment marker and the step
actually ran `echo "Destroying PR environment for PR` — an unterminated
quote. Moved to a block scalar with the PR number in an env var.
- `yaml.yml`'s `yaml-linter` job read `needs.files-changed.outputs.yaml`
without declaring `needs:`, so its `if` was always false and the linter
never ran.
- `containerize-uberjar.yml` referenced `github.event.inputs.commit` in
three places, but its `workflow_dispatch` declared no inputs.
- The three Slack notification workflows interpolated
`github.event.issue.title` straight into a `github-script` body; it now
goes through an env var.
- Several step-summary blocks wrote `echo '... ${{ ... }}'` in single
quotes, which never expanded as intended.
The rest is mechanical: quoting `$GITHUB_OUTPUT`/`$GITHUB_ENV`/
`$GITHUB_STEP_SUMMARY` redirect targets, grouping consecutive appends
into `{ ...; } >> file`, replacing `ls | xargs basename` with a glob
loop, `A && B || C` with `if/then`, `[[ "$?" != 0 ]]` with `if ! cmd`,
and assorted quoting.
`containerize-jar.yml`'s `release` input is now a real `boolean` rather
than a string of `"true"`/`"false"`, which also removes the
`${{ 'true' }}` workaround at its call sites.
Adds `.github/actionlint.yaml` to declare the `arm-ubuntu-24-2core`
self-hosted runner label.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Split the research catalog tool to keep its payload bounded
get_research_candidates returned every research-eligible metric with every
candidate dimension inlined — on instances with many metrics the JSON blob
overwhelmed the following LLM request's context.
Two-tier replacement:
- list_research_metrics (new): slim index — id, name, description
(truncated to 150 chars), in_library — no dimensions. Optional q filters
by substring on metric and dimension names.
- get_research_candidates now requires metric_ids (max 20 per call) and/or
q, and returns a deduped shape: same-group dimensions resolve to the same
underlying Field, so descriptive fields (name, types, interestingness)
are stated once per dimension group, with each metric's dimension id and
per-metric display name (renames are common) in
dimension_id_and_name_by_metric. A q match beyond the cap is ranked
library-first-then-interestingness, truncated, and stamped
{truncated, shown, matched}. Argument-less calls return an instructive
error instead of the catalog.
add_research_groups is unchanged — the ids the model echoes back are the
same per-metric dimension ids as before. explorations.selmer updated to
describe the new contract (factual sync only).
* Cap the list_research_metrics index
Load-testing with 3,000 metrics (~62k dimensions) showed the unfiltered
index was still a ~394 KB / ~100k-token blob — slim rows don't save you
when there are thousands of them. Above 500 matches the index is now
ranked library-first-then-interestingness, truncated, and stamped
{truncated, shown, matched} — the same contract as the
get_research_candidates q cap — so the model knows to narrow with q.
Under the cap, rows keep their natural library-first-then-name order.
At the 3,000-metric load the unfiltered index drops to ~66 KB; q-filtered
responses are unaffected.
* Code review cleanup
---------
Co-authored-by: Nathan Voxland <nathan@metabase.com>
* Declare two request params the handlers read but never declared
Request decoding drops keys a schema does not declare, so a handler that
reads an undeclared key silently gets nil.
- POST /api/card/pivot/:card-id/query destructured `parameters` and
passed it to `process-query-for-card`, but declared only
`ignore_cache` (and `dashboard_id`) — parameterised pivot queries
silently ignored their parameters. Declared exactly as the non-pivot
sibling in the same namespace does.
- POST /api/notify/db/:id destructured `synchronous?`, the documented
flag that makes the sync run synchronously, without declaring it, so
every call ran asynchronously.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Add regression tests for both undeclared params
Each test fails on master and passes with the schema fix:
- pivot: reuses the native card whose `{{category}}` template tag is
`:required`, so the query only runs if the request body's value reaches
the query processor. Without the declaration the request 500s.
- notify: blocks the redefined `sync-database!` on a promise and asserts
the response is still outstanding, which can only hold when the handler
derefs the sync future. Without the declaration `{:success true}` comes
back immediately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Avoid row-constructor IN in multi-catalog Databricks sync (GHY-4263)
On a multi-catalog Databricks connection, describe-fields and describe-fks
filtered schemas with `(table_catalog, table_schema) IN ((?, ?))`. Databricks'
planner is orders of magnitude slower on that form against
`system.information_schema.columns` than on AND-ed equalities: on a Unity
Catalog workspace with ~220k columns the same query drops from 3m27s to 2.0s
when the predicate is rewritten. sync-fields issues one such query per schema,
so a ~50-schema sync took hours and never ran to completion.
Emit `catalog = ? AND schema = ?` per pair, OR-ed together for multi-pair
calls, instead of the row constructor.
* Update comment
* groundwork cleanup & gap fixing in provider adapters
* moonshot / kimi BE support
* FE support
* fix provider name & default model to kimi k3
* clean comments
* Honor suite-level quarantine rules in the gate
/api/quarantine serves a top-level `suites` array of glob rules alongside
`tests`. A rule matching the job's reporting suite quarantines the whole
suite, so the gate passes without comparing failures test by test.
Globs match as an unanchored, case-insensitive substring and support `*`,
`?` and `{a,b}` — a subset of the minimatch semantics ci-conductor
validates against, compiled inline because this package carries no
runtime dependencies and CI runs it straight from the checkout.
Both response keys are read as optional, so the script and the server
need no lockstep deploy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Drop the driver "info" status
Suite-level quarantine rules say the same thing from CI Conductor, for
every suite rather than drivers alone: run it, but don't let it gate.
mage now reads only "skip" from ci-test-config.json, and no longer emits
a per-driver status; the workflows drop the plumbing that carried it,
along with the job summary and PR comment that reported an info driver's
real outcome. The quarantine gate's own verdict and CI Conductor's UI
cover that visibility.
"skip" stays: not running a cloud driver at all saves real spend and has
no equivalent in the quarantine endpoint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Remove dead checks from drivers
* Tweak comment
* Drop the "info" status from notification
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The upload named the artifact after the event's own commit, while every
lookup around it — the concurrency group, check-existing-artifacts, the
bundle-stats upload and check-jar-health — keys on the dispatch input
first. A dispatch with an explicit commit therefore built that commit and
uploaded it under the dispatch branch's tip.
Inline the composite action into its only caller so the name comes from
the same expression as the lookups instead of a second copy of the
fallback chain.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ignore missing sandbox/perms features when syncing
for consistency with other code, we should not worry about whether the
premium feature in question is actually enabled. If sandboxing is
enabled and configured and we're runnning EE code we should respect it
when a new table/group/db is synced.
* dedupe implied rows when setting data permissions
build-database-permission and build-table-permissions merge the results
of their recursive implication calls with `merge-with concat`, and
several implication paths can produce the same row:
- setting view-data to :blocked implies `transforms :no` both directly
and via the implied `create-queries :no`, so the DB-level path
INSERTed the transforms row twice
- coalescing table perms to a DB-level row (all tables agreeing on a
create-queries value) implies a DB-level `view-data :unrestricted`
row, and the recursive table-level view-data call coalesces to that
same row, so the table-level path INSERTed the view-data row twice
Nothing deduped the merged :to-insert list, so every such call silently
persisted duplicate data_permissions rows. Duplicate DB-level rows are
the arming condition for the UXW-4927 sandbox data leak: the
going-granular path for newly-synced tables only deleted one row per
(group, perm-type), and a surviving duplicate out-votes the new table's
:blocked row at enforcement time.
* make the going-granular DELETE+INSERT atomic
The bulk path for newly-synced tables issued its DELETE and INSERT
outside any transaction, so a failed insert — e.g. the unique-constraint
violation added in the next commit — could leave the DB-level rows
deleted with the table-level expansion half-written. Wrap the pair in a
transaction. Nested transactions are savepoint-based (see
metabase.app-db.connection), so this is safe under callers that already
run inside a transaction, like the Table after-insert hook.
* dedupe data_permissions and add a unique constraint
data_permissions had no unique constraint on
(group_id, perm_type, db_id, table_id), so nothing prevented duplicate
rows for the same scope, and duplicate DB-level rows armed the UXW-4927
sandbox data leak. Three v58 changesets (v58 is the oldest release this
will be backported to):
1. Delete existing duplicates, keeping the most restrictive perm_value
per scope, lowest id as tie-break.
2. Add a generated column unique_perms_helper = COALESCE(table_id, -1).
All three app DBs treat NULLs as distinct in unique indexes, H2 has
no partial indexes, and MariaDB has no functional indexes, so a
NULL-safe generated column is the only portable mechanism — same
approach as metabase_table.unique_table_helper (v55.2025-05-19).
Unlike that precedent the base column carries an ON DELETE CASCADE
foreign key, which MySQL rejects for STORED generated columns
(errno 1215), so MySQL/MariaDB use a VIRTUAL column; Postgres has no
virtual generated columns and no such restriction, so it keeps
STORED. Dedupe, constraint enforcement, and FK cascade verified on
Postgres 17, MySQL 8.0, and MariaDB 10.6.
3. Add a unique constraint on
(group_id, perm_type, db_id, unique_perms_helper).
Inserts stay fail-loud: after the previous commits, a constraint
violation can only mean a genuine bug, and it should abort the write
(the going-granular DELETE+INSERT now runs in a transaction) rather
than be silently absorbed.
The generated column is excluded from the model (after-select) and from
load-from-h2/dump-to-h2 copies, mirroring unique_table_helper. Tests
that inserted raw duplicate rows on top of the hook-created defaults now
set permissions through the API instead.
* sidebar empty states
* fix e2e test
* fix tests after rebase on master
* keep empty failed/canceled threads in the sidebar so they can be restarted
* review comment
* Prefetch a code-split page when a link to it is hovered
* Show a fallback while the router hydrates a lazy route
* Split the query builder into its own chunk
* Move the query builder's action types off the actions barrel
* Split the question selectors into their own module
* Read the question selectors from the leaf module in the app shell
* Import the query builder's view components directly in table editing
* Keep a pending lazy navigation from being replaced
* Prefetch the query builder chunk from links into it
* Drop the redundant job id from the granular-rerun artifact name
For most drivers `github.job` and `junit-name` are the same string, so the name
came out as `failed-tests-be-tests-snowflake-ee-be-tests-snowflake-ee-Driver
Tests-attempt-1`.
`junit-name` plus `artifact-suffix` already discriminate every call site in the
artifact namespace that run-tests.yml shares between app-db.yml, backend.yml,
drivers.yml and semantic-search.yml — including the `be-tests-postgres` job that
exists in both app-db.yml and drivers.yml, where the two derive the same
`be-tests-postgres-ee` junit-name but differ on suffix.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Stop the rerun artifact restore from erroring when there is nothing to restore
An attempt that fails with nothing attributable to a var uploads no failed-tests
artifact, so the next attempt's restore legitimately finds none and falls back to
the full suite. That is the designed path, but `download-artifact`'s `name` input
treats a miss as an error, printing a red annotation on a job whose real failure
is elsewhere.
`pattern` filters the artifact list the action already fetches and downloads
nothing on zero matches, so this is the same single API call as before and costs
no extra rate-limit budget. The pattern is an exact literal, and artifact names
are unique within a run, so it matches at most one.
`merge-multiple` is deliberately left unset: a single match already lands at
`target/failed-tests`, and setting it would merge a hypothetical second match into
that one file — letting a matrix leg rerun another leg's failures — where the
default nests them and falls back to the full suite instead.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Chris Vandoodewaard <chris.vandoodewaard@metabase.com>
* Render pinned collection items in a single grid of compact static cards
* Fix pinned-card e2e waits that relied on removed previews
* Register explorations as pinnable and movable drag types
* Address review comments
* Memoize search model hooks
Cache model hooks by the registered spec methods using a single-entry FIFO. This avoids repeated per-row hook construction while bounding reload-time cache growth and keeping invalidation best-effort.
* Trim the model-hooks cache test
The finally-clause recompute was dead: restoring the method restores the cache key, so
the probe's entry can never be hit again. Split the caching assertion out of the
invalidation test.
* Assert the model-hooks cache is warm before probing invalidation
The first call registers spec methods as it resolves models, so it caches under a key
that is stale by the time it returns. Probing from that state proves nothing: the miss
comes from the stale key, not from the redefinition.
* Note that the model-hooks cache key is only an invalidation token
* Set *warn-on-reflection* in the search spec test namespace
Clj-Kondo flags any interop call in a namespace that leaves the var unset; the
.addMethod probes are the first interop here.
* [Metabot] Pretty print tool call results
Split a tool call's result into Response, Structured output and Other
fields sections instead of dumping the raw JSON-encoded result map into
a single JSON code editor. Text output renders with real newlines, and
structured_output (also the kebab-case spelling kept by rows migrated
from the v1 format) gets its own section.
* Simplify the tool call result parser
* Centralize remote-sync read-only handling in useTransformPermissions
Fixes Create index (and other transform mutations) being offered in
remote-sync read-only mode and failing with 403. The hook now returns
combined readOnly plus granular permissionsReadOnly/remoteSyncReadOnly,
and all direct getIsRemoteSyncReadOnly usages across transforms are
replaced with hook values or props. Run button stays gated by
permissions only (running is allowed in remote-sync read-only).
* Rename RunButtonSection readOnly prop to permissionsReadOnly
* Drop redundant remoteSyncReadOnly from incremental switch disabled state
* Fix plugin setup order in indexes page spec
* Enable Kimi K3 via OpenRouter
* Enable DeepSeek V4 Flash via OpenRouter
* Raise the conversation-title output budget from 128 to 512 tokens
Reasoning models spend the output budget on reasoning before emitting the forced
tool call, so at 128 tokens the structured call often returns no tool call at all
and no title is saved.
* Fixed failing CI
* Add query timestamp, refactor error marker
* no-mbql-introspection pass
* Order temporal-axis exploration variants by date, not aggregation
When the row cap fires on a time series, aggregation-desc ordering keeps
the highest-count buckets scattered across the full range: gaps in the
x-axis, old spikes beating recent data. Temporal axes now order by the
temporal breakout desc (aggregation desc as tiebreak) before the limit,
so the most recent contiguous window survives.
- time-facet: order by the temporal breakout (second breakout) desc
- default: branch on dim type — temporal dims by recency, categorical
dims keep aggregation desc
- per-value-time-series: gains the same date-desc cap (had none, so a
single fine-grained series could blow query-caching-max-kb)
- all three also filter the NULL date bucket: it can't render on a time
axis, and its position under ORDER BY DESC is warehouse-dependent
(Postgres NULLs first, MySQL last), making cap survival
non-deterministic otherwise
- temporal-pattern-day/hour unchanged (bounded, no cap interaction)
* Added debug info to interestingness calculations
* even less mbql introspection
* oops, delete .scratch
* Sidebar UI fixes - error marker, loading shimmer
* Interestingness >= 0.7
* Adjust prompts list animation
* Fix e2e
* Note that the LLM query planner is a POC, not used in production
* Fix searching for explorations
* Use llm-metabot-provider for everything
* Log interestingless calculation as info for now
* Log interestingless calculation back as debug
* Fix FE lint
* fix ai summary showing dirty while generating
* Use llm-metabot-provider
* collection breadcrumbs
* add back null values for temporal variants
* order by for temporal pattern variants
* fix top-n-other
* doh, fix temporal pattern queries
* fix name in viz header, don't show be error in tooltip
* sort numeric bins in heat map header, fix tests
* UXW-4483 Add matching colors for category + breakout charts (#75912)
* Fix CI
* Fix units, fix loki
* UXW-4522 Update last query run timestamp in background
* Fix loki
* Fix e2e
* Improve Explorations viz types (#75958)
* UXW-4483 Add matching colors for category + breakout charts
* improve viz types first pass
* add error boundary
* add row chart support for dimension_value_colors
* Fix typecheck
* fix tests
* better handling of segments with row fallback, fix null color bug
---------
Co-authored-by: Denis Berezin <denis.berezin@metabase.com>
* UXW-4402: Make explorations respect sandboxing/impersonation/db routing (#75572)
* Make explorations respect sandboxing/impersonation/db routing
* some self-review fixes
* more cleanup
* test fixes
* doh
* Fixing CI failures
* capitalize 'By' consistently
* Fixed CI failure
* add loki snapshots
* Review fix
* Fixes after merge
* Rework Explorations UX (#76300)
* comment on exploration viz first pass
* wip
* wip
* wip
* UX changes first pass finished
* code cleanup, fix, add tests
* address review comments
* Code cleanup
---------
Co-authored-by: Nathan Voxland <nathan@metabase.com>
* Refine explorations with metabot (#75326)
* teach metabot about new exploration ux
* refine explorations
* address self-cr
* pass over FE
* CR response
* fix test
* Add :model/Exploration to api + eid-translation model-imports
These modules reference :model/Exploration after merging the latest
explorations base; declare the imports to satisfy model boundaries.
---------
Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
* Harden explorations runner:
- planner crash recovery
- Bound timeline scoring
- add metrics
* UX tweaks
* Additional logging of errors on ai summary, plus increased thinking budget
* Improved timeline interestingness calculation
* Handle sonnet 5 etc better
* Make explorations groups a real thing (#76643)
* Rename ExplorationThreadGroup model to ExplorationBlock
Explorations organize their queries in a hierarchy: an Exploration has
Threads (one per run), a Thread has Blocks, and a Block is one
metrics-by-dimensions grouping that renders as a single sidebar heading.
That persisted grouping entity was named ExplorationThreadGroup, and the
word "group" was overloaded across the backend and frontend. Rename the
entity to Block — the name the frontend already uses — so the larger
data-model changes that follow start from one clear name. No behavior
change.
Renames :model/ExplorationThreadGroup -> :model/ExplorationBlock and
table exploration_thread_group -> exploration_block.
- Append a renameTable changeset rather than editing the createTable in
place: the original is already applied to running dev DBs, so an
in-place edit would fail checksum validation and force a DB rebuild.
- Rename the model namespace/file; update all call sites, the require
alias (:as block), and insert-blocks!.
- Update model registrations (resolution, serdes models, copy, entity-id
test) and re-sort the entries.
- Refresh the model's docstrings to describe a "block".
The planner's :thread-groups ctx key and the read-side group-tree are
left untouched here; they belong to later read-side work.
* Persist exploration pages with stable ids reconciled across reruns
Within a Block, an Exploration's queries are organized into Pages: a Page
is the bundle of queries for one (metric, dimension, query_type). Until
now a Page existed only as a derived, content-addressed key computed at
read time, so anything that wanted to point at a Page had to reference a
string that changed whenever the grouping definition changed. This makes
a Page a real persisted row whose surrogate id is its identity, so
comments (and stars, coming next) can anchor to a Page and survive both a
rerun — where queries are deleted and regenerated — and a future
re-grouping.
Introduce ExplorationPage: the persisted (block, card, dimension,
query_type) page within a block.
- New exploration_page table + model, FK to exploration_block (cascade)
and report_card. Registered in resolution, serdes (excluded), copy,
and the entity-id test.
- Add exploration_query.page_id (NOT NULL, FK, cascade): the reconciler
stamps it on every query, so it's required from the start. group_id
stays for now as the block pointer the read-side and deep-link URLs
use; dropping it waits for the read-side rework.
- Reconcile in insert-plan-rows!, the single insert point for both
initial Start and rerun: find-or-create a page per distinct key
(stamping page_id), then GC pages left with no queries. reset-thread-
for-rerun! is unchanged — pages survive a query delete (cascade is
page->query), so find-or-create recovers the same page ids on rerun.
- No DB unique index on the key: dimension_id is text (not portably
indexable on MySQL) and planning is single-threaded, so uniqueness is
enforced by find-or-create and matching is done in Clojure.
- Migration changesets appended (the table/group_id changesets are
already applied to the running dev db).
Tests cover page-id stamping, rerun id-stability, orphan GC, and perms.
* Return nested blocks→pages from the read API; drop query.group_id
An Exploration Thread's structure is Blocks, each containing its Pages,
each Page bundling the ids of its queries. Until now the backend instead
returned a hand-rolled flat list of typed nodes (sidebar/singleton/page)
linked by parent_group_id and keyed by auto:<...> strings, which the
frontend then re-nested. Now that Blocks and Pages are real rows, return
the nested shape the tables already encode: GET /:id returns :blocks,
each with its :pages, built from ExplorationBlock + ExplorationPage rows
and queries grouped by page_id.
- New metabase.explorations.blocks (renamed from .groups): blocks-tree
builds the nested shape; block headings + page names + score-sorting
ported over; block-display-name made public for the LLM planner ctx.
- Deep links go to a page by id: page-url → /question/research/<exp>/
page/<page-id>; the AI-summary and document-append builders pass the
primary query's page_id.
- Drop exploration_query.group_id (and its FK/index); a query reaches its
block through its page now — build-row-context (used by the runner to
finalize each row's MBQL) resolves the block via the page, which is what
the group_id read there used to do.
- API schemas: ExplorationQueryGroup → ExplorationBlockNode +
ExplorationPageNode.
- Migration changesets appended (the table is live on the dev db).
- Rewrite the read-model + endpoint tests for the nested shape; every
ExplorationQuery test fixture now hangs off a page (page_id NOT NULL).
The create payload key stays :groups for now (separate input contract,
touches the data-picker flow). FE consumption of :blocks lands next.
* Consume nested blocks→pages on the frontend
The frontend renders an Exploration Thread as sidebar headings — one per
Block — with a navigable item per Page (a Page being the bundle of
queries for one metric/dimension/variant). It used to receive a flat
typed-node `groups` list and re-nest it itself; this switches it to the
nested `blocks` array (block → pages) the read API now returns, and
renames the navigable unit from "group" to "page" throughout. Users see
the same sidebar and charts — this lines the frontend up with the
backend's shape.
- exploration.ts: add ExplorationBlockNode / ExplorationPageNode; thread
`.groups` → `.blocks`; remove ExplorationQueryGroup / its display-type;
the navigable id is now a page node id (kept as an opaque string).
- Sidebar utils build the tree directly from blocks→pages (one heading
per block, one child per page); score-sorting + empty-block filtering
preserved.
- The navigable unit is renamed group→page end to end: the route segment
becomes /page/<id>, matching the backend's AI-summary deep links. The
selection/url/comment-draft id stays a string via String(page.id), so
the existing string-keyed plumbing is unchanged.
- Visualization toolbar/header/comment-draft props renamed group→page;
the mark-as-interesting handler is left a stub for the stars feature.
- Specs, stories, and test-utils rewritten to the nested shape.
The create-payload request key stays `groups` (separate input contract).
* Retain comment-anchored pages across rerun GC; point comment URLs at /page/
A Page bundles the queries for one (metric, dimension, query_type), and a
comment can be anchored to a Page — comment.child_target_id holds the page
id. On a rerun an Exploration's queries are deleted and regenerated, and
any Page left with no queries is garbage-collected. A Page a comment
points at must survive that cleanup even when it ends up empty, or the
comment would resolve to nothing. This makes the rerun GC comment-aware
and moves comment deep-links onto the page route.
- gc-orphan-pages! now retains an orphan page when some "exploration"-
targeted comment's child_target_id equals the page id (compared as
strings). Empty + no comment is still GC'd as before. Page ids are
globally-unique PKs, so matching on child_target_id alone is
unambiguous.
- exploration-comment-url: /group/<child> → /page/<child>, matching the
page route and blocks/page-url; stale "group ID" docstring fixed.
- explorations imports :model/Comment for the retention lookup (comments
doesn't depend on explorations, so no dependency cycle).
- No backfill: the dev Postgres has zero exploration comments and zero
auto:* child_target_id rows, so the optional migration is skipped.
- Test: comment on a page → drop that page's selection → rerun → the page
is retained and the comment still resolves; a no-comment empty page in
the same rerun is still GC'd.
* Generate short and long page names from metric, dimension, and variant
A Page — the bundle of queries for one (metric, dimension, query_type) —
is shown in the sidebar under its Block's heading, but is also referenced
on its own elsewhere (comment deep-links, AI-summary chart titles,
search). Page names are derived, not stored, so a Page needs both a short
label that omits whatever its Block heading already shows (e.g. under a
"Number of Orders" heading: "By Category over time") and a full,
self-describing label for context-free places ("Number of Orders by
Category over time"). This generates both from the Page's parts — the
metric name, the dimension name, and a short per-variant qualifier —
rather than by string-slicing a finished name.
- variants/variant-qualifier: query_type → a short, param-free localized
qualifier ("over time", "(Day of week)", ...) or nil for the default
breakdown. Lives next to query-name so all variant naming is in one
place. Unit-tested.
- blocks: page-long-name and page-short-name generate from parts (metric
+ dimension + qualifier) using tru templates and the block's anchor;
page-node emits :name and :long_name.
- API ::ExplorationPageNode and the FE ExplorationPageNode type gain
:long_name (FE display wiring left for separate work).
- Same-dimension pages that differ only by variant (default vs
top-n-other) are now distinctly named instead of colliding; a
blocks-tree test that asserted the collision now asserts they differ.
* Add ability to star pages, sidebar tabs UX (#76755)
* add ability to star, sidebar tabs
* - renamed "user interestingness" to "starred"
- Let starred just be true|false, no null
- Removed DELETE starred API, just PUT now
---------
Co-authored-by: Nathan Voxland <nathan@metabase.com>
* Fix CI: test compile error, migration boolean type, e2e formatting
- documents/search_test.clj: qualify text->prose-mirror-ast with the
documents.test-util/ alias in the exploration-thread exclusion test.
The unresolved symbol broke backend test compilation, which cascaded
to every app-db/driver test job and clj-kondo ("0 tests" everywhere).
- 20260428_explorations.yaml: use ${boolean.type} instead of bare
boolean for exploration_page.starred (required post-v49 so MySQL
uses BIT(1)).
- oxfmt formatting for two explorations frontend files (fe-lint).
* Update e2e specs for reworked comment URL + explorations sidebar
- comment helper: match the node comment link by path only. The
`?new=true` marker was removed from comment URLs (base commit
"respect query params in all urls"); the CommentsMenu unit test was
updated but this e2e helper was missed. Verified the comment flow
still works (opening a node's thread shows the new-comment editor).
- explorations spec: align with the reworked sidebar —
* `by <dim>` leaf label is now `By <dim>`
* Scratchpad doc test: only Scratchpad is created synchronously; the
AI Summary doc is created asynchronously, so stop asserting it (and
the removed "Findings" grouping) at creation time.
* AI-summary completion test: the AI Summary doc row now hangs under
the thread heading ("Initial investigation"), not a "Findings" group.
* remove the two "Add to document" tests — that control was removed.
* Fix stylelint rule-empty-line-before in explorations CSS modules
Add blank line before nested &:hover rules in ActionToolbar.module.css
and ExplorationGroupVisualization.module.css. These surfaced in fe-lint's
lint-css step once the oxfmt formatting failure was resolved.
* - cleaned up migration file
- Improve group renaming in code
- preserve starts between re-runs like comments
* Fixing CI failures
---------
Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
Co-authored-by: Nathan Voxland <nathan@metabase.com>
* Don't send empty messages to the LLM
* address review comments from 76755
* UXW-4765 Hide charts (#77488)
* UXW-4765 Hide charts
* Review fixes
* Fix the failing explorations unit tests and Loki stories (#77564)
* Fix the failing explorations unit tests and Loki stories at the branch tip
* Restore the original all-hidden copy and drop thread headings from it
* Delete the outdated ExplorationGroupVisualization stories and their Loki references
* Rephrase the hide toast to put the chart name first
* UXW-4764 Remove interestingness badges on results (#77544)
* UXW-4764 Remove interestingness badges on results
* Drop e2e assertion for the removed sidebar interestingness markers
* Colocate the interesting and error markers with their only consumers
* Remove the interestingness marker from the timeline picker too
* fix types, lints, formatting
* Uxw 4779 update tab menu to match designs (#77543)
* UXW-4779 Update sidebar tab menu to match designs
* Show a pointer cursor across the whole SegmentedControl
* Give the icon tabs img semantics and hide the decorative icon names
* Let getExplorationSidebarTabsInfo own the tab icon config
* Revert the SegmentedControl root pointer cursor, Mantine already puts it on the labels
* Regenerate modules config after master merge
The merge left 'explorations' and master's new 'entity-retrieval' out of
sorted order, which metabase.core.modules-test asserts on.
* Float the action toolbar pinned to the bottom of the chart canvas (#77667)
* fix types
* revert timelines in documents
* Review fixes: stop requiring the QP's internal cache.impl from other modules
Explorations code that saves and loads query-result snapshots was requiring
metabase.query-processor.middleware.cache.impl directly — an internal
namespace of the query-processor module that other modules aren't supposed
to reach into. Since the module's :api set is generated from actual usage,
those requires had put cache.impl on the module's public surface — something
master never exposed. Modules talk to each other through a small public
facade (metabase.query-processor.core) instead.
The two helpers those callers need — do-with-serialization (write a QP
result as a gzipped blob) and with-reducible-deserialized-results (read one
back) — are now re-exported from the qp.core facade, and the explorations
callers go through it. One consumer can't: queries/cached_result.clj is
itself loaded while qp.core is loading, so requiring the facade from there
would be a circular-load error at boot. It gets a proper home instead — a
new small, documented public namespace,
metabase.query-processor.result-serialization, re-exporting the same two
helpers. With nothing outside the module requiring cache.impl anymore, the
regenerated :api drops it, matching master.
* Review fixes: explorations planner and AI-summary polish
- Nested cardEmbeds: the AI-summary postprocessor wrapped only top-level
cardEmbed nodes in resizeNode containers, while the card materializer
walks the whole document — so a cardEmbed the LLM emitted inside e.g. a
blockquote got a real card but rendered collapsed to zero height. The
wrapper now walks the tree too (skipping already-wrapped nodes).
- Awareness-tier sort validation: phase 2 validates that embedded
categorical charts carry an explicit sort instruction, but the validation
set was built only from top-tier breakouts — an awareness-tier categorical
chart escaped validation entirely, and its prompt block (slim-block) never
told the model a sort was required. Both tiers are validated now and
slim-block emits the same per-chart sort directive as full-block.
- Contextual-scoring gate order: safe-score+describe resolved its scoring
context (a Card select and a SQL compile) before checking whether the row
is scorable at all (chart config present, creator known, non-blank thread
prompt) — wasted DB reads and a query compile for every unscorable row.
The cheap gates run first now. Its docstring also claimed the LLM gate
throws and is swallowed; it actually returns nil from a pre-flight cond.
- Discovery-cache TTL: the top-K category discovery cache was a
process-lifetime LRU, so a stale top-K result could be served forever on
a long-lived instance; entries now also expire after 15 minutes.
- Chart-name i18n: filtered-subset chart names built their multi-value
summary as (str (count values) " values") — a hardcoded, unpluralized
English fragment inside an otherwise-translated name. It uses trun now.
- Docstring/comment corrections: effective-cardinality documented (and
branched on) a :num-bins bucket that no caller can produce — branch and
docs removed; run-one-plan-iteration! said it runs "the LLM planner" when
the default is the mechanical one; a variants-test section comment claimed
temporal variants add a not-null filter that no variant adds.
Also repoints this module's result-blob serialization calls at the qp.core
facade (the consumer half of the cache.impl cleanup in this stack's first
commit).
* Review fixes: explorations API permissions, documents, and search
- Creating an exploration accepted arbitrary card ids (in blocks) and
timeline ids without checking the caller could read them, so you could
attach someone else's private metric or timeline and see its name/events
later. Both are read-checked at create time now, and timeline hydration
filters out timelines the viewer can't read. A duplicated timeline id used
to hit a unique constraint and 500 the request; ids are deduped.
- The restart endpoint re-selected the Exploration row after resetting the
thread, but the reset never touches that row — the redundant select is
dropped.
- GET /api/document listed documents that belong to exploration threads,
which search, recents, and collection items all deliberately hide. It
applies the same filter now — which as a side effect removes an N+1
pattern where each listed doc could trigger several permission queries.
- The document validator rejected {:type "doc" :content []} — the exact
empty document the backend itself creates for new threads. An empty root
doc is valid now.
- Search: is_published was deliberately hidden from search responses on
master (#66841) and this branch accidentally re-exposed it — hidden again
in both search engines. Exploration descriptions are searchable in the
legacy engine too, matching the new engine.
- Data picker: metrics and dimension groups now use the same
interestingness filter (a metric could previously reference dimension ids
that appeared in no group), search matches the displayed
"<group> - <dimension>" text, and a collection-ids query that ran twice
per request runs once.
- Consistency: the create event publishes after the transaction commits
(observers could previously fire before the data was visible).
* Review fixes: metabot / LLM infrastructure
- Asking Claude for extended thinking while also forcing a tool call
(tool_choice "required") produced a request Anthropic rejects with a 400.
The guard that already handled this for structured output now covers the
general tools path too.
- The e2e guard that stops tests from hitting a real LLM provider threw a
raw MalformedURLException on unparseable URLs; it now fails closed with
its normal friendly message — and it was moved out of a catch-all that
would have rerouted it, so it fails loudly.
- The timeline tools handed the LLM broken XML (an attribute quote and a tag
never closed) and interpolated user-written names/descriptions unescaped,
letting content break the markup. Output is well-formed and escaped.
- Hardening/docs: the retryable-error check bounds its exception cause-chain
walk at 10 (a cyclic getCause could loop forever); the
contextual-interestingness response schema now really forbids extra
properties (its docstring claimed it already did); docstrings about
thinking model buckets, return shapes, and inputs corrected.
- Also repairs three test breaks that already existed at HEAD: an arity
mismatch from the OpenRouter merge, a test asserting thinking blocks are
dropped (they now stream as reasoning parts), and tests that didn't stub
the LLM gate's newer usage/permission checks.
* Review fixes: lib-metric, metrics transforms, startup sync, interestingness
- group-by-source was re-exported for JVM Clojure but missing from the
ClojureScript branch of lib-metric.core; added.
- Metric/measure display-info inherited a default :long-display-name even
where the method overrode :display-name, so the two could disagree;
they're set together now.
- A failure in the startup metric-dimension sync aborted server boot
entirely; it's wrapped to log an error and continue.
- The interestingness "leftovers" backfill loaded every unscored field for
the database into memory in one select; it now streams one field at a
time, and the namespace docstring mentions this second recompute trigger.
- Added a unit test covering the dimensions :out transform (kebab-key
renames + type-value keywordization).
* Review fixes: enterprise perms/sync/deps, cached card query, test quality
- Remote sync: an event payload that simply omitted a key used to satisfy a
nil condition (e.g. "exploration_thread_id must be nil") — conditions now
require the key to be present with the right value, checked with
(find object k), which distinguishes a present-but-nil entry from an
absent key. Vector values in removal conditions rendered as broken
equality clauses; they render as IN.
- Dependency graph: smartLink nodes with nil/placeholder ids added nil to
the dependency set (the sibling cardEmbed branch already guarded this);
guarded, with regression tests for both branches.
- Test quality: the revisions exclusion test asserted the impl against its
own private set, so it could never fail; it checks a hardcoded expected
list now. Exploration bookmarks get real API coverage
(create/list/delete/ordering/archived), and a mislabeled test string was
fixed.
* Review fixes: v4 instance-analytics views with explorations usage sources
The AI-usage analytics views translate machine source names ("exploration",
"contextual_interestingness", …) into human-readable labels with a SQL CASE.
The explorations stack added its new sources to the v1 SQL files — but the
views actually installed on instances are v3, and nothing reinstalled them,
so users saw the raw machine names fall through the ELSE branch.
Adds v4 of ai_usage_log and metabot_conversations (v3 plus the new CASE
branches, with one consistent "Explorations" label — v1 said "Exploration"
for the source but "Explorations" for the profile), a migration that
reinstalls the views at v4, an updated pointer comment in usage.clj, and a
parity test that fails whenever a known source/profile is missing a CASE
branch in the latest views — exactly the drift that happened here.
* Review fixes: frontend (minimal) and e2e
Only the low-risk frontend fixes from the review pass, plus the e2e test
fixes; the other frontend findings are deliberately left out to keep this
branch's FE diff small.
- getDimensionIcon existed byte-for-byte in two files; the duplicate module
is deleted and its one importer now uses the copy in
common/utils/columns.ts.
- e2e: an assertion that searched for text inside another text node (and so
always passed) is meaningful now; readSelectedText no longer breaks
Cypress's retry behavior by asserting after a .then; and the seemingly
pointless GET /api/metric/:id in seedMetrics got a comment explaining the
side effect it exists for (it persists metric dimensions).
* Remove the LLM query planner
Explorations plans queries with the mechanical planner only now. Removed:
the LLM planner namespace, its prompt templates (plan.selmer /
repair.selmer) and prompt loader, the prompt-markdown renderers in the
planner-context namespace, the explorations-query-planner setting
(mechanical/llm/auto), and the :skip-no-llm exit for "planner requires an
LLM but none is configured".
pick-planner! now always returns the mechanical planner — kept as a seam
so tests can stub it and a future planner has an obvious dispatch point.
The planner-contract protocol and the shared context builder stay (they
are planner-agnostic), and the query_plan_transcript column stays (every
planner writes a transcript); its remark no longer describes an LLM
exchange.
* remove graph._dimension_value_colors (per bar colors)
* Add row count to explorations API (#77740)
Already available, we just need to persist it and thread it through the
API.
* Format fix
* Exploration UX... um explorations. (#76901)
* click thru
* better comment links
* chart avatars
* avatars
* allow subsubexplorations
* disable explore further for Other group in top n charts and dimension anchored blocks
* fix explore further for binned numeric columns
* comment highlighting, remove avatars
* comment label formatting
* map and table highlighting
* use built in viz click actions popover instead of custom
* fix comment badge in dark mode
* self review cleanup
* fix and simplify new thread toast
* fix be lints, add tests
* fix tests
* add fe, e2e tests
* - POST /:id/explore-further now verifies the clicked page
belongs to this exploration before copying its block, closing a hole where
any page in the instance could be copied into any writable exploration.
- key on the stable :explore-filters spec instead of a hash of
the per-row-reconstructed :dataset_query (which minted fresh :lib/uuids and so
never hit). build-row-context exposes :explore-filters on the row context for
the cache to key on.
Also reuse metabase.util/capitalize-first-char in the block title and thread
name helpers instead of hand-rolled str/upper-case.
* Code cleanup
* Gate exploration derived metadata by viewer's permissions
* fix lints
* Code review improvements
* Den/uxw 4741 explorations new sidebar (#77674)
* Update tabs view, add filter button menu
* Fix tests
* Add sidebar items vertical lines + icons
* Other sidebar changes - viewed state, sticky hierarchy
* Show sub-explorations near it's parent item
* Fix issues
* Self review
* Fixes after merge
* Self review
* Review fixes
* Fix empty states
* Review fixes
---------
Co-authored-by: John Swanson <john.swanson@metabase.com>
Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Denis Berezin <denis.berezin@metabase.com>
* Explorations self-review cleanup (#77717)
* Consolidate the explorations migration in place
The 063 explorations migration had grown by accretion: tables were created
and then altered by later changesets in the same unreleased file. Since the
branch convention is to edit unmerged changesets in place, the final shapes
are folded into the createTables:
- exploration_query gains params, nullable dataset_query/entity_id, and
page_id inline (the page_id FK and index stay separate — exploration_page
is created later in the file, so they can't run earlier).
- stored_result gains data_access_token inline; its backfill changeset was
deleted outright — created and backfilled in the same release, it could
only ever run against an empty table.
- exploration_query_result gains chart_stats / metric_description /
chart_description inline, which also retires the randomly-named changeset
id v63.gr324asd.
- idx_eqti_query_id deleted: the unique constraint on the same leading
column already provides that index.
The completed_at and analysis_started_at backfills are deleted outright:
the explorations feature merges to master in one piece, so at upgrade time
no instance can have pre-existing exploration threads to backfill. (They
were also dangerous — they stamped columns the cancel endpoint and the
completion handler gate on being NULL, permanently locking out any thread
in flight during the migration.)
Cosmetics: backfilled default docs are named "Scratchpad" to match runtime
(the backfill said "Findings"); stale remarks corrected (chart_stats is EDN
not JSON, the real query_type variant list, what collection_id actually
defaults to).
Existing dev databases will need a migration reset.
* Set exploration thread started_at in the INSERT
The thread was inserted without started_at and then immediately updated to
set it, on the theory that the background worker must not see the thread
before its dependent document/block/timeline rows exist. The whole create
shares one transaction, so intra-transaction ordering is invisible to the
worker (it reads on another connection; the commit is atomic) — the flag
can simply be set in the INSERT, saving a write.
* Guard exploration restart against in-flight work
The restart endpoint could fire while the planner was still working on the
thread, deleting its rows mid-plan and wedging the thread in a state no
worker would ever pick up again. reset-thread-for-rerun! is now a CAS: it
applies only to a terminal thread (completed_at set) with no query still
mid-execution, and the endpoint returns 409 ("cancel it before restarting")
otherwise. A successful reset leaves the thread exactly in the shape the
planning worker's claim predicate picks up.
The endpoint also published :event/exploration-update, which provably did
nothing: restart changes no Exploration column, and the revision system
skips unchanged objects. The publish is removed; the AI-summary doc reset
still records a Document revision, which is what /mine's last-touched
ordering composes.
* Route mid-stream LLM errors through the provider-friendly rethrow
LLM responses stream lazily; a connection timeout between SSE chunks
escaped as a raw SocketTimeoutException because the provider error handling
only wrapped request setup — the reduction happens later, in the agent
loop. Every provider adapter's SSE reducible (claude, openai, openrouter,
azure, bedrock) is now wrapped so consumption-time failures get the same
translation as request-time errors.
* Grant the read-only timelines scope under metabot-nlq
The explorations metabot profile instructs the model to use the timeline
tools, but those tools were granted only under
:permission/metabot-other-tools while the profile is gated on NLQ — so for
NLQ-only users they were silently filtered out of scope while the prompt
kept referring to them. agent:timelines:* (whose only member is the
read-only list/details scope) is now granted under both permissions.
* Don't retry deterministically-failing fields in the interestingness leftovers pass
The leftovers pass selects on dimension_interestingness IS NULL, so a field
whose scoring deterministically throws was re-attempted on every sync
forever. A per-process attempted-marker now skips fields that already
failed in this process; a restart re-enables retry, so transient failures
still converge. Process-local by design — no schema change, and no sentinel
score leaking into product surfaces that read the column as a number.
* Fail closed when a sandboxed field's user attributes are missing
sandbox-token-for-table returned nil when the field was sandboxed but the
user's sandbox-attribute lookup came up empty — and the downstream
compatibility gate reads nil as "unrestricted". That's fail-open. It now
returns a user-scoped "indeterminate" sentinel that matches nobody else's
token (two indeterminate users don't match each other either): fail-closed.
Also adds direct and end-to-end tests for the impersonation and routing
token producers, which previously had none.
* Require the (card, stored_result) pairing on the cached card-query path
POST /api/card/:id/query with a stored_result_id read-checked the card but
then served whatever snapshot id the client sent — nothing verified the
snapshot actually belonged to that card. The pairing must now exist in
stored_result_use (404 when it doesn't, so an unpaired probe can't confirm
a snapshot exists). Both materialization paths already insert the pairing
rows, and the table ships in the same unreleased release, so no legacy data
lacks them.
The viewer-lens check also denied a nil dataset_query only by accident (a
schema exception swallowed by the catch — and in prod, where mu/defn logs
instead of throwing, it would have failed open); it denies explicitly now.
Plus: consistent t2/select-one forms in the endpoint, a stored_result_use
docstring update, and the generated module config picking up the new
StoredResultUse consumption.
* Apply the personal-collection default when it resolves after first render
The "default new explorations to my personal collection" logic captured
personalCollectionId once in a useState initializer — if the selector
resolved after the first render, the default never applied. It now
reconciles when the id arrives, without overriding an explicit user choice.
* Move the explorations migrations to v64
The v63 release branch has been cut, so the explorations feature will ship
in v64 and its migrations belong in the 064 directory. Both files move
(20260428_explorations.yaml and 20260519_exploration_bookmark.yaml) and
every changeset ID is reprefixed v63. -> v64.
No changeset content changes. Since we're already editing unreleased
changesets in place on this branch, existing dev databases needed a
migration reset anyway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* v_metabot_conversations v4: restore include-deleted semantics lost in merge
Master's #77542 (conversation branches) and this branch both created
metabot_conversations/v4 in parallel: master's dropped the
deleted_at IS NULL filters so soft-deleted (rewound/regenerated) messages
are counted; ours added CASE branches for the new explorations usage
sources and profile. The 'Merged from master' resolution kept only our
copy, silently losing the deleted-message semantics — which is why
deleted-messages-included-test and last-message-at-test fail: they assert
master's behavior against our view.
This drops the three filter lines from each dialect's v4 file so v4 is the
union of both changes. Both installing changesets (our v63 and master's
v64) are runOnChange: true and checksum the referenced file, so every
instance reinstalls the corrected view on upgrade — no new version or
migration needed.
* Drop the redundant v_metabot_conversations v4 installer
Master's #77542 (064/20260713_metabot_analytics_views_include_deleted.yaml)
already installs metabot_conversations v4 via a runOnChange changeset against
the same SQL files, which now carry both sides' changes: the explorations
CASE branches and the include-deleted semantics. A second installer here was
redundant — and would have collided with master's changeset IDs
(v64.2026-07-13T00:00:00/01) had this file been mechanically renumbered to
v64 like the other explorations migrations.
The ai_usage_log v4 changeset stays: nothing on master installs that view.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Move the ai_usage_log v4 migration to 064
Same reasoning as the explorations tables: v63 is cut, this ships in v64.
The changeset ID takes the next free slot for its date
(v64.2026-07-13T00:00:02 — master's include-deleted file owns 00 and 01)
rather than a mechanical v63->v64 rewrite that would have collided.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Show a crossed-eye indicator with a tooltip on hidden pages in the exploration sidebar (#77907)
* Explorations no AI Summary (#77878)
* Extract AI Summary + document-in-exploration out of explorations
Removes the LLM-generated "AI Summary" feature and the ability to display a
document within an exploration. The feature-restoring (inverse) commit is kept
on branch `explorations-ai-summary`; cherry-pick it to bring the functionality
back.
Backend:
- Delete the explorations.ai-summary.* pipeline, composite, document-perms,
prompt templates, and their tests
- Drop exploration->Document creation, the /thread/:id/documents endpoints,
the :documents hydrate, ai_summary_document_id, and the AI-summary reset
- Revert the documents-module coupling: content-visibility gate, the
document.exploration_thread_id FK/column, the view-log skip, and the
GET /document list filter; delete documents/validate.clj
- Drop the collection-items show_exploration_documents option and the
exploration-doc exclusions in recents / search / serdes / remote-sync;
drop the search exploration-thread-id attribute
- Remove the ai_summary_document_id and document.exploration_thread_id
migration changesets (edited in place)
Frontend:
- Delete the ExplorationDocument component and its tests
- Remove the AI-summary sidebar node, completion toast, and the document
API / urls / types; keep the shared comments sidesheet and page routing
* Extract static card-embed in documents out of explorations
Removes the static (cached-snapshot) card-embed capability from documents, so it
can land on its own branch (`static-embed`) independently of the AI-Summary
feature. The feature-restoring (inverse) commit lives on `static-embed`.
Keeps the base `stored_result` model, the exploration query-result cache, and the
cached-result permission helpers (`assert-can-view-cached-result!`) that the
exploration `/query/:id` streaming endpoint uses. Also keeps the general
`report_card.document_id` "card in a document" feature (it predates this work).
Removed:
- The cached-result serve path (`cached-dataset`, `apply-sort`,
`deserialize-cached-result`, `allowed-chart-sorts`, `viewer-can-view-cached-result?`)
and the `stored_result_id`/`sort` branch of `POST /api/card/:id/query`
- `stored_result_use.card_id` (column + model arm; keeps `exploration_id`)
- The documents serdes static-node filter (reverted to master's live-only rewrite)
and the prose-mirror static docstrings
- Frontend: the tiptap `cardEmbed` static rendering (the `stored_result_id` + `sort`
+ `chart_href` trio) across the CardEmbed node/menu, document hooks, redux, and
`card.ts`; deleted the CardEmbedNode static unit spec
* Fix pre-existing cljfmt/oxfmt formatting to unblock CI
These 4 files (a stray blank line each) are unchanged from the base explorations
branch — the format checks were failing on inherited, not extraction, changes.
* more documents cleanup
* Update Loki Snapshots
* change references from entity (which was page or document) to just page
---------
Co-authored-by: Kevin Kelleher <kevin.kelleher@metabase.com>
Co-authored-by: Metabase Automation <github-automation@metabase.com>
* Restore fix
* Fix Metabot exploration tools streaming empty results to the client (#77990)
Since 6912a50e85 ([Metabot] Chat internal format improvements, #77301)
only a tool result's :output string makes it onto the tool-output-available
SSE event; a bare map streams as "". The exploration plan tools returned
bare maps, so the FE — which applies plan edits by parsing the streamed
tool result — silently dropped every edit while the LLM (which still saw
the full result) reported success.
Wrap each tool's payload in {:output (json/encode ...)} and add a
regression test that runs real tool results through parts->aisdk-sse-xf
and asserts the payload survives onto the wire.
* Explorations timeline fixes (#77958)
* fix timelines
* disable timelines for row charts
* Explorations sort order (#77980)
* sort empty pages to bottom
* sort subexplorations by the initial exploration's interestingness
* Add e2e tests
* Explorations use persistent queue (#77901)
* explorations: run planning/queries/scoring on the persistent queue
Switch explorations from the hand-rolled polling runner to the mq persistent
queue: three queues (plan, query, timeline-score) with per-queue on-error
handlers and worker-count-bounded concurrency.
The generic mq subsystem changes this builds on live in a separate branch
(mq-queue-improvements) so they can land on master independently. Until that
lands and flows back into `explorations`, this branch depends on those mq
features (:on-error, :max-concurrent-batches) and won't run standalone.
* Remove explorations settings-test
* Code cleanup
* Code cleanup
* Use in-memory queue backend for e2e tests
The persistent Quartz queue backend stores its durable-job state in the app
DB's QRTZ_* tables. e2e restores app-db snapshots between tests (H.restore),
which drops those tables out from under the running scheduler while the
per-process ensured-jobs cache still believes the jobs exist. Post-restore
publishes then target a non-existent job and never deliver, so exploration
planning produces no queries and the detail-page sidebar stays empty.
e2e is a single node, so the in-memory backend (no app-db state, unaffected by
snapshot restore) is the correct fit. Set via the :e2e alias so it applies to
both local (runFromSource) and CI (runFromJar, via getJvmOptsFromDepsEdn).
* fix type
* Remove Timeline interestingness from explorations (#78013)
* Remove Timeline interestingness from explorations
Removes the LLM-driven per-(query, timeline) interestingness scoring
feature and all of its wiring
* Improve names (#77862)
* improve names
* review changes
* fix dimension disambiguation not working
* Fix explore-further on expression columns (#77900)
1. The 500 — normalize-target-ref (in metrics/transforms.clj) silently dropped the expression name. The :field ref schema has a legacy-form decoder that relocates the id into MBQL-5 position; the :expression schema has none, so a
valid legacy ["expression" "Gear"] normalized to [:expression {…} nil]. That nil-named ref then failed Malli validation inside find-matching-column mid-request → 500.
2. The drill couldn't work anyway — a top-n-other chart breaks out on a synthetic CASE expression that exists only in the variant query, never on the metric card. So even a valid expression ref could never resolve against the card.
* Code cleanup
---------
Co-authored-by: Nathan Voxland <nathan@metabase.com>
* Update Loki Snapshots
* Improved LLM error handling
* Fixed queueng of explore-further
* Fix e2e tests
* Add copy link hotkey support
* Fix e2e tests
* Fix CI
* Fix after merge
* Make highlighted state in line charts more obvious when hovering over comment badge (#77998)
* show tooltip when highlighting comment for cartesian charts
* set blurScope to blur across stacked chart grids, only show tooltip for single line series
* review feedback
* add types file that was supposed to be included with 77998
* self review
* Auto-switch the add metrics modal to the All tab when the library has nothing to show (#78070)
* Fix back button (#77995)
* Fix back button
Co-Authored-By: Andrei Chernykh <a.d.chernykh@gmail.com>
* Remove the stray /question/explore route, fix the entry spec's removed setMode prop, and clean up dead code
* Keep the research draft and conversation alive across the entry/plan step and browser back/forward via a shared provider on the research route
* Fold the plan-step URL assertion into the existing manual-flow e2e test instead of a separate one
---------
Co-authored-by: Ethan McCue <ethan@mccue.dev>
* Explorations: remove dormant extended-thinking support; unify LLM timeouts (#78173)
* Remove unused Anthropic extended-thinking support
Extended thinking was fully dormant: the Claude adapter supported it
(streaming translation of thinking blocks, config normalization, request-body
support) and call-llm-structured-with-trace forwarded a :thinking opt, but no
production caller ever passed :thinking, and the multi-turn agent path
(call-llm) never accepted it at all. Remove the adapter support and the
structured-path plumbing until a real consumer exists.
This also resolves several open review questions about the thinking code
(signature round-trip on multi-turn continuations, structured-output
guarantees under thinking, per-model capability guards) by removing the code
they applied to.
* Unify LLM HTTP timeout knobs across metabot.self and llm.anthropic
metabase.metabot.self.core/request hardcoded its connection (10s) and
socket (120s) timeouts, diverging from the metabase.llm.anthropic HTTP
path, which already reads the operator-tunable llm-connection-timeout-ms /
llm-request-timeout-ms settings. Operators had two uncoordinated knobs.
Delete the two private constants and read the settings at call time in
`request` (keeping the per-request `(merge req)` override). Raise the
setting defaults to the former constants' values (5000 -> 10000,
60000 -> 120000) so no path loses its safety margin, and move the
constants' streaming/inter-byte rationale into the defsetting docstrings.
Both LLM HTTP paths now share a single operator knob pair.
Resolves PR #1 review comment 2.
* UXW-4758 Ability to view all timeline events
* Show proper empty state message in sidebar for initial load
* Bump prefer-with-dynamic-fn-redefs ratchet to 28
The explorations MQ queue tests need three real with-redefs (not
mt/with-dynamic-fn-redefs): the handlers run on an MQ worker thread that
does not carry the calling thread's dynamic bindings, as documented
inline at test/metabase/explorations/queues_test.clj:102,153,265. Seed
the budget to match so metabase.core.kondo-ratchet-test passes.
* fix regression of #77667 caused by #76901
* Restore explorations CASE branches in v_metabot_conversations (v6)
master's #77549 rewrote v_metabot_conversations to v5 (summary->title
rename) without the explorations source/profile CASE branches that v4
carried, so the exploration/contextual_interestingness sources and the
explorations profile surfaced as raw machine names and failed
metabase-enterprise.metabot.usage-test/known-sources-and-profiles-have-view-case-branches-test
across every dialect.
v5's installer is not runOnChange and is already released on master, so
edit it in place would break the checksum. Instead add v6 (v5 + the three
branches) and a runOnChange installer that reinstalls the view after the
v5 rename, matching the v4 installer pattern.
* fix errors caused by merging master
* UXW-4758 Close timeline events sidebar when comments are opened
* Explorations queries api performance (#78175)
* revert error message display changes
* Avoid a full fingerprint version increment
* Fix unit tests error logs
* Removed the composed LRU + TTL cache that is not supported by the core.cache factories. The data should be naturally bounded enough that TTL should be good enough.
* Added scopes for explorations in metabot
* Explorations PUT safety (#78188)
Don't allow random assignment on PUT /api/exploration/:id
* Pick the right column as the metric for interestingness scoring (#78269)
Pick the right column as the metric
* Explorations unstranded threads (#78250)
Fixing ways threads can be stranded
* Hide SQL error messages from API response
* Explorations pass research plan (#78236)
Correctly pass research plan
* Revert in-place edits to historical instance-analytics view SQL
The explorations CASE branches were added directly to the v1/v4 view SQL
files, but those files back runOnChange changesets: editing them changes
the checksum, so on an already-migrated DB the old installers re-fire.
metabot_conversations v1 still selects c.summary, which the v64 rename
(summary -> title) has dropped, so booting the branch on a master-migrated
DB fails with 'column c.summary does not exist' (all six cross-branch
migration CI jobs).
The branches live on in the branch-new final versions (ai_usage_log v4,
metabot_conversations v6), so nothing is lost on fresh installs.
* Raise prefer-with-dynamic-fn-redefs ratchet to 29
The branch added four with-redefs ignores in explorations/queues_test.clj
(all justified: the redefined handlers run on MQ worker threads, which do
not see thread-local dynamic bindings, so mt/with-dynamic-fn-redefs cannot
work there) but the budget was only bumped to 28. Verified all 29 ignores
in the tree still sit on live with-redefs calls, so none can be removed
instead.
* Improving exploration error handling
* Fix with-current-user binding in exploration runner (#78163)
* Better error vs. empty handling in query cache
Conflicts:
src/metabase/explorations/query_plan/variants.clj
* self review
* Added FKs
* fix e2e
* Added visible-metric-cards-where-clause to queries.core
* Cleaned up removed-in-master agent:table scope
* Reuse Event card design from QueryBuilder
* Handle prompt injection
* Fix hotkeys hijack default actions
* Reuse the query instead of continually re-running it
* Fix extra rerenders on queries polling
* Review fixes
* Review fixes
* Fix chart skeleton for all screens
* review comments
* Fix Andrey's review comments
* Fix Andrey's review comments
* Fix Nemanja's review comments for e2e
* Fix Nemanja's review comments for e2e
* more review comments
* Ensure user has read permissions on cards when doing an explore-further
* Handle numeric PKs/FKs better in explorations (UXW-4757) (#77991)
Numeric PK/FK dimensions produced unusable one-bar-per-id charts and were
auto-selected as interesting dimensions. Two root causes:
1. The query planner treated any numeric dim as auto-binnable, but...
they're not, and we emitted unbounded bare breakouts, and ruled out
the top-n-other variant.
2. Dimension interestingness hard-zeros PKs but not FKs, and the
cardinality scorer's auto-binnable branch gave numeric FKs 0.9 for a
binned chart the QP can never produce, so they got auto-selected.
Fixes:
- default-bucket-for-dim returns nil for :Relation/* dims, so numeric keys
band by raw distinct count: <=100 -> default, >20/unknown -> top-n-other.
- top-n-other stringifies its CASE THEN values so numeric dims compile to
a single-typed CASE (mixed numeric/string CASE breaks strict warehouses).
- type-penalty hard-zeros numeric FKs (semantic FK + numeric effective/base
type). Non-numeric FKs (e.g. a country-code FK) stay scoreable.
- The cardinality scorer excludes :Relation/* from the auto-binnable
branch, matching lib's own binning rule.
* UXW-4475: self-heal missing metric dimensions in explorations read path (#78061)
Metrics whose dimensions were never successfully synced (dimensions NULL) -
e.g. metrics created on a model before it had result_metadata, or SQL-model
metrics predating the source-card sync fix - showed zero dimensions in the
explorations picker and Metabot research tools forever, because those paths
read persisted dimensions without ever syncing.
hydrated-metrics now runs sync-dimensions! for loaded metrics that have a
dataset_query but empty dimensions, then reloads the healed rows. Metrics
that compute nothing persist nothing and retry cheaply on a later request
(app-DB reads only, no warehouse queries, no write when nothing changed).
* Fix stored-snapshot permission gate ignoring card-sourced queries (#78455)
The data-access-lens capture derived its table set from
query->source-table-ids on the raw stored dataset_query, which only
collects numeric :source-table ids. For queries built on cards
(:source-table "card__N"), neither the creator's nor the viewer's token
picked up any tables, so both sandbox dimensions were empty and
data-access-compatible? returned true — letting a sandboxed viewer
stream an unsandboxed creator's snapshot.
Add query-perms/query->resolved-source-table-ids, which preprocesses the
query (as admin, via the existing preprocess-query helper) so source
cards — nested chains and card-sourced joins included — resolve to their
underlying table ids, and use it at both lens-capture sites (the
exploration runner's token write and the cached-result read gate). The
preprocess binds qp.i/*skip-middleware-because-app-db-access*, the same
flag the offline semantic checker uses, so the routing/impersonation
preprocess middlewares don't resolve the *viewer's* destination/role
(and throw on lapsed premium features) while we're only after the
query's table footprint. The helper throws when the card chain can't be
resolved (deleted card); non-admin viewers are then denied while
admins — never sandboxed, impersonated, or routed off the router db —
fall back to the same admin-only access as a nil token.
Also make stored_result.dataset_query NOT NULL via a follow-up
addNotNullConstraint changeset: the gate derives the viewer's lens from
it, so a query-less snapshot has no defensible verdict. A gate caller
passing a map without the key now gets a loud exception (500) instead of
a quiet fallback, so trimmed-row bugs surface in development.
Also fix explore-further-nested-thread-name-omits-metric-prefix-test to
reference a real exploration_page instead of a hardcoded id 999 that
violates the source_page_id FK on fresh databases.
* Fix build
* Fixed comment
* Better cached result testing
Conflicts:
src/metabase/queries/cached_result.clj
* Hash data access token info
* Added test from before
* Fix unit tests
* Fix Andrey's review comments
* Sloan review comments
* Explorations - fix metrics case (#78411)
* Add delay for Sidebar shimmer text, split ExplorationTreeNode
* Fixes for routing after merge
* Latest reviews fixes
* UXW-4908 new Comments UI
* add more analytics
* Code review fixes
* Fix scroll to view for focused comment
* Correct permissions "lenses" (#78697)
* Correct permissions "lenses"
Some early code had a huge, obvious bug, asserted in tests, for database routing.
For explorations, because queries are pre-computed, we need to calculate "lenses" representing the state
of database routing, sandboxing, and impersonation. These lenses are calculated when the exploration is
created, and then calculated when someone attempts to view the exploration. The idea is that if the lens
reveals that the viewer would have been able to view the underlying queries, then they should also be able
to view your exploration.
Until now, a viewer who was "unrestricted" on one of those dimensions was allowed to see a restricted
snapshot. I.e., a "non-sandboxed" user could see a sandboxed user's exploration, a "non-impersonated" user
could see an impersonated user's snapshot, and a "routed-to-the-router" user could see a "routed-to-a-destination"
user's snapshot.
The issue here... "unrestricted" does not necessarily mean "unrestricted." It's entirely possible that:
- User A is sandboxed with a sandbox that replaces Table T with card C, which actually queries Table FT.
User B is permitted to see Table T (unrestricted) but is blocked on Table FT.
- User A is impersonated - to a *more* permissive role. User B is unimpersonated, but should not be able to see
the results of a query made by User A.
- User A has database routing, configured to point to `MySecretTenantData`. User B is routed to `__METABASE_ROUTER__`,
which holds a template schema and maybe some data to play around with. User B should not be able to see the results of
A's exploration.
This simplifies the code here:
- superusers can see every exploration, regardless of lens
- everyone else sees an exploration's results *ONLY* when their data-access lens is *identical* to the creator's. Same sandbox
(and relevant user attributes), same impersonation role, same routing destination. Unrestricted only matches unrestricted.
Also hardened the write side: capturing the creator's lens fingerprint used to be best-effort, silently saving a snapshot with
no fingerprint when it failed. Now the exploration query fails visibly instead, and the read side treats a
fingerprint-less snapshot as a bug (denied to non-admins, logged as an error) rather than something to adjudicate.
* Apply suggestions from code review
clean up test descriptions and other docstrings
Co-authored-by: Timothy S. Dean <timothy@timothydean.dev>
* Respond to CR
* fix impersonation test
---------
Co-authored-by: Timothy S. Dean <timothy@timothydean.dev>
* Fixed compile issue
* Address query-plan review comments on #73244
- context.clj: correct the column-fingerprint-for-target docstring (missing
fingerprints make default/time-facet eligibility fail closed and route
dims to top-n-other, not the reverse), fix the applicability return-shape
doc, and extend the ns docstring to cover build-row-context.
- context.clj + variants.clj: remove the dead temporal_dimension_id override
plumbing (nothing ever sets that param; the mechanical planner never emits
per-value-time-series items).
- variants.clj: drop the unreferenced known-variants var.
- query_plan.clj + planner.clj: validate every planner result against
result-schema in the orchestrator, treating a mismatch as a planner
failure, and document that the schemas are enforced.
- planner.clj: fix the plan! docstring's claim that the ctx shape is
documented on generate-query-plan!.
* Remove unused planner result schemas instead of enforcing them
Drops plan-item-schema and result-schema from the planner contract (review
follow-up on #73244): they were never applied to any value, and rather than
wiring validation into the orchestrator we document the plan!/result shapes
in prose on the protocol. Also rewords the context ns docstring's
plan/validate/materialize mention accordingly.
* Trim redundant clause from query-plan.context ns docstring
* Fix FE typecheck and unit tests
* Fix some tests
* Fix dimensions display in NewExplorationData
* Select all dimensions for a metric by default
* Fix metric-dimensions dependency test vs metric dimension auto-sync
The test (from master) created a metric card with hand-written
dimension_mappings and nil dimensions. On this branch a metric with nil
dimensions counts as uncurated, so the after-insert dimension auto-sync
seeds dimensions from the query and overwrites the provided mappings —
the mapped categories table never became a dependency.
Set the mappings via a plain update after insert instead: the
after-update hook only re-syncs when dataset_query changes, so they
stick, and the test still exercises what it means to — the dependency
backfill turning mapped-dimension tables into table dependencies.
* Fix metric dashcard perms test vs metric dimension auto-sync
The tests (from master) expect a metric dashcard query to return the
bare aggregation. On this branch the after-insert auto-sync seeds
metric dimensions, and qp.dashboard applies the default time dimension
as a breakout, turning the scalar count into a monthly series.
Suppress the auto-sync when creating the metric so the test stays
focused on what it exercises: permissions on the metric's source
model.
* Fix FE typecheck and unit tests
* Skip tests that will be addressed in the next PR
* Fix loki tests
* Add logs to identify e2e test failure reason
* Fix CI e2e failure
* Review improvements
* Redact raw driver errors from the exploration payload
`GET /api/exploration/query/:id` deliberately allowlists its 409 payload and
substitutes a generic message, because a driver error names tables, columns,
and row values from a query the reader may not have permission to run.
`GET /api/exploration/:id` hydrates the same `ExplorationQuery` rows onto its
response and returned `:error_message` verbatim, so the raw error reached
anyone who could read the exploration. Declaring the key in the response
schema does not help: a `defendpoint` response schema validates and encodes
but does not strip.
Give the substitution one home in `redact-query-error` and route both paths
through it — `hydrate-exploration` covers all five `::HydratedExploration`
endpoints at a single point.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Track data_access_token with thread and query instead of relying on the (not always there) cached result
* Key the derived-data verdict on both query projections
* Remove eager dimension-sync for metrics
* Fix unit tests after merge
* Fix typecheck
* Code review fixes
* Revert "Select all dimensions for a metric by default"
This reverts commit d5e8883c95.
* Code review fixes
* Fixing tests
* Fix e2e test
* fix issues after merge
* fix error when saving exploration in Our Analytics
---------
Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Denis Berezin <denis.berezin@metabase.com>
Co-authored-by: John Swanson <john.swanson@metabase.com>
Co-authored-by: Andrei Chernykh <andrei.chernykh@metabase.com>
Co-authored-by: Kyle Doherty <5248953+kdoh@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Metabase Automation <github-automation@metabase.com>
Co-authored-by: Ethan McCue <ethan@mccue.dev>
Co-authored-by: Timothy S. Dean <timothy@timothydean.dev>
* refactor: extract reusable typed schema module
* remove obsolete database-name query parameter
* refactor: split typed schemas rest endpoint module
* refactor: move public facade to core namespace
* refactor: use bare namespace for typed schema endpoint
* docs: describe typed schema options
* refactor: narrow typed schema query parameter
* docs: explain typed schema database names
* docs: clarify typed schema model scope
* re-export render-typescript from core namespace
* refactor: tighten typed schema options
* docs: add docstrings to typed schema query parameters
test: clarify typed schema parser coverage
* refactor: isolate typed schema model scope
* docs: clarify typed schema model scope
* test: simplify typed schema model scope coverage
* test: simplify rest api tests
* test: separate typed schema API and core coverage
* test: cover typed schema database selection
* test: assert generated typed schema tables
* test: normalize typed schema timestamps
* refactor: introduce TypeScript AST between schema and rendering
schema->ast is a pure policy stage producing a tagged-vector AST
(documented and malli-schema'd in typed-schemas.javascript); render-js
is an option-free printer over it. Replaces print-time policy callbacks,
path-based node-kind guessing, and the reference magic-map. Output
verified byte-identical against the previous renderer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* refactor: split typed schema fetching from pure assembly
fetch-items is now the pipeline's only impure stage; create-schema
assembles Items into the semantic schema value and takes an info map so
generated-at/instance-url can be pinned for deterministic output.
build-semantic-schema composes the two, unchanged for callers. Folds
the base-schema namespace into core, drops the duplicate
database-id-filter-clause, and rewrites core tests against literal
items. Output verified byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* refactor: reify typed schema data access as a protocol
SchemaSource names every read fetch-items performs; app-db-source is
the production implementation. Tests reify the protocol with literal
values instead of redefining selection functions across namespaces.
Output verified byte-identical.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* test: add real-db coverage above the in-memory pyramid
source-test exercises app-db-source against with-temp rows — the one
seam where typed schemas meet the database. core-test gains a single
end-to-end test running every entity kind (question, metric, model with
action, table with fields) from with-temp rows through fetch, assembly,
AST and rendering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* test: run the pipeline e2e against the test-data dataset
mt/dataset provides a real synced database, so the e2e cards sit on
real tables and fields — the question and metric need no hand-written
result_metadata, and metric columns are computed from real metadata.
Replaces the hand-rolled temp database/table/field rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* test: assert the e2e schema with =? pattern matching
One structural pattern replaces the key-plucking assertions; a single
exactness check keeps the database-scoping guarantee that partial map
matching would silently lose.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* test: sweep typed schema tests into =? pattern style
select-keys plucking becomes =? patterns (sets stay exact under =?, so
scope assertions keep their meaning); try/catch exception capture
becomes (is (thrown? ...)) + =? on ex-data; arg-asserting mocks become
stubs, with the argument pass-through now covered by the dataset e2e;
vestigial #' derefs on now-public fns are dropped; the five
prefer-with-dynamic-fn-redefs lint warnings are fixed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* docs: state the module's separation rules in namespace docstrings
Future changes should keep the boundaries: reads go through
SchemaSource, stages downstream of Items stay pure with environment
values entering via info, render policy stays schema->AST, TypeScript
syntax stays in the printer, and REST strings decode once at the
endpoint boundary. core-test documents where new tests belong in the
pyramid.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* refactor: drop convenience arities orphaned by the source protocol
app-db-source always passes full arguments, so the single-argument
arities of select-schema-cards, select-tables, question-schemas,
metric-schemas and model-schemas had no production callers left. Each
fetch function now has one full-arity signature.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* refactor: define scopes and trim the library scope to its live keys
scope.clj now defines the vocabulary: references are portable ways to
name content (ids, names, entity ids); a scope is what they resolve to
on this instance — permission-filtered id sets with descendant
expansion, nil-vs-empty semantics, and the 404-vs-empty asymmetry
stated. LibraryScope gets a closed malli schema; its :collection-ids
and :library-collections keys were computed and asserted but read by
nothing, and are gone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* refactor: move scope compilation to the fetch layer as scope-filter-clause
scope.clj now only resolves references into scopes; the fetch layer's
schema.common owns the one function that compiles a scope into a where
clause. The docstring states the nil-vs-empty enforcement and warns
against (seq ...) guards at call sites, which would turn 'resolved to
nothing' into 'unscoped' and select everything.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* refactor: batch collection reference resolution in typed schema scopes
Adds collection/descendants-flat-for (descendants of many collections
in one query, selecting :type) and rewrites scope resolution on top of
it: references partition into id and entity-id batches, resolve in at
most two selects, and 404s name every unresolved reference instead of
a bare 'Not found.'. Query count is now constant in the number of refs.
library-scope enforces its LibraryScope return schema via mu/defn.
Addressing, eligibility, and authorization are distinct layers in
resolve-collection-refs!, and their failures are deliberately
indistinguishable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* refactor: shrink the typed-schemas public surface to its contract
core now exports exactly three vars — SemanticSchemaOptions,
build-semantic-schema, render-typescript — mirroring the module's
data/print split; the pipeline implementation moves verbatim to the
internal pipeline namespace. New exports are compatibility promises,
and nothing needed the other five. The endpoint returns a ring map
directly instead of calling respond (async comes free), and
database-ids-for-ref uses select-one on the id path, documenting that
name references can genuinely match several databases (union semantics
pending a product decision).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* refactor: define the public API in core and name the build side
core now defines build-semantic-schema and render-typescript as visible
compositions of the internal stages instead of potemkin re-exports —
the public API is deliberate and decoupled from internal names. The
internal namespace becomes 'build': it interprets options into
SchemaSource reads and assembles the items into the schema value,
pairing with render/javascript as the module's two sides. The
compositions move out of build and render accordingly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
* test: build e2e card queries with Lib instead of deprecated mbql-query
The e2e's cards now carry lib-built MBQL 5 queries
((lib/query mp (lib.metadata/table ...)) + lib/aggregate) rather than
hand-rolled legacy MBQL via the deprecated mt/mbql-query macro — which
also proves the pipeline handles MBQL 5 dataset_query cards end to end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G69dZFxucVXhR1s11ZxmuB
---------
Co-authored-by: dan sutton <dan@dpsutton.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Drop dead QueryExecution delete from embed download test
with-query-execution! redefs save-execution-metadata!* and captures the
query execution in a promise, so nothing is written to or read from the
QUERY_EXECUTION table. The delete! only served to reach the app DB before
anything had initialized it, failing with "this database is empty".
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Persist the download format on export, never on format switching
The download popover remembers your last-used export format, but it was
persisting that preference on every click of the format segmented control,
spraying identical PUT requests at
/api/user-key-value/.../download_format_preference while the user just
looked around.
Switching formats now sends nothing. The preference is written as part of
handling an actual download, a fire-and-forget side task on top of the
export work, and only when it differs from the value the server already
has (the widget fetches it anyway to preselect the format, and the
mutation keeps the cached copy in sync, reverting it if the write fails).
Downloading with an unchanged format sends nothing at all. User-visible
behavior is unchanged: the popover still reopens on the format you last
exported with. The downloads e2e spec asserted the old
selection-persists behavior and is updated to the new semantics.
Considered and declined in review: guarding the comparison against a
still-loading preference (a download racing the initial GET can send one
redundant idempotent write; ordering a fire-and-forget path is not worth
it), and serializing overlapping preference writes (two different-format
exports within one HTTP round-trip could persist out of order; a mutation
queue for a preference value is overkill). Both races are rare, bounded,
and self-correcting on the next export.
* Trim the comments and the old-behavior test guards
Review feedback: the comment on persistExportFormat ran longer than the
function and said what the code already says, and both the e2e comment
and the no-request-on-switch assertions pinned behavior we removed
rather than behavior we built.
Also makes the optional chaining on formatPreference consistent within
persistExportFormat.
Refresh the bundled browserslist data to clear the "caniuse-lite is
outdated" warning on dev builds; the data was several months stale.
Lockfile only: bumps caniuse-lite to 1.0.30001806 and
baseline-browser-mapping to 2.11.9, and drops stale nested duplicates so
they resolve to the hoisted versions. Hand-edited because
update-browserslist-db misbehaves with bun in this repo.
* Support manifest-based runtime plugins
Summary
- Extract shared dependency pruning for runtime plugin uberjars.
- Initialize non-driver manifest plugins eagerly behind plugin API version 1.
- Preserve lazy loading for legacy driver manifests, while treating an empty driver list as a non-driver plugin.
Scope
This is capability-neutral infrastructure. It does not define an embedding API or ship an embedder.
Testing
- Plugin uberjar pruning tests.
- Plugin initialization, version-gate, empty-driver, and lazy-driver tests.
* Make runtime plugins lazy by default
* Document lazy plugin loader constraints
* Name plugin dependency checks after registration, not init
A satisfied `plugin:` dependency means the other manifest is registered, not that its
code is loaded. Rename the argument threaded through the dependency checks to say so, and
record where load ordering actually comes from for the two driver manifests that declare
one.
* Make a failed lazy-driver load retryable
Load the plugin before removing the placeholder `initialize!` method, so a load
that throws leaves the driver retryable instead of stranded on the no-op default.
Guard the removal so a driver that installs its own `initialize!` during load
keeps it.
Also clarify the `register-plugin!` contract (manifest plugins defer the classpath
add and code load; bare dependency JARs go on the classpath immediately), rename
`prune-provided-libs` to `prune-provided-libs!`, and mark its pure test parallel.
* Drop ^:parallel from prune-provided-libs-test
prune-provided-libs! logs via u/announce, so kondo's :metabase/validate-deftest
rejects the destructive fn inside a parallel test. Keep the ! name, drop the tag.
* Generalize bundled plugin loading to non-drivers; review nits
Rename bundled-driver-manifest-paths / load-bundled-driver-plugin-manifests! to
drop the driver-only framing -- the classpath glob already discovers any bundled
manifest -- and document that bundled plugins carry no add-to-classpath!, so their
code never comes from the writable plugins directory. Add a test asserting a
bundled plugin activates by running init steps only.
Also delay the plugin-uberjar core basis so requiring the namespace no longer pays
for a full dependency-tree resolution, and drop the unused init-plugin-with-info!
compatibility alias.
* Use with-dynamic-fn-redefs in the plugin init tests
Every binding here redefs a plain defn, so prefer the thread-safe dynamic redefs
(clears :metabase/prefer-with-dynamic-fn-redefs) and align each binding block's
values consistently.
* Register bundled manifests before plugins-directory manifests
Registration is first-wins, so a JAR dropped in the writable plugins directory
could shadow a bundled plugin's name and run its own code under that identity.
Load bare dependency JARs first (for class: deps), then bundled manifests, then
user manifests -- so bundled, root-owned code always claims the name.
* Reserve bundled plugin names so plugins-dir JARs cannot shadow them
A bundled plugin with an unmet dependency (Oracle/Vertica without their JDBC JAR)
never registers, leaving its name unclaimed -- a plugins-directory JAR could then
register under it and run as the bundled plugin. Reserve every bundled name up
front and refuse a colliding plugins-directory manifest.
* Reserve bundled names against the dependency batch too
Classification and registration read the writable JAR separately, so a JAR that
looks like a bare dependency but resolves to a bundled-name manifest could
register under it. Compute the reserved set once and pass it to both batches.
* Fail fast on concurrent plugin loading
* Align plugin framework forms
* Guard plugin registration and loading with one lock
Registration synchronized on the registered-plugins atom while loading claimed a second atom by compare-and-set, so
two different locks guarded two views of the same state.
Both paths now take one ReentrantLock: registration waits for it, and loading still fails fast when another thread
holds it.
Because that lock is reentrant, a plugin whose init step loads another plugin no longer trips the concurrency check
on its own thread -- nesting is not concurrency.
The registered and loaded name sets collapse into a single registry atom keyed by plugin name, which leaves
active-plugin-load as a diagnostic that names the loader in the rejection message.
* Reject plugin load cycles
A reentrant lock let a plugin load itself, directly or through a cycle like A -> B -> A.
`:loaded?` is only set once the init steps finish, so each pass through the cycle ran them again until the stack blew.
The lock now carries a stack of the loads in progress: nesting distinct plugins is still fine, re-entering one that is
already loading throws and names the cycle.
A rejected concurrent load no longer reports that plugin `nil` holds the lock when the holder is registering rather
than loading.
* Tidy the plugin cycle tests
Hoist the shared registration out of the first `testing` block, drop the closure that wrapped it, and share one uuid
between the two plugin names.
* Drop the temporary atom in update-unsatisfied-deps!
Everything here runs under plugin-lock, so a read and a reset does the job.
* Allow dropping items into sub-collections shown in the collection items table
* Trim comments in collection drop target
* Accept documents and metrics in collection drag-and-drop targets
* Improve collection drag-and-drop feedback
* code clean up
* Remove redundant React keys from drag source table row
* Retire redux navigation in favour of useNavigate
`push`/`replace`/`goBack` and the `CALL_HISTORY_METHOD` transport are gone.
Components navigate with `useNavigate`. Code that cannot call a hook (thunks,
the session middleware, a reducer, the permission and visualization config
objects) uses a module-level `navigate`, which is the navigator holder `AppShell`
already filled in, exported directly.
`metabase/router` exports its own `useNavigate`, which falls back to that
`navigate` when no router is mounted rather than throwing, because redux
navigation had no opinion on whether one was. The SDK mounts none, which is what
`useMaybeLocation` already exists for.
Four things this could not do mechanically:
* Relative targets resolved from the root under redux navigation, because
`AppShell` sits on a pathless layout route. `useNavigate` resolves against the
component's own match, so `SearchBar`, `SearchApp` and the command palette
needed absolute paths.
* A `LocationDescriptorObject` is assignable to v7's `To`, so passing one to
`navigate` type-checks and drops `state` silently. Six sites carried state,
including the QB card-state contract.
* `mapDispatchToProps: { onChangeLocation: push }` cannot hold `navigate`, since
`connect` dispatches what a bound creator returns. Five containers became
function components, the dashboard context supplies it in the provider, and
three `reference/` pages had it wired but never read it.
* A permission `actionCreator` returned an action to dispatch. It is now
`onSelect`, returning void, and `updateDataPermission` treats the presence of a
post action as handled rather than inferring it from a truthy return.
LOCATION_CHANGE and the location mirror stay: they are the router-to-redux
direction, feeding `isNavbarOpen`, `errorPage` and trace-id rotation.
* Hold navigations only when a router is on its way
A host that builds no router never registers a `navigate` for the holder to
flush against, so anything held is held for the life of the page. The SDK is
such a host and runs for a long time, so every `navigate` call from it retained
a closure and the `state` that closure captured.
`createAppRouter` and `createMemoryAppRouter` now say that a registration is
coming, because both put an `AppShell` above the tree. Without that, a
navigation with nowhere to go is dropped rather than queued, so a router-less
host retains nothing.
The queue stays a queue. Collapsing it to the latest navigation would change the
history stack: a push followed by a replace would apply the replace to the entry
the user arrived on, and lose the intermediate one.
* GHY-4251: Bound dependency backfill memory and honour its off-switch
The BackfillDependencies job OOM-killed instances with large warehouses. Native-query dependency
calculation resolved parsed table names by fetching the Database's entire table catalog and scanning
it in Clojure, so cost scaled with warehouse size rather than query size. Reading an entity also
attaches a MetadataProvider to its query, and the job bound no provider cache, so each entity in a
batch got a private provider holding its own copy of that catalog -- all of it retained for the
lifetime of the batch. At batch size 500 against a ~20k-table warehouse that is ~10M retained table
rows, which no amount of heap fixes.
Three changes:
- Look up only the tables a query names. New :name-ci metadata spec matches names
case-insensitively, since normalize-name lower-cases while sync stores whatever the warehouse
reported (Snowflake reports upper-case). Unlike :id/:name it does not widen the result set to
inactive or hidden objects, so it narrows an otherwise-unfiltered fetch without changing which
tables are eligible. The SQL narrows and find-table-or-transform still decides, so matching
semantics are unchanged. Transforms keep the existing scan -- their target name lives in a JSON
column with nothing to query -- which is fine at their cardinality.
- Scope a MetadataProvider cache to the backfill batch, spanning the select as well as the
processing loop, so entities share one provider per database instead of one each. This is the
invariant deps.findings/upsert-analysis! already enforces by throwing.
- Honour a non-positive batch size in schedule-run! for both dependency jobs. It is the supported
off-switch, but only task/init! checked it; the event-driven trigger did not, so entity changes
kept waking the jobs every second to do nothing. schedule-run! is the one function all three
scheduling paths share. The setting is env-var-only, so it cannot change while the process runs
and the job stays unscheduled until restart.
* GHY-4251: Record the failure when a fatal error kills a backfill entity
backfill-entity-batch! caught Exception, so an Error -- an OutOfMemoryError in the reported case --
escaped without recording anything. The next run then selected the identical batch and died the same
way, indefinitely, with no progress and nothing in dependency_status to show for it.
Catch Throwable instead and record the failure before letting an Error propagate, so a poisonous
entity backs off and eventually goes terminal and the batch can drain. Errors are still rethrown
rather than swallowed: the rest of the batch is abandoned and the job fails, because the JVM is in a
state we cannot reason about. Exceptions keep their existing per-entity behaviour.
* GHY-4251: Only let pending retries keep the backfill job scheduled when licensed
The job reschedules itself when a full batch was selected or when entities are in retry backoff. The
retry check ran outside the :dependencies feature gate that guards the work itself, so an instance
without the feature kept rescheduling hourly forever: a retry marker is only cleared by processing
the entity -- on success via upsert-status!, or by going terminal past max-retries -- and both are
gated on the feature, so the job could never resolve the condition keeping it awake. It took the
"No more entities to backfill for, stopping" branch never.
Gate the check on the feature. Requires the feature to be enabled, and gives up an accidental warm
standby: with the feature off and retries pending the job previously stayed scheduled and would pick
up work the moment the feature flipped. Recovery does not depend on that -- setting the token
publishes :event/set-premium-embedding-token, and every content-change handler triggers the job -- so
the remaining exposure is an upstream-only change to a token's feature set, which starts the backfill
on the next content change rather than immediately.
* GHY-4251: Correct the off-switch rationale in schedule-run! docstrings
Both docstrings claimed the batch size setting is env-var-only. It is not: get-raw-value consults the
application database after the env var, and :setter :none only blocks writes through the settings API,
so a stored value would be honoured if one existed. The conclusion is unchanged -- there is no
supported way to change it at runtime, so a cold stop until restart is right -- but the stated reason
was wrong, and it is the reason a future reader would rely on.
* GHY-4251: Stop the entity check job rescheduling when it cannot do anything
DependencyEntityCheck is a periodic checker with no terminal state, so it always queues another run.
That is correct while licensed, but check-entities! is gated on the :dependencies feature while the
reschedule was not, so an instance without the feature woke every 30-90 minutes forever to do nothing.
Same defect as the backfill job's retry check, reached by a different route.
Extract the reschedule out of the Quartz job body -- it only needed the scheduler, not the whole
execution context, which also makes it testable -- and gate it on the feature. If the feature is
enabled later the job restarts from the content-change event handlers, or from task/init! on the next
restart.
* Update docstrings
* GHY-4251: Record a failure for entities that have no dependency_status row yet
record-failure! only updated an existing row, so it silently did nothing for an entity that had never
been processed. Those are exactly the entities instances-for-dependency-calculation picks up first --
it matches a null status row -- so on a first backfill every entity is in that state. A failure
recorded nothing, the entity was reselected unchanged on the next run, and the crash loop the previous
commit set out to break stayed intact for the population most likely to cause it.
Upsert instead, via app-db/update-or-insert! like the other writers in this namespace. An inserted row
takes the column defaults for stale (false) and dependency_analysis_version (0); 0 is below
current-dependency-analysis-version, which is what keeps the entity eligible for the retry once
next_retry_at elapses.
This also fixes the "failure count: null" log line in the backfill catch path, which read back a row
that did not exist.
Found in review by escherize.
* GHY-4251: Make the dependency jobs' reschedule decisions licence-failure tolerant
Two problems with gating the reschedule on has-feature?, both introduced by the previous two commits.
The entity check job had no event wiring at all, while the backfill job derives
:event/set-premium-embedding-token. Stopping its periodic chain when unlicensed therefore left no way
back except a content change or a restart, because the content-change handlers that would revive it are
themselves gated on the feature. Enabling the feature on an idle instance left the checker dead. Mirror
the backfill job's wiring so setting a token restarts it.
has-feature? also collapses "no licence" and "could not check the licence" into the same false:
*token-features* returns #{} on any Throwable, so a network timeout or tripped circuit breaker reads as
unlicensed. Where that decides whether to schedule it is terminal, since nothing re-fires when the check
later succeeds -- the token never changed. Both inputs to the backfill job's decision consult the
licence, so a single blip stopped that job too.
Resolve the licence for scheduling with canonically-has-feature?, which reports nil when it could not
tell, and stop only on a definitive false. No token at all still answers false, so the common unlicensed
case still goes cold; a token that cannot be validated keeps the job alive. Doing the licensed work
still uses has-feature? and still fails closed -- fail closed on work, fail open on scheduling.
Tests for both jobs now drive all three licence states. The previous two-state tests expressed
"unlicensed" with mt/with-premium-features, which only rebinds *token-features* and so cannot produce a
definitive false; they were passing for the wrong reason.
Found in review by escherize and metamben.
* GHY-4251: Select the analysis batch inside its metadata-provider cache
analyze-batch! selected its instances before entering with-metadata-provider-cache, so each entity in
the batch got a private MetadataProvider attached to its query -- the same shape as the backfill bug,
same default batch size, other job.
Unlike the backfill job this is not a live leak: upsert-analysis! ignores the embedded provider, builds
its own from the entity's database id inside the cache, and passes it explicitly to check-entity, so
those providers stay empty. It is a trap rather than a bug -- the moment an entity is analyzed through
its own query, a batch of 500 retains 500 providers' worth of fetched metadata. upsert-analysis! already
throws when no cache is bound, so the cache was always meant to cover this work; only the select sat
outside it.
Moving the select inside the cache is behaviour-neutral today. The new test pins the intent by comparing
provider identity, since providers compare equal when they wrap the same database id.
Found in review by escherize.
* GHY-4251: Back the dependency test fixture's Tables with the application database
The shared fixture composed its Cards and snippets over metabase.lib.test-metadata, so its Tables existed
only in memory. That is fine for metadata the provider legitimately supplies, but resolving a table name
parsed out of native SQL is a lookup against the appdb, and an in-memory base has no rows to find.
Swap the base to the appdb-backed provider. The H2 test-data database already has the tables these tests
reference, so nothing needs creating; expectations move from mock ids to the real ones. Cards and snippets
stay mocked, since nothing looks those up by name.
These pass both before and after the implementation change in the next commit, which is the point: they
verify behaviour rather than a particular lookup mechanism.
* GHY-4251: Resolve table names with a t2 query instead of a MetadataProvider spec
Narrowing the catalog fetch was implemented as a new :name-ci MetadataProvider spec key. That put the
contract on every implementation for the benefit of one caller, and they did not agree: the overriding
provider ignored the key and fell into its fetch-everything branch, the composed provider took a
fetch-then-refetch path, and the cached provider keyed it by whole-spec so batch sharing was poor.
Resolving a table name parsed out of native SQL is a lookup against the application database, not query
metadata, so do it directly: lib-be/tables-by-name, case-insensitive because normalize-name lower-cases
while some warehouses report upper-case. The SQL still only narrows -- find-table-or-transform continues
to decide -- so matching semantics are unchanged.
Two consequences worth naming. sql-tools now uses lib-be, where the provider previously hid the appdb from
it. And a native query naming a transform's not-yet-created output table now resolves to {:transform id}
via the transform branch rather than {:table <invented-id>} from an override; a dangling invented table id
was the worse answer, but it is a change.
Verified with the OOM repro: 0 catalog fetches, completes in a 1200m heap where master OOMs. Per-card appdb
query volume is unchanged from the spec-key version.
Suggested in review by Alex.
* GHY-4251: Reschedule after a failed run, and keep a disabled job warm
Two changes to when the dependency jobs schedule themselves.
A failure no longer ends the chain. Both jobs are one-shot self-rescheduling with no cron backstop, so an
Error escaping the batch skipped scheduling entirely and the job stayed dead until a content-change event
or a restart. That is survivable for an OutOfMemoryError, which usually takes the process with it and lets
task/init! recover on boot, but not for a StackOverflowError from pathological SQL, which leaves a live
process behind. The backfill job body is extracted to run-and-reschedule! -- it only ever needed the
scheduler, not the whole execution context, which also makes it testable -- and reschedules on the way out
of a failure before rethrowing.
A non-positive batch size now suppresses only the event-driven trigger rather than all scheduling. The
1-second trigger fires on every entity change and never consulted the batch size, which is what made a
disabled job wake roughly once a second; that is the part worth stopping. The job's own periodic schedule
is left alone, so a disabled job keeps ticking slowly and resumes if the setting ever becomes positive
instead of staying cold until a restart. task/init! schedules regardless and logs that nothing will be
processed.
Note the resulting asymmetry is deliberate: scheduling now fails open on an indeterminate licence but still
stops on a non-positive batch size, because the batch size is definitively knowable and a token's status is
not.
Raised in review by metamben; Error case from escherize.
* GHY-4251: Cover the case folding tables-by-name depends on
The narrowed table fetch is a prefilter, not the decision: it selects with SQL lower(name) against names
lowered by u/lower-case-en, then find-table-or-transform matches precisely using the driver's
normalize-name. That is only correct while the prefilter returns a superset of what the precise match would
accept, and one of the two assumptions behind that is testable -- the appdb's lower() has to agree with
Java's.
Add a test that stores tables under ASCII and accented-Latin names and looks each one up by its own,
lower-cased and upper-cased spellings. It needs no per-driver harness: the backend workflow runs it on H2
and app-db.yml runs it on Postgres and MariaDB, so a collation that folds differently fails it there.
Deliberately out of scope, and noted as such: locale-dependent folds like U+0130, where lower() legitimately
differs between engines. The other assumption -- that a driver's normalize-unquoted-name is a pure case
fold -- cannot be tested from here, so it is recorded as a comment at the call site.
* GHY-4251: Cover the other assumption behind the narrowed table fetch
tables-by-name is a prefilter and find-table-or-transform makes the decision, so the narrowing is only sound
while the prefilter returns a superset of what the precise match accepts. The previous commit tested one of
the two assumptions behind that and claimed the other could not be tested from here. It can: "a driver's
normalize-unquoted-name is a pure case fold" is just the assertion that lower-casing the input and the
normalized output gives the same string. Trimming, stripping or substituting characters all break it, and
any of them would make the prefilter drop rows that should have matched.
Each implementation is invoked directly rather than through driver dispatch, so no driver needs registering
or initialising. The test also asserts which implementations it found, because `methods` only sees those
loaded in the run -- without that it would quietly keep passing while covering less and less.
Comment at the call site updated: both assumptions now name their test rather than one being described as
untestable.
* GHY-4251: Avoid the deprecated mbql-query in the new findings test
The full kondo lint that CI runs reports cross-namespace deprecation warnings that per-file runs do not,
and CI fails on warnings. The new provider-sharing test used mt/mbql-query, deprecated since 0.61.0 for
returning legacy MBQL 4.
The test only needs two cards with distinct queries, so use native ones, matching the equivalent test in
backfill_test. That avoids the deprecation rather than adding a namespace-level exclusion for it.
* GHY-4251: Resolve table ids with t2 in sql-tools, then fetch metadata through the provider
Replaces the lib-be/tables-by-name added in 88b011c527. Resolving a name parsed out of native SQL is a
lookup, so sql-tools does that itself against the appdb and gets back ids only; the metadata for those ids
then comes through the MetadataProvider via lib.metadata/bulk-metadata, which is the ordinary :id spec.
Better than fetching the metadata directly on three counts. lib-be grows no public API. Overrides and
caching still apply to the metadata, because it goes through the provider. And the :id spec is cached
per-id, so entities in a batch naming overlapping tables share those entries -- the whole-spec keying of the
earlier version meant {orders} and {orders, products} were separate cache entries fetching orders twice.
The active and visibility filter moves into the id query: an :id lookup does not apply it, and the catalog
fetch this replaced was active-only, so it has to happen where the ids are chosen. The case-folding test
moves with the function, and gains coverage of that filter.
sql-tools no longer depends on lib-be. The module config records :model-imports #{:model/Table} instead,
which scopes the new dependency to the one model rather than a whole module.
Verified with the OOM repro: still 0 catalog fetches, completes in a 1200m heap where master OOMs.
Review feedback from Alex.
* GHY-4251: Drop an incidental reformat from lib_be/metadata/jvm.clj
Left over from the :name-ci spec key that was added and then removed. Restoring the line takes lib_be out
of the diff entirely, so this PR no longer touches any Querying Platform owned path.
* Add backend granular-rerun collector
Scan hawk's JUnit output and emit the failed test vars as a hawk `:only`
vector so a failed backend/driver run can be rerun targeting only those
tests instead of the whole suite. Reuses the existing backend JUnit
adapter. Writes nothing unless every failure is attributable to a var, so
an unattributable failure falls back to a full rerun.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Wire granular reruns into the driver test action
On a re-run of a failed backend/driver job, restore the tests that failed
on the prior attempt and run only those via hawk `:only`, instead of the
whole suite. After a failed run, collect the failed vars and upload them
as an artifact keyed by junit-name; a re-run downloads it and targets those
tests. Falls back to the full suite whenever no confident failed-test set
exists (first attempt, missing/empty artifact, or unattributable failures).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Exempt stress workflows from granular reruns
Add a `granular-rerun` input (default true) to the driver test action,
gating the restore/collect/upload steps on it. Stress workflows set it to
false: they intentionally run a fixed test set `:times N` to hunt
flakiness, and narrowing a re-run to only the failed vars would drop that
repetition.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Wire granular reruns into the non-driver backend tests
Extend the be-tests job with the same restore/collect/upload flow as the
driver action: on a re-run of a failed job, run only the tests that failed
on the prior attempt via hawk `:only`. The rerun drops the tag and
partition args, since the restored vars are already this partition's own
failures — re-partitioning the small set would drop half of them. Each
partition keeps a distinct artifact, so it reruns exactly its own failures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Give app-db partition legs a per-job rerun artifact key
The granular-rerun failed-tests artifact was named solely from junit-name.
app-db's partition legs (Enterprise Tests, EE App DB Part 1/2) share one
junit-name, so they collided on a single artifact and a rerun could restore
the wrong leg's failed set. Add an optional rerun-artifact-key input to the
test-driver action and set a per-job-unique value for those legs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Collect hawk's var-less-errors JUnit file for granular reruns
hawk (PR #50) now writes fixture-init and namespace load/compile errors to a
fixed-name mb_hawk_var_less_errors.xml, with a testcase that has a name but no
classname. The backend collector only scanned *_test.xml, so it silently
ignored that file and the run's failing set stayed incomplete. Match the file
too; its classname-less testcase parses to a namespace-less failure, which
correctly forces a full rerun instead of a misleadingly narrow one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add a granular-rerun knob
* Scope the rerun artifact to the attempt that recorded it
The failed-tests artifact was named per job, so it outlived the attempt
that wrote it. An attempt that passed, or that failed with nothing
attributable to a test var, uploads no artifact, leaving the previous
one in place for the next attempt to restore -- narrowing a run that
should have gone wide.
Name it `...-attempt-N` on upload and read `attempt - 1` on restore, so
only what the immediately preceding attempt recorded is ever used and
anything else falls back to the full suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Compute the previous attempt number in the shell
Workflow expressions have no arithmetic operators, so
`${{ github.run_attempt - 1 }}` is not a valid expression -- actionlint
rejects it, and the runner would not evaluate it either.
Resolve it with `$((GITHUB_RUN_ATTEMPT - 1))` in a preceding step and
read it back from the env context.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Clear pre-existing actionlint findings in the stress workflows
These workflows are only linted when a PR touches them, so the findings
below sat unreported until this branch edited both files.
`transforms-python-stress-test.yml` omitted the required
`previous-step-outcome` input on all eight `upload-test-results` steps;
each `test-driver` step already carries `id: test-driver`, so wire its
`test-outcome` output through.
`drivers-stress-test.yml` tripped shellcheck: an unquoted `$JAVA_HOME`
(SC2086) and three consecutive appends to an unquoted `$GITHUB_OUTPUT`
(SC2086, SC2129), now a single quoted group redirect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Rename logs-artifact-suffix to artifact-suffix and reuse it for reruns
The failed-tests artifact needs a per-leg discriminator, which test-driver
cannot derive on its own: `matrix` and `strategy` are unavailable inside a
composite action, and `github.job` is the job id, identical across legs.
`logs-artifact-suffix` already carried exactly that value, so reuse it rather
than take a second input for the same concept.
Name both artifacts from `github.job` + `junit-name` + `artifact-suffix`.
Neither of the first two is unique on its own once run-tests.yml pulls app-db,
backend, drivers and semantic-search into one run and one artifact namespace:
`be-tests-postgres` is a job id in two of them and both derive
`be-tests-postgres-ee`.
Fill in the suffix wherever a matrix runs concurrent legs but did not set one,
or set one that did not separate them. Those legs already collided on the test
logs artifact; without this they would also restore each other's failed-test
set on a rerun and re-run the wrong tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Correct the var-less error description in the rerun collector
hawk writes var-less errors to mb_hawk_var_less_errors.xml without a
`classname`; the header claimed they leave no JUnit trace at all. What makes
them unusable for a narrow rerun is the missing namespace, not a missing file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Let a colliding failed-tests upload fail instead of overwriting
The artifact name is scoped to the job and the attempt, so nothing legitimately
uploads it twice. With overwrite, a name collision would silently resolve to
last-writer-wins and a rerun would restore another job's failed set; without it
the upload 409s and says so. The step stays continue-on-error, so a collision
degrades to a full rerun rather than failing the job.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Clear the restored failed-tests file when a rerun can't be narrowed
The collector signals "rerun everything" by leaving no file, but on a rerun that
path already holds the previous attempt's set, restored for the test step to
read. Declining to write left it in place, so the upload shipped last attempt's
list as this one's and the next attempt narrowed to it again — a namespace-load
failure could keep the full suite from ever running again.
Delete it on the no-confident-set path, and again in the CI step before the
collector runs, which also covers the collector dying before it decides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Move metabase:// protocol link helpers from metabot to urls
The metabase:// URL protocol helpers (parse/create functions, regex
constants, and entity model types) are a cross-cutting URL scheme used
by both metabot and rich_text_editing, so they belong in metabase/urls
rather than metabot/utils. Moved links.ts (and its spec) to
urls/metabase-protocol.ts, exported it from the urls index barrel, and
updated all importers.
* mysql mbql5
* clickhouse mbql5
* starburst mbql5
* sqlite mbql5
* presto-jdbc mbql5
* druid-jdbc mbql5
* vertica mbql5
* athena mbql5
* hive-like mbql5
* fix databricks
* fix path
* sqlserver mbql5
* oracle mbql5
* fix sqlserver tests
* fix test failures
* fix sqlserver test errors
* clean up
* kondo, test fix
* remove sql-mbql5
* remove from more drivers
* update empty string is null driver
* simplify helpers
* remove unneeded defmulti
* fix field clause
* sqlserver mbql5
* oracle mbql5
* fix sqlserver tests
* fix test failures
* fix sqlserver test errors
* clean up
* kondo, test fix
* small changes
* remove mbql-clause and mbql-clause-with-opts
* make fixes
* more fixes
* remove new mbql5 usages
* Require metabase.driver.sql.pivot so the [:sql :pivot] emitter registers
Removing the sql-mbql5 driver moved sql_mbql5/pivot.clj to sql/pivot.clj,
but the side-effect require moved nowhere: on master
metabase.driver.sql-mbql5 required its pivot namespace, and after the move
no production namespace loaded metabase.driver.sql.pivot at all. Without
that load the apply-top-level-clause [:sql :pivot] method never registers,
so native pivot queries compile to a plain GROUP BY with no pivot-grouping
column (breaking pivot results at runtime). Whole-suite CI masked this
because loading the pivot namespace's own test file registers the method.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* lower ratchets
* fix ratchets whitespace
* use map over mapv
* remove oracle :value
* fix bigquery
* add back comments, update change log
* move comments
* remove unneeded mbql4 cases
* move pivot to sql
* destructure after clause check
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* GDGT-2938: Review fixes
* Keep the broken-dependents column wide enough for its header
Capping the column truncated the 'Broken dependents' header, which turned on
the Ellipsified tooltip and left two copies of that text in the DOM once the
header was clicked. Its cells hold a short count, so auto width measures the
header and is already stable across pages.
* GDGT-2938: Address PR comments
* Load permission caches whole, with tables in a RoaringBitmap
Each of the three request-scoped permission caches now loads every
database on its first miss, instead of loading the scope it was asked
about and tracking which scopes it covers. That removes the reason the
prime functions existed: a caller no longer has to declare what it is
about to check, so `prime-table-perms-cache`, `prime-db-perms-cache` and
`prime-schema-perms-cache` are gone along with their twenty-odd call
sites, and with them the whole class of bug where a new call path silently
becomes a query per table.
Loading everything is only affordable because the table cache stops
storing a value per table. It now keeps, per permission type, one
RoaringBitmap per permission value holding the IDs of the tables that
resolve to it — a partition, so a lookup walks the type's values and stops
at the one that matches, and each table ID is stored once. Table IDs are
unique across databases, so nothing is keyed by database any more, and
only tables with a grant of their own appear at all.
The bitmaps are built by mutation while private to the load and never
written to once published.
Uncached callers — a check on a user other than the requester, or inside
`disable-perms-cache` — still load just the one table or database they
ask about, since no later check will amortize a wider load.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Only use a permission cache that a request scope actually bound
The three caches are dynamic vars, and `use-cache?` checked only that the
user being asked about was the requester -- not that anything had bound a
fresh atom. Code running outside `with-relevant-permissions-for-user` read
and wrote the JVM-wide root atom.
That was survivable while a cache tracked which databases and tables it
covered, since an unseen one still forced a load. Now that a load covers
everything, the first check for a user writes "all of it, for good" into
an atom with no lifetime, and every later check for that user in the same
JVM is answered from it -- so a permission granted afterwards is never
seen. Tests, which set `*current-user-id*` freely outside any request
scope, poison it on the first check.
The root binding is now nil and `use-cache?` takes the cache, so an
unbound one reads through to the database. This is what bounds the
staleness each cache's docstring promises.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Record which databases a cache load covered
A cache that loads everything cannot tell "the user has no rows for this
database" from "this database did not exist when I loaded", and answered
both as no-permissions. Callers fall back to the least permissive value,
so a database created inside a request scope came back `:blocked` --
which is what broke the driver and app-db suites: `mt/dataset` builds a
database inside the scope `mt/with-test-user` has already opened, and
every check against it was denied.
Each cache now stores, per user, the set of database IDs its load
covered alongside the index. A question about a database outside that
set reloads. The set is read before the load, so a database appearing in
between counts as uncovered and forces a reload rather than being
wrongly marked covered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Revert the bitmap cache, back to scoped permission caches
Loading every database on a miss traded per-call scoping for a cache that
then has to prove what it covered, and it cost two correctness bugs of
the same family -- the cache over-claiming, first across time (no
lifetime) and then across databases (no coverage record). The memory win
did not pay for that surface.
Back to master's caches, which load the table or database they were asked
about and record it. The next commit adds what this branch was really
after: making an unprimed check loud instead of silently expensive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Add strict cache-miss detection and prime the paths it found
Adds *perms-cache-misses-are-errors?*, bound true by
with-relevant-permissions-for-user in dev and test. A cache's first load
is ordinary; a later miss means an earlier load already covered a
different scope, which is the signature of an unprimed run.
prime-table-perms-cache now also primes the db cache and takes :db-ids,
since table-permission-for-user reads the database's own grant too, and
call sites pass :db-ids wherever they have them.
Genuine N+1s this found and fixed: readable-fields-only, metric
dimension filtering, and the schema listing's table filter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Throw on the fifth cache miss, not the second
Measured across the search, sandbox, dashboard and sync suites: at a
limit of three, 30 tests fail; at five, 18; and raising it to eight or
twelve changes nothing. So the cases between three and five are bounded
-- a dashboard whose parameters span two databases, an entity read plus
a chain-filter elsewhere -- and everything still caught at five is an
unbounded run. MB_PERMS_CACHE_MISS_LIMIT overrides it for re-measuring.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Drop the miss-limit env var, keep the measured constant
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Correct the miss-limit measurement, revert the unfounded search prime
The 18 failures I measured against were pre-existing: the same batch on
the base commit produces them identically, so the apparent plateau from
five upward was those failures, not the limit catching anything.
Re-measured against that baseline: a limit of two flags 19 sites and
three flags 10, all bounded -- an entity read plus a chain-filter, a
dashboard spanning two databases -- and at four and above none flag. Five
stays, but as a backstop against runs that grow with the data rather than
a linter for every second check.
Also reverts the search prime, which was added on the assumption those
failures were permission misses. They were not, and it bought a query per
search for nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Prime the db perms cache for search's per-table permission checks
`check-permissions-for-model` resolves `table` and `indexed-entity`
results against their database, so a search spanning N databases cost N
queries. Results are consumed as a stream, so the databases involved are
never known up front -- and a search ranges over all of them anyway, so
prime every database.
Caught by the strict cache-miss check: metabase.search.api-test's
permissions-test failed with 24 database-level misses, all from
search.impl/can-query?. Postgres-only because the appdb search engine is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Order broken pulses by id so notification order is deterministic
`broken-pulses` selected with no ORDER BY, so the order the notifications
went out in was whatever order the rows came back in.
handle-broken-subscriptions-due-to-bad-parameters-test asserts the second
email is the one about the slack pulse, so it fails whenever the two
broken pulses come back the other way round. Shard-dependent, since the
physical row order shifts as other tests churn the table -- it does not
reproduce standalone, and it fails on master too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Exclude destination databases from full-database selects
Destination databases are reachable only through their router and never
carry data_permissions rows of their own, so listing them just widens the
load. The permission graph in permissions_rest already filtered them;
admin-permission-graph and the new search prime did not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Prime table perms, not just db perms, for search's table results
A `table` search result is checked with `can-query?`, which goes through
table-permission-for-user -- and that reads the table's own grant as well
as its database's. The database half short-circuits only when the db-level
grant is already maximally permissive, which is why the CI failure showed
database misses alone; a user with table-level grants would have gone on
to miss the table cache once per result.
prime-table-perms-cache covers both caches. Passing only :db-ids keeps it
affordable: the table cache holds data_permissions rows carrying a
table_id, so the load is bounded by the user's table-level grants rather
than by how many tables exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Point the cache-miss error at one prime function and one entity
The message listed all three prime functions and the ex-data carried only
a kind, so it said a check was slow without saying which check.
cached-database-perms takes the table-id its caller was checking, when it
has one. A miss reached that way is a table check -- table-permission-for-user
reads the database cache on its way through -- so it now reports kind
:table and names prime-table-perms-cache, which primes both halves;
prime-database-perms-cache would have left the table half unprimed.
The kind -> function map is spelled out rather than built from the kind, so
renaming one of those functions turns it up. Also renames db to database in
the function names touched here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Rename full-db-permission-for-user to full-database-permission-for-user
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Batch the db-ids table-perms load, and stop formatting ids as numbers
Two review findings, both real:
prime-table-perms-cache batched only its :table-ids branch. The :db-ids
branch passed the whole set to one statement, and search now hands it every
non-destination database, so a large enough instance would blow the
bound-parameter cap. Batched like its sibling and like
prime-database-perms-cache.
The miss message went through tru, so MessageFormat rendered table 1234567
as "table 1,234,567". It only ever throws in dev and test, so it does not
need translating -- plain format, no grouping. The test used id 42, which is
below the grouping threshold and so passed against the broken format; it now
uses ids past the thousands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The query_execution_partitioned migration failed on stats because the primary key
constraint had an inconsistent name; it originally came from h2 so it didn't match
the constraint name we saw in development. This migration should do nothing unless
the DB is in the same situation as stats where it has a bad constraint name.
`=?` applies an expected function to the actual value, so `complement` --
`clojure.core/complement`, the higher-order function that negates a
predicate -- evaluates to `(complement <actual-value>)`, which returns a
new function. Functions are truthy, so those checks passed for every
input and asserted nothing. `:hawk/key-not-present` is the assertion the
lines were reaching for: the key is absent from the spec.
Hawk also stopped feeding absent keys into the expected predicate as a
truthy sentinel, and now reports a missing key as a diff outright, so
the remaining checks over keys that are not present no longer pass
vacuously either.
In `can-change-from-password-test` that exposed two assertions the test
DB could never satisfy: it authenticates with an uploaded private key
and sets `use-password false`, so `:password` was absent from both the
details and the resulting spec, while the first two cases assert that a
password is present and takes precedence over a key file. The details
now carry a password to switch away from, which is what the test is
about.
In `normalize-use-password-test`, `normalize-details` only ever assocs
`:use-password true`, and only when no private key is configured. The
DB under test has a private key id, so the key is never added -- absent
rather than false.
* Validate OAuth token scopes for MCP
* Test MCP tool calls are refused outside token scopes
* Clean up scoped MCP token test
* Use scoped credentials for MCP UI
* Use MCP public API for UI credentials
* Fix credential test regex, auth-method arity test, reflection warnings
- api_test: the uiCredential extraction regex was over-escaped (matched
literal backslashes) and could never match the rendered HTML, so
mcp-ui-credential-validation-test failed on every backend job
- middleware session_test: auth-method grew a 5th arg (mcp-ui-info) but
auth-method-test still called it with 4, throwing ArityException; also
cover the new "mcp-ui" auth method
- session.clj: type-hint the signature comparison so Eastwood's
reflection check passes
- align let/map bindings per style guide; mention the MCP UI credential
path in wrap-current-user-info's docstring
- frontend: scope the no-literal-metabase-strings disable to the header
line with a reason; drop now-unused setSessionTokenHeader and update
stale comments that referenced it
---------
Co-authored-by: Bryan Maass <bryan.maass@gmail.com>
The "Failed to cancel sync" toast used the app-wide 5s default, which is
too short to read and screenshot, especially since the same handler
dismisses the sync modal underneath it.
Fixes#79029
BOT-1937: Restore the tool call details popup in Metabot chats
Moving the AI analytics views to Monitor replaced the self-contained tool
call modal with an onSelect callback, wired up only by the Monitor
conversation detail page. Normal Metabot chats pass no callback, so
clicking a tool call in a /debug chat did nothing.
Bring the modal back for callers that don't render details themselves,
composed from the existing ToolCallTitle and ToolCallDetailsContent so
the chat and the Monitor sidebar keep sharing one details renderer.
* GDGT-2931: Make Alerts Management tabs visible by default
Removes the full-page blocking loader on Alerts Management so tabs,
filters, and search render immediately, matching the pattern used by
Dependency Diagnostics and CLI/MCP analytics. The grid now shows an
animated skeleton while loading instead of blocking the whole page.
All three tabs (All/Failing/Ownerless) always render with a live count
badge instead of hiding tabs with zero results.
* Add positive assertion for the tab count loading skeleton
Give the skeleton badge a test id so the loading-state test can assert
it's actually rendered, not just that the count text is absent.
* Drop the visually-hidden loading status for the table skeleton
Reverting a restore I mistakenly applied earlier — this simplification
was an intentional manual edit, not something to undo.
* Remove the tab-switch refetch test and its listGate scaffolding
* GDGT-2931: Address PR comments
* Use react-router's own <Navigate>
* Interpolate redirect targets with generatePath and resolve them like v7
* Justify queryToSearch's encoding on its own terms, not history@3's
* Re-export react-router from the barrel instead of a file each
* Flatten the v7 folder into metabase/router
* Drop the V7 suffixes and merge the two router providers
* Use react-router's own hooks and types instead of mirroring them
* Drop the dead function form of a Link target
The subscription save path borrowed getActionErrorMessage from
metabase/actions/utils, whose fallback text ("Something went wrong
while executing the action") is wrong for this call site. Call
getResponseErrorMessage from metabase/utils/errors directly with a
subscription-specific fallback, and drop the actions import.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Put CI MySQL/MariaDB datadirs on tmpfs
A full app-db migration chain on a stock mysql:8.0 container is ~59s of
which ~44s is fsync waiting: MySQL 8 images default to full durability
with the binlog on (log_bin=1, sync_binlog=1, flush_log_at_trx_commit=1),
every DDL statement implicitly commits, and DDL additionally fsyncs the
table files it creates — one measured setup-db! run performed ~50k data
fsyncs + ~37k redo-log fsyncs across 1,464 changesets. No statement is
hot; it is a uniform ~30ms/changeset disk tax, paid for a database CI
deletes minutes later.
Durability knobs cannot recover it: the dynamic ones
(flush_log_at_trx_commit=2, sync_binlog=0) win back only ~20s because DDL
data-file fsyncs are not governed by them, and --skip-log-bin needs a
server command-line flag GHA service containers cannot pass. --tmpfs fits
the one field they do accept (options:) and neutralizes every fsync class:
measured 59.3s -> 15.8s per setup-db!, reproduced.
Applied to every mysql/mariadb service container (app-db, drivers,
semantic-search, cross-branch-migrations). Expected effects: the :db init
timeouts that outran #79014's 120s bump within a day become structurally
impossible (~5-16s init), the ~25min of Liquibase inside a mysql app-db
job mostly evaporates, and job duration stops tracking runner disk
contention. Cost: the app db lives in runner RAM (test DBs are MBs) and
images with pre-baked datadirs re-initialize at container start.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCo9W7tgK13nNSfWTtQoUc
* Add required description to the cross-branch-migration-test action
Pre-existing actionlint error surfaced by touching
cross-branch-migrations.yml (actionlint scopes to changed files).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WCo9W7tgK13nNSfWTtQoUc
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Run route-leave guards on native useBlocker
* Chain the route-leave guards and scope the registry to the provider
* Keep the prompted destination when another navigation supersedes it
* Keep the held navigation in the ref, not resynced from state
* Resume the captured navigation instead of reissuing it
* Revert the supersede handling, keeping the guard fan-out
* Stop the dashboard syncing its URL while a leave prompt is up
* Partition the query_execution table when using Postgres AppDB.
The `query_execution` table gets very big on large installs and poses
performance problems. Partitioning it will allow us to get those under control.
This will only apply when using Postgres as the AppDB. If you have an H2 AppDB,
it's probably not big, and if you have a MySQL AppDD, then, uh, well, too bad.
The basic plan is to rename the existing `query_execution` to
`query_execution_old` and then create a new `query_execution` partitioned table
with the same schema. Once that's done, we can attach `query_execution_old` as
a partition to the main table, and all the data should be available transparently.
Unfortunately partitioned tables have gotchas that complicate things. This
means the migration plan is split into different steps.
The partitioned table must not have a primary key that doesn't include the
partition key. In order to do this, we have to stop using `id` as the primary
key and replace it with a composite key of `id, started_at`.
The first migration swaps the primary keys around, dropping the ones
based on the id alone and replacing them with ones based on `id` and `started_at`.
The second migration renames `query_execution` to
`query_execution_old` and then creates the new `query_execution` table
which is based on `query_execution_old` but partitioned by
`start_at`. Then it takes the `query_execution_old` table and attaches it as a
partition into the full `query_execution` table. This partition covers up to
the current month, plus one more after, in order to handle the case where the
migration runs at the end of the month and query executions need to be written
to disk before the job to create the next partition gets a chance to run.
The table's IDENTITY has to be dropped since this can't be done in a partition;
it should be handled by the higher level table.
The v_query_log view references the old query execution (now `query_execution_old`)
table. We need to re-create the view to point it at the new table instead.
Finally a migration dynamically calculates a new name for `query_execution_old`
to turn it into `query_execution_2026_08` or whatever, depending on
the date of the data that the table will actually contain.
* Add a metabase.app-db.partitions jobs for managing old/new partitions.
The migration puts us in a good state for the present, but as time
goes on, we'll need new partitions for new months. The new
`metabase.app-db.partitions` namespace handles this by defining a new
`ManagePartitions` job that runs daily to see if any new partitions
need to be created or whether any existing partitions need to be
detached.
Each run checks the current month's partition and the next month's
partition and creates them if they haven't been created yet. It also
checks for the retention cutoff period to see which partitions to
detach.
The individual logic functions and the `manage-partitions` function
that tie them together are tested, but the `defjob` is not; I don't
know if there is a way to test jobs.
There is no mechanism for actually deleting the old data once it
passes out of the retention period; it merely becomes detached. We may
need to document this somewhere so that users know they can clean it
up to save disk space. (It's unclear to me where that would be
documented.)
* Disable truncate-audit-tables on query_execution when using postgres.
* Address review suggestions.
* Set sequence on new partitioned table to match old sequence value
* Ensure partitioning job is loaded at startup
* Fix off-by-one error in partition boundaries for dropping
* Use locale-independent yyyy year formatting
* Move to metabase.audit-app.task.partitions
Still needs to address the fact that rollback only converts a single
table instead of all partitions back into one table.
* More review suggestions:
* Fix rollback sequence restoration to target old identity sequence.
* Fix rollback of table renames to use the oldest partition.
* Make partition management cron fire immediately in cases when it's been missed.
* No need to export retention days setting in audit-app module.
* Don't recreate primary key with id, started_at; just drop the primary key.
As far as I can tell, the primary key is not used any more since the
field_usage table got dropped; all reads of this table are just counting.
* Drop partitions instead of just detaching when cycled out of retention.
* Fix rollback to drop view first, then recreate at the end.
* Avoid relying on max(id) in partition migration.
The index will be gone by the time this runs, so it would result in a
full table scan of a HUUUUUUGE table.
* Bump `hawk` to `1.0.18`
This corresponds to
c17e2b9b4e
* fix bad tests
* select data key
---------
Co-authored-by: Riley Thompson <rileythompson99@gmail.com>
* Retry the quarantine list fetch before giving up
A dropped socket or a 5xx from ci-conductor made the gate report COULD NOT
CHECK on the first try. Transport errors, timeouts, 429s and 5xx now get three
attempts with exponential backoff and jitter; a 4xx returns straight away.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* improve retry api
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ryan Laurie <iethree@gmail.com>
* Add allowedCustomVisualizations setting to EAJS embeds
This commit adds the plumbing necessary to pass down the
value of the allowlist for custom viz.
It warns when wrong names are passed (ie: not starting with `custom:`),
and forces [] for guest embed as the feature is not supported there.
* Move sandbox-host to its own namespace to remove +auth from it
The donor document is loaded as an iframe src, which cannot carry session
headers, so the endpoint must be reachable without authentication. Auth is
applied per-namespace by ns-handler in custom-viz-plugin.api and cannot be
lifted for a single endpoint, so the endpoint moves verbatim to its own
namespace, mounted without +auth next to the session-authed routes (both
still behind the :custom-viz premium gate). No behavior change other than
dropping the auth requirement.
* Serve the custom viz sandbox donor to EAJS embeds
Makes the donor iframe-able as long as either a valid session is passesed
or the correct sec-fetch-site headers are passed.
* Allow blob: images on the EAJS embed page for custom viz icons
Custom viz icons in the chart type picker are served by the SDK asset
manager as blob: URLs (a cross-origin img cannot carry the session
header, and EAJS has no cookies). The modular embedding page runs on the
Metabase origin under a strict img-src that omits blob:, so the browser
blocked the icons and left a broken image in the picker. Add blob: to
img-src only for the /embed/sdk/ entrypoint, gated on a URI predicate so
the main app and static or public embeds keep the stricter policy.
* chore: tidy the SDK release dispatch forms and bump output
The dispatch form renders an input's description as the control's label,
which is a single plain text node: newlines collapse to spaces and no
markdown is parsed. Both SDK release workflows had multi-line
description blocks written as if they rendered, so release_type's
occupied ten rendered lines and pushed the select below the fold.
- release_type: one-line description keyed by option, options reordered
into release-cycle order, and default: alpha pinned so the reorder
does not move the preselection. Option values are unchanged.
- Detail that no longer fits the form moved to the file header.
- Shortened the two read_first option strings that overflowed the
collapsed select at 44 and 81 characters.
- Gave the version-bump PR body's table a real header instead of an
empty one (EMB-2167).
- Capture the new PR's URL and append it to the job summary; the
existing summary step runs before the PR exists (EMB-2168).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENnTtrTcig1vH3QADbsGgo
* chore: capitalise release type names in the dispatch label
The label renders as plain text, so no markdown emphasis is available.
Caps makes each option findable when scanning the wrapped paragraph.
Values stay lowercase - this is display emphasis only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENnTtrTcig1vH3QADbsGgo
* chore: say what each dispatch input is for, not what it isn't
prerelease_id read as an output description, so it never said what to
type. Phrase it as a mapping: enter data-apps, get 0.62.5-data-apps.0.
Same length, same six rendered lines.
The release workflow's label opened with a double negative - use it
'when neither the bump PR nor re-running the failed jobs will do it' -
which left the reader to infer the purpose. It is a manual override, so
say that first. Retry guidance stays in the file header, where there is
room for it. Eight rendered lines down to six.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENnTtrTcig1vH3QADbsGgo
* chore: drop a redundant comment above the dispatch label
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ENnTtrTcig1vH3QADbsGgo
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dependency upgrade: align both drivers that ship org.apache.thrift/libthrift on the latest release (0.24.0).
hive-like: bump the existing direct pin 0.16.0 → 0.24.0. Since 0.19.0, libthrift declares runtime deps (httpclient5, httpcore5, jakarta.servlet-api) that only serve its THttpClient/TServlet classes; hive-jdbc is built against httpclient 4.x (already pinned in this module) and cannot use them, so they are excluded to keep the driver jar lean. Verified by scanning every class in the built jar: the only classes referencing those packages are libthrift's own five HTTP/servlet classes - nothing in the hive/hadoop stack touches them.
databricks: pin 0.24.0 over the 0.23.0 bundled with databricks-jdbc-thin 3.4.1, following this module's existing pattern of pinning newer transitives (lz4, bouncycastle, jackson, commons-configuration2).
Previously the two driver jars bundled different libthrift versions (0.16.0 and 0.23.0) into the shared plugin classloader; this aligns them on a single version.
Also updates the Spark SQL connection form's "additional JDBC options" placeholder from ;transportMode=http to ;transportMode=binary - binary is the default and supported transport; hive-jdbc's http transport mode is not compatible with libthrift ≥ 0.19 (it passes an httpclient 4.x object to a constructor that now takes httpclient 5.x, see #46793), so the form should not suggest it.
Routine dependency upgrade: bumps the pinned at.yawk.lz4/lz4-java (maintained fork of the discontinued org.lz4:lz4-java) to the latest release, 1.11.1, in the ClickHouse and Databricks driver modules.
Upstream changes between the pinned versions and 1.11.1 are additive only (new fast-reset compressor classes, a linux-riscv64 native binary, bugfixes) — no classes removed, same net.jpountz packages, so it is a drop-in update for both JDBC drivers. Neither driver module calls the library directly.
* GDGT-2884: Move AI analytics to Monitor (#78222)
* GDGT-2874: Relocate AI analytics source under monitor/ai-auditing
* GDGT-2874: Add Monitor URL helpers for AI Auditing
* GDGT-2874: Repoint hardcoded legacy paths to Monitor URL helpers
* GDGT-2874: Swap MetabotAdminLayout for MonitorMain in AI Auditing pages
* GDGT-2874: Fix stale audit_app import in metabot-analytics test-support mock
Plan gap: this file wasn't in Task 1's moved-tree scope and wasn't listed as an
external consumer to fix in a later task. Caught during Task 4 verification
via type-check-pure.
* GDGT-2874: Add AI Auditing sections to monitor_section_clicked event
* GDGT-2874: Add canAccessAiAuditing selector and route guard
* GDGT-2874: Add combined AI Auditing routes getter and plugin index
* GDGT-2874: Move AI Auditing plugin wiring from PLUGIN_AUDIT to PLUGIN_MONITOR
* GDGT-2874: Mount /monitor/ai-auditing route block
* GDGT-2874: Add AI Auditing tab to Monitor nav
* Add regression tests for AI Auditing getActiveSection ordering
Lock in the order of the mcp/conversations/usage-stats route-matching
clauses in getActiveSection: the bare Urls.monitorAiAuditing() path
string-prefixes both sub-paths, so the more specific clauses must be
checked first (ts-pattern's P.string.startsWith is first-match-wins).
Without this, a future reorder would silently break active-tab
highlighting for the two sub-routes.
* GDGT-2874: Remove Auditing folder from Admin AI nav
* GDGT-2874: Add legacy redirects for /admin/metabot/usage-auditing
* GDGT-2874: Fix moved AI Auditing spec mocks and path assertions
* GDGT-2874: Repoint slash-command URL helper off the legacy admin path
The Metabot /inspect slash command still built its target URL via
metabase-enterprise/urls.ts's adminMetabotUsageAuditingConversation,
which hardcoded the old /admin/metabot/usage-auditing/conversations
path. It only worked because it bounced through the legacy redirect.
Delete that helper (its only caller) and use the canonical
Urls.monitorAiAuditingConversationDetail directly.
* GDGT-2874: Update E2E specs to canonical /monitor/ai-auditing URLs
usage-auditing.cy.spec.ts and mcp-analytics.cy.spec.ts predate the
Monitor migration and still visited/asserted on the legacy
/admin/metabot/usage-auditing* URLs, which only worked via the legacy
redirect. In-app navigation (row clicks, breadcrumbs, back button) now
lands on /monitor/ai-auditing* URLs, so any post-navigation URL
assertion in these specs was exercising stale paths. Point every
cy.visit()/pathname assertion at the new URLs, and add a small
regression test asserting the legacy URL still redirects, since the
main specs no longer exercise it directly.
* GDGT-2874: Apply AI Auditing cross-review fixes
* GDGT-2874: Add AI Auditing availability placeholders
* GDGT-2874: Gate AI Auditing pages with settings placeholders
* GDGT-2874: Keep unavailable AI Auditing links navigable
* GDGT-2874: Prioritize global AI settings for MCP availability
* GDGT-2874: Fix empty screen layout
* GDGT-2874: Use semantic AI Auditing navbar icons
* GDGT-2874: Restore Audit plugin ownership
* GDGT-2874: Test legacy redirects in app routes
* GDGT-2874: Add canonical Usage stats route
* GDGT-2874: Cleanup
* GDGT-2874: Address PR review feedback
- Rename AiAuditingAvailabilityLayout -> AvailabilityLayouts: the file
exports two layout components (Metabot and MCP), neither named after
the file/folder.
- Use !!useSetting(...) for the enabled/configured flags, matching the
convention used everywhere else these settings are read.
* GDGT-2883: Restyle AI Auditing to match Monitor (#78356)
* GDGT-2874: Fix moved AI Auditing spec mocks and path assertions
* GDGT-2874: Apply AI Auditing cross-review fixes
* GDGT-2883: Restyle AI Auditing to match Monitor
* GDGT-2883: Fix inconsistencies
* Fix issues surfaced by restacking onto gdgt-2874
- Remove index.unit.spec.ts: orphaned artifact from a superseded
gdgt-2874 draft, testing a ./index module that was never actually
relocated here (registration logic still lives in audit_app/index.js,
already covered by routes.unit.spec.tsx).
- Un-mock ConversationHeader in ConversationDetailPage.unit.spec.tsx:
the blanket mock predated the conversation-title test and was
hiding the real header it needs to assert against; the endpoint
mocks setup() already provides make the real component safe to
render.
- oxfmt fix in ConversationsTable.tsx.
* GDGT-2883: Fix Monitor layout consistency across AI Auditing pages
- Contain the Tool calls grid to the viewport (MonitorMain only clips
when its parent is a flex container, so wrap it in a Flex h="100%"
on the events tab; the charts tab keeps natural page scroll)
- Do the same for the Conversations grid page
- Hide the events table's pagination until it has loaded once, instead
of showing a nonsensical "1 - 0 of N" range
- Add showTotal to the Conversations page's pagination, which was
missing it entirely
- Match tabs/filters/content spacing (md) to Background Tasks on Usage
stats and MCP analytics, which had drifted to lg
- Vertically center the ellipsis menu button against the conversation
title in ConversationHeader
* GDGT-2883: Make all AI Auditing grid columns sortable, fix non-clickable cursor
- TreeTable: only show the pointer cursor on rows that actually respond
to a click (onRowClick or getRowHref set), instead of unconditionally
on every row. Fixes MCP analytics' Tool calls grid looking clickable
when it isn't, and the same latent bug on several other display-only
TreeTable consumers.
- MCP analytics Tool calls: wire up sorting for the remaining columns
(ID, Client version, Tenant, IP address, Error type, Error message).
The query builder already supported ordering by any valid view
column, so this is purely frontend wiring.
- Conversations: add Title as a sortable column, including the backend
allow-list/schema entry it needs. Queries and Searches stay
unsortable — they're computed in-memory per page from message
content, not real SQL columns.
- Fix a stale field name (summary -> title) in ConversationsPage's
test mock that was failing type-check.
* GDGT-2883: Show tool call details in a sidebar on the Conversation Detail page
Replace the tool-call-click modal with a Monitor sidebar, matching the
rest of the AI Auditing views. The live Metabot chat widget keeps its
modal — AgentToolCallMessage now takes an optional onSelect callback
that, when provided, defers to the host instead of opening the modal.
Also style the sidebar's Request/Response copy buttons with the same
Copy/Copied! tooltip used elsewhere in the conversation view, and fix
a pre-existing bug where copying re-stringified the already-formatted
JSON text, copying an escaped string instead of clean JSON.
* GDGT-2883: Toggle the tool call sidebar closed on a second click
Clicking the same tool call record that's already selected now closes
the sidebar instead of re-opening it with the same content.
* GDGT-2883: Constrain conversation content to an 800px column, fix page scroll
Wrap the whole page body (header, stats, feedback, conversation, and
generated queries) in a single horizontally-centered 800px column
instead of leaving it full width.
Fix the page-level scroll along the way: the Flex wrapper added for
the tool call sidebar's width measurement gave MonitorMain's parent
display:flex, which made MonitorMain's flex+overflow:hidden CSS
actually take effect and clip content instead of letting the page
scroll. This page has no internal scroll container of its own (unlike
table-driven Monitor views), so drop the bounded height and let it
scroll naturally.
Also add the missing Copy/Copied! tooltip to AgentToolCallMessage's
own copy button, matching the one already added to the sidebar's
Request/Response copy buttons.
* GDGT-2883: Make AI Auditing pagination abortable, sync page in URL instantly
Every paginated AI Auditing grid (MCP analytics Tool calls, Conversations)
kept a stale in-flight request running when the user paged again before it
resolved, and the URL's page param lagged behind clicks by the shared
useUrlState hook's 300ms debounce — unlike Dependency Diagnostics, where
page changes are both abortable and instant.
- Swap McpEventsTable's and ConversationsPage's queries onto
useAbortableQuery (the same hook DD already uses), so switching pages
cancels the stale request instead of letting it complete in the
background.
- Add an opt-in `immediate` option to useUrlState's patchUrlState that
bypasses the debounce for a single patch, and use it for pagination
clicks specifically. Filter/sort changes keep debouncing as before —
this only affects callers that pass the new option.
* GDGT-2883: Sync pagination to the URL instantly across other Monitor list pages
Same delayed-URL-on-pagination-click bug as AI Auditing, found on a sweep
of every useUrlState consumer with a paginated grid: TaskListPage,
TaskRunsPage, NotificationsAdminPage, and ErrorOverview. Pass the new
patchUrlState immediate option on their pagination handlers too.
TaskListPage's pagination test asserted the old debounced timing (URL
unchanged immediately after a click, then advance the fake timer); updated
it to assert the new instant update instead. The other pages' tests only
asserted the eventual URL state, so they needed no changes.
* GDGT-2883: Use the empty-dashboard illustration for the disabled MCP analytics page
Swap the generic metabot icon for the EmptyDashboardBot illustration and
make the CTA a secondary button, matching other Monitor empty states.
Scoped to the MCP-disabled case only — the AI-features-disabled and
not-configured states elsewhere in this file are unchanged.
EmptyState gains an optional actionVariant prop (defaults to "filled",
preserving every other caller's current look).
* GDGT-2883: Apply cross-review a11y/correctness fixes
- Empty-dashboard illustration + secondary CTA now apply to all four
AI Auditing unavailable states (AI disabled, not configured, MCP
disabled), not just MCP-disabled — that was the intended scope; a
narrower revert from a review pass was itself reverted here.
- Give the tool call copy buttons accessible names (were icon-only).
- Make TreeTable sort column headers keyboard-operable.
- Clear the selected tool call when navigating to a different
conversation, so the sidebar can't reopen with stale data.
- Move focus into the tool call details sidebar on open and back to
the trigger on close; label it as a region.
- Ignore a hidden/PII-gated sort column from a stale URL instead of
querying by it.
- Add a Title-sort UI test and a one-shot immediate-bypass regression
test.
* GDGT-2883: Fix code-review findings in TreeTable and AI Auditing
- Stop sortable header keydowns from bubbling to the treegrid, which
could also activate a stale keyboard-focused row
- Give every populated TreeTable header the columnheader role, not
just sortable ones
- Drop the redundant onRowClick push() on conversation rows, which
double-navigated alongside the row's own link
- Render the MCP analytics loading/empty states inside the active
Tabs.Panel so the selected tab's aria-controls stays valid
* GDGT-2883: Trim redundant comments in McpEventsTable and ConversationDetailPage
* GDGT-2883: Cleanup
* GDGT-2883: Fix e2e tests
* GDGT-2883: Address PR comments
* GDGT-2919: Migrate CLI usage analytics to Monitor (#78900)
* Project MCP events query to curated PII-aware columns
Restricts buildEventsQuery to exactly the columns the events table
displays, keyed off tenant/PII retention settings, instead of shipping
the view's full row (including ip_address/error_message) to the
browser regardless of the setting.
* GDGT-2919: Migrate CLI analytics to Monitor
* GDGT-2919: Drop ai-features-enabled? gate for CLI analytics
The gate was new, not preserved: the old admin route only checked
audit_app/admin access, with no ai-features-enabled? check, and CLI's
v_agent_api_calls data is independent of Metabot/LLM configuration.
* Fix oxfmt formatting
* GDGT-2919: Bring CLI analytics UI in line with the rest of Monitor
- Move the calls filter bar under the tabs and use MCP's spacing values,
keeping tabs/filters mounted (and visible) through loading and empty
states instead of swapping the whole Tabs block out.
- Replace CliEventsTable's AdminDataTable (admin-only, fixed
maxBodyHeight) with TreeTable, matching McpEventsTable's structure so
the Calls tab fills the available height like the rest of Monitor.
* Replace withRouteProps with useRouter across Monitor pages
withRouteProps is documented as a temporary v3-router migration shim
that reads location/params via useRouter() and re-injects them as
props. Have the Monitor pages call useRouter() directly instead and
mount them as plain route elements, dropping the wrapper.
* GDGT-2919: Make all Calls tab columns sortable
Matches MCP's events table, where every column (including the PK and
PII columns) is server-sortable. call_id/tenant_name/ip_address/
error_message are all plain columns on v_agent_api_calls, so ordering
by them needs no query changes beyond adding them to the sort list.
* GDGT-2919: Project CLI events query to curated PII-aware columns
buildEventsQuery shipped the view's full row (including ip_address and
error_message) to the browser regardless of the tenant/PII retention
settings; the frontend only hid those columns from the rendered table.
Add cliEventColumnKeys as the single source of truth for both the
query projection (Lib.withFields) and the display columns, mirroring
the same fix already applied to MCP analytics.
* Use absolute imports for cross-module paths in ai-auditing
Replace ../ relative imports (mcp-analytics/cli-analytics reaching
into metabot-analytics, and cross-directory imports within
metabot-analytics) with the metabase-enterprise/... absolute alias.
Same-directory ./ imports are left as-is.
* GDGT-2919: Show an error instead of spinning forever in CliEventsTable
useCliEventsQuery didn't surface the query's error, so a failed
per-page fetch left data undefined and the table stuck on the
skeleton indefinitely. Surface the error and render
LoadingAndErrorWrapper instead.
Also drop useRetainedValue: RTK Query's data (unlike currentData)
already stays sticky across page/sort changes until the next result
resolves, so the manual retention was redundant.
* GDGT-2919: Show an error instead of spinning forever in McpEventsTable
Mirrors the CliEventsTable fix: useMcpEventsQuery now surfaces the
query error, and McpEventsTable renders LoadingAndErrorWrapper instead
of leaving the skeleton up indefinitely on a failed fetch. Drops
useRetainedValue there too, for the same reason (RTK Query's data is
already sticky across page/sort changes).
Also trims redundant comments in both event tables.
* GDGT-2919: Address PR comments
* Revert gratuitous DependencyDiagnosticsPageOwnProps rename
Keep master's DependencyDiagnosticsPageProps naming so this Graphy-owned
file isn't touched by the master merge for no functional reason.
* Fix jest.mock("metabase/AppComponent") to match its named export
Master's router refactor changed AppComponent from a default-exported
connect() component to a plain named export (App). The mock still
returned a default export, so import { App } resolved to undefined and
broke every route render in routes.unit.spec.tsx.
* GDGT-2874: Fix usage audit e2e test
The treemap label model imported VisualizationGridSize from the
enterprise custom-viz package, a cross-boundary type import. A
structurally identical type already exists in
metabase/visualizations/types, so use that instead.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Resolve relative navigation against v7's own match
* Resolve redirect targets off the v7 match branch
* Drop the router argument from useDashboardUrlQuery
* Free Palette and McpAnalyticsPage from the router context
* Read location and params from v7 instead of useRouter
* Scope route-leave hooks from the match instead of an injected route
* Delete RouterBridge, the router contexts, and the v3 route types
HoneySQL munges dashes to underscore in a keyword's namespace, but not in its name. The code was relying on HoneySQL default behavior to turn namespaced keywords into table references, but the inconsistency bit me in some new development work.
Two paths currently pass schema-qualified keywords in cases where a table might leak:
- `drop-table!` targeted a nonexistent object and silently "succeeded" (not dropping the table)
- `compile-transform` (the SQL variant) could target a nonexistent database.
Both `drop-table-sql` and `create-table-sql` are updated (the latter via `quote-table`) to keep them in sync.
Collapsing the namespace into the keyword's name keeps everything verbatim as you'd expect. Callers already passing in `"schema.name"` weren't impacted.
* Fix QB perf
* Forbid loading every field via GET /api/database/:id/metadata
Make skip_fields a required `true` on the FE request type so no caller can ask
for field-level metadata, which grows with every column in the database.
The two callers that needed fields now scope their fetch to a single table:
- Data Reference field pages fetch the table's own query metadata (which also
pulls FK target tables and their fields)
- useAuditTable resolves the table from the plain database response, then loads
/api/dataset/query_metadata for that table
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Withhold the audit table until its fields load
ConversationStatsPage renders charts straight from useAuditTable's table
without checking isLoading, so handing back a table built from the field-less
database metadata made it paint once and re-render when the ad-hoc query
metadata arrived, detaching the chart nodes mid-assertion.
Also make the segment field list spec's database mock honour skip_fields, so
unit tests can catch a caller that stops asking for fields.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix be
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* BE Add Mistral metabot provider
* BE Add prompt-cache-key to LLMRequestOpts for Mistral prompt caching support
Mistral requires a `prompt_cache_key` to be set to enable prompt caching, so add it as an optional key to
`LLMRequestOpts` and thread it through `self/call-llm` and `self/call-llm-structured`.
* FE Add Mistral metabot provider
* Review feedback: fold all provider selectable tests into a single test
* Review feedback: use findPutRequests to reduce verbosity
* BE Add Z.AI metabot provider
* FE Add Z.AI metabot provider
* Move generic chat-completions code to metabase.metabot.self.openai.chat-completions
And use it in both the openrouter and zai providers.
hashSearchTerm and shouldReportSearchTerm are analytics reporting
policy (settings read + SHA-256 hashing), not search UI, so they move
from common/search/term.ts to the lib-tier analytics module and are
exported from its barrel. Consumers now import them from
metabase/analytics, and the common/search barrel no longer re-exports
them.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Relocate the isVisualizerDashboardCard type guard from
metabase/visualizer/utils to metabase-types/guards/dashboard, alongside
the other dashboard type guards, and update all importers to the new
path. Remove the re-export from the visualizer utils barrel and delete
the old file.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add parseSearchQuery and a useNavigationType facade hook
* Move the location.action readers to useNavigationType
* Move the location.query readers to search params
* Drop query and action from the facade Location
* Replace connect() with hooks in the app shells and permissions pages
* Rewrite the reference containers as function components
* Delete withRouteProps
* Pass the props reference children read but do not select
* Cover every reference container page with a render test
* Fetch reference container data before the child's first render
* Add Snowplow event tracking to the metric Dimensions tab
* Update events' definitions to comply with new approach
* Update analytics-events claude skill to account for new approach defined in #71631
TableBrowser only needs the table list, but GET /api/database/:id/metadata
returns every field of every table. Pass skip_fields=true so the response
stays proportional to the number of tables.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
HoneySQL inlines true/false as TRUE/FLASE tokens even when the query is otherwise parameterized, and not every driver target has a boolean literal. SQL Server parses a bare TRUE as an identifier and `insert-into!` fails with `Invalid column name 'TRUE'`. Wrapping boolean row values in `[:lift v]` binds them as paremeters, which is what we do for other value types.
Impacts every `:sql-jdbc` driver's `insert-into!`. Drivers with boolean literals are not impacted.
Fix quadratic :is_personal hydration that made GET /api/collection take ~23s
On an instance with 28k collections — 6k of them personal — `GET /api/collection`
took 23.5s while making only 13 DB calls. Nearly all of it was the `:is_personal`
batched hydration: for every collection it scanned the full set of personal
collection IDs, building a prefix string with `format` on each step, so a request
did ~20k x 6k = 125M `String/format` calls.
Personal Collections only ever live in the Root Collection, so a collection is
inside one exactly when the *first* ID of its location path is a personal
collection. Test that single ID against the set instead of scanning.
Measured on a seeded app DB matching the reported shape (28.6k collections, 6,011
personal roots, 13.6k live non-personal, 7.8k archived, depth to 13):
GET /api/collection 11.2s -> 1.9s
:is_personal hydrate 9.2s -> 75ms
The remaining hot spot is `:can_write` (~1.1-1.5s), which linearly scans the
user's permission set per collection — left for a separate change since it
touches the shared permission check.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Convert the store factory and whitelabel color bootstrap to TypeScript
* Name the deliberately loose reducer map type
* Condense the reducer map comment
* BigQuery: reject oversized queries client-side with a humanized error
BigQuery rejects jobs whose SQL exceeds 1 MB (1,048,576 characters,
including comments and whitespace) with a raw 400 INVALID_ARGUMENT,
which Metabase surfaces to the user as "Error executing query: <raw
message>" -- accurate but technical and not actionable.
Check the final compiled SQL (including the appended remark) before the
BigQuery client/job is created and throw a localized, actionable
:invalid-query error instead. The raw DB error path remains as a
fallback if this limit ever drifts from BigQuery's own.
Quota reference: https://cloud.google.com/bigquery/quotas#query_limitsCloses#79019
* Update error message for SQL query length validation
* Address review: express limit as (* 1024 1024), slim the tests
- max-sql-query-length-chars: write the value as (* 1024 1024) so the
1 MiB intent reads off the page.
- validate-query-length-test: collapse to the two boundary cases (at the
limit allowed; over the limit throws :invalid-query), error type only,
per review feedback to keep it simple.
* Address review: trim noise from max-sql-query-length-chars docstring
Drop the cross-reference to validate-query-length! and the "not ^:const"
justification — commentary, not contract. Leaves what the constant is,
how BigQuery counts, the failure mode, and the quota link.
* Add AI provider setup to onboarding
- Insert AI config step into setup flow
- Track connect and skip analytics events
- Update onboarding tests and schema version
* Preserve existing AI provider during setup
- Wait for the saved provider before mounting the form
- Show the connected provider in the setup step and cover it in tests
* Defer AI provider sync until after setup
- Keep the AI config step visible during onboarding
- Skip legacy Metabot provider migration on fresh instances
* Add AI provider setup to onboarding
- Hide the step when AI features are disabled
- Support Anthropic onboarding setup in e2e
- Mock Anthropic model catalog for credential validation
* Match license step layout in AI setup step
* Fix modules config and flaky setup toast assertion
* Add free_units to managed AI e2e add-on fixture
* Condense AI step ordering comment
* Make AI setup opt-in from the completed step
The AI provider step is no longer part of the wizard by default. The
"You're all set up!" screen offers a "Set up AI" button that appends
ai_config as the last step; connecting or skipping returns to it.
* Scope the settings-list mock to the test that needs it
settingOverrides was only used by one embedding test, so drop it from the
shared setup helper and let that test override the settings-list route.
* Type the visualization registry as components or bare definitions
* Convert the dashboard viz register module to TypeScript and use typed named exports for Action and IFrameViz
* Strip large literal IN lists before GraalPy SQL parsing
Large literal IN lists (like large VALUES clauses) blow GraalPy's ~114KB/AST-node
memory cost and its 30s call timeout: a 300k-item list that sqlglot parses in
1.7s on CPython times out at 30s on GraalPy, poisoning a pooled context on every
background dependency-analysis pass over the offending query.
Extend the JVM-side pre-processing to also replace literal-only IN lists with
more than 100 items with `IN (NULL)`. Only lists consisting purely of literals
(numbers, quoted strings, NULL/TRUE/FALSE) are stripped; column references,
function calls, subqueries, and bind parameters disqualify a list, since
stripping those would change analysis results or parameter counts. Lists nested
inside unstripped IN subqueries are still found.
All callers get the same handling: the stripping happens in the one shared
strip-large-literal-lists (renamed from strip-large-values) used by every
analysis entry point, and is-single-stmt-of-type? keeps the #74284 restore
behavior — validate the stripped SQL, return the original text — so
impersonated queries still execute with their literal lists intact.
With the fix, field-references on a 300k-item IN list drops from a 30s timeout
to ~80ms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Simplify literal-list stripping to a single shared pass
Review cleanup: collapse the two per-keyword scanners into one walk over the
SQL driven by a combined VALUES|IN pattern with small per-keyword handlers,
extract the quote/doubled-quote scanning into a shared skip-string-literal
(also fixing count-top-level-commas, which didn't handle doubled-quote
escapes), and replace the substring-allocating literal-only-list? check with a
single index-based early-exit pass, so IN (SELECT ...) is rejected without
scanning or copying the subquery region. No-strip calls now return the
original string identity.
field-references on a 300k-item IN list drops from ~80ms to ~27ms; all
existing stripping tests pass unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add exhaustive tests for literal-list stripping
Cover threshold boundaries (100 vs 101) for both VALUES and IN, every accepted
literal shape, a disqualifier grid (17 non-literal items x 3 positions),
byte-for-byte preservation of surrounding SQL, nesting (IN inside VALUES
tuples, stripped-VALUES swallowing nested lists, deep IN-subquery chains),
idempotency, original-instance identity on the no-strip path, malformed input
(unbalanced parens, unterminated strings, empty lists), keyword false
positives (margin, JOIN, INT, quoted identifiers), and an end-to-end pass of
every analysis entry point over stripped SQL.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Also strip large ARRAY literals and literal tuple IN lists
Two more static-data shapes reproduce the GraalPy 30s timeout / severe CPU
burn: id = ANY(ARRAY[...]) (the Postgres idiom ORMs emit instead of IN; 200k
items hit the 30s timeout) and composite-key tuple lists
(a, b) IN ((1, 1), (2, 2), ...) (50k tuples burned 21s).
Generalize the flat literal-list scanner to a parameterized closing delimiter
and reuse it for ARRAY[...] -> ARRAY[NULL], and add a tuple-list scanner that
requires every tuple to be literal-only, stripping to IN ((NULL, ...)) with
the first tuple's arity. Subscript access (arr[1], arr[1:5]), subquery tuples,
and any non-literal item still disqualify a list. The is-single-stmt-of-type?
restore behavior covers both new shapes.
After the change: tuple-IN 50k tuples 21.6s -> 525ms; ANY(ARRAY[200k])
30s timeout -> 1.1s. CASE WHEN chains and UNION ALL literal tables remain
unstripped (no lossless strip exists for them).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Treat double quotes as a disqualifier in literal-list scanning
In identifier-quoting dialects (Postgres, Snowflake, ...) "x" inside an IN
list or ARRAY literal is a column reference, not a string, so stripping a
large list of double-quoted items would drop real field references from
analysis. The literal-list scan is dialect-agnostic, so be conservative: only
single-quoted strings count as literals; a bare double quote leaves the list
untouched. skip-string-literal still skips both quote styles where it is used
for paren/comma balance scanning, where either interpretation is a
skip-region.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Use [[strip-threshold]] docstring links instead of backticks
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix Eastwood reflection warning: cast close-ch to primitive char
(= ch close-ch) compared a primitive char against the untyped close-ch
parameter, which Eastwood flags as an unresolvable Util.equiv call.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Ignore keyword matches in strings/comments; reject empty list elements
Review follow-ups:
The keyword matcher previously rewrote text inside string literals and
comments — SELECT 'IN (0, ..., 149)' became SELECT 'IN (NULL)', and an
unbalanced quote inside such a region could corrupt the statement. The walker
now gap-scans between matches with a string/comment-aware cursor
(code-region-end) and copies any match that sits inside '...', "...",
-- ..., or /* ... */ through untouched.
The flat-list scanner also accepted empty elements, so invalid SQL like
IN (,,,) with >100 commas was rewritten into valid IN (NULL), masking syntax
errors from validation. Both the flat and tuple scanners now require content
in every element and reject leading/trailing/consecutive commas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Simplify IN/ARRAY stripping to one simple-list scanner
Stripping feeds fail-soft analysis only, and execution paths restore the
original SQL, so the defensive machinery accumulated from review rounds
(literal word allow-list, tuple/arity validation, empty-element checks,
string/comment-aware outer scanning) bought correctness for cases that don't
matter at the cost of real complexity. Replace it all with one rule: a list is
strippable when, from its opening ( or [, it contains only digits, signs,
single-quoted strings, commas, whitespace, and balanced parens (which covers
tuple lists with no special logic), and holds at least 100 commas. Flat IN,
tuple IN, and ARRAY all collapse into a single ~20-line scanner and one
decision fn; NULL/TRUE/FALSE items now conservatively block stripping. VALUES
keeps its pre-existing count-based path. The best-effort contract is stated in
the public docstring.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Seven sites across four drivers were escaping SQL string literals the same way. This consolidates, and there's a single place to warn not to use escape-sql to sanitize user input.
No behavior changes.
MBQL treats `java.math.BigInteger` and `clojure.lang.BigInt` interchangeably. Only the Clojure class was in the parameter table, so a BigInteger fell through in one of my tests to `:default` and bound as a string.
* Show the output schema and table for a transform run
Adds a TransformOutput component that renders the run's target as `schema › table`
breadcrumbs with a copy button. Both crumbs link out when they exist: the schema to
the data model, the table to the query builder. Wired into the run sidebar
("Output table" row) and the job run sidebar ("Output:" line).
Also fixes the job run sidebar not shrinking below its content width (missing
min-width: 0), which let long table names push it past the viewport.
* fix tests
* drop newTab prop, always open output links in new tab
* addressing PR's comments
* fix race condition
The three preceding PRs removed every shared-tier import from the module: the
route guards, the redux location mirror, and the styled-Link re-export. All that
is left is metabase/utils, which basic may import.
basic rather than lib: router is a React component library, the same shape as
metabase/ui, and nothing in basic/* imports it, so lib would only widen what
lib/utils is allowed to reach for no gain.
* fix(sdk): sdk crashes when resizing column
* do the same on ExecutionOutputTable too
* reword test to make it better
* make sure cols/rows are not new arrays every render when output is undefined
* Derive Tabs value type from the tab values
Mantine types the Tabs value and onChange argument as `string | null`. Make
TabsProps generic over the tab value so callers get their own union back in
onChange instead of a bare string.
* Drop Tabs onChange type workarounds at call sites
Every `<Tabs>` call site now annotates its own tab union instead of casting or
re-narrowing the change value: the six `as SomeTab` casts (CommentsSidesheet,
PythonEditorResults, GroupLimitsSettingsSection, McpAnalyticsPage,
CliAnalyticsPage, ConversationStatsPage), the four in the admin/permissions and
tenants listings, and the handlers that laundered `string | null` through a
lookup or a literal comparison (AddDataModal, NotificationsTabs, TableSection,
the date pickers, TabPanel). AddDataModal's `isValidTab` guard existed only for
this and is gone; DatasetFieldMetadataSidebar's TAB map is now `as const`.
* Cleanup
* Apply suggestions from code review
Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
---------
Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
* Mark metabase/router as side-effect free for rspack
* Route deep metabase/router imports through the barrel
* Let static-viz import the router barrel instead of a deep module
* Guard the side-effect-free contract and scope the include to the directory
metabase/router/Link.ts re-exported metabase/common/components/Link so every
routing import had one home. But that Link is not a routing primitive: it wraps
RouterLink with a Tooltip from metabase/ui, a CSS module, and disabled/variant
props, so the barrel dragged shared/common and basic/ui into the router module
and looped back into router/router-link.
Call sites import it from metabase/common/components/Link again. RouterLink and
RouterLinkProps stay in metabase/router, which is where the primitive belongs.
The boundaries linter never flagged this: element-types checks imports, not
'export ... from'. So the edge had to go rather than be waived, or a lower tier
would have been green while the dependency was still there.
* Fix N+1 query patterns in GET /api/database and GET /api/native-query-snippet
GET /api/database?saved=true&include=tables issued ~6-7 app-DB queries per
saved question (one metabase_database select-one plus a field select and
4-5 hydrations each) — ~3000 queries on instances with a few hundred
eligible cards:
- card->virtual-table: only fetch the database when include-database? is
set, instead of unconditionally per card
- extract cards->card-id->metadata-fields so the result_metadata fields of
all cards are fetched and hydrated in one batch (reused by
batch-fetch-card-query-metadatas), and pass it from cards-virtual-tables
- ids-of-dbs-that-support-source-queries: one select instead of
select-pks-set + per-id select-one
GET /api/native-query-snippet ran user-has-any-perms-of-type? — an
uncached EXISTS against data_permissions — once per snippet via
mi/can-read?, even though the answer is per-user, not per-snippet:
- user-has-any-perms-of-type? now primes the request-scoped permissions
cache for all databases (the same pattern query-creation-capabilities
already uses on GET /api/user/current) and answers by scanning the
cached entries; repeated calls in one request are pure cache hits
- prime-all-dbs-cache records completeness with an ::all-dbs sentinel in
the existing :db-ids set so the database id list is fetched only once
- the uncached EXISTS path is kept for cross-user checks and
disable-perms-cache
Note: the cache prime excludes permission rows for inactive tables, so a
grant existing only on a deactivated table no longer satisfies
user-has-any-perms-of-type?.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Track full-prime state under a dedicated :all-dbs-primed? cache key
Move the all-databases-primed marker out of the :db-ids set (where it sat
as a ::all-dbs sentinel among real database IDs) into its own
:all-dbs-primed? key on *permissions-for-user*. prime-db-cache now
preserves unrelated keys by assoc-ing instead of reset!-ing the atom.
Make prime-all-dbs-cache public with an optional pre-fetched-ids arity,
and switch query-creation-capabilities — the one caller that manually
primed all databases — to use it, so the full-prime work is shared with
user-has-any-perms-of-type? within a request.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Simplify prime-all-dbs-cache: single arity, :all-dbs? key
Drop the pre-fetched-ids arity — the function always fetches the database
ID list itself — and rename the completeness marker from :all-dbs-primed?
to :all-dbs?.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Align :all-dbs? docstring continuation line
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add call-count regression tests for the two fixed N+1 endpoints
Both tests measure warm app-DB call counts via t2/with-call-count at two
entity counts and assert the count doesn't scale:
- GET /api/database?saved=true&include=tables with 2 vs 18 eligible cards;
allows the 1-per-card toucan2 identity-query overhead from
do-after-select but fails on the pre-batching one-select-per-card
behavior (verified failing against the pre-fix code).
- GET /api/native-query-snippet with 2 vs 10 snippets as a non-admin;
fails on the per-snippet permission EXISTS check (verified failing
against the pre-fix code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Use mt/with-model-cleanup in call-count tests
Replaces the try/finally + manual delete pattern, so fixture rows are
cleaned up even when insertion or the warm-up request throws (addresses
review feedback).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rework any-perms-of-type caching to a dedicated request-scoped cache
Instead of priming the full permissions cache for every database and
scanning it, cache per [user-id perm-type] the set of database IDs
granting the most permissive value, loaded across all databases in a
single query. This answers every :exclude-db-ids variant from one small
fetch and keeps semantics identical to the uncached query.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* perms cache
* nil fix
* fix
* current state
* cleanup
* Test that inactive-table-only grants are ignored by aggregate perm checks
user-has-any-perms-of-type? previously counted grants existing only on
inactive tables (its raw EXISTS query had no active-table filter); now
that it is served by the distinct-perms cache it ignores them, matching
every other cached check. This is intentional — cover it with a test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Apply code-review fixes to the permissions cache rework
- Restore batch priming in with-can-run-adhoc-query via a new public
prime-full-perms-cache (table-level checks otherwise load the cache
one database at a time, up to 2 queries per database on card lists)
- Scope the non-cached distinct-perms load to one database instead of
scanning all of the user's permission rows per call
- Extract the shared FROM/JOIN/WHERE of the two cache queries into
perm-rows-query-base so their row sets cannot drift apart
- Simplify the full-perms interner binding and drop a dead fnil and a
redundant double-check inside the distinct-perms swap!
- Select only :id and :engine in ids-of-dbs-that-support-source-queries
(full rows decrypt :details per database, twice per request)
- Own the batched virtual-table Field fetch in a new
cards->virtual-tables entry point; the helper is private again
- Use mt/with-dynamic-fn-redefs in permission-caches-load-per-type-test
- Document that the request caches are point-in-time snapshots
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Treat cached false as a hit in fast-interner
or-based lookups skip false, so interning false re-ran canon-fn and
putIfAbsent on every call (result was still correct). Use if-some so
presence, not truthiness, decides the hit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Document why distinct perms load all databases and why the uncached interner exists
Capture the review-thread answers in the docstrings: distinct-perms
preloads every database because most consumers ask cross-database
questions, and full-perms builds a per-call interner even without a
request cache because group values are highly repetitive.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Batch-prime the full perms cache in the remaining per-table check loops
Endpoint tracing found three more loops that fill the full permissions
cache one database at a time now that priming is on demand: the
include=tables branch of GET /api/database (per-table can-read?/
can-query?/can-write?), batch-fetch-query-metadatas* (per-table
can-read? behind dashboard/card query_metadata), and the EE
filter-tables-by-data-model-perms loop. Each now batch-primes the perm
types it checks, one query per type instead of up to 3 per database.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Simplify perms caches to all-types loading; rename to table/schema caches
Per-type bookkeeping wasn't worth it — nearly every consumer needs
several permission types, and there are only a handful. Both caches now
load every permission type at once: the table-level cache (was
*full-perms-cache*) per database, the schema-level cache (was
*distinct-perms-cache*, table_id collapsed so schema is the finest
granularity it answers) for all databases in a single query on first
use. prime-table-perms-cache takes just database IDs and no-ops for
superusers, whose checks never read the cache.
Also batch-prime the remaining per-table check loops found by a
repo-wide audit: search result filtering, GET /api/table, recent views
(also reusing the already-fetched table instead of re-selecting it),
typed-schemas table listings, and the segment/measure list endpoints.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop the search prime and scope the recents prime to actual table recents
Benchmarks on a 10M-row data_permissions Postgres (200k rows for the
test user) showed both primes regressing their endpoints: search went
40ms to 840ms because priming all databases loads every permission row
of the user on each request while matching results only touch a few
databases, and recents primed the whole instance because table-recents
returns every table when given no IDs. Search now relies on lazy per-db
loads; table-recents returns nothing for empty IDs so the prime no-ops
naturally.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Load the schema perms cache per database, with a priming function
Benchmarks showed the all-databases SELECT DISTINCT costing ~230ms per
request at 200k user rows, taxing endpoints that ask about one database
(dashcard query 228ms -> 445ms). The schema cache now mirrors the table
cache: single-database questions load one database at a time (indexed,
all permission types at once), loops over many databases call
prime-schema-perms-cache, and cross-database questions load everything
once, recorded under :all-dbs? so repeated checks stay one query.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Load perms caches via next.jdbc/plan instead of toucan2 rows
JFR allocation profiling showed toucan2's per-row machinery (row delays,
transient instances, realize maps) dominating the cache loads' share of
request allocation. plan reads columns straight off the ResultSet with
no per-row maps; the folds are unchanged. Runs on the current toucan2
connection so transactional writes stay visible. On the 10M-row bench
this cuts per-request allocation ~10-15% further (e.g. /api/user/current
570MB on master -> 376MB) and 50-90ms of latency on load-heavy
endpoints. Note plan queries are invisible to t2/with-call-count, so
one test now spies on the load fn instead of counting calls.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Store perms caches as mutable Java collections, skipping persistent copies
The folds now build nested HashMaps/HashSets that the caches store
as-is: no per-row persistent update-in churn and no whole-structure
persistent copy at the end. Clojure's lookup/seq functions read Java
collections directly, so consumers are unchanged. Only the two outer
levels (perm-type, db-id) are ConcurrentHashMaps that later loads extend
via putAll of missing databases; everything below is written once by its
load and then only read. Numeric keys are canonicalized to Long at both
fold and lookup time — JDBC hands back Integer for int columns and Java
maps compare boxed keys with equals, not Clojure's equiv. The entry
interner is gone with the persistent copy; instances with thousands of
identical per-database profiles lose that sharing (#76077), which is the
trade-off for the allocation win.
On the 10M-row Postgres bench (200k rows for the user) vs master:
/api/user/current 570 -> 268 MB allocated per request, GET /api/database
?saved=true&include=tables 1773 -> 1032 MB and 4.1s -> 2.5s,
GET /api/table 1152 -> 865 MB and 1.9s -> 0.9s.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Answer cross-database permission questions from a light per-function cache
Instances with thousands of databases and groups make loading tuples
for every database (schema cache with :all-dbs?) scale badly: the
distinct tuple set alone reaches hundreds of thousands of rows. The
questions that actually span databases only need one value per database
for one permission type, so most-permissive-database-permission-for-user
(signature unchanged) and user-has-any-perms-of-type? now share
*most-permissive-perms-cache*: {user-id {perm-type {db-id value}}},
loaded by one indexed query returning distinct (database, value) pairs
and coalesced per database. The :all-dbs? machinery is gone and
query-creation-capabilities needs no priming — its per-database loop
hits the cache after one query.
On the 10M-row bench this takes GET /api/user/current from 722ms/268MB
allocated to 556ms/182MB — 5MB above the response-validation floor
measured with all permission checks short-circuited (master: 833ms/570MB).
Also reorganize the caching section (atoms, then loaders, then the
functions using them; coalesce moves above since a loader needs it) and
relax the xrays call-count expectation: permission-cache loads go
through next.jdbc and are invisible to t2/with-call-count.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Coalesce permissions in SQL and give every check its own per-function cache
Each permission-checking function now owns one atom, one hand-written
aggregate query, and a cache structure fitting exactly its question,
organized per function. Values map to ranks (value-rank-case) and
GROUP BY collapses tables, duplicate values and — via two nested levels
of aggregation for the most-restrictive-within-group-then-most-
permissive-across-groups questions — group identity too, so result sets
scale with tables/schemas/databases rather than raw row counts
(dbs x groups x tables x values). A bucket's [min max] rank pair
reconstructs coalesce exactly, verified exhaustively per permission
type including :perms/view-data's special rule; full-schema keeps its
per-group db-level union by expanding db-level rows into every schema
inside its query. native-download delegates to full-db-permission
(identical semantics), user-has-any derives from the most-permissive
cache.
With the row-count multiplier gone the loads are back to plain t2/query
and persistent Clojure collections: the next.jdbc/plan machinery, the
mutable Java collections, the Long key canonicalization, and the
interner are all gone, and cache loads are visible to
t2/with-call-count again. No public API changes; the batch primer is
now prime-db-perms-cache and warms every per-database cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop a docstring reference to the removed full-perm-rows
The ~15x realize-before-key-access observation stands on its own; the
function it pointed at no longer exists now that the permission caches
load SQL-side aggregates.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rewrite app-db NOT IN (SELECT ...) queries as NOT EXISTS anti-joins
NOT IN (subquery) degrades to a per-row subplan on Postgres once the
subquery result exceeds work_mem. Rewrites all runtime occurrences to
correlated NOT EXISTS (or a literal comparison where the subquery was
0/1 rows). MySQL keeps the double-wrapped subquery in
delete-database-fields! since it cannot reference the DELETE target
table in a subquery.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Keep is_audit predicate as NOT EXISTS in set-default-database-permissions!
Tests create audit DBs with random ids, so the fixed audit-db-id
constant is not equivalent to the is_audit = true subquery here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Snowflake: don't over-escape schema names in the dynamic-table probe
The FK sync path pre-escaped schema/table names (`_` -> `\_`) before handing
them to `dynamic-table?`, which uses the schema as an identifier in
`IN SCHEMA <db>.<schema>`. `"RAW\_DATA"` is not a real schema, so Snowflake
threw for every table in any underscore-named schema and we logged a full
stack trace each time.
`dynamic-table?` now takes raw names and escapes only the `LIKE` argument,
where `_` is actually a wildcard. Escaping for the JDBC metadata call moved
to the call site. Also log the caught exception without the stack trace,
since it fires once per table.
Fixes#78541
Co-authored-by: da <paoliniluis@gmail.com>
* Generate a fresh title for a forked conversation from its first new message
* Show "Forked conversation" instead of "New conversation" in fork header
* Rename first-valid-user-message to first-non-forked-user-message
* Align map value columns in metabot conversation schemas (PR review nits)
* feat: fork a Metabot conversation at an assistant message
* feat: surface Metabot conversation fork origin in analytics
Record forked_from_conversation_id on the conversation and
forked_from_message_id on each copied message, expose them through the
metabot-analytics API and audit views, and link a fork back to its
original thread.
* Update git_branch icon
* Show source conversation title in fork boundary and header
* Revert fork boundary to the attempt where the fork happened
Showing the fork boundary divider on every regenerated attempt was
misleading: the fork only happened on the inherited attempt, so the
divider matches that single message again.
* Refine fork boundary rendering and tests
* Exclude forked (copied) messages from v_metabot_messages
* Address PR review: unify message-action guard, extract ForkBoundary, assert fork toasts via UI
* Reuse createMockMetabotConversationDetail for the fork test fixture
* Replace fork props on Messages with a renderAfterMessage slot; move ForkBoundary to EE
* Drop the redundant UndoListing now that the metabot test setup mounts it
* Index and FK-constrain forked_from_conversation_id so deleting an origin nulls the link
* Split forked messages out of usage metrics with new_message_count
v_metabot_conversations gains new_message_count, which leaves out the
messages a fork copied in from its origin. message_count keeps counting
the whole thread, so the conversations list, the detail page and the
shipped model all report what the reader can actually see.
The usage stats charts aggregate the new column instead, which makes
SUM(new_message_count) reconcile with COUNT(*) FROM v_metabot_messages.
metabot-stats leaves copies out of :metabot-queries and :metabot-users
for the same reason — copies inherit role/ai_proxied and are stamped
with a fresh created_at, so a fork was reporting LLM calls that never
happened.
* Don't offer to fork while a turn is streaming
Only the streaming message's actions were hidden, so earlier assistant
messages kept their fork button mid-turn. Forking then swapped the
conversation out from under an in-flight stream whose reducers are keyed
by agentId alone, so the old turn's remaining deltas rendered inside the
fork.
Withholding onFork while isDoingScience is set is preferable to
cancelling the request: the user asked to branch off an earlier message,
not to stop the response, and loadConversation already avoids cancelling
so an aborted turn isn't recorded.
* Realign the analytics schema maps and fix a migration author
The fork columns are longer than the alignment column the surrounding
maps used. ConversationDetail had already drifted into two alignment
columns before this branch; both maps are now uniform.
Extends the dynamic JSX formatter pattern from #77658 (which did links)
to markdown and email. formatValue and formatEmail now call a registered
renderer through the registry instead of importing ReactMarkdown/Mustache
and ExternalLink directly; the renderers live in ui.tsx and are wired up
at app boot by registerJsxFormatting (called from registerVisualizations).
This keeps the react-markdown/micromark/mustache stack and the mailto
ExternalLink out of the static-viz bundle, which imports the formatting
engine through shared echarts code but never registers the JSX renderers
(so jsx formatting degrades to plain text there, as it already did for
links). No behaviour change in the app.
Static-viz bundle: 3,450,947 -> 3,297,087 bytes (~150KB, 4.5%).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
metabase.metrics.core re-exports sandbox-restricted-fields from metabase.metrics.permissions, and metabase.metabot.tools.entity-details calls it through that API. The passthrough to metabase.permissions.metric was missing, so importing the re-export threw at load and broke every namespace downstream of metabase.metrics.core.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Transform failure emails list root-cause failures by name and summarize dependents that never ran as a count, so the test asserts the tally instead of the dependent's name.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Add Metric Dimensions section
* [metrics] Field IDs are not unique! Multiple FKs to same table.
Fixes a few places where the field ID alone is assumed to be a unique
key for dimension mappings.
* [metrics] Add basic CRUD API for metric v2 dimensions
Fixes UXW-4631.
Fixes UXW-4633.
* [metrics] Add `/api/metric/$ID/dimension/set-default` endpoint
Fixes UXW-4632.
This sets the dimension of a Metric v2 with the specified
`dimension_id` (a body parameter) to be the `:default true`.
All other dimensions have `:default` removed, so there is at
most one default dimension for any metric.
* [metrics] Metric dimensions create dependencies on the tables
Fixes UXW-4636.
* [metrics] Serdes support for curated Metrics v2 dimensions
Fixes UXW-4634.
Previously `:dimensions` and `:dimension_mappings` were marked as
`:skip` on serdes since the dimensions were always synced on read.
Now that they're curated and only synced when blank, we need to
properly serialize dimensions and mappings.
* Adjust Metric dimensions UI + connect to api
* Add dimensions reorder with dnd
* Add metric dimensions usage in metric viewer
* Change Metric Viewer to group dimensions by type
* Implement shared dimensions logic, add e2e tests
* Use default dimension in Metric Viewer
* Align headings in panels / Make trash and add buttons the same height
* Move and simplify the Set as default button
* Make about page use the default curated dimension in the preview chart
* [dimensions] Bump `:card_schema` to 24; backfill `:dimensions` in old style
Fixes UXW-4808.
This gives pre-existing v2 Metrics the full `breakoutable-columns` as
their dimensions, rather than just the main group as is now the
default. That preserves the old behaviour for the dimensions of the
metrics.
* Respect default metric dimensions on dashboards / Add projectDimension lib function (#77910)
* Respect default metric dimensions on dashboards
* Consolidate metric dashboard E2E coverage
* Remove default time dimension from metric editor
* Implement a few design tweaks in Dimensions tab UI
* Use curated metric dimensions on dashboard metric card filters
* Add dimension picker to metric About page
* Auto-pick default dimensions for new metrics
* Additional cleanup to the About page dimension select element
* Expand all metrics in See all dimensions
* Remove table names from metric filters
* Don't show sub-headers in 'See all' dimension view in Metric Viewer
* Remove metric overview tab
* [UXW-4859] Handle empty curated dimension list and removed default dimension edge cases
* Fix default viz type when adding metric to dashboard
* Remove the 'See all' option on explorer page for single metric visualization
* [UXW-4863] Don't use custom display name as field reference in dimension detail column
* [backend] Fix persistence of upgraded metric dimensions
* [UXW-4880] Re-add Overview tab, show curated dimensions
* [UXW-4848] Make sure we show all shared dimensions in the Shared Dimensions view, if the underlying fields match
* [UXW-4771] Make pinned metric in collection respect default curated dimension
* Update e2e tests to account for PR updates [part 1]
* Seed explicitly joined metric dimensions
* Fix metric dimension serialization dependencies
* Fix/adjust backend unit tests
* Fix metric dimension lint issues
* Run metrics with saved card permissions
* Update e2e tests to account for PR updates [part 2]
* Update kondo ratchet for metric serialization test
* Update metric permissions e2e expectation [part 3]
* Fix BE sandbox unit test
* Prefix upgraded metric dimensions with connected table names (#78410)
[UXW-4896] Prefix upgraded metric dimension names
* Address FE issues from July 22 reviews
* use curated dimensions on series breakout list / fix dimension display name on breakout legend and dimension pill bar
* Optimize getMetricMappingOptions
* Adjust about page dimension select margin-bottom and dropdown positining
* Fix curated metric dimension selection across duplicate foreign key paths
* Improve logic to show correct dimension label in the CenterControls button
* Exclude orphaned dimensions from runtime metric metadata
* Address Ethan's comments / Clean up
* Use metadata field cache when checking user permission / Improve field validation when adding dimension
* Address latest reviews
* Update serialization baseline card_schema 23 -> 24
* Tighten docstrings and comments per review feedback
Remove caller references, ticket refs, and usage context from docstrings
and comments; describe contracts instead.
---------
Co-authored-by: Braden Shepherdson <braden@metabase.com>
Co-authored-by: Anderson <anderson@metabase.com>
Co-authored-by: Anderson <ands.alves.nunes@gmail.com>
* Stabilize SSO group-mapping delete flake by re-reading settled state
Selecting groups for a mapping optimistically patches the settings cache,
but the setting update also invalidates and refetches session properties.
A trailing stale refetch can revert the last-added group after the test's
per-selection checkbox assertion has already passed, leaving the mapping
with only one non-admin group. The delete then fires a single deleteGroup
request and cy.wait times out waiting for the second.
Re-load the auth settings page between adding groups and deleting the
mapping, and assert the row shows its full group summary before acting.
The fresh page load aborts in-flight refetches and reads a single settled
server state, so every mapped group is present when the delete fires.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Serialize SSO group-mapping edits against their setting round-trips
Adding a group to a mapping PUTs the group-mappings setting (built from the
widget's current mappings cache) and invalidates session properties, which
triggers a background refetch. The widget optimistically patches the cache,
but a trailing stale refetch can revert the just-added group; the next
selection then builds its PUT from that stale set and overwrites the previous
group server-side (last-write-wins). The mapping ends up with fewer groups
than the test expects, so fewer deleteGroup requests fire and cy.wait times
out waiting for the second one.
Wait for each mapping mutation's PUT and its trailing session-properties
refetch to settle before the next interaction, in createMapping,
addGroupsToMapping (per group), and deleteMappingWithGroups, so every edit is
built from a fully committed round-trip. deleteGroup/clearGroup invalidate
only permissions-group, so updateSetting is the sole source of session
refetches and the waits stay aligned one-to-one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* update getExtraTagsForVersion to append -lts if isLts is set
* add ability to check if a version is an LTS version
* set islts
* update function to determine if we should tag with lts
* add condition for multiple LTS releases so we only tag the latest
* fix actionlint errors
* update lts tag to include major version
* await result of getExtraTagsForVersion
* fix type
* fix merge conflict, don't throw error
* GDGT-2851 Remove color-mix from theme color definitions
color-mix can only be resolved by a browser, so any theme color using it
breaks static viz, which renders on the server (see UXW-4690, where the
gauge failed on background_page-tertiary-inverse).
Per design's proposal, introduce a new orion-95 stop and use solid scale
values for the page backgrounds:
- dark: page primary/secondary/tertiary -> orion 95/100/110
- light: page primary/secondary/tertiary inverse -> orion 95/100/110
The remaining mixes are collapsed to their literal equivalents:
shadow-default to a plain rgba, and tooltip-background-focused to the
next stop down from tooltip-background.
The mixes that derive from whitelabel colors (core-brand, core-filter,
core-summarize) are left alone, since their values aren't known ahead of
time. A unit test now guards against reintroducing color-mix anywhere
else in the theme definitions.
* Update Loki Snapshots
* Remove var() references and non-hsla literals from theme definitions
Static viz feeds theme colors straight into Color(), which only parses
literals, so a var() throws there exactly like a color-mix does — even
when it points at a perfectly ordinary color. Collapse the remaining
var() references to the underlying base color, and normalize the few
hex and bare-keyword values to hsla so every theme color is a literal.
The guard test drops the transitive resolver it no longer needs and just
asserts that every non-whitelabel color is an hsla literal.
* Fix regression in pivot table
* Empty commit to trigger CI
* Manually update pivot table loki snapshot
* Update e2e colour assertions for the new background tokens
Dark background_page-primary moved from orion-100 to the new orion-95,
so anything asserting rgb(7, 23, 34) for it now expects rgb(12, 28, 39).
The dark mode body assertion was matching color(srgb 0.0204 0.06792
0.0996) — Chrome's computed form for the old color-mix on
background_page-secondary. That token is a plain hsla now, so Chrome
reports it as rgb(7, 23, 34).
* Fix e2e
* Fix e2e
* Revert changes in assertDarkMode
* Fix oss e2e spec
* Revert SDK theming assertions to the deprecated background token
The SDK theme presets resolve colours through the first entry of
SDK_TO_MAIN_APP_COLORS_MAPPING, and for `background` that is the
deprecated `background-primary`, which still points at orion-100.
`background_page-primary` sits at index 1 and never gets read, so
table-root is unaffected by the move to orion-95.
* Address review feedback on the colour theme changes
Move the gold and upsell-gem literals into base-colors, since each was
repeated across both theme files. Drop the intermediate staticColors
binding in the guard test in favour of a second filter, and drop the
`?? {}` on theme.colors, which is a required field.
---------
Co-authored-by: Metabase Automation <github-automation@metabase.com>
* Fix click-behavior multi-stage e2e flake: settle mapping list between selections
The "navigating to questions with filters applied in every stage" test sets 11
click-target mappings sequentially, using index-based selectors (.first(),
.last(), .eq(2)) against the unset-click-mappings list. That list re-renders as
each mapping moves from the unset to the set section, but there was no barrier
between steps, so an index-based lookup could resolve mid-re-render and land on
the wrong target — yielding a stable-but-wrong filter set (a duplicated
"Product → Title" filter replacing the dropped "Reviews - Product → Reviewer"
one, which is exactly the assertion that fails).
Extract source selection into selectClickMappingSource(), which asserts the
source picker popover has closed before returning, so every subsequent
index-based lookup runs against a settled list.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix click-mapping settle barrier to assert popover closed correctly
The previous settle barrier used `H.popover().should("not.exist")`, but the
`H.popover()` helper internally chains `.filter(":visible").should("be.visible")`,
so the assertion is self-contradictory: it requires a visible popover to exist
and then asserts it does not, failing deterministically the moment the source
selection closes the popover.
Assert the popover is gone with `cy.get(H.POPOVER_ELEMENT).should("not.exist")`
— the idiomatic pattern used across the e2e suite — so the mapping list still
settles between index-based selections without the contradictory visibility check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Scope click-mapping settle barrier to the visible popover
The unset-click-mappings list re-renders as each mapping moves unset->set.
A bare `cy.get(POPOVER_ELEMENT).should("not.exist")` can never settle in the
click-behavior sidebar: it keeps ~60 mantine popover/combobox dropdowns
mounted-but-hidden, so they are continuously found in the DOM. Filter to
`:visible` so the barrier only waits for the open source picker to close.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Export queryToSearch and searchToQuery from metabase/router
* Serialize the query with queryToSearch at the navigation call sites
* Read search, not query, off a navigation descriptor
* Drop query from LocationDescriptorObject
* Thread QB popState off the routing slice
* Thread dashboard edit-mode off the routing slice
* Thread Metabot profile selection off the routing slice
* Thread document Metabot context off the routing slice
* Migrate component location readers to useLocation
* Retire the state.routing slice
* Convert getExpandedCollectionsById to TypeScript and fix the stale ExpandedCollection type
* Type fake collection ids as literals instead of casting to CollectionId, dropping the String() workaround
* Audit log statements: no stack traces, no queries/user input, no pretty-printing
- Replace throwable args to log macros with (ex-message e) so ex-data and
stack traces never reach the logs (~450 sites)
- Remove SQL/MBQL queries, prepared-statement params, HTTP request/response
bodies, PII, and user-authored names from log output (IDs kept)
- Replace u/pprint-to-str in log calls with pr-str, or drop the value
- Silence sqlglot parser warnings in the GraalPy bootstrap (they quote raw
chunks of customer SQL to stderr)
- Rewrite leftover functional colors (hsl/hsla/rgb/rgba) to hex in static-viz
SVG output so Batik never errors (and stack-dumps) on them
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Use logf variants for exception messages; restore serdes --full-stacktrace
- Convert print-style `(log/warn "msg:" (ex-message e))` calls to
`(log/warnf "msg: %s" (ex-message e))`
- Serdes import/export logs the full stack trace again when the
--full-stacktrace CLI option / full_stacktrace API param is passed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update swap-clauses tests for clause-free warning messages
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update log-content test assertions for the new logging policy
Log calls no longer attach throwables (entries have :e nil) and append
ex-message text to the message instead; body/secret leak tests now assert
the payload never appears in log output.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review comments
- rethrow-api-error!: update docstrings — response body is never logged,
it travels on the thrown ex-data
- expand-macros: log the segment filter-clause count once instead of an
identical trace line per clause
- settings cache: log the whole SQLException message chain (the old
print-sql-exception-chain walked .getNextException; ex-message alone
dropped the chained causes)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add Claude Opus 5 to the anthropic openrouter and bedrock whitelists
- anthropic claude-opus-5
- openrouter anthropic/claude-opus-5
- bedrock anthropic.claude-opus-5
* Scope the invite group picker to the groups with access to the shared item
* Imply All Users membership only for invites with a target, and mock the scoped groups endpoint in share menu specs
* Show all groups when inviting to view and mark which ones can access the item
* Make the invite picker's group section labels visible and tidy up UserForm's membership mapping
* Generalize the group picker into ordered labeled sections with group a11y roles, keeping invite-target semantics in UserForm
* Use the compact Alert size for the invite picker's no-access warning
Suppress api-key-missing exception when failing to generate sample question.
Not having an API key is the default situation and should not result
in an error being logged.
* Let library retrieval use pgvector on the app db
The tables were bare-named, so the drop-and-rebuild in ensure-tables! would have
resolved against the application database's default schema. That is what kept
the feature dedicated-only.
Table names now come from the pgvector mode, the way semantic search picks
between its two index-metadata configs: bare for a dedicated store, so no
existing index moves, and qualified by a library_retrieval schema when sharing
the app db. Its own schema, not semantic search's, whose migration drops every
table in it.
Raw SQL interpolating a qualified name into "%s" would read it as one
identifier, so those sites go through quote-table.
The sync task's scheduling gate stays boot-safe: a dedicated URL, or the license
plus a Postgres app db, decided without resolving the mode. available? checks
the license before the store for the same reason.
* Cover the app-db round trip end to end
Opt-in like the semantic-search one, and picked up by the same appdb-mode CI
job. Asserts the tables land in library_retrieval, the application schema is
untouched, retrieval returns the indexed entity, and a semantic-search wipe
leaves the library index alone -- the reason the schema is separate.
* Check our own schema before reporting the store ready
Semantic search's predicate answers for semantic_search. A role that can use
that schema but cannot create library_retrieval passed it, and then failed every
reconcile inside ensure-tables!. Probe for our schema too: present, or creatable
in a rolled-back transaction, cached because available? runs on every write
nudge and search.
* Gate the round trip on the capability the feature checks
Gating on semantic search's check let the harness pass where a role that can use
semantic_search but not create library_retrieval fails in production.
* Check schema privileges, not schema existence
A role with USAGE but not CREATE passed the existence check and then failed
creating tables. Ask Postgres for both privileges instead, and only fall back to
the rolled-back CREATE SCHEMA when the schema isn't there at all.
The whole probe is inside the try now: the existence query wasn't, so a
transient database failure could throw out of request-entity-sync!, which the
write hooks call fire-and-forget.
* Put the dedicated index in the schema too
A dedicated store kept the index in the default schema, which is exactly what
semantic search's migration wipes -- so enabling or migrating semantic search
dropped the library index and silently re-embedded the whole library. It fires
whenever the stored schema version trails the code, not just on first init.
Both modes now use the library_retrieval schema, so default-tables and
app-db-tables collapse into one. An index built before the schema existed is
moved into it with ALTER TABLE ... SET SCHEMA, keeping its rows; the move runs
before the IF NOT EXISTS creates, which would otherwise leave an empty table
beside it. Creating the schema now needs CREATE on the dedicated database, so a
failure says so.
* Drop only semantic search's own tables in a dedicated store
A dedicated store's default schema is shared -- library retrieval keeps its
index there until it moves -- so wiping every table in it could drop that index
before entity retrieval had a chance to adopt it. Both run as background jobs,
so nothing ordered them.
Scoped to the index_/dlq_/repair_ prefixes, and only in dedicated mode: the
module's own schema in app-db mode holds nothing else, so a reset there is still
free to clear leftovers. The app-db sentinel refusal still reads every table in
scope, since an app db is recognized by tables we would never drop.
* Match the names semantic search generates, not their prefixes
A cohabitant in a shared dedicated store is free to call something
index_history, which the prefix check would have dropped. Match the control
tables exactly, index tables through index-table-name? -- the same contract
orphan cleanup uses -- and DLQ and repair tables by their generated shapes.
Also covers the adoption path end to end: a legacy unqualified index with a row
in it, moved by ensure-tables! and still holding the row afterwards. Asserted
there rather than after the reconcile, which GCs documents the library no longer
implies.
* Stop the availability tests depending on the app db
available?-gates-test left the real schema probe live while stubbing only
the pgvector mode. The probe is Postgres-only SQL, so it threw and read as
unusable on the H2 and MySQL runs, and the case failed there.
The round trip's legacy table declares a vector(4) column before
ensure-tables! runs, and the support check verifies CREATE EXTENSION in a
transaction it rolls back, so nothing has installed the type by then.
* Answer review on the dedicated-store reset
Rename ours? to semantic-search-table?, and point the two docstring
references at namespaces the reader can resolve.
Cover the case where a legacy bare table and a qualified one both exist:
adoption has to leave them alone, since SET SCHEMA onto an occupied name
errors and would abort the whole ensure-tables! transaction.
* Latch the app-db schema check once it passes
The check catches everything and returns false, so on a plain TTL a single
transient app-db failure took retrieval offline for five minutes: available?
went false, search returned nothing and the write nudge no-oped.
A confirmed yes now sticks for the life of the process. Only a no keeps
re-checking, so a privilege granted while we run is still picked up.
* Stub the evals trigger
* use ref arg
* dummy to trigger build
* Clean up
* Use appropriate token
* Simplify pr message
* Use .evals_ref for default ref
* Comment cleanup
* Adjust ref
* Use evals branch
* Use general automation token
* readme for .evals_branch
* Use main of evals for label trigger
* Revert "dummy to trigger build"
This reverts commit e6502c6ff076a47d94eb829b516fc45b37cd7680.
* whitespace
* Address review remarks
* Use ai benchmark naming so we can trigger from pr
* Address review comments
* Remove now redundant branch pinning
* Review remarks
* feat: add lookback
* validate lookback
* feat: apply lookback
* tests
* fix
* fix test
* feat: only support lookback on temporal checkpoints
* update test
* FE
wip
* add :type/Time test
* add test with watermark row removed
* refactor to avoid double get-in
* only support date/datetime
* tests
* fix: support coerced cols
* add more tests
* refactor
* more tests
* test for coerced instant
* use utils in FE too
* chorec
* further validate
* use lib utils
* use strict comparison
* allow clearing lookback
* handle race
* chore: migrate registerVisualization to typescript
* chore: drop 'as unknown as Visualization' cast in custom viz plugin loading
applyDefaultVisualizationProps now takes the plain component and returns
the typed Visualization via Object.assign. The SDK's date_style format
option is widened to string | null to match the ColumnSettings value the
host actually passes to checkRenderable.
* Unit test the native editor run button's unmapped-tag tooltip
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Unit test the Save button's unmapped-tag tooltip and disabled state
Covers both dimension and temporal-unit template tags.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Cover temporal-unit tags in the canRun template tag validation test
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Drop the unmapped field filter e2e test in favour of unit coverage
Every assertion it made is now covered by NativeQueryEditorRunButton and
ViewTitleHeader unit tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Read useLocation and useParams straight from react-router v7
* Re-export react-router's Outlet instead of shimming it through context
* Disable the command palette by path instead of route props
* Drop the now-unused route props plumbing
* Add mdb/with-unshared-connection: appdb connections exempt from ambient reuse
A connection holding fragile long-lived state -- a streaming result set
(postgres portal/cursor), or FOR UPDATE row locks -- must not be picked
up by ambient connection reuse: a borrower that inherits it via
*current-connectable* and commits destroys that state. This is the
failure mode behind the first-boot hang in #78238, the revert (#77048)
of appdb streaming (#76645), and "cursor/portal does not exist" errors
when field updates run mid-sync-stream.
Built on toucan2's new unshared-connection! (camsaul/toucan2#212): the
connection is used when passed explicitly -- (t2/reducible-query conn
...), (t2/with-transaction [t conn] ...) -- but never bound as
*current-connectable*, so toucan calls that resolve their connection
ambiently run (and commit) on other pooled connections.
mdb/with-unshared-connection is checkout -> mark -> body -> close; in
every other respect the connection is ordinary JDBC, configured by the
body (e.g. .setAutoCommit false to stream). The pg test reproduces the
exact #77048 failure on an unmarked control connection (portal killed by
a nested commit mid-reduction) and shows the unshared connection
streaming to completion.
deps.edn temporarily pins toucan2 to the PR sha via :git/sha; a
follow-up commit will switch back to :mvn/version once a toucan2
release includes it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f
* Use released toucan2 1.0.572 for unshared-connection support
camsaul/toucan2#212 merged and released; drop the :git/sha pin.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f
* Hold the audit DB cluster lock on a detached connection (fixes boot stall)
Since #76551 the audit install/load/sync pipeline runs inside a cluster
lock whose SELECT FOR UPDATE ties lock lifetime to transaction lifetime:
the whole pipeline became one long transaction on the lock's connection.
The serdes load's ~3,600 per-entity nested transactions piled up as
savepoints and subtransaction XIDs, and every row lock was held until a
single commit at the end -- stalling first boot for 10-25+ minutes and
blocking concurrent work like config-file database provisioning (#78238).
with-cluster-lock now supports :detached? -- the lock rows are held on a
dedicated unshared connection (toucan2 unshared-connection!) while the
body runs on ordinary pooled connections, committing incrementally in
its own short transactions. Locks release at body completion (commit) or
throw (pool check-in rollback). Re-acquiring a lock this thread already
holds detached is a no-op in both detached and transactional form;
upgrading a detached :share hold to :exclusive throws instead of
self-deadlocking; :retry-transient? is rejected (a re-run would
double-apply incrementally-committed work).
The audit pipeline opts in: per-entity serdes isolation (#74412) is
restored, and a crash mid-pipeline is repaired by the next boot (the
checksum only advances on completion; reconcile runs every boot), which
is what makes incremental commit safe here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f
* Address review: confine detached retry to acquisition; repair dialect-sync window
Review findings (galdre), all majors addressed:
- Major 1: retry/with-retry wrapped the whole detached body, so a
retryable-looking error thrown by the body (e.g. a nested cluster lock
timing out inside the serdes load) re-ran an incrementally-committed
body and then surfaced as "Failed to obtain cluster lock", which
ensure-audit-db-installed! swallows as benign contention. An acquired?
volatile now confines both the retry predicate and the acquisition-
failure wrapper to the pre-acquisition phase in detached mode; body
errors propagate raw with intact cause chains.
- Major 2: a failed or killed engine-changed dialect sync was never
repaired once the content checksum had advanced. New durable setting
audit-db-dialect-sync-pending: set before the sync attempt, cleared on
success, a third sync trigger on every boot.
- Major 3: the mysql/h2 engine-flip visibility window under detached
mode is documented as a KNOWN WINDOW at adjust-audit-db-to-source!
(restores pre-#76551 semantics; overlay-based resolution is follow-up).
- Majors 4-6: reviewer's contract tests adopted (throw-path lock
release, check-in rollback, behavioral pin on :detached?, crash-mid-
load self-heal, plus the two Major-1 reproductions); public docstrings
updated to describe all three lock modes and the upgrade throw.
Minors: unified cross-mode reentrancy registry with eager share->
exclusive upgrade validation (fail-fast in any detached interleaving;
legal same-transaction upgrade preserved); h2 read->write upgrade throws
instead of hanging; lock-row INSERT gets a query timeout (bounds the
N-node cold-start pile-up); install-database! and each reconcile group
are single transactions; unshared checkout goes through *application-db*
so the restore-time read-lock gate applies; registry conveyance and
explicit-tx-on-unshared-connection caveats documented; dual kondo
lint-as entries; test hygiene (finally cleanup, nil ambient contract).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f
* Right-size the engine-flip visibility comment
The committed, briefly-visible engine flip is this pipeline's
longstanding behavior; only #76551's single-transaction era hid it for
one release. A neutral note fits better than a warning block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f
* Tighten comments to constraints; drop review-context narration
Comments now state what the code can't show (crash windows, why raw
JDBC, why the marker precedes the sync) and drop justification written
for reviewers: issue-number history, caller name-drops, repeated
tellings of the retry-confinement story, and cost reassurances.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f
* Make the detached lock a bespoke entry point instead of a with-cluster-lock mode
Reviewer pressure on the :detached? option had generalized
with-cluster-lock into a two-mode API whose cross-mode interleavings all
needed guarding: a held-locks registry, mode-upgrade validation, an h2
upgrade guard, and an acquired? volatile threading retry confinement
through the shared machinery — all protecting call patterns nothing in
the tree performs.
with-detached-cluster-lock is now its own function: exclusive-only,
single lock, documented non-reentrant. Structure replaces machinery —
the body sits outside the acquisition retry, so body errors are never
retried nor rewrapped as acquisition failure (no volatile), and
same-name re-entry throws via a two-line dynamic-set guard instead of a
registry (nested *different* locks, which the audit body does take,
work unchanged). with-cluster-lock returns to its previous shape; the
lock-row INSERT keeps its new query timeout, which bounds the
multi-node cold-start pile-up in both modes. Error shape is unchanged,
so audit-lock-contention? still classifies correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Compact in-memory format for the per-request data-permissions cache
The request-scoped cache in metabase.permissions.models.data-permissions
previously stored raw data_permissions rows as full Toucan instances, keyed
{user-id {perm-type {db-id [row ...]}}}. On instances with thousands of
databases and users in many groups a single prime-db-cache call could
materialize over a million instances (hundreds of MB retained per request,
plus the matching GC churn).
Store a compact entry per (perm-type, db-id) instead:
{:groups {group-id perm-value} ; db-level rows
:tables {table-id [schema-name {group-id perm-value}]}} ; table-level rows
and intern the group->value maps at load time: on big instances they are
typically identical across most databases, so the retained size becomes
proportional to the number of distinct permission profiles instead of
groups * databases.
Also fetch only the columns the checks actually consume (group_id, perm_type,
db_id, table_id, schema_name, perm_value) instead of p.*, and fix an O(n^2)
already-cached filter in prime-db-cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* small fix
* Use metabase.util.performance helpers in the cache hot path
perf/concat + perf/mapv in entry-group-value-pairs. Deliberately NOT
perf/update-vals for the interning pass: it skips replacing values that
are = to the old ones, which would undo the interning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Drop perf helpers from entry-group-value-pairs
perf/concat over an already-built vector is just (into a b), and reduce-kv
iterates the map faster than mapv over entry seqs. Plain into + reduce-kv,
one less require.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address code-review findings on the compact permissions cache
- Duplicate data_permissions rows (no unique constraint on
group/type/db/table) no longer silently last-win: cache slots promote to
a set when duplicate rows disagree, so every value reaches the consumers'
coalescing logic exactly as raw rows did. Regression test added.
- schema_name is stored per table-level row ({:schema .. :value ..})
instead of first-row-wins per table, since it is denormalized and rows
for one table can disagree.
- The interner now lives in the *permissions-for-user* atom, so value
sharing spans every prime in the request (db-at-a-time priming
previously got a fresh pool per call), and it reuses
metabase.util.memoize/fast-interner instead of a bespoke pool.
- rows->cache-entry returns nil for empty rows so cached and non-cached
get-permissions branches agree; the non-cached branch skips interning.
- schema-permission-for-user and most-permissive-database-permission-for-user
now go through entry-group-value-pairs instead of hand-rolled traversals.
- Dropped the impossible-nil guards in prime-db-cache.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix
* Address CodeRabbit nitpicks
- gid->slot -> group-id->slot (no abbreviations)
- native-download-permission-for-user reuses most-restrictive-per-group
instead of re-implementing the grouping inline
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rename local intern -> interner (Eastwood local-shadows-var)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Explain the slot rationale in conj-slot/slot-seq docstrings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* simplify slots
* Document interning with a worked example in the cache docstrings
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014V8QbHjoEkJ8eZLG13sYUe
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: dan sutton <dan@dpsutton.com>
* Restyle the sharing popover with Copy link and Embed buttons above the actions list
* Reserve the copy button width with hidden pseudo-elements instead of inline styles
* swap subscriptions action with auto-refresh action
* hide Subscriptions for non-admins if slack and email aren't set up
* small adjustments
* e2e adjustments
* more e2e adjustments
---------
Co-authored-by: Nick Fitzpatrick <nickfitz.582@gmail.com>
* Convert the legacy chart legend components to TypeScript function components
* Share the legend prop types and extract a typed getChartLayout helper with tests
Deleting a Database hard-deletes its Cards with raw SQL, but nothing
rewrites the `parameters` JSON on Cards/Dashboards that used one of
those Cards as a dropdown source. The parameter keeps a
`values_source_config.card_id` pointing at a row that is gone.
On export that made `*export-fk*` throw `:target-not-found`, which
`log-and-extract-one` swallows as a skip — silently dropping the whole
Card or Dashboard from the archive.
Export each parameter on its own now, and when one carries a card
values-source that can no longer be resolved, emit it without
`:values_source_type`/`:values_source_config`. That is the same shape
the app writes when a source Card is archived, so the parameter comes
back as a plain dropdown over its connected fields.
A source Card that merely isn't part of the export is unaffected —
`*export-fk*` only throws when the row is absent, so those still fail
dependency validation as before.
Fixes GHY-3259
The helper inserts straight into the table to bypass the model's transforms,
which bypasses the hook that fills in entity_id too. #78452 made that column NOT
NULL, so every App DB Tests job has failed on it since.
* Eagerly load tenant auth-provider; fail tenant SSO logins loudly (UXW-4898)
Root cause: metabase-enterprise.tenants.auth-provider — the login! method
that validates a login's tenant claim and stamps :tenant_id onto the user
data — was never eagerly loaded. The JWT/SAML providers only `derive` from
its dispatch keyword, which does not load the namespace, so on a freshly
started instance the tenant step was silently absent from the login!
method chain until unrelated traffic (e.g. /api/user/current hydrating
user attributes) lazily loaded it. A JWT provisioning login in that window
created the user with tenant_id = nil even though the token carried a
valid @tenant, the tenant-group assignment failed and was swallowed, and
the login returned 200 — permanently wedging the account (every later
login 403s with "Cannot add tenant claim to internal user").
Fix, plus defense in depth so no future tenant-drop can wedge an account:
- Add metabase-enterprise.tenants.init requiring the auth-provider, wired
into metabase-enterprise.core.init, so the tenant login step is always
registered at startup.
- create-user! throws instead of silently dropping :tenant_id when the
sso-user-fields field list would strip a tenant assignment stamped by
the upstream tenant flow.
- sync-group-memberships*! no longer swallows real tenant/group mismatch
errors (bad pairs whose :group-is-tenant? is non-nil); the login fails
loudly instead. Nonexistent groups keep log-and-continue. This also
covers EMB-2118 (JWT omitting @tenant with a mapped tenant group).
- The login! provisioning chain (tenant creation, user create/update,
group sync) runs in one transaction, so a failed login leaves no
half-provisioned user row behind.
* Fix cljfmt formatting in jwt_test.clj
* Stop the location-syncing call sites from replacing the url they just read
* Cover the documents /new to /new warning with a unit test
* Adopt v7 navigation semantics, drop the v3 parity guards
The v7 provider mirrored every location into state.routing by calling
useDispatch itself, which is what kept metabase/router bound to the store.
createLocationMirror takes a plain dispatch function instead, so the router
publishes locations without knowing redux exists. app.js and the test harness
supply it as RouterProvider's onLocationChange prop. V7ReduxBridge is renamed
V7RouterBridge now that nothing in it is redux-specific.
metabase/router now passes a basic-tier boundaries trial with zero violations.
Guards are authorization policy, not routing primitives. They were the largest
source of shared-tier imports inside metabase/router (redux, plugins, and four
selector modules), blocking the module from dropping to a lower tier.
The generic factory and the app-wide auth/setup guards move to a new shared
metabase/route-guards module. The monitor and data-studio guards move into their
own features, which lets getMonitorRoutes and getDataStudioRoutes stop taking
them as parameters.
Router violations under a basic-tier trial go from 13 to 3; the rest are the
redux location mirror in the v7 provider.
* Add GeoJSON error mocks and cover map load failures in unit tests
`setupGeoJSONEndpoint` could only produce a valid FeatureCollection, so the
error branches of the custom map flow had no unit coverage.
- `setupMalformedGeoJSONEndpoint` returns a 200 with a payload the client-side
validator in `metabase/api/geojson` rejects, since those checks run in
`transformResponse` and need the payload to reach the client.
- `setupGeoJSONEndpointWithError` returns a non-2xx with a bare string body,
matching what `api-exception-response` sends for an `ex-info` carrying only
a status code.
- `createMockProjectedGeoJSONFeatureCollection` supplies projected coordinates
so every bounds corner falls outside lat/lng range.
Covers the invalid-location, missing-features, and out-of-bounds errors in
CustomGeoJSONWidget.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Drop map GeoJSON error e2e tests in favour of unit coverage
All three asserted error strings are now covered more cheaply. The two
backend-owned messages are pinned by metabase.geojson.api-test against mocked
HTTP, and the modal rendering them is covered by CustomGeoJSONWidget. The two
client-side validation messages come from transformResponse in
metabase/api/geojson, which the e2e tests reached with the backend fully
stubbed or bypassed.
Two of them also drove real outbound requests from CI — one to www.metabase.com
and one to a file on the public repo's master branch.
projected.geojson had no other consumer; the equivalent payload is now built by
createMockProjectedGeoJSONFeatureCollection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Drop admin settings e2e tests that duplicate existing unit coverage
Each of these asserts the same condition as a unit test that already exists,
not merely an adjacent one:
- the HTTPS redirect check failure message is asserted in HttpsOnlyWidget
("should display an error message if the site is not accessible via https")
- the billing info fetch failure is asserted in LicenseAndBillingSettings
("renders error for billing info for store managed billing and info request
fails")
- the absent license input for hosted instances is asserted in
LicenseAndBillingSettings ("does not render an input when token has hosting
feature enabled")
mockBillingTokenFeatures and its feature-key constants had no remaining
callers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Persist disabling a collection from remote sync (GHY-4189)
Disabling a collection deleted its RemoteSyncObject rows outright, so the
removal left no dirty state to push and the collection's files stayed on the
remote. A later pull then re-imported the collection (its YAML still carried
is_remote_synced: true), re-enabling it.
Mark the collection's and its contents' RemoteSyncObject rows as "removed"
instead — a pending deletion the next export pushes to the remote — while
dropping rows still in "create" (never pushed, so nothing to delete remotely).
* Don't delete contents when a collection is re-synced before the push
Disabling a collection records a pending removal on its contents'
RemoteSyncObject rows. Re-enabling it re-tracked only the collection's own row,
so the contents stayed marked for deletion and the next export removed their
files from the remote.
Clear the recorded removal when a collection is re-synced before that removal
was pushed. Restore to "update" rather than "synced": edits made while the
collection was un-synced are not tracked, so the entity must be re-serialized
for the remote to be guaranteed to match local.
* Stop core-test leaking RemoteSyncObject rows between tests
Disabling a collection now marks its RemoteSyncObject rows rather than deleting
them, so rows seeded by a test outlive it and leak dirty state into every later
test. The old delete-on-disable behavior had been doing this cleanup by
accident.
Reset the table around each test with the clean-object fixture.
* Assert the pull succeeds in the disable round-trip test
The test discarded impl/import!'s result, so an errored pull would leave the
collection un-synced for the wrong reason and the following assertion would
pass vacuously.
* Re-check eligibility at write time so a pending removal isn't resurrected
An event handler computes eligibility from its event payload, then writes the
RemoteSyncObject row much later. A disable committing in between left the
handler writing a tracked status over the "removed" status the disable had
recorded, so the entity stayed on the remote after being un-synced.
Re-check eligibility against current DB state before letting a tracked status
overwrite a pending removal, and let the removal stand when it no longer holds.
* Row-lock the sync entry so the un-sync race has no remaining window
Re-checking eligibility at write time narrowed the race but did not close it: a
disable could still commit between the re-check and the update, and the losing
write would take effect.
Row-lock the entry for the transaction so a tracking write and a concurrent
un-sync settle in a fixed order instead of losing one of the two: whichever
locks first commits, and the other observes that result — the un-sync re-marking
the row, or the eligibility re-check seeing the entity leave the synced set.
The race test drives the disable on its own thread and unparks the handler
without waiting for it, since the parked handler holds the row lock the disable
needs. Its assertion holds for either order, so the choreography only biases
which one is exercised.
* Address review: honest race coverage, insert-path guard, tighter asserts
- Split the concurrency test into two, each parking the handler at the point
that exercises one ordering: disable-first (guarded by the write-time
eligibility re-check) and handler-first (guarded by the row lock, which the
disable now provably blocks on). The prior single test parked on the plain
existing-entry read, so it only ever exercised the re-check ordering while its
comment claimed lock contention.
- Re-check eligibility in the insert branch too: a stale tracked-status event
that finds no row inserts a fresh one, a phantom the row lock cannot cover.
This narrows, but cannot fully close, that window.
- Round-trip pull test asserts = :success rather than not= :error, so a
:conflict result can no longer slip through.
- restore-removed-rsos! docstring no longer claims it only clears removals from
record-removed-rsos!; it clears any 'removed' row in the subtree.
* Re-track never-pushed contents when a collection is re-synced
Disabling a collection drops its contents' never-pushed 'create' RemoteSyncObject
rows (the remote never had them, so nothing to delete there). Re-enabling only
restored the rows that were marked 'removed', so a card added-but-never-pushed
before the disable was left with no row — and the next incremental export, seeing
no dirty row for it, silently omitted it.
On re-enable, insert a 'create' row for every eligible content entity in the
re-enabled subtree that has none, so the next export pushes it. The
RemoteSyncObject table only records pending changes, so already-synced content is
legitimately untracked; re-marking it 'create' re-serializes it harmlessly (a
'create' onto its own path is a no-op).
* scaffold: two-step SDK release process (EMB-1518)
Replaces the hardcoded version/dist-tag computation in
release-embedding-sdk.yml with a new sdkRelease metadata block in
package.template.json (distTag, tagAsLatest), plus a new
sdk-version-bump-pr.yml workflow that bumps the version via `npm version`
and opens a PR labeled sdk-release-trigger for review. Merging that PR
triggers the release workflow directly.
Unverified: scaffolded per EMB-1518 ahead of Nicolo/Roman's review, since
this can only really be exercised by triggering a real SDK release. See
.claude/kelvin/2026-07-09-emb-1518-simplify-sdk-release-process-and-checklist-maintenance/
for the plan and open questions (particularly around distTag semantics for
stable/patch/minor releases).
* fix: correct distTag comment, <major>-stable is real (EMB-1518)
npm dist-tag ls @metabase/embedding-sdk-react confirms 51-stable
through 62-stable are already live, actively-used tags - not a new
concept as the prior comment assumed. Resolves the one open question
this scaffold couldn't settle from the proposal text alone.
* fix: address review cycle 1 findings (EMB-1518)
Fixes release-breaking and correctness issues found in review cycle 1
of the SDK release process scaffold:
- beta release_type now defaults to <major>-beta dist-tag instead of
the literal (non-existent) "beta" tag
- validate branch/release_type combinations that the old
getNextSdkVersion used to hard-error on (master requires alpha,
release branches reject alpha)
- pull_request trigger on release-embedding-sdk.yml now scoped to
package.template.json via a paths filter, and the concurrency group
no longer cancels an in-flight release on unrelated PR closures
- readme-PR follow-up steps now skip cleanly when the readme was
already up to date, instead of failing and firing a false alert
- tagAsLatest now always resets to false rather than carrying forward
a prior true, and the actual value is shown in the summary/PR body
- version-bump branch name includes the run id to avoid collisions on
retry
- renamed a step whose name didn't match what it does
See the "Review cycle 1 (fable)" section and the new "Fixes applied
for review cycle 1" subsection in the plan file for full detail on
each finding and its disposition.
* fix: address review cycle 2 findings + simplify version bump (EMB-1518)
Cycle-2 findings: add a preminor release_type so master's next-major
alpha bump no longer needs a hand-made PR (#9); make the Slack
failure notification independent of an artifact upload that may
never have run (#10); guard against two open version-bump PRs racing
each other (#11); stop interpolating the free-text branch input
directly into shell scripts (#12).
Also replaces the npm-version rename dance (rename to package.json,
bump, rename back, plus a .bak copy) with direct calls to the semver
package the bump logic already wraps, writing the result via jq
instead - no functional change, removes the dead .bak file (#13).
* fix: address review cycle 3 findings + rename resolve-release job (EMB-1518)
Fixes findings #14-#16 from the third fable review cycle:
- #14: the failure Slack alert's messageId interpolation was unquoted,
rendering a JS syntax error whenever send-slack-notification-start was
skipped (the exact scenario cycle-2's fix#10 was meant to cover). Now
quoted, and falls back to sendSlackMessage (new top-level message)
instead of sendSlackReply when there's no thread to reply to.
- #15: finished the cycle-2 injection-hardening pass by routing the two
remaining inline branch-value usages (workflow-summary's heredoc,
publish-npm's gh pr create --base) through env: instead of ${{ }}.
- #16: added a semver validity pre-check before the bump step, so an old
release branch's 0.N.x placeholder version fails with a clear ::error::
instead of a silent exit 1 from the semver CLI.
#17 (minor's version-collision caveat on old release branches) is left as
an open question for Nicolo/Roman rather than removed - documented with a
comment and Judgment call #6. #18 (merge-commit-sha race) is deferred -
it fails loudly rather than silently, lower priority than the others.
Also renames the resolve-release job to prepare-release throughout
release-embedding-sdk.yml, per explicit request (not a review finding).
See the plan file's "Fixes applied for review cycle 3" section for the
full verification detail.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2Va4ryWuZbQvmUt2N85ec
* feat: auto-compute tagAsLatest from vars.LATEST_VERSION (EMB-1518)
Resolves Judgment call #2: the SDK version-bump workflow previously
always defaulted tagAsLatest to false because there was no reliable
way to know if a branch is the current "latest" major. The repo
already exposes vars.LATEST_VERSION (major version of the most recent
gold/stable release) for exactly this, so patch/minor bumps now set
tagAsLatest=true when MAJOR_VERSION matches it, false otherwise
(including when the variable is unset). alpha/beta/preminor stay
false. The PR review gate still allows overriding the computed value
before merging.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2Va4ryWuZbQvmUt2N85ec
* refactor: use github.ref_name instead of inputs.branch (EMB-1518)
github.ref_name is already set by GitHub's "Use workflow from" ref
picker on every workflow_dispatch run, so the custom inputs.branch
free-text field was duplicating a selection that was already made -
and could silently diverge from it. Switch every functional read of
inputs.branch to github.ref_name in both release workflows; keep the
branch input field itself, but make it explicitly non-functional
(required: false, no default, description points at the ref picker).
type: choice was considered and rejected again here: it would close
the typo risk but can't accept an unregistered branch, breaking
one-off custom-branch releases without editing the workflow file per
release - exactly the maintenance burden EMB-1518 removes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2Va4ryWuZbQvmUt2N85ec
* fix: remove dead readme-update mechanism (EMB-1518)
update_readme's two sed patterns match zero occurrences in the current
README (restructured by 15f7d3d20e, no version-pinned content remains),
so publish-npm's readme-follow-up-PR step has been a silent no-op on
every release, not just re-runs. Deleted the entire dead block (7 steps,
the env vars and pull-requests:write permission it alone used) and the
now-unused update_readme function.
* fix: drop minor release_type option (EMB-1518)
`semver -i minor` on a release branch jumps to the next major's
version line, colliding with that major's own branch. `patch` already
covers graduation and `preminor` already covers the legitimate
next-major jump (scoped to master). Drop `minor` outright rather than
add compensating validation - resolves review-cycle-3 finding #17 /
Judgment call #6.
* fix: add explicit reviewer request for embedding team on bump PR (EMB-1518)
CODEOWNERS is deliberately stripped on release branches (a repo-wide
practice to cut auto-backport review noise, unrelated to this
workflow), so the bump PR's "code owner review is automatic" comment
was false there - it got zero reviewers requested on release
branches. EMB-1000 documented the identical gap in the old system's
follow-up PR. Fix targets the metabase/embedding team (not a named
individual - the old system's hardcoded reviewer has since left).
* fix: restrict release types by branch, add custom releases (EMB-1518)
A one-off branch could publish as the stable release. Validation only
guarded master and release-x.*, so `patch` on e.g. sdk-data-apps stripped
the prerelease id (0.62.5-data-apps.0 -> 0.62.5), computed the
<major>-stable dist-tag, and - when the major matched vars.LATEST_VERSION
- set tagAsLatest, shipping a feature branch to npm as the stable release.
Only check-git-tag stood in the way, and only if that version had already
shipped.
Each branch class now gets only the release types that make sense for it:
master takes alpha/preminor, release branches take beta/patch, any other
branch takes custom. tagAsLatest is additionally gated on a release-x.*
branch, so nothing published from master or a one-off branch can take the
npm `latest` tag.
The custom type keeps one-off branches (esbuild, data-apps) releasable
under that restriction: prerelease_id is set on the first cut and becomes
both the version's prerelease id and the <major>-<id> dist-tag, matching
the shape already live on npm (56-esbuild, 63-data-apps). Later bumps
leave it empty and semver reuses the id from the committed version.
Also drops the dead release_utils.bash (zero functions, zero callers),
replaces both free-text dispatch inputs with single-option choices that
cannot be typed into, points the release workflow's form at the bump PR,
and removes the scaffold banner comments.
* fix: read vars.LATEST_MAJOR_VERSION, not LATEST_VERSION (EMB-1518)
The variable is LATEST_MAJOR_VERSION - it is what release.yml already
reads to decide Docker's `latest` tag, and what the release checklist has
a github admin bump each cycle. `vars.LATEST_VERSION` does not exist, so
tagAsLatest would have computed false on every bump, including at gold,
without any error - the fallback for an unset variable is false.
* refactor: make the release concurrency group readable (EMB-1518)
Same three cases as before, but each arm now names what it produces
(sdk-release-<branch> for a dispatch or a merged bump PR, sdk-release-noop-<run id>
for a closed PR this workflow ignores), and the comment above maps them in
the order the expression tries them.
The noop arm is not decoration: concurrency is evaluated before any job's
`if:` runs, so a closed PR that prepare-release skips would still enter the
release group without it. cancel-in-progress stays false so a release is
never interrupted mid-publish.
Groups are prefixed rather than bare because concurrency groups are
repo-wide, not per-workflow.
* refactor: build the release scripts once, in their own job (EMB-1518)
The Slack jobs each got at release/dist/index.cjs a different way:
send-slack-notification-start checked out and built it (then uploaded it as
an artifact), send-slack-notification-failure built its own second copy, and
send-slack-notification-successful downloaded the artifact. The duplicate
build existed because the failure job has to fire even when the metadata read
fails - in which case the start job never ran, so its artifact never existed.
A build-release-scripts job now builds it once and uploads it, and all three
Slack jobs download it. The job hangs off prepare-release only, so the
artifact is there no matter how early the release fails - which removes the
reason the second build existed.
* fix: quote the Slack messageId in the success notification (EMB-1518)
A Slack ts is a string. Interpolated bare it becomes a JS number literal and
drops trailing zeros - 1783602196.214000 renders as 1783602196.214, and the
reply targets a thread that does not exist. Pre-existing on master; the
failure job already quoted it, so this makes the two consistent.
* refactor: resolve the release branch in one expression (EMB-1518)
The env: BRANCH indirection was injection hardening from when the value was
the free-text `inputs.branch`. It is github.ref_name now - a real branch
name, same as github.event.pull_request.base.ref, which the next line already
inlined - so it guarded nothing and made the step asymmetric.
base.ref is empty on a dispatch, so `base.ref || ref_name` picks the right one
without branching on github.event_name.
* refactor: drop the duplicate-bump-PR pre-flight (EMB-1518)
It guarded a case the design already handles safely. Two open bump PRs
against the same branch both compute from the branch's committed version, so
both propose the same next version: the first to merge publishes and tags it,
and the second dies at check-git-tag with "Tag ... already exists" (npm would
reject the republish too). Nothing is published twice, and the error says
exactly what happened.
Getting there needs someone to dispatch the bump twice and leave the first PR
open, with both PRs sitting labelled in the list. That is not worth an API
call and 16 lines on every bump.
* chore: trim the bump step's comments (EMB-1518)
* refactor: drop the semver validity pre-check (EMB-1518)
Every supported release branch (57 through 63) already carries a valid
semver, and the new process never writes a `0.N.x` placeholder - gold is a
patch bump (0.63.0-beta.3 -> 0.63.0). The placeholder only ever existed
transiently under the old checklist, which rewrote it back to a concrete
version after each release.
Also saves an npx invocation on every bump.
* chore: spell out PRERELEASE_ID in the bump workflow (EMB-1518)
* refactor: spell out the semver call in each release type (EMB-1518)
alpha and beta shared an arm that passed --preid="$RELEASE_TYPE", and patch
passed -i "$RELEASE_TYPE" - you had to know the variable's value to know what
command ran. Each arm now names its own call.
The release_type description now leads with the phase (before gold, at gold,
after gold), which is what the person dispatching actually knows. It also
corrects preminor: master moves to the next major *after gold*, not at branch
cut - the 62 cycle did it in the after-gold checklist (#75487, 0.62.0-alpha ->
0.63.0-alpha), and 61 before it (#74279).
* docs: stop calling the bump PR a review gate (EMB-1518)
Nothing blocks on approval: the bump PR is not auto-merged but it is not
gated on a reviewer either, and workflow_dispatch can publish with no PR at
all. The computed distTag/tagAsLatest are defaults made visible and editable
- whoever dispatched the bump has already decided.
* docs: note EMB-2095 (auto-merge the bump PR) in the workflow (EMB-1518)
* docs: tighten the tagAsLatest comment (EMB-1518)
* refactor: collapse the tagAsLatest case into one condition (EMB-1518)
* docs: say what the sdkRelease write does (EMB-1518)
* refactor: drop the label-creation step (EMB-1518)
The sdk-release-trigger label is repo-level, so it only ever needed creating
once - it now exists. Re-running `gh label create ... || true` on every bump
swallowed every error (including auth) to no purpose.
* fix: fail on a leftover bump branch instead of side-stepping it (EMB-1518)
The run-id suffix made every retry a fresh branch, so an abandoned bump left
a branch on origin forever and the name was unpredictable. The branch is now
named after the version, and an existing one is an error: an earlier bump to
the same version was never merged, which is worth knowing before opening a
second PR alongside it.
* refactor: drop the no-backport label step (EMB-1518)
Backporting is opt-in - auto-backport.yml only fires on a `backport` label -
and backport-reminder.yml only demands a decision label when a PR touches
docs/**. The bump PR touches one JSON file, so no-backport guarded nothing.
Keeps the reviewer rationale, moved next to the --reviewer flag it explains.
* feat: show the target branch and computed values in the bump PR body (EMB-1518)
The PR body did not say which branch it targeted, which is the first thing
you want to know when a release PR shows up. Branch, release type, version,
dist-tag and tagAsLatest are now a table at the top.
* fix: pin semver and validate prerelease_id (EMB-1518)
npx --yes semver resolved whatever the registry served at dispatch time, on
a job that installs no dependencies - so release-critical version math could
change under us while package.json already pins ^7.6.3. Pinned to semver@7.
prerelease_id was passed to semver unchecked. An id like `DataApps` bumped
fine and then died three steps later in the dist-tag read-back, whose regex
only matches [a-z][a-z0-9-]*, with an error blaming the computed version
instead of the input. It is now rejected up front, with a message naming the
rule.
* fix: take semver's version from package.json (EMB-1518)
Pinning the workflow to semver@7 fixed the drift but created a second
version to keep in step. Read the range the repo already pins instead
(devDependencies.semver, currently ^7.6.3) and hand it to npx, so bumping
semver in package.json is the only place it happens.
* refactor: rename bump workflow input to read_first (EMB-1518)
* feat: reject prerelease_id on non-custom release types (EMB-1518)
* refactor: drop the preminor bump-only warning from the PR body (EMB-1518)
* refactor: drop the duplicate rerun wording in tag-exists error (EMB-1518)
* fix: reject reserved alpha/beta as a custom prerelease_id (EMB-1518)
* refactor: tidy and clarify comments in the SDK bump workflow (EMB-1518)
* fix: satisfy actionlint in the SDK release workflow (EMB-1518)
* feat: extract embedding SDK release logic into a tested helper (EMB-1518)
* refactor: remove dead embedding SDK helpers from version-helpers (EMB-1518)
* fix: drop trailing blank line in the SDK bump workflow (EMB-1518)
* refactor: run the SDK version bump from a tested helper, not inline bash (EMB-1518)
* docs: note the env block guards github-script against injection (EMB-1518)
* refactor: drop the unused LATEST_MAJOR_VERSION fallback in the bump script (EMB-1518)
* feat: show the target branch in the bump PR title (EMB-1518)
* docs: drop the manual-edit invitation from the bump PR body (EMB-1518)
* refactor: tidy the SDK release helper and its tests (EMB-1518)
* refactor: read the SDK release metadata via the tested helper (EMB-1518)
* refactor: env-route the branch name in the bump PR title (EMB-1518)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0156DAaukfiU6xceXd3brbZN
* refactor: simplify SDK release concurrency group (EMB-1518)
Collapse the three-case concurrency expression to one per-branch group, and
switch every multi-item needs array to a block sequence.
The dropped noop group guarded a compound case: GitHub keeps one pending run
per group and cancels any earlier one, so a closed PR touching the template
could evict a release waiting to start. That needs two same-branch releases
in flight at once, so we take the simpler expression. The clean fix is
queue: max, which actionlint cannot parse yet.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Deflake transform revision history spec
The "view and revert transform revisions" test changed the transform
name twice, each firing a "Transform name updated" success toast that
lives for the default 5s undo timeout. Unlike every other update flow in
this spec, these toasts were never dismissed. On a fast runner the test
reached the failed-revert error toast within that window, so a lingering
success toast coexisted with it and H.undoToast() (a single-element
findByTestId on [data-testid="toast-undo"]) failed with "Found multiple
elements".
Dismiss each success toast right after it appears, matching the existing
convention used elsewhere in this file, so exactly one toast is present
when the error toast is asserted.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Nemanja <31325167+nemanjaglumac@users.noreply.github.com>
* Add semantic search health checks
* Split HNSW recovery from semantic health
* Use HoneySQL literals for repair queries
* Use dynamic redefs in semantic search test
* Auto-scroll to a dashcard whenever it becomes the scrollTo target, and keep the scroll until the grid settles
* Drop a stray brace from the auto-scroll spec's first visit URL
* Simplify the auto-scroll correction to a fixed frame window
* Delete the react-router v3 engine
* Sever the react-router package dependency
* Give the SDK data-app router its own browser history on v7
* Rename the react-router-v7 alias to react-router
* Rename getDataAppHistory to getRawBrowserHistory, drop dead getCurrentHistory
Scope existence preconditions to their table; lint against name-only checks
A foreignKeyConstraintExists precondition with only foreignKeyName (or
indexExists with only indexName) forces Liquibase to snapshot the entire
schema to answer the check -- ~2s per precondition on a large postgres
appdb. 27 such preconditions (24 FK, 3 index; 21 in one v59 workspace
block) added ~46s to every full migration: 50.2s of total changeset time,
of which 24 changesets cost 45.9s and the other 1,182 cost 3.7s.
Add the scoping key to all 27. Measured on a fresh postgres migration:
total changeset time drops 50.2s -> 5.1s, with zero changesets over 1s.
Editing released changesets' preconditions is safe: Liquibase checksums
cover the changes, not preConditions. Verified two ways -- ChangeSet
.generateCheckSum is byte-identical before/after for all 27 (v9
checksums), and a clone of an already-migrated appdb re-validates
cleanly against the edited files.
lint-migrations-file: generalize require-primary-key-exists-has-table-name
into require-table-scoped-existence-preconditions, covering
primaryKeyExists/indexExists/uniqueConstraintExists (tableName) and
foreignKeyConstraintExists (foreignKeyTableName), walking the full
precondition tree in both its flat-map and list forms.
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Fall back to .yaml if .yml not found
Update docs to mention `.yaml` support
* Fall back to .yaml if .yml not found
Update docs to mention `.yaml` support
* Update file.clj
* Fix tests
* Address feedback
* Drop field_usage table as it is no longer used.
* Stop saving preprocessed_query in metadata since it's not used any more.
This used to be used for saving off field usage, but that hasn't been
a thing for a very long time.
* Make model-illustration.svg work well in both light and dark mode
* whitelabel svg
---------
Co-authored-by: Nick Fitzpatrick <nickfitz.582@gmail.com>
* Migrate PinMap to a typed function component and extract a pure getPoints
* Type the pin metric as nullable, derive MapSettings from VisualizationSettings, and add createMockVisualizationProps
* Trim comments, replace the map ref cast with an instanceof guard, and cover instance clearing
* Don't block dashboard saves on pre-existing foreign internal cards
UXW-4870: the guard added in #77236 (reject assigning a dashcard's card
to a question internal to another dashboard) checked updated dashcards
unconditionally, so any dashboard with a pre-existing foreign internal
card association became unsavable — every edit, even a resize, returned
400 "Invalid Request.".
Scope the guard to newly-introduced card ids: card ids the dashboard
already references (as a dashcard's card or in a series) are
grandfathered, so existing associations never block saving — including
moving such a card to a different dashcard in the same save. Newly
assigning a question internal to another dashboard is still rejected.
* Clarify arg names in internal-dashcard guard
Rename the guard's params to existing-dashboard/new-dashcards to make
clear the grandfather set comes from stored state while the checked
dashcards are the incoming save.
* Guest embed filter dropdowns should use the router database (UXW-4881)
PR #68952 made guest-embed query execution bypass DB routing by wrapping it
in with-database-routing-off, but the parameter-values endpoints (dropdown
values, search, remapping) didn't get the same treatment: chain-filter runs
through the QP, where attach-destination-db-middleware throws "Anonymous
users cannot access a database with routing enabled."
- Wrap the query-running sections of the four param helpers in
metabase.embedding-rest.api.common in with-database-routing-off, fixing
all six /embed/{card,dashboard}/:token/params/* endpoints plus their
preview_embed equivalents.
- Wrap the guest-embed tiles endpoints, which have the same anonymous-user
bug.
- Wrap preview_embed's query-execution endpoints (query, dashcard, pivot,
tiles) so the admin's embed preview shows exactly what the published
embed will show (router DB data) instead of routing via the previewing
admin's user attribute.
- Replace the manual *current-user-permissions-set*/*is-superuser?*
bindings in the param helpers with the equivalent request/as-admin.
Public sharing has the same gap but whether routed DBs should work over
public links at all is an open product question, so it is intentionally
left out.
* Consolidate embed routing-off wraps into the shared execution helpers
Review feedback: rather than a macro that each endpoint must remember to
use, move with-database-routing-off inside the shared execution helpers in
metabase.embedding-rest.api.common (process-query-for-card-with-params,
process-query-for-dashcard, process-tiles-query-for-dashcard, and a new
process-tiles-query-for-card wrapper), which every /api/embed and
/api/preview_embed endpoint funnels through. Endpoints built on these
helpers now bypass routing automatically, so a future endpoint can't
reintroduce this class of bug.
Removes all per-endpoint wraps from embed.clj (including the two original
ones from #68952) and preview_embed.clj; the invariant now lives in one
file with one explanatory comment.
* Fix tooltip scrollbar flicker in TreeTable (GDGT-2822)
Hovering a cell tooltip in a virtualized TreeTable while scrolling made a
page-level vertical scrollbar flash on and off. The tooltip is portaled to
<body> and positioned with real top/left insets, and with the document root
sized to the exact viewport height, a tooltip positioned past the fold grew
the document and flashed a scrollbar.
Portal cell tooltips into the table's own scroll container instead of <body>,
so a tooltip positioned past the fold is clipped by the (already-scrolling)
table rather than stretching the page. This is wired through a small
TooltipPortalTargetProvider context so the metabase/ui Tooltip wrapper picks
up the target automatically — no per-tooltip wiring, and any TreeTable with
cell tooltips benefits (e.g. the notifications admin table).
* Address PR comment
* Fix Tooltip wrapper not forwarding ref
* Add monitor dir in metabase-enterprise
* GDGT-2676: Migrate Dependency Diagnostics under Monitor space
* GDGT-2677: Migrate Admin -> Tools under Monitor space
* GDGT-2676: Fix paths
* GDGT-2676: Create separate high-level directories for migrated features
* GDGT-2657, GDGT-2686: Monitor space + migrate Dependency Diagnostics (#76527)
* GDGT-2657: Introduce Monitor space layout and routing
* GDGT-2686: Migrate Dependency Diagnostics to Monitor
* Monitor: Add Monitor plugin. Cleanup + fixes
* Monitor: Update graphy ownership path for Dependency Diagnostics
* Monitor: Cleanup PR
* GDGT-2697: Update hotkeys hints for Toggle navbar
* GDGT-2684: Address Copilot review comments
* GDGT-2684: Address PR comments
* GDGT-2684: Rename SpaceLayout to AreaLayout. Split to separate files
* GDGT-2684: Refactor route guards + add tests
* GDGT-2684: Move Admin Tools pages to the Monitor space (#76606)
* GDGT-2684: Move Admin Tools to Monitor space
* Monitor: Extract diagnostics sidebar into shared Monitor sidebar outlet
Add a useMonitorSidebar context to MonitorContent so Monitor pages can
render sidebars in a dedicated right-hand region outside the padded,
scrollable main content area. Rewire Dependency Diagnostics to render its
detail sidebar through this outlet, and promote the resizable shell
(SidebarResizableBox) into shared monitor/components.
* Monitor: Add MonitorHeaderTitle component for consistent page headers
Introduce a shared MonitorHeaderTitle matching the small secondary text
style used by Data Studio breadcrumbs, and apply it across Monitor pages:
Dependency Diagnostics header, Jobs, Logs, Model cache log, Task list/runs,
Task and task-run details, Erroring questions, and Alerts management.
* GDGT-2684: Cleanup
* GDGT-2684: Update monitoring tools access permissions
* GDGT-2684: Add handling for not-found routes
* GDGT-2684: Fix not-found routes handling in Data Studio
* GDGT-2684: Extend route tests. Minor styling refactoring
* GDGT-2684: Refactor Alerts Management sidebar
* GDGT-2684: Move old SidebarResizableBox to ModelSibar
* GDGT-2684: Cleanup
* GDGT-2684: Refactor Sidebar solution to use Portal
* GDGT-2684: Fix Monitor Tools E2E specs (#76886)
* Format monitor router files after the master merge
* Import Route from metabase/router facade in AreaTab spec
Fixes the no-restricted-imports lint error banning direct react-router imports.
* GDGT-2713: Update Tasks page styling (#77088)
* GDGT-2713: Update Tasks page styling
Restyle the Monitor Tasks and Runs pages to match Dependency diagnostics:
- Replace the SettingsSection card + default tabs shell with the Monitor
frame (fixed-height page, pills tabs; only the table scrolls)
- Rebuild TasksTable and TaskRunsTable on the TreeTable data-grid
(sticky header, virtualized rows, skeleton loading, a11y labels)
- Move filters into a Group bar and pagination below the table with a
total results count
- Keep the 2-state sort contract (always sorted, desc/asc) on top of
TanStack's 3-state cycle; asc-first on all sortable columns
- Add MonitorHeaderTabs (Monitor-owned pills tabs, no data-studio
dependency) and extract the shared LinkTab router-link tab to
metabase/common/components
* GDGT-2713: Fix e2e test
* Migrate LinkTab to the metabase/router facade
* Remove unused Link import from PaneHeader
The Tasks styling switched the tabs to LinkTab, leaving Link unused.
* Address PR review comments and fix entity_name typing
Use strict undefined comparisons instead of loose null checks where
values can only be undefined (optional props, RTK Query error,
destructured SortingState), and strict null comparisons where the
value can only be null (Task.db_id).
Also fix TaskRun/RunEntity entity_name typing: the backend always
assocs this key (nil when the entity lookup misses), so it's
string | null on the wire, not optional.
* GDGT-2712: Update Jobs page styling (#77100)
* GDGT-2712: Update Jobs page styling
Restyle the Monitor Jobs page to match Dependency diagnostics:
- Rebuild the page on the Monitor frame (fixed-height page; only the
jobs table scrolls, with a sticky header)
- Render the scheduler info in a Code block instead of a bare pre
inside a settings card
- Rebuild the jobs table on the TreeTable data-grid (skeleton loading,
empty state, a11y label, keyboard row activation); row click still
opens the job triggers modal
- Move JobInfoApp into a folder and add unit coverage; add
TaskInfo/Job API mocks and a task-info server mock
* Make the Monitor sidebar outlet resizable
Move the resizing behavior from the Dependency diagnostics sidebar into
the shared Monitor Sidebar so every Monitor view gets it without
duplication:
- Relocate SidebarResizableBox from metabase-enterprise/monitor into
MonitorLayout and wrap the Sidebar portal content with it
- Sidebar API: optional resizable (default true) with containerWidth
required while resizable, optional defaultWidth (512 by default);
consumers measure their content area with useElementSize and pass it
- Protect the Monitor content area from text selection while dragging
inside Sidebar instead of per-view
- Simplify DiagnosticsSidebar/DependencyDiagnostics accordingly and
drop the now-empty metabase-enterprise/monitor/components tree
- Make the Alerts management detail sidebar resizable (fills the
resizable box, keeps its 560px default width)
* GDGT-2712: Jobs page styling fixes
* Replace the job triggers modal with a Monitor sidebar
Clicking a job on the Jobs page now opens the job's triggers in the
resizable Monitor sidebar outlet instead of a modal:
- New JobTriggersSidebar renders each trigger as a vertical key/value
block (all fields the modal table had), with a divider between
triggers and a close action that returns to /monitor/jobs
- The /monitor/jobs/:jobKey route stays, so deep links and back/forward
keep working; JobInfoApp reads the jobKey param and renders the
sidebar, measuring its content area for the resize constraint
- Delete JobTriggersModal; add a Trigger API mock
- Give the scheduler info Code block the old card padding and keep it
from shrinking
* Apply cross-review fixes for the jobs sidebar work
- Render trigger details as a dl/dt/dd list for proper label/value
semantics (no visual change)
- Cover the shared Sidebar resize side effect (user-select class on the
Monitor container across drag start/stop/unmount)
- Cover keyboard row activation, all trigger attributes with null
placeholders and the may-fire-again 'No' branch, and the no-triggers
empty state
* GDGT-2712: Cleanup
* GDGT-2712: Add distinct error message when job is not found
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* Migrate JobInfoApp to the metabase/router facade
* Disable text selection on body while resizing the sidebar
Address review comment: dragging the resize handle otherwise causes
the browser to continuously select/deselect page text, since the
sidebar is portaled and doesn't share a DOM ancestor with the main
content to scope a CSS class to.
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* GDGT-2711: Update Logs page styling (#77160)
* GDGT-2711: Extract LogsViewer to common Monitor components
* GDGT-2711: Update Logs page styling
* GDGT-2711: Cleanup
* GDGT-2711: Address PR comments
* GDGT-2711: Address PR review comments
Rename LogsViewer's process prop to processUUID, drop the unused
logs-content id, and remove a few comments flagged as redundant.
* GDGT-2661: Update Erroring questions page (styling + refactoring) (#77167)
* GDGT-2661: Restyle Erroring questions page
Rewrite the EE /monitor/errors page off the legacy audit_app viz onto the
Monitor recipe used by the other Monitor tools pages, keeping the same
backend audit query:
- TreeTable with the 11 audit columns, 2-state sortable headers, and native
row selection; "Rerun Selected" moved to the standard bottom BulkActionBar
- Dependency-diagnostics-style debounced filter inputs
- Page no longer scrolls; the table body is the sole scroll region
- Pagination outside the table with a total count (new bad-table-count audit
query), hidden until the data has loaded, and synced to a ?page URL param
- Requests abort the previous in-flight one on paging/sorting/filtering
Replaces the AuditParameters/AuditTable/ErrorMode stack; deletes the old
ErrorOverview and its click-mode.
* Remove dead legacy audit-table v1 code
The Erroring questions page was the last consumer of the Audit v1 table
stack. With it rebuilt on TreeTable, these are unreferenced and removable:
AuditTable, QuestionLoadAndDisplay, AuditTableVisualization (the last
"audit-table" display), AuditParameters, the bad_table card def, and
AuditMode. The rest of audit_app (unsubscribe UI, InsightsLink,
metabot-analytics) is untouched.
* Fix duplicate/incorrect cards in the erroring-questions audit query
The latest_qe CTE joined query_execution to the per-card MAX(started_at)
subquery on started_at alone, so executions across cards that shared a
timestamp fanned out — surfacing duplicate rows (and cards whose latest run
didn't actually error). Join on card_id as well. Regression test covers a
shared-timestamp collision.
* GDGT-2661: Address review feedback
- Surface count-query failures in the page error state so a failing count
can't silently hide pagination
- Filter bar reflows (wrap) instead of clipping at narrow widths / zoom
- Skeleton renders the checkbox column to avoid a layout shift on load
- Centralize the internal-dataset-query cast to a single boundary
- Add tests: count db/collection filter clauses (backend), 202-body
data.error path, real filter-reset assertion, rerun failure recovery
* GDGT-2661: Fold pagination total into the rows query via COUNT(*) OVER()
The page ran two /api/dataset requests per view — the bad-table rows and a
separate bad-table-count — each recomputing the expensive latest_qe scan over
all query executions. Add a COUNT(*) OVER () total_count column to bad-table so
the full filtered total rides on the rows response, and drop the count query.
Halves the round-trips and eliminates the second latest_qe scan (~2.1s -> ~1.4s
of DB work on the Stats dump); the window count adds no measurable overhead
since it runs over the already-materialized result set before LIMIT.
* Migrate ErrorOverview to the metabase/router facade
* GDGT-2661: Return erroring-questions total_count at the response root
The bad-table query computes COUNT(*) OVER () as a column, which repeated the
total on every row. Strip that column via an xform and surface the value once
as a top-level total_count on the response; the FE reads it from there.
* GDGT-2661: Non-blocking per-row rerun with loader in erroring questions
Rerun now runs in the background per card instead of blocking on the whole
batch: clicking Rerun Selected closes the bulk bar immediately and fires each
request independently, so more questions can be queued while slow ones run.
A rerunning row shows a loader in place of its checkbox, is excluded from
Select all, and its loader clears when the request settles; the list refetches
once the last rerun finishes.
TreeTable gains a generic isRowLoading prop and makes the whole checkbox cell
a click target so a near-miss no longer navigates to the row.
Also hoist total_count onto the Dataset type and drop the local helper.
* Extract shared generic sorting helpers for monitor tables
The monitor tables (erroring questions, tasks, dependency diagnostics) each
duplicated getSortingState/getSorting/isSortColumn. Extract them into a generic
metabase/utils/sorting module keyed by the column type. getSorting is overloaded
so the un-sort behaviour is type-driven: pass the current sort for always-sorted
tables (direction flips) or omit it to allow an unsorted result.
Refactor the three monitor tables onto the shared helpers.
* Update erroring questions E2E selectors for the new table
Point the tests at the TreeTable-based ErrorOverview: erroring-questions-table
container, erroring-question rows, and the always-enabled filters / bulk bar
that closes on rerun instead of a disabled button.
* Bind *driver* when reducing audit internal queries
The MySQL timestamp reader calls results-timezone-id, whose driver arg is
malli-validated as a keyword in tests. reduce-results* bound the timezone
override but not *driver*, so it was nil and audit queries returning timestamps
errored on MySQL/MariaDB. Bind *driver* to the app-db driver alongside the
override.
* GDGT-2661: Address PR comments
* GDGT-2661: Merge erroring questions input into one, extend to support name search
* GDGT-2661: Fix checkbox cell width in rows
* GDGT-2661: Fix e2e test
* GDGT-2661: Address PR comments
* GDGT-2710: Update Model cache log page styling (#77277)
* GDGT-2713: Update Model cache log page styling
Restyle to match Dependency diagnostics: the page no longer scrolls
vertically, only the table does. Replace the hand-rolled AdminS.ContentTable
with a metabase/ui TreeTable (skeleton while loading, empty state, refresh
action), and drop the emotion-styled ModelCacheRefreshJobs.styled.tsx in
favor of a CSS module.
Model and Collection columns are the two stretching columns (fill remaining
width) and support client-side sorting, since the underlying /api/persist
endpoint has no sort param.
* Unify monitor tables on 3-state sortable headers
Erroring questions and Tasks forced a 2-state flip-forever cycle by passing
the current sort back into getSorting(); drop that so a 3rd click clears the
header's sort arrow and falls back to the page's default sort, matching
Dependency diagnostics and Notifications.
The default-sorted column on each page needs sortDescFirst matching its
default direction (desc), otherwise the very first click on that column is a
no-op: TanStack's per-column cycle treats desc as the removable/last state
when sortDescFirst is false, so a column that starts at desc goes straight to
removed on click one.
* Make all applicable columns sortable in Model cache log and Jobs
Both are fully-loaded, unpaginated lists, so enabling client-side sorting on
the remaining columns is safe. Model cache log's action column stays
unsortable (no header text or data). Tasks and Task runs are left as-is:
their backends only support a fixed set of sort columns (or none), and
sorting the rest client-side would silently reorder only the current page.
* GDGT-2710: Address PR comments
* GDGT-2709: Update Alerts management page styling (#77364)
* GDGT-2709: Update Alerts management page styling
Restyle to match Dependency diagnostics: the page no longer scrolls
vertically, only the table does. Move pagination outside the table into a
plain footer and show the total result count via PaginationControls'
showTotal.
* Restrict Alerts management to admins only
The /api/notification/admin* endpoints are superuser-gated on the backend,
but the frontend route and nav link were reachable by anyone with the
broader can_access_monitoring permission (shared with the other Monitor
Tools pages), so a non-admin holding it could reach the page while every
API call inside it 403'd. Add a dedicated canAccessAlertsManagement
selector and route guard so the route/nav link match the backend's
admin-only restriction.
* Fix misleading test name in MonitorLayout spec
The test asserts the link is hidden for a non-admin analyst, not that it's
shown for an admin.
* GDGT-2794: Update Task/Run details page styling
Extract the "Back to Tasks"/"Back to Runs" breadcrumb into a shared
MonitorBackLink component, styled and positioned like other Monitor
page headers. Wrap both detail pages in the same non-scrolling shell
used elsewhere in Monitor so only the content area scrolls internally,
instead of the whole page scrolling behind the floating AppSwitcher.
* Extract MonitorPageContent to replace SettingsSection in Monitor
SettingsSection carries admin-page title/description logic that Monitor
pages don't use. MonitorPageContent keeps the same card styling with none
of that logic, dropping the last two /monitor call sites still importing
from metabase/admin.
* GDGT-2709: Fix layout in Tasks Details page
* GDGT-2709: Refactor NotificationsTable row selection logic. Fix selection with spacebar
* GDGT-2791: Abort superseded in-flight list requests in Monitor (#77694)
* GDGT-2791: Abort superseded in-flight list requests in Monitor
Changing page/sort/filter on a Monitor list page left the previous, often
expensive, list request running to completion on the server. Subscribed RTK
useXQuery hooks only abort when their last subscriber unmounts, so introduce
a shared useAbortableQuery hook built on lazy triggers (whose promise exposes
.abort()) that cancels the superseded request on arg change, unmount, and
skip transitions, and suppresses the abort error so cancellations never flash
the error view.
Caching mirrors useQuery: cache-first by default, refetchOnMountOrArgChange
to opt into always-fetch; manual refetch() always hits the network. Every
converted call site keeps its pre-migration caching semantics.
Converted: TaskListPage, TaskRunsPage, TaskRunEntityPicker,
ModelCacheRefreshJobs, NotificationsAdminPage (main list),
DependencyDiagnostics, BrokenDependentsSection, and ErrorOverview (its
one-line useAbortableAdhocQuery wrapper is inlined and hooks.ts deleted).
* GDGT-2791: Cleanup
* GDGT-2791: Fix type narrowing in TaskRunEntityPicker
* GDGT-2791: Clamp pagination range to total
* GDGT-2791: Test that tag invalidation does not refetch superseded args
* GDGT-2791: Scope query aborts per hook instance via injected cache key
* GDGT-2791: Migrate _refetchDeps to the shared __rtkCacheKey mechanism
* GDGT-2791: Refine baseQuery typings. Cleanup
* GDGT-2791: Replace interface in params declarations with type. Refine BaseQuery params type
* GDGT-2832: Group Monitor navbar items into sub-menus (#77706)
* GDGT-2832: Move AreaContent to AreaLayout and normalize Monitor page spacing
* GDGT-2832: Group Monitor navbar items into sub-menus
* GDGT-2832: Cleanup. Fix AreaTab outline overflow
* GDGT-2832: Match Alerts Management tab spacing to Dependency Diagnostics
* GDGT-2832: Replace collapsible navbar groups with static labelled sections
* GDGT-2832: Update Monitor views headings to match navbar. Fix e2e test
* Monitor section design tweaks (#78007)
* icon and spacing updates
* remove info icon from background tasks page heading
* improve empty states
* GDGT-2810: Enable sorting in all Monitor views (#77842)
* GDGT-2810: Enable sorting on all Background tasks and Task runs columns
* GDGT-2810: Remove bold styling from first Monitor table columns
* GDGT-2810: Fix reflection lint warnings in task history API test
* GDGT-2810: Qualify task_run/task_history filter columns
The entity_name and task_count sorts add joins (report_card and
report_dashboard both have their own entity_id column), so unqualified
filter columns become ambiguous: /api/task/runs?entity-id=N&sort_column=entity_name
returned a 500. Qualify the filter columns and add a regression test
combining filters with the derived-column sorts.
* GDGT-2810: Cleanup
* GDGT-2810: Use kebab-case sort params on the task runs endpoint
* GDGT-2810: Extract shared useSortingStateChange hook for TreeTable sorting
* GDGT-2810: Extract shared page/sort URL param parsers
* GDGT-2810: Fix e2e tests
---------
Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>
* GDGT-2843: Integrate Monitor upsell pages (#77948)
* GDGT-2843: Migrate Monitor upsell pages into monitor scope, link to /upgrade/monitor
* GDGT-2843: Tighten monitor module boundaries to standard shared tier
* GDGT-2843: Change Monitor app icon from gauge to pulse
* GDGT-2843: Update pulse icon and Monitor logo
* GDGT-2843: Address PR comments
* GDGT-2843: Fix e2e tests
* Fix Monitor code review findings (#78106)
* Fix Monitor code review findings
* Remove obsolete dependencies redirects
* Monitor review: Cleanup
* Address review comments
* Cleanup
* Remove Dependency diagnostics link from Data Studio nav
It already lives under Monitor (/monitor/dependency-diagnostics); the
Data Studio route just redirects there now.
---------
Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
Co-authored-by: YB Tan <tybmeta@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Maz Ameli <maz@metabase.com>
Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>
* WIP: start flipping unit-test harness to v7
Default routerEngine to v7 and render bare (non-Route) children under a
catch-all route so v7's <Routes> renders them. role=link nav failures still
to diagnose. Suite not yet green.
* Give v7 tests a history handle and drop DOM-wrapped route config
The harness now owns the v7 memory history and hands specs a v3-shaped adapter
(getCurrentLocation/push/replace/goBack/listen), so the ~42 specs that drive and
assert navigation through the returned history keep working on v7.
Route config wrapped in a <div> is a v3 artifact that v7's <Routes> cannot read;
swap those two for fragments.
* Pin the v3-vs-v7 conformance suites to the v3 engine
They exist to prove the facade on v3 matches real v7 and are built from raw v3
route config, so they must not follow the harness default now that it is v7.
* Resolve a relative initial route in the v7 test history
* Buffer navigations dispatched before the v7 router registers
* Carry route props onto the v7 matched-route stubs
* Fix the v48 /browse/:dbId-:slug legacy redirect on v7
* Render facade routes in renderHookWithProviders on v7
* Migrate unit specs off v3-only routing behavior for the v7 flip
* Drop buffered v7 navigations when the router unmounts
* Default use-v7-router to true
* Re-home history@3 types onto the metabase/router facade
* Format the re-homed import blocks
* Merge duplicate router imports and drop unused lint directives
* Re-home the router-listen spec's history Location type
Wait for the DebouncedFrame to settle via ensureChartIsActive() before
clicking a group tile to drill in. While the frame transitions it carries
pointer-events: none, so the drill click was silently swallowed and the
breadcrumb button never rendered.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Migrate AuditTableVisualization to a typed function component
* Fix Erroring Questions column sorting by forwarding sorting props through Visualization
* Extract typed viz definition to drop casts per review
* GHY-4016: Replace Field YAMLs with FieldUserSettings in git sync (PR 1/3)
Git sync currently exports one Field YAML per database column. For a
typical customer this is ~6396 files — 84% of the entire repo. Most of
that data (active, database_type, position, dimensions, ...) is sync
metadata that git sync has no business tracking; the user-curated part
lives in FieldUserSettings.
This PR switches git sync to export FieldUserSettings instead of Fields.
Only fields with user-curated metadata get a YAML at all.
Measured against the stats-remote-sync repo (196 published tables,
~20 columns each):
Import: 43s → 14s (3x faster, 7659 → 1263 files)
Export: 6.3s → 2.0s (3x faster, 2155 → 239 entities)
Files: 7659 → 1263 (84% reduction)
For a large customer (563k files, 522k Fields), ingest time alone drops
from 116s to 7s.
Full Field serdes is unchanged — non-git-sync export/import is
unaffected. FieldUserSettings already had complete serdes support
(generate-path, storage-path, load-find-local, make-spec); this PR
wires it into the git sync paths.
Changes:
- `serdes/descendants "Table"`: branches on `:user-edits-only` opts.
When set, emits `["FieldUserSettings" field_id]` only for fields with
a FUS row; otherwise emits `["Field" field_id]` as before.
- `spec.clj`: named `git-sync-extract-opts` constant (with
`:user-edits-only true`); public `pk-col` helper for the non-standard
FieldUserSettings PK (`:field_id` not `:id`).
- `extract.clj` (OSS): local `pk-col` for `:where [:in pk-col ids]`
(module boundary prevents depending on EE spec).
- `impl.clj`: `extract-chunk` uses `spec/pk-col` for FieldUserSettings.
- `models.clj`: adds `"FieldUserSettings"` to `data-model-in-collection`.
- `events.clj`: removes `:model/Field` from `register-events-for-spec!`
so field-update events no longer create Field RSOs. A dedicated
`::field-update-event` handler creates FieldUserSettings RSOs only
when a FUS row exists. This completes the incremental path: full
export uses `descendants` + `:user-edits-only`; incremental export
uses FUS RSO rows.
Tests assert on the full RSO table so stray Field RSOs fail immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Address PR review: defensive get for dynamic pk-col, fix content-hash test for FieldUserSettings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* don't need maps, k/v pairs work
* Declare :model/FieldUserSettings module boundary export/import
remote-sync references :model/FieldUserSettings in events.clj, so it must
be exported by warehouse-schema and imported by enterprise/remote-sync.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Remove unused f1-id binding in extract test (kondo)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Test that Field and FieldUserSettings are serdes descendants leaf nodes
Cements the design discussed in review: Table's descendants method is the
only source of field-level entries, and the :user-edits-only branch is
exclusive — you get Field refs or FieldUserSettings refs, never both via
a transitive Field -> FieldUserSettings edge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* WIP: dag run BE
* split into new table
* run utils
* remove dag changes on job runs
* new dag run model
* abstract over parent-run
* separate dag model/api
* factor common job/dag code in a coordinated-run primitive
* fixes
* dag test
* fix migration
* kondo
* missing resolution
* exclude model
* kondo config
* fix test
* fix: exclude for deser
* fixes
* fix: exclude for deser
* fix: missing fk
* random ids
* chore: rename
* feat: add unioned runs
* remove dead endpoints
* api tests
* extend filter semantics
* trim docstrings
* kondo
* gap: allow canceling job runs
* share code
* fix: job start should return proper job id
* fix: mig to 64
* simplify
* simplify further
* simplify
* dead code
* add unique constraint on dag runs
* tighten schemas
* tighten permissions
* fix test
* relax schema
* fix: set null on delete
* handle deleted jobs/transforms for dag runs
* appease the sql linter
* fix test
* simplify rollbacks
* simplify callsite
* fix tests from bad rebase
* fix: fallback when transform is deleted
* add transform_count
* fix tests
* log with context
* varchar 32 for status
* Add UI for running transform with upstream or downstream (#77367)
* GDGT-2625 wip: reprocess DAG UI for transforms
- Split "Run now" button (upstream/downstream) + confirmation modal
- runDag / dag-transforms / cancel-dag API + analytics event
- Tabbed Runs page: transform-graph runs (default) + individual transform runs
- Graph runs table (Run/Type cols), filter bar (Type/Transform/Started/Status),
sidebar with member transforms; shapes aligned to bronsa's /api/transform/runs
- List + members still served from a mock (transform-graph-runs.mock) via queryFn
* GDGT-2625 wip: align graph runs to /api/transform/runs, sidebar + cancel
- Reshape TransformGraphRun to backend RunSummaryResponse (run_type/entity_id/name)
- Filters mirror Transform runs: Type/Transform/Started/Ended/Status/Trigger
- Sidebar: title = Run name, Type as first metadata row, cancel section below
metadata (dag/job/transform), confirm modal; job-run cancel endpoint wired
- Mock covers job/dag/transform in all states (succeeded/started/failed/timeout)
- Reorder columns: Status/Trigger after Duration
* GDGT-2625 wip: sidebar header entity links
Add "View this job" / "View this transform" (+ dependency graph for
transforms) action links to the graph-run sidebar header, mirroring the
individual transform-runs sidebar.
* GDGT-2625 wip: migrate graph runs to real /api/transform/runs
- Swap list + member queryFn mocks for real queries (list endpoint, dag/job
member drill-down; standalone runs synthesized). Delete the mock fixture.
- Rewrite page spec against real endpoints via fetch-mock + new mock factories.
- Reword DAG run confirm modal (drop "in this order", bullet list) since members
can run in parallel.
* Adjust wording
* GDGT-2625 wip: scheduled state + cleanup
- Show a "scheduled" run state on the transform page after a DAG reprocess is
triggered, so the queued transform gives feedback instead of looking idle
- Drop dead DAG-run types/tag-providers/mock helpers left from earlier iterations
* Address code-review findings for reprocess DAG UI
Correctness:
- Migrate transforms.cy.spec.ts to /runs/individual (the /runs route now
renders the graph-runs page, not the individual run table)
- Gate the "Run all" DAG confirm button on the mutation, preventing a
double-click from triggering two DAG runs
- Toast when run-dag returns a null dag_run_id instead of silently no-op
- Stop the "Scheduled" banner from polling forever when the member-runs
request errors
- Keep the graph-run sidebar open when the selected run scrolls off the
current page during polling
- Invalidate transform state on cancelDagRun/cancelJobRun so member
transforms' last_run refreshes after a cancel
Cleanup:
- Translate whole phrases (with context) for DAG run names
- Replace a typecast with a type guard in TypeFilterWidget
- Memoize parsed params and filter options on the graph-runs page
- Drop the unused direction field from the scheduled-DAG-run state
- Convert the sidebar's IIFE switch to ts-pattern match
* Handle deleted entities in graph runs; fix run-list cache invalidation
- Graph runs table: grey a deleted job/transform's run name (italic +
"has been deleted" tooltip), keeping the row clickable
- Sidebar: keep showing member transforms for deleted DAG and standalone
runs (fetched by dag-run id / synthesized from the run); hide the section
only for deleted job runs, whose members can't be fetched without the
(now-null) job id
- api/transform.ts: invalidate the run lists on deleteTransform (a
transform's runs survive its deletion) and cancelCurrentTransformRun, so
the Runs pages reflect the change instead of relying on polling
- Trim narrating comments from the transform API types
* Cleanup
* Fix transforms cancel e2e: click renamed "Cancel run" confirm button
The transform-page cancel confirmation button was renamed from "Yes" to
"Cancel run", but two cancelation tests still clicked "Yes". The cancel was
never confirmed, so the slow (pg_sleep) transform kept running and held a lock
on the writable DB, hanging the next test's resetWritableDb and failing the
suite. Point those two clicks at the new label (the individual-runs-page cancel
still uses "Yes" and is left unchanged).
* Avoid type cast in RunSection spec to satisfy no-unjustified-type-casts
Replace `body as string` with a typeof guard when reading the run-dag request
body, which the new lint rule flags.
* Show dependency count in the graph-run label
Use bronsa's new transform_count to label DAG runs as
"{n} dependencies › {name}" (upstream) / "{name} › {n} dependents"
(downstream), n = transform_count - 1, joined by a small chevron icon.
Bold the job/transform name; fall back to the plain name when the count is
absent (job/standalone runs, or DAG runs predating the column).
* Replace runs tabs with Detailed view switch; polish graph-run name rendering
- Swap the two runs tabs for a Detailed view switch that toggles grouped
(/runs) and detailed (/runs/individual) views in place; add RunsPage shell
and DetailedViewSwitch, remove RunTabs
- Render the sidebar header run name like the table (dimmed deps count +
chevron), with wider gap and aligned font size/weight
- Keep the Detailed view switch label on one line
- Bump graph-runs page size to 25 to match the individual runs page
- Fix stale react-router-redux import
* Preserve shared filters across Detailed-view toggle; drop deprecated CSS util
Toggling the Detailed-view switch previously called the run-list URL helpers
with no arguments, discarding the active filters. Forward the params common to
both views (statuses, transform ids, start/end time, run methods) via a new
CommonRunListParams/pickCommonRunListParams helper; page and sort are dropped
since the two views paginate differently and use incompatible sort columns.
Also replace the deprecated CS.overflowHidden global utility class in the new
graph-run table with an inline overflow style prop.
* Track Detailed-view toggle in transform runs analytics
Emit a transform_runs_view_toggled simple event from the DetailedViewSwitch
toggle handler with the resulting view mode (detailed/grouped), so both the
grouped and individual run-list call sites are covered.
* Fix reprocess run status: don't re-show Scheduled after a member completes
- Derive the DAG scheduled state from this transform's own member run:
a member run row only exists once the transform starts, so its presence
means the transform already ran this cycle. Prevents isScheduled from
re-arming (and masking the completed status) after this transform's run
finishes while sibling DAG members are still running
- Keep the graph-run sidebar polling while the top-level run is canceling,
mirroring isPollingNeeded
* Cleanup
* Address PR review nits from Uladzimir
- Extract isActiveRunStatus helper and use it for the duplicated
started/canceling checks (RunButton, both isPollingNeeded, sidebar
shouldPoll, RunSection dagFinished)
- DetailedViewSwitch: replace styles prop with a CSS module class
- Convert renderRunName/renderRunEntityName to RunName/RunEntityName
components
- Drop redundant String() around an already-string cell value
- Cover RunButton onRun click and the run-options menu show/hide
* Adapt runs routes to the react-router migration on the rebased base
The base migrated transforms routes to element=/withRouteProps with
Outlet-based layouts. Make RunsPage an Outlet layout and render the runs
list pages via element in the specs.
* wip
---------
Co-authored-by: Oleh K <oleh@metabase.com>
* Treat a v7 Link with an empty `to` as a button, not a navigation
* Make a v7 replace to the current URL a no-op so URL-sync effects settle
* Provide router.listen on the v7 engine and keep the router shim stable
* Serialize a v3 location query into the search string on v7
* Fix lint and type issues in the v7 router shim
* Keep the v7 matched-route branch stable across renders
* Republish the v7 splat param under v3's `splat` name
* Enable the react-router v7 engine by default
* Commit the v7 location update synchronously instead of in a transition
* Mirror the v7 location into redux from the history subscription
* Format the v7 router provider
* Sort query keys when serializing a v3 location query
* Match v3 query encoding and resolve bare link paths against the root
* Keep v3 query encoding and leave absolute link urls alone
* Always push when a v7 link targets the current url
* conversation list endpoint
* add rtkquery endpoints
* generate conversation title
* better title generation logic, poll for title if not recieved before end of first message
* render conversation title in ui and show conversation list
* allow resuming historical conversations
* show exploration conversation history
* refresh current conversation ui for 409 case
* surface conversation titles in usage analytics
* add /metabot/conversation/:id route
* remove pending title data part, assume from lack of title data part we need to poll, fix loading a conversation that is mid-response
* title backfill job
* rename title event
* FE prevent polling multiple times for conversation title
* improve title generation prompt
* ci fixes
* automated review feedback
* e2e test fixes
* reduce diff size
* self-review
* Address PR review feedback
- api: pass profile-id through valid-usage-profile-id when ensuring a
conversation title, matching the backfill job
- title backfill: gate readiness on check-usage-limits! (instance-level)
so the job pauses and reschedules cleanly when a usage limit is hit
- title backfill: bound the title future .get with a 60s timeout so a
stuck generation can't block the backfill loop indefinitely
* Add Metabot option to the Transforms New menu (GDGT-1773)
Adds a "Metabot" entry as the first option in the Transforms "New" menu.
Clicking it opens the data-studio Metabot sidebar with a pre-seeded
"Create a transform that " prompt (not submitted), gated on Metabot access.
Also fixes the tiptap prompt input so a programmatically seeded prompt
places the caret at the end instead of the start (autofocus "end" plus a
caret-to-end after external setContent), which also improves the retry path.
* Cleanup
* Cleanup unnecessary code
* Test CreateTransformMenu Metabot item without mocking
Render the real CreateTransformMenu + Metabot via the shared metabot test
setup so the menu item, permission gating, and prompt seeding are exercised
end to end instead of through stubbed hooks.
Bump babashka postgresql pod to 0.1.6 so mage runs on Apple Silicon
The 0.1.0 pod manifest has no macos-aarch64 entry -- only macos-amd64.
Babashka downloads that x86_64 artifact and caches it under the aarch64 path
anyway, so on an Apple Silicon machine without Rosetta every mage command dies
before doing any work:
Cannot run program ".../mac_os_x/aarch64/pod-babashka-postgresql":
Exec failed, error: 86 (Bad CPU type in executable)
Clearing the pod cache does not help -- the re-download is the same x86_64
binary. 0.1.5 is the first release with a real macos-aarch64 artifact; this
bumps to the current 0.1.6, which resolves to a native arm64 executable.
0.1.6 keeps every platform 0.1.0 shipped (macos-amd64, linux-amd64,
windows-amd64) and adds macos-aarch64 and linux-aarch64, so no one loses
coverage. `pg/execute!` is the only pod fn used anywhere in the repo, and its
signature is unchanged.
Verified on arm64: the cached binary is now `Mach-O 64-bit executable arm64`,
`mage.bot.setup-worktree` loads, and `./bin/mage fix-kondo-ratchets` runs to
completion.
Claude-Session: https://claude.ai/code/session_01X5dVcVX59aLonqqsYsqU65
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Restore navigation blocking on the v7 engine via a blocking history
* Render the v7 Link on the v7 engine so links navigate in modals
* Scope v7 leave hooks to their route so within-route navigation is allowed
* Don't let a v7 Link with no destination navigate on click
* Type the app Link's `to` as optional to match the runtime contract
* Apply activeClassName on the v7 engine via NavLink
* Hide settings tab in TagEditorSidebar if there is no tags
* Don't crash query metadata fetch on un-normalizable queries
batch-fetch-query-metadata filtered empty queries before normalization,
but normalize-query returns {} when a query can't be normalized (e.g. a
native query with an incomplete field-filter/table template tag). That {}
then failed batch-fetch-query-metadata*'s strict ::query schema, 400ing the
whole request and tripping the query editor error boundary. Filter empties
after normalization too so an unnormalizable query just yields no metadata.
* Hide template-tag 'Try it' examples in transforms editor
The template-tag help sidebar's 'Try it' buttons load an example query
against the Sample Database. Transforms can't target the Sample Database,
so clicking 'Try it' switched the transform to an invalid database and left
the editor in a broken state. Add a canUseSampleDatabase QueryEditor option
(default on) that transforms set to false, which withholds the sample
database id so TagEditorHelp hides the 'Try it' buttons.
* Add regression tests for transform variables sidebar fixes
- batch-fetch-query-metadata skips queries that normalize to {} instead of
400ing the whole request (the P1 crash)
- TagEditorHelp hides the 'Try it' examples when no sample database is available
- TagEditorSidebar hides the Settings/Help tabs until the query has a variable
* Address PR review feedback
- Simplify batch-fetch-query-metadata with (keep (comp not-empty normalize-query))
- Replace the hidden-tabs settings behavior with an empty state in the Settings
tab (no more layout shift / auto-switch to Help); use the shared EmptyState
- Make sampleDatabaseId a required, DatabaseId-typed setup option
- Add a NativeQuerySidebar test asserting 'Try it' is hidden for transforms
(canUseSampleDatabase=false) and shown otherwise
- Re-add the getEditorOptions canUseSampleDatabase assertion
* split use-metabase-query.unit.spec.tsx into multiple files
* split test fixtures from setup.tsx
* move useMetabaseQuery tests into tests folder
* rename useMetabaseQuery hook test
* remove unused query test fixture fields
* rename mock dataset query helper
* rename bundle exports
* chore: tiny formatting changes
* test: rewrite stale query results test
* test: extract public api tests to type and runtime tests
* Convert the Table visualization from a class to a function component
* Stabilize the Table question so async metadata updates don't rebuild columns mid-hover
* Type useSyncedQuestion via VisualizationProps so the SDK d.ts doesn't leak internal types
* Author route paths in v7 syntax, shim to v3 in the facade
* Add use-v7-router flag and frontend engine reader
* Add flag-gated react-router v7 engine (provider, tree mapper, bridge)
* Re-point redux navigation and location to the v7 router
* Support both router engines in renderWithProviders
* Switch the app mount to the v7 router behind the flag
* Point setRouteLeaveHook no-op comment at DEV-2374
* Revoke schema-level grants when destroying Redshift workspace isolation
destroy-workspace-isolation! discovered schemas to revoke via
svv_relation_privileges, which only surfaces relation-level grants: a granted
input schema with no tables (or whose tables were dropped after provisioning)
kept its schema-level USAGE grant, and DROP USER then failed with 'user ...
cannot be dropped because the user has a privilege on some object'. Union in
svv_schema_privileges so schema-only grants are revoked too.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rename workspace+det to workspace in the new destroy test
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Run Redshift workspace teardown statements individually, not as a JDBC batch
A failure now surfaces as the plain server error instead of the
BatchUpdateException wrapper ('Batch entry N ... Call getNextException to see
other errors in the batch.'); the transaction still aborts as a whole on the
first error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Revert "Run Redshift workspace teardown statements individually, not as a JDBC batch"
This reverts commit 4bf2d241b5.
* Unwrap BatchUpdateException in Redshift workspace teardown
Keep the batch (there can be many granted schemas, two revokes each) but
rethrow .getNextException so failures read as the plain server error instead
of 'Batch entry N ... Call getNextException to see other errors in the batch.'
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* remove comment
* Unwrap BatchUpdateException across all workspace-isolation batch sites
New driver.u/batch-exception util returns the per-statement exception hiding
under .getNextException (or the throwable unchanged). Applied to every
workspace init/grant/destroy executeBatch site (h2, mysql, postgres,
clickhouse, redshift) — init sites unwrap before scrub-exceptions — so
failures read as the plain server error instead of 'Batch entry N ... Call
getNextException to see other errors in the batch.'
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Keep the batch exception on init-workspace-isolation! failures
The init statements carry secrets and pgjdbc echoes the aborted statement in
the BatchUpdateException message — scrub-exceptions redacts it, and the
GHY-3660 test (init-workspace-isolation-never-exposes-password-test) relies on
that redacted CREATE USER echo as proof the run reached the statement.
Unwrapping to the inner server error discarded the echo. grant/destroy keep
the unwrap: their statements carry no secrets and the plain server error is
the better message there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Unwrap the batch envelope on init-workspace-isolation! too
All workspace batch sites now rethrow the underlying per-statement exception;
init scrubs the unwrapped exception so passwords stay out of the message. The
GHY-3660 test now proves the flow reached CREATE USER by asserting the
server's permission error instead of the redacted batch echo (which no longer
exists).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Test batch-envelope removal for grant! and password scrubbing after unwrap
- driver-level: a failed grant-workspace-read-access! must not surface a
BatchUpdateException (h2 runs locally; pg/mysql/clickhouse in driver CI;
non-batching impls pass trivially)
- unit: scrub-exceptions composed with batch-exception redacts the password
in the unwrapped per-statement message
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Batch init/grant/destroy on every SQL workspace driver
Redshift grant, and all three Snowflake / SQL Server workspace methods, now
run their statements as JDBC batches like the other drivers — one round trip,
per-statement exceptions unwrapped via driver.u/batch-exception (init unwraps
before scrub-exceptions). Redshift grants commit before grant! returns, so a
subsequent describe-database from another connection still sees the
privileges; the destroy pre-loop for foreign-grantor default-priv rows stays
per-statement on purpose (each must survive the others failing).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fold Redshift foreign-grantor revokes into the destroy batch
The per-statement autocommit pre-loop is gone: one transactional batch now
covers the FOR USER default-priv revokes, relation/schema revokes, and the
drops. A stale discovery row fails the whole destroy, which is fine — the
workspace layer records the failure and the deprovision retry re-discovers
fresh state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Escape workspace passwords in DDL like every other interpolated value
CREATE/ALTER USER statements interpolated the generated password raw into the
SQL string on postgres, redshift, h2, clickhouse, and snowflake (mysql and
sqlserver already escaped). Escape with sql.u/escape-sql :ansi (quote
doubling, valid on all five engines) and scrub both the raw and escaped forms
from failure messages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Unwrap batch exceptions from all JDBC connectors; fix sqlserver batch var scope
The grant-envelope test caught two real issues in driver CI:
- MariaDB's connector chains the per-statement exception via .getCause and
mssql-jdbc chains nothing at all, so batch-exception now falls back to the
cause and, failing that, rebuilds a plain SQLException with the same
message/SQLState/error code.
- mssql-jdbc executes a Statement batch in one T-SQL scope, so the two
sqlserver destroy statements both DECLAREing @sql collided; the variable
names are now unique.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Type-hint the rebuilt SQLException ctor to avoid reflection
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Return the batch exception as is when nothing is chained
mssql-jdbc's BatchUpdateException message is already the plain server error —
only pgjdbc adds the 'Batch entry N ... getNextException' wording. So the
unwrap falls back to the original exception instead of synthesizing a
SQLException, and the driver test asserts the envelope wording stays out of
the message rather than checking the exception class.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Interpolate the escaped password in Snowflake workspace user DDL
CREATE/ALTER USER spliced the raw password while every other workspace
driver uses the ANSI-escaped value; the escaped form was already
computed and scrubbed here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
A STRUCT column's INFORMATION_SCHEMA data_type spells out its entire
nested schema, and the COLUMN_FIELD_PATHS LEFT JOIN repeats it on every
nested-leaf row -- O(n^2) bytes for a column with n leaves. Dynamic-key
STRUCTs (e.g. Cloud Functions log-sink tables whose JSON payloads use
user IDs as keys) reach thousands of leaves with ~270KB type strings;
one such table produced ~4,000 rows x 271KB = 1.09GB buffered in
sync-fields!'s partition-by, OOMing the instance mid-sync.
Only short type-string prefixes are ever consumed
(raw-type->database+base-type dispatches on prefixes up to 10 chars and
stores at most RANGE<TIMESTAMP> verbatim), so SUBSTR(..., 1, 200) in the
query -- applied server-side, before the wire -- is behaviorally
identical while collapsing response size, parse cost, and the buffered
partition (1.09GB -> ~2.8MB for the table above).
The test needs no dataset or network: it captures the compiled SQL via
*process-native* and asserts both data_type columns are truncated.
* remove the last test file from the legacy structure
* convert test table into format of newer tests
* remove test for undefined since this isn't a valid type for RowValue
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>
* migrate TokenField
* fix RemappedOption type for Autocomplete ComboboxStringItem
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* address RecipientPicker review: memoize maps, share lookup/label helpers
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix single-value search: accept top match on Enter
Autocomplete only completes on Enter when an option is highlighted, but
search options load async so none is. Restore old TokenField behavior via
onKeyDown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix letters swallowing
* addressing the PR's comments
* added missing tests
* introduce Autocomplete + refactoring
* apply filter by clicking on enter
* fix test
* refactor autocomplete
* refactor autocomplete
* fix edge case + added unit tests
* fix type
* fix test
* fix test
* get rid of lint suppression + allow autoselect on open for multiautocomplete
* revert autoselect destructuring, restore eslint suppression (tests failing)
* explain exhaustive-deps suppression in MultiAutocomplete effect
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Remove withRouter from production components
* Delete the public withRouter export
* Type the route-leave hook for PlainRoute and drop the route casts
* Add element/Outlet + useRoute support to the router facade
* Convert root route tree to element/Outlet
* Convert admin route tree to element/Outlet
* Convert data-studio, models and metrics route trees to element/Outlet
* Convert transforms, account, monitor and public route trees to element/Outlet
* Convert enterprise route trees to element/Outlet and drop dead library segment routes
* Convert tenants admin route tree to element/Outlet
* Convert redirect, createEntityIdRedirect and modalRoute to element
* Ban component= on routes with eslint
* Drop component= from the facade Route type and convert remaining routes
* Drop the component= eslint rule, the facade type now enforces it
* Remove the unused withOutlet helper
* Assert the required tenantId route param instead of coercing to NaN
* Bridge element={null} to a nothing-rendering component, matching v7
* Memoize router context value so element routes re-render once per navigation
* Convert data_apps and impersonation routes merged from master to element
* initial proof-of-concept for data apps
POC 2
POC 3
POC 4
Revert "POC 4"
This reverts commit 1f62e574dc.
POC 5
POC 6
* Expose all SDK components to child realm
* Setup app publishing and serving
* Refactor distortion helpers
* Adjust skill
* Add useQuestionQuery SDK hook (#74900)
Co-authored-by: Timofei <timofei@metabase.com>
* Mock METABASE_EMBEDDING_SDK_BUNDLE for data app sdk hooks/components
* Adjust skill for TS project
* Update skill to clearly mention TS
* Add blocked APIs list to skill
* Prototype schema exports and typed hooks for custom data apps (#74978)
* add prototype for schema generation
* add formatting for generated schemas
* add skill file
* useQuestionQuery should take only ID based on @sanex3339 feedback
* add segments, measures, tables and useMetabaseQuery hook
* Add useMetabaseQuery to endowments
* Fix queryDataset
---------
Co-authored-by: Timofei <timofei@metabase.com>
* Add ability to update app bundle
* Simplify hook
* Add spinner instruction
* Mode Data Apps to OSS
* Instruct to not not use blocked event handlers
* Improve API, add styles isolation, add router components (#75418)
* Prepare skills for data app template + adjust router components (#75490)
* Fixes after rebase
* Move migration to v63
* Move migration to v63 #2
* Fix config.edn
* Fix BE tests
* Fix team name
* Fix duplicated config
* Adjust version text in skill
* Fix data app bundle uploading after legacy client removal
* Remove `actions` from config edn
* Fix failing test
* Fix failing test #2
* Add custom actions for data apps (#75627)
* Fix imports
* Fix type annotations
* Adjust migration
* Adjust permissions
* Fixed iframe security
* Data Apps publishing via remote repository POC (#75702)
* Update gitignore with data-app.html
* [PoC] Type safety and modularity improvements for custom data apps (#75381)
* allow separate libraries in schemas
* add metric type checking
* add collections parameter
* check that segments and measures belongs to table in useMetabaseQuery
* require less type information for measures
* require less type information for segments
* use metric dataset endpoint when querying measures in metrics
* add validation for breakout buckets
* fix filter operation type scoping
* expose filter type helper for nicer autocompletions
* update skill file
* add breakout schema helper for better type errors
* update breakout schema helper to accept binning and bucket
* move fields that are not used in compile and runtime to comments to shorten type errors
* enforce data truthfulness in skill
* enforce semantic layer richness in skill
* handle dimension key collisions mapped from many tables
* update skill description
* prevent hallucinated measures and breakouts
* fix field serialization
* add guidance on using filter matcher
* add skill guidance on long results and units
* accept query object in StaticQuestion and InteractiveQuestion
* extract use-metabase-query to multiple files
* update skill file to use sdk question by default
* update skill file to avoid hallucinated derived calculations
* add new functions into sandbox
* kondo and cljfmt lint fixes
* update CSP rules to allow style tags for data apps
* address review feedback on scoping and metric filters
* address code review feedback
* Add aggregations support for breakouts in query hooks (#75723)
* add aggregations support
* add additional aggregation helpers
* Add metrics support in useMetabaseQueryObject hook (#75728)
* add metric queries
* indicate metric support in skill file for useMetabaseQueryObject
* add new version for release
* fix query dataset tests
* update release workflow temporarily
* update skill description
* update skill to use more generic examples
* hotfix: allow esm usage in vite dev mode
* skill file update for table caveat
* update sandbox
* fix type errors in useMetabaseQuery
* add v0.63.2 to release workflow
* update skill file to use the 63-data-apps version
* Move data apps to the EE directory and under `data-apps` token feature (#75871)
* remove skill file alias
* Add error handling for data apps
* Add spinner while iframe is loading
* Add OSS/no token upsell banner placeholder for data apps (#75893)
* ensure a fixed height is set if container has height
* Allow including everything in data and metric library in schema endpoint (#75944)
* add filter for including library and data collections
* address review on skill file
* remove id field from rendered comment
* remove useQuestionQuery and useMetricQuery hook
* Improve env files lookup in skills (#75955)
* Allow `unsafe-eval` for data app iframe (#76004)
* Fix lint
* Fixes after rebase
* Fix duplicated test
* Improve Providers and typesafety (#75934)
* chore: reformat single expression functions to arrow
* Use entity id in questionCollections when generating typed schemas (#76230)
* allow using entity ids for question collection filter
* guide the agent towards using entity ids
* add failing test for invalid entity id
* Make custom viz work in data apps (#76237)
* Allow blob: images in data-app CSP (EMB-1923)
SDK custom visualizations load their icon assets via blob: URLs (to avoid
the cross-origin auth issue), but the /embed/data-app/ route allowed
'unsafe-eval' without the blob: scheme in img-src, so icons were blocked
under a production CSP. Add blob: to img-src scoped to the data-app iframe
route only; the main app img-src stays strict.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* EMB-1880: support for `providerProps` in data apps
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Reduce schema bloat in dimensions of generated metrics when many tables are involved (#76003)
* Add useQuestionQuery SDK hook (#74900)
Co-authored-by: Timofei <timofei@metabase.com>
* Prototype schema exports and typed hooks for custom data apps (#74978)
* add prototype for schema generation
* add formatting for generated schemas
* add skill file
* useQuestionQuery should take only ID based on @sanex3339 feedback
* add segments, measures, tables and useMetabaseQuery hook
* Add useMetabaseQuery to endowments
* Fix queryDataset
---------
Co-authored-by: Timofei <timofei@metabase.com>
* [PoC] Type safety and modularity improvements for custom data apps (#75381)
* allow separate libraries in schemas
* add metric type checking
* add collections parameter
* check that segments and measures belongs to table in useMetabaseQuery
* require less type information for measures
* require less type information for segments
* use metric dataset endpoint when querying measures in metrics
* add validation for breakout buckets
* fix filter operation type scoping
* expose filter type helper for nicer autocompletions
* update skill file
* add breakout schema helper for better type errors
* update breakout schema helper to accept binning and bucket
* move fields that are not used in compile and runtime to comments to shorten type errors
* enforce data truthfulness in skill
* enforce semantic layer richness in skill
* handle dimension key collisions mapped from many tables
* update skill description
* prevent hallucinated measures and breakouts
* fix field serialization
* add guidance on using filter matcher
* add skill guidance on long results and units
* accept query object in StaticQuestion and InteractiveQuestion
* extract use-metabase-query to multiple files
* update skill file to use sdk question by default
* update skill file to avoid hallucinated derived calculations
* add new functions into sandbox
* kondo and cljfmt lint fixes
* update CSP rules to allow style tags for data apps
* address review feedback on scoping and metric filters
* address code review feedback
* Add aggregations support for breakouts in query hooks (#75723)
* add aggregations support
* add additional aggregation helpers
* Add metrics support in useMetabaseQueryObject hook (#75728)
* add metric queries
* indicate metric support in skill file for useMetabaseQueryObject
* add new version for release
* fix query dataset tests
* remove useQuestionQuery and useMetricQuery hook
* Data Apps publishing via remote repository POC (#75702)
* avoid duplicating dimension fields
namespace dimensions by table
add dimension id to mapping
pick from table fields for only allowed dimensions
fix incorrect type guards
allow querying metric using question as source
remove createMetabaseQuery from public interface
add sourceFieldId to metric
* doc generation type fixes
---------
Co-authored-by: Timofei <timofei@metabase.com>
* Add allowed hosts handling for data apps (#76094)
* Update version
* Properly include data app changes in `pull changes` modal (#76294)
* Move app template into main repo, handle skill versioning flow (#76288)
* feat(data-apps): allow passing down a card with viz settings (#75993)
support card prop in sdk question component
* Rename data app env variables and read them only in dev mode (#76604)
* Add custom data apps layout with sliding toolbar (#76600)
* Remove unused helper
* Dev env Sandboxing, Human Readable Errors and Copy Helper (#76597)
* Revert wrong rebase changes
* Improve creating filtering interfaces in custom data apps (#76401)
* update skill files to guide filter usages
* update skill file to prevent filtering mistakes
* move filtering ui patterns into a separate file
* do not treat mb_replace_me as valid creds
* use combobox for filters
* pass title false to sdk question
* combobox trigger before typing
* Add missing exported types
* Apply visualizations and visualization settings from SDK package (#76659)
* ask the agent to pull possible visualizations
* ask the agent to use card prop
* teach the agent when to use viz settings
* Support query-only SDK cards
* Remove cross references between data apps skills (#76661)
Remove data app skill cross references
* Move Data App Dev Vite App setup into SDK package (#76695)
* Don't navigate on ad-hoc question chart clicks (#76751)
* Fix sandboxing crashes and update skill file mistakes (#76858)
Fix data app sandboxing and skill guidance
* Block form submissions in data apps via CSP (#76882)
* Fix SDK theming hover text colors in generated data apps (#77083)
update guidance on colors
* Remove SDK question query prop for data apps use cases (#77080)
* Remove SDK question query prop
* update tests to use card prop instead
* Adopt Lib.createTestQuery for table sources when querying in data apps (#75957)
* Add base metabase-lib query implementation
* switch to Lib.createTestQuery DSL
* Load query metadata at runtime for metabase-lib instead of hard-coding (#76874)
* load query metadata dynamically
* accept id-only source references
* Wait for SDK login before resolving query metadata
* refactor: use useAsyncFn to handle states
* pass source-name to createTestQuery
* Add metric aggregations for useMetabaseQuery via Lib.createTestQuery (#76947)
* add metric sources for useMetabaseQuery via Lib.createTestQuery
* use MetabaseQueryObject instead of opaque type marker
* Fix metric schema and join source validation
* add metric library filtering to skill files
* make metric an aggregation expression instead of join sources
* mark tableId as never in joined fields
* export public MetabaseQueryObject type
* use camelCase for source name and field id
* Add initial data app e2e setup (#77101)
* Add sorting and fields to useMetabaseQuery via Lib.createTestQuery (#76966)
* add sorting, limiting and fields to useMetabaseQuery
* refactor: cleanup sorting and aggregations
* Fix metric order by validation
* Support saved questions for useMetabaseQuery via Lib.createTestQuery (#77073)
* add saved questions as useMetabaseQuery input
* remove extra operators for saved cards
* Add unit fe/be tests for data apps (#76979)
* Fix lint
* Use modern api in query-dataset.ts
* Fix BG color
* Fix execute-action tests
* Update SDK-data-apps version
* Move migrations to v64 folder
* Adjust router components to use metabase/router
* Allow opening a data app for any signed user
* Rename data app skills (#77266)
* Update DataAppLayout.tsx `go back` button logic
* Harden data apps skills and action validation errors (#77261)
* harden data apps skills and actions error validation
* do not display models as questions in data apps
* clarify action result types
* Agents should report errors when models, actions or questions cannot be schema generated (#77260)
* return an error if models or saved questions are invalid
* return an error if actions are invalid
* update skill file guidance on model generation
* fix model boundary
* Add data apps admin UI + routing/sandboxing/isolation e2e (#77271)
* Rename data-apps paths to /apps (#77298)
* Include all action-included models in the typed schema when a mutation is specified (#77291)
* add includeModels=true flag to include all models
* omit model columns and models without actions
* remove extra metadata
* Adjust admin UI for Data Apps (#77420)
* Add more e2e/unit tests for Data Apps (#77524)
* Update bun.lock
* Fix errors after rebase
* Parallelize dts rollup (#77663)
* revert EMB-1840 measures support in metric dataset endpoint
* chore: add data app module (#77705)
* test ci
* apply code review suggestion
* Setup separate entries for client and node dev data app entries (#77687)
* Fix type casts lint errors (#77696)
* Adjust data apps setup + add proper analytics setup (#77604)
* Extract common and scope helpers + tests for typed schema module (#77403)
* refactor: extract common and query param helpers
* refactor: use kebab-case for query params
* refactor: cleanup schema mapping
ci: fix module config
* refactor: rename schema scope helpers to scope.clj
* refactor: extract scope_test.clj
* Extract typed schema rendering helpers to render.clj (#77703)
* extract schema render function
* move runtime compact test out
* remove javascript and json endpoints from schema generator
* refactor: cleanup javascript.clj and render.clj
* Extract typed schema generators for tables and questions (#77729)
* refactor: extract schema generator modules
* remove question-details from model-schemas
* fix: batch queries in table and question schema generator
* remove N+1 in typed schema field table lookup
* Extract typed schema generators for models and actions (#77739)
* extract model schema generator
* batch model action querying to avoid n+1 queries
* Extract typed schema generator for metrics (#77806)
* Extract typed schema generators for models and actions (#77739)
* extract model schema generator
* batch model action querying to avoid n+1 queries
* extract metric schema generator
* test: simplify fields in metric_test fixtures
* refactor: clean up metric schema generation
* remove n+1 from metric field dimension read
* disable default temporal dimension breakout
* surface errors if metric schema cannot be built
* revert metrics/api.clj to master to remove outdated comments
* Fix routes
* Take a data app's slug from its directory name (#77688)
* Merge data-apps specific migrations into one
* ci: fix failing backend test and cljfmt lint
* Add a beta badge in data apps admin page heading (#77877)
marks the data app as beta
* Revert "Merge data-apps specific migrations into one"
This reverts commit 383a5fa28d.
* Revert "Revert "Merge data-apps specific migrations into one""
This reverts commit 948d904b62.
* ci: actionlint fix
* Fix data apps route
* Use getSubpathSafeUrl in attachIframeUrlMirror
* Fix comment
* Do not send apps with sync error for token check
* Add data app listing sidebar section for non-admins (#77977)
* show data apps in sidebar for non-admins
* add app icon to data apps
* use available=true to filter no sync error and enabled apps
* fix navbar keys in data apps
* test: use non-admin user to assert non-admin response
* fix toucan query in data apps listing endpoint
* re-export entity detail helpers and result column to metabot core
* open data apps in new tab from the sidebar
* Prevent race conditions in responses of useMetabaseQuery hook (#78023)
prevent stale responses in useMetabaseQuery
* ci: remove tools.util from module declaration
* Merge migrations into a single one
* reset release-embedding-sdk.yml to master
---------
Co-authored-by: Phoomparin Mano <poom@metabase.com>
Co-authored-by: Phoomparin Mano <phoomparin@gmail.com>
Co-authored-by: Nicolò Pretto <info@npretto.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Some cleanup of some false alarms I encountered in test environments while working on a different issue. Destination databases (those with `router_database_id` set) are reached only through their router: metadata, sync, permissions, etc. Only execution goes against the destination's connection. A `data_permissions` row pointing at a destination is never consulted -- it's already actively stripped.
There are test utilities that helpfully create in-test permissions for all databases created, so in a test environment it would look exactly like the invariant was being violated, but that was only because the test utilities deviated from production behavior. This brings the test behaviors into line with production (no permissions get written).
There should be no change in production behavior in this PR, but as a bonus, I figured that a safety guard filtering rows out on reads is not as good as filtering them out on reads *and* failing loudly on writes. I also added guards that a normal database can't be turned into a destination database, which was a behavior tests were relying on for easier setup (but again, the tests then deviated once more from production behavior).
Since 4.28 Liquibase ships an opt-out analytics feature that, on the first
migration run, fetches config from config-endpoint.liquibase.com and sends
anonymized usage events.
* Parse :absolute-datetime string literals in repr resolve
Metabot's representations repair pass wraps bare ISO `between` bounds as
`[:absolute-datetime {} "2024-01-01" :day]` to satisfy the lib `:between`
temporal schema, leaving the literal as a string. That form is schema-valid
(so it passes construct_notebook_query validation), but the JVM query
processor expects a real java.time value — optimize-temporal-filters throws
on a bare string, and the QP's own wrap-value-literals only parses temporal
strings that arrive as raw values, not ones already wrapped in
:absolute-datetime.
Parse those string literals into java.time values in resolve-query, where the
portable repr form becomes QP-ready pMBQL, alongside the existing
annotate-field-types pass.
* Address review: tighten docstrings, use update over assoc
- parseable-temporal-string?: contract-first two-line docstring
- parse-absolute-datetime-literals: move the "why" into inline comments at
the walk, keep the docstring to contract + idempotency
- use (update node 2 u.date/parse) instead of assoc
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Address review: year/year-month, fail-fast, export round-trip
- parse year/year-month :absolute-datetime literals too (u.date/parse maps
them to LocalDate), so they are QP-ready instead of crashing later
- fail fast on a structurally-ISO but invalid literal (e.g. "2024-13-45"):
re-throw as an :agent-error? ex-info so the LLM gets a clean message
- export: format java.time values back to ISO-8601 strings in
portable-repr-form so resolve -> export round-trips to the documented
string form
- tests: per-shape parsing (date/datetime/offset/year/year-month),
idempotency, invalid-literal, and export round-trip
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Move :absolute-datetime string parsing to the QP layer
Revert the resolve-layer parsing and instead teach wrap-value-literals to
parse string-valued :absolute-datetime clauses, not just raw values. The
repair pass wraps bare `between` bounds as `[:absolute-datetime {} "2024-01-01"
:day]`; wrap-value-literals previously only parsed temporal strings that
arrived as raw values, so a pre-wrapped string slipped through and
optimize-temporal-filters threw on the bare string.
The QP is the right layer: wrap-value-literals runs at execution time, where
both the report timezone (bound) and the comparison field's type are
available — so the parse is type- and timezone-aware for free, and the stored
query keeps its portable ISO-string form (no export round-trip to invert).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fail fast on malformed :absolute-datetime literals in resolve
Add a resolve-side validation pass that rejects temporal literals which pass
the schema's structural regex but are not real dates (e.g. "2024-13-45"), so
the agent gets an actionable error at construct time instead of a failure at
query execution.
Validation only — it does not coerce. The string -> java.time coercion stays
in the QP's wrap-value-literals, where the field type and report timezone are
available. Accepts every valid :absolute-datetime shape: date, datetime,
offset-datetime, year, and year-month.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Preserve unit and wrap mixed :between bounds for string :absolute-datetime
When re-parsing a string-valued :absolute-datetime in wrap-value-literals:
- Carry the clause's own temporal unit through instead of discarding it. The
comparison field's bucket still wins, but for an unbucketed field the literal's
unit is now the fallback (rather than :default), so a year/year-month literal
like [:absolute-datetime {} "2024" :year] keeps its bucket instead of silently
collapsing to a single instant.
- Wrap each :between bound independently so a mixed bound (string :absolute-datetime
on one side, :now / :relative-datetime / an already-parsed :absolute-datetime on
the other) still coerces the wrappable side instead of falling through unwrapped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Guard comparison literals and tidy :absolute-datetime validation
- wrap-value-literals: guard the field position in the :=/:!=/<…> arms so a
comparison with a wrappable literal on both sides can't bind a literal as the
field and leave it unparsed; it now falls through unchanged.
- resolve: validate :absolute-datetime literals via lib.walk/walk-clauses
(no tree rebuild, shares clause-of-type? detection with the QP) instead of
clojure.walk/postwalk.
- Move cross-subsystem rationale out of the string-valued-absolute-datetime
docstring into an inline comment.
- Add a test covering the two-literal comparison fall-through.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Parse fieldless :absolute-datetime comparisons and bound bad-literal errors
Wrap a string-valued :absolute-datetime on both sides of a comparison
(no field to wrap against) so each literal is parsed with its own unit
instead of leaving a bare string for later middleware to choke on.
Truncate the untrusted literal before it lands in the agent-facing
exception message and ex-data, so a pathological value can't flood logs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Treat :default field unit as unbucketed for :absolute-datetime literal fallback
A temporal field can carry :temporal-unit :default (add-default-temporal-unit
stamps it on :temporal/requires-default-unit drivers, and an LLM can author it
directly). The unit-fallback guard only skipped a nil unit, so a :default field
dropped the literal's own unit and collapsed e.g. [:absolute-datetime {} "2024"
:year] to a single :default-bucketed instant. Treat :default as unbucketed too.
* Validate :absolute-datetime literals with plain walk to keep resolve gate friendly
validate-absolute-datetime-literals walked the query with lib.walk/walk-clauses,
a mu/defn whose input schema validates the whole query against ::lib.schema/query.
Under test instrumentation that threw a raw "Invalid input" error on an otherwise-
malformed query (e.g. an :offset in :expressions), pre-empting the friendlier
query-not-runnable agent error the downstream canRun gate is meant to surface.
Use clojure.walk/postwalk and a bare [:absolute-datetime _ s _] shape check, matching
the sibling annotate-field-types pass, so only the literals are inspected.
* Use match/match-many for :absolute-datetime literal validation, add mixed-between raw-value test
Addresses PR review feedback: swap the manual walk/postwalk in
validate-absolute-datetime-literals for match/match-many, and cover a
:between with a raw (unwrapped) string bound plus a :relative-datetime
bound in wrap_value_literals_test.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Show honest "still working" state when a Git sync push stalls (GHY-4132)
A large push can stop reporting progress while the export is still running; the
backend then reports the task as :timed-out with ended_at nil. The push modal
treated that as neither success nor error and stayed frozen on "Exporting
content…" until the page was refreshed.
Render a distinct stalled state instead: drop the frozen progress bar and show
"Still working. No update for N minutes." (internationalized, singular/plural
aware). The counter derives from last_progress_report_at and stays fresh via the
existing 2s current-task poll — no separate ticker.
* Add throttled monotonic progress reporter for remote-sync tasks (GHY-4132)
* Ramp remote-sync export progress across plan + serialize phases (GHY-4132)
* Report remote-sync export commit checkpoint before push (GHY-4132)
* Report remote-sync push progress via JGit ProgressMonitor (GHY-4132)
* Fix throttle, dead 0.66 write, and push liveness in export progress (GHY-4132)
commit-staged! was passed a pre-forced report closure, so every push
ProgressMonitor tick bypassed the 10s throttle and hammered the DB. Force only
the one-shot commit checkpoint (0.8) explicitly in finish-commit!, and let
push ticks go through the throttled reporter.
The forced 0.66 serialize-boundary write ran after commit-staged! returned,
by which point the 0.8 checkpoint (and push ticks) had already advanced the
monotonic reporter past it, so it was silently dropped. Move it to the end of
each export's stage-fn, before the checkpoint/push run.
The push monitor only reported during the exactly-titled "Writing objects"
phase with a known positive total; a non-English JVM locale renames that
title and JGit may report an unknown (zero) total, so the push phase could go
silent for its whole duration and re-trip the stale-progress timeout. Report
on every update tick regardless of phase/total, falling back to the 0.8
push-start fraction — repeated equal values are monotonic no-ops but still
bump last_progress_report_at, throttled to at most once per 10s.
* Use dayjs diff for the stalled-progress minute counter (GHY-4132)
* Expose search's ranked set and page-hydration as separate steps
`search` already ran in two stages: rank the permission-filtered result set
(capped at max-filtered-results), then slice that set to the requested page
and hydrate only those rows. The comment on `search-results` spells out why
the slice comes first — hydration does per-row work, including the expensive
`can_write` check, so it should only touch rows that are actually returned.
Both stages were sealed inside `search`, so a caller holding several ranked
sets had no way to combine them and paginate the combination. Its only option
is to ask `search` for N hydrated pages and merge those, which hydrates N
pages to return one, and — because each page is sliced from its own ranking
before merging — cannot page the merged order correctly at all.
Split the two stages into `ranked-results` and `search-results` and export
both from the module API. `search` is now their composition and behaves
identically; no call site changes.
The motivating caller is metabot's multi-query search, which runs a search per
query and fuses the rankings with RRF. Fusing at the ranked-results seam lets
it hydrate only the page it returns, page the fused order coherently, and
report a total for the fused set.
* Remove comments from search core.clj
Removed comments explaining the purpose of `ranked-results` and `search-results`.
* kick ci
* Convert sandboxes and advanced_permissions plugin modules to TypeScript
* Address review comments: remove typecasting from sandboxes actions and unit spec
* Fix layout shift when switching tabs on the transform job page
The job Overview and Run history pages each rendered their own header:
Overview used an editable name input plus a `...` menu, while Run history
used a static title with no menu. The differing heights shifted the tab
row, so the clicked tab jumped when switching between the two.
Extract a shared JobHeader component (and a useJobHeaderState hook for the
readOnly/name-change logic) rendered identically by both pages, mirroring
the MetricHeader pattern. Run history now also shows the `...` menu and the
editable name, and Metabot is consistent across both tabs.
* Unify JobPage header logic with useJobHeaderState
JobPage duplicated the readOnly computation and name-change handler that
now live in useJobHeaderState. Source them from the hook instead; keep the
local useTransformPermissions call only for the page-level loading gate.
* Use strict undefined comparisons in job header code
Address review: job/jobId are never null, so compare with undefined
strictly and derive jobId from job.id in the JobHeader render.
Fixes GIT-10551: connecting an MCP client to a Metabase instance served
under a subpath (site-url like https://example.com/metabase behind a
prefix-stripping reverse proxy) failed at the consent step.
The OAuth flow already derives every advertised URL from site-url —
discovery metadata, authorization/token/registration endpoints, and the
WWW-Authenticate resource_metadata header are all subpath-correct. But
three browser-facing pieces assumed Metabase lives at the domain root:
1. The consent form posted to root-relative /oauth/authorize/decision,
so the browser sent the POST outside the subpath and the proxy
returned 404 (the reported bug). The form action is now the absolute
site-url-based URL, via the same absolute-url helper the page already
uses for the logo. The bundled @font-face URLs on the page get the
same treatment.
2. The CSRF cookie was scoped to Path=/oauth/authorize. Under a subpath
the consent page lives at /metabase/oauth/authorize, so the browser
would never send the cookie back and the decision POST would fail
CSRF validation even with the form action fixed. The cookie path now
carries the path component of site-url.
3. The logged-out flow: the backend bounces to the login page with a
basename-relative redirect target (the server sits behind the
prefix-stripping proxy and never sees the subpath). The frontend
guard resolved that target against the bare origin, losing the
subpath on the full-page redirect back to /oauth/authorize. The
guard now normalizes targets through the router basename in both
directions: paths without an explicit origin get the basename joined
(browser-real href), and pathnames get it stripped (router path, so
the backend-only-path prefix check still matches).
Why this is safe:
- Root deployments are bit-for-bit unchanged. site-url has no path
component there, so the cookie path is still /oauth/authorize, the
form action still targets /oauth/authorize/decision, and with an
empty basename the guard resolves every input shape (relative path,
bare path, absolute URL) to exactly the URLs it produced before.
- Under a subpath, the only outputs that change are ones that were
previously unroutable (404 through the proxy or a path the SPA
router would double-prefix).
- The server never sees the subpath, so no routes, parameters, or
validation logic change; every fix is to a browser-visible artifact
derived from site-url.
Tests pin both halves: existing root-deployment assertions are
untouched and still pass, and new tests cover the subpath form action,
cookie path, login redirect target, and the full consent -> decision
round-trip with a subpath site-url. Also verified live against a local
prefix-stripping Caddy proxy with a real MCP client flow (DCR, consent,
PKCE token exchange, tool calls).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Rewrite image-buffer pool on dirigiste (same pool as the GraalVM contexts)
Replace the hand-rolled ConcurrentLinkedDeque pool, stats counters, sweeper
daemon, and logging with the dirigiste Pool already used for static-viz
contexts, with the same semantics: 0-3 pooled int[] arrays of the fixed
oversized size, held exclusively per render, idle arrays dropped after ~10
minutes. acquire blocks until an array is free when the requested size fits;
larger requests get a fresh unpooled BufferedImage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Extract the dirigiste pool factory into metabase.util.pool
create-pool was duplicated verbatim between the static-viz graal renderer
(channel.render.js.common) and the sqlglot graal runner (sql-parsing.common),
and image-buffer had just picked up a third use. Move it to metabase.util.pool
and use it from all three; the pool behavior tests move from
sql-parsing.common-test to util.pool-test with it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add prometheus counters for image-buffer pool usage
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Rename image-buffer metrics to metabase-notification namespace
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Migrate nav search and search result components from Emotion to Mantine
* Address review feedback: close button label, valid footer markup, footer role and hover transition
* Restore search dropdown footer ref by returning a ref callback from useListKeyboardNavigation
* Improve page/container margins when printing document
* do not call flushSync while printing
* Revert "do not call flushSync while printing"
This reverts commit 37c6fe2b50.
* Resize ECharts synchronously for print layout changes
* Refactor ECharts print resize tests
* Clean up
* remove compile-with-inline for clickhouse, no longer needed since the driver got fixed
* re-enable full tests
* fix: for max behavior on clickhouse
* fix: insert for clickhouse
* add temporal reg test
* Generate expression parser as TypeScript
Add --typeScript to lezer-generator so the parser emits typed output; convert lezer.js to lezer.ts.
* Migrate NotebookStepPreview to TypeScript
* Reuse QuestionResultLoaderChildrenProps for VisualizationPreview props
* Add element prop support to facade Route
* Compose route guards as element wrappers
* Memoize the element bridge by component type and render off the injected route
* Clarify that index routes accept component or element
* Test that the element bridge reuses shared components across sibling routes
* Migrate DataSelector SavedEntityPicker to TypeScript
* Align DataSelector picker prop types with actual usage
* Migrate DataSelector to TypeScript and adapt consumers
* Migrate DataSelector unit tests to TypeScript
* Remove unused DataSelector props
* Add TODO for DataSelector api/metabase-lib structural-type cleanup
* Remove unused hasBackButton/onBack from DataSelectorSchemaPicker
* Use setState updater form in setStateWithComputedState
* Guard currentUser instead of faking a zero user id
* Avoid passing a null database to SchemaAndTableDataSelector
* Convert SavedQuestionHeaderButton unit spec to TypeScript
* Justify or remove DataSelector type casts for no-unjustified-type-casts
Different endpoints dealing with destination databases handled checks differently, causing confusing error messages depending on code path. This consolidates and standardizes both the messages and the HTTP code.
* Remove table_id from measures and segments
* Clear stale Card table_id when the query loses its source table
populate-query-fields only wrote table_id when the derived value was
truthy, so converting an MBQL question to native SQL (or pointing it at
a native model) left table_id referencing the old table. Make it
unconditional, the same fix#68080 applied to source_card_id.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: validate derived table on segment/measure updates
- PUT /api/segment/:id and PUT /api/measure/:id now derive the table from an
updated definition: table-less definitions are rejected (consistent with
POST), and if the definition implicitly moves the entity to another table,
the user must also pass the create permission check for the new table
(write-check alone only covered the old table).
- Drop the dead :table_id insert arg in POST /api/measure — before-insert
already derives it from the definition.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Derive segment/measure perms from the definition, not table_id
Per review: instead of API-handler checks against a separately derived table
id, the mi/can-read?, mi/can-write?, and mi/can-create? implementations for
:model/Segment and :model/Measure now derive the source table from the
definition via lib/primary-source-table-id (same admin-or-analyst check as
before, rejecting definitions with no source table), and the API handlers no
longer deal with table ids at all — table_id only remains in what we return
to the client.
Segment definitions are now normalized to MBQL5 at the API boundary (like
measures already were), so the perms and model hooks always see full queries.
Updating a definition re-runs the create check with the new definition, since
the write-check only covers the existing one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Fix remote-sync segment permission tests for definition-derived perms
The three remote-sync segment tests used empty {} definitions and relied on
table_id/hydrated :table for the permission checks. Perms now derive the
source table from the definition, so give the segments a real MBQL5
definition on the venues table. Also coerce the can-manage helpers to
booleans so callers asserting false? keep working.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Strip stray :aggregation from MBQL4 segment fragments at the API boundary
The model migration always dropped :aggregation from legacy segment
fragments (segments cannot have one). When definition normalization moved to
the API layer, that behavior was lost, so fragments with a stray aggregation
(which several Cypress specs post) started failing schema validation on
insert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reject invalid segment definitions instead of stripping :aggregation
Per review: don't silently fix up segment definitions with a stray
:aggregation — validate the normalized definition against the segment schema
at the API boundary and return a 400. Remove the aggregation key from the
Cypress specs that were posting it (it was always dropped by the old model
migration, so those specs never exercised it anyway).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Simplify: keep table_id derivation at the API, revert the perms refactor
Restore the segment/measure permission methods, search filtering, and the
associated tests to their previous table_id-based behavior. The PR keeps to
its core change: the segment/measure APIs no longer accept table_id — it is
derived from the definition and used for the permission checks (including
re-checking the newly derived table when an update moves the entity).
Also update compare-to-the-rest-15655-test for the card change in this PR:
a wrong table_id can no longer be persisted (it is always derived from the
query), so the comparison dashboard for a nil-table card is built from the
field-distribution path now.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Only clear a stale card table_id when the query itself changes
populate-query-fields' unconditional :table_id meant any card update
(rename, archive, collection move) would wipe table_id whenever the
derivation came back nil — e.g. when the query's source card had been
hard-deleted. Gate the nil-overwrite on :dataset_query actually being in
t2/changes; inserts and query edits still clear stale values.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Persistent queue: concurrent batches, per-queue error handler, config
Generic improvements to the mq persistent-queue subsystem, extracted from the
explorations work so they can land independently:
- max-concurrent-batches: a queue can process multiple batches in parallel,
bounded by a (optionally dynamic) limit (metabase.mq.queue.concurrency).
- on-error: per-queue handler invoked when a message exhausts its retries,
so callers can record terminal failures instead of silently dropping them.
- Reworked polling, quartz scheduling, and registry for the above.
- Expanded mq/README.md and tests (concurrency, on-error, quartz affinity).
* Fix Eastwood reflection warnings on Scheduler.checkExists in quartz-test
`Scheduler.checkExists` is overloaded (JobKey vs TriggerKey), so the un-hinted
`nudge-key` made the three call sites reflective. Import `JobKey` and hint the
local; CI's Eastwood step fails the build on reflection warnings.
* Code fixes
* Migrate remaining ModalContent Emotion styles to Mantine and fix dead EmbedModal header overrides
* Make the modal header back button keyboard-accessible with a descriptive label
* fix orphan semantic search index table cleanup
* Harden orphan index cleanup: schema-scope the scan, skip views, isolate drops
The orphan scan now only considers base tables (a view in the DROP TABLE
batch would abort every nightly cleanup) and, in dedicated-pgvector mode,
only current_schema() — where the unqualified DROP actually resolves;
previously it scanned all schemas and logged drops that never happened.
Stale/orphan tables are dropped one statement at a time with per-table
error handling so a single undroppable table (e.g. one with a dependent
view) cannot permanently block stale, tombstone, and repair cleanup.
Also anchors index-table-name-pattern (\A/\z) so it stays substring-proof
independent of re-matches, documents that legacy pre-BOT-337
index_table_* names must keep matching, and derives the force-reset test
case from model-table-name/model-table-suffix instead of a literal.
* Count actual index-table drops and reap legacy index_table_ stragglers
drop-index-table! now reports :dropped/:missing/:failed — dropping without
IF EXISTS so a vacuous drop (metadata row whose table is already gone)
does not count — and cleanup logs dropped-of-found. The name pattern
gains an index_table_<anything> alternative: the prefix belongs
exclusively to the pre-BOT-337 era, whose digit-less stragglers were
otherwise unreachable.
---------
Co-authored-by: Chris Truter <chris@metabase.com>
* Do not duplicate user context between system prompt and last user message
In #71045 we moved most of the user context out of internal.selmer system prompt and injected it into the last user
message. However, other system prompts still injected user context into the system prompt, so we wound up formatting
and including it twice. If the `:user_is_viewing` context happens to be large (many related tables / fields), then the
memory and token cost can be significant (#76493).
This commit removes everything from system prompts that was already being injected into the last user message, namely:
- `current_time`
- `first_day_of_week`
- `current_user_info`
- `viewing_context`
It also moves `recent_views` into the last user message (not previously included there).
In addition to saving input tokens and memory, this should also help with caching on providers like openai that do
automatic prefix caching, since we now always inject the current date/time near the end of the prompt.
This also means all system prompts can drop the `<<<METABOT_CACHE_BREAKPOINT>>>` since they no longer have any dynamic
content after the breakpoint.
* Add :slackbot and :document-generate-content to profiles-excluding-recent-views
Their system prompt never included recent_views, so preserve the behavior now that we inject recent_views in the last
user message.
A failed CREATE USER used to surface the generated workspace password
verbatim in the JDBC exception message, which provisioning then logged
(GHY-3660). Scrubbing landed in #74657; this adds a regression test that
reproduces the failure for real: init-workspace-isolation! runs over a
connection authenticated as a low-privilege workspace user (granted
database-level CREATE on postgres/redshift so execution reaches the
password-bearing CREATE USER), and every piece of text a logger could
print from the thrown exception - stack trace, Throwable->map, causes,
suppressed, ex-data, and SQLException nextException links - must not
contain the password. A postgres-only assertion requires the redaction
marker in the echoed statement so the test cannot pass vacuously.
JDK 25 changed the empty file path to resolve to the current directory
(File("").exists() is now true), so a long-standing MB_CONFIG_FILE_PATH=''
made the config-from-file loader try to parse the current directory as YAML
and fail at boot. Ignore blank values like metabase.config.core/config-str
does everywhere else, and don't count them as config-text-file being enabled
in anonymous stats.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* initial migration
* initial migration
* remove deadcode loaders
* one more site is migrated. remove dead code
* make data-testid="loading-indicator" to be by default for Loader component
* fix unit test
* Omit negative values from treemap instead of producing wrong percentages
Treemaps summed metric values as signed totals and divided by that signed
total, so rows of 30 and -50 rendered a surviving leaf as -150%. Negative
tiles also have no positive area, so ECharts dropped them with no warning.
Mirror the pie chart: omit negative values (with a warning) during
aggregation, or fall back to absolute values when every value is negative.
With all node values non-negative the downstream percentage math is correct.
Fixes GDGT-2760
* Tighten null check and document per-row negative filtering
* Bump mise JDK to temurin 25 to match CI
CI defaults to Java 25 (.github/actions/prepare-backend and the backend
workflow matrix), but mise.toml still pinned temurin 21.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update remaining Java 21 leftovers to 25
The e2e matrix builder and docs still referenced JDK 21, while CI,
Docker images, and release requirements for v60+ are on Java 25.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Pin MBQL transform source schema in transforms e2e spec
The many_schemas Postgres fixture creates an "Animals" table in every
schema (Schema A through Schema Z). createMbqlTransform resolved its
source table via getTableId by name only, so Array.find returned an
arbitrary schema's "Animals" depending on /api/table ordering. The
"convert an MBQL transform to a SQL transform" test asserts the compiled
SQL contains "Schema A", so a non-Schema-A match made it flake.
Add an optional sourceSchema to createMbqlTransform and pin the spec's
source to Schema A so the resolved table and its SQL are deterministic.
* Wait for MBQL->SQL transform save to settle before clicking Run tab
The Run tab only exists in read-only view mode; after clicking Save the
test must wait for the update PUT and the navigation off /edit, otherwise
it races the still-in-flight save and times out looking for the Run tab.
* only pin source schema for the default source table
---------
Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>
* Add index-route support and redirect() to the metabase/router facade
* Adapt ModalRoute and DataPermissionsPage to the function-based Route
* Replace IndexRoute/IndexRedirect/Redirect with index routes and redirect()
* Convert createEntityIdRedirect to a facade-hook wrapper component
* Update route test scaffolding for index routes and redirect()
* Build the Route shim via createRoutes instead of augmenting RouteUtils
* Point the guards spec at the relocated Route export
* Stabilize transforms permission navigation in e2e specs
* Freeze redirect() target at mount to fix chained index-redirect race
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* Don't GZIP sync-flush per object when serializing QP cache entries
Sync-flushing after every cached row meant one native Deflater call in a
JNI critical region per row; on JDK <= 21 (no G1 region pinning, JEP 423)
that stalls GC via the GCLocker under concurrent cache writes. Let the
deflater buffer normally instead, and have result-fn finish the stream
(draining the deflater and writing the GZIP trailer) before returning the
bytes. Old cache entries (unterminated gzip) still deserialize since the
reader already treats EOFException as end-of-stream.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Don't flush the JSON download writer per row
Each per-row flush cascaded into a sync-flush of the gzipped HTTP response:
one native Deflater call in a JNI critical region per exported row (GCLocker
stalls on JDK <= 21) plus one TCP packet per row (#34795, fixed for the :api
writer but not :json downloads) and a worse compression ratio. Let the
BufferedWriter push rows through as it fills, like the CSV writer does;
finish! still flushes the tail.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Make Snowflake CI runs use a randomly-generated, isolated DB names.
Something is still randomly calling `destroy-db!` on the test-data
dataset and wrecking things for CI runs:
```
(pprint (jdbc/query (no-db-connection-spec)
["SELECT query_text, end_time
FROM SNOWFLAKE.ACCOUNT_USAGE.QUERY_HISTORY
WHERE query_text LIKE '%_c1bae%'
AND (query_text LIKE '%DROP%' OR query_text LIKE '%drop%')
ORDER BY end_time DESC limit 128"]))
[...]
"DROP DATABASE \"sha_rel_c1baee7db240aa419104c2d925a07a4d4faeeb24_test-data\";"
```
This particular query (without `IF EXISTS`) only exists on master in
the `destroy-db!` method. However, this method already has protection
against being run on the `test-data` dataset and will throw an
exception if this is tried! And the main call to `destroy-db!` is
gated behind a feature check for `:test/dynamic-dataset-loading`. So
there's nowhere this could be coming from on master.
Rather than trying to track it down, I think it's better to just treat
all snowflake DBs as isolated and disposable. Update `qualified-db-name`
to just prefix the DB name with a random prefix, and use the
`after-run` hook to drop it when done. This also allows us to get rid
of the janky and weird locking system, because no two jobs should ever
try to hit the same DB.
* Add per-version download link to Security Center advisories
Thread optional download_jar_url through backend schema/fetch and render
a Download link next to each fixed version in AdvisoryCard.
* fix aligning
* Read instance version via useSetting; simplify affected-version lookup
* rollback a redundant change
* addressing PRs comments
* use sequence to nil-coerce download_jar_urls
* addressing the PR's comments
* add download-click analytics
* fix test: add download_jar_urls to Advisory factory
* add e2e test
* initial migration
* fix link input size
* remove dead files
* Use Input.ClearButton for textinput clear icon
Replace hand-rolled Tooltip+ActionIcon+Icon clear pattern w/ Mantine
Input.ClearButton across 5 sites; c=text-secondary + rightSectionPointerEvents=all.
Comment why LinkViz needs pointer-events on both root+input.
* fix unit test
* Update loki refs for parameter search Input->TextInput
* Use Input.ClearButton in PermissionsEditorContent; rm blank line in SearchBar styled
* addressing the PR's comments
* validate client sent parent_message_id
* implement more restrictive retry logic
* move history to the backend
* migate state from conversation to message records
* client nows generates message ids to shrink case of client not knowing id of written message record on aborted requests
* pr feedback
* [Metabot] Implement conversation branches in usage analytics (#77542)
* implement branches in usage analtyics
* sol pass at making code terser / cleaner
* handle error messages correctly + allow branching after assistant message
* pr feedback
* fix test failures
* remove migration logicalFilePath
* Hide write-only share actions when entity is not writable (#72752)
The Question and Dashboard sharing menus, and the public-link popover,
offered write actions to any admin regardless of write access. On a
read-only remote-synced instance a card/dashboard in a synced collection
is not writable (can_write=false), so these actions 403'd or authored
state that doesn't stick — the "updating embed fails on read-only
instance" symptom in #72752.
Gate the write actions on can_write, matching how the normal edit
affordances already disappear when the entity can't be written:
- Embed (publish): hidden when not writable.
- Create a public link: hidden when not writable; an existing public link
stays visible so it can still be viewed/copied (those are reads).
- Remove public link: hidden when not writable.
* Disable embed Publish instead of hiding the option on read-only entities (#72752)
Keep the Embed sharing option visible for a read-only remote-synced
card/dashboard, but disable the Publish/Unpublish buttons in the embed
modal with a tooltip explaining the entity is synced and read-only.
This reverts the earlier hiding of the Embed menu item; the public-link
gating (hide create / remove, keep an existing link viewable) is
unchanged.
* Translate the read-only embed tooltip per resource type (#72752)
Interpolating resourceType left the noun untranslated and prevented the
rest of the sentence from agreeing with its gender.
* Add ai-tracing: eval-time trace capture for the Metabot agent
Introduce a new `ai-tracing` module that captures complete, per-run agent
traces during benchmark/eval runs and exports them to an external eval
backend (Confident AI) over OTLP — separate from production observability.
- Capture is gated by a per-run dynamic binding (`*capture*`), orthogonal to
MB_TRACING_*, so it never fires on organic traffic. Enabled via the
env-only MB_AI_EVAL_CAPTURE setting; the API path emits the trace as an
`eval_trace` data part and exports it.
- Captures the full agent.turn -> llm.call -> tool.* tree with prompts,
completions, tool args/results, durations, and concurrent-tool nesting
(conveyed across the agent's virtual threads).
- OTLP export uses a dedicated TracerProvider (SimpleSpanProcessor +
force-flush, never the production one), replaying the in-memory tree
post-hoc. The domain -> Confident `confident.*` semconv mapping lives in a
single chokepoint (`node->semconv-attrs`); vendor/schema swaps are one edit.
- Wires capture into run-agent-loop, loop-step, call-llm, and run-tool.
- Trace-level input/output (user question + final answer) set on the root.
- Export failures are logged loudly instead of swallowed.
- Tests cover the gate, nesting, enrichment, timing, concurrency, the
semconv mapping, and a metadata-size regression guard.
* Make ai-tracing export more idiomatic; refresh stale docs
- export.clj: replace node->semconv-attrs's nested cond-> of repeated
type checks + json/encode with :ai/keys destructuring and a flat
medley/assoc-some table; compose final-llm-output as transducers;
extract event->attributes; tidy parse-headers (for-comprehension),
build-provider! (doto), and ensure-provider! (fresh? helper).
- core.clj: drop the stale "seam not wired" comment block and the
outdated ns docstring (it referenced the abandoned gen_ai/OpenInference
schema and "single namespace" framing); describe the shipped export.
No behavior change; 14 tests / 56 assertions still pass.
* Replace ai-tracing OTLP export with per-session JSONL log files
Switch the eval-trace collection method from OpenTelemetry/OTLP export to a
dedicated file-based logger, and extend instrumentation to the MCP server and
agent-api in addition to the Metabot agent loop.
- Rip out all OTLP/Confident code (export.clj, the OTLP settings, the OTel
provider/semconv mapping, the eval_trace SSE part).
- New metabase.ai-tracing.log sink: each span streams one JSON line on a
dedicated logger, routed per session to its own <session-id>.jsonl by a
Log4j2 RoutingAppender keyed on mb-eval-session-id. Pure JSONL (%m%n), full
payloads, no size limits.
- Session model = OTel trace-id analog: *session-id* minted at a fresh
capture's root, or supplied by the caller, conveyed across virtual threads
by bound-fn*. Nodes carry :id/:parent-id for tree reconstruction.
- Generic with-eval-session entrypoint (gated by MB_AI_EVAL_CAPTURE, inherits
when nested) reused by run-agent-loop, MCP dispatch-request (keyed on the
Mcp-Session-Id so a conversation lands in one file), and agent-api routes.
- MB_AI_EVAL_CAPTURE is the single on/off switch; the logger sits at info and
writes nothing unless a capture is active.
- run-agent-loop emits a tiny eval_session SSE part so the HTTP harness can
locate the session file.
Verified end-to-end: real agent run writes a per-session JSONL file with the
correct span tree (parent chain, full structured + normal tool output);
sessions route to separate files. 14 tests / 51 assertions pass.
* ai-tracing: capture full request/response context and add trace read API
Enrich eval spans so a captured trace is a self-contained record of a run:
- agent-api: record HTTP request body, user id, and the response status/body
via a wrapping respond fn that fires inside the span.
- mcp: record JSON-RPC params, user id, scopes, and the materialized result.
- metabot agent: thread an optional eval-session-id through run-agent-loop so
the harness can name and read back the per-session trace file; record the
full request (messages/context/state), turn-level token usage, finish reason,
and final agent state (link registry / generated entities). Surface the loop
finish-reason out of loop-step instead of only logging it.
- log: guard span serialization so a non-encodable attribute can never break a
traced run.
Add metabase.ai-tracing.api: a superuser-only GET /api/eval-trace/:session-id
that returns the captured JSONL, mounted only when MB_AI_EVAL_CAPTURE is on.
Filenames are validated against path traversal. Wire the module into routes and
the kondo module config.
* ai-tracing: preserve OpenAPI spec on agent-api routes; harden trace sink
The eval-tracing wrapper turned agent-api `routes` into a plain fn, which no
longer implemented OpenAPISpec — full-API spec generation (openapi.json,
endpoint-dox, the openapi-all-routes test) threw when the walk reached
/api/agent. Wrap it in `handler-with-open-api-spec`, delegating the spec to the
underlying ns-handler.
Also: narrow the trace-sink catch to Exception so Errors/InterruptedException
propagate; stream the trace file instead of slurping it into heap; and correct
the "single nil-check" docstring claim.
* ai-tracing: validate eval-session-id at mint time to prevent path traversal
The trace read endpoint sanitized the session-id before touching disk, but the
write path fed the raw, caller-supplied eval_session_id straight into the
RoutingAppender's `<session-id>.jsonl` file name — a value like `../../foo` on
an eval instance escaped the trace dir (arbitrary .jsonl write).
Hoist the safety regex to a single `safe-session-id-re` in ai-tracing.core and
add `checked-session-id`, which mints a uuid when none is supplied and throws on
an unsafe supplied id. `with-eval-session` now validates every caller-supplied
id before it can reach the log-file path; ai-tracing.api references the shared
regex so read/write validation can't drift.
* ai-tracing: gate span-macro arg construction on capture-active?
The span macros expanded to an eval-span* call, so span-name and attrs
were built on every invocation and discarded on the nil-check when no
capture was live. Hoist the capture-active? guard into a `spanning`
helper macro (and an inlined variant in with-tool-call, whose name
derives from attrs) so organic traffic never builds the span name string
or attrs map — it pays only the *capture* nil-check plus one thunk
closure. with-tool-call now evaluates attrs exactly once, and only when
capturing.
* ai-tracing: test cross-module span nesting; make agent tests capture-agnostic
Add eval-tracing-nesting-test: drives the real run-agent-loop under
capture-reducible with only the LLM + search backend mocked, and asserts
the emitted span tree nests agent.turn -> llm.call -> tool.search. This
covers the wiring the ai-tracing docstrings promise — the turn/llm spans
opened in metabot.agent.core, the tool span in metabot.self.core, and
*parent* conveyed across bound-fn* into the virtual-thread tool executor
— which the ai-tracing unit tests exercised only at the primitive level.
Also pin eval-capture off for the namespace via a fixture. run-agent-loop
appends an eval_session data part to the stream whenever capture is active,
so every exact-match assertion on its output broke under MB_AI_EVAL_CAPTURE
(which dedicated eval instances set). The fixture keeps those assertions on
the production stream shape; the new test forces capture independently.
* ai-tracing: key MCP eval session on UUID correlator; complete finish-reason
Key the MCP eval-trace session on the session id's UUID prefix rather than
the full <uuid>.<base64> id: the prefix is stable across the conversation and
always filesystem/URL-safe, so a base64 payload that require-valid-session
accepts but safe-session-id-re rejects (e.g. `=` padding) can no longer make
checked-session-id throw ahead of the JSON-RPC try/catch and 500 the request.
Record :finish-reason on the empty-response and reduced (early-termination)
loop terminations too, so the turn span's :ai/finish-reason is populated on
every path, not just natural completion.
* ai-tracing: harden trace sink serialization and error capture
- emit!: fall back to a JSON-safe copy when an attribute value isn't
encodable, so one bad value no longer drops the span (and, for a root
span, orphans its children).
- span error events now carry the exception class and ex-data, not just
the message.
- rename JSONL :start-ns/:end-ns to :start-epoch-ns/:end-epoch-ns (they
hold wall-clock epoch nanos, not System/nanoTime).
- test the api trace-file path-traversal guard directly.
* ai-tracing: bound session ids, gate user-input build, snapshot finish-node
- Cap supplied eval-session-id at 200 chars on the API schemas and in
checked-session-id, so a supplied id can't produce a filename that fails
creation and silently drops the trace (charset was already enforced).
- Compute the turn span's :ai/user-input inside the capture-gated attrs map
instead of unconditionally on every run-agent-loop call.
- finish-node takes a single consistent snapshot of the node atom.
* ai-tracing: enforce session-id cap+charset at API boundaries
Route the 200-char cap through ait/max-session-id-length everywhere (read
endpoint, run-agent-loop opts, POST /api/metabot params) instead of
hardcoding the literal, and bound the previously-unbounded read-endpoint
:session-id path segment.
Validate the charset via safe-session-id-re in the Malli schemas so a bad
eval-session-id 400s at the boundary rather than throwing deep in
checked-session-id and surfacing as a generic agent error. Anchor
safe-session-id-re with \A/\z so it stays exact under both re-matches (mint/
read checks) and re-find (Malli :re) — unanchored, :re accepted any string
containing a safe run (e.g. ../../etc/passwd).
* ai-tracing: key MCP eval trace on a harness-supplied session id
Let an eval harness name and later fetch an MCP conversation's trace. The MCP
endpoint honors an `x-eval-session-id` header — `dispatch-request` keys
`with-eval-session` on it (validated through `checked-session-id`, the single
source of truth for the safe-id contract), falling back to the `Mcp-Session-Id`
correlator when absent. This is the MCP analogue of metabot's `eval_session_id`,
needed because the client owns the `Mcp-Session-Id` and never exposes it.
Also record the resolved query on the construct-query endpoints as `ai/query`
(inert unless capturing): the MCP `construct_query` tool returns only an opaque
handle, so this is the only place a harness can recover the agent's query off
the trace to grade it.
* ai-tracing: address review — guard :reduced append, cover the read API
- run-agent-loop: on a :reduced stop (client disconnect / cancellation) the
reducing fn has already terminated, so don't step it again to append the
eval-session pointer or debug part — that violates the transducer contract.
Fold both trailing appends into one capture-gated cond-> that no-ops on
:reduced (also fixes the latent debug-branch case). The harness reads the
JSONL file, not the stream, so a skipped pointer on disconnect costs nothing.
- agent-api: record :http/user-id in the respond closure, where +auth has bound
*current-user-id* — it is unbound when the span opens, so direct HTTP callers
previously recorded nil.
- ai-tracing.api: add endpoint integration tests — capture-gated 404,
superuser-gated 403, missing-trace 404, and existing-trace 200.
- log: mark node->entry ^:private; document that trace-file retention is the
operator's responsibility (append-only, no rollover, re-runs accumulate).
- mcp: extract the dispatch lambda (line length) and note the fresh-file-per-
request fallback when neither session id is present. Mark pure tests ^:parallel.
* ai-tracing: drop dead exports, document capture id, harden json-safe
- Remove the `sanitize-sql` re-export and `event!`: neither has a
non-test caller. Dropping `sanitize-sql` also removes ai-tracing's only
dependency on the `tracing` module (trimmed from `:uses`). The sites that
put a query in a span record the raw value on purpose (a scorer wants the
real SQL/MBQL), so the sanitizer was advertising an inconsistency the
design doesn't want.
- Document that `capturing`/`capture-reducible` is return-value-based: a
session id supplied to an inner `with-eval-session` is ignored because
capture is already active, so no `<id>.jsonl` is written on that path.
- `json-safe` now stringifies non-scalar map keys too, so the fallback can
never itself throw and orphan the root span it exists to save. Covered by
a vector-keyed-map regression test.
* ai-tracing: address review — gate pointers, bound json-safe, drop indirection
Correctness:
- capturing/capture-reducible no longer mint a session id, so the in-process
path is truly file-less (read :trace) instead of writing a stray <uuid>.jsonl
- gate the trailing eval_session pointer on a non-nil *session-id* so the
file-less path doesn't emit a pointer to a <nil>.jsonl
- add *retain-tree* so the in-memory span tree is only built when a caller
reads it back (capturing); the file-routed path no longer pins heap for the
whole run
- depth-bound json-safe so a deep/cyclic attr degrades to a string instead of a
StackOverflowError escaping emit!'s catch
- agent-api trace wrapper: only record a plain-data :http/response (skip a
streaming/opaque body rather than stringifying it); fix the stale comment
Cleanup:
- drop the eval-capture-enabled? indirection; entrypoints and tests consult the
ai-eval-capture setting directly
- with-eval-session: inline supplied-id, ^:once, generic 404 gate message
- extract mcp dispatch-method to de-nest the JSON-RPC case
- remove the dead ai-tracing.core/event! kondo entry
- keep eval JSONL spans out of the shared test log (log4j2-test.xml)
Tests: cover the agent-api trace wrapper, the file-less no-pointer/no-file
behaviour, and drive the log-sink assertions through with-eval-session.
* ai-tracing: fix route sort order and wire settings through init
Two structural test failures on the branch:
- Move the `/eval-trace` route into sorted position in the route-map
(keep-routes-sorted-test reads the file and asserts the keys are sorted).
- Add `metabase.ai-tracing.init` (requiring the settings ns), require it from
`metabase.core.init`, and add it to the module's `:api` — so
settings-namespaces-should-be-required-by-init-namespaces-test passes and the
`MB_AI_EVAL_CAPTURE` setting is registered deterministically at startup rather
than only when some instrumented namespace happens to load `ai-tracing.core`.
* ai-tracing: store span wall-clock timestamps as epoch millis, not nanos
Review: the epoch fields were nanos synthesized from currentTimeMillis (ms-
granular anyway), and precise sub-ms duration already lives in :dur-ms — so nanos
bought nothing. Rename :start-epoch-nanos/:end-epoch-nanos → :start-epoch-ms/
:end-epoch-ms (and the JSONL keys :start-epoch-ns/:end-epoch-ns → :start-epoch-ms/
:end-epoch-ms). End is still start + the monotonic duration (rounded to ms), so it
never reads a second, possibly backward-jumping wall clock.
visitDashboard waits on every dashcard query with cy.wait's default 5s
per-alias window; with 20+ cards the tail queries stagger behind the HTTP/1
concurrency limit and miss it (the dashcardQuery### "no request occurred"
flake). Add a dashcardTimeout option to visitDashboard and use it for the
large-dashboard visits in this test, so the downstream card assertions can
stay at their default timeouts. Also give the 20-request bulk move a longer
window, since its tail lags under CPU constraint.
The ci-conductor scripts are shared CI infrastructure: every supported
release branch runs them, and they keep evolving on master. A copy
committed to a branch goes stale, and syncing six branches by hand on
every change does not scale.
Add a prepare-ci-conductor action that overlays release/ci-conductor and
e2e/support/ci_conductor.ts from master, so a release branch runs the
current scripts while its workflow wiring stays branch-historical.
Anything headed for master uses its own sources, so a PR can still test
its own changes.
quarantine-gate and report-failures-to-ci-conductor delegate their bun
setup to it. prepare-cypress calls it too, because e2e/support/config.js
imports the reporter at Cypress bundle time and it has to be in place
before the run starts.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Implement schema, lib helpers, viz-settings + query -> query
* Implement grouping-set compilation
* Implement query nesting to support breakouts with parameters
For example, when selecting nested JSON fields in postgres,
the generated SQL query contains prepared query parameter
placeholders and the engine cannot determine that all copies
are the same expression.
* Keep dominant treemap share honest instead of rounding to 100%
The treemap percent formatter used a fixed 2 decimal places, so a dominant
leaf's share rounded up to a misleading 100.00% (e.g. 100000 vs 1 rendered
as 100.00% instead of 99.999%).
Mirror the pie chart: derive the decimal count from every node share via
computeMaxDecimalsForValues, so a tiny sibling forces enough precision to
keep the dominant share honest. Floored at the existing two decimals so
ordinary treemaps are unchanged, and a genuine whole still shows a clean 100%.
Fixes GDGT-2761
* Pass tree to test setup via SetupOpts object
* Delete react-router-redux and redux-auth-wrapper
* Dedupe redux to a single v5 copy
Removing the @types packages dropped a transitive redux@3 pin that kept a
typeless redux at the top level and masked a redux@4-vs-RTK-v5 type conflict.
Force a single redux@5 so types stay consistent.
* Migrate remaining react-router-redux references
Drop the now-dead eslint import bans for the deleted package, remove the
obsolete redux-action cast in auth logout, and reword stale comments. Keep the
metabase/router provenance docs that name the package as their reference point.
* First pass for updating Lib
* Update metabase.lib.query.test-spec
* All Lib + Lib BE tests are passing
* Fix remaining tests in Lib + QP
* Update Lib JS template tag functions
* cljfmt
* Another round of test fixes 🔧
* Leave NativeQuerySnippet :template-tags alone for now; test fixes 🔧
* cljfmt
* kondo
* Test fixes 🔧
* FE test fix? 🔧
* Another round of test fixes 🔧
* Test fix 🔧
* Test fixes 🔧
* Test fix 🔧
* Test fixes 🔧
* PR feedback
* PR feedback
* 🔧
* Test fix 🔧
* Use qualified db names for locks in snowflake.
We're seeing some timeouts in Snowflake on CI. There are a few reasons
for this, but part of it is that we are locking a lot waiting for
access to the datasets. These locks are currently based on unqualified
database names, not hashed and branch-qualified, so they lock more
often than they need to. Using the qualified names spreads things out
a bit.
In order to do this, we need to change the `dataset-lock` method to
take the whole dataset (to hash it) not just the dataset's database-name.
* Fix upload test to not assume table set is stable.
The purpose of this test is to ensure that the created table is gone,
but the assertion it makes is far too wide. This breaks in snowflake
where we have other tests randomly creating tables in the shared cloud
DB across jobs.
* Rename snippets and application_permissions plugin modules to TS extensions
* Convert snippets and application_permissions plugin modules to TypeScript
* [pr-3] Move SDK error types to embedding-sdk-shared
* [pr-3] Move SDK auth-config types to embedding-sdk-shared
* remove un-useful usage of SdkCollectionId to remove import of sdk code in core app
we don't really need that type and they both end up being `number | string`
* [pr-3] Move PLUGIN_NOTIFICATIONS_SDK to the core app plugins
I don't particularly like this: this is one of those "plugins" used
both to activate enterprise code and to inject behavior from the SDK
into the core app. We don't have an alternative at the moment, so this
is the change with the smallest blast radius. The plugin was already
used in the core app, so this just moves it to the right place.
* [pr-3] Move onEditQuestion from the SDK dashboard context to DashboardContext
DashCardMenuItems (feature/dashboard) reached into the SDK bundle's
SdkDashboardContext just for the onEditQuestion override.
I moved the onEditQuestion to the DashboardContext instead, as
it's used in the core app dashboard.
* [pr-3] Remove CommonSdkStoryWrapper from SimpleDataPicker stories
It was not needed
* [pr-3] Move useLazySelector to the SDK package
The hook exists to read the lazy-loaded bundle's redux store from the
npm package, so the package is its natural home. Its only bundle-side
consumer, useInitDataInternal, already receives the redux store as a
parameter and now subscribes to it directly instead of going through
the window props-store singleton.
* [pr-3] Introduce SdkSharedStoreState
Some core app code currently needs to read _one_ piece of information
from the SDK redux state, I moved that piece of info into a new type
called `SdkSharedState` which I documented as being something we
should avoid adding to in the future.
* [pr-3] Promote embedding-sdk-bundle to the app tier
Promotes the sdk bundle to app tier, it needs a special rule for the
bridge
module that we'll hopefully clean up in the future
* [pr-4] Promote embedding-iframe-sdk to the app tier
EAJS becomes app tier, but we need to whitelist the theme preview from
importing it
* [pr-5] Move DashboardSharingEmbeddingModal to the dashboard module
The modal is dashboard UI living under metabase/embedding, giving the
embedding module an outgoing edge into dashboard (it imports
metabase/dashboard/selectors). Move it next to its domain so the next
commit can enforce the embedding module's outgoing boundaries.
* [pr-5] Move UserForm and group helpers to the common module
UserForm is used by the new "invite people to view" modal
(InviteToViewModal, which lives in metabase/embedding, shared tier),
so it moves out of feature/admin down to metabase/common together with
GroupsMultiSelect and the group utilities it depends on. Import paths
updated, no logic changes.
* [pr-5] Move DashCardMenuItem to the embedding-sdk plugins contract
It's imported by shared/embedding-sdk but it lived in feature/dashboard,
moved it down to remove violation that will be enforced on one of the next
commits.
* [pr-5] Enforce outgoing boundaries for shared/embedding and shared/embedding-sdk
* [pr-1] Define embedding modules
Defines the embedding modules without promoting them yet: modules whose
back-edges still need cleanup stay at their current tier (shared, outgoing
unenforced). Each module is then promoted to app in its own follow-up PR,
together with the fixes its back-edges require.
* [pr-1] Promote embedding-sdk-package to the app tier
The npm package is an entrypoint and should be able to import from
anything, and nothing should import from it.
I raised it up as app tier
* [pr-1] Promote embedding-sdk-ee to the app tier
This should probably be "embedding-features" in the future, but for now
I'm moving it to app to be above enterprise (which it imports from) and
to prohibit other things from importing from it
* [pr-2] Move LegacyStaticEmbeddingModalProps to the plugins contract
Module `embedding-iframe-sdk-setup` will be moved to app tier, so we move
`LegacyStaticEmbeddingModalProps` to the oss plugin file to avoid an import
going up to app
* [pr-2] Promote embedding-iframe-sdk-setup to the app tier
`embedding-iframe-sdk-setup` imports EAJS, which is app tier
so this also needs to be brought up to app tier
TBD how we want to structure this, but it kinda makes sense that some
parts of the core app are above EAJS as they use it (we also use it
to render theme previews in the admin settings for themes)
* Move root route onEnter/onChange/onLeave into components and effects
* Replace data-studio index onEnter redirect with a component
* Address review: settle LoadCurrentUser on rejection, drop test any-cast
* Point test Route import at the metabase/router facade
* Freeze has-user-setup on entry so the setup wizard is not interrupted
* Colocate route lifecycle components with the modules that own them
* Drop onEnter/onChange/onLeave from the facade's Route props
* Update to typescript 7.0 (beta)
* Switch to tsgo in scripts
* Remove NODE_OPTIONS from typecheck script
* Use typescript 7 instead of native-preview, but keep 6 around for lint
* Use typescipt 7.0.2
* Use real typescript 6.0.3 for eslint, run tsc 7 by explicit path
* Fix type errors surfaced by typescript 7
* refactor(sql-parsing): protocol + graal split, render.js-style context pool
Mirror the static-viz renderer structure (channel/render/js) for the sqlglot
parser:
- protocol.clj: strongly-typed SqlParser protocol for the sql_tools operations
- graal.clj (replaces pool.clj): GraalPy implementation on a 0..2 dirigiste
pool; contexts share one refcounted Engine and one bootstrap Source, created
with the first context and closed with the last, so an idle pool releases
all native memory (replacing the old TTL/min-size-1 leak mitigation);
timed-out contexts are interrupted and disposed instead of released
- common.clj: create-pool copied as-is from render.js.common (replaces the
PythonEval protocol); the future external-python impl will share it
- core.clj: public API unchanged; keeps JVM-side pre/post-processing
(VALUES stripping, field/error conversion) and delegates through the
protocol
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* tune(sql-parsing, static-viz): 0..3 pooled graal contexts, 10-minute idle reap
:idle-minutes is now an explicit create-pool option in both pool factories
(no default): graal pools (sqlglot + static-viz) keep idle workers for up to
10 minutes, the node renderer pool stays at 1.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(sql-parsing): native CPython parser, mirroring the static-viz node renderer
- python.clj: :python SqlParser running sqlglot in a pool of external CPython
child processes speaking newline-delimited JSON over stdin/stdout
(sql_tools_cli.py); in dev the child imports straight from
resources/python-sources, from the jar the sources are written to a fresh
temp zip per spawn (zipimport) and deleted with the process; single-process
pool (0..1, 1-minute idle reap), timeout kills a wedged process, one retry
when an acquired process turns out dead, call-level Python errors release
the healthy process back
- settings.clj + parser.clj: sql-parsing-mode setting (:graalvm default |
:python) and the mode-selection namespace, like static-viz-mode/renderer
- common.clj: python-sources locations + dev sqlglot install move here from
graal.clj (shared by both transports), plus make-parser building the
protocol reify over a transport call fn (marshalling written once)
- fix latent nil-schema bug in validate-query arg encoding: (json/encode
(or schema "{}")) double-encoded, making Python treat the string "{}"
as a strict-mode schema; encode an empty map instead
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sql-parsing): transport-agnostic parse errors; 10-minute idle for process pools
Both parser transports now throw the same ex-info for Python-side failures
(common/call-failed-ex: :sql-parsing/error + extracted Python error type) —
graal wraps PolyglotException, the CPython transport wraps its ok:false
responses — and core exposes parse-error? so callers stop catching
PolyglotException (sql-tools.sqlglot updated; a python-mode instance
previously bypassed its parse-error fallbacks entirely).
Also raise the external-process pools (sql-parsing python, static-viz node)
to a 10-minute idle reap, matching the graal context pools.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(sql-parsing): un-nest testing blocks
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* build(docker): install python3 for the :python sql-parsing mode
Both images already carry nodejs for the :node static-viz renderer; add
python3 (~45MB on alpine) so MB_SQL_PARSING_MODE=python works out of the box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* build(docker): default sql-parsing to python and static-viz to node in the entrypoint
The image ships python3 and node, so prefer the external-process backends
(lighter and faster than in-process GraalVM) unless MB_SQL_PARSING_MODE /
MB_STATIC_VIZ_MODE are set explicitly, following the MB_JETTY_HOST pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* tune(static-viz): keep the node process pool at a 1-minute idle reap
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(sql-parsing): wire settings into module init
metabase.core.init-test requires every .settings namespace to be reachable
from an init namespace; add metabase.sql-parsing.init and register it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Resolve the default search engine from capability precedence
The search-engine setting's :appdb default meant the engine precedence
list never chose semantic, no matter how many of its dependencies were
met. Engine selection now works in three layers:
- supported-engine? is a pure capability check (app-db type, premium
features, infrastructure) and no longer reads the search-engine
setting.
- The default engine is the configured override when servable,
otherwise the first supported engine in the precedence
(semantic > appdb > in-place). Unknown configured values warn once
and degrade instead of breaking all of search.
- Indexes are maintained for the default engine, the engines it
declares as dependencies (semantic mixes in appdb results), and any
engines opted in via the new additional-search-engines setting.
Reading the search-engine setting now resolves the engine that
actually serves search, so the admin UI needs no changes; the raw
override is available via configured-search-engine.
An explicitly passed per-request search_engine that cannot serve now
returns a 400 naming the cause (unknown / not supported / not enabled)
instead of silently falling back; legacy engine cookies keep the
fallback. The engine-active prometheus metric now reports active-set
membership rather than capability.
MB_SEMANTIC_SEARCH_ENABLED remains a kill switch, folded into the one
canonical semantic availability predicate (so repair/cleanup tasks now
respect it too); it will be removed once Cloud stops setting it.
Part of BOT-1832
* Tighten search engine docstrings and comments
* Validate cookie-carried search engines before use
A stale engine cookie naming an unmaintained index (e.g. appdb on an
in-place-default instance) now degrades to the default engine instead
of querying a missing index. The in-place exemption in engine-active?
uses = rather than isa?, so derived engines don't inherit a free pass.
Updates the prometheus search-engine-metrics-test to the new
engine-active semantics.
Part of BOT-1832
* Serve per-request engines through validated query-params; gate semantic writers on activity
Review follow-ups from roborev job 2600 and the workflow review:
- The engine middleware now reads and writes :query-params - the
:params injection had been dead since the api.macros migration, so
engine cookies had no effect on the served engine. Cookie values are
validated (stale/unknown degrade to the default), and param values
are normalized: blank means absent, the qualified form the API
returns in :engine is accepted, and the legacy fulltext name maps to
appdb at the boundary so activity checks use exact matches.
- engine-status in search.engine is the one classification of whether
an engine can serve (:ok/:unknown/:unsupported/:inactive); the API
layer maps it to 400s and no longer re-encodes engine invariants.
- Semantic fallback (EE core and the metabot search tool) prefers
engines with a maintained index over merely supported ones, so an
in-place default with additional-search-engines=semantic mixes with
the warm appdb index instead of in-place.
- The semantic indexer and index-repair tasks gate on the new
semantic-search-active? instead of availability, so a supported but
inactive semantic index no longer accrues hourly embedding spend.
- active-engines guards the pre-init nil default and only
capability-checks dependency entries.
Part of BOT-1832
* Canonicalize legacy engine names at every boundary; tolerate repeated engine params
search.engine/canonical-engine resolves legacy names (fulltext -> appdb)
for configured values, additional-search-engines, and request
params/cookies alike, so active-engine membership compares exactly.
A repeated search_engine query param parses as a vector; the middleware
now passes it through for schema validation to reject with a 400
instead of throwing.
Part of BOT-1832
* Review cleanups: single evaluation of engine resolution values
Bind configured-engine, the parsed param engine, and active-engines
once per expression instead of re-evaluating (Copilot); remove blank
lines inside the check-engine-serves! case form (CodeRabbit).
Part of BOT-1832
* Engine-active metric means can-serve; harden param parsing and dependency filtering
engine-active shares engine-status's definition: in-place is always
active while it exists, indexed engines while their index is
maintained. Fixes the prometheus test assertion that compared a metric
to itself.
param->engine strips the search.engine/ prefix rather than splitting on
slashes, so junk values 400 with the full value named instead of
validating their last segment, and degenerate values like // cannot
produce an empty or nil keyword.
active-engines filters dependency engines through known-engine? before
supported-engine?, matching the startup-ordering guard in
supported-engines.
Part of BOT-1832
* Reject malformed explicit engine values instead of treating them as absent
A non-blank search_engine that strips to a blank suffix (e.g.
search.engine/) parses to an unknown engine and 400s naming the raw
value; only genuinely blank values fall through to the cookie/default.
Part of BOT-1832
* Clarify the unknown-engine filter comment in supported-engines
* Fix review findings: runtime kill switch, csv hygiene, cookie unpinning, shared fallback
- Semantic Quartz jobs are scheduled when the instance is capable
(pgvector + feature) and check availability/activity per execution,
so the kill switch works at runtime again instead of gating job
scheduling at boot.
- semantic-search-active? is plain active-set membership; the HNSW
build gates on it like every other index writer.
- additional-search-engines values are trimmed, blanks dropped, and
qualified/legacy names canonicalized (prefix handling now lives in
canonical-engine, the one boundary fn).
- An explicit blank search_engine clears the engine cookie (the unpin
mechanism this branch had removed) instead of being ignored.
- fallback-engine lives in search.engine; the EE semantic core and the
metabot search tool share it.
- The runtime-activation backfill note moved from the
additional-search-engines description to a comment.
Part of BOT-1832
* Initialize the semantic index from repair when none is active
Semantic can become active at runtime (kill switch re-enabled, or added
to additional-search-engines) without init! ever having run; the hourly
repair task now initializes and backfills in that case instead of
no-oping until a manual /re-init.
Also renames warn-once to warn-once! per the side-effect convention and
adds end-to-end coverage for the pinned-cookie and blank-unpin paths.
Part of BOT-1832
* Cover the repair-initializes path; unpin test asserts the resolved default
The blank-unpin api test compared against a hard-coded appdb default,
which does not hold on MySQL/MariaDB app dbs. New repair test starts
from a blank pgvector db and asserts repair-index! creates an active
index and gates the documents.
Part of BOT-1832
* Trigger search index init when additional engines are set at runtime
Registers the (previously dangling) init-job-key as a durable,
trigger-less quartz job wrapping the search init, and the
additional-search-engines setter fires it when it exists. In tests the
scheduler is not running, so the setter stays side-effect free there;
env-var configuration is covered by the startup init.
Part of BOT-1832
* Rename the engine cookie and cover the pin/unpin round trip
metabase.SEARCH_ENGINE becomes metabase.SEARCH_ENGINE_OVERRIDE, so the
population of long-lived cookies written before engine validation
existed stays inert.
The round-trip test covers pinning via an explicit param, serving from
the cookie, and clearing it with a blank param.
Part of BOT-1832
* Order semantic capability gates cheapest first
The license check short-circuits before the pgvector URL check, whose
slot becomes a DB probe under BOT-1796.
Part of BOT-1832
* Only trigger search init when the setting change activates an engine
Semantic init re-gates every searchable document, so an unconditional
trigger made any write to additional-search-engines cost a full app-db
sweep. The setter now diffs active-engines across the set and triggers
only for newly activated engines.
Part of BOT-1832
* Defer the engine-activation trigger to after commit
The activation diff and quartz trigger run via do-after-commit in a
future (extracted to trigger-init-for-newly-active-engines!), observing
the final committed settings state - a surrounding set-many! may
include the setting that makes the engine supported - without I/O on
the transaction's thread.
The cookie round-trip test uses the in-process client so the handler
runs inside the fixture's engine and index bindings.
Part of BOT-1832
* Review nits: log async trigger failures, quote the cookie regex, fix a docstring
The post-commit init trigger logs exceptions instead of dropping them
in the future; the cookie test quotes the cookie name before building
its regex; build-hnsw-index-async! says active, matching its gate.
Part of BOT-1832
* Schedule semantic jobs on the boot-static gate; indexer initializes missing indexes
Job scheduling happens once at startup, so it now gates on the only
boot-static input: whether a pgvector DB is configured. The runtime
gates (license, kill switch, engine activity) are checked per
execution, so applying a license or re-enabling the kill switch at
runtime no longer strands semantic as an unindexed default until
restart.
The indexer's 20-second tick initializes the index when it is missing,
so every runtime activation path (license, kill switch, setter on a
stale cluster node) converges within seconds instead of waiting for the
hourly repair or a restart.
Also: the blank-param unpin deletes the engine cookie (max-age 0)
instead of writing an empty 20-year one; the activation trigger helper
lives in search.task.search-index with static requires; the setter test
uses poll-until for the async positive case and a direct synchronous
call for the negative.
Part of BOT-1832
* Runtime self-heal initializes all active engines and the HNSW index
The indexer's missing-index branch runs the generic search init, which
covers newly active dependencies, and applies the same HNSW-strategy
safety net as task registration.
Part of BOT-1832
* Ensure the HNSW index on the indexer's active path
An index-backed strategy configured while semantic was inactive misses
the setter's build event; the indexer tick now fires the async build
when the active index lacks its HNSW index. The pg_indexes existence
check is self-limiting: CONCURRENTLY registers the index when the build
starts.
Part of BOT-1832
* Fix engine tests on app dbs without appdb search support
Explicitly requesting an unservable engine now 400s instead of silently
falling back, so tests must not hard-code appdb: the data-studio search
tests iterate only supported engines, the cookie staleness test pins
in-place (servable everywhere), and the not-enabled case runs only
where appdb is supported.
Also realigns warn-once! call args (cljfmt).
Part of BOT-1832
* Keep semantic active under the search test fixtures
active-engines now derives from the default engine, so fixtures that
pin default-engine were silently deactivating semantic: semantic
queries in the metabot search tool fell through to the term engine.
Both fixtures pin active-engines to the set the old derivation gave
them - appdb plus semantic when supported, or semantic alone for
legacy search.
Part of BOT-1832
* Rename with-new-search-* test fixtures to with-appdb-search-*
The appdb engine predates semantic search; new no longer disambiguates.
Part of BOT-1832
* Update the last supported-implies-active consumers
The metabot semantic-search feature predicate gates tool schemas on
engine-status :ok rather than capability, so semantic_queries is only
advertised when a semantic index is actually maintained.
The metric collector runs only when semantic is active, and index
cleanup no-ops until the bookkeeping tables exist: available but never
initialized is now a steady state, and both jobs would otherwise warn
or throw about missing tables on every run. Cleanup still runs while
merely available so an opted-out instance's leftover indexes get
dropped.
Part of BOT-1832
---------
Co-authored-by: Tyler Plude <tsplude@gmail.com>
* Migrate query_builder/analytics to TypeScript
* Type Card.creationType and drop analytics cast
Address review: type Card.creationType as a union instead of a plain
string, narrow Question.creationType(), and make the new_question_saved
type field optional/nullable to match the Snowplow schema. Removes the
cast in the analytics tracker.
* Drop as const in load-question via Card annotation
* Re-own react-router-redux mechanisms behind the metabase/router facade
* Wire store, app and store types onto the re-owned router facade
* Re-point react-router-redux call sites onto metabase/router
* Update router tests and harness for the re-owned facade
* Ban react-router-redux imports outside metabase/router
* Model routing state's null location contract instead of casting
* Flatten CallHistoryMethodAction into per-method history action types
* Match history actions with ts-pattern in routerMiddleware
* Preserve explicit null location in createMockRoutingState
Adds `bun run email-dev`: a small Bun server + static page that renders
the Handlebars email templates in src/metabase/channel/email/ with
editable sample data, so designers and copywriters can iterate on email
copy and layout without running Metabase or sending real emails.
- live-reloads when a .hbs file changes on disk (1s polling)
- Fields/JSON panels to fill in template values; edits persist in
localStorage per browser
- Desktop/Narrow/Phone width toggles and Copy HTML
- sample-contexts.js mirrors the real send-time contexts (traced from
messages.clj and the notification handlers)
- mirrors the jknack custom helpers (equals, format-date, now, initials,
card-url, dashboard-url, trash-url) in handlebars.js
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Migrate Link, ListItem, and LoadingSpinner from Emotion to Mantine + CSS modules
* Fix TableList disabled-state assertions by exposing data-disabled instead of relying on Emotion-injected styles
* Name the referring entity in remote sync not-found errors
When an import references content that doesn't exist on this instance, the
error named only the missing entity's raw serdes id. It now also names the
entity holding the dangling reference, and backtick-quotes names and ids so
leading and trailing whitespace is visible.
`load-one!` takes the referrer as an explicit parameter rather than carrying
it on the load context: `load-deps!` reduces over sibling deps and each
recursive `load-one!` returns its own context, so a context key would leave
later siblings labelled with the deepest child's referrer.
Message construction lives in `missing-reference-message` so the format can
change without touching the throw sites.
* Attach the referrer by catch-and-rethrow instead of threading it
Replaces the extra `load-one!`/`load-deps!` parameter with a try/catch around
the dependency load. The nearest enclosing `load-one!` unwinds first, so it is
the frame that attaches itself as the referrer; outer frames see the key
already present and rethrow unchanged. The original exception is kept as the
cause, preserving the stack trace of the detection site.
Adds an end-to-end assertion in bare-import-test that a real import of a Card
referencing a missing Database carries the Card as `:referrer`, which the
hand-built ex-info unit tests could not cover.
* Key the database-not-found error off ex-data, and name the referring card
`source-error-message` detected this case by matching "database not found"
against the cause's message text, then interpolated that raw internal string
into the user-facing error:
Import failed: A referenced database does not exist on this instance.
table id present, but database not found: [clickhouse nil some_table]
`import-table-fk` now tags the throw with `:error ::database-not-found` and
`:db-name`, and the render walks the whole cause chain looking for that key
rather than checking only the immediate cause. `::load-failure` carries the
referring entity, so both missing-content paths now render through
`missing-reference-message`:
Import failed: Card `Some card` (`abc123`) references Database (`clickhouse`),
which does not exist on this instance. ...
Also fixes `:database-names` in that ex-data, which selected names from
:model/Table rather than :model/Database. It has no consumers.
The end-to-end test uses a result_metadata `:table_id`, since that is the path
that actually reaches `import-table-fk` with an undeclared database:
`result-metadata-deps` derives deps only from `:field_ref`, and a Card's own
top-level `table_id` is recomputed from `dataset_query` by
`populate-query-fields` on insert.
* Backtick-quote file paths in remote sync ingest-error messages
GHY-3992 asks for content names to be quoted so leading and trailing
whitespace is visible. The ::not-found path already did this; the
ingest-errors branch still joined raw paths, and the message already uses
": " between path and reason and "; " between entries, so a path containing
punctuation or trailing space was ambiguous to read.
No fallback is added for a nil :file. All three ingest-error producers
(ingest.clj file-hierarchy!, ingestable.clj read and parse failures) always
set it, and only YamlIngestion and IngestableSnapshot produce a non-empty
ingest-errors; the other Ingestables delegate.
Splits source-error-message-entity-not-found-test into three deftests, one
per error branch, which kondo requires now that each has grown assertions.
* Render load-failure errors with the entity name, reason, and stripped keys
::load-failure fell through to the generic "Failed to reload from git
repository: <ex-message>" fallback, showing a NanoID and burying the
stripped-keys note. It now reads:
Import failed: could not save Card `Orders by Month` (`abc123`).
NOT NULL constraint failed: report_card.display.
and, when the entity was part of a dependency loop, adds "It may have been
saved without: `dashcards`, `parameters`." — a partial row for it survived
the import, which the user previously had no way to learn.
Renames the ::load-failure ex-data key `:referrer` to `:entity`. It names the
entity that failed to load, which is a referrer only relative to the missing
database in the ::database-not-found case; that branch now reads `:entity` and
passes it as the referrer. ::not-found keeps `:referrer`, where it really is
the parent.
The new branch is ordered after the tenant-collection and database-not-found
branches, both of which match on causes that a ::load-failure wraps.
::circular is left on the fallback. Rendering its cycle needs an ordered
expansion chain — `expanding` is a set — and it only fires when the
key-stripping recovery has already failed.
* Address CodeRabbit review
- Remove a duplicated `testing` block in impl_test.clj. It was byte-identical
to the one above it, introduced when the deftest was split. Replaced with a
doseq covering trailing, leading, and no surrounding whitespace.
- Mark `missing-reference-message` and `load-failure-message` private; neither
is used outside impl.clj.
- Capture the exception once in bare-import-test's not-found case instead of
running the same failing import twice, and flatten the resulting nested let.
* Fix metabase.metabot.api/openrouter-model-group
Fallback to parsing the id if the display_name has no ":"-separated prefix.
* Add a default OpenRouter model so connect-without-model works
Without an entry in default-llm-metabot-model-by-provider, a PUT /api/metabot/settings that connects openrouter with
only an API key would save the key but silently fail to switch llm-metabot-provider.
* Use real dotted OpenRouter model IDs in defaults and docs
OpenRouter model IDs use dots in version numbers, e.g. anthropic/claude-haiku-4.5. The hyphenated
anthropic/claude-haiku-4-5 referenced by the default args and docstrings does not exist.
* FE: enable openrouter provider in metabot admin UI config
* Whitelist openrouter models
Filter openrouter list-models down to the same curated Anthropic and OpenAI model set the direct providers support,
instead of returning the full ~340-model catalog.
* Throw an exception if callers pass :ai-proxy true for openrouter
Similar to bedrock and openai, supporting this would require work on the ai-service side, and we already reject
non-anthropic providers in validate-metabase-managed-provider!
* Remove explicit credentials check in list-all-models
We want to allow callers to omit the creds and fallback to the configured api key from settings. core/resolve-auth
still handles the auth check and will throw if no credentials are provided or configured.
* Support prompt caching for openrouter
Report `:cacheCreationTokens` and `:cacheReadTokens` in the `:usage`.
For anthropic models, add the `:cache_control` breakpoint on the system message to enable caching (openai caching is
automatic).
* Apply code review suggestion in AIProviderSettingsSection.unit.spec.tsx
Co-authored-by: Maksym Yakubych <undrfined@gmail.com>
* Add gpt-5.6 and drop gpt-5 from openrouter supported-models
See also #77450
* Apply code review suggestion in fe tests
Co-authored-by: Maksym Yakubych <undrfined@gmail.com>
---------
Co-authored-by: Maksym Yakubych <undrfined@gmail.com>
* Move remote-sync branch switching into Settings with guard rails (GHY-4019)
Branch switching ran a full import that hard-deletes any synced-collection
content absent from the target branch, and it lived as a routine app-bar
dropdown item. A forced switch could permanently delete never-pushed content.
Backend:
- spec: detect collection-scoped deletion conflicts (Cards/metrics, Dashboards,
Documents, Timelines), not just the transform family; wire into get-conflicts
- import: expose force_deletion on POST /import so a forced version/dirty
override no longer silently forces deletions
- async-import dirty guard now returns the dirty objects (what would be lost)
- PUT /settings rejects a read-write branch switch (must use the guarded import);
read-only changes and first-time config still allowed
Frontend:
- remove the Switch branch affordance from the app-bar sync controls (branch
name stays as a read-only label; push/pull intact); drop the switch-branch
conflict variant
- add a guarded branch switcher to the read-write Settings panel: destructive
warning copy, hard block when there are unsaved changes (lists them, no force
path), destructive confirm when clean, stale-tab handling
* Refine Settings branch switcher: choice modal, admin gating, direct clean switch
Follow-up to the branch-switching rework, from review feedback:
- Offer a choice modal (push / new branch / discard) when switching with
unsaved changes, instead of a hard block — restores the switch-branch
conflict variant, now driven from the Settings panel BranchSwitcher.
- Drop the destructive confirmation on a clean switch: with everything pushed
the switch is reversible, so it runs directly.
- Show a red "N unsaved change(s)" label beside the dropdown when dirty.
- Gate the switcher on superuser: non-admins (e.g. settings managers) see the
section with an "admin required" message instead of a control they can't use;
the section no longer depends on the admin-only branch setting being readable.
- Match the section subtitle styling to the other settings sections.
- Strengthen the import superuser test to cover a branch switch.
* Update remote-sync E2E for Settings-panel branch switching
Branch switching moved from the app bar to the Settings panel, so the
branching specs and helpers drive it there now:
- Replace the app-bar switch helpers with Settings-panel ones
(visitRemoteSyncSettings, getSettingsBranchSwitcher,
createBranchViaSettings, switchBranchViaSettings).
- Rewrite the branching tests to create/switch branches on the Settings
page, navigating back to the app for content changes.
- Switching with unsaved changes opens the choice modal (wait for the
unsaved-changes indicator first); a clean switch goes directly.
- Snowplow triggered_from: app-bar -> admin-settings.
* cljfmt
* Address review: guard-bypass fix, anti-join, shared removal predicate
- api: block blanking the branch in read-write mode too, so it can't be used to
reset the branch-switch guard and then switch freely; regression test added.
- spec: compute collection deletion conflicts with a SQL anti-join + count/limit
instead of loading every candidate and an exists? per row, so it scales when a
switch would drop a large synced collection.
- Extract spec/removal-where-clauses as the single definition of what an import
reconcile removes for a model; remove-unsynced! and the deletion-conflict check
both build their WHERE from it (removes the duplicate build-entity-id-where-clause).
- Add a parity test asserting the conflict warning is exactly the unsynced subset
of what remove-unsynced! removes.
- Nits: be.visible for the dirty-warning indicator; wrap a long test string.
* Build the deletion-conflict WHERE with a thread-first instead of nested into/conj
* Give check-deletion-conflicts the same anti-join as the collection check
Was doing a t2/exists? per candidate (and materializing every candidate).
Now counts via one SQL anti-join, sharing removal-where-clauses and the new
model-id-column / unsynced-anti-join helpers with check-collection-deletion-conflicts.
Adds a direct spec-level test for it.
* Build collection deletion conflicts with cond-> instead of if/into
* Cover NativeQuerySnippet in the deletion-conflict warning
Generalize check-collection-deletion-conflicts -> check-content-deletion-conflicts:
warn before an import deletes any unsynced entity-id content, not just collection-
scoped models. This pulls in NativeQuerySnippet (global, no collection scope), which
otherwise fell through both conflict checks and could be silently deleted on a forced
import/branch switch. Collection stays excluded (structural; its contents are covered).
Adds a snippet test case.
* Address branch-switch review: drop force_deletion param, refetch dirty at switch, add tests
- Remove the unused force_deletion param from POST /import (schema, destructure,
and async-import! call); internal force-deletion? threading is unchanged.
- BranchSwitcher refetches dirty state at switch time so a stale settings-page
snapshot can't route a switch down the wrong (delete-without-warning) path.
- Add BranchSwitcher unit tests: non-admin message, dirty count label,
clean direct switch, and the choose-what-to-do modal on the dirty path.
* Left-align the branch value in the settings switcher for consistency with other selects
* Show a 'Using <env var>' note when the sync branch is pinned by an env var
* Fix branch-switcher e2e: scroll the switcher into view, drop spurious branch-switched event
The Sync branch section sits below the fold at the default viewport, and Cypress treats an element
clipped by a scrollable ancestor as hidden, so the switcher assertions failed (surfaced by
testing-library as a misleading "Unable to find an element"). Scroll to it before asserting.
BranchSwitcher also reported a branch switch when creating a branch, emitting a spurious
remote_sync_branch_switched alongside the branch picker's remote_sync_branch_created. Creating a
branch forks the current one without a reconcile, so only the creation is reported now.
Introduces a `QUARANTINE_ENGINE` variable that acts as a control knob. Either Trunk enforces a quarantine or CI conductor does. So far wired only in three places:
1. fe
2. be
3. e2e
* Preserve native model column types during serialization (GHY-4043)
export-result-metadata checked (model? card) before the native branch, so
native models were routed through lib/model-preserved-keys — a metadata-merge
soft-key set that omits :base_type/:effective_type. On a read-only Remote Sync
target the column types could not be re-derived without executing the SQL, so
computed columns (e.g. STRING_AGG(...) AS names) fell back to type/* and their
filter UI collapsed to Is empty / Not empty.
Reorder the cond so native cards — models included — go through the branch that
preserves structural type info; only MBQL models, whose types are re-derivable
from the query at import, use the soft-key set.
* Update native-model extract test for preserved column types
extract-result-metadata-native-model-test asserted the old behavior — that a
native model dropped :base_type on export. Update it to expect structural type
info to survive, matching the GHY-4043 fix. Drop the now-unused clojure.set
require.
* Ensure openai reports `:cacheReadTokens` in `:usage`
Add `openai-usage->aisdk-usage` to convert openai usage format to aisdk format and report `:cacheReadTokens`.
* Add undocumented :cache_write_tokens as well
`parse-grant` lowercased role names parsed out of `SHOW GRANTS FOR <user>`.
Those names are fed back into `SHOW GRANTS FOR <user> USING <roles>`, and
MySQL 8 treats backticked role identifiers as case-sensitive, so any role
with uppercase characters made the follow-up query fail with error 3530 and
took the whole metadata sync step down with it.
Lowercasing is still correct for the privilege types in the sibling
`:privileges` branch; it was never right for role identifiers.
Also add the missing space before `USING` in the follow-up query, which
only parsed because the user identifier ends in a backtick.
* Static-viz: dispose ECharts SSR instances + faster SVG dominant-baseline patch
Two isolated, output-preserving fixes to the static-viz render path:
- Dispose the ECharts/zrender SSR instance after renderToSVGString() in every
chart component. These instances were never released, leaking ~MBs per render
and OOMing a long-lived renderer process; memory is flat after.
- Replace patchDominantBaseline's full hast parse+serialize round-trip over the
whole SVG with a targeted string transform (like the sibling
transformSvgForOutline), cutting a large share of big-chart render time with
byte-identical output.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Update BoxPlotChart.tsx
* Update ComboChart.tsx
* Update PieChart.tsx
* Update SankeyChart.tsx
* Update ScatterPlot.tsx
* Update TreemapChart.tsx
* Update WaterfallChart.tsx
* Drop hast deps entirely; svg test uses DOMParser
The SVG dominant-baseline patch no longer uses hast, and its test was the last
user of hast-util-from-html. Rewrite the test with the jsdom DOMParser and
remove hast-util-from-html and hast-util-to-html from the deps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Fix formatting in ComboChart.tsx
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* [Metabot] Migrate BE to v2 message format (#75672)
* migrate codebase to writing v2 format
* move to camelCase to match upstream schema changes
* improvements
* improvements
* correction
* metadata corrections
* handle string outputs correctly
* put add error code to message metadata in finish message
* self-review pass
* record step start to db
* correct issue in claude provider that sent too long of message ids
* record text state to open up future ability to write in progress work
* increase malli usage
* limit client streamed output for tool calls
* emit cache token usage correctly
* ensure started before error is emited
* update migration version
* [Metabot] FE read new message format (#76429)
* initial impl
* self review
* update e2e tests
* copilot feedback
* pr feedback
* fix lint
* feedback - test helper ergonomics
* misc. fixes to metabot chat improvements integration branch (#77321)
* fixes due to stale branches, correct data- prefix not getting recorded, remove unused code
* clean up debug visuals
* ci fixes
* fix couple more tests
* pr feedback
* test: add repro for SDK viz-settings column edits being dropped
Reproduces EMB-2057 — in modular embedding (embed.js), removing a
column via Visualization settings > Add or remove columns silently
no-ops. Fails against the current QuestionSettings.tsx onChange,
which drops the second question argument carrying the query edit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix: forward query edits from viz settings in SDK QuestionSettings
QuestionSettings.tsx's onChange dropped the second `question`
argument that ChartSettings forwards for query-level edits (e.g.
add/remove columns), applying only the settings to the stale closure
question and never re-running the query. Column-removal edits landed
in the query object but the table kept rendering the old result.
Forward `nextQuestion` when present and re-run only in that case, so
pure viz-settings-only edits (no query change) keep their existing
no-run behavior.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Fix code format
* move test to where it belongs
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Currently they are timing out with some regularity in CI. We've done
this same thing to bigquery and redshift already.
Not only will this get us pass/fail results more quickly; it also
reduces the scope of failures so retries don't have to redo as much
wasted work.
* docs: add page for treemap visualizations
* Optimized 1 images, saved 237.8 KB (44.8% smaller)
* fix broken link
* Edits from Jeff
* Edits from Robert
* Edits from Jeff 2
---------
Co-authored-by: Metabase Automation <github-automation@metabase.com>
Stop running metabase-enterprise.transforms-inspector-test in cloud DBs.
These tests create new tables in the default schema, which is fine for
most drivers, but not for cloud DBs like snowflake and bigquery, because
concurrent CI jobs share the same jobs, which means this will cause test
failures in other unrelated tests which list tables, such as
metabase-enterprise.impersonation.driver-test/resilient-connection-options-test
We need to get these tests to stop creating tables in the default schema,
then we can drop the requirement for the :test/dynamic-dataset-loading feature.
* Replace the group picker in the user form with a multiselect
* Port the tenant group-picker e2e steps to the new multiselect
* Add group-manager controls and a section separator to the multiselect, with chevron and Backspace fixes
* Fix external-default group ordering in the multiselect dropdown, with manager-flag and divider tests
* Update frontend/src/metabase/admin/people/components/GroupsMultiSelect/GroupsMultiSelect.tsx
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Update remove-button test assertions for the group-specific aria-label
---------
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* Serialize static-viz GraalJS context creation (GHY-4077)
Even with the shared-Source code cache (#77180), evaluating the bundle
in a fresh context churns through ~500 MB of allocation. That memory is
transient — allocated and discarded during the eval, with only ~10 MB
retained per extra live context — but it all still has to pass through
the heap. The dirigiste pool grows during render bursts, and nothing
stopped 2-3 context creations from running concurrently: ~1-1.5 GB of
allocation churn stacked on top of in-flight renders, which on a 2 GB
heap outruns the collector and drives GC thrash / OOM.
Serialize creation on a private monitor so at most one generate runs at
a time; waiting acquires just queue on the pool. Also jitter the
10-minute expiry timestamps (+/- 3 min) so pool members created in the
same burst don't all expire and regenerate in the same window.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Document static-viz pool memory model in namespace docstring
Capture what the OOM investigation measured so the pool's design
choices stay legible: engine + parsed sources ~220 MB retained (paid
once); fresh-context eval ~500 MB of collectable churn with only
~16 MB retained per live context (first-in-JVM cold parse ~1.2 GB);
renders ~1 MB/row transient. Also spell out why creation is serialized
on a monitor rather than time-throttled: bursts genuinely need
multiple contexts, so the pool still grows — just one creation at a
time.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Make test stats upload more robust
* Fix trailing whitespace
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Fix rendering of tenant avatars
* Remove unused Avatar code, cleanup initials logic, more explicit types
* Fix type used in isUser
* Stricter types to avoid initial bug in the future
* Wire embedding tests to ci-conductor reporting
* Add ci-conductor quarantine gate to embedding Cypress jobs
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add withRouteProps facade shim for the router migration
* Convert DatabaseListContainer to a function component on facade hooks
* Put route-prop reference containers on withRouteProps
* Require route props in withRouteProps generic constraint
* Convert ScrollToTop from withRouter to useLocation hook form
* Fix merge fallout: rename RouteProps shim and repoint react-router imports
* Facade re-exports full react-router surface via a single leaf seam
* Re-point all react-router imports onto the metabase/router facade
* Ban react-router imports outside the metabase/router facade (eslint)
* Migrate InviteToViewModal Link import onto metabase/router facade
* Add serialization-dependencies multimethod + Card impl (GHY-4010)
Foundation for export-time dependency validation. serialization-dependencies
runs on a raw appdb entity and returns its references tagged :content (must be
in the export) or :data (database/table/field that must exist in the appdb).
Unlike the load-time `dependencies` multimethod, it operates pre-serialization
and includes tables/fields, which import otherwise creates on the fly.
The Card impl derives content deps from `descendants` (so the two can't drift)
and adds data-model deps from the query via the lib walkers. Not yet wired into
the export pipeline.
* Replace escape analysis with general dependency validation (GHY-4010)
extract-subtrees now runs a dry-run pass (collect-dependencies) that shares the
real export's extract queries but neither serializes nor writes. For every entity
that will be extracted it derives serialization-dependencies and checks them:
content references must be in the visited/extracted set (or be analytics cards),
and data-model references (database/table/field) must exist in the appdb. Any
unsatisfied reference aborts the export (unless continue-on-error), before any
archive is produced.
This generalizes part A's Card-only, collection-id escape analysis: it now also
catches non-Card content escapees and dangling data-model references (a deleted
table/field that would otherwise serialize to a malformed [nil nil nil] ref).
Adds a Dashboard serialization-dependencies impl. Tests not yet updated.
* Update serialization tests for dependency validation (GHY-4010)
The dependency-validation warnings replace the old "Failed to export
Dashboard/Cards ... outside requested collections" messages with a uniform
"<entity> references <model> <id> which is not included in the export /
is missing from the source database" form, so the escape tests now assert
on the new wording. Adds dependency-validation-missing-data-ref-test, which
covers the new data-model case: a card referencing a deleted field aborts the
export (and continue-on-error still exports the rest).
* Fold dependency collection over a monoid, with batched queries (GHY-4010)
Replaces the nested reduce in collect-dependencies with a {:visited :deps}
monoid: entity-deps maps one entity to its contribution, merge-deps is the
associative combine (0/1/2-arity, usable directly as a transduce reducing fn),
and collect-dependencies folds every entity into the whole via a single
transduce. content-id-batches yields [model id-batch] pairs so each extract
query's :in :id clause stays bounded (query-batch-size 1000), and the data-dep
existence check is batched the same way. Behavior is unchanged.
* Flatten dependency collection into four transducer stages (GHY-4010)
entity-deps now reads its model off the entity (extract-query returns modeled
rows) instead of taking it as an argument, so collect-dependencies becomes a
flat pipeline: filter content models, resize into id batches, mapcat each batch
through extract-query into an entity stream, then map entity-deps — no eduction
nested inside the mapcat. Behavior unchanged.
* Move dependency validation into its own namespace (GHY-4010)
Extract the export-time dependency-satisfaction check out of
serialization.v2.extract into serialization.v2.dependency-validation. extract
now just calls dependency-validation/validate-dependencies! and keeps ownership
of the analytics-card set, which it passes in. The moved code carries this
session's cleanups (map-kv-vals for the existence lookup, set-based :deps).
Tests move to a matching dependency-validation-test namespace; the escape
assertion embedded in selective-serialization-basic-test stays with extract as
an extraction-level integration check.
* Address review: drop :kind, transients, cover segment/measure (GHY-4010)
Alex's PR comments on the raw dependency validation:
- serialization-dependencies no longer tags deps with :kind; it returns plain
{:model :id} and the validator classifies each by model (serdes.models
content vs data-model), since serdes already knows which is which.
- collect-dependencies accumulates into transient sets — an export can walk a
lot of entities.
- Card serialization-dependencies now covers every reference in the query,
including :segment and :measure (both data-model, existence-checked), via the
lib walkers, closing the gap the field/table/card-only version had.
Refs embedded in parameter_mappings / visualization_settings / result_metadata
still aren't covered, and the raw serialization walker still duplicates the
serialized *-deps knowledge; both are tracked for the follow-up unification.
* Cover result_metadata and visualization_settings field refs (GHY-4010)
serialization-dependencies "Card" now gathers Field references from every
routine-backed source, not just the query:
- query: lib walkers (fields, tables, segments, measures, template-tag fields)
- result_metadata: each column's :id
- visualization_settings: the JSON-encoded column-setting keys, parsed with
mb.viz/db->norm (the same routine the dependency-analysis project uses)
New test covers a field referenced only in visualization_settings. Field refs
buried in parameter_mappings / parameters value-fields are bare [:field ...]
clauses the lib walkers reject; those remain for the dep-walker unification.
* Rename `dependencies` multimethod to `deserialization-dependencies`
Makes the load-time dep multimethod parallel with the export-time
`serialization-dependencies`. `dependencies` runs on the serialized/ingested
form for load ordering and omits tables/fields (import synthesizes them);
`serialization-dependencies` runs on the raw entity and includes them. The
name now says which is which.
Mechanical rename of the defmulti + default + 19 impls across 18 models + 3
callers (load, metabot, remote-sync) + tests + doc-links. No behavior change.
* Address CodeRabbit: batch analytics lookup, fix test targeting (GHY-4010)
- analytics-card-ids batches the card-id :in via a partition-all transducer, so
a large closure doesn't exceed database parameter limits (matches the batching
already used in dependency collection).
- escape-report-test's ":parameters" case targeted coll2 (re-testing the
source-table card); target coll3 so it actually exercises the parameter card.
- Correct the continue-on-error test wording: it suppresses the abort but does
not skip the affected card at export (the malformed card is skipped on import).
* Unify serialization/deserialization dependency walkers (GHY-4010)
Make the mbql-deps walker family format-aware so a single traversal handles
both raw (numeric ids, export/serialization) and serialized (portable refs,
load/deserialization) forms, gated by *serialization?* / raw-ref-id?. Card and
Dashboard now share one deps helper across both dependency multimethods.
Implement serialization-dependencies for the remaining content models
(Action, Collection, Document, NativeQuerySnippet, Timeline), computing deps
from the raw entity: Action fetches its query_action child row; Collection
derives its parent from the raw :location path.
Refine export-time validation: a content reference to a deleted entity is not a
failure (export can't emit a portable id for a missing row, so fk-elide drops
it), and structural Collection references are ignored (a selective export may
omit the containing collection tree).
Tests: mbql-deps format-parity test, direct per-model serialization-dependencies
coverage, and the full baseline load->extract round-trip over every model.
* Trim duplicated dependency docstrings and consolidate abort tests (GHY-4010)
Dedupe the "content refs must be in export / data-model refs must exist"
explanation that appeared in four docstrings, and collapse the three
near-identical "delete a data-model ref -> export aborts" tests into one
parameterized test over Field/Segment/viz-settings references.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Share query-batch-size constant; use pos-int? for raw ref ids (GHY-4010)
Hoist the `:in`-clause batch size into a single serdes/query-batch-size
constant instead of a private def plus a bare 1000 literal in extract, and
tighten raw-ref-id? to pos-int? (entity ids are always positive).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Replace transient accumulator with a named merge-entity-deps monoid (GHY-4010)
The set-building is not the hot path (batched DB queries and the mbql walk
dominate), so drop the transients for a plain `merge-with into` monoid,
extracted as `merge-entity-deps` — the same idiom as u/traverse and
remote-sync's merge-incremental-export-plans.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fold partition-all into the existing-ids transducer (GHY-4010)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fold distinct into the existing-ids transducer too (GHY-4010)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Use peek and set-as-predicate in dependency validation (GHY-4010)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* SPIKE: thread allow-int-ids? param instead of *serialization?* dyn var (GHY-4010)
Replaces the *serialization?* dynamic var with an explicit allow-int-ids?
parameter threaded through the mbql walkers (mbql-deps/-map/-vector,
parameters-deps, visualization-settings-deps, viz-link-card-deps) and the
shared model helpers (card-deps, dashboard-deps, serdes-deps-dashcard,
result-metadata-deps). The name states the behavior: integer ids count as
references only when set (raw appdb entities), never on the serialized/load
form where a stray numeric must be ignored.
The parameter is mandatory everywhere — no default arities. Load-side callers
(measure/segment/transform/action deserialization impls) pass false; the three
serialization-dependencies impls pass true.
Spike to compare against the dyn-var approach; drop if we prefer that.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix cljfmt: align card-deps destructuring continuation lines (GHY-4010)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Don't compute related-tables or metrics in get-report-details
`get-report-details` drops `:related_tables` and `:metrics` via a `select-keys`, so don't ask `card-details` for them.
* Migrate remaining color tokens to new names (GDGT-2535)
Migrate the remaining legacy design-system color tokens to their new
names per the GDGT-2517 migration guide:
- feedback: error/danger -> feedback-negative, success ->
feedback-positive, warning/gold -> feedback-warning
- misc/core: info -> core-info, white -> core-white, shadow ->
shadow-default, overlay -> modal-overlay, focus -> input-focus,
metabase-brand -> core-metabase_brand
- admin: admin-navbar* -> navbar-admin*
- upsell/illustration: per guide
- remove unused bronze/copper/silver
Covers all reference forms: CSS `var(--mb-color-*)`, `color()`/helper
calls, Mantine `c`/`bg`/`color`/`iconColor` props (string and ternary),
object-literal `color:`/`iconColor:` keys, `ColorName` return values,
arrays, and `Record<_, ColorName>` value maps. Tightens `Undo.toastColor`
from `string` to `ColorName` so future token removals fail at compile
time. Maps the `Alert` component's semantic `color` prop to feedback
tokens internally so its call sites stay unchanged.
Also sweeps up brand/filter/summarize consumer stragglers left over from
the earlier (merged) migration. SDK/whitelabel input keys, the
derive-theme alias definitions, and static-viz's local palette are left
on their existing names by design.
* Migrate missed success-secondary props in StepperWithCards
The .module.css was migrated to feedback-positive-selected but the two
inline c="success-secondary" props (Done check icon + label) were missed.
* Update Loki Snapshots
* Fix toastColor type widening in metabot toast-prop helpers
Undo.toastColor tightened to ColorName in this branch; the two get*ToastProps
helpers returned object literals where toastColor widened to string (missing
as const, unlike their iconColor/dismissIconColor siblings).
* Update TransformListPage sync-color spec to migrated feedback token names
utils.ts color map was migrated (success/warning/danger -> feedback-*) but
the spec assertions still expected the old names.
* Update SDK theme + treemap specs for feedback/misc token mirrors
- get-embedding-theme: SDK mapping now fans text-white->core-white and
focus->input-focus, so the expected override gains those keys.
- treemap option: getColor("white") became getColor("core-white"); add
core-white to the test's MOCK_COLORS palette.
* Update Loki Snapshots
* Update toast color-attr assertions to migrated feedback token names
* Cleanup
* Migrate remaining color tokens in Button and Table stories
* Add explainer comment for colors naming convention
---------
Co-authored-by: Metabase Automation <github-automation@metabase.com>
* fix: Toaster crash on long-running questions from color-mix tokens
The Toaster's styled-components computed colors at render time with the
JS `color` library (via the deprecated lighten()/alpha() helpers). One
of the tokens they parsed, `background_page-tertiary-inverse`, is now
defined as a color-mix() value, which the `color` library can't parse —
so it threw "Unable to parse color from string" and crashed.
Replace those runtime helper calls with CSS color-mix on the variables
directly, so the mixing happens in CSS where color-mix()/var() resolve.
* Adjust dismiss color
* Add react-router v7 as a test-only aliased devDependency
* Add differential conformance harness for the router facade
* Add v7 conformance test for useLocation
* Add v7 conformance test for useParams
* Add v7 conformance test for useNavigate
* Add v7 conformance test for useSearchParams
* Add v7 conformance test for Navigate
* Add v7 conformance test for useSearchParams defaultInit
* Pin react-router v7 to 7.18.1 for conformance
* Add type-level conformance tests against react-router v7
* Guarantee cleanup between facade and v7 renders in runBoth
* Migrate ChartSettingsSidebar unit test to TypeScript
* Migrate DatasetMetadataStrengthIndicator unit test to TypeScript
* Migrate QuestionDataSource unit test to TypeScript
* Migrate ViewTitleHeader unit test to TypeScript
* Correct onSave return type to Promise<void> and relax Toaster className
* Migrate query builder View components to TypeScript
* Preserve collection type on move and tighten timeline callback types
* Remove dead onCloseQuestionInfo prop from structured sidebar
`enhance-context-with-schema` resolved each `:user_is_viewing` item's used tables and then ran them through
`table-utils/enhanced-database-tables`, which fetched and formatted the full visible-columns set for every used table, including all implicitly-joinable columns from related tables. That work was discarded anyway since columns were recalculated via `entity-details/get-table-details` when rendering the context. On large, highly-connected schemas this could load thousands of columns into the request metadata cache and exhausted the heap (metabase#76493).
`:used_tables` entries now carry only `{:id :type :name :database_schema :description}`. name/schema/description are kept for the `simple-entity` error fallback. `enhanced-database-tables` had no other callers, so it is removed.
Fixes UXW-4704.
Support emoji in PDF rendering - in card titles, text cards, and the dashboard title.
These are monochrome only, since monochrome emoji are regular font glyphs. Colour emoji requires unusual bitmap-backed font support, and the PDFBox engine doesn't support that. It could be done, but it would require rendering the emoji glyphs to PNGs, then embedding those in the PDF. It's not worth the effort or including a bulky bitmap emoji font for a cosmetic feature.
This includes the Noto Emoji font in the JAR, and adds the Open Font License notice file covering all the PDF fonts.
The "Transforms" group header in the push/pull changes list is backed by a
virtual collection with the sentinel id -1 (TRANSFORMS_ROOT_ID). Every -1 case
is special-cased in displayGroups.ts except the header breadcrumb, so the
sentinel flowed into collectionUrl({ id: -1 }) and produced a dead
/collection/-1-transforms link (a hyphen at the start of the last path segment).
Link the transforms-root segment to the transforms list (/data-studio/transforms)
instead. Covers both the virtual-root group and the transforms-namespace prefix
on sub-collections, since the guard sits at the single render site.
* Whitelist bedrock models
* Whitelist anthropic models
* Add claude-fable-5 to supported-models for bedrock and claude providers
* Include a display_name for all supported-models
* Filter to available-model? in bedrock's list-models
Snippet template-tag ids were regenerated on every serdes import: YAML
ingest keywordizes the tag-name keys, so the NativeQuerySnippet load hook
could not match them to preserve the existing :id and minted a fresh UUID
instead. In a remote-sync import/export loop this produced a spurious id
diff for every snippet with variables on every cycle.
Normalize :template_tags on import in make-spec so the tag-name keys are
strings before the model hooks run; export is unchanged. Add a round-trip
regression test asserting template-tag ids survive export/import.
* [GHY-3974] Add failing tests for Usage Analytics upgrade failure modes
Two invariant tests covering the audit-DB upgrade breakage, asserted
through ensure-audit-db-installed! so they're independent of the fix
mechanism and leave the generic sync path untouched:
- Mode A (audit-db-self-heals-duplicate-rows-from-stale-schema-test):
an interrupted host-adjust + schema sync duplicates a v_* view row and
retires the original, breaking customer content. ensure-audit-db-installed!
must reconcile back to one active row per view and keep dependent content
on an active table.
- Mode B (checksum-not-advanced-until-host-adjust-completes-test):
last-analytics-checksum must not advance if adjust-audit-db-to-host! is
interrupted, so the next boot re-runs the load and the host-adjust.
Both fail on master.
* [GHY-3974] Fix Usage Analytics upgrade failure modes
Mode B (checksum committed before host-adjust): in maybe-load-analytics-content!
advance last-analytics-checksum only after adjust-audit-db-to-host! completes,
so an interrupted host-adjust leaves the checksum behind and the next boot
re-runs the load + adjust. Also force a reload, regardless of the checksum, when
audit tables are stuck at the postgres "source" schema on a non-postgres host
(audit-db-in-source-state?) so already-corrupted instances self-heal.
Mode A (schema-renamed view duplicated + retired by sync): add
reconcile-audit-db-duplicates!, run on every ensure-audit-db-installed! so
already-corrupted instances (whose checksum already matches, skipping the load)
still recover. For each duplicate same-name group in the audit DB it keeps the
row dependent content points at, repoints stray report_card.table_id refs to it,
normalizes it to the host-canonical name/schema, and deletes the orphans. Schema
is derived from the host engine, not the surviving row, so a duplicate left at
the postgres source schema (Mode B masking Mode A) is corrected too. Scoped
entirely to the audit DB; the generic sync path is untouched.
Both audit-test regression tests pass on h2 and postgres.
* [GHY-3974] Serialize audit DB load/adjust/sync across nodes (1b)
with-duplicate-ops-prevented is per-process only, so on a rolling upgrade
two pods could run the audit load/adjust/sync concurrently and produce the
half-applied schema states behind both failure modes. Wrap the
load+adjust+sync+reconcile in an app-DB cluster lock (metabase_cluster_lock)
so only one node runs it at a time. If another node already holds the lock it
is doing the same work, so skip rather than fail the boot.
The sync must run inside the lock to be serialized, so maybe-sync-audit-db!
now runs synchronously instead of in a background future: on a transactional
appdb a sync on another thread would escape the lock's transaction and
deadlock against its uncommitted metabase_table writes.
Full audit-app namespace passes on h2 (in-process RW lock) and postgres
(transaction + SELECT FOR UPDATE), 52 assertions each.
* [GHY-3974] Filter audit view lookups in SQL, not in memory
audit-view-table and the Mode A heal assertion were selecting every active
audit table and filtering by lower(name) in Clojure. Push the predicate into
the query with a lower(name) where clause.
* Better comments
* [GHY-3974] Don't force a reload for an inactive public orphan (MySQL fix)
audit-db-in-source-state? treated any `public`-schema audit table on a
non-postgres host as the interrupted-host-adjust state and forced a reload.
But a Mode A duplicate leaves an *inactive* `public` orphan, and on MySQL
(canonical schema NULL) forcing a reload made adjust-audit-db-to-source!
move the active NULL row to `public`, colliding with the orphan on the
(db_id, schema, name) unique index (Duplicate entry '…-public-v_content').
Only active rows signal the genuine stuck source state; inactive public rows
are retired orphans handled by reconcile-audit-db-duplicates!. Restrict the
check to active rows. Surfaced by the MySQL/MariaDB app-db CI run of
audit-db-self-heals-duplicate-rows-from-stale-schema-test.
* [GHY-3974] Update modules config: audit-app no longer uses config
Removing the metabase.config.core require from the audit-app module (the
unused config dep dropped when the sync future was removed) leaves config
out of enterprise/audit-app :uses. Fixes modules-config-up-to-date-test.
* [GHY-3974] Only swallow lock-contention failures for our own audit lock
A different cluster lock's acquisition failure raised from inside the locked
body (e.g. the permissions lock taken when serdes inserts a Database) also
carries :lock-names; it must propagate rather than be swallowed as audit
contention and silently abandon the load.
* [GHY-3974] Bail faster on audit lock contention
Drop the lock-acquisition timeout to 5s and retries to 2, so a node that loses
the boot-time audit lock skips in ~17s instead of ~150s. The work is idempotent
and the lock holder is doing it, so a losing node need not win; keeping a couple
retries still lets a node pick up the work if the holder crashes mid-load.
* [GHY-3974] Serialize audit DB install/adjust under the cluster lock too
The install-time adjust-audit-db-to-host! + schema sync (the ::installed and
::updated branches of maybe-install-audit-db!) previously ran outside the
cluster lock, so two nodes detecting an engine mismatch could adjust and sync
concurrently against a half-adjusted schema. Move install inside the lock so the
whole install+adjust+load+sync+reconcile is serialized across nodes; a node that
acquires the lock after the installer commits sees the DB already exists and
no-ops instead of colliding on the audit DB primary key.
* [GHY-3974] Harden reconcile-audit-db-duplicates! and Mode B test
- Sort rows by id before grouping so survivor selection (including the
referenced? tiebreak) is deterministic regardless of appdb row order.
- Clear is_defective_duplicate on the survivor so it re-enters idx_unique_table
(a flagged row has a NULL unique_table_helper and is excluded from the index).
- Comment why orphans are deleted before the survivor is re-canonicalized.
- Mode B test: move cleanup into a finally and reinstall the audit DB so an
unexpected throw can't leave sibling tests without an audit DB.
* [GHY-3974] Make reconcile lossless: move content off orphans before deleting
The previous reconcile did (t2/update! :model/Card {:table_id orphan} {:table_id survivor})
then deleted the orphan. That update is a no-op — Card's before-update populate-query-fields
re-derives table_id from the unchanged dataset_query — and report_card.table_id is ON DELETE
CASCADE, so deleting the orphan destroyed any customer card whose query pointed at it. This
happens in real Mode A: bundled audit cards reference the retired row (making it the survivor)
while a customer question built during the corrupted window references the freshly-synced active
row (the deleted orphan).
Now repoint-cards-to-survivor! rewrites each referencing card's dataset_query (source-table)
and result_metadata, remapping field ids onto the survivor's same-named fields, before the
orphan is deleted. Handles both pMBQL and legacy field refs.
Adds audit-db-reconcile-preserves-content-on-the-deleted-duplicate-test, which asserts the card
survives and its field refs resolve to active fields on the survivor (fails on the old code).
* [GHY-3974] Use lib field-ref helpers instead of hand-indexing refs
repoint-refs now reads/writes the field id via lib/field-ref-id and
lib/with-field-ref-id rather than indexing the ref vector by position. Expose
with-field-ref-id from metabase.lib.core (the setter alongside the already-public
field-ref-id getter) so it's reachable through the lib module's public API.
* [GHY-3974] Use lib clause predicate / field-ref-id for remaining MBQL access
Replace the hand-rolled field-ref? predicate with lib/clause-of-type?, and the
positional field-id extraction in the test with lib/field-ref-id.
* better docstring
* [GHY-3974] Order reconcile rows in the query instead of in memory
Address review: let the DB sort (:order-by [[:id :asc]]) rather than sort-by; group-by
preserves the order within each group, keeping the survivor tiebreak deterministic.
* [GHY-3974] Move query id-remap into lib; handle result-metadata thoroughly
Review (@metamben): the ref manipulation should live in lib and be general-purpose,
and queries vs result metadata shouldn't share one generic walker.
- Add metabase.lib.walk.util/replace-field-ids and replace-table-ids (general, built on
lib.walk): field refs everywhere via walk-clauses (filters/aggregations/breakouts/joins/
nested stages), :source-field/:fk-field-id opts, native :template-tags :dimension refs, and
:source-table per stage/join. Exposed via metabase.lib.core.
- audit-app repoint-cards-to-survivor! now rewrites dataset_query via the lib helpers, and
handles result_metadata separately (non-generic): :id, :fk_target_field_id, and :field_ref
covering both legacy ([:field id opts]) and pMBQL ([:field opts id]) shapes.
- Test asserts legacy result_metadata :field_ref is remapped onto the survivor.
Covers review threads: field-ref manipulation in lib (#383), FK/source-field refs (#418),
legacy result-metadata refs (#392).
* [GHY-3974] Batch the reconcile referenced? check into one query
Address review: instead of a t2/exists? per row in a duplicate group, fetch the set of
card-referenced table_ids for the whole group in a single query and test membership.
* [GHY-3974] Review cleanups: cond-> in replace-field-ids; update-existing in result-metadata remap
- lib replace-field-ids: use cond-> for the template-tags/:dimension updates instead of if/else.
- audit remap-result-metadata: use m/update-existing with an identity-for-unmapped fn instead of
a cond-> with duplicate lookups; drop the redundant pos-int? guard in remap-result-metadata-ref
(field-id-remap is always a map, so it already returns nil for names/nil).
Fixes UXW-3770.
The default limit of 50 levels of nesting is too small for deeply nested
MBQL expressions, such as a `replace(replace(replace(...)))` tower.
This PR raises the limit to 10k, and includes a test that 500 levels
parses fine. 10k works at my REPL but I don't want to commit a test like
that in case other JVMs hit a `StackOverflowError`. Rejecting the import
is a better UX than crashing Metabase!
Fixes UXW-4108.
When no `database`, `dashboard` or `question` parameter is supplied, the
endpoint does not invalidate anything. It previously returned a 404 in
that case, indicating that nothing was found to invalidate.
But since no attempt to invalidate anything is made without at least one
of those parameters, the 404 was misleading. Instead, this malformed
request should return a 400, which it now does.
No impact to the Metabase FE, which only ever calls this endpoint with
the parameters supplied.
* Add an Invite someone to view this action to dashboard and question share menus (#76209)
* Add an Invite someone to view this action to dashboard and question Share menus
* Update admin create-user e2e to reveal the temporary password via the new toggle
* Add a shareable item link to the invite modal's no-email states
* Remove the stale password-toggle aria-label and reword the temp-password invite copy
* Land invited users on the shared dashboard or question after signup (#76505)
* Land invited users on the shared dashboard or question after signup
* Scope the invite email to the shared dashboard or question
* Code cleanup
* Polish the invite email's join button spacing, label, and padding
* Remove stale TODO about synchronous invite email
* Add tests for invite someone to view (unit + e2e) (#76589)
* Add unit and e2e tests for the invite-to-view feature
* Fix the invite-to-view e2e signup button and no-access selectors
---------
Co-authored-by: Nathan Voxland <nathan@metabase.com>
* Add user_invited and invite_to_view_opened analytics events (#76646)
* Add user_invited and invite_to_view_opened analytics events
* Reword the external-invite tracking comment in NewUserModal
* Type isEmailAlreadyInUse's error with a guard instead of a cast
* Return an error_code for the duplicate-email 400 and key isEmailAlreadyInUse off it
* Move invite-events analytics into common/analytics and assert events fire once
---------
Co-authored-by: Nathan Voxland <nathan@metabase.com>
* fix(custom-viz): allow map tile server host in img-src CSP
When Custom Visualizations are enabled, csp-img-enabled is forced on,
switching img-src from a permissive wildcard to a restricted allowlist.
Map tiles (OpenStreetMap by default, or a custom map-tile-server-url)
were never added to that allowlist, so map visualizations broke.
Automatically add the configured tile server's origin to img-src,
mirroring how custom font files are added to font-src. The {s} subdomain
placeholder becomes a * wildcard and the {z}/{x}/{y} path is dropped.
* docs: note map tile server is always allowed under restricted image domains
* Missing update to setting description
* Update comment
* Fix whitespace
* Rename router useRouter to kebab-case use-router
* Add router facade shared types and path/search utils
* Add useNavigate facade hook
* Add useLocation facade hook
* Add useParams facade hook
* Add useSearchParams facade hook
* Add Navigate facade component
* Add Outlet facade shim
* Add Link facade re-export
* Reference react-router v7 docs from each facade helper
* Lock facade tests to react-router v7 behaviour
* Import useRouter from the metabase/router barrel
* Sort metabase/router barrel exports
* Test useNavigate splits query from a string destination with state
* Test setSearchParams clears the query when called with no argument
* Use router/<name> pattern for facade test describe blocks
* Link react-router v7 API reference from facade types
* Use react-router's official descriptions for facade type comments
* Use useMount instead of a bare useEffect in Navigate
* Expand inline JSDoc in router utils to multiline
* Implement useNavigate over v3 history instead of redux dispatch
* Navigate via react-router's router API instead of history directly
* Re-navigate on prop change in Navigate to match v7
* Default useLocation state to null and note v3 helper limitations
* Pass Navigate state by reference and clone search-param updater arg
* Expose v3 splat param under v7's * key in useParams
* Drop the existing hash in setSearchParams to match v7
* Churn useNavigate identity per navigation to match v7
* Support defaultInit in useSearchParams to match v7
* Match v7's URLSearchParamsInit pair-tuple type
* Match v7's useParams generic signature
The work done in #76355 and #76504 to fix some OOMs while generating
subscriptions modified the core rendering pipeline (static viz charts to
PNGs) concurrently with the work happening on PDFs.
This adapts the PDF generation to reuse both in-memory and on-disk query
results, and to use the whole-dashboard budget of results in memory before
spilling to disk. Cleans up all temporary files when finished.
Take existing tables into account when determining already-loaded bigquery.
We no longer have bigquery tables that constantly think that they need
to be recreated, but we have fallen a little too far in the other
direction; datasets that get their DB created but fail midway thru
will still be treated as loaded.
In snowflake we have a way of checking this down to the individual row
counts and counting as not-already-loaded if there's a mismatch. I
don't see a way to do this in bigquery, but we can at least check that
all the tables exist. If we get a failed partial load that gets all
the tables created but fails the rows that will still require manual
intervention.
Also stop using (config/current-major-version) when qualifying dataset
names; turns out this is not at all reliable in CI. This unfortunately
means that we don't actually have cross-major-version isolation; the
best we can do is isolate master/release from all other branches,
which is still an improvement over what we have now.
`get-day-of-week` accepts a positional week-mode, and the ::week-mode
schema admits :iso, :us, and :instance (shared with get-week). But only
the default (no-mode) and :iso forms have a lowering in lib.filter.desugar
— :us and :instance desugar to a nil temporal-extract unit and 500 at SQL
compile time (observed: an LLM emitting "instance" for "day of week on
weekends"). Drop the unsupported mode, leaving the start-of-week-aware
default. :iso and the bare form are untouched.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prompt-snippet updates, independent of any code change:
- discovery: search to choose a source, navigate to drill in (don't skip
the curated layer just because a raw table name is recognizable)
- data-sources: a source the user explicitly names is authoritative
- grounding: when several columns plausibly match, read descriptions and
sample values rather than picking by name resemblance
- construct_notebook_query: a "translating the request" section (constraint
vs breakout, apply every constraint, absolute vs relative dates)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(custom-viz): make dev hot-reload resilient to transient failures
When the dev server is mid-rebuild its bundle endpoint can transiently
fail, and rapid edits / overlapping reloads could leave the viz showing a
stale bundle or a spurious "unavailable" toast.
- Backend: fetch-dev-bundle returns nil on upstream failure (mirroring
fetch-dev-manifest) so the endpoint serves its existing retryable 503
instead of a hard 500.
- Frontend: retry the dev bundle fetch on transient 5xx (fail fast on
4xx), and tag each load with a monotonic sequence so a slower/older
overlapping load can't overwrite a newer applied bundle or fire a stale
toast (latest-wins).
* fix(custom-viz): render the freshest bundle after overlapping dev reloads
Overlapping dev reloads (rapid edits) could apply the newest bundle to the
visualizations registry but leave the chart rendering an older one: the
visualization only re-reads the registry when it remounts on the loading
toggle, and a plain boolean cleared after the first load resolved.
- Refcount in-flight loads so `loading` stays true until the last settles,
so the single remount reads the freshest registered bundle.
- Stop retrying a load once a newer one has started (it would be discarded
by the latest-wins guard anyway, and would keep `loading` held).
* fix(e2e): restore custom-viz dev plugin source between retries
The development-mode custom-viz test mutates the scaffolded plugin
source in place ("Above threshold" -> "Way above!") to verify hot
reload, but never reverts it. Since `before` does not re-run on a
Cypress retry, a retried attempt started from the mutated file and the
opening "Above threshold" assertion timed out against stale state.
Capture the pristine source in `before` (after the clean re-scaffold)
and rewrite it in `afterEach` so each attempt starts from the default.
* fix(e2e): use frozen lockfile for custom-viz SDK bun install
Skip lockfile resolution during the dev-mode `before` hook so the
install step is deterministic and faster, reducing the chance of the
120s setup timeout.
* fix(e2e): start custom-viz dev server per test, not per suite
The dev-mode test stops the dev server as its final assertion (revert to
default), but the server was started once in `before`. Since `before`
does not re-run on a Cypress retry or during burn-in, every run after the
first hit a dead server and failed at "verify the dev plugin is
registered".
Move the server start to `beforeEach` and the stop to `afterEach` (the
stop task is idempotent), keeping the heavy build/scaffold/install in
`before`.
* chore(custom-viz): trim verbose comments per review
* refactor(custom-viz): reuse retry util for dev bundle fetch
* fix(custom-viz): use > not >= in superseded check
The == case is impossible (loadAppliedSeq is only set to a load's own seq
on success, which returns before the catch; seqs are unique), so >= and >
are equivalent here. > matches the sibling loadStartedSeq check and reads
as "a strictly newer load exists".
* fix(custom-viz): only the latest-started load registers its bundle
The apply-guard bailed only when a newer load had already applied; bail
when a newer load has merely started too (applying implies starting, so
this subsumes the old check). Reuses isLatest(), consistent with the
retry loop and the superseded check.
* refactor(custom-viz): name load-sequence maps by plugin id
* test(custom-viz): scaffold dev plugin per attempt instead of capturing source
Move the plugin scaffold (init + npm i) into beforeEach so each attempt
starts from a pristine source, dropping the pristineSrc capture/restore.
The expensive SDK build stays one-time in before.
* fix(embedding-sdk): gate query builder controls on create-queries permission
Filter/Summarize/Breakout dropdowns and the notebook editor toggle rendered
regardless of query permissions, showing non-working controls for users
without create-queries access. Mirrors main app's FilterHeaderButton/
QuestionSummarizeWidget/QuestionNotebookButton gating (isEditable, !isNative,
!isArchived) via a new shouldRenderQueryBuilderEditingControl.
Fixes EMB-1963
* test(embedding-sdk): unit test shouldRenderQueryBuilderEditingControl
Covers editable/native/archived/no-question cases, plus the missing
create-queries case by building a query against metadata with the source
table excluded (mirrors what the backend does when that permission is
missing) rather than mocking metabase-lib.
* fix(embedding-sdk): hide decorative icon from ToolbarButton accessible name
Icon set role="img" aria-label="<name> icon" unconditionally, so a button's
accessible name became "filter icon Filter" instead of just "Filter",
breaking cy.button()/getByRole name lookups.
* test(embedding-sdk): e2e test for query builder permission gating
Confirms Filter/Summarize/notebook editor toggle are hidden for a user
without create-queries permission on the underlying table, and shown for
one with it, using existing readonly/readonlynosql fixture users whose
default group permissions already match each scenario.
Fixes EMB-1963
* docs(embedding-sdk): tighten comments, scope test fixtures per-test
Bullet the three omitted main-app gating conditions instead of run-on
prose, shorten the missing-permission test comment, and move each
question fixture into the test that uses it instead of module-level
constants shared across unrelated tests.
* fix(embedding-sdk): guard BreakoutDropdown on missing question
Matches the !question || !shouldRenderQueryBuilderEditingControl(question)
pattern used by FilterDropdown and EditorButton.
* Let fetchDashboard accept a prefetched dashboard
(cherry picked from commit 63425d8754777a96f023c3be6028c5a45b4607d5)
* Use save response to refresh dashboard instead of refetching
(cherry picked from commit 8f5aff044beb8290f9c63f7ebb041ffbe8c2b392)
* Test save reuses response without refetching the dashboard
(cherry picked from commit a90b2885f7ac19ed472bed1018a88fa29fee60be)
* Update e2e save helper for dropped post-save dashboard refetch
(cherry picked from commit 41bba584ae41640903c06fcd76022a1569d6f087)
* Update e2e dashboard reload expectations for save-response reuse
(cherry picked from commit 97fabf4f8df7d0c1345ea33a6136a1c3ba76933f)
* Wait on dashboard UI instead of phantom reload when returning from drill
(cherry picked from commit 19181e1345af6af1fe46f323c3b0362a5286e1b4)
* Add waitForDashcardsToLoad e2e helper and use it in #32444 test
(cherry picked from commit fa3a0e989f2166f50af10a0294fcda9bfef968d9)
* Settle saveDashboard on dashcards loaded instead of fixed sleep
(cherry picked from commit b7a2e157b6db12a2f4f8ab10b7c19f68437b94ed)
* Drop redundant saveDashboard waitMs args now that it settles deterministically
(cherry picked from commit e8827b06955f581dde739491d3aef2bee5f90be9)
* Drop redundant post-save sleep now covered by saveDashboard settle
(cherry picked from commit c3ae1c65997165fffc0fa1c394d1d2765b4fb2b8)
* Drop tombstone comments per review
* Make waitForDashcardsToLoad tolerant of dashboards without a grid
* Anchor dashcard settle on dashboard body to avoid early return
* Revert broad saveDashboard auto-settle; keep save-reuse and opt-in waitForDashcardsToLoad
* Settle saveDashboard on dashcards loaded + grid layout stable; drop waitMs sleeps
* Wait for filter widgets to settle after save (parameter panel re-render)
* Make editDashboard resilient to a dropped Edit click after save
* Make editDashboard retry work inside cy.within (use cy.document not cy.get body)
* auto stress test
* better options
* fix(e2e): restore 30s timeout on findByText for impersonated permission test
The timeout was removed in a8cc638d2f when updating the error message text.
The impersonated write path takes 15-20s, far exceeding the default 4s timeout,
causing consistent test failures (quarantined for 33 days).
---------
Co-authored-by: Ryan Laurie <iethree@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Ryan Laurie <30528226+iethree@users.noreply.github.com>
* Fix flaky pivot-table dashboard filter spec: wait for dashcard load before opening question
The "does not use extra filtering stage for pivot tables" spec clicked the pivot
dashcard's legend title immediately after exiting edit mode. The title's
navigation handler is only wired once the card series has reloaded, so an early
click was a no-op and the /api/dataset/pivot request never fired, timing out
cy.wait("@datasetPivot"). Wait for the dashcard to finish loading (visible title
+ no loading indicator) before clicking.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Give pivot-table dashboard filter spec a throttle-tolerant pivot wait
Clicking the pivot dashcard title navigates to an ad-hoc QB that must boot
the route, load table metadata, and only then issue /api/dataset/pivot. Under
network throttling that chain can exceed cy.wait's default 5s timeout, so the
wait occasionally fails even though the request does fire moments later. Wait
on the @datasetPivot alias with a throttle-tolerant timeout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* WIP: support for merge strategy via delete + insert
* PoC FE
* fix
* overridable method
* support for py
* refactor
* FE
* changelog
* FE fixes
* validate UK
* chore
* update FE
* tests
* remove compile-merge multi
* FE tests
* remove feature
* move component to its own file
* chore: test composite keys
* fix: override for sqlserver
* fix(embedding-sdk): open SQL editor when Metabot navigates to native hash URL
When Metabot sends navigate_to with /question#<native_hash>, SdkAdHocQuestion
was returning questionId=null (no numeric slug), so isNewQuestion=false and the
SQL editor never opened. Now detects dataset_query.type==='native' and returns
'new-native', which makes SdkQuestionProvider set isNewQuestion=true and
useNewQuestionEditorSync open the editor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dwi9BjxKNSJsztkkVrsU8d
* test(sdk): use real embedding Metabot stream for SQL editor test
Replaces hand-rolled mock payload with a navigate_to event captured
verbatim from an actual embedding Metabot response, so the test
fixture matches real wire format (db id adjusted to match env).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wNQmcGH7PCXMD1PqFLiBS
* refactor(sdk): call resolveQuestionId inline instead of in useMemo
It's a cheap derivation, not worth complicating the memo for.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wNQmcGH7PCXMD1PqFLiBS
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* GDGT-2534 Migrate icon tokens (disabled/secondary swap + text aliases)
Per design, the icon tokens are corrected: `icon-disabled` and
`icon-secondary` swap meaning, and all four icon tokens become runtime
aliases of their text counterparts.
- Swap consumers: `icon-disabled` <-> `icon-secondary` call sites.
- Redefine in both themes:
- icon-primary -> var(--mb-color-text-primary) (value unchanged)
- icon-secondary -> var(--mb-color-text-secondary)
- icon-disabled -> var(--mb-color-text-disabled)
- icon-brand -> var(--mb-color-text-brand) (brand[40] -> brand[50])
Token names are unchanged, so there's nothing to remove at cleanup. Icon
tokens aren't SDK public theme keys and aren't used in static-viz, so the
var() aliases are safe everywhere.
* Update Loki Snapshots
---------
Co-authored-by: Metabase Automation <github-automation@metabase.com>
* refactor: add basename module for app-wide basename state
* refactor: move basename off ApiClient into the basename module
* refactor: make setBasename tolerate nullish input
* refactor: drop redundant basename guard in sdk init
* test: capture original basename per-test in setupBasename
* fix: strip basename only as a prefix in getDatasetDownloadUrl
* Measure SDK/app bundle sizes from both uberjars, apples-to-apples
* Always report brotli bundle size, estimating when no .br is shipped
* Bundle-size stats: upload per-bundle deltas, skip unchanged commits
* Bundle-size stats: always record releases, gate/report on brotli delta
* Scope app to app-main and SDK totals to reachable code via the chunk graph
* Fall back to whole app/dist when stats lack reachableAssets
* Stats logger: record master only, drop release tags from the live series
* Measure SDK async chunks as loadable; split legacy initial/total
* Point bundle-size logger at table 79702
* Skip SDK bundle-size gate when base ref predates reachable-chunk stats
* Split bundle-size scripts into pure cores + I/O wrappers, add unit tests
* Bundle-size: extract legacy embedding-sdk.js fallback; fix stale as-served comments
* Retry stats upload with backoff; keep bundle-size run green if it still fails
* Add default-openai-llm-metabot-model gpt-4.1-mini
* Filter openai list-models by supported-models
OpenAI's /vi/models endpoint returns ~140 models, many of which are not relevant (text embeddings, audio/video models)
or else deprecated. Filter this down to a whitelisted set of known supported models.
* Add grouping for openai models in provider-models-response
* Throw an exception if callers pass `:ai-proxy true` for openai
Similar to bedrock and azure, supporting this would require some work on the ai-service side, and we already reject
non-anthropic in validate-metabase-managed-provider!
* FE: enable openai provider in metabot admin UI config
* Drop gpt-4.1-mini from supported-models and change default to gpt-5.4
* Replace gpt-4.1 with gpt-5 in supported-models
* Bump metabase.metabot.example-question-generator/max-tokens to 4096 for gpt-5
GPT-5 produces a lot of reasoning tokens and sometimes exceeded the old limit.
* Remove reference to gpt-4.1-mini in docstring example
* Remove references to gpt-4.1 and gpt-4.1-mini in FE unit tests
* Skip empty remote-sync commits after a no-op export (GHY-3934)
A dirty RemoteSyncObject row whose serialized content already matches the
repo stages a net-zero tree change, so finishing the commit produced an
empty commit that still got pushed. GHY-3933's content-hash gating catches
the no-op-update-event case, but not the others: a full-export fallback
that re-serializes byte-identical content, a rename/delete whose old path
isn't known yet, a dedup name collision, or path/hybrid models.
Add an `empty-commit?` query to the CommitBuilder protocol (git layer
answers whether the staged tree equals the parent's; it does not decide
policy). full-export!/incremental-export! branch on it: when empty, abort
the commit, leave the version unadvanced, and report push-skipped — while
still reconciling RemoteSyncObject state so the stale dirty flags that
triggered the no-op are cleared.
* Docstring
* Unify remote-sync export commit lifecycle and skip empty merges
Extract commit-staged! — the open/stage/empty-commit?/finish-or-abort
lifecycle — and route full, incremental, and merge exports through it.
merge-and-store! moves from source.clj into impl.clj (next to the other
export paths) so it shares commit-staged! instead of duplicating the
lifecycle via replace-all-files!; source.clj keeps the merge computation
(compute-merge, preview-merge).
commit-staged! returns the :remote-sync/empty-commit sentinel when the
staged tree matches the parent, so the merge path no longer pushes an
empty commit either. An empty merge folds in any remote changes and
advances to the remote tip, reporting "pulled" (or "push-skipped" when
nothing changed on either side) rather than a misleading "merged, pushed 0".
* Inline merge-and-store! into export-merged!
The intermediate {:status ...} map that export-merged! re-dispatched via
case added nothing: inline the merge computation and commit so the merge
path reads like full-export!/incremental-export! — compute, commit-staged!,
reconcile, outcome.
Move the test seam down from merge-and-store! to source/compute-merge: the
export! tests stub the pure merge function and exercise the real commit
staging, and the three dedicated merge-and-store! unit tests are dropped as
redundant with merge-test (algorithm) and the commit-staged!/export coverage.
* Better docstrings
* Test empty-commit? against a real git repo
Exercise empty-commit? through the real JGit commit builder (not a mock):
identical re-stage and delete-of-absent-path read empty, real edits and new
files don't, aborted no-op checks leave the branch untouched, and the
memoized tree still yields a correct commit when finish-commit! follows.
Serdes stamped every non-Setting entity (and the Card column) with the
instance build version on extract. Since the value differs between
instances, it produced spurious uncommitted changes during remote sync.
Compatibility is now governed by the versioned representation spec
(:card_schema + upgrade-card-schema-to), so the per-entity version stamp
no longer carries useful information. Stop serializing it:
- extract: remove the metabase_version stamp
- Card serdes spec: move :metabase_version from :copy to :skip
- load: remove the now-dead version-mismatch warning machinery
Legacy baseline fixtures still carry the field, exercising that older
exports import cleanly now that it's skipped.
Replace e2e's local ConductorTest with the shared NormalizedTest wire shape
(plus a transient screenshotPath) and import toNumber from the module, so the
report path and the quarantine path can't drift on a test's identity. The
transport stays forked: e2e posts mid-run from Cypress's after:spec hook on
node-fetch, with screenshots and a dry-run mode the module's post-run transport
doesn't cover.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:13:29 +00:00
8108 changed files with 746343 additions and 492563 deletions
@@ -27,9 +27,28 @@ This guide covers Clojure and ClojureScript coding conventions for Metabase. See
- Format using Markdown conventions
- Reference other vars with `[[other-var]]` not backticks
A docstring states the *contract*: what it returns (including when it returns nil), what its
params mean, what it throws versus swallows, and any precondition or invariant the caller must
respect. A params block for non-obvious or optional args is good documentation — length is not
the problem.
These seven things are, and each is a reason to cut or relocate:
| Anti-pattern | Why | Where it goes instead |
| --- | --- | --- |
| **Facts you don't own** — how a *callee*, another namespace, or another repo works internally | Drifts silently; nothing fails when it goes stale | Cut. Name the callee and let its own docstring say it |
| **Said twice** — ns docstring restating its vars', a caller restating its callee's, sibling fns repeating boilerplate | Two copies means one is already wrong | Cut the copy. One fact, one owner |
| **Design rationale** — why this approach, why it lives in this module, what the alternative was | The caller can't act on it | Commit message, or an inline comment on the specific line it constrains |
| **Development history** — "matching master", "the X branch", "currently", "for now", "used to" | Stale by construction | Cut |
| **Instructions to the reader** — "callers should trust this", "don't bother checking", "you should use X" | State the contract; the caller decides | Cut, keeping any actual guarantee as a plain statement |
| **Invented precision** — score bands, thresholds, or tables no code enforces | Reads as spec, isn't | Cut, or make the code enforce it |
| **A note on the wrong var** — describing how overrides behave, on the base/default impl | Nobody reading the override sees it | Move to the overrides it describes |
**Comments:**
-`TODO` format: `;; TODO (Name YYYY-MM-DD) -- description`
- Prefer an inline comment over docstring prose for anything that explains *the body*. Put it on
the line it explains, not at the top of the function.
- Run targeted tests, and lint continuously during development
- Prioritize understanding existing patterns before implementing
- Don't commit changes, leave it for the user to review and make commits
- Minimize comments. Document only non-obvious intent, trade-offs, or constraints that the code itself cannot convey. Do not narrate what the code does — keep code self-descriptive through naming and structure.
@@ -10,23 +10,23 @@ This skill helps you add product analytics (Snowplow) events to track user inter
## Quick Reference
Analytics events in Metabase use Snowplow with typed event schemas. All events must be defined in TypeScript types before use.
Analytics events in Metabase use Snowplow with typed event schemas. Simple events are declared **where they are used** — `trackSimpleEvent` is generic and validates the payload at the call site.
**Key Files:**
-`frontend/src/metabase-types/analytics/event.ts` - Event type definitions
**Primary standard: the [`typescript-write`](../typescript-write/SKILL.md) skill.** Load it first — it defines the authoring rules this review enforces, alongside `frontend/CLAUDE.md` and `docs/developers-guide/frontend.md`.
Adherence to `typescript-write` is the **highest-priority** review dimension: rank any violation of its provisions above all other findings. Treat its **no-`any` hard rule** (no explicit *or* implicit `any` in new code) as **blocking**, and verify it with the LSP rather than by eye.
Adherence to `typescript-write` is the **highest-priority** review dimension: rank any violation of its provisions above all other findings. Treat its **no-`any` hard rule** (no explicit *or* implicit `any` in new code) as **blocking**. Use TypeScript LSP tools to inspect inferred types when available; otherwise rely on type-checking and linting.
@@ -13,7 +13,7 @@ description: Write TypeScript and JavaScript code following Metabase coding stan
- **New code must not introduce `any`, explicit or implicit.** No `any` annotations, no `as any` / `as unknown as`, no untyped parameters or returns that infer `any`, no implicitly-`any` destructures or array/object literals.
- **Untyped third-party / boundary values** must be typed at the boundary (a declared type, `unknown` + type guard, or a small typed wrapper) — never let `any` propagate inward.
- **Mandatory LSP verification.** After writing or editing any TS/TSX, inspect the changed symbols with the TypeScript Language Server (hover to read inferred types; `goToDefinition` to confirm sources) and run the project type-check.
- **Mandatory type verification.** Before finishing a TS/TSX change, run `bun run type-check-pure`. If TypeScript LSP tools are available, also inspect changed symbols with hover and go-to-definition; otherwise skip LSP check.
## Type tightening
@@ -26,6 +26,7 @@ description: Write TypeScript and JavaScript code following Metabase coding stan
- **Avoid non-null assertions (`!`)**. Prefer a guard, early return, or `?.`. Use `!` only when non-nullness is provably true and localized, with a comment.
- **Type guards belong in `frontend/src/metabase-types/guards/`**. Do not redefine them locally.
- **A cast you can't avoid needs a real justification comment.** The `metabase/no-unjustified-type-casts` rule accepts any preceding comment — state the actual reason the cast is safe. NEVER write the legacy `// Unjustified type cast. FIXME` placeholder; it exists only on casts that predated the rule, and copying it sneaks an unjustified cast past the linter. If you can't articulate why the cast is correct, the cast is wrong — fix the types.
{clojure.core/print{:message"Use metabase.util.log instead of clojure.core/print"}
{metabase-enterprise.entity-retrieval.core/search-unfiltered{:message"Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
metabase.entity-retrieval.core/search-unfiltered{:message"Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
metabase.entity-retrieval.mirror/search-unfiltered{:message"Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
clojure.core/print{:message"Use metabase.util.log instead of clojure.core/print"}
clojure.core/println{:message"Use metabase.util.log instead of clojure.core/println"}
clojure.core/printf{:message"Use metabase.util.log instead of clojure.core/printf"}
clojure.core/prn{:message"Use metabase.util.log instead of clojure.core/prn"}
description:Cherry-picks a commit onto a release branch and opens the backport pull request.
inputs:
target-version:# major version number like 34 or 86
@@ -42,7 +43,7 @@ runs:
git cherry-pick --abort
echo "Could not cherry pick because of a conflict"
echo "has-conflicts=true" >> $GITHUB_OUTPUT
echo "has-conflicts=true" >> "$GITHUB_OUTPUT"
# Add a shell script for resolving conflicts
echo "git reset HEAD~1" > ./backport.sh
@@ -57,27 +58,30 @@ runs:
#${ORIGINAL_PULL_REQUEST_NUMBER}
> [!IMPORTANT]
> Manual conflict resolution is required.
> Checkout the branch and run \`./backport.sh\` script. Force push your changes after cherry-picking. Check the box below when done.
> Checkout the branch and run \`./backport.sh\` script. Force push your changes after cherry-picking. Mark the PR as ready for review once conflicts have been addressed.
@${githubUsername} You have modified embedding code. Please make sure the PR title follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style.
msg="🚨🚨 Backport failed because \`double-backport\` and \`triple-backport\` labels are **deprecated**! Use the \`backport\` label to port changes to all supported releases instead. 🚨🚨"
message:"⚠️ Main app initial bundle size (gzipped) increased by ${{ steps.compare.outputs.app_size_change_percent }}% compared to the ${{ github.base_ref }} branch (threshold: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}%). This is the JavaScript every user downloads before the app renders, so check whether the new code can be loaded lazily instead."
include-log:false
- name:Comment on PR if app bundle size has decreased
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.