Generate Clojure query fns with Malli result schemas from .sql files, using
sqlc for SQL parsing and type inference and sqlc-gen-json to expose its
analysis as JSON.
Verified end to end: migrations against Postgres 16 (1427 changesets, 190
tables), pg_dump fed to sqlc unmodified, real queries ported from the
glossary/sso HugSQL spike branches, and the generated file compiles with
schemas that accept valid rows and reject bad ones.
See dev/src/dev/sqlc/README.md for open issues (no H2 engine, weak param
type inference, no dynamic SQL).
Fixes https://github.com/metabase/metabase/issues/65493
If you have a `details.schema` property set in your connection (this was
removed in v42 in favor of schema filters), you can't change it in the
UI in the current Metabase version. If you wanted to specify a schema
through the "Additional JDBC connection string options", the old
buried schema property would take precedence anyway.
This makes it so the deprecated field is removed when the schema is
set in additional-options.
I've verified this using a real connection and querying `SELECT current_schema()`
as suggested in the bug report.
The test read `:future-ref` straight off the registry entry as soon as the
fetch signalled it had started. But `detached-fetch!` registers the entry
before the fetch begins and only stores the future just after submitting it,
so the thunk can deliver `started` before the `reset!` lands. When it does,
`stalled-future` is nil and `future-cancelled?` throws an NPE.
Under 300 spinning threads a probe caught the window 1-4 times per 20000
fetches; inserting a 5ms sleep between the `future` and the `reset!` makes
the old test fail every run.
Poll for the future instead. Only the test is affected: the sweep acts on
entries older than *fetch-max-age-ms* (an hour), so it can never observe one
whose `:future-ref` is still unset.
Claude-Session: https://claude.ai/code/session_01J1yFPpYiWH2ik8LB9rs7BT
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* multiple jvms per runner for bigquery
* trigger CI
* add failing test
* lower timeout
* remove debug stuff
* clean up comments
---------
Co-authored-by: Claude <noreply@anthropic.com>
* BigQuery: use calendar days for datetimeDiff day unit
Fixes#82193
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Avoid :raw for date part in BigQuery day diff
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* clean up
* add timezone to test
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* add saved question extraction prototype
* fix additional failure cases
* refactor: reconciler
* revert me: manual skill file edit for source swapping
* set data permissions to view for databases referenced in the query
* rename data app collection and group
* prevent agents from removing savedQuestionSourceId
* docs: clarify data app query location
* chore: cleanup query-sync canonical module
* refactor: split query tests into different files
* Swap query source for data app query hooks and apply dynamic filters on top of it (#79584)
* code improvements and bug-fixes for data apps permissions
* Add data apps model imports
* address reviews on data apps permissions
* run sync queries before vite build
* Add data app resource link (#79878)
add button to view data app resources
* Show permission error when users cannot access a data app (#79874)
show permission error page when user do not have permission to view data apps
* Add data app group link in overflow menu (#79873)
Add data app group link
* Store and reconcile data app entity ids in data_app.yaml (#79833)
* Store data app resource entity IDs
* make entity ids in data_app.yaml authoritative
* add top-level value after path key
* simplify data_app.yaml manifest parser
* only allow changing data app collection if empty
* consolidate backend test helpers and remove redundant tests
* chore: restore README changes
* ci: fix failing tests
* Add support of actions to Data App reconciler (#79842)
Add support of actions to Data App reconciler
* Fix bugs/inconsistency and add more e2e for data apps reconciler/syncing (#80002)
* Update semantic schema skill about preventing segments and measures in dynamic parts of a query in Data Apps (#80288)
* Grant view data access on a table-by-table basis in data app groups (#80189)
* grant view data on table instead of database level
* refactor table scan in reconciler
* update e2e tests for permission grants
* Grant data app view permissions per table
* Skip unchanged data app permissions
* add e2e data app test for sandboxing
* reset host app sources after runs
* Preserve sandboxing for data app users (#80406)
* add e2e data app test for sandboxing
* improve e2e tests for data apps
* Remove sandbox E2E from table permissions change
* preserve existing sandboxing from sandboxed groups in data apps
* assert that we grant non-sandboxed tables
* Explain data app sandbox exclusion
* Add data app sandbox E2E coverage
* Exercise sandbox preservation in data app E2E
* Disable saved question sources in data apps (#80139)
* Disable saved question sources in data apps
* Preserve question-only schema scope
* Remove the saved question schema parameter
* Copy table-sourced metrics into data app collection and do not codegen card-sourced metrics (#80086)
* copy table-sourced metrics into the data app collections
* copy table-sourced metrics into the data app collections
* Address metric sync review feedback
* Accept keyword metric display in query response
* Fix data app metric reconciliation ordering
* Remove old lockfile format
* exclude card-sourced metrics from schema generator
* do not use card terms in metrics to avoid ambiguity with card-sourced metrics
* fix model imports
* Fix data app metric reconciliation
* refactor: cleanup reconciler unit tests
* Fix duplicated metric client helper
* add safe metrics path
* Handle unsupported metric dependencies
* Fix data app CI test regressions
* Fix "Re-adding removed query fails to adopt deleted card" (#80341)
* Clean-up data apps permissions endpoints (#80825)
* Remove entity ids from data_app.yaml (#81317)
Remove data app entity ids from data_app.yaml
* Adjust data apps permissions code (#81093)
* Remove the data app table permission grants (#81584)
* Add permissions_group.is_data_app_group and hide data app permission groups from UI (#81705)
* Add data app user management and access warnings (#81839)
* Address data app user access review feedback
* additional e2e test cases for bugs in manage users page
* fix failing frontend type check on unit test
* fix migration id collision for data apps
* combine migration changes into one file
* fix data app metric sync after trashing copied metrics
* fix cli runtime dependency packaging for embedding sdk
* avoid loading data app bundle during resource setup
* prevent redundant data app collection permission updates
* allow admins to remove data app members after token expires
* update docs and add e2e test for missing resource collection case
* isolate data app sync using transactions
* do not leave user on an empty page when removing the last member on the page
* reserve sandbox-host slug in data app config
* ignore generated data app action fixtures
* move queries into db.clj
* preserve legacy permissions when adding data app members
* regenerate the kondo modules config
The data-apps module reaches for :model/Permissions and :model/DataPermissions,
neither of which was recorded in :model-imports, so metabase.core.modules-test
fails on the current tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNKkhQjjiUSM7qwLReyfP
* reject data app queries whose metrics read another card
Sync copies a referenced metric but rewrites nothing inside the copy, and
copies nothing that copy in turn reads, so those references still point at
the originals. The app publishes successfully and then fails for every
viewer without access to the originals' collections.
Checking while resolving the query, rather than at codegen, also catches a
metric edited into that shape after its schema was generated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNKkhQjjiUSM7qwLReyfP
* exclude metrics that read a saved card anywhere from generated schemas
source-card-id only reads stage 0, so a table-sourced metric that joins a
saved question passed the codegen filter. The CLI checks the whole query and
rejects such a metric, aborting sync-resources for any app that uses it.
Checking the whole query here keeps codegen consistent with what sync
supports, and removes the database round trip that asked which of the
referenced cards were metrics -- the step that let a saved-question join
through in the first place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNKkhQjjiUSM7qwLReyfP
* resolve copied resources' table dependencies through the server
Numeric source-table entries miss implicit joins: a copied model querying
Orders and reading Products.Category through a foreign key names Products
nowhere, so it was left out of the app's permission warnings.
Run the same metadata-based lookup the query endpoint uses, through a new
endpoint the CLI calls for copied models, actions and metrics, and drop the
JSON walk it replaces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNKkhQjjiUSM7qwLReyfP
* cover the create-queries grant the database defaults leave on app groups
An app group that predates the first non-audit database is given
create-queries :query-builder-and-native by set-new-database-permissions!,
alongside the :blocked view-data it special-cases for app groups. A user with
data access through another group would gain native query authoring just by
joining the app.
The reconciler already resets it, but nothing failed when :perms/create-queries
was dropped from its list. The pairing cannot be produced through
set-database-permission!, which cascades view-data to :unrestricted for any
create-queries value other than :no, so the test writes the row the way the
defaults insert it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011nNKkhQjjiUSM7qwLReyfP
* prevent models based on saved questions from being copied
* return to a valid member page after overlapping removals
* do not resolve query objects when either enabled flag is false
* prevent shorthand properties and spreads from overriding generated ids
* prevent adding duplicate members and retry only members that are failed to add
* define schema for data app query endpoint
* reconcile only the current data app group during resource setup
* expand the schema fingerprint
* add data app group field in permission group schemas
* prevent new databases from granting query creation to data app groups
* throw validation errors when invalid query spec fields are passed
* do not fail validation on columns metadata
* add enabled metadata to query definition schema
* accept more data app specific query metadata and binning options
---------
Co-authored-by: Timofey Kachalov <timofei@metabase.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* test(ui): add PasswordInput Storybook story
* test(ui): add PasswordInput to Loki as baseline for restyle
* feat(ui): restyle PasswordInput from Figma and add input-background token
* test: cover PasswordInput visibility toggle in unit and e2e tests
* fix(PasswordInput): use highlight border color in focused error mode by overriding Input styles
* Update Loki Snapshots
* test(ui): add Loki reference for PasswordInput Overview story
* fix(PasswordInput): address review feedback
- keep border width constant and use an inset ring for focus/error to avoid layout shift
- tie right padding to the toggle button size so text clears it in lg
- restrict size prop to sm | md | lg
- order color keys alphabetically
- explicit visibility assertion in the e2e spec
* Update Loki Snapshots
---------
Co-authored-by: Metabase Automation <github-automation@metabase.com>
* Use JetBrains Mono as the code font
Ship JetBrains Mono (regular + bold) and point the monospace font family
at it, replacing the hardcoded Monaco/Menlo stacks scattered across the
codebase.
- Add the woff2 files, @font-face declarations, and the type, fallback
map, docs and backend test entries that keep the admin font picker in
sync with the fonts directory.
- Route every monospace call site through
--mb-default-monospace-font-family, and define that variable in the
embedding SDK's variable set, where it was previously missing. With
the variable always defined, the per-call-site `, monospace` fallback
is redundant and is dropped.
- Inline the variable at the CodeMirror theme, which was the only
consumer of monospaceFontFamily, and delete typography.ts.
- Set the Code component to 13px/22px per the design spec.
* Update Loki Snapshots
---------
Co-authored-by: Metabase Automation <github-automation@metabase.com>
Bedrock takes an AWS access key pair and Google Gemini Enterprise a service
account key file, so the AI settings page can't keep calling every provider
connection an API key.
The tests pass ids like 789 for entities they do not create. format-entity looks the
id up, and the test asserts on the fallback rendering that a 404 produces. When a
parallel test happens to hold that id, read-check gives a 403 instead, which renders
nothing, and the test fails.
Use Integer/MAX_VALUE, which no fixture allocates.
Claude-Session: https://claude.ai/code/session_01GCoSa7L7KrXh42rDRYR3Nk
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Read a card's UI parameters off its question
* Drop a tombstone comment
* Pin the agreement between getCardUiParameters and Question.parameters
* Type the unsaved card in the agreement test
* Format the agreement test
* Move the application permissions selector slot into current-user
PLUGIN_APPLICATION_PERMISSIONS.selectors was only read by current-user, so the slot now lives there as PLUGIN_APPLICATION_PERMISSIONS_SELECTORS with its OSS defaults, and the EE application permissions plugin registers into it from metabase/current-user. getRoutes and tabs stay in metabase/plugins for the admin permissions editor.
* Translate viz nouns and formatting operators on demand
`noun` was the one field on a visualization definition still translated at
import. `get noun()` matches what ObjectDetail and TreemapChart already do,
and the registry stores a definition by reference, so the getter survives.
The table-formatting operator names were four module-level objects. They are
functions now. `ALL_OPERATOR_NAMES` becomes `getAllOperatorNames()`, which
its three readers call.
25 of the 124 sites that translate before a catalogue can be applied.
* Translate settings titles on demand
`title` in a settings definition follows the same rule as the rest of the
object. 152 settings already use `get title()`; these 23 were the laggards.
The group-managers confirmation held two frozen strings, so it becomes a
function rather than an object with getters.
49 of 124.
* Lift translated containers into functions
Where a whole array or object is translations, the function goes on the
container so consumers keep a plain array or object: PieChart's ring titles,
the whitelabel illustration options and descriptions, PivotTable's partitions
(which held JSX built at import), the model-caching options, the cloud
migration messages, and the geo filter placeholders.
Where only a field or two of a mixed config is translated, and call sites read
it by property, the getter goes on the field instead. That covers the
collection configs, the permission option records and the loading messages,
none of which are spread anywhere.
Four files carried a whole-file disable rather than one per site, hiding 20
sites behind 4 directives. The real total is 140, not 124.
118 of 140.
* Clear the last module-scope translations
Validation schemas become functions, matching getOidcFormSchema and the other
lazy schemas already here. Module-local error strings become functions called
at their use sites. Three exported constants follow: NULL_DISPLAY_VALUE,
BEFORE_UNLOAD_UNSAVED_MESSAGE and NO_X_AXIS_VALUES_ERROR_MESSAGE, across 37
files.
The two Mantine `aria-label` defaults get a getter. Mantine merges
`defaultProps` by spread on each render, so the getter is read every time.
`ttag/no-module-declaration` now passes across all five trees it covers with
no opt-outs left, and `--report-unused-disable-directives` confirms none were
stranded.
* Stop spreads from flattening lazy translations
A spread or a rest destructure reads every getter it copies. Where an object
holds `get title()` so the string translates when it is read, spreading that
object at module scope translates it at import instead, which is the bug the
getters were meant to avoid.
Adds `mergeLazily` / `omitLazily`, which copy property descriptors instead,
and uses them where the flattening happened at import:
- `viz-core/lib/settings/utils.ts` flattened twice, once in the rest
destructure and once in `...def`, so every setting built through
`fieldSetting` / `metricSetting` / `dimensionSetting` froze at import.
- `palette/shortcuts/index.ts` spread five shortcut maps into one.
- `PaletteShortcutsModal` spread each shortcut again while grouping, so that
now happens per render.
Measured with a test that imports every source module with `ttag` mocked:
40 modules and 370 translation calls at import, down to 35 and 256.
* Build parameter, admin and graph settings when read
* Build option lists when they are rendered
* Guard import-time translation with a test
* Make the instance analytics message a function so the plugin registry can replace it
* Resolve admin path names when the navbar renders them
* Give the enterprise caching tabs a lazy name too
* Skip generated cli-snippets from the side-effect scan
* Key the null slice by its untranslated label
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUuaopJ7ZtMVvdwwxF6qyx
* Fail the import-time guard on unlisted import errors
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUuaopJ7ZtMVvdwwxF6qyx
* Read lazy translations once per render, not per lookup
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUuaopJ7ZtMVvdwwxF6qyx
* Let jest transform the MCP ext-apps package
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUuaopJ7ZtMVvdwwxF6qyx
* Skip the SDK cli scripts in the import-time guard
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUuaopJ7ZtMVvdwwxF6qyx
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Namespace the table name BigQuery's compiled native form carries as :qp/table-name
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Update the BigQuery compile tests for :qp/table-name
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Update the remaining BigQuery compile tests for :qp/table-name
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Refuse to create new tables in static datasets with Snowflake.
Table creation will always succeed on non-static datasets, but if it's
static then the table being created has to be a table that's in the
dataset definition.
Ensure all tests which alter the dataset by creating new tables
declare a non-static dataset. The transform-related tests switch over
to using transforms-dataset/transforms-test which most of them are
already using.
Upload tests have their dataset defined in a fixture so we can do it
all in one place instead of editing 60 different deftests.
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>
* Update map tile server settings with suggested copy from DOC-301
And also include a link to the relevant page in the metabase docs.
* Add a note about not including private keys to the docstring for map-tile-server-url
so that the warning also shows up in the env var docs.
* BigQuery: use PERCENTILE_CONT for median and percentile
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* update docs
* update doc
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Fixes UXW-4714.
The static viz for tables uses a fixed width, but it was too big for the
PDFs, causing tables with long text in columns to lay out badly.
This PR disables the fixed width of `780px` on tables for PDFs, so that
the tables now lay out correctly.
* Fix collisions on metabase.notification.payload.core-test namespace name. Lint for future collisions.
* Include .clj-kondo/{src,test} in backend check triggers
* Expect the new project test namespace
* Symbolically reference project test suite namespaces
* Include resources, test_resources, test_config dirs in project checks
* Return [namespace relative-path] pairs.
Skip local/src
* Catch read-file errors and explain why some files may not have ns forms
* Sort paths that collide
* Copy the transform tables in dump-to-h2 and load-from-h2
None of the nine transform_* models were in copy/entities, but metabase_table is,
and it carries a transform_id FK to transform. A table row reached the target
referencing a transform row that never arrived, and the deferred FK check
rejected it at commit.
transform_run_cancelation also goes in entities-without-autoinc-ids, since its
primary key is run_id rather than an autoincrement id.
Adds copied-tables-have-copied-fk-parents-test, which migrates a throwaway H2
database and fails if any copied table's FK parent is not copied.
* Simplify the copy foreign-key regression test
* Clarify the foreign-key coverage test names
* Make defendpoint reject request schemas that reach an open map, a keyword-keyed map-of or :any
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Close the MBQL 5 query and column schemas and declare the keys the query processor adds
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Type the shared schemas requests reach and drop ms/Map and ms/JSONString
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Close every map schema a request can reach
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Regenerate the modules config
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Keep action parameters string-keyed and type lens params and permission graph ids
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Declare the sandbox coercion keys a column's options can carry
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Declare the alias-info and Mongo keys a column can carry and strip internal keys from legacy column metadata
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Carry the Mongo column keys in the serialize test fixture
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Declare the keys the frontend and query processor put on columns, parameters and card embeds
- column metadata: :remapping, a typed :lib/options, the nest-breakouts option key
- query parameters accept the rest of a stored filter declaration
(sectionId, isMultiSelect, values source config etc.)
- static-list values may be one-tuples; raw-value template tags may
carry a nil widget-type
- card embed attrs declare chart_href, child_target_id and host_data;
explore filter refs accept the legacy field_ref the chart column has
- notification card echoes a metric's query_description
- serialization deep-sort orders mixed keyword/string keys
- viz-settings column refs are normalized before matching; a Field
hydrated with its table can be fingerprinted; broken segment
definitions read back as nil
- test fixtures: real fingerprints, snippet tag type, http channel
details, comment content, no min_duration_ms on duration strategies
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Wire the defendpoint check, keep every map single-keyed, and apply the review findings
- defendpoint now calls closed-schemas/check! on every request schema;
the walker no longer lets :any leak through collections, treats
map?/:some as :any, skips :=/:enum/:fn values and never skips the
default entry
- no map mixes keyword and string keys: ms/string-keyed-object gives an
opaque JSON object string-keyed declared entries; ProseMirror attrs
(documents, comments, explorations embeds), CSV upload parts, search
weights and testing query specs use it and their readers use string
keys; the serdes deep-sort comparator is back to master
- the Metabot context is fully typed from the frontend types instead of
an opaque default; a draft transform query without a database is
accepted
- permission graphs, settings PUT and SCIM PATCH reject a body whose
map-of entries the decoder dropped; ad-hoc query endpoints drop a
client-sent cache strategy; action parameter values keep collections
out without nil-ing them; SCIM meta.resourceType is optional; sandbox
user attributes are LoginAttributes; a metric filter clause carries its
offset options; explore filter refs are the legacy field_ref the
chart column has; result_metadata written by older releases and
hasVariableTemplateTagTarget on parameters are declared; a bare
map-of on a request path is guarded against scalars
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Close the endpoints tests define and let a column's target and name field be an id reference
The check runs on every defendpoint, the ones test namespaces define included. A column's
:target and :name-field are hydrated Fields on the backend but id references in the
metadata the frontend hands over; the closed column schema nil'd the reference, which
lost the search field of a model's primary key.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Hand an action's query only the parameter values, and fix two fixtures the closed schemas caught
A query action's stored parameters carry the frontend's widget settings; the query gets
the value keys. The Metabot viewing-context tests use ids no card can have, and the
Mongo cache test spells the strategy key the schema does.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Declare the table name BigQuery's compiled native form carries on a native stage
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Keep a BigQuery RECORD's sub-field sync metadata off its result column, and fix a binning fixture
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Move Snyk scans into the Lint Security workflow
Fold snyk.yml's three jobs into lint.security.yml as snyk-generate-poms,
snyk-frontend-projects, and snyk-maven-projects, and delete snyk.yml.
Snyk now runs on the existing daily 11:00 UTC cron instead of its own
05:00 cron and dependency-file push trigger. The notify job reports Snyk
failures to Slack alongside CodeQL and Trivy, and Snyk's upload-sarif is
bumped from v3 to v4 to match the rest of the workflow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* add timeouts
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* GDGT-3132 fix misaligned undo toast
* migrate UndoListing to from styled components to CSS module
* absolute px -> rem, compact -> compact-md
* fix inconsistent toast height, use line-height instead of measuring the DOM to handle single vs multi line layouts
* strip comments
* GDGT-3195: Fix inefficiencies in formatValue
formatValue is fine per-cell but pathological in bulk; the "Copy results"
button made this visible, with a 100x2000 grid spending ~4.5s in this path.
- Drop a dead dayjs strict parse. The format literal 'T' is not valid dayjs
escaping (dayjs wants [T]), so isValid() returned false for every value.
It sat above the string/number/boolean branches, so every non-date cell
paid for it.
- Only JSON.parse the value when a markdown_template will consume it,
instead of on every call.
- Memoize the per-column type predicates (isURL/isEmail/isTime/isDate/
isNumber/isCoordinate/isBoolean) in a WeakMap keyed on the column. Each
fans out into recursive type-hierarchy lookups that cross into CLJS, and
they only depend on the column. Entries snapshot the type fields they
were computed from, since the custom-viz API hands third-party code
mutable column objects.
- Skip new URL() construction for values with no ":" in getUrlProtocol.
- Cache Intl.NumberFormat by its constructor options, bounded at 100
entries with oldest-first eviction.
Also make the value-formatting signatures honest: formatUrl, getLinkText,
getLinkUrl and getRemappedValue take unknown, which is what they actually
receive. getUrlProtocol keeps its string signature, with getLinkUrl
narrowing before the call. This removes both `value as string` casts.
Benchmarked with a synthetic 100x2000 copy path (medians of 5):
full table 4512ms -> 440ms, text 1231 -> 12, currency 637 -> 20,
integer 535 -> 15, boolean 343 -> 5, email 101 -> 4.
Claude-Session: https://claude.ai/code/session_01KUcocQj2bJPwVBi4dXKyEP
* Cleanup
* Revert the createPlainCellFormatter change
Hoisting the options spread out of the per-cell closure is not worth the
churn. Measured in isolation with a realistic 22-key columnSettings, the
spread costs 0.023us per cell against 0.002us to reuse a hoisted
reference, so it saves ~4ms per 200k cells — a rounding error next to the
dead dayjs parse at ~13.5us per cell, and nothing at all for columns with
click behaviour, where both shapes spread per cell anyway.
* Address review feedback
Guard the markdown_template JSON.parse with a typeof check instead of
carrying the placeholder cast comment.
Cover the two changes the review called out as untested:
- numberFormatterForOptions returns the same formatter for equivalent
options, a separate one for different options, and evicts the oldest
entry once the cache is full. The eviction test resets the module so
the 100-entry boundary does not depend on what earlier tests cached.
- formatUrl passes strings and numbers through untouched and renders
booleans and objects rather than returning them raw.
Both were checked by mutation: breaking the coercion and stubbing the
cache lookup to always miss fails exactly those assertions.
* Move the DataReference library pane slot into querying
DataReference is the slot's only reader and querying owns its props type, so the slot lives next to it as PLUGIN_DATA_REFERENCE.LibraryPane.
The enterprise library plugin registers into it at boot, and plugins/oss/library.ts no longer imports querying's DataReference types.
* Move the verified search filter slot into search
SearchSidebar is the only reader of PLUGIN_CONTENT_VERIFICATION.VerifiedFilter, so the field becomes PLUGIN_SEARCH_FILTERS in metabase/search/plugin, next to the other search filter components. The EE content verification plugin registers the filter there from initializePlugin, and the rest of PLUGIN_CONTENT_VERIFICATION stays in plugins without the search import.
* Declare the search filters slot with definePluginSlot
* Import definePluginSlot from the plugins entry point
* Describe the verified filter default without naming its consumer
* Cover enterprise feature gates and plugin reset behaviour
* Remove redundant plugin registration tests
* Move the useGetIcon plugin slot into hooks/use-icon
The hook is the slot's only reader, so the slot and its OSS default live next to it as PLUGIN_ICON.
The enterprise collections plugin registers into it at boot, and plugins/oss/collections.ts no longer imports metabase/hooks/use-icon.
* Declare the entity icon slot with definePluginSlot
* Import definePluginSlot from the plugins entry point
* Cover enterprise feature gates and plugin reset behaviour
* Remove redundant plugin registration tests
* Trigger the stats workflows from Run tests, gated on the uberjar artifact
Both workflows have recorded nothing since 2026-09-03. PR #81238 took the push
trigger off `uberjar.yml`, so master merges reach it only through
`workflow_call`. A reusable workflow reached that way raises no `workflow_run`
event, and both watchers went quiet. 143 master commits went unmeasured.
They now watch "Run tests", which is what builds the jar on a master merge.
Its conclusion is not a usable gate, because it is `failure` on master more
often than `success`, from jobs that have nothing to do with the jar. The
uberjar artifact is the right gate: a new job looks it up without downloading
it, and a docs-only merge that skipped the build finds nothing and stops.
Calling both from `run-tests.yml` would have been lower latency, but a stats
failure would then turn the master run red, and `rerun-workflows.yml` watches
that run and would re-run master CI over it. Keeping them in their own run
keeps a stats problem to itself.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01171Lkmug15JzrcN2aMBLMk
* Let a PR run the size benchmark against its own uberjar
A `workflow_run` workflow always runs the copy on the default branch, so a
change here cannot be tried out before it merges. This matches the trigger
Bundle Load Stats already carries. A called run measures and reports; the
upload step stays gated on the `workflow_run` path, so it writes no row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01171Lkmug15JzrcN2aMBLMk
* Trim the trigger comments in the stats workflows
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Give the visualizations a question builder instead of the metadata object
* Let the visualizations read the question builder themselves
* Read the store when a chart brush builds its question
* Drop the single-use question helper
* Give the map and object-detail views the question builder too
* Drop the last v1 metadata reads from the visualization tree
* Send the Field columns a parameter widget reads in param_fields
* Stop re-resolving a parameter widget's fields through the store
* Keep the empty default a parameter widget's fields prop had
* Read the driver-dependent Field columns in param_fields expectations
* Add a harness for measuring bundle load time
* Report the second load and make the benchmark safe to run in CI
* Run the load benchmark across network and CPU conditions
* Sign the load benchmark in to a real instance
* Track page load times on every master merge
* Backfill load times for recent master commits
* Rewrite the load-time backfill in TypeScript with a tested core
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Launch the benchmark jar the way we ship it, and lint the .ts scripts
* Measure the whole load, not just DOMContentLoaded
* Do not let the performance marks gate a reading
* Drop the backfill workflow
The goal is a global trend, which the per-merge job builds on its own after a
few days. Backfilling only bought a readable chart sooner, at the cost of a
second workflow and the largest script in the PR.
The commit date stamp goes with it. It existed so a backfilled row could claim
its commit's date rather than today's, and the per-merge job never set it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Let a PR run the load benchmark against its own uberjar
A `workflow_run` workflow always runs the copy on the default branch, so a
change here cannot be tried out before it merges. The `workflow_call` trigger
lets CI hand the workflow a commit and reuse the uberjar it already built. A
called run measures and reports; it does not write rows to Stats.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01171Lkmug15JzrcN2aMBLMk
* Report each state from one load, not a median per reading
Every reading was medianed on its own, so a row described a load that never
happened. It could also put the readings out of order and print a page-ready
that precedes the TTFB beside it.
`measure.js` now picks the run whose DOMContentLoaded is the median of its
series and reports that run whole. The second visit happens once per browser
profile, so it stays the run itself, and its page-ready now comes from that same
load rather than from a median that included the series' first, uncached run.
`Cold spread %` still comes from all the cold runs. A spread is a property of the
series rather than of one load.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01171Lkmug15JzrcN2aMBLMk
* Read the largest paint with an observer, and cut the timeout
The first CI run reported a zero largest paint in all four conditions, and would
have done so forever. These entries never reach the performance timeline, so
`getEntriesByType` cannot see them. Only a `PerformanceObserver` can, and it has
to exist before the paint. Paint entries are buffered, which is why
first-contentful-paint needed none of this.
The browser keeps revising the value until the user interacts, and the reading is
taken once the page reports ready. It is a lower bound rather than the final
figure, which is what makes it comparable between commits.
That same run took 4.4 minutes, of which the measurement was 164 seconds. The
timeout drops from 25 minutes to 10.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01171Lkmug15JzrcN2aMBLMk
* Move the bench to frontend/test and write it in TypeScript
---------
Co-authored-by: Fraser <frasermcintosh1@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Take a metadata provider in the detail-view query helpers
* Select the detail-view queries instead of building them
* Take the provider for the table's own database
* Guard the table query selector's memoisation
* Type the table query guard against the API table
* for EE/white_label - do not use hardcoded metabase in filenames
* test coverage includes test for special characters in application-name
* also use gibberish names in tests
Closes QUE2-820
Closes#80557
There are two changes:
1. handle `nil` nodes gracefully during traversal, and
2. don't add `nil` nodes to the graph in the first place.
Both of these changes alone fix the reported issue, but both make sense on their own.
* Stabilize collection type-filter options via an items-metadata endpoint, default the type checkboxes to unchecked, and hide the filters for collections of 10 or fewer items
* Show every type in the filter popover, disabling the ones without matching items
* Use kebab-case params on the metadata endpoint and adapt toolbar tests to the visibility threshold
* Scope the items metadata to the models the list shows, name its count total_items, and align root permissions with the items list
* Assert the restricted root metadata count against the items list instead of a fixture-dependent number
* Code review improvements
* Cleaned up kebob usage
* Adapt the branch to master's show-exploration-documents param and re-record kondo budgets
* Cover the items-metadata endpoint's exploration-document scoping
---------
Co-authored-by: Nathan Voxland <nathan@metabase.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every provider behind Metabot now streams reasoning to the chain-of-thought UI,
with one predicate per adapter deciding the settings gate, the request directive,
stream forwarding and replay together.
* BOT-1895: Shared groundwork. Drop the pre-registry saved-model fallback from
Azure and Google; make tool_calls outrank reasoning in delta classification,
which previously lost a tool call's only id/name chunk; consolidate
reasoning-part coalescing into one helper.
* BOT-2027: Bedrock. Request thinking per model via the shared builders. The
openai-family gate answers false — probes across all four GPT models confirm
Bedrock never emits reasoning summaries.
* BOT-2028: Azure. Gate derived from the admin-typed deployment name, matched
case-insensitively, which also fixes a latent temperature 400 on cased Claude
deployments. Negative paths verified live; the positive path rests on docs.
* BOT-2025: Z.AI. Forward reasoning_content and send the thinking directive
explicitly — GLM was already thinking invisibly on every chat. Thinking-only
models get reasoning_effort and a 2048 floor on forced tool calls, flagged on
the supported-models row so the sets cannot diverge.
* BOT-2024: OpenRouter. Forward flat delta.reasoning and classify all 26 models
by live probe; the gpt-5.5/5.6 family streams under the server default, where
an explicit enable verifiably suppresses it. Forced tool calls floored on
mandatory-reasoning models.
* BOT-2026: Google Gemini. One catalog drives context windows, directive and
gate. Thought summaries on chat, thinkingLevel LOW on structured (no off
switch), and a 2048 floor — probed, a real prompt at 512 spent the whole
budget thinking and returned no tool call.
* BOT-2030: Mistral. Net-new cost: the default sends no thinking and only
"high"/"none" are accepted. Flattens vector content chunks into the shared
xf's events, and adds the opt-in reasoning replay hook Mistral's docs require.
* BOT-2046: Moonshot. kimi-k3 always thinks — explicit effort, mandatory in-turn
replay, 2048 floor; kimi-k2.6 is optional and disabled under a forced tool
choice, which it rejects while thinking.
Co-authored-by: Paolo Rascuna <1936871+prascuna@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Take a metadata provider in the shared query helpers
* Select the segment query instead of building it
* Take the provider for the segment definition's database
* Guard the segment query selector's memoisation
* Guard the segment query assertion against a vacuous pass
* Add a jest project for heap retention specs
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Scope the parsed x value cache to one getXValues pass
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Assert heap retention on the settled pass, not the first touch
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Key the log timestamp cache on the log object
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Key the schema viewer flow graph on the response object
The memo key serialized every table and field of the schema, so each distinct
schema held a megabyte-scale string forever. RTK Query returns a stable
reference per cache entry, so a WeakMap keeps the identity that useGraphSync
depends on without retaining the schema.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Key pie.rows on the dataset instead of a JSON copy of it
The memo key serialized every row and column, so each dataset the user viewed
held roughly its own size in cache key for the life of the tab. A WeakMap on
the dataset with a short settings subkey keeps the same hit rate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Ban new uses of underscore's memoize
The 17 remaining call sites are allowlisted. The list may only shrink.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Narrow the memoize rule to module scope
A cache built inside a function, a component render, a class field or a
constructor is released with whatever owns it. Only a module scope cache lives
for the life of the tab, so the rule now flags just those.
This drops the allowlist from 17 files to 5, and each remaining entry carries
the reason its set of keys stays small.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Remove the memoize allowlist
Three of the five module scope caches are gone:
- getScrollBarSize takes no arguments, so it is a lazy value, not a cache.
- getScheduleExplanation returns a string that no caller compares by identity.
- cronToBuilderValue moves to the one caller that needs a stable identity.
Schedule takes its value as a useMemo dependency, so StrategyForm now holds
that memo itself. A user can write any number of cron strings, so a module
cache for them was unbounded.
The remaining two carry an eslint-disable with the reason their set of keys
stays small.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Follow renderer_utils into the viz-core module
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Cover metabase/utils/memoize in the module scope rule
That memoize roots its WeakMap on the function it wraps, so it only releases
once that function does. At module scope the function never does, and the
nested Maps hold every argument ever passed. No call site does this today, but
the rule could not have caught one.
The rule now covers both sources, so it drops "underscore" from its name.
memoizeClass stays allowed: it keys on the instance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Drop re-reselect from the dashboard selectors
createCachedSelector keyed getQuestionByCard and
getDashcardParameterMappingOptions on the card id, a primitive, in a
FlatObjectCache. That cache is a plain object with no eviction, so a session
kept one Question per card id it ever saw, and each Question pins the whole
Metadata snapshot it was built with.
reselect v5 defaults to weakMapMemoize, which keys on the card and the metadata
objects themselves. Entries are released when their card leaves the store or
the metadata is replaced, and identity still holds for every card in the
working set, which is what connect and useSelector compare on.
Measured over 300 cards, with the states dropped: 6.2 MB retained before,
0.0 MB after. Per entry, 21462 B before and 51 B after.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Drop re-reselect from the lockfile
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Back metabase/utils/memoize with reselect's weakMapMemoize
The hand-rolled implementation had two problems. It allocated two arrays on
every call, including cache hits: 225 ms against 20 ms for weakMapMemoize over
two million hits. And its WeakMap was keyed on the wrapped function rather than
the wrapper, so a cache built around a module-level helper outlived the wrapper
that owned it. A plain Map would have been safer.
weakMapMemoize matches on the same terms, identity for objects and value for
primitives, so no call site changes. It also weak-keys object arguments, which
the old one held strongly.
definition-cache passed weakMapMemoize as the memoize and argsMemoize options.
reselect v5 already defaults to both, so those are dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Route all memoization through metabase/utils/memoize
underscore's memoize is gone from the app. Its 16 call sites now use the house
helper, which is reselect's weakMapMemoize. Three of them were in metabase-lib,
which cannot import metabase/utils, and all three took no arguments, so they
became plain lazy fields instead.
Two rules keep it that way:
- no-direct-memoize-import bans memoize from underscore and weakMapMemoize or
lruMemoize from reselect, in any scope. Only metabase/utils/memoize may reach
for an implementation.
- no-module-level-memoize keeps its job, now that the house helper is the only
one left to police.
measureTextWidthSafely took a style object that was rebuilt on every render, so
identity keying would never have hit. Its third argument is now the font family
string, which is the only field any caller ever set.
useMemoizedCallback returns the callable shape rather than casting to T, since
the memoized function also carries clearCache and resultsCount.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Build memoizeClass on the house memoize
It kept a module-level WeakMap and walked a nested Map trie keyed on the
instance, the method, the argument count and then each argument, assembling
that path into an array on every call.
Keying weakMapMemoize on the instance and then the arguments gives the same
per-instance caching. One cache per decorated method replaces the shared trie,
so the method no longer needs to be part of the key, and neither does the
argument count. The file is down to 61 lines from 128.
A leak spec covers the property this rests on: an instance's results are
released with the instance.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Measure memoizeClass retention under load
2,000 instances per pass, 20 calls each, across a method with no arguments and
one taking a primitive and an object. Nothing is retained once the instances
are dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Delete memoizeClass
It decorated a prototype method by rewriting the property descriptor, and put
the memoization 600 lines away from the method it applied to. Its one consumer
memoized a method whose body uses no `this`, so the same caching comes from
assigning a memoized arrow to a class field. The field is created per instance,
so the cache is released with the instance, and the memoization is now visible
at the definition.
metabase/utils/memoize is 14 lines, from 128.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Run the heap specs from their own jest config
They were a project inside jest.config.js, and a bare `jest` runs every
project, which is what CI does. So the sharded unit run picked them up, without
--expose-gc, and they failed as designed.
They now live in jest.memory.conf.js, the way the timezone specs live in their
own config. The shared base moves to jest.base.conf.js so neither file has to
duplicate it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Reduce the heap specs to one collection assertion each
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
* Follow the store mocks into test support
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012b67ubbXJVMuQrpuEksbeh
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A settings manager could change a connection's base URL without entering its
masked secret again. The update merged the stored secret back in, and
credential verification sent it to the new host before anything was persisted.
A network policy cannot tell a legitimate provider from a public host the
caller controls.
Changing a connection's effective base URL now requires freshly submitted
credentials for every sensitive field that would otherwise carry a secret to
the new destination. Environment-owned credentials must be repointed through
deployment configuration. Trusted provisioning such as config.yml is
unchanged.
Generic writes to the llm-providers setting run the same field validators, so
PUT /api/setting cannot bypass the connection API's checks.
* Enforce a network policy on LLM provider base URLs
A settings manager could point any provider's base URL at an internal
host; the model listing behind the admin Connect button then fetched
GET /v1/models from it with bare clj-http, and a 4xx body preview came
back in the error. Semi-blind SSRF.
New llm-allowed-networks setting, same shape as warehouse-allowed-networks:
external-only on Cloud, allow-all self-hosted (vLLM/Ollama on localhost
or a private network is the normal case there). Admin-only, so the
settings-manager class can't loosen it.
Every base-url registry field validates against it on write, so the
API rejects a forbidden URL before verify-credentials! makes the
request. core/request and llm.anthropic/chat-completion check again at
request time and resolve DNS through the policy resolver, which closes
the rebinding gap. The AI proxy URL is operator config and is exempt.
e2e pins MB_LLM_ALLOWED_NETWORKS=allow-all next to the warehouse pin,
since the mock LLM server is on localhost.
* Cover the embedding and Google token sinks too
Semantic search posts embeddings to llm-openai-api-base-url and to the
settings-manager-writable ee-embedding-service-base-url with bare
clj-http, so the same writer class reached the same kind of sink. Those
now run the llm-allowed-networks check and DNS resolver; the operator's
ai-service-base-url is exempt, and ee-embedding-service-base-url is
validated on write.
A Google service account key carries its own token_uri, and the
credential library posts to it to mint an access token before any
request we guard. Keys are now refused unless token_uri is one of
Google's OAuth hosts.
* Use var links in LLM policy docstrings
* Keep SSRF rejections out of the embedder breaker
* Normalize connection-time LLM policy errors
* Link LLM timeout settings in request docs
* Fix reflection warnings in LLM URL validation
* Use explicit policies for managed AI endpoints
* Address final Copilot review feedback
* Reject non-global special-purpose addresses
* Align network policy terminology
* Make external-only the LLM network policy everywhere
llm-allowed-networks is now environment-only (:internal, no setter)
and defaults to external-only on every instance. A settings manager is
who it defends against, and on Cloud a customer admin loosening it
would be reaching for our own infrastructure, so nobody sets it through
the API. Self-hosted operators with a vLLM server on a private network
opt in with MB_LLM_ALLOWED_NETWORKS; a value that is not one of the
three policies fails closed with one warning.
The rejection message says what to do: self-hosted, it names the
setting and which value fits; on Cloud, that private networks are out
of reach. The vLLM field's placeholder and help text no longer
advertise a private host as the default setup.
The managed endpoints' :allow-private is now a floor rather than a
replacement, so an explicit allow-all keeps an AI service on localhost
reachable. An embedding service URL supplied by the environment gets
the same floor, since the setter cannot vet it.
* Enforce the LLM network policy at connection time only
The request-time check resolved the host with a blocking lookup that
the connection's DNS resolver then repeated, and in the embedding path
it ran ahead of the breaker's fast-fail. Requests now run only the
I/O-free syntax check up front; the resolver decides on the addresses
the connection actually opens, and its refusal maps to the same 400
the set-time check gives. The set-time check still resolves, so a bad
URL is refused when it is entered.
One helper, llm-request-opts, replaces the three hand-copied
sequences; the refusal is thrown from one constructor. It carries
:status 400 beside :status-code because the semantic-search dead-letter
queue files errors by :status and was retrying a refused endpoint on
the fast schedule.
A base URL with a username or password is refused outright, and the
refusal names only the host, since the message reaches Metabot chat
users and the API's 400 body. A connection-time refusal is logged; it
is the only trace a host that rebinds leaves.
* Read llm-allowed-networks from the environment alone
get-value-of-type falls back to the application database, so a value
stored there before the setter went away kept working. The getter now
reads only the environment variable, and the setting carries a :doc
string so the generated environment-variable docs keep it now that it
has no setter. On Cloud the default applies; the operator's environment
is still honored, which the e2e runner's pin relies on.
* Disable redirects for LLM requests
* Normalize embedding service base URLs
* Normalize configured embedding URLs on read
* Clarify LLM network policy documentation
* Revert "Clarify LLM network policy documentation"
This reverts commit 8f2cff7c4963efba6756c117927ca56492cd49c5.
* Polish new LLM security comments
* Clarify embedding API key fallback
* Treat unallocated IPv6 space as non-global
* Grant private-network policy floors to environment-supplied URLs only
* Refuse IPv6 space IANA has not allocated
2000::/3 is the global-unicast space, but everything in it from 2d00:: up
is reserved: no ISP routes it, so a name resolving there is either a
mistake or an internal network squatting on it. `3000::1` passed as public
before.
* Pin Google's token endpoints by whole URI
The transport posts to whatever `token_uri` says, so checking only the
scheme and host let through another path, port, or query string on
accounts.google.com.
* Validate connections written as the llm-providers setting
The connection endpoints check a base URL against the network policy, but
the list they write is a setting in its own right: PUT /api/setting or a
config.yml entry saved a loopback URL straight past them. Run the field
validators in the setter too, on the connections a write changes.
* Hold the token-cache invalidation to the LLM network policy
/api/premium-features/token/refresh POSTs to ai-service-base-url with the
premium token in the URL path. That setting is admin-writable, so the
request needs the same syntax check, redirect ban and DNS policy as any
other LLM request; only an environment-supplied URL earns the private
floor.
* Check the target host when a JVM proxy is in the way
clj-http's route planner honours -Dhttps.proxyHost, so the connection is
opened to the proxy and the :dns-resolver is handed the proxy's hostname:
the target is resolved by the proxy, out of reach, and a private egress
proxy is itself refused under external-only. Treat an operator-configured
proxy as the trusted boundary it is and check the target host up front
instead.
* Judge a NAT64 address by the IPv4 it reaches
64:ff9b::/96 is globally reachable, but its low 32 bits are the IPv4 a
gateway translates to, so 64:ff9b::7f00:1 passed external-only and reached
127.0.0.1. RFC 6052 forbids the prefix carrying a non-global IPv4; nothing
on this side of the gateway enforces that.
* Validate only the connection fields a write changes
Comparing whole connections meant rotating an API key re-ran the base URL
check, so a private URL stored before the check existed made its own
connection unwritable.
* Name a network policy that would change the outcome
A refusal under a deployment endpoint's allow-private floor advised
setting MB_LLM_ALLOWED_NETWORKS=allow-private, which was already in force.
The resolver records the policy it refused under; use that for the message
and the log line.
* Refuse an unresolvable host behind a JVM proxy
Everywhere else a host that will not resolve is allowed, because the
connection then fails on its own. Behind a proxy there is no connection to
fail: the proxy resolves the target, so a split-horizon or cluster-internal
name went out with nothing checked at all.
* Keep proxy-only DNS working
* Document and test the LLM proxy enforcement boundary
* Match JVM proxy detection to Apache routing and fix tile fixture
* Build query builder questions from the store's builder
* Build document questions from the store's builder
* Build SDK and document questions from the store's builder
* Build queries from a provider, not the raw Metadata object
* Build questions from cards through the metadata store
* Type the dashcard menu against the question dashcards its callers pass
* Name the guard the callers actually use
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HNvsg9cPuNQ6BHiDwa1bif
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The illustrations switch only appeared next to a custom icon, so turning illustrations off
and then removing the icon left no way to switch them back on. It now appears whenever
illustrations are off as well.
Clackbot answered every failure with "Something went wrong", so a user denied by AI
permissions or an admin with a misconfigured provider had nothing to act on. Both the
streamed error part and the thrown-exception path now look the failure up by error
code and reply with copy that names the problem, and usage-limit errors pass their
server-authored message through. Anything unrecognised keeps the generic line, so raw
provider errors stay out of shared Slack channels.
* Serve the metabase-lib provider from the metadata store
* Serve metric metadata providers from the store too
* Serve providers for metrics and for per-item database lookups
* Take a provider lookup in the transforms query helpers
* Resolve SDK query sources from the RTK cache, not the mirror
* Delete the unused isSourceEmpty helper and the dead tableId prop chain
* Canonicalise getMetadata's options so both call shapes share one Metadata
* Build v1 Questions through the metadata store (#81666)
* Use explicit hierarchy for action keywords
The :table.row/* -> :table.row/common and :data-grid.row/* -> :data-grid.row/common
relationships lived in Clojure's global hierarchy, sharing one mutable namespace with model
keywords, hook traits and every other subsystem. Move them into metabase.actions.hierarchy,
mirroring metabase.events.hierarchy: a defonce var holding a hierarchy map, mutated through
derive!.
Four multimethods consume those parents -- action-arg-map-schema, normalize-action-arg-map,
validate-inputs! and default-mapping -- and now dispatch on the new hierarchy. Driver-level
action methods are untouched: they dispatch on [driver action] through the driver hierarchy
and only ever name leaf action keywords.
The enterprise action_v2 module derives through metabase.actions.core, which re-exports
derive!.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* State the real reason driver action dispatch is unaffected
The hierarchy docstring said driver-level action methods are safe because they only ever name leaf action
keywords. That describes an incidental convention -- `[:postgres :model.row/create]` happens to be the only
such method -- and implies a fragile dependency that does not exist.
`driver/hierarchy` is its own `(make-hierarchy)`, and `isa?` against an explicit hierarchy never consults the
global one for keyword parents, so the derives this namespace moves had no effect on `prepare-query*` before
the move either. Say that instead, and note it at `prepare-query*` too, where someone adding a driver method
will actually be reading.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Chris Truter <chris@metabase.com>
* Use explicit hierarchy for auth providers
Provider relationships (:provider/saml -> ::provider/provider, etc.) lived in Clojure's
global hierarchy, sharing one mutable namespace with model keywords, hook traits and every
other subsystem. Move them into metabase.auth-identity.hierarchy, mirroring
metabase.events.hierarchy: a defonce var holding a hierarchy map, mutated through derive!
and underive!, read through isa? and descendants.
The three provider multimethods (validate, authenticate, login!) now dispatch on that
hierarchy via methodical's :hierarchy option.
Callers outside the module go through metabase.auth-identity.core, which re-exports the four
operations; namespaces inside the module use metabase.auth-identity.hierarchy directly, since
core requires them.
derive! is whitelisted as :parallel/safe because each test registers its own distinct
:provider/test-* keyword, so the additions never overlap. The plain `derive` calls it replaces
were equally global but escaped the linter by not ending in `!`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Register test providers at namespace level, not inside parallel tests
Each of these eight ^:parallel tests registered its provider by calling derive! and
methodical/defmethod in the test body. Both mutate process-wide state, so the tests were never
actually parallel-safe -- only derive! was visible to the linter, because defmethod does not
end in `!`.
Hoist both registrations above their deftest, alongside the test providers already registered
at the top of the namespace. The deftests are now pure assertions over providers that exist by
the time the namespace has loaded, which is what ^:parallel claims. No linter exemption for
metabase.auth-identity.core/derive! is needed, so .clj-kondo/ratchets.edn is untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Chris Truter <chris@metabase.com>
Domain entity types derived from :DomainEntity/* in Clojure's global hierarchy, sharing one
mutable namespace with model keywords, hook traits and every other subsystem. Specs are loaded
from YAML files, so adding a spec file was writing into that shared space.
Move them into metabase.xrays.domain-entities.hierarchy, mirroring metabase.events.hierarchy:
a defonce var holding a hierarchy map, mutated through derive!. A leaf namespace rather than
an in-file var because both specs (which registers types and validates them) and core (which
ranks candidate entities by ancestor count) read it.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Key-value namespaces registered by defnamespace derived from ::registered-namespace in
Clojure's global hierarchy, sharing one mutable namespace with model keywords, hook traits and
every other subsystem. Namespaces are loaded from EDN files on disk, so an operator adding a
file was writing into that shared space.
Move them into a hierarchy var private to this namespace, mutated through derive! -- the same
shape as metabase.events.hierarchy, kept in-file because defnamespace is the only writer and
known-namespaces the only reader.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Fail the mage suite when a test namespace is not required
`mage -test` runs `run-all-tests`, which only sees namespaces that are already
loaded, and loading happens solely through this namespace's `:require`. A test
file missing from it never runs, and nothing says so.
`mage.quick-test-runner-test` and `mage.start-db-test` were missing that way.
They are required by `mage/test/mage/runtests.clj`, but CI runs `mage -test`,
which does not read that file, so their four tests had not been running.
* Drop the dead mage test runner and a test that cannot fail
Nothing referenced mage/test/mage/runtests.clj. It required three namespaces
directly, which made mage.quick-test-runner-test and mage.start-db-test look
wired up while `mage -test` never loaded either.
mage.start-db-test asserted a string literal, so it passed unconditionally.
Wiring it in would have added a green test covering nothing. start-db still
deserves real tests; this makes their absence visible.
* Fix EditableDashboard collapsing when entering edit mode without a sized parent
* Also fix EditableDashboard collapsing while sharing
* Revert "Also fix EditableDashboard collapsing while sharing"
This reverts commit be2f86c738.
GDGT-2028: Fix wrong column headers in alerts with a remapped foreign key
The header titles and the header row were built separately and zipped by
index. Only the header row dropped the remap target column, so every title
after it shifted by one. Both now derive from `visible-columns`.
An H2 dump made by an OSS build kept the metabot_permissions rows that migration 061 seeds into the fresh target instead of copying the source's own rows, because the table only appears in cmd.copy/entities when the Enterprise backend is on the classpath. The seed references the four magic groups at ids 1 to 4, which on the source are often different groups or no groups at all, so restoring that dump into Cloud either failed on a foreign key or handed Metabot permissions to whoever happened to sit at ids 3 and 4.
The table is now copied on every edition. The load side also skips rows whose group is missing from the dump, so the dumps customers have already sent restore once Cloud picks this up, and magic groups left without rows get the 061 seed re-run by magic_group_type so a restored instance matches a fresh one. Grants that landed on a real custom group cannot be told apart from a deliberate grant and stay a data fix, tracked in CLO-5999.
* Start subprocesses with ProcessBuilder
Runtime.exec's three-arg form has been deprecated since JDK 18, and it forced the command and environment
through String arrays with array type hints to match. ProcessBuilder takes both as collections, so the
call site is one line and the array juggling is gone. `:env` still replaces the parent environment rather
than adding to it, which is what the docstring promises: that is `.clear` then `.putAll`, not a bare
`.putAll`. read-lines now hints the BufferedReader the namespace already imports.
* Spend one timeout budget across a command's three waits
sh* waited for the exit code, then stdout, then stderr, giving each a fresh timeout-ms. A command that
dribbled output could take three times its budget, and the exception still claimed the single figure, so
"Timed out after 200 ms." could be thrown 600 ms in. The three waits now measure against one deadline
taken when the process starts.
* Measure the backend health-check wait with nanoTime
check-backend-health! polls for five minutes against a wall-clock start, so a clock adjustment mid-poll
could cut the wait short or stretch it. The interval literal is now written as a duration too.
* Kill the command however the wait ends, not only on timeout
Three fixes around sh*'s failure handling.
The catch was for ExceptionInfo carrying :timed-out?, so any other way out left the command running: a
reader future failing on the pipe, or the calling thread being interrupted. It now catches Throwable and
asks whether the process is still alive, which is the question that actually decides whether to kill.
:or does not fire on an explicit nil, so a caller computing :timeout-ms from an env var and handing us nil
got a NullPointerException building the deadline rather than the default.
The test's 200 ms budget left the shell to fork twice and write both pids with no margin. On a loaded
runner the file could come up a line short, failing the descendant assertion for the wrong reason. A
second is still far below the bound the test asserts.
* Stop waiting on a command that orphaned our output pipe
sh* could sit for as long as an orphan lived, whatever its timeout said: 62 s against a 500 ms budget in
the case that prompted this. with-open closed the BufferedReader, and that close takes the same lock a
blocked readLine holds, so it waited for EOF -- which a process the command left holding our stdout never
delivers. Closing the pipe underneath ends the read at once, so the streams are now closed directly.
Closing the pipe means a cut-off read reports EOF rather than an error, which would make truncation
invisible. Two things guard against that. The readers get a floor of their own once the exit code is in,
so a command that finished a moment inside its budget is not robbed of the output it had already written.
And if the pipe is still held when that floor runs out, sh* says so and throws, because half a command's
output returned as though it were all of it is worse than a failure.
The orphan itself is still running afterwards. Reaching it needs process-group handling, which is a
larger change and no mage command leaks one today.
* Rewrite the comments and docstrings in mage.shell
The docstrings now state the caller's contract and nothing else: what sh* returns, what it throws, what
the timeout actually covers now that one budget spans the whole command. Rationale about how the body
works moved down to the lines it explains -- why the descendants are listed before anything is signalled,
why the deadline is read off a monotonic clock, why the pipes are closed rather than the readers.
* Tighten the comments once more, and drop the duration glosses
A second pass over the comment wording, and a correction: a DST change does not move the epoch
milliseconds `currentTimeMillis` reports, so it was never one of the things nanoTime protects the deadline
from. NTP corrections and manual clock changes are.
The `; 5 seconds` style glosses are gone throughout. Writing the constant as `(* 5 1000)` already says
which unit it is in, and the name carries the rest.
* Serialize mage shell output and fix test namespace loading
`println` writes the text and its newline separately, so the stdout and
stderr readers could land halves of two lines on one line of the terminal.
Lock on the destination, which also keeps two commands streaming at once
from splitting each other.
`mage -test` runs `run-all-tests`, which only sees namespaces that are
already loaded, and nothing required mage.bot.pr-env-test or
mage.bot.repl-eval-test, so eight tests never ran. The `(comment ...)`
block meant to hold these requires open only referenced an alias that
existed for its own benefit; kondo does not flag alias-less requires, so
it was protecting nothing.
A Bedrock connection with no access key pair now signs with whatever the AWS
SDK default credentials chain resolves (environment, IRSA, EKS Pod Identity,
ECS task role, EC2 instance profile), and the SDK refreshes those on its own,
so a pod that already has a role needs no long-lived keys. Explicit keys still
override the chain, and half a pair or a lone session token is rejected.
On Metabase Cloud the chain would resolve our own identity rather than the
customer's, so a keyless connection is rejected there, and a token status the
license service could not confirm counts as hosted.
The streamGenerateContent xf walked each event's parts with a plain
`reduce`, which strips the `reduced` wrapper a cancelled consumer
returns. The stop never reached the outer reduction, so every event
that followed was still translated and pushed downstream — Google's
response was read to the end, and billed, after the client had gone.
The other adapters propagate the stop; only this one dropped it.
Use u/reduce-preserving-reduced so the early stop survives, and pin it
with a test that cancels on the first text delta: before the fix the
consumer kept receiving text from later events.
Co-authored-by: Paolo Rascuna <1936871+prascuna@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* Only serve tenant attributes and member_count to superusers
* Scope pulse recipients and the Slack directory by tenancy
* Update cross-tenant recipient test for non-superuser scoping
* GDGT-2467: Restrict Paper padding/radius to curated token options
* Update Loki Snapshots
* GDGT-2467: Update entity picker width e2e expectation for Paper padding
* GDGT-2467: Keep Paper default padding at 0, add no-padding option to stories
* GDGT-2467: Keep all padding options in Paper stories, only curate radius
* Update Loki Snapshots
* GDGT-2467: Type PAPER_RADIUS_OPTIONS against RadiusScaleKey
---------
Co-authored-by: Metabase Automation <github-automation@metabase.com>
* Resolve the under-test transform's target live in cycle detection
* Split the change-target overwrite e2e test into the two cases it conflated
Retargeting onto the transform's own source table is a self-cycle (400), not a
target conflict, so exercise the conflict case with a table produced by another
transform and add a dedicated self-cycle case.
* Make the transform under test win target collisions in get-transform-cycle
* remove live-target-table-id
* Make fix-sql-params regex linear-time
The tag-body class excluded only `}`, so `\s*[^}]+\s*` had three overlapping
quantifiers and backtracked cubically on an unclosed `{ {` followed by
whitespace. Exclude `{` from the body and drop the redundant \s* so every
position matches in linear time.
* Use :standardsql dialect in test so no kondo ignore is needed
Previously it was just picking a table at random and using that row,
but in redshift it turns out that table's row doesn't actually exist,
causing a bunch of failures. Let's look specifically at the people
test-data table instead of just using whatever.
Sweep orphaned search_index__* tables at the start of reindex! instead of at activation, so a rebuild
that dies partway is cleaned up by the next run instead of leaving its table behind. The sweep is best
effort and never aborts the reindex.
* add test
* Cast DATE inputs before timezone conversion on Oracle and SQL Server
`convertTimezone` on a `DATE` column failed on both drivers:
- Oracle: `FROM_TZ` only accepts a `TIMESTAMP` and refuses to implicitly
promote a `DATE` (ORA-00932: inconsistent datatypes: expected TIMESTAMP got
DATE). Cast date-typed inputs to `TIMESTAMP` first.
- SQL Server: `AT TIME ZONE` rejects a `date` argument (error 8116: Argument
data type date is invalid for argument 1 of AT TIME ZONE function). Cast
date-typed inputs to `datetime2` first.
Fixes `convert-timezone-date-column-test` for both drivers (#27186).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYP35BadwdEL9LAC9zHWQZ
* Derive expected DATE column values from the results timezone in test
`convert-timezone-date-column-test` hardcoded the raw `d` column as
midnight `+07:00`, which is only right for drivers that support
`:set-timezone`. SQL Server can't set a session timezone, so its results
timezone falls back to the database timezone (UTC in CI) and the column
formats as `Z`. Compute the expected values the same way `format-rows`
does so the assertion holds on every driver with `:convert-timezone`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RYP35BadwdEL9LAC9zHWQZ
---------
Co-authored-by: Claude <noreply@anthropic.com>
We want to drop as much usage of legacy-mbql as we can. This function
is never used internally, but it's exported in the driver-api. We
should mark it as deprecated since everything else in this namespace
is deprecated, so we can remove it when the time comes.
Need to get this in today so it's included in the 64 branch.
* Replace Bigquery/Snowflake dataset tracking table; track by name instead.
The current system of using a tracking table was based on the
assumption that all datasets would need to be tracked by access
time. In fact, datasets fall into one of two buckets, either they are
static and immutable, or they are temporary one-offs. Static datasets
change *very* rarely and do not necessitate automated tracking. If we
neglect to clean them up, we could go for decades before the
accumulated datasets pose a real problem. But non-static datasets are
used once and accumulate *very* rapidly, needing to be cleaned up.
All datasets defined using `defdataset` are considered static.
Rather than using a database to track which ones must be cleaned up,
the logic can be much simpler; use the date embedded in the dataset's
name. It turns out it's even simpler because the logic to track
datasets based on their name has existed this entire time since 2023
in `metabase.driver.sql.test-util.unique-prefix`; we just stopped
using it in Snowflake/Bigquery in favor of the overly-complex tracking
table which A) leaked like mad and B) mistakenly marking static
datasets as old and causing them to get deleted while they were still
in use.
Redshift already uses the `unique-prefix` namespace generating its
schemas, so it only needs a few minor changes to adapt to the changes
in the gc job and the `unique-prefix` functions.
This also simplifies the gc nightly task; rather than having two
separate settings, we only need one setting for how many hours old a
non-static dataset has to be before cleaning it up. Also adds a
`dry-run?` option for manual testing.
* Address review feedback.
* Update workflow yaml to refer to single :hours parameter
* Remove 256-row limit from bigquery/snowflake
* Fix snowflake dataset :name in report data
* Get rid of with-write-stmt helper.
* Accept tracked? arg for manually GCing tracked datasets.
* Keep the data model in sync when a change is undone
* Extract shared TableSection update handlers into a hook
* Undo a custom field order with a single request
* Guard the optimistic table patch by table id
* Reproduce the dropped refetch scenario in the middleware test
* Link the upstream RTK issue from the dropped-refetch middleware and test
* Accept "all" as a collection id in the browser public API
Adds the public api plumbing
* refactor: own the ItemsTable container query inside ItemsTable
The CSS container was defined in CollectionContent.module.css, which meant it only worked on ItemsTable when it was rendered inside CollectionItemsTable. The virtual "all collections" root renders ItemsTable on its own, so the container moves onto ItemsTable.
* refactor: split the collection breadcrumbs into a view and its data
We'll need to customize what breadcrumbs will be rendered for the virtual root.
This commit extracts the view that we'll use directly.
* Clear the browsed collection while the next one loads
RTK Query keeps serving the previous collection from `data` while the next one
loads, and keeps serving it once the argument becomes skipToken. A caller that
reads `can_write` during the switch therefore offers to create content in the
collection the user just left.
`currentData` drops to undefined on any argument change, so it covers both
cases and the manual clearing from 397a3e82e04 goes away.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gdru2a8afSo6y24e99W6hy
* Don't let the popped stack entry override the clicked breadcrumb
Clicking a collection breadcrumb from inside a question landed on the
collection the browser was mounted on, not on the one the user clicked.
Opening a question, a metric or a model pushes a navigation stack entry, so the
back button keeps working. That entry carried an onPop that reset the view to
the mounted collection. The breadcrumb handler sets the view to the clicked
crumb and then pops the stack, so onPop ran last and overwrote the click.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gdru2a8afSo6y24e99W6hy
* Render the virtual "all collections" root in the collection browser
collectionId="all" now lists everything the current user can reach, as flat
siblings, in the same order the entity picker uses: "Our analytics", the shared
tenant collections, "Our data" and the personal collection.
useAllCollectionsItems builds those rows:
- A 403 on GET /api/collection/root means the user cannot read "Our analytics"
itself, so the hook falls back to /api/collection/root/items, which has no
read check on the root. The models=collection argument is required: without
it the endpoint returns no models at all.
- "Our analytics" needs a numeric id for the table, so it gets 0. Real
collection ids start at 1, and the caller maps 0 back to "root".
- The shared tenant collections come from the collection tree, gated on the
use-tenants setting and on the tenants plugin. shallow=true keeps the
backend from building a nested tree the caller never reads.
- The backend excludes the tenant collection from /api/collection/root/items,
so the hook adds it from the current user.
CollectionBrowser renders the rows with ItemsTable directly, without the
archive column: archiving resolves its permissions from a parent collection,
and the virtual root has none.
The breadcrumbs put a static "All collections" crumb in front of
CollectionBreadcrumbsView. The personal and the namespaced collections lose
their "Our analytics" crumb, because the API reports them as its children
while the virtual root shows them as siblings.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gdru2a8afSo6y24e99W6hy
* Key the SDK breadcrumbs by type and id
Breadcrumb ids come from separate id spaces, so a collection and a dashboard
can both be 5. On an instance where collection 5 holds dashboard 5 the trail is
[all-collections "all", collection 5, dashboard 5], and two children of the
same list share the key 5. React warns about the duplicate key in the host
application's console.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gdru2a8afSo6y24e99W6hy
* fix sdk package type
* e2e tests
* Unit test the virtual "all collections" root
* "all collections" -> "all-collections"
Co-authored-by: Mahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev>
* pageSize support
* Assert the error message, not the error container
The virtual root error tests only checked that the sdk-error-container
testid rendered. That proves an error appeared, not which one, so any
other failure rendering into the same container would keep them green.
They now look for "Failed to load collections".
Verified by mutation: changing that string in CollectionBrowser turns
all four tests red, where the testid version stayed green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C9MrpBnd3VEiXVvYf15Qqd
* change root id from 0 to -1 and wrap the mapping
* import already existing defer
* not.toHaveLength(0) -> toBeGreaterThan(0)
* clarify that the root is 403 in the test
* `SdkBrowseCollectionId` -> `SdkBrowserCollectionId`
* docs(sdk): list the accepted collectionId values as bullets
The CollectionBrowser `collectionId` doc described five accepted values in
one running paragraph, and adding "all" pushed it past readable. Split it
into one bullet per value, and document the entity ID string, which the
paragraph never mentioned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UTu6CuVJnRjzS1HrmrJQT3
* else if
* rename the virtual root identifiers to all mode root
* refactor root status into union
* clarify when the root could be empty
* fix eslint
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Mahatthana (Kelvin) Nomsawadi <me@bboykelvin.dev>
* Warn when startup or a migration encrypts data that was stored unencrypted
The settings backfill reports that settings saved by an older version are being
converted, the startup sweep reports each column whose plaintext values it
encrypted, and the migrations that encrypt existing rows report how many they
touched, so an operator can see when and why data changed at rest.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Find legacy unencrypted values first and hand them to a handler before encrypting them
`legacy-unencrypted-value?` decides what counts as legacy plaintext, the startup
sweep and the encrypting migrations filter their rows with it, and
`handle-legacy-unencrypted-values!` is called with the count before anything is
encrypted, so it can turn from a warning into something else later. The
migrations keep private mirrors of both, as they do for the encrypted JSON
helpers, and read their rows eagerly: reducing a reducible query closes the
transaction's connection.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Call the handler's location parameter title
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Make the encryption migrations no-ops and move the app-db module's queries to db.clj
The Encrypt* custom migrations did the same work `encrypt-plaintext-columns!` does
on every startup, so their forward bodies are gone; the reversible ones keep their
decrypting rollback for downgrades. The handler now takes only a `table.column`
location. The encryption and setting namespaces run their Toucan 2 queries through
the new `metabase.app-db.db`, like the other modules.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Drop the decrypting rollbacks of the no-op encryption migrations
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Make a fresh install come out fully encrypted from the migrations alone
The encryption-touching data migrations that only rewrote existing rows are no-ops
now that the startup sweep covers their columns, and MigrateMetricsToV2 goes with
them. Two v58 changesets left rows a fresh install would then see as legacy: the
SQL copy of password authentication into auth_identity is retired from the
changelog and redone in Clojure with the credentials encrypted, and sample content
is created by CreateSampleContentV3 once setting.value_with_aad exists so the
example-dashboard-id row is written whole. The settings backfill skips the
encryption-check sentinel, which the encryption tooling rewrites itself. The
fresh-install test asserts that setup logs no app-db warning and that the sweep
afterwards finds nothing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Keep the sample-content migration tests in version order
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Stop inserting the plaintext encryption-check marker on new databases
The v53 changeset that inserted it is retired from the changelog: the encryption
tooling writes the sentinel itself, whole, and reads a missing row the same way, so
the settings backfill no longer needs to special-case it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Delete the plaintext encryption-check marker instead of retiring its changeset
The v53 changeset inserted the marker on every new database, encrypted ones
included, so it never said anything about the actual state. It stays in the
changelog as a checksum-agnostic no-op, since a rollback to a release that still
has it must not re-run the insert, and a new changeset deletes the marker rows: with
no row the state is unknown and judged from the content, which the encryption
tooling already does.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Leave the v53 encryption-check changeset untouched
Its checksum is recorded on every existing database and validCheckSum: ANY does
not lift the check, so it keeps inserting the plaintext marker on a new database
and the v58 changeset deletes the row right after.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Keep the data migrations, retire the Encrypt* changesets, and never change a row's encryption state
The migrations that rewrite encrypted JSON columns are back with their original
bodies, but `encrypted-json-in` now writes a row back the way it was stored:
encrypted only when the stored value decrypts with the current key, so a
migration never turns plaintext into ciphertext or the reverse. The Encrypt*
migrations are removed outright, with a changeset retiring their changelog
records, since encrypting legacy plaintext is the startup sweep's job.
CreateSampleContentV2 is untouched again and a small migration fills its
setting's value_with_aad from the strictly decrypted value; the LLM providers
migration writes both columns too.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Address the review: decide by decrypting, stream the sweep, and write NULL rather than an empty SET
The settings backfill now treats a value as encrypted only if it decrypts, so a
plaintext value that merely looks like ciphertext is filled in instead of being
skipped forever. update-setting-values! takes the columns to set as a map, so a
value that encrypts to nothing is written as NULL as before instead of compiling to
an empty SET. The startup sweep streams each column again, warning once before the
first row it encrypts. The unused bytes predicate is gone, the string one is
private, and the two changelog-retirement changesets are one, using the changelog
table property.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Keep the SQL copy of password authentication; the startup sweep encrypts the credentials
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Give the internal user no password
It is inactive and can never log in, and a password would be copied into
auth_identity bare by the v58 SQL changesets, leaving a fresh install with one
row for the startup sweep to encrypt.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* add deterministic data_sensitivity rules classifier
* fill data_sensitivity rule table to full enum breadth
* add data_sensitivity scan step and opt-in setting
* register classify-data-sensitivity as the last analyze step
* document PUBLIC as scanned-or-reviewed for data_sensitivity; add end-to-end tests for the data_sensitivity scan
* label non-Text base types and integer/band variants in the data sensitivity classifier
* add reset-data-sensitivity! to clear classifier-written labels
* Add test for cumulative sum of a literal number (#27807)
Adds a QP test verifying that a custom aggregation expression of
CumulativeSum(1) on the Products table, broken out by Created At by
month and ordered ascending, runs successfully instead of failing
with 'Field 1: Field does not exist'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011kKRbqUbfSKNViTEzS82ss
* clean up
* undo
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Metadata for plain-`:field` expressions should propagate Field ID and Table ID
* Fix 🔧
* Add tests 🔧
* Test fix 🔧
* Test fix 🔧
* Test fixes 🔧
* Test fix 🔧
* add nullable data_sensitivity column to Field and its user-settings mirror
* Expose data_sensitivity on PUT /api/field/:id
* add tests for data_sensitivity survives sync, reactivation, and base-type changes
* add serdes round-trip tests for data_sensitivity on Field and FieldUserSettings
* Expose data_sensitivity in Lib column metadata
* tweak wording
* Keep on-demand FieldValues fetches alive past request cancellation
On-demand FieldValues are fetched on the request thread, so a canceled HTTP
request threw the warehouse scan away: every retry started the scan over and
nothing ever landed in the app DB, which is what makes these requests feel
like they take forever.
Run the fetch on a detached thread instead. It runs to completion and persists
what it fetched even when the caller stops waiting, and concurrent callers for
the same field (and sandboxing/constraint context) share the one in-flight run
rather than each kicking off their own scan.
Fixes GHY-2937 (#67843)
* Log detached FieldValues fetch failures
When every caller has walked away — the case detached-fetch! exists for —
nobody derefs the promise, so a thrown exception was dropped without a trace.
* Don't wipe cached FieldValues when the warehouse scan fails
`distinct-values` catches and logs warehouse errors, then returns nil — but
`create-or-update-full-field-values!` destructured that nil into empty values
and handed it to `persist-field-values!`, which reads empty as "this field has
no values" and calls `clear-field-values-for-field!`. So any failed or
timed-out scan deleted the values the field already had cached, and a field
slow enough to hit the QP timeout could never keep a cache at all.
Distinguish the two cases: nil means the scan failed, so leave whatever is
cached alone and return ::fv-fetch-failed. A scan that genuinely comes back
empty still clears, as before. The sync path already made this distinction
(see fetch-distinct-per-field's :failed-fields).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
* Guarantee detached FieldValues fetches always complete
A registry entry left behind with its promise undelivered is worse than a
leak: every later caller for that key parks on it forever, holding a request
thread each. Two paths could do that.
The `log/warnf` added for failed fetches sits inside the catch, so an appender
that throws skips both the registry cleanup and the delivery. And the registry
entry is claimed before the work is submitted, so a rejected submit — which is
what `future`'s pool does once the JVM starts shutting down — orphans it.
Route both through `complete-fetch!`, which still drops the entry before
delivering so a woken caller starts a fresh fetch rather than re-attaching to
a finished one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
* Sweep and cancel stalled detached FieldValues fetches
Nothing bounded the in-flight registry except every code path remembering to
clean up after itself, which is a property that keeps not holding — the
logging call has now stranded entries twice.
Give each entry a timer and its future, and sweep on the way into
detached-fetch!, which is the only moment the registry can grow. The sweep
cancels the work and *completes* the entry rather than just forgetting it:
callers park on the promise, so dropping one undelivered leaves them blocked
forever, each holding a request thread.
Only a fetch wedged outside the query processor can reach the max age — the
warehouse query is already capped by *query-timeout-ms* (20 minutes in prod).
Removing an entry is now conditional on it still being the registered one, so
a swept fetch that later finishes can't evict its own replacement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
* Filter stalled fetches in the doseq rather than up front
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
* Cap the in-flight FieldValues registry at 100 entries
Callers park on their fetch, so the number in flight is today limited by the
request thread pool — but that is an accident of how these endpoints are
served, not something this namespace can guarantee, and it stops holding the
moment anything frees a caller early.
Past the cap, detached-fetch! stops detaching and runs the work on the calling
thread. That degrades to what happened before detaching existed — no
single-flighting, no surviving cancellation — rather than failing the request,
so a ceiling that should never be reached can't take a filter dropdown down
with it. Capacity is checked inside the swap, so the ceiling holds exactly
under concurrent claims.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
* Refuse FieldValues fetches past the in-flight cap
Reaching the cap means something is wrong — callers park on their fetch, so
the number in flight is bounded by the request thread pool unless that stops
holding. Absorb it by running the work inline and the ceiling stops being a
signal; throw a 503 and it says what it means.
Raised to 200 now that hitting it fails the request rather than degrading it:
an instance has to have raised MB_JETTY_MAXTHREADS well past its default of 50
before this is reachable at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
* Pin down that joining an in-flight fetch bypasses the cap
The behavior was already right — claim-fetch!'s `contains?` short-circuits
before the count check — but it reads as incidental, and a caller that only
attaches to existing work adds no entry, so the ceiling has no reason to
refuse it. Say so, and test it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
* Bound how long a caller waits on a detached FieldValues fetch
sweep-stalled-fetches! only runs when a request arrives, which is exactly what
the case it guards against prevents: callers parked on a wedged fetch are
request threads, so enough of them and nothing reaches the sweep to cancel
anything. The watchdog was only reachable while the system was healthy.
Bound the waiter's deref by the same max age. The expensive thing here is the
parked request threads rather than the registry entry, and giving those back
lets requests flow again — at which point the next one sweeps the corpse. No
scheduler needed.
fetch-max-age-ms stays a backstop rather than a request-latency bound: a value
nearer a sensible HTTP timeout would return the thread far sooner, but
answering before the values are ready changes what the endpoint promises, and
that wants deciding on its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
* Report scan failures from POST /api/field/:id/rescan_values
The endpoint discarded the result and always answered {:status :success}. That
was already misleading, and the cache-wipe fix made it worse: a failed scan now
leaves the existing FieldValues untouched, so there is no observable trace of
it at all — the caller is told the scan succeeded and sees the values it had
before.
Return a 500 when the scan failed. The button already toasts on error, so this
is the difference between "Scan triggered!" and a visible failure.
Unlike the table and database endpoints, this one runs the scan synchronously,
so it is in a position to know.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
* Serve warm advanced FieldValues without a detached fetch
The advanced branch wrapped the whole select-or-insert in detached-fetch!, so a
cache hit — one SELECT — paid a thread handoff and a registry sweep for a plain
read. field-id->field-values-for-current-user walks advanced fields serially,
so a sandboxed user with N filters paid N of them.
Look the row up on the calling thread and detach only on a miss, which is the
only case that scans the warehouse. This is what the full-values path at
get-or-create-full-field-values! already did.
The miss path gains one extra SELECT, since select-or-insert! still does its
own read before inserting. That is noise next to a warehouse scan, and keeping
select-or-insert! there is what makes two callers racing a genuine miss safe —
detached-fetch! only single-flights within one JVM.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
* Stop the FieldValues tests mutating JVM-global state
Two tests could only be trusted because of a test-runner setting. The cap test
stuffed 200 fake entries into the shared in-flight registry, so anything else
asking for field values during that window would have been refused with a 503;
three tests swapped distinct-values with `with-redefs`, which rebinds the root
for the whole process. Under the current runner nothing else is running, but
that is a property of `:multithread? :vars`, not of the tests — flip it and the
failures land in other people's namespaces.
Make the two knobs dynamic so the cap test can bind the ceiling to 0 and drive
everything through the public API instead: one real fetch held in flight, a new
key refused, and a joiner admitted. That tests more than the fake entries did.
Use the thread-local `mt/with-dynamic-fn-redefs` where the work stays on
threads that inherit bindings. Where it delegates to the original, capture it
with `mt/original-fn`: `patch-vars!` installs its proxy as the root for the
life of the JVM, so a bare symbol captured after some earlier test patched the
same var would have recursed into the proxy.
The rescan test keeps `with-redefs` — `mt/user-http-request` is served on a
Jetty worker that inherits nothing, so only a root rebind is visible there.
Commented so nobody "fixes" it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwGt3aocjPgJeGXTuEjujp
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The three system prompts, the two usage limit settings and the quota message no
longer read their environment variables, so they are set through the admin page,
the API or a config file like every other setting. Name, icon and
show-illustrations keep theirs, which matches the MB_APPLICATION_* whitelabel
settings that self-hosted deployments set once at startup.
Demoting a setting used to drop it from the config file template as well, because
the template generator shared the environment variable docs filter. That filter
now sits with the docs alone, which puts use-tenants back in the template.
transforms-setup-complete is derived and never read back, so it is marked
read-only and the template stops advertising a key that does nothing.
An `:error-code` means the throw site wrote this as an API response rather than
hitting an unexpected failure, so the client gets the message and the error data
and nothing else. The Metabot token-limit 402 and the transforms trial-limit 402
both returned a full backend trace before this.
Validation errors, real 500s and the older `:error_code` spelling are untouched.
* MCP v2 prep: extract query guards + pulse helpers; additive shared-ns changes v2 needs
Behavior-preserving groundwork for the v2 MCP surface, landed while v1 is
untouched and v2 is not yet present:
- NEW metabase.agent-api.query-guards (+ its test): query safety checks
extracted so REST and MCP run identical guard logic.
- metabase.pulse: recipient-filtering and sensitive-metadata-stripping
logic moved from private fns in api/pulse.clj into models/pulse.clj and
exported via pulse.core / pulse.api, so REST and MCP share it. Pure move;
existing REST pulse tests are the check (+ new direct tests).
- metabase.channel.urls: adds metric-path, transform-path, collection-path
helpers (additive).
- metabase.metabot.scope: adds agent:{content,query,sql,delivery}:* scopes
and collapses agent:viz:mcp-ui:{query,drill-through} into
agent:viz:mcp-ui:* — a proven superset, backward-compatible with v1's
gating (REPL-verified: old concrete scopes still match). Also regates
agent:metric:* from SQL-generation to NLQ permission (scope_test).
- metabase.mcp.settings: adds mcp-v2-disabled-tools and
mcp-query-handle-ttl-hours (additive; v1-safe; tests included).
- metabase.models.serialization.resolve{,.mp}: adds *numeric-ids-allowed?*
dynvar; resolver miss messages no longer name a specific discovery tool
(the calling surface owns the recovery sentence) — mp_test updated.
- metabase.server.streaming-response: optional :executor option threaded
through do-f-async; default path (shared pool) unchanged. Needed by
long-lived SSE keepalive streams so they can't exhaust the fixed pool.
Review question for this PR: does REST/v1 behavior stay identical?
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjQcjQx7NPGr3Rpf6QaG4b
Review fixes (2026-08-27 deep review of #81244):
- native-marker? requires a truthy :native value (an explicit-null key from a
JSON round-trip is not a marker); docstrings state the guard/agent-api
sequencing explicitly (live v1 path untouched; call sites migrate at retirement).
- check-mcp-ui-native-query! accepts agent:sql:execute (v1's concrete scope)
alongside agent:sql:run, and documents that scope claims arrive with the v2
session rework — until then native-over-UI-credential fails closed.
- mcp-query-handle-ttl-hours getter no longer rethrows on an unparseable env
var (falls back to the default instead of killing the GC task).
- *numeric-ids-allowed?* docstring rewritten in future tense naming its binder
(the v2 query pipeline) and reader (the dialect rework PR).
- viz-scope comment corrected (v1 resources still register those strings until
retirement; wildcard keeps them satisfied); mp.clj database-id tru args
str-wrapped against MessageFormat locale grouping; streaming-response
:executor docstring notes the Future.cancel(true) interruption requirement.
Follow-up (review round 2): the guards are now genuinely shared — agent-api's
private copies are deleted and its execution paths call query-guards directly.
Deliberate tightening: raw-scan native detection replaces normalize-then-inspect
(fail-closed on unparseable payloads). Full agent-api api-test green.
Re-review round: agent:sql:execute compat branch now tested; TTL getter's
catch path tested ("forever"); reject-native-query! no longer echoes the
payload in ex-data (matches the old live rejection shape); viz-scope comment
typo fixed; pure predicate tests ^:parallel; check-token-query-permissions!
docstring states the stage-0-fast-path/QP-backstop contract.
--- Close-out review response (slice 00's first adversarial round) ---
CRITICAL (native-query false-positive on legacy MBQL with a field/expression/tag named `native`)
FIXED. native-query? scanned the whole payload via tree-seq, so a legit query whose :expressions or
:template-tags map had a key named "native" was rejected (400) though it ran fine on master. It now
scans only the structural query-nesting edges (:stages/:joins, :query/:source-query), never
descending into caller-named :expressions/:template-tags; a malformed value off a structural edge is
still deep-scanned to stay fail-closed. Proven: a 17-case REPL matrix — every crafted false-positive
now false, every genuine native (legacy top/nested, MBQL5 stage, joins, string-keyed, malformed)
still true.
SECURITY (v1 MCP bearer authenticates a deactivated user) FIXED. resolve-access-token — the single
resolver shared by the v1 transport and the core session bearer bridge — now gates on
(t2/exists? :model/User :id user-id :is_active true). v1's transport dispatched straight on :user-id
with no active check; v2 was already refuse-only. Proven fails-when-broken: reverting the guard lets
a deactivated user's token resolve; restored, it is refused. v1 mcp.api-test (598 assertions) green.
IMPORTANT fixed: agent:delivery:write now granted by the metabot-other-tools bucket (was dead
config → alert_write would 403 everyone); metabot-nlq grants agent:content:read not :* (was
over-granting write to NLQ-only users); update-pulse-with-perm-checks! validates :cards against
CoercibleToCardRef (a bad string id was a 500, now a 400); check-mcp-ui-native-query!'s docstrings
now state it is dormant until the v2 session-rework slice wires it (was claiming to be live).
MINOR fixed: thrown-status test helper distinguishes no-throw from throw-without-status; kill-switch
check moved after the scope check (a scope-denied caller no longer learns the kill-switch state);
the query-handle-ttl default is one const, not two literals; mp_test gains positive assertions.
Re-verified (fresh JVM, run separately): query-guards + scope + settings + mp 328 assertions;
oauth + v1 mcp.api-test green. The one red in pulse-test is create-csv-xls-test = GDGT-3161, a
pre-existing EE-app-db bug (empty {} dataset_query -> download-perms 500) in the CREATE path (not
this fix's update path), reproducing on plain master — not introduced here. kondo 0/0; cljfmt clean;
modules-config regenerated (oauth-server :model-imports gains :model/User).
--- master parity sync (user-approved Option A) ---
Carries master 8c3ea6f40b (#80554 Remove Explorations dimension anchored blocks) + master search/appdb/index.clj (isolate-write! savepoint isolation). Stack forked before both; without them the dropped exploration_block.type migration is absent -> Liquibase changelog-integrity failure on real app-DBs (App-DB Part2 CI). Synced at base to restore parity. 41 files, all slice-untouched.
* Fix save-access-token! call arity after master merge
Master gave the test helper a 5th arg (client-id via
oauth-server.tu/with-oauth-client); the new
resolve-access-token-deactivated-user-test still called the 4-arg form.
Wrap it in with-oauth-client and pass client-id, matching every other
call site. Verified: full session-oauth-test namespace green locally
(10 tests, 27 assertions, 0 failures).
* Fix native-guard snake_case gap and make the tenant recipient filter work end-to-end
- native-map-edges gains :source_query/"source_query": legacy MBQL
normalization canonicalizes the snake_case spelling too, so a payload
like {"query" {"source_query" {"native" ...}}} reached the query
processor as a native stage while native-query? said false — an
MBQL-only-scoped caller could run raw SQL through /v1/execute,
bypassing the scope split and the execute-sql kill switch. Regression
tests cover both the string and keyword snake forms.
- The cross-tenant recipient filter compared :tenant_id on recipient
maps that the :recipients hydration never populates, so a tenant
caller lost every user recipient and a tenantless caller was filtered
by nil = nil. Tenant membership is now looked up from the app DB
inside the filter, and the filter only engages for a caller who
belongs to a tenant — a tenantless user sees recipients unfiltered,
as before tenants existed. The test now builds real users in real
tenants (production-shaped recipient maps, ids only) instead of
fabricating :tenant_id keys, and pins the tenantless-caller
passthrough.
* Match native-SQL markers case- and separator-insensitively
`native-query?` matched edge names and markers case-exactly, but the QP
normalizer canonicalizes keys case-insensitively and treats `_` and `-`
alike. `{:query {:SOURCE_QUERY {:native "select 1"}}}` therefore scanned
as not-native and then executed as a native stage — REPL-confirmed, along
with `:Source_Query` and an uppercased `:NATIVE` marker.
That is a live bypass rather than a latent one: since
`+refuse-unscoped-native-sql` went live on /api/dataset, this scan is the
only gate between an unrestricted-stamped MCP UI credential and raw SQL.
The same scan backs `reject-native-query!` on the agent-api execution
paths, so those inherit the fix.
All key and value matching now goes through one `token` fn — namespace
kept, lowercased, `_` folded to `-`. Nodes are walked as `[token value]`
pairs rather than by fixed lookup, so a node carrying two spellings of
one edge has both scanned instead of one silently shadowing the other.
* Deep-scan a non-map stage element instead of throwing
`native-query?`'s `scan-map` iterated the node's entries without checking it was a map, so a
junk payload like `{:stages [1]}` threw "Don't know how to create ISeq from Integer" and
`/v2/query` answered 500 where shape validation should have answered 400
(`combined-query-rejects-malformed-payload-test`). A non-map where a stage belongs is now
deep-scanned like every other malformed sub-tree: never throws, and a marker after it still
trips the guard.
* Address round-3 review findings on the extraction slice
- The bearer resolver's is_active gate is defense in depth, not the primary control: deactivating
through the model already revokes the user's tokens. The docstring now says so, and the test
deactivates with a raw core_user UPDATE (the path the gate exists for) and asserts the token was
not revoked, so the gate is what the test exercises.
- Pin that a tenant-scoped caller sees only same-tenant pulse recipients (tenantless users hidden).
- scope.clj comments described a wildcard collapse that is not in this diff; say what the slice
does: adds scopes, removes none.
- Correct the guard test docstring: the /api/dataset gate is wired by the next slice.
- Pin the guard's deep-scan fallbacks and the streaming response's :executor option with tests.
* Widen the recipient merge-back to cover the tenant filter
Addresses review feedback on the tenant recipient filter: a filtered read
that is submitted back as a write silently deleted the recipients the
caller was never shown.
The `:channels` submitted to an update are authoritative --
`update-notification-channels!` deletes any recipient not present -- so
every recipient hidden on the read path has to be restored on the write
path. Master already did this for one read filter (sandboxed and
impersonated users, via `maybe-add-recipients`), but the tenant filter
added in this slice had no corresponding write-path compensation. A
tenant-scoped caller doing GET then PUT of the returned body therefore
dropped every other-tenant and tenantless recipient row.
Rather than add a second parallel mechanism, the tenant visibility rule
is now a single predicate (`recipient-visible-to-tenant-caller?`) used by
both sides: the read path filters with it, and the write path merges back
exactly what it excluded via `hidden-cross-tenant-recipients`.
Two supporting fixes:
- `maybe-add-recipients` now normalizes `:channel_type` the way
`email-channel` does. The old `(= "email" ...)` string comparison only
matched the REST shape, so a direct call with a hydrated pulse (keyword
`:email`) skipped the merge entirely -- which also made the pre-existing
sandbox protection REST-only.
- Merged recipients are de-duplicated by `(some-fn :id :email)`, the same
key `update-pulse-with-perm-checks!` uses to diff recipients, so a
caller matching both filters cannot get a recipient appended twice.
Verified in a REPL: the new endpoint-level round-trip test goes red
without the merge-back (two recipient rows destroyed) and green with it.
It asserts on `pulse_channel_recipient` rows rather than the API response,
since the response is filtered either way and cannot distinguish "hidden"
from "deleted". Pulse, pulse-channel and enterprise sandbox pulse suites
show no new failures (`create-csv-xls-test` fails identically on the
unmodified branch).
---------
Co-authored-by: Eric Normand <ericwnormand@gmail.com>
In https://github.com/metabase/metabase/pull/80957 we trimmed this job
down so it no longer doubles the tests run. But it's still pointless
to have it as a separate job; it should be tested inline with the main
suite.
We still run some individual tests twice using the
`with-and-without-multi-level` macro when we need to cover both the
multi-catalog behavior and the non-multi-catalog behavior, but this is
only a handful of tests now instead of the entire suite or the entire
namespace.
Removal of `when-not (tx/db-test-env-var :databricks :multi-level-schema false)`
causes a lot of indentation changes here, so it's best to review with
whitespace diffs turned off.
* Split the SSO auth settings pages into card sections
* Deflake modal escape close in data-model e2e specs
* Move docs changes to a separate PR
* Finish token-scale translation in the merged css modules
* Deflake dark mode toggle assertions in palette spec
* release JDBC savepoints when nested transactions succeed
* tighten inline comment
* pr feedback
* Release savepoints only when no later scope is still running
* Fix sqlglot parse failure for ?::type placeholder casts (#81373)
With connection impersonation enabled, native queries are parsed by
sqlglot after template-variable substitution, when {{var}} has already
become a JDBC `?` placeholder. sqlglot's base tokenizer treats `?::` as
a single token (DuckDB's try-cast operator) in every dialect, and only
DuckDB's parser can consume it, so `{{var}}::date` -- a very common
pattern on Postgres/Redshift/Snowflake -- failed with "Unable to parse
native query".
Fix: before parsing in is_single_stmt_of_type, rewrite `?::` to `? ::`
(which sqlglot parses fine, emitting CAST(? AS type)). Occurrences are
located via the tokenizer's own QDCOLON tokens, so `?::` inside string
literals, dollar-quoted strings, and comments is left untouched, and
placeholder order is preserved in the re-emitted SQL. Multi-statement
rejection and the rest of the impersonation validation are unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XkBejATyUcaq2R74xFd5M1
* comments and tests
* Assert all placeholders survive; fix QDCOLON dialect guard (databricks, not duckdb)
Review feedback: the placeholder-cast test now asserts that every ? in the
input survives into the re-emitted SQL, not just that one does.
While adding duckdb coverage, it turned out the dialect guard in
_split_placeholder_casts named the wrong dialect: in sqlglot 28.6.0 the
?:: (QDCOLON) token is emitted by the *base* tokenizer but only
Databricks' parser consumes it (the expr?::type try-cast column
operator); duckdb's parser does not, so duckdb was still failing on
placeholder casts. The guard now skips databricks instead, duckdb gets
the same ?:: -> ? :: rewrite as everyone else, and tests cover both:
duckdb placeholder casts parse with placeholders preserved, and
databricks' native try-cast is left intact (re-emitted as TRY_CAST).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* clean up
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Make ratchet shrinking explicit, and check it in one place
The ratchet test fixture ran `fix!` on every non-CI run, so `project-tests
ratchets` rewrote .clj-kondo/ratchets.edn in your working tree before
asserting against it. A check that edits what it checks leaves a diff nobody
asked for, in the file every PR conflicts over, and its only reachable
failure was a bug in the fixer that had just run.
Removing the auto-fix forces the dev/CI split to go too: a strict local test
that no longer tightens would fail on every branch that removes an ignore.
So over-budget is now the rule everywhere, and shrinking is only
`./bin/mage kondo-ratchets-shrink`, which the shrink workflow runs on master.
That left two implementations of nearly the same check. The babashka
`check-report` gains the justification finding — the one assertion the JVM
suite had that it lacked — so `./bin/mage kondo-ratchets` covers over-budget,
config over-budget, unjustified ignores, normalization and unknown linters on
its own. CI runs it instead of booting a JVM, and the five now-duplicated
in-repo assertions leave the test namespace, which drops from 6.2s to 0.8s
because it no longer walks the source tree.
Task names follow: check-kondo-ratchets (which nothing called) becomes
kondo-ratchets, fix-kondo-ratchets becomes kondo-ratchets-shrink, and the
merge task becomes private -kondo-ratchets-merge, since only the git merge
driver invokes it. bin/merge-kondo-ratchets keeps its name; installed merge
drivers point at it.
* Polish kondo ratchet guidance
* Prime the ratchet shrink workflow
Intentionally raise the :case-symbol-test budget from 1 to 2 without adding a suppression. The read-only check allows the unused budget; after this PR merges, the shrink workflow should restore it to 1 through a Tighten ratchets automation PR.
* Make the ratchet suite check the project
Keep the ratchet tooling unit tests in the backend suite, but make project-tests ratchets run the read-only repository check. Include it in the bare project-tests command so local project-level checks still validate the checked-in ratchet policy.
* List project-test suites once
* Address review findings on the ratchet check
check-report threw NPE on a ratchets map without :comment-exempt, while
tolerating a missing :ignore-counts or :config-counts. Default it, so the
three policy keys behave alike.
The run-project-ratchet-checks description and the step comment both still
described the old wiring. The check now runs alongside the backend checks,
which no longer include it, and "no JVM" is not quite true: resolving
clj-kondo on a cold cache shells out to the JDK prepare-backend installs.
* Warn about stale comment exemptions
The only check that a :comment-exempt entry is still needed was a local-only
test, which went vacuous when the dev and CI semantics were unified, so
nothing named a stale exemption any more. The check now reports one the way
it reports an :unlimited policy nobody uses: informational, never a failure,
since only the shrink workflow removes them.
* Say why the merge driver finds mage through its own path
The script runs against whatever repository holds the conflict, which is not
always the one it lives in -- the tests merge stages in a throwaway repo. A
relative bin/mage looks plausible and fails there, so record the reason.
* Stop wrapping two comments that did not need it
One fits on a single line once it says less. The other breaks at its sentence
boundary rather than mid-clause.
* Break the ratchets header on its sentences
Four sentences started mid-line and wrapped mid-clause, one of them trailing
"in the PR." onto a line of its own. Same words, one sentence per line.
* Keep comment exemptions until someone removes them
fix! dropped a :comment-exempt entry as soon as every ignore of its linter
carried a comment, or the last uncommented one was deleted -- so an exemption
could disappear as a side effect of unrelated work, and getting it back is a
change to defend in review.
An exemption records a decision about a linter, not a count, which is the
reason :unlimited policies are already kept at zero and only warned about.
Treat the two alike: the check and the shrinker name a stale exemption, and a
person deletes it.
* GDGT-2486 Add elevation, radius, and spacing scale variables
* GDGT-2486 Remap scale tokens in files merged from master
* Address review
Review fixes for the GDGT-2486 scale migration:
- Revert double-shifted tokens from the merge-remap pass: it re-applied
the old-to-new mapping to 22 files already remapped by the first pass,
shifting spacing values one extra step (e.g. xl 24px -> xxl 32px).
- Fix codemod escapes where old key names kept their name but changed px:
Modal default padding (lg->xl, 24px), MultiSelect radius (sm->xs, 6px),
MetricSearchInput radius-sm, ThemeEditor PreviewPanel radius-md,
ColumnInput theme.spacing.xs->xxs, ChartTypeSettings Space h props,
TableRowForCacheableItem icon placeholder w (md->lg, 16px),
ResizeHandle/ResizableBoxHandle grip h (xs->xxs, 4px).
- Pin Group/Stack default gap to "lg" (16px): Mantine's default is
"md", which is 12px in the new scale (was 16px in the old scale).
- Allowlist the runtime-defined --mantine-spacing-xxs/-xxl and
--mantine-radius-xxs CSS variables in find-never-defined-css-variables
(fixes fe-lint CI).
- Add theme.unit.spec.ts covering the spacing/radius/shadow scales,
dark-mode shadow set, whitelabel independence, and a guard asserting
every scale-based component default resolves to a defined key.
- Derive Card/Paper story argTypes options from the real scale.
- Augment MantineThemeSizesOverride so the custom scale tokens appear in
editor autocomplete for p/m/gap/radius/shadow props.
- Update the emotion-migrate and metabase-ui-component-from-figma skill
token tables to the new scale (spacing/radius values, shadow tokens,
snap tables).
* Update Loki Snapshots
* Address review: fix h/w style props missed by the scale codemod
The codemod did not remap height/width style props, so every bare h=/w=
scale-key value silently changed px with the new scale. Remap each to the
px-preserving key (xs 4px->xxs, md 16px->lg, lg 24px->xl, xl 32px->xxl):
- PaneHeader breadcrumb row h xl->xxl (32px, restores data studio
pane header height)
- BrowseDatabases logo boxes h/w xl->xxl (32px)
- DatabaseConnectionStringField rows h lg->xl (24px)
- DatabaseEngineList logo Center h/w lg->xl (24px)
- Metabot chat chevron flexes h/w md->lg (16px)
- MetricsFilterPill h lg->xl (24px)
- DatasetNotebook divider h xs->xxs (4px)
- DataReference NodeList icon w md->lg (16px)
- transforms-python ResizableBoxHandle grip h xs->xxs (4px)
Audited: no remaining bare h/w/maw/mah/miw/mih old-key props whose px
changed (verified line-identical against master for every occurrence).
* Address review: fix bdrs and responsive-object size tokens missed by the codemod
Two more escape classes from the scale codemod, found by a full sweep of
every touched-token form (string style props, responsive objects, CSS
vars, theme accessors) with each line compared against master:
- bdrs style prop (border-radius): remapped px-preservingly at 69 sites
in 58 files (xs 4px->xxs, sm 6px->xs, md 8px->sm). The codemod remapped
radius= but not its style-prop alias bdrs.
- Responsive object values (e.g. p={{ base: "md", sm: "xl" }}): remapped
the token VALUES at 9 sites in 6 files (base/sm/md breakpoint keys are
untouched; only their spacing values shifted: md 16->lg, xl 32->xxl,
xs 4->xxs).
Sweep result after fixes: zero remaining line-identical old-key token
references to spacing xs/md/lg/xl, radius xs/sm/md, or shadow xs/md in
any form. Intentional survivors unchanged: radius/shadow/bdrs "lg"
(disclosed broken-key-now-16px class), spacing/radius sm where the
px value did not change.
Verified: tsc, eslint (changed files), metabase/ui jest suite.
* Address review: fix remaining token escape forms (object props, braces props, style objects)
Final codemod escape classes from the exhaustive token sweep, all
remapped px-preservingly:
- Object-literal / default-parameter prop values (15 sites / 8 files):
ItemList picker-item default containerProps pb xs->xxs (4px, the
reported gap), FormFooter pt lg->xl, SlackSetup pt lg->xl,
EmbeddingSecretKeyWidget mb xs->xxs, CreateDashboardForm labelProps
mb xs->xxs x2, databases styles mb lg->xl, NotebookDataPicker
menuDropdownProps mt xl->xxl, Banner story py md->lg
- Braces-wrapped string props: CloudSMTPConnectionCard p lg->xl (24px)
and gap md->lg (16px), SettingsLdapForm mt xs->xxs,
DashboardLeaveConfirmationModal mb lg->xl
- style-object CSS properties: DatasetNotebook borderRadius xs->xxs
- Ternary token values inside responsive objects: SummarizeStep gap
md->lg (16px)
Sweep now returns zero real misses across every checked form: string
style props (incl. h/w/bdrs), braces props, responsive objects with
ternaries, object-literal/default-param props, style-object CSS props,
CSS var() references, theme.* accessors. Remaining grep hits are
untouched scales (lh, fz, size, breakpoints) or px-identical sm values.
* Address review: preserve Mantine spacing defaults exposed by Loki
* Update Loki Snapshots
* Address review: fix remaining scale regressions and visual guards
* Address review: make Loki update capture new references
* Update Loki Snapshots
* Address review: keep only targeted Loki references
* Address review: assign ticket component elevations
* Update Loki Snapshots
* Address review: tighten MonthPicker header spacing
* Address review: refine elevation visual stories
* Update Loki Snapshots
* Address review: keep renamed Menu Loki story selected
* Update Loki Snapshots
* Address review: preserve inherited Mantine defaults
* Address review: allow stock Mantine shadow tokens
* Address review: replace token audit test with ESLint rule
* Address review: drop unrelated whitespace-only dev/src changes
* Address review: use rem() in radius scale and rem docs
* Update Loki Snapshots
* GDGT-2486: Address PR comments
* GDGT-2486: Adjust negative tokens
* GDGT-2486: Address review findings
* Update Loki Snapshots
---------
Co-authored-by: Metabase Automation <github-automation@metabase.com>
* Move the entities store into the metadata module and hide normalizr
* Record the moved schema file's import-time effect
* Describe what the metadata reducers do, not what they replaced
* Move the mirror's fixtures and slice-name helpers into the module (#81297)
* Move the mirror's fixtures and slice-name helpers into the module
* Build the QuestionSources fixture through the metadata store barrel
* Document why the metadata store is not a deletable mirror slice
* Trim the barrel and write-door comments
* only derive site-url from authenticated superuser requests
`maybe-set-site-url*` (global Ring middleware) derived `site-url` from request
headers whenever `(or (not (setup/has-user-setup)) superuser?)`. The left arm
needed no authenticated caller, so an unauthenticated request reaching the
instance before its first user was created could pin `site-url` from a forged
Host/Origin/X-Forwarded-Host header. Because the write is first-writer-wins and
setup never overwrites it, that attacker-chosen value survived setup and
poisoned every later auth/email link (notably password-reset URLs) for the life
of the instance.
Gate derivation on `superuser?` alone. `:is-superuser?` is already on the request
by the time this middleware runs (wrap-current-user-info is inner/earlier in the
onion), so an admin's own request still auto-derives `site-url` -- which happens
on the operator's next request after an interactive setup. An unauthenticated
pre-setup request now writes nothing; `site-url` simply stays unset (nil is
handled everywhere it's consumed, and a reset link built on nil is relative,
never attacker-controlled).
Deployments that provision headlessly, run multi-tenant, or never sign in as an
admin will otherwise sit at a nil site-url, so note in the setting's docs that
they should set MB_SITE_URL explicitly.
* Read Quartz job-data blobs through a class allow-list
Quartz persists job data, calendars, and BLOB triggers as serialized objects.
Metabase's job data is only ever EDN-shaped data (keywords, strings, numbers,
maps/sets/vectors) wrapped in Quartz's own JobDataMap/trigger classes, so when we
read it back we constrain deserialization to exactly those classes and decline
anything else.
Install a per-DB DriverDelegate (StdJDBCDelegate for H2/MySQL, PostgreSQLDelegate
for Postgres) that overrides getObjectFromBlob to read through a class allow-list
built from the plain-data classes job data consists of. This is scoped to
Quartz's own blob reads, so nothing else in the JVM is affected.
- metabase.util.deserialization-allowlist: a reusable, composable ObjectInputFilter
allow-list. clojure-data-prefixes covers the EDN value space; callers add their
own wrapper prefixes and read via read-object / read-object-bytes.
- metabase.task.secure-delegate: the allow-set (EDN + org.quartz.*), the blob-read
bodies, and install!, wired into task.bootstrap. The gen-class delegates are
AOT-compiled in the jar; from source they're compiled on demand so the class is
loadable by name through the classloader Quartz uses. That runtime compile is
fenced to source only (config/jar?): from a jar a missing class is a build error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjQcjQx7NPGr3Rpf6QaG4b
* Don't deserialize Java objects from H2 query results
Metabase reads scalar and collection values out of query results, never
serialized Java objects. Register a JavaObjectSerializer on the H2 driver so H2's
JAVA_OBJECT (de)serialization is declined rather than reconstructing arbitrary
classes from stored bytes. With that in place the ad hoc JAVA_OBJECT check in
read-column-thunk is redundant, so it's removed; read-column-thunk keeps its CLOB
handling. Ordinary values are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjQcjQx7NPGr3Rpf6QaG4b
* Strip internal keys from binning maps
Add :decode/api and :encode/serialize remove-internal-keys hooks to the
::binning schema so clients can't smuggle internal query-processor keys in
through a :binning map.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Strip internal keys from more client-submitted query maps
Extend the :decode/api / :encode/serialize remove-internal-keys hooks to the
::parameter, ::template-tag, ::constraints, ::middleware-options, and ::page
schemas so clients can't smuggle internal query-processor keys in through them.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Reformat strip-internal-keys-test doseq bindings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test improvements and fix reflection warnings
* Validate HoneySQL idents at the app-DB compile step
Honey SQL renders an ident as bare, unquoted SQL in several positions --
a form head it doesn't recognize, an ORDER BY direction, a DDL modifier,
the function name of a `:%fn.arg` var -- so an ident reaching any of them
is raw SQL text. Hold every ident to a plain-identifier shape, with an
explicit allow-list for the keywords Honey SQL renders as fixed SQL of its
own (operators, DDL modifiers, column types, ORDER BY directions).
Also close two gaps found along the way: symbols walked past the `:raw` and
`:inline` gates, since Honey SQL resolves symbol heads and clause keys the
same as keywords; and `safe-value?` trusted any value shape it didn't
recognize, where it now enumerates the types bound as `?` parameters.
Query idents that were kebab-case are renamed in the SQL only -- the
Clojure keys they feed, including the `:metabase-user-id` request key, are
mapped back at the read site and unchanged for their consumers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Allow the Postgres operators our drivers register globally
`metabase.driver.postgres` registers the PG JSON, array and regex operators
with `honey.sql/register-op!`, which makes them global -- and the app DB can
be Postgres, so app-DB queries reach for them too: the semantic-search
migration compiles a `[:->> ...]`. Add them, and a test that holds the
allow-list to every non-namespaced operator Honey SQL knows, so an upgrade
or a new registration can't silently leave one out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Snake_case the remaining app-DB query idents
Four more sites the sweeps turned up, all renamed in the SQL only:
- `request/session.clj` selects the current user under the same kebab
aliases the session middleware did, so it gets the same rename-at-the-read
-site treatment.
- Search scorer names double as SQL aliases, and three of them are
kebab-case. They also key the weights in `search.config` and name the
scores the API returns, so the conversion happens at the one seam that
writes and reads the alias, leaving the scorer name alone. The alias gains
a `_score` suffix because `:view-count` and `:data-layer` are named after
the very columns they read.
- The model alias registry in `api/common.clj` feeds
`column-with-model-alias`, which assembles `alias.column` idents, so its
six kebab aliases are snake_cased along with the literal uses in the
in-place search engine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Constrain embedded card parameter values by the locked parameters
A dashboard's parameter value lookups receive the values of the other
parameters as chain-filter constraints, so the values offered for an
enabled parameter are limited to the rows the locked ones select. The card
lookups had no constraint argument to receive them: `card-param-values`
bound the JWT's params, used them only to check the searched parameter was
enabled and not pinned, then resolved the parameter to a Field and returned
that Field's whole value list.
Give the card lookups the same constraint argument and pass the JWT's
locked values from the embed layer. The argument is optional, so the
authenticated and public routes keep their current behaviour.
`param-type->op` and `param-type->default-options` move from
`parameters.dashboard` to `parameters.params`, so both the dashboard and
card constraint builders can reach them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Snake_case the app-DB query idents the guard rejects
CI caught `queries.models.query`, and a grep for the same shape found four
more sites the earlier sweeps had missed -- they were confined to the
app_db, collections, search and permissions modules. The aliases move to
snake_case in the SQL; where the keys they feed are confined to their own
namespace the readers move with them, and where they have to match a shape
from elsewhere they are renamed back at the read site:
- `queries.models.query` aliases so the app-DB branch matches the keys
`lib.metadata/card` returns, so it renames rather than propagates.
- `cache.task.refresh-cache-configs` feeds its aliases through a nested
subquery whose outer `:select` names them, so both levels move, along
with the readers and the test's expectations.
Also widen `safe-parameter?`: a `query_hash` read back from H2 arrives as a
`java.sql.Blob` rather than a byte array, and the enumeration rejected it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Remove a scratch probe test
Committed by accident in the previous commit -- a throwaway namespace with
printlns, used while checking chain-filter behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Fix the locked-parameter constraints for previews and stale slugs
Three problems with the constraints the card parameter lookups build:
The preview routes passed the sanitized Card, whose `:parameters` has the
locked ones removed, so the constraints were always empty and a preview
offered wider value lists than the embed it previews. They now pass the
stored Card and take only `:embedding_params` from the token, which is what
the published routes already do.
`embedding_params` is keyed by slug and `:parameters` by id, and nothing
keeps them in sync; a locked slug naming no parameter produced no
constraint and widened the values rather than narrowing them. It is now
refused. `param-values-merged-params` refuses the same on dashboards, where
the slug otherwise became a nil key that the constraint builder dropped.
A locked value of `[]` reached the filter builder as a value rather than as
"no value", producing a `:=` clause with one argument.
The constraints also carry the parameter's own `:options` now, as the
dashboard builder does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Allow a qualified argument in a :%fn.arg ident
`:%lower.metabase_field/name` renders `LOWER("metabase_field"."name")` --
Honey SQL splits the ident at the first `.` and formats what follows as
quoted entities, so a `/` there separates the parts of a qualified column
rather than landing in the SQL. Only the function name, which is rendered
unquoted, has to be a bare identifier.
The pattern rejected `/` anywhere after the `%`, which took the database
autocomplete endpoint with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Snake_case more app-DB query idents, and allow the FOR UPDATE modifiers
Found by widening the search: the earlier grep looked for the alias and its
`:select` on one line, so it missed every alias sitting on a continuation
line -- which is most of them.
- `activity_feed`: the recent-views queries alias `database-name`,
`initial-sync-status`, `entity-coll-id` and `moderated-status`. The keys
are read only within their own namespace, which builds the API response
from them under different names, so the readers move too.
- `xrays.automagic-dashboards`: `num-fields` and `list-like?` -- the latter
loses its `?`, which no snake_case spelling keeps.
- `:skip-locked`, `:nowait` and `:no-wait` are lock modifiers Honey SQL
matches by name in a `:for` clause, not idents it renders, so they join
the allow-list rather than being rewritten.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Move the query-analyzer's readers to the renamed table id
`query-analyzer/tables-for-native` aliases its table id, and that alias was
snake_cased without moving one of its two readers: `used-tables` still asked
for `:table-id` and so silently collected nothing. Unlike the write side,
which the guard rejects outright, a reader left behind reads nil.
The `tables-for-native` redefs in the tests carried the old shape too, so
they asserted against a result the function no longer produces.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Revert the HoneySQL ident guard and the renames it required
The guard held every ident in an app-DB query to a plain-identifier shape,
which turned out to have a long tail: each release of it found more query
aliases to rename, and a rename that missed a reader failed silently rather
than loudly, since the reader simply got nil.
This reverts the guard along with every alias rename made to satisfy it,
leaving the parameter-value security fixes, which do not depend on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Harden site-url derivation fix: fail-closed redirect check, discerning test
Follow-ups to gating site-url auto-derivation on authenticated superuser
requests.
- check-sso-redirect: fail closed when site-url is nil. Previously a nil
site-url left our-host nil, so a redirect with a nil host but a non-nil
scheme was non-relative yet matched (= nil nil) and passed -- an open
redirect. Require a known host to match against: (and
our-host (= (.getHost redirect) our-host)).
- misc-test: bind has-user-setup false in the non-superuser negative case.
With has-user-setup true (the test-DB default) the old gate
(or (not has-user-setup) superuser?) was already false for that request,
so the test passed against the vulnerable code and distinguished nothing.
- misc: rewrite the SETTING SITE-URL block comment and the maybe-set-site-url
docstring, which still described the removed behavior (site-url set on the
initial setup request).
- system settings: drop the blank line in the site-url :doc (it made the new
paragraph render as an indented code block in the generated env-var docs)
and replace an em-dash with a comma.
* Read a card's parameters the way the rest of the code does
`card-param-constraints` read `:parameters` alone, but a native card can be
described entirely by its template tags and carry none -- which is how the
static embedding e2e fixture builds one. The locked slug then matched no
parameter and the lookup was refused. Use the same either/or source
`get-param-or-throw` uses a few lines above.
A parameter with no value constrains nothing whether or not it still
exists, so the existence check now runs after the empty-value guard rather
than before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rQMWmi2o1WGSqK5DDZ99u
* Fix jar startup crash when the jar's directory contains a space
find-in-current-jar built a jar: URL by string-concatenating
get-jar-path, but get-jar-path returns the *decoded* filesystem path
(URL -> URI -> .getPath turns %20 back into a literal space). URL.toURI
then throws URISyntaxException on the raw space, killing startup while
loading bundled driver manifests.
Open the jar via the Path-based nio-fs helper instead — Paths don't
round-trip through URI encoding, so spaces are fine. This also stops
closing a filesystem out from under other consumers: the old code, on
FileSystemAlreadyExistsException, grabbed the provider's *cached*
filesystem and closed it in with-open; the Path-based overload creates
an independent uncached instance whose close affects only itself.
Verified against a jar in a spaced directory: the manifest URI is found
and slurpable, and a concurrently-open URI-cached filesystem for the
same jar stays open and registered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Fpja4n6bzdCy4QufM49Hb
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Alexander Polyankin <alexander.polyankin@metabase.com>
* Don't crash the modules require hook on dynamic requires
(requiring-resolve (:failing-test-var opts)) has no statically-known
namespace; unwrap-require now returns nil for such forms instead of
falling through, and both callers skip non-symbols. dev.clj has carried
a live :hook crash from this since the hook was added.
* Add a hook regression test for dynamic requiring-resolve
Covers the crash fix: dynamic arguments lint to nothing, static
cross-module violations still report. hooks.* test namespaces join the
outside-the-module-system prefix list.
* Fail closed on dynamic module requires
* Polish module require hook tests
* Assert module hook findings directly
* Document intentional dynamic module requires
* Update module linter ratchet
* Add a manual kondo ratchet conflict merger
* Split kondo ratchet merge tests
* Document the ratchet conflict merger
* Simplify unlimited ratchet merge policy
* Simplify ratchet merger for final policy schema
* Resolve ratchet conflicts to the stricter policy
Concurrent policy changes now always resolve: the smaller budget wins, a
finite budget beats :unlimited, and a removed entry beats any budget.
Comment exemptions merge on their own. Every stage is read through
read-ratchets and shape-checked before a disabled stage decides the result.
bin/merge-kondo-ratchets reads the index stages by number, so a file
deleted on one side and changed on the other exits nonzero with nothing
staged, and both-sides-add merges against an empty base. It runs from any
directory in the repository and stages only .clj-kondo/ratchets.edn.
mage.merge-kondo-ratchets-test drives the script against real conflicts
in temporary repositories.
* Point the clojure-write skill at the ratchet merge resolver
* Reject malformed stages before merging ratchets
The shape checks move out of read-ratchets into validate-policies, and merge-ratchets runs them on every stage, so a stage with :config-counts [] or :comment-exempt [] is an error rather than an empty set of policies that the merge would read as removals.
* Cover unreadable stages in the merger tests
An empty file, two forms, or a non-map stage now fails the reader instead of merging as no policies; the script leaves such a conflict for a human and stages nothing.
* Reject a non-map ratchet stage before merging
A nil stage passed every field check because dissoc, keys, and the defaults all accept nil, so a malformed stage could merge as an empty set of policies.
* Reject non-keyword linter names
The merge keys on (str linter) and renders the same way, so a string, nil or
symbol name was rewritten as a keyword, collided with one, or produced a file
that no longer read back -- and the script still exited 0 saying it had staged
the merge. Also describe the two sides by git stage: "current" and "incoming"
are inverted during a rebase.
* Run each refused merge once in the tests
leaves-unresolved? and output each ran the script, so every refusal ran it twice
and the "changed nothing" assertion covered a different run from the one whose
message was checked. Read the merged policies with the EDN reader while here.
* Compare the conflicted file, not just its status
A conflicted path reports UU, DU or UD however its contents are rewritten, so
comparing git status alone did not show that a refused run left the file alone.
* Tighten merge-tool docstrings and the ratchet conflict docs
* Compare the index stages of a refused merge too
Porcelain status reports a conflicted path as DU/UU whatever its stages hold, so it alone does not show
that a refused run left the conflict alone. The stages carry each side's blob.
* Spell out the unmerged status codes
UU/DU/UD as bare shorthand assumes the reader knows git's porcelain codes for a conflicted path.
* Clarify the unmerged-state snapshot comment
* Add trusted master ratchet shrinker
* Retry ratchet shrink after concurrent master pushes
* Test the check-kondo-ratchets report
* Apply ratchet policy to the check report
* Add severity-based inline ratchet policies
* Update ratchet policy documentation
* Finalize ratchet policy enforcement
* Move seeded linters to bounded ratchets
* Simplify unlimited ratchet policies
* Finish ratchet CI integration
* Document unlimited ratchet policy values
* Keep ignore counts as the policy map
* Validate ratchet policy keys against known linters
Policy keys in .clj-kondo/ratchets.edn must name a clj-kondo built-in from the
pinned version, a linter configured under .clj-kondo/, or an allowlisted
external diagnostic. The check and the fixer fail with one error listing every
unknown name instead of counting it. Config budgets must be non-negative
integers.
Drop :unresolved-require, a linter that never existed, from the semantic-search
ignore and the ratchets file.
* Keep unlimited ratchet policies with no ignores left
An :unlimited entry in :ignore-counts records a decision about the linter, so
the fixer no longer drops it when its count reaches zero. The check and the
fixer print one sorted warning naming such entries; it never fails CI. Bounded
zero budgets are still dropped, and :comment-exempt is unchanged.
* Deliver ratchet shrinking through an automation PR
* Validate seeds and read only tracked kondo configs
The known-linter set now comes from git-tracked `.edn` files under
`.clj-kondo`, so the dependency configs `mage kondo` copies in locally
(gitignored, absent from a clean checkout) no longer make validation
depend on the machine. `--seed` is checked against the same set before
anything is written, and a hand-written bounded `0` with no ignores
left is reported when the fixer drops it.
* Reopen the ratchet automation PR after a close
The "already carries this result" skip now requires an open PR, so a
branch left behind by a PR closed without merging is overwritten and
gets a fresh PR instead of silencing every later run. The compare step
reads PR state with the default token under `pull-requests: read`.
* Let the ratchet automation PR approve and merge itself
Copies the backport recipe: github-actions[bot] approves the PR with the default token (a user cannot approve their own PR, and the automation user owns it), then the automation user enables squash auto-merge. The commit identity is the docs generator's, which merges with that one approval. A ratchets-only change no longer flips backend_all, so the PR waits on the Project tests job alone.
* Describe the self-merging ratchet automation PR
* Expect the ratchet check namespace in the project-test suites
* Read the ratchets file as exactly one map
An empty file, a non-map, or a trailing second form used to read as an empty policy set, which the merger would have staged as the removal of every budget. Tracked kondo configs missing from the worktree (unstaged deletions, sparse checkouts) are skipped instead of failing the scan.
* Skip the uberjar for a ratchets-only change
* Keep fork PRs away from the ratchet automation steps
The head-branch lookup matched any repository's branch of that name, so a fork PR could have reached the approval and auto-merge steps; only an open PR from this repository counts now. Approval and auto-merge key on the open PR rather than on whether the branch was pushed, so a run that failed after opening the PR is repaired by the next one.
* Look up the ratchet automation PR by owner-qualified head
gh pr list caps at 30 results and filtered forks client-side, so enough fork PRs on the same branch name could hide the real automation PR and make the create step fail. The REST head=owner:branch filter is applied server-side and matches only this repository's branch.
* Stop checkout credentials from overriding the automation push token
actions/checkout persists GITHUB_TOKEN as an Authorization header, which takes precedence over the token embedded in the push URL, so the force-push to the automation branch would be rejected.
* Tolerate config budget and exemption reductions in CI
CI already let inline counts fall below their budgets so the shrinker could record them after merge, but a lowered config budget or a stale comment exemption still needed an exact match, so two PRs that each removed a suppression could fail the second merge. Config counts now fail CI only when they grow, stale exemptions never fail CI, and check-kondo-ratchets reports config growth alongside inline growth. Local runs stay exact.
* Say what CI rejects without listing the cases
* Keep actionable ratchets bounded
* Exclude the ratchets file with one pattern
Path filters are OR-ed, so a separate "!.clj-kondo/ratchets.edn" entry matched
every path that was not that file: backend_all became true for every PR, and
no_source_code_changes could never be true. The exclusion has to live inside the
pattern, as modules/drivers does above.
* Arm auto-merge against a known commit
The approve and merge steps keyed on the PR number alone, so nothing had checked
what the fixed automation branch name currently holds. Both paths now record the
sha they pushed or compared, and auto-merge matches it. Also guard the job to
this repository, as the workflow it replaces did.
* Report ratchet validation failures without a stack dump
check read and validated the file, then read it again. Now it parses once and
catches inside, so read-ratchets, validate-linters! and scan all report the same
one-line message. Also: CI=false was truthy in the tests, seeding a recorded
linter with no ignores said "nothing to seed" while dropping its policy, and
lowered-counts rebuilt the seeded set per entry.
* Keep the drift message off stdout
The drifted path prints its findings and then exits; folding it into fail! added
a summary line that check-reports-empty-unlimited-test pins. Split the throw from
the print so only messages the run has not already shown are printed.
* Tighten ratchet docstrings and the generated header
* Guard against standalone negations in the path filters
The patterns in a filter are OR-ed, so a "!..." entry matches every path the other entries don't
instead of subtracting from them, and the filter is then always on. The ratchets exclusion is written
inside one pattern for that reason; this keeps the next one from being written the other way.
* Reuse the automation PR on its ratchet file alone
The extra check that master had not touched ratchets.edn since the branch was cut only bites when master
moved to a different value that shrinks back to the same result, since any change to the result is already
caught by comparing the file. Dropping it means only a shallow fetch of the branch tip is needed.
* Force-push the automation PR when it conflicts
A PR whose ratchet file already matches is otherwise left alone, and a conflict is the one thing that
state cannot clear on its own. Mergeability is computed lazily, so UNKNOWN is polled past and, if it
never resolves, left for the next run.
* Ask once whether the automation PR conflicts
Polling for a definitive answer bought nothing: the next push to master runs this again within the hour,
so a mergeability check still in flight can read as not conflicted and be settled then.
* Say what the reuse comparison is against
git diff FETCH_HEAD -- <path> reads as a commit-to-commit diff. It is against the working tree, which is
the only reason comparing the branch tip answers whether the PR still delivers the right file.
* Fix the circular Filter type alias
Record<string, Filter> is expanded eagerly, so naming it inside Filter's own definition trips TS2456. An
inline index signature is deferred and holds the same shape.
* Prove a ratchets-only change skips the backend filters
* Run the path-filter specs when the filters change
* Use gh CLI to approve ratchet PRs
* Reuse toucan2's current connection in Quartz to prevent deadlocks
When code inside a with-transaction or with-connection block triggers a
Quartz operation, Quartz would check out a new connection from the pool.
If the pool is saturated, this deadlocks the thread. Now the
ConnectionProvider checks for a bound toucan2 *current-connectable* and
reuses it via a non-closeable proxy that suppresses close/commit/rollback/
setAutoCommit, preventing interference with the outer transaction.
Closes UXW-307
* Address review comments
verify that asking for a connection while one is already bound via
t2.conn/*current-connectable* reuses the identical underlying connection
instead of checking out a new one. Plus fix an `if` that should be a
`when`.
* Revert Quartz connection-reuse proxy
Revert the non-closeable connection proxy that let Quartz reuse the
thread's bound toucan2 connection with close/commit/rollback/setAutoCommit
suppressed. Suppressing those methods breaks Quartz's data integrity:
JobStoreTX only releases its SELECT ... FOR UPDATE row locks on QRTZ_LOCKS
via commit/rollback, so the proxy either held the cluster lock for the
length of the outer transaction or, for autoCommit connections, defeated
the cluster mutex entirely, and made partial job-store writes
un-rollbackable.
The deadlock this was fixing (UXW-307) is addressed in the next commit by
giving Quartz its own connection pool instead.
* Give Quartz a dedicated app-db connection pool to prevent deadlocks
When code inside a with-transaction or with-connection block triggers a
Quartz operation, Quartz checks out a connection from the app-db pool. If
the pool is saturated, this deadlocks the thread (UXW-307). Fix it at the
root: Quartz now draws from its own small c3p0 pool, so job-store
operations never compete with application code for main-pool connections,
and Quartz keeps full control of its connections' transaction lifecycle.
- ApplicationDB now carries a quartz-data-source: a separate pool (max 5,
min 1, configurable via MB_QUARTZ_MAX_CONNECTION_POOL_SIZE) when
create-pool? is true, the raw data source otherwise. It inherits all the
main pool's properties except sizing.
- mdb.connection/quartz-data-source exposes it behind the ApplicationDB
read-write lock, re-exported via metabase.app-db.core.
- The Quartz ConnectionProvider draws from it; the tracing
connection-interceptor hook is unchanged.
- Tests cover provider behavior (no reuse of the bound connection, not even
wrapped), pool construction/sizing, lock-respecting acquisition, and a
deterministic pool-of-1 deadlock regression test.
* Reject already-pooled data-sources for the dedicated Quartz pool
connection-pool-data-source returns an already-pooled data-source
as-is, so building the Quartz pool from one would silently hand Quartz
the main pool itself -- reintroducing the starvation deadlock the
dedicated pool exists to prevent. No current caller does this (the only
production :create-pool? true site uses the always-unpooled env-derived
data-source), but nothing stopped a future caller from doing it.
Fail fast instead: quartz-connection-pool-data-source now throws on
pooled input, and application-db documents that :create-pool? true
requires an unpooled data-source.
---------
Co-authored-by: John Swanson <john.swanson@metabase.com>
Refresh the settings cache before reading LLM provider connections
Provider connections all live in the one llm-providers setting, and settings
are cached in process, so an instance only learns about another instance's
edit when its cache next polls — once a minute.
Until then it answers a page load with a list that predates the edit, and it
decides whether an edit is allowed against that same list: a connection
another instance has just deleted still looks present, and the type it
belonged to still reports itself as connected, so a legitimate create is
refused.
Force the staleness check on the five endpoints that read the list. It costs
the one query that reads settings-last-updated, unless something has actually
changed.
This does not make the read-modify-write of the list atomic: two instances
editing different connections at the same moment still lose one of the two
edits. That needs a lock, and is left for its own change.
With a driver bound, [:inline "!"] compiles through driver-specific
inline-value; MySQL emits the hex literal _utf8mb4 X'21', whose implicit
collation (utf8mb4's default, utf8mb4_0900_ai_ci) clashes with the
connection collation when MySQL aggregates collations for LIKE, failing
with 'Illegal mix of collations'. ::h2x/literal compiles through its own
registered formatter to a plain '!' on every dialect, which takes the
connection collation like the pattern parameter next to it.
* Don't lose the last change to a setting when a save fails
* Drop a duplicated test title
* Drop the no-op catch on the write chain
* Replace the write queue with a pessimistic disabled state
* Extend the failed-save revert to select and radio inputs
* Enforce single-statement impersonated queries on all sql-jdbc drivers (SEC-1189)
Connection impersonation confines a non-admin's native queries to a restricted
DB role, which Metabase activates with a `SET ROLE` prefix. To stop a user from
appending `; SET ROLE <broader>` and climbing out of that role, impersonated
native queries must be a single statement. That guard
(`driver/validate-impersonated-query`) was only overridden for `:postgres` and
`:sqlserver`; every other impersonation-capable JDBC driver (MySQL, Snowflake,
ClickHouse, Starburst) inherited the no-op `:default` pass-through and ran the
stacked query verbatim. On MySQL a stacked `; SET ROLE ALL` activates every role
granted to the connection user, escaping the restricted role entirely.
Define `validate-impersonated-query` once at the `:sql-jdbc` floor so every
impersonation-supporting driver enforces the single-statement validation, and
remove the now-redundant per-driver `:postgres`/`:sqlserver` overrides.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XM9HwBRFsa3ptVAP7UWV14
* clean up
---------
Co-authored-by: Claude <noreply@anthropic.com>
* improve card type usage
* fix a few more Card type casts
* use same gate for visualize another way button as in getVisualizerInitialState so button doesn't silently do nothing
* Escape LIKE metacharacters in user-supplied search patterns (SEC-847)
Name the LIKE escape character explicitly so the escaping also holds on app DBs without a default one
Add h2x/like-substring and h2x/like-prefix and use them at every search sink
* Update in-place search filter tests for escaped LIKE patterns
Grant native create-queries explicitly in transform API tests
The transform API tests grant :perms/transforms explicitly but rely on the ambient db-level create-queries default (:query-builder-and-native) for the shared dataset DB.
That default is fragile: the native level only exists db-level, so any table-level create-queries row written for the group granularizes it down to :query-builder, and re-coalescing can never bring the native level back.
A target table synced by an async transform run during another test's snapshot-restore window leaves exactly such a row, and full-database-permission-for-user takes the most restrictive value across the DB.
When that happens, every test creating or running a native-query transform as a non-superuser 403s for the rest of the run while the MBQL-source tests keep passing (seen on the SQL Server 2022 driver job).
Replace the bare transforms grant with with-transform-db-perms!, which also grants db-level create-queries :query-builder-and-native.
A db-level set deletes every row of that perm type - leaked table rows included - so the grant repairs any polluted state for the test and the scoped restore puts the prior rows back afterwards.
Admins can turn on fast mode for Metabot when the selected Anthropic model offers it. A rejected fast request falls back to standard speed and holds it for five minutes, so the speed and the cache prefix stay put.
4.2.1 is published for eight more of the Hive pins in the hive-like driver,
so move them along with hive-common rather than leaving the family split
across two patch versions.
hive-jdbc, hive-service and hive-hplsql stay at 4.2.0 -- no 4.2.1 was
published for those three.
* Add a lint check requiring job-level timeout-minutes in workflows
A job without `timeout-minutes` inherits GitHub's 6-hour default, so a hung
job burns six hours of runner time - and holds up everything queued behind it -
before anyone notices.
actionlint cannot enforce this: its rule set is compiled into the binary, and
`.github/actionlint.yaml` only accepts `self-hosted-runner`, `config-variables`,
and `paths`, the last of which suppresses findings rather than adding them. So
the rule lives in a small yq-based script instead.
It runs in the lint job next to actionlint, gated on the same workflow-file
filter and passed the same changed-file list, and emits `::error file=,line=::`
annotations. Jobs that call a reusable workflow are skipped, since GitHub
rejects `timeout-minutes` there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Fold the actionlint path filter into the workflows filter
The `actionlint` filter matched exactly the same paths as `workflows`, and the
lint job was the only consumer of it. Drop the duplicate and point the actionlint
steps at `workflows` / `workflows_files`.
Also give lint.yml's own snowplow job a 10 minute timeout, so the workflow
passes the check it adds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Stop yq shifting reported line numbers on files with header comments
yq slurps a leading comment block before parsing unless
--header-preprocess=false, so every line number it reported was short by the
size of that block. Five workflows start with a comment; on cache-warm.yml the
`warm` job was annotated at line 18 instead of 42.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Update .github/file-paths.yaml
Co-authored-by: Ryan Laurie <30528226+iethree@users.noreply.github.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ryan Laurie <30528226+iethree@users.noreply.github.com>
`parse-approved-origins` runs on essentially every API response (via
`approved-origin?` in the security-headers middleware, and again in the MCP
origin check) and re-parses the whole CORS-origins allowlist each time. Any
entry that fails to parse -- e.g. a malformed `http://localhost:3000:*` saved
into the SDK/MCP origins setting -- hits `log/errorf "Invalid URL"` on every
request, producing bursts of identical errors around each dashboard load.
The allowlist strings are slow-moving globals, so LRU-cache the parse. Callers
pass a couple of distinct strings (the merged SDK+MCP allowlist, and the
MCP-only one), so a small LRU bound keeps both cached without either evicting
the other, while remaining bounded.
Claude-Session: https://claude.ai/code/session_01BjQcjQx7NPGr3Rpf6QaG4b
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Fail closed when an OAuth token's client is gone
resolve-access-token looked up the access token, checked expiry, and read the
user id off the token row — but never consulted oauth_client. So an access
token kept authenticating even if the client had been deleted.
Require the token's client_id to still exist in oauth_client, using the
client_id already present in the fetched token data — one indexed lookup on the
bearer path, no schema change. A missing client now fails closed (401).
Tests:
- core-test: token resolves while its client exists, returns nil once the
client row is deleted.
- session_oauth_test: register the referenced oauth_client so the bearer-bridge
tests exercise the new client-existence check (previously they minted tokens
against a client_id with no row).
* Revoke sessions and OAuth tokens on user deactivation
Add an `:event/user-credentials-revoked` event, emitted in the user
`before-update` hook when a user is deactivated, with two subscribers:
- session: delete the user's core_session rows
- oauth-server: mark the user's access and refresh tokens revoked_at
* Pin that an API key's lifecycle follows its synthetic user
An API key authenticates as its own synthetic user (api_key.user_id); the
creator_id column that records the admin who minted it is attribution only and
is never consulted at auth time. So deactivating that admin does not revoke the
key — which is the intended design: keys are org-level service credentials,
visible to and revocable by any admin from the API-key page, independent of who
created them. Revoke a key by deactivating its synthetic user
(DELETE /api/api-key/:id), not the human who minted it.
No behavior change. Add a regression test that pins this: a key survives its
creator's deactivation (200) but is revoked when its own synthetic user is
deactivated (401), so the two paths cannot be conflated by a future creator_id
join. The DELETE-endpoint revocation path is already covered by api-keys-work-e2e.
* Register the credentials-revoked topic in the events hierarchy
The events system validates topics against its own hierarchy in
metabase.events.hierarchy, not Clojure's global one, so the plain
derive calls left :event/user-credentials-revoked unregistered and
publish-event! threw a 500 whenever a user (or API key's synthetic
user) was deactivated. Use events/derive! like every other subscriber.
* filter lts versions when finding latest from tags
* always skip lts tag since it isn't valid for finding latest versions
* add guard for valid version to release-log
* add tests
* fix actionlint
* clean up/fix logic in release-log
* Keep returning the major version for rolling .x tags
getMajorVersionFromRef now recognizes v0.56.x / v0.51.2.x style tags in
addition to valid version strings, so only lts and non-version tags get
an empty string back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* remove dimension anchored blocks from FE
* Remove dimension-anchored blocks from the backend
Research-plan groups are now always a metric sliced by dimensions; the
dimension-anchored shape (one dimension slicing several metrics) is gone:
- add_research_groups: drop anchor/dimension_id/metric_ids, require metric_id
- remove_from_research_plan: members lose metric_ids; dimension_ids required
- research-groups validation: metric-only, anchor dispatch removed
- blocks-tree: no :type on nodes; "By <dimension>" headings and the
same-name disambiguation machinery deleted
- BlockSelection/ExplorationBlockNode schemas lose :type; the
exploration_block.type column is no longer written (left in place,
nullable)
- explorations.selmer prompt rewritten for the single group shape
Legacy dimension-anchored rows keep rendering (as metric-headed blocks);
pinned by the new blocks_test regression test.
* more FE cleanup, fix tests
* Drop exploration_block.type column
---------
Co-authored-by: John Swanson <john.swanson@metabase.com>
Co-authored-by: Nathan Voxland <nathan@metabase.com>
* Replace @cypress/skip-test with a local e2e helper
* Document --spec requirement in the e2e-test skill
* Tidy skip-test helper comments and skill doc wording
* Wait for the query to reach the reduce step before cancelling in cancel-test
cancel-test gave the query a fixed 100ms head start before future-cancel and
assumed it would be sleeping inside *reduce* by then. Only an interrupt that
lands inside *run*'s try posts ::cancel to the canceled channel, so whenever
pre-pipeline setup took longer than 100ms the future died silently during
setup and the test timed out waiting for the cancel message.
That made the test flaky under CI load and deterministically red when run in
isolation: on a fresh JVM the first query's setup takes seconds, so the
granular-rerun step (which re-runs only the failed test) could never pass it,
pinning any PR that flaked onto it. Synchronize on a promise delivered from
*reduce* instead of sleeping.
* Block on a latch instead of sleeping in *reduce*
The 1000ms sleep was a second timing assumption: the cancel had to arrive
within it or the reduce would complete and save the execution. Await a latch
nobody opens instead — the interrupt wakes it just like the sleep, with no
window to miss. The 30s timeout only bounds the worker thread if cancellation
is genuinely broken.
* Use a never-delivered promise and drop the dead *result* call
A promise is the same CountDownLatch under the hood (interrupt propagates
identically from deref) and matches the started promise next to it. The
*result* call after the block was unreachable on the happy path — the
interrupt throws out of the deref — and pointless on the timeout path, where
the test failed 28 seconds earlier.
* Collapse the port/val channel assertions into one
A timeout or closed channel takes nil, which fails against ::cancel on its
own — no need to name the winning port. Also fixes the doubled word in the
testing string.
* fix cancellation test cleanup race
* Clarify cancellation test scope
* Strengthen cancellation metadata assertion
* Polish cancellation test formatting
Clean up stale, contradictory, and unreachable guidance in Metabot prompts and skills
- Remove references to tools/params that never existed (`search_tables`, `search_transforms`, `get_report_details`, doc-search tool, `collection_id`/`database_id` on search, `update_dashboard` mutations, `is_library_member`, dead `source_tables` param, duplicate Slackbot tool)
- Align per-profile guidance with actual tool wiring: SQL-only profile has no notebook/metric/chart tools, Slackbot has no `read_resource`, `construct_notebook_query` schema differs by profile, SQL query-link behavior differs by tool
- Fix `read_resource`: pagination bug (server now returns a ready-to-use `next-page-uri` instead of broken `?page=N` guidance), curation-signal comparison, container/document listing
- Fix a Selmer `{% safe %}` vs `{% verbatim %}` bug that silently blanked all `{{#model_id}}` examples in the SQL profile prompt
- Document previously-missing required arguments across several tools (`create/edit/replace_sql_query`, `edit_chart`, time-clause `unit`, etc.)
- Fix `document_schema_collect`'s unreachable stop instruction and missing `database_id`
- Fix `list_available_fields` fallback for saved questions
- Add regression tests locking in the template-rendering and pagination fixes
* key guest embed tokens per instance instead of one shared slot
Concurrent guest StaticQuestion/StaticDashboard embeds under one
MetabaseProvider wrote into a single Redux rawToken slot, so the
last-mounted instance's token won for all of them. Key by a stable
per-mount instance id instead.
* refactor: name the guest token key mountId
instanceId read as the Metabase instance, and collided with
EnsureSingleInstance's own instanceId. Also simplify the two reducer
cases the fix rewrites, which Immer lets us assign into directly, and
drop a !mountId guard that currentToken already covers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CkZ5rUUDeeWN9GU2wuEUHk
* test: drop the multi-dashboard guest token spec
It passed with the whole fix reverted to master: asserting each endpoint
was called at least once is already true there, because every mount's
first render uses its own token prop and only the re-render collides. The
name also implied multiple StaticDashboards work, which they never have —
they share one global state.dashboard slot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CkZ5rUUDeeWN9GU2wuEUHk
* test: cover a guest refresh that switches resource
Every existing refresh case reissues a token for the same resource, so
none of them notice which token the component ends up using — PR #81240
flipped that precedence and CI stayed green. Switching to a question that
takes different parameters is what makes the choice observable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CkZ5rUUDeeWN9GU2wuEUHk
* fix: drop a guest mount's token when it unmounts
The iframe embed route re-keys its children when the token changes, so a
parent-side token update remounts the component with a fresh mountId.
Nothing removed the old entry, so Object.keys(...)[0] in
getOrRefreshGuestSession kept picking the dead mount and every request
was rewritten with its stale token. Once that token expired, the refresh
wrote back to the same dead key, so the map never recovered.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016stkr2R8EEPxp5gbyqDjdR
* test: cover a guest remount with a new token
Mounts a guest question, re-keys it with a second token the way the
iframe route does, and asserts the query goes out with the new token.
Fails without the unmount cleanup: no request reaches the new token's
url at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016stkr2R8EEPxp5gbyqDjdR
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-user sandbox fingerprint (the advanced-FieldValues cache key, and
the sandbox component of data-access tokens) narrowed the hashed login
attributes to remappings whose dimension target resolved to a field of the
sandboxed table itself. But the query processor applies *every* attribute
remapping when it runs the sandboxed query, including ones targeting
columns of tables joined inside the sandbox card, or columns referenced by
name. Those remappings were silently dropped from the fingerprint, so two
users whose sandboxes differed only by such an attribute hashed to the
same field_values row and were served each other's cached values (and
could read each other's stored exploration results via the colliding
data-access token).
Fix: fingerprint every key in attribute_remappings, regardless of what
its target resolves to. This exactly mirrors the set of attributes the QP
consumes, keeps unrelated login-attribute changes from busting the cache,
and leaves hashes for own-table-only sandboxes (the common case)
unchanged. Existing colliding rows become unreachable under the new hash
and are reaped by the delete-expired-advanced-field-values sync job.
Fixes SEC-874.
* Small correctness and hardening fixes
A handful of independent, low-risk fixes bundled together — each too small to
ship on its own, each a behavior improvement that also closes a minor
hardening gap.
Reduce DB load / avoid unconditional work:
- Settings cache: a settings-last-updated cookie now triggers a cheap
single-row staleness check and reloads only if the cache is genuinely
behind, instead of unconditionally reloading the whole settings table.
Exposes restore-cache-if-needed! with a :force-check? option as the shared
"reload if actually stale" primitive the cookie path now uses.
Reduce log noise:
- CORS origin validation runs on every request's raw Origin header, so it now
parses that client-supplied value with the silent try-parse-url rather than
the variant that logs "Invalid URL" at ERROR — a malformed Origin no longer
fills the operator's error log. Parsing behavior is otherwise identical, so
no CORS allow/deny decision changes.
Correctness (case-insensitivity):
- forgot_password throttles on the lower-cased email, matching the
case-insensitive account lookup, so User@x and user@x share one bucket
rather than each getting a fresh allowance.
Add a missing throttle:
- PUT /api/user/:id/password throttles the old_password re-auth check, which
previously had none, bringing it in line with the other credential
endpoints.
* Set create queries permission explicitly for transforms tests
---------
Co-authored-by: dan sutton <dan@dpsutton.com>
* Withhold fingerprint statistics from row-restricted users in get_field_values/read_resource
Fingerprints were read from the shared Field row and refingerprinted on demand with no
restriction gating -- the same leak #436 fixed in llm/context.clj, left open here. Gate
both behind a fail-closed, per-database-batched row-restricted-table-ids check, resolving
each Field's owning table canonically rather than trusting caller-supplied metadata.
Also guard list_available_fields, viewing-context entity lookups, and typed-schemas
table/card selection against destination-database resources, and apply a missing
permission-filter-columns pass to card-details' FK targets.
* Widen LLM schema-context row-restriction handling to sandboxing and routing
row-restricted-user? only checked impersonation and failed open on a feature/license
lapse. Replace it with metabot's fail-closed row-restricted-table-ids, applied per table so
one restricted table doesn't blank out fingerprints for the whole response.
Also filter a table's own sandbox-restricted columns (previously only FK targets were),
constrain mentioned/FK-target tables to the requested database, add a database read-check,
and cap+log restricted-table field-values fetches per table.
* Cover destination-DB exclusion in typed schemas; quiet expected 404s in viewing context
Adds test coverage proving select-schema-cards/select-tables actually exclude
tables and cards backed by a destination (routed) database from typed-schema
generation -- the only guard on that path, previously unverified.
Also stops fetch-and-format logging every expected 404 (missing entity, or a
destination-db routing guard) at ERROR level; it now logs at DEBUG and still
falls back to the simple entity rendering, same as before.
* Quiet expected destination-DB 404s in get_metadata
safe-fetch was logging every fetch failure at error level, including
the routing-destination-database 404s from check-table-resource-database
/check-card-resource-database, which are an expected outcome rather
than a bug. Matches the debug-level treatment already given to the
same case in agent/user_context.clj.
* Style pass: trim design-rationale docstrings, shorten oversized fns, wrap long lines
- row-restricted-by-db: move the retry-ladder rationale out of the docstring and
onto the try/catch lines it explains.
- permission-filter-columns: extract column-table-id/field-visible?/caller-joined?/
referenced-field-visible?/column-visible? as top-level private fns instead of
closures, bringing the function back under ~20 lines.
- build-schema-context: extract drop-or-enrich-fingerprints.
- Wrap a handful of lines pushed past 120 chars by the with-cache nesting and new
test fixtures.
No behavior change; same 83 tests (315 assertions) still pass.
* Fail closed on unresolvable table IDs in row-restricted-table-ids
A table ID that couldn't be resolved to a Table row silently fell out
of the per-database restriction map and defaulted to unrestricted,
contradicting the fail-closed contract this function documents.
* Log restriction-probe failures in row-restricted-by-db
All three exception paths in row-restricted-by-db were silent, so an
impersonation/routing attribute failure left no trace of why every
fingerprint for a database fell back to the per-table restriction path.
* Handle unrecognized get_field_values data_source gracefully
mu/defn schemas aren't enforced in production, so an LLM-supplied
data_source could reach a case with no default clause and throw
uncaught, failing the whole tool call. Widen the case (and its schema)
to cover "question"/"report" like field-values already does, and give
both the permission-check case and field-values a graceful fallback
for anything else.
* Return an agent-facing 404 for a field-id with no resolvable table
check-column-table-perms! used api/check-404, which throws without
:agent-error?, so handle-agent-error rethrows it and fails the whole
tool call. This is reachable from ordinary stale result_metadata (a
dropped column, or serdes drift), not just a malicious input.
* Apply restricted-field-values cap after the eligibility filter
The per-table cap on restricted-table FieldValues fetches was applied
to raw columns before filtering to those that should have FieldValues,
so ineligible columns ahead of an eligible one in the raw list could
consume its slot in the budget and leave it without values. Filter
first, then cap -- grouping by the persisted Field's own table_id
while at it, consistent with the permission checks elsewhere in this
namespace.
* Move database read-check out of the extract-sources/generate-sql prelude
fetch-accessible-tables-with-columns required database read access
unconditionally, so /extract-sources hard-403'd even when a caller only
wanted its independent :card_ids behavior. generate-sql is the path
that needs the 403, so move the check there via build-schema-context
and leave get-tables-with-columns (extract-sources) to return an
empty result instead.
* Exclude destination-DB cards and fieldless models from answer-sources
get-metrics-and-models had no destination-database guard, so a card
backed by a destination (routed) database could still reach
list_available_data_sources -- the same gap already closed for
get_metadata. Also omit models whose :fields end up empty after
permission filtering (no view-data access to their table) instead of
listing them as data sources with nothing queryable.
* Treat API-key users consistently across user endpoints and attributes
API keys are backed by rows in core_user with type = 'api-key'. Most
/api/user endpoints already restrict themselves to personal users, but
a few didn't, and user attributes were resolved for all user types.
Align everything on the rule that only personal users are managed via
/api/user and only personal users carry attributes:
- GET /api/user/:id and PUT /api/user/:id now 404 for non-personal
users (API-key users and the internal user), matching the sibling
endpoints (password, reactivate, password-reset-url, delete, modal)
and the list endpoint's existing type = 'personal' filter. API keys
are managed via /api/api-key instead.
- EE PUT /api/mt/user/:id/attributes now 404s for non-personal users.
- user/add-attributes only resolves attributes (tenant + jwt + login)
for personal users; other types resolve to {}. Sandboxed queries run
by a non-personal user therefore report a missing user attribute
rather than reading attributes stored on the user row.
No migration: any pre-existing login_attributes on API-key rows are
left in place but no longer take effect. GET /api/user/current still
works for API-key authentication, with an unchanged response shape.
Scoped programmatic access as a first-class feature is tracked
separately in #72959.
* Provide a schema for `/api/user/current`
* Update kondo ratchet budgets
- :metabase/validate-defendpoint-has-response-schema 441 -> 440, since
GET /api/user/current now has a response schema.
- :discouraged-var 100 -> 101 for the mt/with-temp ignore in the new
sandboxing test: it creates a :model/User, which metadata providers
do not support, matching the exception documented in the linter
message and the file's existing ignores.
* CR resp - throw on missing `:type`
* Send finish-reason length for a Gemini MAX_TOKENS truncation
Translate Gemini finishReason values to AI SDK v5 finish reasons and add them to the `:usage` chunk, as the other
adapters do. `MAX_TOKENS` now reports `:finish-reason "length"` instead of an in-stream error part, so the agent loop
stops cleanly and the client can offer to continue.
* Add missing image-related stop-reasons to gemini provider
The image related stop reasons should happen in practice since we only do text generation, but add them for
completeness.
https://docs.cloud.google.com/gemini-enterprise-agent-platform/reference/rest/v1/GenerateContentResponse#FinishReason
* Add stop-reasons from the Gemini Developer API as well
https://github.com/metabase/metabase/pull/80959#discussion_r3867216474https://ai.google.dev/api/generate-content#FinishReason
* Review feedback: only add synthetic usage part for finish-reasons-without-error
* Review feedback: name the early-stop-without-error subset
* Deterministically gate remote-sync branch-switch test on its async boundaries
* Sign in before activating the token in the admin settings describe
* Wait for collection items responses before asserting synced content
* Reword test comments per writing conventions
* Trim comments and move timeouts to request/test-level options
* Remove test comments and restore waitForTask retries cap
* Fix H2 deadlock between the post-import search reindex and readers
* Additional cleanup
* Cover the in-task H2 reindex with a unit test
* Consolidate remote-sync e2e setup into a shared helper
* Support Anthropic partner models via the Google provider
Anthropic models are served by `streamRawPredict` with an Anthropic Messages body, so route them through the new
`metabase.metabot.self.google.raw-predict` namespace, which wraps the existing claude adapter. Connecting probes the
model's own `streamRawPredict` route with an empty body: Google resolves the credential, project, location and model
from the URL before Anthropic sees the body, so an Anthropic-shaped 400 validates all four without spending a tokens.
* Title-case the Gemini model display names
To match the convention for all other providers and models.
* Translate mid-stream failures on the Google provider
`google-raw` handed back the SSE reducible unwrapped, so a failure while the agent loop consumed it escaped the
adapter untranslated and surfaced as a raw 500. Wrap it in `core/reducible-with-api-errors`, like every other adapter.
* Carry the probed model in :learned-config
We already reported `:probed-model` as sibling of `:learned-config`. Nest it inside so that it gets saved with the
learned config so we can use it for subsequent connection probes for the Google provider. This tends to be a much
better guess for what model we should use for the connection test rather than just using the first model in the fixed
models list, since there is no guarantee that model is actually present in the currently configured location.
* Probe a Google connection against the model it serves
The listing path re-probed whichever model the fixed catalog names first, so a connection at a location serving Claude
but not Gemini 3.x saved fine and then reported itself broken the moment its cache entry expired. Probe the model the
connection is known to serve instead — the one Metabot is pointed at, or the `:probed-model` its last successful probe
recorded — and fall back to the catalog's first model only when it has neither.
* Start the edit form on the model the connection was probed against
Reopening a connection while Metabot points elsewhere reset the Model picker to the type default and submitted that as
the probe target, so a Google project whose location serves Anthropic partner models but not Gemini had a no-op edit
rejected against credentials that work. Fall back to the connection's own probed-model, when the catalog still offers
it, before the type default.
* Ensure the obfuscated-value? regex matches values ending in newline
Enable "dotall" mode for the obfuscated-value? regex to support obfuscated values where one of the last two chars is a
newline. For example, the contents of JSON service account key used for Google cloud auth or similar.
https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/regex/Pattern.html#DOTALL
* Review feedback: validate-google-model!
* Review feedback: use proposed-model in vllm list-models
* Review feedback: ensure new-value is not an obfuscated-value? in set-single-provider-setting!
* Add comment about `model->family` dispatch
Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
---------
Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
Allow embedding readable cards in documents without query-authoring perms (UXW-5037)
Saving a document clones every embedded existing card into a document-owned
card row. The clone path ran check-run-permissions-for-query on the cloned
query, which for a native query demands create-queries=query-builder-and-native
on the whole database. A user who could view (and run) an existing native
question therefore could not save a document embedding it, losing their work
with a vague 403.
That check is an authoring gate, but the embedding user is not authoring a
query: the query text comes from an existing card row they passed a read
check on -- the client supplies only a card ID, never SQL. Running the clone
is still gated by the usual runtime permission checks (card read perms,
result-metadata view-data, block perms), which are the same checks that gate
running the source card, so cloning grants no new runtime capability. This
mirrors POST /api/card/:id/copy, which likewise clones a readable card
without an authoring check.
Split the documents-API card creation helper into create-card! (still
checked; used for client-supplied draft cards) and clone-card! (used when
cloning embedded cards and when copying a document's cards). Authoring a new
query -- via draft cards or by editing a cloned card's query through
PUT /api/card/:id -- still requires the appropriate query permissions.
* Bump timeout to 60 minutes
* Run redshift GC in parallel, with 1 worker per cluster node
* Run snowflake GC with N=8 parallelism
* Record dataset census before GC sweep
* Satisfy the linter
* More consistently hide tenant users/groups
There were a few locations where we weren't correctly hiding the
existence of Tenant Groups such as "All Tenant Users" when the setting
was turned off. This also hides tenant *users* from the API surface.
* allow specifically showing tenant users
I realized that there was a possible regression in the last commit. We
were hiding tenant users across the board. This meant that a workflow
that was previously possible was not possible anymore:
- an admin turns off the tenant feature or disables tenants
- then the admin uses the API to reactivate some tenant users and turn
them into internal users.
This is probably super niche anyway but it seems nicer to allow `GET
/api/user` to show these users, IF you pass the
`tenancy=external&status=all` flags.
Then you can reactivate the user and detach them from the tenant.
This is all API only so it's probably academic, but figured I'd keep the
door open.
* fix tests
* fix e2e tests
* Move tenant tests to enterprise tenant test namespaces
* Address review feedback on tenant-group hiding
- Rewrite hidden-tenant-group-ids to intersect the input IDs with all
tenant-group PKs in memory instead of building an unbounded IN clause
- Return 404 from PUT/DELETE /api/permissions/group/:group-id for hidden
tenant groups, matching GET /api/permissions/group/:id
- Assert GET /permissions/graph/group/:id succeeds when use-tenants is on
- Remove duplicate create-tenant-group-requires-use-tenants-test
`dashboard/update-dashcards!` calls `(params/dashcards->param-field-ids
(t2/hydrate new-dashcards :card))` to compute the field IDs referenced by
parameter mappings. `t2/hydrate` is a no-op when `:card` is already present
on a dashcard, so if the client `PUT`s `/api/dashboard/:id` with a dashcard
that includes a nested `:card` (the standard shape from `GET`), that object
is forwarded verbatim into `param-target->field-id`.
That function validates its `card` arg against `:metabase.queries.schema/card`
(added Oct 2025), which requires keywordized metadata (`:type/Category`,
`:source/table-defaults`, ...). After JSON round-trip through
`cy.request`, those values are strings, so the request 500s with
`Invalid input: {:result_metadata [{:semantic_type [...]}]}`.
`dissoc :card` before the hydrate call forces a fresh DB select with
properly-typed metadata, keeping the on-demand-DB field-value update path
unchanged.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A handful of independent, low-risk fixes bundled together — each too small to
ship on its own, each a behavior improvement that also closes a minor
hardening gap.
Reduce DB load / avoid unconditional work:
- Settings cache: a settings-last-updated cookie now triggers a cheap
single-row staleness check and reloads only if the cache is genuinely
behind, instead of unconditionally reloading the whole settings table.
Exposes restore-cache-if-needed! with a :force-check? option as the shared
"reload if actually stale" primitive the cookie path now uses.
Reduce log noise:
- CORS origin validation runs on every request's raw Origin header, so it now
parses that client-supplied value with the silent try-parse-url rather than
the variant that logs "Invalid URL" at ERROR — a malformed Origin no longer
fills the operator's error log. Parsing behavior is otherwise identical, so
no CORS allow/deny decision changes.
Correctness (case-insensitivity):
- forgot_password throttles on the lower-cased email, matching the
case-insensitive account lookup, so User@x and user@x share one bucket
rather than each getting a fresh allowance.
Add a missing throttle:
- PUT /api/user/:id/password throttles the old_password re-auth check, which
previously had none, bringing it in line with the other credential
endpoints.
* Flush other executions before test. Divert other executions during test.
* Flush after closing the batch ingress path.
* Remove extraneous changes
* Remove more extraneous changes
* cleanup data warehouses nightly
* rename
* don't reinvent wheels
* simplify
* more
* cleanup
* updates
* Absorb failures at the redshift cluster level and at the driver level
* Extract old dataset name pattern
* Update ratchets
* Merge MB_REDSHIFT_TEST_HOSTS and MB_REDSHIFT_TEST_HOST when both are set
* Allow ad-hoc dispatch and report on outcomes of gc sweeps
* Don't run on forks
* Update ratchets
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Ben Grabow <ben.grabow@gmail.com>
Make isRenderedWithinViewport retry until element settles in viewport
The command read getBoundingClientRect() once, so a caller asserting right
after a scroll-into-view action (focusing a recently moved dashboard filter)
could observe the element mid-scroll (top ~1647px vs 800px viewport) and fail.
Wrapping the geometry checks in .should() lets Cypress re-read the rect until
it lands within the viewport.
* fix: recognize lib/*-stripped pMBQL in ->legacy-mbql, restore edit-chart's query-id
->legacy-mbql gated pMBQL->legacy conversion on :lib/type alone, so a query that
lost its :lib/* keys (e.g. round-tripped through the frontend's viewing context)
fell through unconverted. The raw pMBQL then got embedded in /question# links,
crashing the frontend with "Stage 0 does not exist". Detect pMBQL structurally
(:lib/type or :stages) and normalize against the query schema before converting.
Also add the query-id edit-chart's result was missing, so extract-charts can
capture edited charts into conversation state and chart->xml stops rendering
an empty query-id attribute.
* fix: preserve edit-chart's query in memory, don't fail turn on link conversion
edit-chart's result set :query-id without :query, so extract-charts (which
requires both to fire, then rebuilds :queries from :query alone) overwrote
the chart's real queries with [nil] on the next update-memory call.
->legacy-mbql could also throw instead of degrading: normalizing a
:lib/*-stripped query mints fresh :lib/uuids that don't match positional
aggregation/expression refs elsewhere in the query (e.g. an order-by on the
query's own aggregation), failing conversion inside the streaming transducer
and taking down the whole agent turn instead of just producing a bad link.
Falls back to the raw query on conversion failure now.
* fix: document ->legacy-mbql's conversion-failure fallback
Docstring claimed conversion always succeeds after normalize, but the
try/catch fallback added earlier can still return raw pMBQL when a
stripped query's positional refs don't resolve.
* test: mark edit-chart-result-survives-update-memory-test ^:parallel
Doesn't write shared state, same as the test above it.
* fix: thread edit-chart-tool's resolved query into edit-chart
edit-chart-tool falls back to current-queries-state when a chart's own
:queries is empty, but edit-chart only checked charts-state, so its
result carried :query-id with a nil :query in that case -- the same
[nil]-in-memory bug fixed earlier, via a different path. Pass the
caller's resolved query through as a fallback.
* fix: capture viewing-context charts' query-id, don't clobber on edit
chart-config->chart never set :query_id, so a chart seeded from viewing
context rendered query-id="" on edit and was never captured into :charts
state by extract-charts. Reuse the context-item id already shared with
:chart_id.
That capture also collided with edit-chart-tool's own memory write for
the same chart-id: extract-charts fully replaced the entry, dropping
:image_base_64/:timeline_events/:chart_config the tool had just written.
Merge onto the existing entry instead.
* fix: separate ->legacy-mbql's docstring from its bug history
Docstring for ->legacy-mbql read as the change's rationale (BOT-1604,
the :stages detection story) rather than the function's contract.
Trim the docstring to what a caller needs -- normalizes MBQL 5 to
legacy, passes through unchanged on failure -- and move the "why"
for the :stages guard down to an inline comment next to the check.
Remove stale /* eslint-env node */ comments from frontend/build files
Flat config ignores eslint-env comments (ESLintEnvWarning per file, hard
errors as of ESLint v10), and they are redundant: eslint.config.mjs already
defines globals.node for frontend/build/**/*.js.
* respect default time dimensions from list of dimensions
* Curate a default time dimension on the time-facet test fixture
Master's #79409 stopped auto-assigning a default dimension to new
metrics, so the time-facet tests' fixture card no longer gets a default
time dimension from dimension auto-sync. Since this branch makes the
curated default authoritative for the time-facet variant (no fallback to
the query's temporal breakout), curate the default explicitly on the
fixture.
The earlier fix (#80388) made *queue declarations* reload-safe by guarding
them with a load-time ownership claim and replace-on-reregister semantics,
but left the *listener* registry non-idempotent: register-listener! threw
"Listener already registered" whenever the channel was already present in
the root *listeners* atom.
Fix mirrors the queue side exactly: def-listener! now claims its channel's
declaration site at load time (claim-listener-declaration! — same-ns
re-claims are no-ops so reloads stay legal, cross-ns claims throw), and
activates through register-declared-listener!, which replaces any existing
registration instead of throwing. Ad-hoc batch-listen! keeps its strict
throw-on-duplicate contract (double-listen-throws-test et al.), since
runtime registrations have no load-time claim protecting them.
* Encrypt subscription/alert recipient details; enforce allow-list on send
Encrypt at rest the `details` column of `notification_recipient` and
`pulse_channel`, which stores external email recipients for alerts and
dashboard subscriptions, so plaintext values are rejected on the strict
read when an encryption key is configured. Register both columns in
`encrypted-string-columns` so `encrypt-db`/`decrypt-db` and key rotation
re-encrypt them, and add a v58 migration that encrypts existing rows and
decrypts them on rollback.
Also enforce `subscription-allowed-domains` at send time (previously only
on write): the notification send path calls `validate-email-handlers!`
before delivery and throws if any external recipient is not permitted by
the allow-list. The exception is re-thrown with a generic message so
recipient addresses are never written to the logs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Move the v58 pulse_channel encryption migration test into the v58 group
order-your-migration-tests-test requires migration tests to be grouped by
major version in ascending order; the new test had landed among the v48 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Fail the test when a rollback-only scope cannot roll back
Every top-level `with-temp` runs in a rollback-only transaction. If something
inside one commits -- DDL does that implicitly on H2 and MySQL, which also
destroys the savepoint -- the rows written up to that point are durable and
leak into every later test. Warning about it hides the leak until some
unrelated test counts rows and gets the wrong answer, and the six offending
sites found so far were caught exactly that way.
Throw instead when running tests. That covers every cause -- raw SQL DDL,
HoneySQL DDL, a savepoint lost to a deadlock -- where a guard on the SQL we
build would miss the raw statements `drop-table!` sends. Production keeps
warning and discarding what is still pending: there the transaction is a real
one that has already committed, and failing the request would not undo it.
It sucks!
We will need to put our own error handling in place to do Actually
Good retries for 429s, but it's not clear yet where the best place for
that would be, because we're not getting stack traces from the 429s currently.
* Write the metadata mirror from one listener, not 33 endpoint hookups
* Describe the hydration listener by what it does, not what it replaced
* Point the original_fields note at the listener, not onQueryStarted
* Pin the hydration listener to Api actions, and report a failed write
SEC-669: re-check read permission when listing bookmarks
GET /api/bookmark (bookmarks-for-user) filtered only on the target's
archived column and never re-checked read permission, so a bookmark kept
leaking a card/dashboard/collection/document/exploration's name,
description, and type after the user lost read access without the item
being archived (moved to a private collection, or a revoked/narrowed
collection grant).
Add a visible_collection_ids CTE (permissions only; archived filtering is
left to the existing where-conditions) and filter each bookmark branch by
collection visibility via visible-collection-filter-clause. Visibility is
evaluated for the passed user-id via a UserScope, so the function is
correct for any user, not only the current request's user.
* Redesign Radio
* Add Radio stories to loki filter
* disallow radio labelPosition
* use StoryBoard in radio stories
* add new story's snapshots
* Radio.Card: border, leftSection, migrate call sites
- card border 0.5px border-neutral-strong, none when disabled
- children disallowed, leftSection slot for icons
- EditUserStrategyModal + DataSegregationStrategyPicker on label/description
* update loki snapshots
* Icon color prop instead of css module in DataSegregationStrategyPicker
* remove extra div
* make the indicator in the radio card optional
* fixed radio size, no size prop
* radio card body via mantine text/stack
* forwardRef radio card
* radio card border: highlight when selected
* update loki snapshots
* drop size="xs" from radio call sites
* fix card growing on select, interactive card stories, optional checkbox card indicator
* skip interactive card stories in loki, update card snapshots
* Key cached session tokens on the app db
with-empty-h2-app-db! swaps in a different application database, and this cache was keyed on the username
alone. A token minted against the previous database names a session that does not exist in the new one, so
the request 401s, and the token minted inside leaks back out when the original is restored. The retry in
client-fn cleared the whole cache and tried again, which papered over it often enough to look like flake:
"Failed to authenticate X after three tries".
Key it on the app db as well, like every neighbouring cache does via memoize-for-application-db.
databases-list-can-upload-test and dismiss-spinner-test go from failing to passing; both were in the CI
failure list, the former across 12 jobs.
* Fix rollback-only app DB transaction semantics
* Reject rollback-only combinations that cannot roll back
- :nested-transaction-rule :ignore skips the savepoint :rollback-only needs, so the pair is rejected at any
depth rather than silently committing
- a throw out of the transaction body now rolls the connection back explicitly, since restoring autocommit
commits whatever is still open -- a requested rollback that failed was committing the work it had to discard
- the unsupported-options error names the options, and drops the "application database" framing that
misattributed failures on other java.sql.Connections
* Restore transaction state even when the savepoint rollback throws
The callback accumulators are discarded unconditionally at the top of rollback!, so leaving the state
restore behind a throwing .rollback left the tree half-unwound: an outer scope that caught the error and
committed would run its before-commit callbacks against the failed nested scope's state.
* Create test-data Databases outside the caller's transaction
Their ids are memoized for the whole app db, so the Database has to outlive whatever scope first asked for
one. Created inside a test's rollback-only with-temp it is rolled back while the memo keeps its id, and every
later lookup hands out an id for a row that never existed.
do-outside-transaction is the same detachment run-after-commit-callbacks! already did privately.
* Don't cache a personal collection id created inside a transaction
The cache's premise is that a Personal Collection cannot be deleted, so its id never changes. A rollback
breaks that: the collection goes away and the cached id points at a row that never existed, which reads as
a 403 on every later request that resolves the user's personal collection.
* Skip the test-database cache in a transaction rather than detaching
Detaching made the Database durable but took a second connection while the caller's transaction was still
open. When that transaction holds cluster lock rows -- data_permissions and search index setup both take
them -- the second connection waits on them until the lock acquisition times out.
Bypassing the cache keeps the work on the caller's connection, and matches how the personal collection id
cache handles the same problem. do-outside-transaction goes with it, having no other callers.
* Stop the outer commit when a requested rollback failed
The full rollback only runs when this call turned autocommit off, so for a nested scope or a caller-managed
connection a failed rollback! just propagated. Code between there and the outermost scope can catch it, and
the commit branch would then commit the writes the caller asked to discard.
Mark the tree instead, and refuse to commit it. Throwing rather than quietly rolling back: the caller asked
to commit and cannot, and dropping the outer work silently trades one bug for a worse one.
* Tolerate a savepoint an outermost rollback-only can no longer roll back to
A statement in the body can commit the transaction under us -- DDL does that implicitly on H2 and MySQL --
which invalidates the savepoint. Those writes are durable by then, so failing the scope reports a problem
nobody can act on while leaving the data exactly where it was.
At the outermost scope, discard whatever is still pending and log where it happened. Nested scopes still
poison the tree and throw, since there the enclosing work is pending and must not be committed.
* Note the option of rejecting DDL inside a savepoint-bearing transaction
* Never commit a transaction whose rollback failed
Four review passes flagged the same hole from different angles. Restoring autocommit commits whatever is
still pending, and after a rollback we were relying on to discard those writes, that is exactly the work
we were told to throw away. Track it and leave autocommit off instead, letting the pool roll the
connection back on check-in.
Two narrower routes to the same place:
- rollback! now marks the tree itself, so an exception-driven rollback that fails poisons it too. Before,
only a requested rollback did, and the error path could still reach a commit.
- the mark is rechecked after before-commit callbacks drain. A callback can catch a nested scope whose
rollback failed, which lands after the earlier check has already passed.
Two tests changed meaning as a result: a nested scope whose rollback throws now stops the outer scope
committing, so it no longer completes and its own after-commit callbacks no longer run. That is the point
-- its writes are still there.
Also adds the regression test for personal collection id caching, models autocommit honestly in the mock
rather than pinning it true, and records what the in-transaction test-Database lookup gives up.
* Build the test-data Database before with-temp opens its transaction
Created inside a rollback-only scope it never survives, so it is rebuilt for every test, and the ids
memoized in metabase.test.data.impl outlive it. Cards then get inserted against table ids for rows that no
longer exist: FK_REPORT_CARD_REF_TABLE_ID on a fresh app db.
Materialise it before the transaction opens, where it can commit. The table and field lookup maps get the
same in-transaction bypass the Database id already had, for the case where a dataset is first asked for
inside a scope anyway -- neither is enough on its own.
Reproduce with a throwaway app db, which is what CI has and a local one does not:
MB_DB_TYPE=h2 MB_DB_FILE=/tmp/fresh-$$ ./bin/test-agent :only '[metabase.queries.models.card-test]'
* Only pre-warm the dataset for the default driver
db-id resolves through the driver under test, so in a driver-test namespace the pre-warm reached for that
warehouse's dataset -- work a with-temp there may never need, against a warehouse that may not be up.
Driver failures went from 2 to 12 in the push that added it.
The FK problem it fixes is on the app-db side, for the default dataset, so confine it there. Best effort
even then: a dataset that cannot be built must not take an unrelated with-temp down with it.
* Evict temporary users from personal collection cache
* Evict the personal collection cache on the rollback, not on the user
Clearing the temp User's own entry misses the case that actually bites: a committed user's Personal
Collection is created lazily on its first permission check, which often lands inside somebody else's
with-temp. Nothing evicts it when that scope rolls back, and every later request for that user 403s.
Key off the with-temp boundary instead, and drop whatever it may have cached. Costs a re-lookup per user
afterwards; correct whichever user and whichever scope created the entry.
agent-api + activity-feed went from 21 failures to none.
* Skip the dataset pre-warm where the app db is deliberately empty
with-empty-h2-app-db! rebinds the app db, and the memo key includes its identifier, so the pre-warm missed
the cache and created a test-data Database inside a scope whose whole contract is emptiness.
databases-list-can-upload-test snapshots an empty Database set and saw two.
Bind *skip-dataset-prewarm?* there instead. Also drops the earlier :h2 restriction, which came from a bad
reading of two CI runs sampled at different completion stages: it could never have helped here, since those
helpers are H2, and it took the pre-warm away from driver tests that needed it.
Reproduce with a throwaway app db, which is what CI has and a local one does not:
MB_DB_TYPE=h2 ./bin/test-agent :only '[metabase.queries.models.card-test]'
* Register the cache eviction with a unique key
metabase.test.redefs already holds `:around :default` on do-with-temp*, and methodical overwrites methods
sharing a key rather than composing them. Whichever namespace loaded last would win, silently dropping
either this eviction or the rollback-only transaction wrapper. It happened to work; it should not depend on
load order.
Also notes what a failed connection-level discard leaves behind. Only a caller-supplied connection is
exposed: on one we own, the same flag stops autocommit being restored, so the writes are never committed.
It logs rather than throws because throwing from that catch would mask the exception being unwound, and
master never attempted the discard at all, so a caller-supplied connection is no worse off than before.
Signalling it properly would mean telling the caller some other way.
* Note that session tokens are cached without regard for transactions
The user ids directly above skip the memo inside a transaction, since users created in a `with-temp` are
discarded with it. The token cache has no equivalent guard, so a session rolled back with its scope leaves a
token behind and the request fails with "Failed to authenticate X after three tries". The retry in
`client-fn` hides it more often than not.
* Note that a rollback can leave a stale personal collection id cached
"A Personal Collection cannot be deleted" does not hold for a transaction that rolls back. Tests evict on
the with-temp boundary; production has no equivalent and keeps the id for the rest of the TTL. Accepting
that for now -- it predates honoring :rollback-only, and closing it properly wants an after-rollback hook
alongside do-before-commit/do-after-commit.
* Follow the created-globally rename in the token cache TODO
* Build the query for export inside the guard, not beside it
`:query_json` already returns nil for a card whose query cannot be exported, but `lib/query` was called in the
argument position, outside `try-export-query`. A stored `dataset_query` with no stages builds a query that fails
its own output schema, so one such Card takes down every response that walks a set of Cards -- the typed-schemas
endpoint among them.
metabase.search.appdb.scoring-test alongside metabase.typed-schemas-rest.api-test goes from 13 failures to 0.
* Don't run a test request inside a rollback-only transaction
`user-request` takes a temporary Session when it is handed a User or an id rather than a `:rasta`-style keyword,
and the handler runs in-process on this thread. Now that the with-temp rolls back for real, every app db write
the request made is discarded when it returns -- the warehouse table writes survive on their own connection and
the API still answers 200, so the test sees a successful call that changed nothing.
Insert the Session and delete it in a `finally` instead. A caller already inside a transaction still gets the
same behaviour it always had; what goes away is the extra rollback scope around the request itself.
metabase-enterprise.action-v2.models.undo-test goes from 25 failures to 0, passing all 134 assertions, which
matches master.
* Mint the test users' sessions before anything opens a transaction
`authenticate!` ran about 150 times in a single `pulse.api.pulse-test` run, nearly always inside a transaction.
A session minted inside a `with-temp` is rolled back with the scope, the next request 401s, and the retry in
`client-fn` cleared the whole cache -- every user's token, not just the dead one -- so everyone re-authenticated
in-transaction and the cycle repeated. Each of those inserts bumps the user's `last_login` through the
`:model/Session` after-insert hook, which is what breaks a test that reads the creator before making its request.
Mint the four test users' sessions at initialization, where nothing has opened a transaction yet, so they are
durable. On a 401 forget only that user's token, so one temporary user's rolled-back session cannot evict them.
pulse.api.pulse-test goes from one failure to none, matching master. publish-refresh-error-event-sends-admin-email-test
passes too, where it fails standalone on master: that email only goes to admins with a `last_login`, and
crowberto's was rolled back with every login.
* Ask for the FieldValues the chain filter test needs
The full FieldValues row for `categories.name` is created lazily, and `update-field-values-for-table!` only
refreshes rows that already exist -- so a run that creates it inside a `with-temp` discards it and nothing
makes another. `select-one-pk` then returns nil and the test dies on "Not something with an ID".
Same shape as the rest of this branch: a durable row created on demand, now rolled back with the scope that
created it. Ask for the row instead of assuming an earlier test left one behind.
* Run the metric leaf queries one at a time inside a transaction
A transaction owns a single connection and `future` hands that binding to every thread it starts, so the
leaves' app db writes -- used-card stats behind a cluster lock, query rows -- interleave on one session.
Savepoints are per session: a thread rolling back to its own destroys the ones its siblings are holding, the
next release fails with "savepoint does not exist", and the transaction aborts, taking the rest of the test
with it. That is why api-arithmetic-test loses a different test on each CI run.
Only tests get here. A request runs in no transaction, where each future takes its own pooled connection and
the parallelism is worth having.
On a throwaway Postgres the namespace errors on two tests per run before this and passes three runs out of
three after.
* Commit a lock row that would otherwise roll back with the caller
A lock row is written once and read forever after, and acquisition leans on that: rival first-time inserters
block on the winner's uncommitted unique-index entry and retry once it commits. Inside a `with-temp` that
commit never comes, so the row is gone again and the next acquisition replays the race -- on MariaDB until the
lock-wait timeout expires.
Insert it on a connection of our own when the caller was already in a transaction, which is the only case
where the row would not survive. Nothing in production takes a cluster lock from inside another transaction,
so this leaves that path alone -- worth keeping that way, since lock names are dynamic in places and rows that
used to roll back with a failed transaction would otherwise persist.
Reasoning rather than measurement; the TODO on the function says so and says what to do if it does not help.
* Bound the out-of-band insert on its own connection
`set-query-timeout!` returns false on MySQL running MariaDB Connector/J, and the fallback sets
`innodb_lock_wait_timeout` on the session -- but `acquire-lock-row!` sets it on the caller's connection, not
the one the insert opens. Blocked by a rival's uncommitted row, the insert would then wait out the server
default instead of the second it was given, on exactly the databases this is meant to help.
Give it the same treatment on its own connection. Also note in the docstring that the second connection is
only taken until the row exists, which is what bounds the cost of holding two at once.
* Resolve the venues id before counting queries
The cache is fine; the query the test counts is its own `(mt/id :venues)`. Inside a transaction the test-data
id lookup deliberately skips its memo, so the call lands in the window that is supposed to see none.
* Keep the pre-warm out of app dbs that are meant to be empty
`with-empty-h2-app-db!` already opts out of the dataset pre-warm; the serialization test dbs and the
schema-migration ones never did. A `with-temp` inside them materialised the test-data Database, and an extract
then saw a Database no assertion expects -- serialization.v2.load-test/database-test wanted one and found two.
* Do not insert a lock row out of band outside tests
The claim in "Commit a lock row that would otherwise roll back with the caller" was wrong: production does take
cluster locks inside another transaction. `load-from-h2!` inserts Databases, and so takes the batch-permissions
locks, inside one big load transaction. Our fresh connection then blocked on a next-key gap lock the caller's
own SELECT ... FOR UPDATE was holding, and a thread cannot outwait itself -- it timed out, retried, and failed.
That is what broke dump-to-h2, load-from-h2 and rotate-encryption-key on MariaDB.
Gate the out-of-band insert on `config/is-test?`, which is the only place a transaction may never commit, so
production goes back to exactly what it did before. Keep a fallback for the same shape inside tests, where
`load-from-h2!` can block on itself just as readily: on a lock-wait timeout, insert on the caller's connection.
* Stop three card tests leaving rows behind
`updating-model-query-does-not-shift-metadata-overrides-test` had no cleanup at all, and
`create-card-with-type-and-dataset-test` opened a plain `t2/with-transaction` where its siblings pass
`{:rollback-only true}` -- so both committed cards with a `{}` dataset_query that every later test walking the
card table then had to survive. That leak is older than this branch; honoring the rollback is what makes the
second fix work.
`get-series-for-card-permission-test` read crowberto's personal collection with `select-one-pk`, and a nil
collection_id puts the card in the root collection, which rasta can read -- so the test quietly asserted 403
against a card that was never private. Ask for the collection instead.
* Truncate the heartbeat fixture times to milliseconds
Postgres stores microseconds, so a timestamp ending .7369995 comes back a millisecond above its own
millis-truncation and the assertion misses by one.
* Tolerate a card whose query will not parse when finding related entities
Nothing at the schema level stops a stored `dataset_query` being empty, and such a card simply has no
context-bearing forms. Without the guard one of them takes down the related-entities computation, and with it
every x-ray, for everyone else. Same treatment `card-details` already gives an unexportable query.
* Give the end-to-end schema test a Database of its own
The test asserts exactly which questions, metrics and models fall inside the test-data Database's scope, which
holds only as long as no other namespace commits a Card against that Database. Two such leaks have been fixed
on this branch already and there is at least one more; the assertion cannot be made safe by chasing them,
because any test added later can break it again.
Run it against a copy of the Database instead, so nothing else can write there.
A throwaway namespace that commits one Card on the test-data Database ahead of it reproduces the CI failure
exactly (core_test.clj:147) and passes with this change.
* Recognise the MySQL lock-wait timeout in the in-band fallback
`lock-wait-timed-out?` knew about `SQLTimeoutException` and our own `::acquisition-timeout` marker, but not
ER_LOCK_WAIT_TIMEOUT itself. The server raises that on its own schedule, so on MySQL and MariaDB -- the
databases the fallback exists for -- it could arrive as a plain `SQLException` and the fallback would not fire.
Check the code through the cause chain.
* Take the revisions with the cards
`with-model-cleanup` deletes rows directly, which skips the Card hook that would have removed the revisions
this test's POST and two PUTs create. Name Revision in the cleanup as well.
* Truncate once, and align the chain filter's temp values
The second `truncate-to` could not do anything the first had not.
* Create the test users before the related tests race for them
`user->id` skips its memo inside a transaction and creates the users there instead, so on this branch they roll
back with the scope and the next test makes them again. These tests are parallel and every one of them takes a
`with-temp` Card whose creator defaults to rasta, so several threads insert the same user at once and all but
one time out on the row lock. The namespace goes from three errors to none.
Also covers the guard in `related/definition`: a Card whose stored query will not parse contributes no
context-bearing forms, and a genuinely similar Card still outranks it.
* Cover the card whose query will not build
The guard in `card-details` had no test: a Card with a stage-less stored query answers without `:query_json`,
and the Card beside it in the same response is unaffected.
* Cover the batch path and the lock-wait code
The first version of the card test called `get-table-details` once per card, which cannot see the failure it
claims to cover -- the bug was that one bad card killed the whole `cards-details` seq the others were in. Go
through that seq instead. Removing the guard turns the test red at the same malli output error CI was showing,
which the per-card version did not.
Also cover `lock-wait-timed-out?`: MySQL's ER_LOCK_WAIT_TIMEOUT on its own and inside a cause chain, and any
other error still propagating rather than being taken for a timeout.
* Create the temp search index before the transaction, not inside it
`with-temp-index-table` runs CREATE TABLE and DROP TABLE on the ambient connection, and on H2 and MySQL that
commits the transaction around it. Six tests opened it inside a `with-temp`, so the rollback had nothing left
to take back and every row they created leaked -- a Transform, some Cards and Collections, a published Table, a
Document -- to any later test that walks the app db.
That one leaked Transform is the whole of the serialization failures: a collection-scoped export includes every
Transform in the instance, so all four tests came back with one entity more than they expected.
Take the index table before the `with-temp` instead. The nested scope reuses it, so nothing else moves.
`metabase-enterprise.search.scoring-test` also had no fixtures at all, and fails on its own: the first lazy
`initialize :db` fires mid-test and recreates the H2 database underneath a test already writing index
bookkeeping.
The 133 namespaces that run before serialization.api-test in the Enterprise job reproduce the CI failures
exactly -- 5011 assertions, 5 failures -- and pass with this, at 4714 assertions.
* Say the cluster lock comments plainly
Lead with the point rather than building up to it, and drop the clause chains. No behaviour change.
The TODO above `clear-cached-session-tokens!` was stale: it asked for the `created-globally` treatment, which
landed earlier on this branch. Point at what is actually left instead -- initialization can still first happen
inside a `with-temp`, and then the sessions roll back with it.
* Keep the non-index assertions where the app db cannot index
`with-temp-index-table` is a `when`, so wrapping a whole test in it skips the whole test on MySQL and MariaDB.
Two of the tests moved in the last commit also assert things that need no index, and those assertions went
quiet there. Add a sibling that runs the body either way, and use it for those two.
`search.appdb.scoring-test` had no initialization fixture, the same gap its enterprise counterpart had: the
first lazy `initialize` can recreate the H2 db underneath a test already writing index bookkeeping.
* Sort the fixtures require
* Download through the client that retries a 401
`downloaded-card-filenames-test` passed a raw cached token straight to `client-full-response`, so it skipped
the retry every other test gets from `client-fn`. A session rolled back with the scope that made it then came
back here as a bare 401, which is what the BigQuery job has been failing on.
`user-http-request-full-response` still returns the headers the test reads.
* Improve rollback test isolation and comments
* Do not poison the transaction tree when the savepoint is already gone
MySQL and MariaDB roll back the whole transaction to break a deadlock, discarding every savepoint with it. The
ROLLBACK TO SAVEPOINT that follows fails with "SAVEPOINT does not exist", and marking the tree rollback-required
there is backwards: the server has already discarded the writes, so nothing is pending for an enclosing scope to
commit by mistake. A rollback-only scope whose DDL committed implicitly already gets this treatment.
The refusal message named a rollback-only transaction, but the ordinary error path sets the flag too.
Also validate :nested-transaction-rule with a throw. It is a public option, and `assert` compiles away under
`*assert*` false. Sorting the unsupported keys keeps the message stable across runs.
* Keep the test request's session out of the login hooks
:model/Session's after-insert hook publishes :event/user-login, which stamps User.last_login, and
:event/user-joined for a User that has never logged in. Deleting the Session afterwards undoes neither, so every
user-http-request left both behind for whatever ran next.
Write the row instead. created_at has to come along, since the model supplied it.
* Ask whether the app db can hold the index, not whether any engine can
supports-index? is true for any active engine. With semantic search on MySQL or MariaDB, the macro that exists to
run the body without an app-db index took the app-db branch and tried to create the table anyway.
* Say something when a stored query is dropped
Both guards swallowed every exception silently, so a Card whose query failed to build for an unrelated reason
looked exactly like one that has no query. try-export-query, right next to the Metabot guard, already warns.
Related entities logs at debug: it runs for every candidate Card while ranking.
* Commit the out-of-band lock row rather than assume autocommit
The docstring said the row was committed, but nothing committed it -- the insert relied on the pooled connection
arriving in autocommit, which the pool does not promise. The caller's SELECT cannot find the row otherwise.
* Let a failed dataset pre-warm be heard
Now that the pre-warm runs for every driver, a warehouse that is down fails it on every top-level with-temp. At
debug that is invisible on a normal run and resurfaces much later as a Database that does not exist. Warn once
per message.
Also note that mt/id costs a query inside a transaction, which is what a with-call-count window has to plan for.
* Polish prewarm comments
* Mark the tree rollback-required even when the savepoint vanished
A vanished savepoint does not mean nothing is pending. DDL committing implicitly, or a server breaking a deadlock,
ends the transaction and discards the writes so far -- but anything written afterwards sits in a fresh transaction
and is still pending. Skipping the flag there let an enclosing scope swallow the rollback error and commit exactly
those writes.
Say so at the point of the catch, since the reasoning does not survive a quick read.
* Gate every app-db index helper on app-db support
with-temp-index-table and with-appdb-search-if-available-otherwise-legacy still asked supports-index?, which is
true for any active engine. With semantic search on MySQL or MariaDB they ran app-db DDL the app DB cannot take,
including from scopes nested inside the fallback body that exists to avoid exactly that.
Cover the borrowed session too: it must leave last_login empty, and delete itself afterwards.
* Cover the out-of-band lock row against a connection in a transaction
Hands the dedicated connection over with autocommit off and checks the row survives the caller's rollback. Without
the explicit commit the insert dies with the connection, and acquisition then fails on the row it just wrote.
Runs on MySQL and PostgreSQL only; H2 takes an in-process lock and writes no row.
* Pin the vanished-savepoint case against the outer commit
A savepoint the server already discarded says nothing about what was written after it. The existing rollback tests
throw generic exceptions, so nothing stopped the SQLState 3B001 / MySQL 1305 exemption from coming back.
* Pin the engine in the otherwise-legacy fallback
Where the app DB cannot hold an index, the fallback took whatever default engine was active. Semantic can be
active there, and the bodies are written for an index the app DB does not have. Run them under legacy search, as
the macro's name says. with-legacy-search moves above its first use.
* Polish rollback and search comments
* Stop two remapping scopes deadlocking on MySQL and MariaDB
A remapping writes a Dimension for a Field the whole suite shares, and holds it until its scope ends. Two threads
doing that at once deadlock on the unique index over dimension.field_id: each holds the record lock the other's
uniqueness check wants, while asking for the insert-intention gap in front of it. MySQL and MariaDB pick a victim
and roll it back, which drops its savepoints, so the rollback that follows fails on a savepoint that is gone.
Serialise remapping scopes against each other. Tests that never remap keep running in parallel alongside them.
The helper already carried a TODO saying it was not thread-safe.
Reproduced on MariaDB 12.3 and confirmed on master, so this predates the branch. custom-values-test failed nearly
every run before, and three runs in a row are clean after. Verified on H2 as well.
* Serialise every temporary Dimension, not just remapping scopes
Remapping is not the only way a test writes a Dimension for a shared Field: card-test, card/metadata-test,
field-values-test and data-studio's table-test all create them directly, in parallel, on the Orders and Products
fields. Those still raced with the remapping scopes.
Move the lock to `do-with-temp*` for :model/Dimension so every site is covered, including ones added later.
`locking` is reentrant, so a `with-temp` holding several Dimensions still works.
* Take the search index out of six more with-temp transactions
Same fix as the earlier pass over the search tests, for the ones that pass a warehouse driver: their index scope
sits inside `with-temp`, so `drop-table!` runs DDL on the ambient connection. H2 and MySQL commit the transaction
to do it, which makes the temporary Transforms durable and destroys the savepoint. The rollback that follows can
no longer take them back, and it discards toucan's compensating deletes along with everything else still pending,
so the Transforms outlive the test.
Three tests later in the same namespace count transforms and saw the strays: root-collection-items-include-
transforms-test expected one and found four.
These were missed the first time because `mt/normal-drivers-with-feature :transforms/table` skips them on H2, so
only a driver run reaches them.
Reproduced with a MariaDB warehouse: transform-test went 3 failures to 0, and the four namespaces that failed
together now match master at 2532 assertions. H2 is unchanged at 109.
* Let the failed-rollback warning reach the console
The shared Console appender only accepts FATAL, so the warning naming a rollback-only scope that could not roll
back went to logs/test-log.json and nowhere a CI run gets read. It took hours to find six tests leaking rows
because of it; the warning had been firing the whole time.
Give that logger an appender that accepts WARN. The message carries the mb-test key, so it names the test that
broke its own isolation rather than the later one that trips over the rows.
* Hoist the search index scope above with-temp in two document search tests
Both tests opened `with-appdb-search-and-legacy-search` inside `mt/with-temp`.
That scope creates and drops the index table on the ambient connection, and on H2
and MySQL the DDL commits the transaction `with-temp` meant to roll back, so the
temp Documents leaked into later tests.
Found by the throw this branch adds: both showed up as errors at connection.clj:293
in the app DB test jobs.
* Serialize the reduced-fields card test
It holds a rollback-only savepoint across two `with-temp` Cards and an API call. Run in parallel, something else
commits the transaction that savepoint lives in, so both Cards are discarded and the downstream query reports its
source Card as missing.
Only MySQL surfaces it, and only under a full suite: the test passes on its own, and six local configurations could
not reproduce it. Confirmed by running the MySQL and MariaDB driver jobs with this single change -- all four green,
against four runs where they failed.
* Materialize the test users' Personal Collections before with-temp opens
`user->personal-collection` is a get-or-create and sits on the permissions path every API request takes. A
namespace that does not initialize `:test-users-personal-collections` creates Lucky's the first time a request
calculates permissions -- which happens inside a `with-temp` -- so the transaction rolls it back. Every later
request tries to create it again, and the parallel ones time out on each other's COLLECTION table lock:
GET /api/database expected a status code of 200, got 500.
Timeout trying to lock table; INSERT INTO COLLECTION (..., "Lucky Pigeon's Personal Collection", ...)
That is what failed `databases-list-include-saved-questions-test` on the Mongo, Redshift and Athena jobs -- the
empty database list was the 500, not a visibility problem.
Prewarm them next to the dataset, under the same skip flag, so helpers that need an empty app DB are unaffected.
This also closes the related hole where `:test-users` itself first initializes inside `with-temp`, since that
component initializes it.
Confirmed on the Mongo driver jobs: MongoDB Latest went from failing to green.
* Update the test-user session note now that initialization is prewarmed
* Ratchet config-level kondo suppressions too
:config-counts in ratchets.edn tracks, per linter, the :off switches and
:exclude entries in .clj-kondo/config.edn (top-level and per ns-group).
Only weakening counts: adding discouragements or turning a linter on is
free. Lowering works like the inline budgets; raising is a hand edit.
* Count every excluded item and :config-in-comment in the config ratchet
Map-shaped :exclude values with vector entries counted as one suppression
({compojure.core [GET DELETE POST PUT]} is 4, not 1), and the
:config-in-comment :off switch wasn't aggregated at all, so both could
grow without tripping CI. Reseeded.
* Count :config-in-call suppressions, and self-heal on config.edn
`:config-in-call` is a scoped `:linters` map like `:config-in-ns`, so a
linter switched off inside one was invisible to the ratchet. Both scopes
now fold into the same count.
The self-heal workflow only watched Clojure sources, which was right while
budgets came from inline ignores alone. Now that `.clj-kondo/config.edn`
feeds `:config-counts`, removing a suppression there can leave a budget
stale with no way to trigger the fix.
* Count scoped :off suppressions at any nesting depth
Linter scopes nest more than one level: :discouraged-java-method keys by
class and then by method. Counting only the first level meant an :off one
map further down waived a warning the ratchet never saw. No budget moves --
the repo's only two-deep entry is a :level :error discouragement.
* Regenerate kondo ignore budgets after restacking onto master
* Fix flaky document-comment split test by using realClick on hover-revealed button
The test clicked the hover-revealed "new thread" comment button of the
second paragraph via Cypress's synthetic .click(). After realHover() moved
the cursor to reveal the button, the synthetic click could act on the button
while its hover transition was unwinding, so the click did not reliably
trigger the route navigation that opens the comments sidebar — leaving
getSidebar() to time out.
Switch to .realClick(), which moves the real mouse onto the button (keeping
it visible/stable) and dispatches a real click that reliably fires the link.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Wait for document save to persist before opening comments sidebar
The split-paragraph test opened the comments sidebar immediately after the
Save button disappeared, but a successful save schedules a deferred
navigation back to /document/:id. Under network throttling that navigation
resolved after the sidebar route had opened, clobbering it, so the sidebar
never mounted and getSidebar timed out. Wait on the PUT request and the
"Document saved" toast so the post-save navigation settles first.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Google SSO already requires the id-token email to be verified; the
generic OIDC provider (and slack-connect, which derives from it) did
not look at the claim at all.
Tokens with email_verified present and not true are now rejected. An
absent claim is still accepted: it is optional per OIDC Core and some
IdPs never send it.
* Fix: Support Google Cloud Universe Domain in BigQuery driver
* feat(bigquery): Add universe domain to connection properties UI
* feat(bigquery): improve universe domain helper text
* Update bigquery_cloud_sdk.clj
Indeed we are out of sync as the impersonating was removed from master branch.
* removed empty comment
* Fixing formatting
Wait for the question move PUT to settle before navigating to the
affected dashboard, so its empty state is asserted only after the
dashcard has actually been removed.
* ensure highlighted bars are included in brush drill filter
* fix e2e
* fix e2e again, December 2026 label was not appearing maybe due to a screen size flake
The 'adding non-series columns to the tooltip' scatter test hovered a
bubble immediately after render. The ExplicitSize throttle re-renders the
chart once its container is measured, moving bubbles out from under the
realHover() before the tooltip fires, so [data-testid=echarts-tooltip]
was intermittently never found (4000ms timeout).
Apply the same view-toggle stabilization the sibling tooltip test already
uses (extracted as stabilizeScatterChart) before each hover, forcing a
re-render against an already-measured container.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Require a justification comment above inline ignores, ratcheted per linter
An ignore of a linter outside :comment-exempt needs a substantive ;;
comment on the line above or trailing on the same line (bare ;; dividers
don't count, and no attempt is made to check the comment is about the
ignore). ~18% of ignores have one today, so 35 linters start exempt.
The set only shrinks: once a linter's last uncommented ignore gains a
comment, the fixer drops its exemption and CI holds the line.
* Locate trailing justification comments in masked source
A semicolon inside a string or character literal on the ignore's line
counted as a trailing comment, so an unjustified ignore could pass.
The masker now keeps the ; that starts a comment (and blanks the char
after a backslash), so justified? finds only real comment starts.
* Share one tree scan across the ratchet tests
Each ratchet test walked and slurped the whole source tree; a delay
runs the scan once for all three.
* Invalidate the shared tree scan per test run
The namespace-level delay cached the scan for the JVM's lifetime, so a
long-lived REPL compared fresh ratchet-file contents with stale counts.
The :once fixture now resets the cache; a directly-run test var scans
fresh.
* Scope the shared tree scan to the fixture's lifetime
The atom cache survived a run, so a later directly-invoked test var
(which skips :once fixtures) reused a stale scan. The cache now holds a
delay only while the fixture is live; without it every call scans fresh.
* Require canonical syntax for kondo ignores
Keep ignore detection deliberately lexical and fail closed: every real marker must use the key-first map form, otherwise the ratchet rejects it with an actionable error. This avoids maintaining a partial Clojure reader for the two attr maps that previously placed the ignore behind another key.
Justification comments must be substantive and either directly above the ignore or trailing on its line. Authenticate comment openers in masked source so strings and code-line comments cannot pose as justifications.
* Harden kondo ignore scanning edge cases
* Use reader delimiters for kondo marker boundaries
* Match Clojure reader whitespace in kondo markers
* Reserve clj-kondo namespaced map syntax
* Harden kondo scanner diagnostics
* Rebuild the budgets and exemptions on the named-linter base
Rebased onto the ignore-naming branch, so every budget is recomputed from
the tree and :comment-exempt is reseeded with exactly the linters that
still have an uncommented ignore: 38, up from 35. The three extra are
linters that were hidden inside bare `#_:clj-kondo/ignore` forms until
they were spelled out; :all itself drops out, having no ignores left.
* Point at :comment-exempt after inserting ignores for a new linter
kondo-insert-ignores writes comment-less ignores, which the justification
ratchet rejects for non-exempt linters.
* Leave ignores preceded by a # for a human, and gate the exemption advice
A `#` immediately before the matched form is ambiguous: in `#^{...}` it is
legacy metadata that removal must take with it, in `x#^{...}` it closes a
syntax-quote gensym that removal must leave alone. Telling them apart means
reading Clojure, which this scanner deliberately does not do -- so report
the site under :skipped, as it already does for prefixless maps and
unbalanced braces, and let a human excise it.
Separately, kondo-insert-ignores no longer advises a :comment-exempt entry
unconditionally. An inserted ignore lands under whatever comment already sat
above the flagged form, so a linter whose sites are all justified would get
an exemption that no-stale-exemptions-test then rejects.
* Align the scan-test expectations
The occurrence vector's :justified? column and the counts map both drifted
out of alignment as linters were added to the fixture.
* Regenerate kondo ignore budgets after restacking onto master
* Make every kondo ignore name the linters it suppresses
The vector-less `#_:clj-kondo/ignore` suppresses every linter on the next
form, so a site added to silence one warning quietly absorbs any other that
appears there later. Replace all 50 with explicit linter vectors.
The budgets absorb what the bare forms were hiding: :discouraged-var
106 -> 129, :unresolved-namespace 3 -> 9, :type-mismatch 2 -> 6,
:unresolved-symbol 4 -> 9, plus first entries for :consistent-alias,
:duplicate-require and :redundant-do. :all drops out entirely, so a new
bare ignore now fails the ratchet with no budget to land in.
* Use the mdb.env alias in app-db-as-data-warehouse
Var-quotes resolve namespace aliases, private vars included, so the
fully-qualified name was never needed. Dropping it retires the
:aliased-namespace-symbol ignore and the unused-alias warning the
workaround caused.
* Regenerate kondo ignore budgets after restacking onto master
* Add primary/secondary NavLink variants and sibling components (GDGT-2469)
Restyle the design-system NavLink from Figma:
- Add `primary` (brand-fill selected) and `secondary` (subtle gray selected)
variants, with Default/Hover/Pressed/Selected states.
- Add the `icon-{primary,secondary,disabled,brand}-inverse` color tokens.
- Add `NavLinkButton` and `NavLinkBadge` sibling components for the right
section; both invert automatically on a selected `primary` item (no prop),
the button also has its own hover/press and stops click propagation.
- Give NavLink a `leftIcon` prop so it renders and colors the leading icon
itself instead of styling the section slot.
- Add a two-variant state-matrix Storybook showcase (Loki-covered).
* Migrate NavLink call sites to primary/secondary variants (GDGT-2469)
- Admin nav: `admin-nav` -> `secondary`
- Entity picker: current level -> `primary`, other levels -> `secondary`;
search results -> `primary`
- Remove the now-unused `default`/`mb-light`/`admin-nav` variant CSS
* Revert NavLink `leftIcon` prop back to `leftSection` (GDGT-2469)
Drop the polymorphic NavLink wrapper and colour the leading icon via the
`.NavLinkSection[data-position="left"]` CSS overrides again.
* Update Loki Snapshots
* Cleanup
* Update Loki Snapshots
* Add Loki reference for the NavLink Overview story (GDGT-2469)
* Update Loki Snapshots
* Address NavLink review comments (GDGT-2469)
- Migrate the remaining `default` NavLink (embedding SimpleDataPicker) to
`primary`, restoring its selected styling
- Render NavLinkButton as a `<div role="button">` (valid inside a NavLink's
`<a>`) with keyboard activation, instead of a `<button>`
* Update Loki Snapshots
---------
Co-authored-by: Metabase Automation <github-automation@metabase.com>
For tests of the config itself, use `./bin/test-agent :only '[metabase.core.modules-test]'`. After editing Clojure files, run `clj-paren-repair`. After editing `config.edn`, re-run the modules-test.
For tests of the config itself, use `./bin/mage project-tests modules`. After editing Clojure files, run `clj-paren-repair`. After editing `config.edn`, re-run the modules-test.
**Update your agent memory** as you discover idioms: which modules use `:friends` and why, which models are most-imported (suggesting they belong in their own module), which settings are accessed cross-module via `(setting/get …)` instead of `:uses` deps, and which refactors the team is actively running.
For a single require entry, hang it as metadata on that entry:
@@ -320,7 +321,12 @@ For a single require entry, hang it as metadata on that entry:
[metabase.query-processor.store:asqp.store])
```
Common Metabase-specific rules you'll see: `:metabase/modules`, `:metabase/validate-defendpoint-route-uses-kebab-case`, `:metabase/validate-defendpoint-has-response-schema`, `:metabase/prefer-with-dynamic-fn-redefs`, `:deprecated-namespace`, `:discouraged-namespace`. Don't use the keyword form (`#_:clj-kondo/ignore`); always the map form so it's grep-able by which rule(s) are suppressed.
Use the map form, not the keyword form (`#_:clj-kondo/ignore`), so searches can identify the suppressed
rules. Keep `:clj-kondo/ignore` as the map's first key; the ratchet scanner rejects other orderings.
Fix the warning when possible. Adding a suppression is a last resort and requires approval. If one is
necessary, add an explanatory `;;` comment directly above it or at the end of the same line. CI enforces
these requirements with `./bin/mage kondo-ratchets`.
@@ -37,14 +37,16 @@ First check if snapshots exist:
ls e2e/snapshots/default.sql 2>/dev/null &&echo"snapshots exist"||echo"snapshots missing"
```
Always pass the spec path via `--spec`. The runner feeds its arguments to Cypress's CLI parser (`cypress.cli.parseRunArguments`), which silently drops bare positional paths and then runs the entire suite. A leading `run` argument is harmless, but the path itself must follow `--spec`. Sanity-check that the first `Running:` line says `(1 of 1)`.
If snapshots exist, skip regeneration for speed:
```bash
MB_EDITION=oss CYPRESS_VIDEO=falseCYPRESS_RETRIES=0CYPRESS_GUI=falseGENERATE_SNAPSHOTS=false bun test-cypress $ARGUMENTS
MB_EDITION=oss CYPRESS_VIDEO=falseCYPRESS_RETRIES=0CYPRESS_GUI=falseGENERATE_SNAPSHOTS=false bun test-cypress --spec path/to/spec.cy.spec.js
```
If snapshots are missing (first run), let the runner generate them:
```bash
MB_EDITION=oss CYPRESS_VIDEO=falseCYPRESS_RETRIES=0CYPRESS_GUI=false bun test-cypress $ARGUMENTS
MB_EDITION=oss CYPRESS_VIDEO=falseCYPRESS_RETRIES=0CYPRESS_GUI=false bun test-cypress --spec path/to/spec.cy.spec.js
```
When running enterprise tests, replace `MB_EDITION=oss` with `MB_EDITION=ee` in the commands above.
@@ -67,9 +69,11 @@ GREP="test name here" MB_EDITION=oss bun test-cypress --spec path/to/spec.js
(The stress script forwards its arguments to `bun test-cypress`, so the same `--spec` rule applies.)
Set `E2E_STRESS_RUNS=N` for more iterations (default: 5). Stops on first failure and prints screenshot paths. Read those screenshots to analyze the failure.
@@ -31,6 +31,11 @@ Phases 1, 2, 5 and 6 are shared. The middle two differ in **order**: update mode
- **The component's intended API** — variants, sizes, and states the design system supports for this component. This defines the axes of your showcase (Phase 3).
3. Decide the mode (see **Two modes** above): search for an existing component — in `metabase/ui` or as a legacy component elsewhere. Found → **update**; nothing equivalent anywhere → **create**. Confirm with the user when unsure.
4.**Verify a Figma desktop MCP is connected before doing the work** — this skill depends on it for exact tokens in Phase 4 (and styling can't proceed accurately without it). Do a quick read against the issue's node (e.g. `get_metadata`/`get_variable_defs`); if it errors with "nothing selected" or the server isn't connected, prompt the user to enable it (Switch to Dev Mode → MCP section in the right sidebar) and select the relevant layer before continuing.
5.**Figma Desktop MCP environments only:** open the issue's exact page with [`figma-open`](./figma-open) before MCP reads. Do not run this helper when Desktop MCP is unavailable.
A URL with `node-id` needs no token. Bare page-name and `--list` lookups require an already-configured `FIGMA_TOKEN`.
## Phase 2 — Understand the component
@@ -205,7 +210,7 @@ Per `docs/developers-guide/frontend.md` ("Colors"): every color must be a **sema
The same discipline applies to **dimensional** values — radius, spacing/padding/gap, elevation/shadow, and typography (font size, line-height, weight). Prefer the codebase's existing scale variable over a literal: `--mantine-radius-*`, `--mantine-spacing-*`, `--mantine-shadow-*`, and the Mantine font tokens.
But Figma's scales don't always line up with the codebase's. When a Figma value **has no matching step** in the corresponding scale — e.g. Figma `radius/md = 12px` while `--mantine-radius-md` is `8px`, or a Figma `xxxs` spacing step with no Mantine equivalent — **don't silently bake in a literal.** Flag it to the user the same way you flag primitive colors: name the property, the Figma value, and the nearest codebase token, so it can be resolved with design / the scale owners (align the value, or add a scale step). Only fall back to a literal if the user confirms it's an accepted one-off, and leave a short comment in the code explaining why.
But Figma's scales don't always line up with the codebase's. When a Figma value **has no matching step** in the corresponding scale — e.g. Figma `radius/md = 16px` while `--mantine-radius-md` is `0.75rem`, or a Figma spacing step that has no Mantine equivalent — **don't silently bake in a literal.** Flag it to the user the same way you flag primitive colors: name the property, the Figma value, and the nearest codebase token, so it can be resolved with design / the scale owners (align the value, or add a scale step). Only fall back to a literal if the user confirms it's an accepted one-off, and leave a short comment in the code explaining why. The codebase scales are defined in `frontend/src/metabase/ui/theme.ts` (`SPACING_SCALE`, `RADIUS_SCALE`, `LIGHT_SHADOWS`/`DARK_SHADOWS`).
### Mantine implementation notes (reference)
@@ -240,5 +245,6 @@ Only after sign-off:
- [ ] Storybook showcase matrix built from the component's full state space (theme excluded — global toggle drives light/dark), single panel, hover/pressed forced via `storybook-addon-pseudo-states`, controls scoped per story; lint + type-check pass. (Update mode: built before styling, **component styles untouched**. Create mode: built after the component is implemented in Phase 4.)
- [ ] Figma desktop MCP available; exact tokens extracted and mapped to semantic `--mb-color-*` (NO `color-mix`, NO primitives) and to existing scale vars for radius/spacing/elevation/type — any color *or* dimensional value with no matching token/scale step flagged to the user, not baked in as a literal.
@@ -17,10 +17,10 @@ Adherence to `typescript-write` is the **highest-priority** review dimension: ra
Review in this priority order:
1.**Violations of [`typescript-write`](../typescript-write/SKILL.md) provisions** — no-`any`, type tightening, type modeling, null/undefined handling, naming, structure, comments. Highest priority; block on the no-`any` rule.
1.**Violations of [`typescript-write`](../typescript-write/SKILL.md) provisions** — no-`any`, type tightening, type modeling, function signatures, null/undefined handling, naming, structure, comments. Highest priority; block on the no-`any` rule. Apply conditional guidance in context: explain the unsupported type guarantee or concrete readability problem, rather than treating every preference as a blanket ban.
2. Compliance with `frontend/CLAUDE.md`.
3. Readability and maintainability.
4. Appropriate test coverage.
4. Appropriate test coverage. For internal typed callers, avoid requesting tests solely for inputs the type system excludes. External API data, deserialised values, storage and JavaScript callers can violate annotations: test runtime validation and nontrivial assumptions at those boundaries. Types do not replace behavioural, security or data-integrity tests.
## Blind spots — act as the missing reviewer
@@ -31,3 +31,4 @@ These rarely surface in team reviews, so this skill should raise them. They are
- **Security.** Evaluate potential security issues in new code.
- **Bundle size.** Flag new large dependencies, default imports from icon or util libs, and heavy modules imported at route-load time.
- **Analytics.** User-facing flows should emit tracking events. If a PR adds a new flow (button, modal, navigation) without a tracking event, ask whether one is expected.
- **Public API surface** (embedding SDK). Consumers should be able to use public signatures and name types they need to import. Export those types deliberately and document public behaviour, including `@deprecated` for deprecated APIs; a referenced structural type does not automatically need its own named export.
@@ -20,20 +20,27 @@ description: Write TypeScript and JavaScript code following Metabase coding stan
- **Avoid type casts and loose `unknown`** — fix the signature instead.
- **If a function only needs one field of a wide object, accept that field** — not the wide object. The cast often disappears once the signature is right.
- **Reach for `Partial<T>`, `Pick<T, K>`, `Record<K, V>`, and generics** before reaching for a cast.
- **Match dictionary keys to the data.** Use a finite key union when the keys are known. Open dictionaries can use an index signature, `Record<string, T>`, or `Map`; index signatures still constrain values, and changing their spelling to `Record<string, T>` does not make missing-key access safe.
- **Prefer making props/components generic** (`<T>`) when a value flows through unchanged and the caller knows the type.
- **Prefer `unknown` over loose typing** and narrow before use — an `unknown` value forces a guard at the point of use.
- **`satisfies` for object literals** that must conform without widening (config objects, lookup maps, discriminated literals) — better than `: T` (widens) or `as T` (unsafe).
- **Avoid non-null assertions (`!`)**. Prefer a guard, early return, or `?.`. Use `!` only when non-nullness is provably true and localized, with a comment.
- **Guard indexed lookups that may miss.** Arrays and dictionaries can return `undefined` even when the inferred type omits it. Use an iteration form that preserves the key/value relationship, and only assert `keyof` when the runtime keys are known to belong to the declared type.
- **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.
- **Keep unavoidable assertions local.** Isolate a repeated or complex assertion behind a helper when that makes its invariant easier to enforce. Test nontrivial runtime assumptions that justify it; a trivial assertion does not automatically need a new helper or test. Do not weaken a public signature just to silence implementation errors.
## Type modeling
- **Reuse existing types; don't re-declare them.** Use canonical IDs and domain entity types from `metabase-types/api` (`FieldId`, `TableId`, `ConcreteTableId`, `SchemaName`, …) and key data structures by them (`new Map<ConcreteTableId, …>()`). Don't duplicate generated/API types — compose or derive (`Pick`, `Omit`, indexed access `SomeType["field"]`, `ReturnType`).
- **Use generics to allow TypeScript to infer correct types** when creating functions and components that need to be reusable and type-safe. Don't hesitate to introduce complex generics if they allow to derive types automatically instead of manual narrowing.
- **Generics must make promises the implementation can keep.** A caller-selected `get<T>(): T` must not disguise an unchecked assertion about external data. Return `unknown` and validate, or accept a validator that establishes `T`. A factory such as `function empty<T>(): T[] { return []; }` is valid; judge the implementation, not how often `T` appears in the signature.
- **Prefer an honest type over false precision.** Use generics when they express a real relationship. If a complex type cannot model the behaviour accurately, choose a simpler type or `unknown` with narrowing instead of asserting an unsupported guarantee.
- **Model the actual data contract; keep types narrow.** Optional `field?: T` for a key that may be absent, `field: T | undefined` only when the key is always present but the value may be undefined, `| null` for explicit API nulls. Prefer domain unions over broad `string` / `number` / loose `Record`.
- **Refer to API implementation** when defining or refining types to ensure they match the actual data structure. When considering a type cast, first consider if the type should be refined to match the actual data structure.
- **Optionality must reflect absence.** Keep required fields required and optional fields optional. Normalise input when the application has a meaningful default, not merely to remove a type error. Preserve the actual wire shape in API types.
- **Represent related absence together when modelling internal state.** If several fields exist or disappear together, consider a nullable containing object or a discriminated union. Do not reshape a raw API declaration unless the data actually has that shape.
- **Prefer explicit special states when designing a format.** Use nullability or named union variants when sentinel values such as `-1` hide meaning. Preserve established protocol sentinel values unless the behaviour is deliberately changed, or normalise them at an explicit boundary.
- **Discriminated unions for variant state, with exhaustive checks.** Model "one of N shapes" as a union with a literal discriminant rather than a bag of optional fields, and exhaust it with ts-pattern's `.exhaustive()` so adding a variant becomes a compile error:
```ts
import { match } from "ts-pattern";
@@ -45,20 +52,30 @@ description: Write TypeScript and JavaScript code following Metabase coding stan
.exhaustive(); // Compile-time guarantee all cases handled
```
- **Derive union types from constants** (`as const` + `typeof`/`keyof`) so the type and the values can't drift.
- **`readonly` / immutability where mutation isn't intended** — component props, shared constants, exported config. Prefer `readonly T[]` / `ReadonlyArray<T>` for inputs you don't mutate.
- **`readonly` / immutability where mutation isn't intended** — component props, shared constants, exported config, and unmutated parameters. Prefer `readonly T[]` / `ReadonlyArray<T>` for inputs you don't mutate. Functions that mutate caller-owned data should make that behaviour explicit.
- **Construct complete, well-typed objects where practical.** Prefer an object expression when it avoids partially initialised state or assertions. Incremental construction is fine when it is clearer and maintains the type's invariants.
- **Treat `metabase-lib` opaque types as black boxes.** Use `metabase-lib` functions to work with types such as `Lib.Query`, rather than casting into their internal representation.
- **Type async and error states explicitly** (a discriminated union or the data-layer's typed result) — never leave loading/error/empty implicit.
## Function signatures
- **Make public contracts explicit where it improves stability and clarity.** Annotate parameters and return types at shared boundaries when useful; let local values infer. Use `satisfies` or an annotation when a declaration needs an explicit shape check.
- **Accept the inputs the operation supports and return the most precise honest result.** Narrow avoidable uncertainty inside the function, but preserve meaningful nullability and union variants in its return type.
- **Use named options when positional arguments are easy to confuse.** Consecutive arguments with the same type are a useful warning sign, not an automatic requirement to rewrite a clear API.
- **Use `async`/`await` when it clarifies control flow or error handling.** Returning an existing promise directly is also valid; a Promise return type alone does not require adding `async`.
## Null and undefined
- **Narrow at the source**. If a value is optional only in a corner case, don't thread `undefined` through every layer — guard at the producer.
- **Sensible defaults for optional values**. Use `?.` and `??` at the consumer.
- **Lists should be filtered** before being used in a map or other iteration.
- **Narrow nullable list elements when the operation requires present values.** Filter with a type guard when missing entries should be discarded; preserve them when their absence or position carries meaning.
- **Avoid non-strict null comparisons** (`X != null`) when `X` can never be `null` — use a strict check or narrow the type. Use `checkNotNull` where necessary.
- **Check actual nullability against API implementation**. Find the API endpoint implementation and check if the field can actually be null.
## Naming
- **Names describe the entity, not the mechanism**. A name must reflect what the value holds.
- **Use domain vocabulary and include units where useful** (`timeoutMs`, `widthPx`, `temperatureC`). Prefer a more specific name when `Info`, `Data`, or `Entity` obscures the meaning, while keeping established terminology when it is clear.
- **Align sibling concepts**: keep verb conventions consistent across a related API.
- **No names that encode implementation history** rather than current meaning. Suffixes like `Base`, `New`, `Old`, `Initial` need a real semantic distinction, otherwise drop them.
- **Avoid cryptic identifiers** (`v`, `n`, `$n`) for domain values; short names are fine only in tiny conventional contexts (loop index `i`, coordinates `x`/`y`, generic params `T`/`K`/`V`).
printf'%s\n'"printf 'Resolve conflicts and force push this branch.\\n\\nTo backport translations run: bin/i18n/merge-translations <release-branch>\\n'"
) 2>&1|whileIFS=read -r line ||[ -n "$line"];doprintf'[p%s] %s\n'"$idx""$line";done
rc="${PIPESTATUS[0]}"
echo"$rc" > "$STATUS_DIR/p$idx"
}
rm -rf "$JVM_DIRS_ROOT"
mkdir -p "$STATUS_DIR"
echo"Runner $TEST_RUNNER_INDEX of $TEST_RUNNER_COUNT: running $TEST_JVMS_PER_RUNNER concurrent JVMs for partition(s) ${INDEXES[*]} of $PARTITION_TOTAL (-Xmx$TEST_HEAP_PER_JVM each)"
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.