Compare commits

...

120 Commits

Author SHA1 Message Date
Kyle Doherty
16bbf677a5 Rename nav sections, flatten Library, move Metabot into the page header
- Nav section switcher tabs are now "Library" (repository icon) and
  "Playground" (beaker icon); browse page copy updated to match.
- Library rail content drops its collapsible heading; folders render directly
  (section keeps its "Library" accessible name).
- Metabot button moves from the Playground rail into the app page header,
  between search and the app switcher (hidden in iframes), and reserves its
  32px slot while permissions load so the header controls don't shift.
2026-09-28 15:36:15 -04:00
Kyle Doherty
e1d1d31a1a Polish main-app metric page header and app page header
- App header breadcrumbs render without icons (CollectionBreadcrumbs showIcons),
  match the page's gutter/background via a published page frame
  (setPageFrame replaces setPageBackground), and align vertically with the
  nav rail's Official/Unofficial switcher.
- Metric header: no metric icon; main-app about page title uses h1 size
  (PaneHeader titles gain a size option).
- Main-app metric pages render title and description read-only;
  Data Studio metric pages opt into inline editing (isInlineEditable).
2026-09-28 13:31:54 -04:00
Kyle Doherty
7b91252075 Trim official metric page and drop remote-sync collection styling
Metric page (main app):
- Pills under the chart reuse the Breakdowns logic: first 4 dimensions,
  "Show more" adds 4, curated default stays visible. Breakdowns panel removed.
- Definition panel collapsed by default (DetailPanel gains collapsible /
  defaultCollapsed).
- Dimensions and History panels shown only in Data Studio
  (showManagementPanels).

Remote sync:
- Collections no longer swap to the synced_collection icon; they show their
  own icon (CollectionCaption now honors collection.icon). Removes
  isSyncedCollection plugin, REMOTE_SYNC_COLLECTION, and the isTenantUser
  icon plumbing.
- Removes unsynced-changes dots from all nav collection trees, including the
  CollectionsNavTree plugin and isCollectionDirty. Data Studio tab badges kept.
2026-09-28 13:11:34 -04:00
Kyle Doherty
8dfb8c5b67 Merge remote-tracking branch 'origin/master' into nav-split-prototype
# Conflicts:
#	frontend/src/metabase/browse/models/BrowseModels.tsx
#	frontend/src/metabase/search/components/DropdownSidebarFilter/DropdownSidebarFilter.tsx
2026-09-28 11:09:02 -04:00
Eric Normand
e8f85e28e0 GHY-3274: Initial sync should not attempt to create field values (#82963)
* GHY-3274: Skip the FieldValues scan on the initial sync of a Database

FieldValues are only created on demand, by the /values endpoints. A new Database has none. But the initial sync (sync-database!, which the database-create event runs) still ran the field-values phase. That phase did per-Field app-DB work for every Field: a count and a delete of expired advanced FieldValues, and an exists check before clearing each non-list Field. On a new Database none of that work can find anything.

sync-database! now reads the initial_sync_status of the Database from the app DB when it starts. When the initial sync is not complete, a :full sync runs the metadata and analyze phases and skips the field-values phase. The status comes from the app DB and not from the map that the caller passes, because the metadata phase marks the Database complete, and callers can hold a map from before the first sync. Later syncs, and the scheduled field-values task, run the scan as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* GHY-3274: Read initial_sync_status from the database map, not the app DB

sync-database! fetched the Database from the app DB to read its initial_sync_status. Production callers do not need that: the database-create event and advanced-config pass the Database they just created, the sample-data code creates or selects the Database first, and the notify API loads it per request. Only tests passed a map from before the first sync to a later sync. So sync-database! now reads the status from its argument, and saves one app-DB query per sync.

Three places in the tests re-synced with a stale with-temp map and expected the later sync to refresh FieldValues: one-off-dbs/insert-rows-and-sync!, scanning-test, and sync-and-find-values! in the warehouse-schema FieldValues tests. They now select the Database from the app DB before each later sync.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* GHY-3274: Stub the unique task list in the task filtering e2e test

The "filtering should work" test in tools.cy.spec.ts opens the task list with task=field values scanning, and checks that the "Filter by task" picker shows that value. The picker is a Select whose options come from GET /api/task/unique-tasks, and a Select shows an empty input when its value is not an option. The test stubbed GET /api/task but not unique-tasks, so it depended on real task history.

That real task history had a "field values scanning" row only because the initial sync of the Sample Database ran the FieldValues scan. This branch skips that scan on an initial sync, so the name is no longer in the list, and the picker was empty. The test now stubs unique-tasks with the task it filters on, in the same way that it already stubs the task rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:02:54 +00:00
Denis Berezin
63cbcc92e3 Render the move-questions-into-dashboards modal without a second overlay (#83023) 2026-09-28 19:46:21 +05:00
Denis Berezin
16ad7b75f2 Keep query column order for new table columns (#82476) (#82972)
* Keep query column order for new table columns (#82476)

* Fix CI
2026-09-28 19:45:55 +05:00
Nemanja Glumac
eedda27c89 Cover Mongo convert-to-native on a unit test level (#83011)
* Move Mongo convert-to-native coverage out of e2e

The Mongo e2e test asserted on a specific product row, but the converted
pipeline has no $sort, so row order depends on the Mongo container's
natural order and the row often fell outside the rendered grid.

Replace it with backend tests (no default $limit, nested query over a
converted native card runs, re-converts with its collection and without
Bson wrappers, and the re-converted query runs) and Jest tests for the
Mongo-specific labels and the collection handoff on convert.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Keep the positive collection case next to the negative one

The "no collection" case only proves something if the same fixture can
produce a collection; without the database feature flag, native extras
are silently dropped and the negative assertion passes vacuously.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 16:44:17 +02:00
Nemanja Glumac
1a0ff4340b Replace flaky metric-aggregation e2e test with a unit test (#82964)
* Replace flaky metric-aggregation e2e test with a unit test

The CodeMirror value() e2e helper read the editor text once, so a read
that landed before the initial format settled failed without retrying.
It is now a Cypress query that re-reads on every assertion retry.

The notebook e2e test is replaced by an AggregateStep unit test. Writing
it exposed a blur during the initial format overwriting the formatted
source with the stale empty one; blur is now ignored while formatting.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Read CodeMirror text through invoke queries instead of a custom command

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:32:38 +02:00
Nemanja Glumac
0bdad48e91 Command palette e2e spec simplification (#82905)
* Move command palette shortcut coverage down to RTL

Split the shortcuts monolith into a navigation test and a focus-suppression
test, and drop the e2e assertions a unit test can make deterministically:
shortcut modal toggling, c f / c d, the [ navbar toggle, admin tab digits,
the mobile search button, palette action presence, and the snowplow counts.

New unit specs drive the real shortcuts through kbar and assert the router,
the redux modal state and the tracking call. Admin settings links per role
were already covered by the PaletteResults suite, so those two e2e tests go
away as duplicates.

AdminNavbar's spec binds digits against a fixture tab list; the real list's
order, which those digits index into, is pinned next to getAdminPaths.

Dropped Meta+[ outright: toggle-navbar binds "[", and the modifier trigger
is $mod+. handled outside kbar, so the press matched nothing and the
assertion after it passed on the sidebar's pre-existing state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fold the documents-in-recents palette test into the main one

The test was kept separate because it needed the bleeding-edge token, which
enabled enough extra features to disturb the main palette test. Documents
moved to OSS: the token activation and the /api/ee/document endpoint went
with it, so the separation costs a cold app boot and buys nothing.

The document is created before the dashboard so the dashboard stays the most
recent entry and the existing selection assertion holds.

Also pin the personal-collection name on the read path in Clojure, which is
the one thing the e2e assertion covers that no backend test did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Fold the API-order palette test into the main one

Both tests are "real search results render correctly in the palette" and
each paid its own cold app boot. The order check moves into the query block
of the main test, right after the other search assertions.

The searchData alias is registered immediately before its query rather than
at the top of the test: cy.get yields the last matching request, and the
earlier searches in the merged test would shadow it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Anchor the shortcut navigation test on rendered pages

cy.location resolves when a route transition starts, not when it finishes, so
chaining chords on URL assertions alone lets the next chord land mid-mount
and be dropped. Each hop now waits for content on the destination page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Sync the dashboard bookmark shortcut on its API writes

The o shortcut decides between add and remove from the refetched bookmark
list, so pressing it twice in a row can toggle against stale state and leave
the dashboard bookmarked. Each press now waits for its write, and the
positive assertion between them proves the list refetched before the second.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Set up the palette permissions test through the API

The test needs a non-admin with settings, monitoring, table-metadata and
database-management access; how those grants get made is not what it covers.
Driving the two permission graphs directly replaces a cold boot into the
admin permissions screen, ten UI interactions and two save round-trips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Wait for the bookmark toggle before the dashboard shortcut

visitDashboard resolves on the dashcard query responses, not on render, so
the o keystroke could land before DashboardBookmark mounted and registered
the shortcut — no request, no toggle. Anchor on the button that component
renders, which is the same thing that owns the shortcut.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Wait for the app bar before opening the palette after sign-in

The palette registers its toggle only once its effect sees the logged-in
user, and the home greeting renders independently of the app bar, so the
ctrl+k could land while the palette was still disabled and be swallowed.
Anchor on the search button, which the same shell renders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Tighten restricted-shortcut and admin-path filtering tests

Assert the pathname stays put right after a restricted shortcut, and cover
the admin paths reducer dropping paths for a non-admin.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Mount the admin app reducer at its typed store path in tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 15:31:35 +02:00
Nemanja Glumac
9b193b0937 Replace flaky help text docs link e2e test with a unit test (#82954)
* Replace flaky help text docs link e2e test with a unit test

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Assert the help text docs link click is not cancelled

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:14:41 +02:00
Nemanja Glumac
1139be54dd Replace upload-permission e2e tests with unit coverage (#82973)
* Replace upload-permission e2e tests with unit coverage

The "permissions" e2e block restored the postgres-12 snapshot only to
assert upload button visibility, which is covered by backend
can_upload tests and CollectionHeader unit tests. Add the missing
unit case: button hidden when uploads are enabled but canUpload is
false.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Add a missing backend coverage

* Make upload-permission unit tests assert the right cause

The sandboxed can_upload test enabled uploads on the original database
while with-gtaps-for-user! queries a temporary copy, so can_upload was
false because uploads were disabled. Enable uploads inside the gtaps
block and sanity-check with an admin on the same database.

Add CollectionContent cases driving the upload button from the
database's can_upload response.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:13:18 +02:00
Nemanja Glumac
32176e152a Keep the Google Drive modal closed until the connection status loads (#83009)
While the connection query was loading, its missing status was read as
"not-connected", so connected admins briefly saw the connect modal. The
modal now renders nothing until the status arrives.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:12:16 +02:00
Chris Vandoodewaard
1d5c08b71d make number of versions we patch nightly configurable (#82766)
* make number of versions we patch nightly configurable

* if 0 should still patch current version
2026-09-28 08:51:21 -04:00
Anna Tomka
bc75a88888 GDGT-2458: Accordion (#82562)
* test(ui): add Accordion Storybook story and Loki filter

* feat(ui): restyle Accordion from Figma

* fix(Accordion): keep theme selectors one class deep so call-site overrides still win

* refactor: drop Accordion call-site overrides now covered by the theme

* test(ui): add Loki reference for Accordion Overview story

* fix(Accordion): hide the default chevron from assistive tech

* test: update DataComplexityCards snapshot for the Accordion separated default

* fix(RemoteSyncDependencyModal): drop overrides the new Accordion theme makes redundant, keep control transparent

* fix(RemoteSyncDependencyModal): let the Accordion theme space the rows

* fix(Accordion): keep icon grey on disabled hover, honour the radius prop

* test(ui): add disabled hover row to the Accordion Overview story

* Update Loki Snapshots

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-09-28 10:20:07 +00:00
Voytek Pituła
cd6bcc23e0 [BOT-2138] Centralize shared logic of metabot LLM provider adapters (#82165)
* Refactor metabot LLM provider adapters: centralize shared logic in `adapter.clj` while simplifying  implementations.

* Clarify error message for unsupported adapter models

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Rework auth, add some types

* drop span cusotmization

* drop extract and headers from CatalogOpts

* fix counts

* revert adapter/context-window-fn

* more tests for headers merging

* normalize-known-model fix

* doc fix

* more strict catalog name resolution

* simplify

* google coverage v1

* registry v1

* registry v2

* simplify

* align bedroc, drop dead ai-proxy?

* fix docstring

* improve tests

* rename listing to model-listing

* rename ai-proxy?

* rename keywords

* improve catalog error handling

* drop redundant content type

* move google auth inside request span

* route vllm's lsit-models through request!

* Update src/metabase/metabot/self/registry.clj

Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>

* remove history from docs and comments

* Update src/metabase/metabot/self/registry.clj

Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>

* fix var refs in optional/required

* refactor model context-window handling to use registry function

* migrate span attributes to nested :attributes map in adapter and update tests

* standardize function schemas with `mu/defn` and introduce `ResolvedRef` for adapter inputs

* remove redundant comment block in adapter test

* adjust schemas for closed maps enforcment

* reject unsupported AI proxy usage in Azure adapter and expand test coverage

* refactor registry-test to dynamically derive providers and improve adapter validation

* bring back old logging behavior

* stringify `:family` span attribute across adapters and update tests

* fix vllm test name and bedrock reference

* standardize `list-models` function schemas across adapters with `mu/defn` and update related tests

* add schema conformity test for `:supported-models` across adapters

* fix Auth schema

* Get back old tru errors

* Bake-in content-type header

* Expand `ModelListing` schema to include `learned-config` with additional closed map fields for connection reasoning.

* Remove `^:parallel` marker from test to disable parallel execution

* Update src/metabase/metabot/self/claude.clj

Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>

* Rename `:learned-config` to `:connection-info`

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
2026-09-28 11:07:48 +02:00
Fraser
381ab6c1a0 Small store batch: QueryModalType, metabot re-export (#79347)
Move QueryModalType and the metabot slice type out of shared redux

Co-authored-by: Romeo Van Snick <romeo@romeovansnick.be>
2026-09-28 21:31:09 +13:00
YB
176d0682e2 Document where action runs are logged (#82742)
document where action runs are logged
2026-09-28 13:18:46 +08:00
Fraser
daa0c256a6 Build Storybook with --test for Loki screenshot runs (#83003) 2026-09-28 11:16:08 +13:00
Romeo Van Snick
2df3f6daa0 Avoid modulejs warning in stylelint (#82988)
* Avoid modulejs warning in stylelint

* Fix eslint errors in stylelint plugin after .mjs rename

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 18:21:41 -04:00
Eric Normand
503f96cb55 GHY-4651: SendMeteringEvents does full table scan of query_execution (#83034)
GHY-4651: Let the metering query use the query_execution started_at index

SendMeteringEvents runs every 15 minutes and on every boot. It counts yesterday's query executions with WHERE CAST(started_at AS DATE) = CAST(? AS DATE). Casting the column hides it from idx_query_execution_started_at, so the app DB scans the whole query_execution table. On busy instances that table holds millions of rows.

The query now filters with a half-open range, started_at >= yesterday 00:00 UTC AND started_at < today 00:00 UTC. The DB can answer that range from the index. The db function becomes query-execution-statistics-between, and query-execution-last-utc-day computes the UTC day bounds. The docstring already said UTC day; the old cast used the DB session time zone instead.

The new test in token-check-test runs metering-stats, captures the SQL it sends for query_execution, runs EXPLAIN on the H2 app DB, and checks that the plan uses the started_at index. A second test pins the day boundaries: yesterday 00:00Z is in, today 00:00Z is out.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 16:09:44 -05:00
Andrei Simionescu
f2e185369f Always show the Metabot illustrations toggle (#82946)
* Always show the Metabot illustrations toggle
* Only mention the chat sidebar in the illustrations label and docs
* Test the illustrations toggle with the default icon
2026-09-25 21:30:29 +01:00
Eric Normand
9db952d568 GHY-4323: related-tables lookup does a permission query per table and an uncapped IN (#82962)
GHY-4323: Cap related-table ids before the lookup in get_fields

related-tables-by-requested-table collected every FK target of every requested table into one IN clause, then applied the 50-table cap to the result. A table with many FK columns made the lookup ask for tables that the response then dropped. The cap now applies to each table's targets before the lookup, so the IN holds at most 50 ids per requested table.

The :target hydration in the metadata fetch already drops unreadable targets, so only inactive or unbrowsable targets can now shrink the surfaced list below 50.

The other half of the ticket, one permission query per related table, is already fixed. Since 6908f26b76, field/readable-fields-only primes the table perms cache for every FK target while it hydrates :target, and the related tables are exactly those targets. The new API test covers it: as a non-admin with per-table grants and 60 related tables, the call succeeds under the N+1 guard, and it fails with that prime removed.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 13:18:51 -05:00
Chris Truter
9e27bdf31f Take host-part credentials off the pgvector URL (#83010)
* Take host-part credentials off the pgvector URL too

Credentials written before the host (user:pass@host) stayed on the URL.
pgjdbc reads that form as part of the hostname, so the password showed up
in its "unknown host" error and its DEBUG line.

- Move them into the credentials map, whatever the scheme, splitting at
  the host part's last @, with %-escapes decoded and + kept as is. Setting
  one both there and as a param fails at startup.
- Refuse any other unencoded @, which is what an unencoded / or ? in the
  password leaves behind, with a message that quotes none of the URL.
- Rethrow a malformed %-escape without the decoder's message, which quotes
  part of the value.

* Accept an unencoded @ in pgvector URL query values

pgjdbc decodes query values and gives @ no special meaning, so Azure's
documented user=name@server must keep working. Only a host part with no
path after it, the shape an unencoded ? in a password leaves, still
refuses an @ in the query.
2026-09-25 16:07:23 +00:00
Chris Truter
20f3f6cbdc Keep pgvector credentials out of logs (#82930) 2026-09-25 15:22:14 +00:00
Nemanja Glumac
2f3fe9904e Stop ErrorOverview recovery test from racing the search debounce (#83005)
Stop ErrorOverview recovery test from racing the search debounce

Fake timers skip the 500ms debounce so it no longer eats into findBy's
1s budget, and the lookup is scoped to the row so TreeTable's transient
off-screen measurement copy of the cell can't produce a duplicate match.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-25 12:14:18 +02:00
Nemanja Glumac
57a08becb3 Cover pivot subtotal date localization with a Jest test (#82947)
Pivot a month-bucketed date breakout under a German locale and assert the
subtotal label keeps the localized month. Formatting the value a second time
re-parses "Mai 2026" and yields "Januar 2026", which is the regression the
assertion pins.

Drop the Cypress reproduction: it rendered a pivot of Product Title x month x
User ID to check the same label, and was racing the render.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 08:25:36 +02:00
Andrei Chernykh
fc5cf5c469 Bump @snowplow/browser-tracker to 4.10.2 (#82955)
* Bump @snowplow/browser-tracker to 4.10.2

* Fix SDK analytics spec for batched Snowplow requests
2026-09-24 14:18:19 -07:00
bryan
c03c02a9a1 Lint values, not call style, and write the value-binding rubric down (#82686)
* Lint values, not call style, and write the value-binding rubric down

Two changes to the value-binding sweep (GHY-4477), both correcting guidance that
earlier namespace PRs were written against.

Stop asking for query maps. The linter's `lint-kv-args!` check asked every
`:column value` call to be rewritten as a query map, on the premise that a query
map is the shape the HugSQL extraction in step 2 reads off. That premise does not
hold. Both extraction spikes show it: the glossary spike (#82530) extracted
cleanly *from* kv-args, and the SSO spike (#82587) still needed substantial
rewrites *from* query maps -- a `SELECT 1 ... LIMIT 1` reshape and empty-collection
sentinels. Extraction is semantic. It turns conditional clauses into flags, picks
portable SQL, and discards the HoneySQL shape entirely (`h2x/like-substring`'s
`[:escape ...]` form becomes plain `ESCAPE '!'` text). A query map is not closer
to the target, just a different thing to throw away.

Converting also introduced a bug class. Toucan applies a model's `deftransforms`
`:in` fn only in kv-arg position -- it hooks `query/apply-kv-arg`, and a raw
`{:where ...}` map never passes through there (toucan2 calls HoneySQL "an outlet
to bypass type transforms"). So moving a keyword value into a where map drops the
transform, and HoneySQL formats a keyword in a value slot as an *identifier*:

    (t2/select :model/SearchIndexMetadata :engine :appdb)
    ;; => ["... WHERE ENGINE = ?"  "appdb"]          transform ran

    (t2/select :model/SearchIndexMetadata {:where [:= :engine :appdb]})
    ;; => ["... WHERE ENGINE = \"APPDB\""]           transform skipped, broken

That shipped twice on sweep branches before being caught (`sync/db.clj`'s
`:semantic_type :type/Name`, `collections/db.clj`'s `:type :api-key`). It compiles,
runs, and returns wrong rows. Nothing static can check the hand-written
post-transform value, so the check that produced these conversions is removed
rather than narrowed.

Lint kv-arg values instead. Removing the style check on its own would have left a
real gap: `value-nodes` only understood query-map clause shapes, so a value passed
as a kv-arg pair was never examined for a missing marker. `lint-unmarked-values!`
now walks kv-arg pairs too, starting after the `:model/...` keyword rather than at
a fixed offset (several fns take an argument before the model, e.g.
`(t2/select-one-fn :value :model/Setting :key k)`). Inside a kv-arg the column
comes from the pair key, so every argument of an operator form is a value --
`:id [:in ids]` checks `ids` -- unlike a query-map clause where the first argument
is the column.

Net effect on findings: call style is no longer reported; an unmarked value is
reported whichever style it was written in. Verified against
`settings/db.clj` (kv-arg style, opted in temporarily): both `setting-key` reads
are now flagged, and the `value` in `update!`'s changes map is correctly not.
`content_translation/db.clj`, the merged worked example, stays at zero.

Write the rubric down. docs/developers-guide/value-binding-rubric.md records every
decision the sweep needs, so the remaining namespace PRs stop re-litigating them in
review. Seven ordered rules, each with an example; the two that were previously
implicit are now explicit -- never mark a literal (it cannot carry a request value,
and in step 2 it becomes a HugSQL hole every caller fills with the same constant),
and leave a possibly-empty collection unmarked (Toucan rewrites `[:in col []]` to
`false` inside the step the marker wraps). It also records the `t2/update!` shapes
that work, what `value_guard` refuses for you, and the known limits the lint cannot
see -- so "zero findings" is replaced by "every finding explained by a rule".

* Frame the call-style rules as scaffolding that HugSQL extraction retires

A HugSQL query fn takes one map and has no override surface, so the
kv-arg-vs-query-map question -- and the transform-position trap that comes with
it -- does not exist for an extracted namespace. Say so, rather than leaving
these rules to read as permanent house style.

* See value slots inside conditionally built clauses

value-nodes stopped at a list node, so any value a when/if/cond-> built was
invisible to the unmarked-value check -- a false negative in a security lint,
and a common shape: permissions/db.clj and collections/db.clj both assemble
where clauses that way.

    (t2/select :model/Setting {:where [:and (when k [:= :key k]) [:= :o o]]})
    ;; o was flagged, k was not

Walking list-node children finds it. This surfaces real unbound values in four
namespaces (dependencies 12->17, sync 7->10, collections 5->7, search 5->6);
they are bound in the namespace PRs stacked on this branch.

* Correct the transform section: the call style breaks it, not the marker

The section implied that marking a value on a transformed column is what
produces the identifier comparison. It is not. value_guard lifts the
[:auto/param column v] 3-arity from inside apply-kv-arg, so a MARKED kv-arg
still runs the transform -- verified against :model/SearchIndexMetadata, the
bare and marked kv-arg forms compile to byte-identical SQL.

Only the where-map form skips the transform. So a marker on a transformed
column in kv-arg position is redundant rather than dangerous, and the reason to
leave it off is that the transform already binds the value.

Caught by a reviewer that compiled all three forms rather than taking the
section at its word.

* Rewrite the transform and written-value rules as single explanations

The transform correction was bolted onto the end of the section, so it stated
the rule, then contradicted its own framing, and showed the same example twice.
Rewritten as one block that shows all three forms together -- kv-arg bare,
kv-arg marked, query map -- so the variable is visible: the call style is what
takes a value off the transform path, not the marker. Adds the converse case a
sweeper actually hits: a call that was ALREADY a query map on master must carry
the post-transform value written out.

Rule 1's example showed a t2/update! CONDITIONS map while the prose was about
the changes map, which is the confusion the rule exists to prevent. Replaced
with a real settings/db.clj call where the condition is marked and the changes
map is bare, plus an explicit note that only the trailing changes map is exempt.

Also corrects the stated mechanism. The reason not to mark a written value is
that the column's `:in` transform runs on the marker and persists it as data --
mi/transform-encrypted-json is an :in transform, so an encrypted column
encrypts the marker. The earlier text attributed this to a
define-before-insert hook on :model/Setting, which does not exist; Setting's
encryption is application code plus a column transform.

* Refuse a marker outside a value slot, and see two more shapes

Three findings from review, all verified against a running app DB.

The guard was silent on the one case it most needed to catch. `auto-param`
lifts a marker wherever it sits, so one written in a `:select`, `:from`,
`:order-by` or `:group-by` clause became a bound parameter in a slot HoneySQL
wanted an identifier for -- compiling to the literal identifier `PARAM` and
discarding the value, with nothing downstream able to notice:

    (t2/query {:select [[:auto/param "name"]] :from [:setting]})
    ;; => ["SELECT \"PARAM\" AS \"P3R1MO8JB3WD0J\" FROM \"SETTING\""]

`check-marker-placement!` now refuses that as `::marker-outside-value-slot`. It
stops at a nested query map, because a subquery's own clauses are checked on
their own terms -- `t2/exists?` wraps the query in `:select [[[:exists {...}]]]`
whose inner `:where` is a real value slot, and scanning the outer `:select`
wholesale rejected it. A join's ON condition is likewise a value slot, so only
the table halves of a join clause are checked.

The lint could not see a conditions map. `(t2/update! model {:col v} changes)`
filters on `v`, but it is not a `:where` clause, so neither walker descended it
-- and the rubric recommends that exact shape, so a namespace written that way
passed vacuously. Now walked for `update!`, `update-or-insert!` and `delete!`,
taking only the map immediately after the model (a later map is the changes map,
exempt by rule 1). An operator form there holds only values, since the column
comes from the map key.

The lint could not see into a subquery. The operator branch returned its
arguments without descending, so `[:in :id {:select ... :where [:= :z z]}]` left
`z` unchecked. It now keeps each value and descends into it; descending alone
would have dropped the plain-token case.

Rubric corrections from the same review, each verified rather than reasoned:
rule 2's failure mode (marking a column keeps the comparison and binds the
column NAME, it does not drop it), rule 5 (`:not-in` gets no empty-collection
rewrite, so an empty one emits invalid `NOT IN ()` marked or not; rule 4 wins
for collections of ids; a non-empty proof must be local), rule 4 (`long`
truncates a non-integral number rather than throwing), rule 1 (a written value
is not the injection shape this project targets, rather than never structural),
rule 7 (booleans from outside are values too), the transform mechanism
(`apply-kv-arg` maps the transform over an operator form's tail, which is why a
marked kv-arg keeps it -- nothing to do with `value_guard`), and a new
false-negatives subsection, because the rubric previously described the lint as
only over-flagging.

* Scan subqueries for misplaced markers, and allow one inside an expression

Two holes in check-marker-placement!, both verified by compiling the form.

It only scanned the top-level map, so the silent case survived one level down:

    {:select [[[:exists ^:allow-subquery
                {:select [[:auto/param "name"]] :from [:setting]}] :e]]}
    ;; => SELECT EXISTS (SELECT "PARAM" AS "PACGBLW31WM3R" FROM "SETTING")

No throw, value dropped -- exactly what the check exists to prevent. The
docstring, the test, and the rubric all claimed a subquery was "checked on its
own terms", which was not implemented; nothing re-ran the clause scan on a
nested map. warehouse_schema/db.clj:317 already uses ^:allow-subquery with
:exists, so the shape is live. It now walks every nested map and scans each
one's clauses.

It also refused legitimate value slots. A comparison inside an identifier
clause is a real value slot, and both of these threw at compile:

    {:select [[[:= :engine [:auto/param "h2"]] :is_match]]}
    {:order-by [[[:case [:= :a [:auto/param 1]] 1 :else 2] :asc]]}

So a converted namespace would break the moment it grew a computed projection
or a CASE sort key. `:join` already needed a carve-out for its ON condition;
this is the same need, and the file already recognizes the shape via
`operator-form?`, so the scan descends past a keyword-headed form.

Pre-existing failures in metabase.app-db.custom-migrations-test are unchanged:
24 on origin/ghy-4477-rubric-foundation, 24 with these fixes.

* Classify identifier slots positionally, and close four lint blind spots

Findings from four independent reviews of this PR, each verified by compiling
the form or by linting a probe rather than by reasoning.

The guard missed its own target case. `marker-in-identifier-position?` decided
what was a value by head shape, but HoneySQL tells `[expr alias]` from
`[:= a v]` POSITIONALLY. So `[:t [:auto/param "al"]]` read as though `:t`
headed an operator form, and the alias slot -- the shape the check exists to
catch -- passed:

    {:from [[:t [:auto/param "al"]]]}    => "SELECT * FROM t AS param k"
    {:select [[:a [:auto/param "al"]]]}  => "SELECT a AS param k FROM t"

Now an entry is destructured as `[expr & aliases]`: index 0 may be an
expression and is descended, everything after it is a name and is refused.

`:order-by` and `:group-by` are no longer refused. HoneySQL binds a param in
both (`ORDER BY ?`), so a marker there is a pointless no-op, not a dropped
value -- and the test asserted the throw as correct, pinning a premise HoneySQL
contradicts. Refusing it would have turned a harmless mistake into a
production exception.

`:cross-join` no longer takes the alternating path. It has no ON condition, so
`take-nth 2` skipped every odd-indexed table; a marker there was missed.
Only the four join clauses that actually pair a table with a condition use it.
Added `:using`, `:create-table`, `:drop-table`, `:truncate`, `:partition-by`,
`:window`, `:select-top`.

The scan now short-circuits on `contains-marker?`. It runs on every app-DB
query at compile, and almost none carry a marker. 18.0us -> 13.7us on a
join-heavy query with no marker.

Four lint blind spots, all with live call sites:

  `[:!= :col v]` / `[:<> :col v]` were unchecked -- `value-operators` listed
  only `not=`. collections/db.clj:283 passes a request `user-id` through `:!=`
  in a namespace a stacked PR reports as converted. Added `!=`, `<>`,
  `is-distinct-from`, `is-not-distinct-from`, `not-between`.

  A value inside a function-call form -- `[:= :k [:lower v]]`, which compiles
  to `LOWER(?)` -- was dropped by the `:else` branch, whose token children
  return nothing on their own. Live in audit_app/db.clj. Same fix reaches a
  literal collection: `[:in :k [a b]]` now checks both elements.

  A two-arity `t2/update!` had its CHANGES map read as conditions. Toucan's
  arglist ends in the changes map, so a lone trailing map is changes --
  remote_sync/db.clj's mark-all-rsos-synced! was flagged for a written value,
  contradicting rubric rule 1. `delete!` has no changes map, so its lone map
  is still conditions.

  A query map passed where conditions go was walked twice, reporting every
  value twice. Live in mcp/db.clj, metabot/db.clj, testing_api/db.clj.

Findings are now deduped by source position, which also covers a multi-arg
operator in a kv-arg: `:d [:between lo hi]` reported `hi` twice, because
`value-nodes` misreads the vector as a clause while `kv-arg-value-nodes` reads
it correctly. Deduping is robust where keeping the walkers disjoint is not.

Also: the marker payload is no longer descended (a marked value was reported as
though bare once the `:else` branch started yielding children), `ex-data`
carries the query so a dynamically-built one can be traced, and `args` is no
longer shadowed inside the conditions-map walker.

* Bring the rubric's guard and false-negative sections up to date

The guard section described the head-shape classifier and claimed :order-by /
:group-by were refused; both changed. The false-negative list still carried the
conditions-map, function-call-form, literal-collection, operator-alias and
subquery shapes as open, which are now covered -- and was missing the two that
remain open, a positional pk and a non-literal model.

* Drop the unused clojure.string require and the bang from check-marker-placement

Two clj-kondo failures on the previous push, both mine.

`clojure.string` became unused when the join carve-out stopped matching clause
names by suffix.

`check-marker-placement!` only throws -- it mutates nothing -- but the bang made
`:metabase/validate-deftest` read every call in a `^:parallel` test as a
destructive operation. Renamed rather than whitelisted, since the name was
wrong: the convention marks state change, and this is a predicate that refuses.

* Don't ask for a marker where the guard refuses one

`value-nodes` walked every clause of a query map, and the `:else` vector branch
yields each child as a candidate value. Together those reported symbols sitting
in clauses that name columns and tables, and the message always says the same
thing: write it as `[:auto/param x]`. In a `:select` or a `:from` that advice is
not merely noise -- `check-marker-placement` in this same PR throws
`::marker-outside-value-slot` when you follow it, so the two halves of the
mechanism contradicted each other.

A query map's clause values are now split. A clause that names things
contributes only what is nested inside it: a subquery's own clauses, and the
arguments of a keyword-headed comparison. Everything else is unchanged.

Measured over `src/metabase` and `enterprise/backend/src`, with the linter
forced on for every namespace: 1981 warnings to 1963. Eighteen removed, none
added. All eighteen are identifier slots -- `output-table-id`,
`table-wildcard`, `sort-direction`, `order-column`, `id-column`, `cast-form`,
and a `union-query` alias. Every value-carrying shape survives: a computed
projection, a `CASE` sort key, a subquery nested in `:from`, a subquery in an
`:in`, a plain where clause, a kv-arg.

`:order-by` and `:group-by` are in the set even though the guard deliberately
allows a marker there. A marker in `:order-by` compiles to `ORDER BY ? ASC`,
which loses the ordering, so asking for one is still wrong advice.

Two related shapes stay open, both false positives that ask for a marker in a
position that holds no value:

- A clause built with `assoc` or `cond->` is not a map in the source text, so
  the walker still reads it as a plain vector. `dependencies/db.clj:492` keeps
  its three `sort-direction` warnings.
- A binding vector inside a `let` or a `for` is walked like a collection
  literal, so the binding names are reported.

Neither is urgent: the linter is opt-in per namespace, the noise surfaces one
namespace at a time during a conversion that reads every warning, and the
rubric's gate is already that a rule or a limit explains each one.

* Allow a marker at index 0 of an expression entry

In :select, :select-distinct, :select-top, :returning and :partition-by,
index 0 of an [expr alias] entry is an expression, and HoneySQL binds a
param there (SELECT ? AS a). The guard refused it as an identifier.

The table clauses also bind a param at index 0 (FROM ? AS a), but a
table cannot be a parameter, so the guard still refuses a marker there.

* Refuse a marker that is the whole identifier clause value

When the value of a clause was itself a marker, as in :from [:auto/param "t"],
the guard split it into :auto/param and the payload. Neither is a marker, so
the guard passed it, and it compiled to FROM param, k with the value dropped.
Treat a marker value as a single entry, ahead of the join branch too.

* Remove the value-binding rubric from the repo

A reviewer asked that it not live in the repo.

* Give the lint the clause context it was guessing at

Two fixes, both places where the lint's judgement of a slot did not match
`value-guard`'s.

**Split the expression-entry clauses.** `identifier-clauses` was one flat set,
so the lint stayed silent about every position in `:select`, `:returning` and
friends. The guard no longer agrees: its `expression-entry-clauses` treats index
0 of an `[expr alias]` entry as a value slot, and `{:select [[[:auto/param "a"]
:k]]}` compiles to `SELECT ? AS "K"` with the value bound. The silence there was
a false negative -- a request value in a computed projection reached the
statement text with nothing to report it.

Index 0 of an entry is now a value slot. Everything after it is a name, and a
bare entry is a name, which matches what the guard refuses:

    {:select [[:auto/param "a"]]}        refused   (bare entry)
    {:select [[:key [:auto/param "a"]]]} refused   (alias)
    {:from   [[[:auto/param "t"] :x]]}   refused   (table)
    {:select [[[:auto/param "a"] :k]]}   binds     SELECT ? AS "K"

**Carry clause context through `assoc`.** A clause written outside a map literal
reached the walker through the list branch, which keeps no record of anything,
so the name-slot check never ran. The lint then asked for a marker on a column
name and on a sort direction. Both fixes it named break the query, and one
breaks it in silence: marking the direction throws a ClassCastException, and
marking the column compiles to `ORDER BY ?` and loses the ordering with nothing
at runtime to catch it.

A clause key inside an `assoc` call now routes its value the way a key in a map
literal does. `cond->` is covered because the walk descends into the nested
`assoc`.

Measured over `src/metabase` and `enterprise/backend/src`, with the linter
forced on for every namespace: 1963 warnings to 1954.

Fifteen removed, all of them the `assoc` case: `sort-direction` and
`sort-column` in task_history/db.clj:174, cache/db.clj:102 and
dependencies/db.clj:492, `column` in queries/db.clj:53, models/db.clj:152 and
:171 and documents/db.clj:58, and `table-level-case` in permissions/db.clj:89.

Six added, all of them expression entries the flat set had been hiding:
`output-table-id` in data_studio/db.clj (three sites), `cast-form` in
app_db/db.clj:20, `domain-expr` in internal_stats/db.clj:206 and
`admin-group-id` in permissions/db.clj:681. Each holds a column reference today,
so each is a rule 2 finding to record rather than a value to mark -- but the
lint can no longer tell, which is the point.

Still open, and not addressed here: a binding vector inside a `fn` or a `let` is
walked like a literal collection, so its names are reported; `:order-by` and
`:group-by` are absent from the guard's set, where a bare marker entry drops the
value; and the guard's DDL keys are still missing from the lint's set.

* Give the rubric comments their own reasons

The rubric left the repo, so five comments cite a file a reader cannot open.
"Rubric rule 1" was a useful short form while it was here; now it asks the
reader to go looking in the git history.

Each one now states the reason instead of pointing at it. A written value is
never marked because the column's `:in` transform runs on the marker itself and
stores it as data, and the conditions-map shape is checked because the sweep
leaves it in place.

Comments only. No behaviour changes.

---------

Co-authored-by: Eric Normand <ericwnormand@gmail.com>
2026-09-24 21:05:55 +00:00
Kevin Kelleher
af8ca49a25 Prevent showing stale exploration summary documents (#82442)
avoid fetching stale document while a save is ongoing
2026-09-24 15:32:23 -04:00
Kevin Kelleher
cfb8cbd6f9 Hide exploration error pages for unrequested query variants (#82450)
filter out failed variant pages
2026-09-24 15:32:11 -04:00
Eric Normand
3eff0fac3d GHY-4213: Remote Sync: importing a GUI model before the target's database schema sync completes stores untyped columns (#82945)
GHY-4213: Infer untyped model metadata after a metadata sync

A GUI model that is deserialized (for example by a Remote Sync import) before the target has synced its tables cannot infer its result metadata. The import then stores only the exported model overrides, with base_type type/* and no field ids, and nothing revisits it. Filters on those columns offer only "Is empty" and "Not empty".

After each metadata sync of a database, look at the unarchived MBQL models on that database. For each model whose stored metadata has no known base type, infer the metadata again, keeping the model overrides. A model that still cannot be inferred is left as it is. The update goes straight to the app DB, so it publishes no card-update event and does not mark remote-synced content dirty.

Tests: an API-level test imports a GUI model through POST /api/ee/remote-sync/import before its table is synced, runs POST /api/database/:id/sync_schema, and checks the column types in GET /api/card/:id. Contract tests cover untyped-metadata? and backfill-untyped-model-metadata!.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 14:16:53 -05:00
metamben
b6f625ed4b Use jdbc-type for SQL Server user defined types as fallback (#82860)
Closes #62335
Closes QUE2-143
2026-09-24 17:49:26 +00:00
Eric Normand
bb9482f0fd GHY-4399: Remote Sync doesn't clean up nested content when a parent collection is archived, so orphaned files accumulate in git forever (#82876)
* GHY-4399: Cascade collection archive to nested content in the remote sync ledger

Archiving a collection archives its whole subtree in bulk SQL and publishes one collection-update event for the top collection only. The remote sync collection handler marked only that collection as delete, so the next push removed its own file and left every nested collection, card, and other item in the repo for good.

The handler now brings the ledger rows of the subtree in line with each entity archived state when the collection is archived, or when its own row was a pending delete (it was unarchived before a push). Rows of archived entities become delete. Delete rows of entities no longer archived become update, so restoring a folder before a push does not delete its files.

subtree-expr matched descendants by the prefix /<id>/, which is correct only for root collections. It now uses the location the children have (location + id + /), which is the same string for roots and is also correct for nested collections.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* GHY-4399: Cascade collection archive to transforms in the remote sync ledger

The cascade marked a subtree row as delete only when the entity itself was archived. Transforms have no archived column, so a transform in an archived Transforms folder stayed synced, and the next incremental push left its file on the remote. This is the first repro in the ticket.

A row now also counts as archived when the collection it is in is archived. Unarchiving restores it to update when neither the entity nor its collection is archived and the entity still exists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 11:59:46 -05:00
Andrei Chernykh
8317274709 Migrate AutocompleteInput dropdown off Emotion (#82948) 2026-09-24 09:46:29 -07:00
Sébastien
c796ec01ec Handle both list and search default-value pickers in embedding e2e test (#82727) 2026-09-24 12:35:20 -04:00
Eric Normand
fa7362a1e4 GHY-4554: Mark untrusted data in MCP v2 tool results with an unforgeable boundary (#82879)
* GHY-4554: Mark untrusted data in MCP v2 tool results with an unforgeable boundary

Tool results put instance data (card names, warehouse rows, column comments) next to server prose, with nothing to mark where the data stops. A planted instruction in a name could pass for an instruction from Metabase.

message/data holds data from outside the server. render writes it as JSON between <data boundary="..."> and </data boundary="..."> lines that carry a random 32-hex-digit value, which the data cannot predict and so cannot close early. The initialize instructions explain the convention in one line.

message/json-text encodes JSON and then writes invisible, line-breaking, private-use, and unassigned code points (Cc, Cf, Zl, Zp, Co, Cs, Cn) as \uXXXX escapes. Decoded values do not change. The transport encodes JSON-RPC responses with it, so structuredContent is escaped on the wire too.

Every (message/raw (json/encode ...)) site and common/success-content now use message/data. Tool tests read the text through test-util/strip-data-boundary.

* GHY-4554: Make question_write and transform_write text-only

Both tools sent their payload as structuredContent as well as in the text block. Claude Code shows the model structuredContent in place of the text block, so the model saw the payload with no data boundary and with invisible characters raw. No client reads their structuredContent, so they now answer text-only.

* GHY-4554: Say to keep untrusted content out of structuredContent

A client may show the model structuredContent in place of the text block, and there the data has no boundary and its invisible characters decode raw.

* GHY-4554: Put glossary lookup results inside a data boundary

Glossary terms and definitions are user-written free text. The glossary() listing already put them inside the per-response data boundary, but glossary(term) quoted them into server prose. The lookup now returns its matches in the same data envelope as the listing, so a planted closing tag or instruction reads as data.

* GHY-4554: Make strip-data-boundary strict

The test helper returned text with no data boundary unchanged, so a tool that stopped wrapping its result still passed its suite. It now throws, and the suites that read error results through the same helper read those raw. With the renderer changed to emit bare JSON, every MCP v2 tool suite now fails.

* GHY-4554: Extend the never-follow rule to quoted values

Server prose quotes every value it did not write, so a planted instruction can reach the model inside a quoted value as well as inside a data boundary. The initialize instructions now name both.

* GHY-4554: Name the sliced table by id in the get_fields paging line

The paging line put the warehouse-controlled table name in server prose, outside the data boundary, and printed null when a fields projection dropped the name and id. It now names the table by the id from the source row and gives the exact next call, so the name stays inside the data boundary and the agent always knows which table to page.
2026-09-23 21:21:22 +00:00
Eric Normand
361fc91d80 GHY-4636: MB CLI doesn't have flag for expected branch, git-sync import fails with expected_branch: missing required key, received: nil (#82874)
GHY-4636: Let remote-sync import run without expected_branch

The mb CLI calls POST /api/ee/remote-sync/import with only a branch. GHY-4019 made expected_branch a required key, so every CLI import failed schema validation with 'expected_branch: missing required key'.

expected_branch is a compare-and-swap guard against a stale browser tab. A headless client has no cached view that can go stale, so the key is now optional. The endpoint runs the guard only when the client sends the key. The web UI still sends it, so its behavior does not change.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 14:49:22 -05:00
Nemanja Glumac
f4ab93af92 Extract the backend profiling trigger into a single entry point (#82854)
* Give on-demand test profiling a single entry point

backend.yml, app-db.yml, drivers.yml and semantic-search.yml each carried their
own workflow_dispatch block, their own PROFILE_TESTS/ONLY_TESTS env block, and a
selector input, all to support the same thing: run one leg of one suite on
demand, under a profiler, optionally narrowed to named tests.

profile.backend.yml now owns the only workflow_dispatch and calls the four
unchanged. They declare nothing for this feature: inside a called workflow
`github.event` is the caller's payload, so they read `target` and `tests` off
`github.event.inputs` directly. On a normal CI run those are absent and render
empty, so every gate's `== ''` clause holds and each suite behaves as before.

One `target` dropdown rather than a suite plus a leg keeps the gates at their
existing two clauses, and disambiguates `postgres`, which names a leg in three
suites.

Also fixes two ways a profiled run produced no flamegraph:

- test-driver built `:profile-tests` into BASE, but BASE is used only on the
  ONLY_TESTS branch. Profiling without a test selection ran unprofiled while
  still reporting profiled=true. The alias now goes on ALIASES too, which the
  other three branches use.
- The multi-JVM path runs each JVM from a sibling directory, so the profiler's
  relative output dir resolves per JVM. merge_outputs copied back junit and
  logs but not the flamegraphs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Collect profile output from multi-JVM test runs

collect-profile predates the multi-JVM test path and reads only the layout a
single-JVM run produces. A partitioned run nests its junit XML one directory per
partition under target/junit, and merges one logs/test-log-p<index>.json per
JVM, so the non-recursive `target/junit/*.xml` glob and the exact
`logs/test-log.json` path matched nothing: an empty wall-time summary, and an
artifact holding the flamegraphs alone.

The two summary greps become `grep -r --include='*.xml'`, which reads both
layouts without needing globstar, and the upload paths become globs matching
test-driver's own failure-log upload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Simplify inputs

* Use `test` namespace

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 18:40:49 +00:00
Timofey Kachalov
6c4b0182c1 Fixed custom actions from the mapQuestionClickActions plugin not appearing in EditableDashboard click action popover (#82832) 2026-09-23 22:33:12 +04:00
Phoomparin Mano
0694a11c90 Fix typed schema action template tag handling (#82488)
Co-authored-by: Timofei <timofei@metabase.com>
2026-09-23 19:41:59 +04:00
Egor Grushin
fa20179ff6 Restyle Button to the Figma spec and migrate call sites (#82236) 2026-09-23 16:59:13 +02:00
Riley Thompson
cdc7f386af Compile mbql stages to CTEs for postgres (#81914)
* initial commit

* Use CTEs for every multi-stage Postgres query

Drop the "standalone statement" gate and the native/persisted-stage and
join-source fallbacks. Postgres accepts WITH everywhere we embed a compiled
query (JOIN parens, CTE bodies, card-reference subqueries, CREATE TABLE AS,
INSERT INTO), so the only remaining condition is the driver opting in and
the query having more than one stage.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix changelog

* simplify with mapv

* comments

* Build CTE tests with lib and assert results only

Rewrite the four multi-stage tests added on this branch to build their
queries with lib/query and a metadata provider instead of mt/mbql-query,
and drop the compiled-SQL string assertions so they only check that each
query runs on Postgres and returns the expected rows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* trim comments

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-23 14:09:50 +00:00
github-automation-metabase
d994a365f7 Auto-update documentation for master (#82878)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-09-23 09:40:25 -04:00
Phoomparin Mano
d7b9a71698 Fix including library sub-collection throws an error in typed schemas (#82513)
Co-authored-by: Timofei <timofei@metabase.com>
2026-09-23 17:34:22 +04:00
YB
12a22c0392 [GDGT-3229] read ClickHouse indexes from SHOW CREATE TABLE instead of system tables (#82573)
ClickHouse denies SELECT on system.data_skipping_indices, so index verification failed for every request (the index itself was created). fetch-table-indexes now reads one SHOW CREATE TABLE and parses its INDEX and ORDER BY lines. A failed warehouse read now carries the driver's message on the request and as warehouse_error on the indexes list, with an alert on the indexes page.
2026-09-23 21:14:23 +08:00
Ryan Laurie
6ec07f7518 Pin to clickhouse 26.8 (#82868)
pin to clickhouse 26.8
2026-09-22 22:28:11 +00:00
Fraser
aa9c3797e8 Move the embed setup modal state into the embedding module (#81656)
Co-authored-by: Romeo Van Snick <romeo@romeovansnick.be>
2026-09-22 18:27:38 -04:00
Eric Normand
8ce3936116 GHY-4642: Renaming a collection and exporting via Remote Sync doesn't update repo paths for content in renamed collection (#82865)
Remote sync: full export when a synced collection is renamed or moved (GHY-4642)

A collection path is also the directory of its contents. The incremental export wrote the renamed collection file and deleted the old one, but left every descendant file under the old directory, and kept their stale file_path values. Later incremental pushes did not repair it.

An update to a Collection whose stored path differs from its new path now falls back to a full export, which rewrites every path and records each file_path. This matches the import side, which already does a full import for any Collection change. In-place collection edits that keep the path stay incremental.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 14:11:38 -05:00
Oleksandr Yakushev
5bcd89d33f [pivot] Optimize BE+FE pivot generation (#82811) 2026-09-22 21:22:53 +03:00
Eric Normand
916ee6554c SEC-1208: Login throttle failures crash with fk_login_history_session_id FK violation, causing self-reinforcing lockout (#82859)
SEC-1208: test that a concurrent session delete cannot fail a login

Add an API test for POST /api/session. Between the session insert and the login_history insert, another connection deletes the user's sessions, as a password change does. The test logs in 11 times like this. Each login must return 200. Then a normal login must work, and GET /api/login-history/current must show all 12 logins.

The bug is already fixed. Commit 4a7f73029a (#80718, backported to 61 as #80962, first in v0.61.20.1) writes the session row and the login_history row in one transaction. The reporter ran v0.61.1, which does not have it. When that transaction is removed, this test fails as the ticket describes: each login returns 401 with an fk_login_history_session_id violation, and after 10 of them the throttle returns "Too many attempts" for the correct password.

The test does not use mt/with-temp for the user. with-temp binds a transaction connection, and the test client conveys it into the request, so the delete could not see the login's rows. For the same reason, the delete runs on a plain Thread, not a future.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 11:33:28 -05:00
Kyle Doherty
401167cc00 Merge branch 'master' into nav-split-prototype 2026-09-22 06:20:55 -04:00
github-automation-metabase
fff70175e0 Update translations 2026-09-22 (#82845)
* Update translation sources 2026-09-22

* Update translations

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-09-22 06:21:08 +00:00
Alexander Polyankin
74216b3098 Transform tests batch 2 (#82770) 2026-09-21 16:17:38 -04:00
Kyle Doherty
2947b47c23 Select Library tables in the Official rail
A published table has no page of its own — the rail links it as the
ad-hoc question that reads it, /question#<serialized card> — so there was
no card id to key a selection off and the row never highlighted.

Recover the table from the serialized card in the hash, load it, and
select it. Its collection also feeds the section seed, so landing on or
reloading a table URL stays on Official, matching metrics.

The hash is user-controlled, so its shape is checked rather than
asserted; anything unrecognised just leaves the rail alone.
2026-09-21 15:41:57 -04:00
Kyle Doherty
abb710334f Keep Library content out of the open rail
Opening a metric pushed it into the Unofficial rail's open list. The
Library is a curated reference you navigate to and read, not a piece of
work you open, and it already has its own home in the Official rail.

Skip any card or dashboard whose collection is part of the Library.
Dashboards cannot live there today, but the rule is about the Library
rather than about metrics, so it is applied to both.
2026-09-21 15:36:48 -04:00
Kyle Doherty
fed0dbfd58 Read the metric route's card id from the right param
The metric routes declare their param as :cardId, but the navbar read
params.slug for every card path. On /metric/:cardId that was undefined,
so the card was never fetched — which meant no collection to derive the
nav section from and nothing to select, leaving a reload on Unofficial
with nothing highlighted.

With the card loaded the rest of the chain already worked: the seed
resolves to Official off the library collection, the selection resolves
to metric-<id>, and the tree auto-expands the ancestors of the selected
node, so the library collection opens with the metric highlighted.
2026-09-21 15:34:36 -04:00
Kyle Doherty
edd44ef851 Seed the nav section from what is open, and select metric pages
Reloading an official metric dropped the rail to Unofficial: the section
override lives in redux, so a refresh wiped it and the fallback keyed off
the pathname, which says nothing about authority. /metric/... is not
/browse, so it resolved to Unofficial.

Derive a seed from the collection of whatever is open, taking Official
when that collection is marked official or is part of the Library.
Precedence is now explicit choice, then the open entity, then the URL.

getSelectedItems ignored metric paths entirely, so nothing was selected
on a metric page. Handle them alongside questions and models, and carry
the search model on the selection so the Official rail, whose rows are
keyed model-id, can highlight the open entity rather than only its
containing collection.
2026-09-21 15:27:25 -04:00
Kyle Doherty
1cd4d62061 Make the Official/Unofficial switcher a view-only toggle
Switching sections navigated to whatever you last had open in the target
half, so flipping the rail threw away the page you were on. The rail is
a view of the sidebar, not a destination; toggling it should only change
what the sidebar shows.

Drop the navigate call. That makes hrefFor, the per-section last-path
map and the effect feeding it dead, so they go too. The URL still seeds
the initial section for a deep link.
2026-09-21 15:18:50 -04:00
github-automation-metabase
0509e1a5e2 Tighten ratchets (#82829)
Co-authored-by: github-actions <github-actions@github.com>
2026-09-21 18:27:18 +00:00
Oleksandr Yakushev
81736f712a [kondo] Squelch missing-docstring linter for Potemkin-imported vars (#82812) 2026-09-21 21:21:36 +03:00
Michael Eby
812e9b2254 add IP allowlist page to nav (#82774)
* add IP allowlist page to nav

* fix nav and metabase ip addresses label
2026-09-21 12:40:46 -04:00
Timofey Kachalov
d0ce86c19e Fixed InteractiveQuestion and StaticQuestion flashing the "run your code" empty state instead of a loader while card.query is still unresolved (#82693) 2026-09-21 18:48:24 +04:00
Jeff Bruemmer
72e2e9b234 docs - ai providers updates (#82376) 2026-09-20 14:33:58 +00:00
Kyle Doherty
426c9e6406 Use the resolved collection icon in the main navbar
buildCollectionTree already derives each node's icon via
getCollectionIcon, which is where a collection's own icon is honoured.
SidebarCollectionLink then re-derived it from the tree node, whose icon
field is the resolved IconData object rather than the stored name, so a
custom icon never survived to the Official and Library nav sections and
every folder fell back to its type default.

Read the resolved icon off the node instead. The tenant override moves
with it: MainNavbarContainer already passes isTenantUser into the tree
builder, and the one remaining caller that did not is unreachable for
tenant users.
2026-09-20 09:26:20 -04:00
Kyle Doherty
feb7aa645c Allow arbitrary Library folders with custom icons
Library folders were limited to the two seeded Data and Metrics
collections. The backend already inherited the library type onto
children, so subfolders worked, but nothing was exposed in Data Studio
and the Library root rejected everything but those two sections.

Top-level folders reuse the existing `library` type rather than adding
a new one: the root is told apart from user folders by is_library_root,
which is keyed off the hard-coded entity_ids and already existed. The
content rules become: the root holds folders only, a user folder holds
tables, metrics and nested folders, Data and Metrics are unchanged.

Collections gain a nullable icon column so a folder can carry a chosen
icon; when it is null the icon is still derived from the type.

canPlaceEntityInCollection now takes the destination collection instead
of a bare type string. The rule depends on root-vs-folder, and those
share a type, so the string alone would have made the Library root a
valid table-publish target.

The frontend move rule disagreed with the backend in both directions,
permitting a Data subfolder onto the root and blocking user folders from
nesting. It now mirrors check-library-update.
2026-09-20 09:19:20 -04:00
github-automation-metabase
93742a629c [chore] Update E2E test timings with recent data (#82804)
update e2e timings 20260920-062425

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-20 07:03:45 +00:00
Nathan Voxland
b485c0f7fa Improve *-allowed-network validation (#82519) 2026-09-20 01:32:39 -05:00
Nathan Voxland
76979e6167 Limit unauthenticated request body sizes (#82572) 2026-09-19 15:32:34 -05:00
Nathan Voxland
da7629bba6 Serve no values for a filter whose widget is an Input box (#82535) 2026-09-19 15:32:12 -05:00
Nathan Voxland
280c1e7be2 Improve indexed-entity handling in xrays (#82699) 2026-09-19 14:50:48 -05:00
Aleksandr Lesnenko
3d924036db bind dev Docker ports, socket REPL, and rspack dev server to loopback only (#82707) 2026-09-19 10:27:04 +07:00
Chris Truter
883e944cce Preserve Slack attachment names in thread history when text is missing (#82631) 2026-09-19 02:48:34 +00:00
Tyler Plude
9d1c919f6b Move glossary entity_id migration from v65 to v64 for backport (#82776)
* move glossary entity_id migration from v65 to v64 for backport

* reset Liquibase changelog cache in glossary precondition test

* Scope glossary precondition test column check to the current schema
2026-09-18 18:06:53 -07:00
Tyler Plude
b08a2f0e65 [GHY-4593] Make remote sync robust against restarts (#82553)
* add remote_sync_task.last_heartbeat_at and a per-task heartbeat thread

* always close a remote sync task row when its worker exits

* close this JVM's running remote sync tasks on graceful shutdown

* reap stale remote sync tasks at boot and when the current-task endpoint observes one

* keep a committed remote sync version as the sync base when its task is cancelled or fails afterwards

* return the initiating user on remote sync task responses

* show interrupted, cancelled, and quiet states in the remote sync progress modal

* report remote sync import progress through every phase with one throttled reporter

* use with-dynamic-fn-redefs for the heartbeat touch-task! redefs in remote_sync_task_test

* log the ids of stale remote sync tasks when the reaper closes them

* fix migration

* materialize data apps inside the import's version transaction

* discover a running or stuck sync from a fresh tab

* drop the unreachable sync interrupted view
2026-09-18 15:43:48 -07:00
John Swanson
22b1a393dc Read c3p0 pool stats from the pool objects instead of JMX (#82760)
* Read c3p0 pool stats from the pool objects instead of JMX

The Prometheus c3p0 collector enumerated every pool MBean and read its
attributes over JMX. A JMX read locks the MBean and then the pool, while
pool construction locks the pool and then the MBean
(swaldman/c3p0#95). The collector guarded against this by holding the
warehouse pool guard, but the application, Quartz and semantic-search
pools are built outside that guard, so a scrape overlapping one of those
constructions deadlocked and then held the guard forever: a fresh pod
passed its health checks but never completed a warehouse query.

`connection-pool-info` now enumerates c3p0's own registry
(`C3P0Registry/getPooledDataSources`, a copy made under the registry
lock) and reads each `PoolBackedDataSource` with the default-user
getters the MBean used. No JMX, no `locking`. A pool closed mid-scrape
throws and is skipped. Metric families, labels and the bug-reporting
endpoint's output shape are unchanged.

Warehouse pools of a non-default connection type get a name suffix
(`-write`, `-admin`, `-transform`) so two pools of one warehouse no
longer share a `database` label and overwrite each other.

The regression test scrapes on one thread while another builds pools
through the app-db path and a third builds a warehouse pool through the
guarded path; on the old collector it deadlocks within a second.

* Disable c3p0 JMX MBean registration

The Prometheus collector no longer reads pool MBeans, but any other
in-process JMX reader (the Datadog agent's JMXFetch, JConsole, a JMX
exporter) can still deadlock a pool construction the same way
(swaldman/c3p0#95). Set the c3p0 management coordinator to the null
one in resources/c3p0.properties so no pool or registry bean is
registered. c3p0 reads the key once through its layered config, with
system properties last, so an operator can restore the beans with
-Dcom.mchange.v2.c3p0.management.ManagementCoordinator=...ActiveManagementCoordinator.

Verified with a standalone harness: with the property, 37534 pool
creations under a concurrent JMX reader over 30 s with no deadlock and
no c3p0 beans; with the -D override, the beans return and the harness
deadlocks in 0.5 s, as it does without the property.
2026-09-18 14:44:18 -07:00
appleby
f58c520891 Require channel write permission to send to a channel by id (#82608) 2026-09-18 14:15:33 -07:00
Nathan Voxland
f2364b1d27 Format the blocked table's ID as a plain number in error message (#82772)
Don't format ids in error messages as if they were normal numbers
2026-09-18 20:35:31 +00:00
github-automation-metabase
39bd87ede3 Tighten ratchets (#82777)
Co-authored-by: github-actions <github-actions@github.com>
2026-09-18 20:20:06 +00:00
Tyler Plude
56232fc3f8 [GHY-4600] Improve memory allocation of remote sync (#82625)
* move the serdes :field normalize schema into a var

* extract the library once on a merging pull or push

* stream the export preflight extraction instead of realizing it

* index each merge side once for the merge and its casualties

* fix test

* share one registered :field normalize schema between serdes and resolve

* test the merging pull and push extract the library once
2026-09-18 13:14:18 -07:00
Michael Eby
61b6635f13 docs: IP allowlist (#82761)
* docs: IP allowlist

* edits from jeff
2026-09-18 15:16:45 -04:00
Alexander Polyankin
86b1f4bca4 Transform tests (#82495) 2026-09-18 14:50:16 -04:00
Eric Normand
ac1f4cdc00 Add a glossary tool to the MCP server (#82696)
* Let a v2 MCP tool describe itself from the instance's own data

deftool gains an optional :description-suffix, a fn of no arguments whose
result is appended to the tool description on every tools/list. The static
part stays in the manifest cache and only the suffix is recomputed, so a
suffix that reads the app DB doesn't drag the schema conversion along with it.

tools-hash skips it on purpose. The keepalive polls that hash every 30 seconds
on every open stream, so covering descriptions would buy a change notification
for a suffix at the price of its query per stream per tick. A suffix reaches a
client when it next lists tools instead.

* Add a glossary tool to the MCP server

glossary() returns every term with its definition; glossary(term) returns
one, matched case-insensitively, since a model echoes a term as it read it
rather than as it happens to be stored.

The term names ride the tool's own description rather than waiting behind the
call. A model only looks a word up when something tells it the word is worth
looking up, and the terms that most need a company definition are the ones
that read as ordinary English -- account, active user, churn -- which a model
believes it already understands. Metabot gets this for free by pasting the
whole glossary into every message it sends, because it owns the prompt; here
the description is the only channel that reaches the model unprompted.

Definitions stay behind the call, and the term list is capped at 100. Nothing
bounds a glossary -- no cap on entries, and a definition is TEXT -- and a
description long enough for a client to truncate costs more than its tail is
worth.

A client may cache a description for the life of its session, so a term added
mid-session shows up when that client next connects.

* Keep glossary out of the driver-test trigger set

mcp now uses glossary, and mcp's transitive dependents are effectively the
whole graph because some modules declare :uses :any. That made a glossary
change reach driver, query-processor and transforms, so every glossary edit
would have run the full driver suite. mcp is already excused from this for the
same reason; glossary joins it rather than raising max-allowed-count, since
none of it is driver logic.

Also fixes two whitespace-linter errors: a literal U+2028 landed in the
glossary test where an escape sequence was meant. The separator is now built
with char, which is what the test was trying to say anyway.

* Reject a :description-suffix that only takes one argument

ifn? was the wrong predicate. Keywords, maps, sets and vectors satisfy it but
are callable only with one argument, so they passed registration and then threw
ArityException at the first tools/list -- the deferred failure the guard exists
to prevent. Worse, the accepted registration sat in the shared registry, so the
throw came from listing any tool at all, not from the one that was wrong.

Vars stay allowed: one holding a fn of no arguments is callable, and :handler
is already registered as a var.

* Point the MCP server instructions at the glossary

The initialize instructions are the only channel that reaches the model before
any tool call, so this is where the glossary gets announced.

The line is unconditional on purpose. An instruction to look a term up when you
don't recognize it never fires, because the words that most need a company
definition are the ones that read as ordinary English -- account, active user,
churn -- which a model believes it already understands. So it asserts that
these terms matter and to fetch them before answering, with no qualifier about
which words. A test pins that: it fails if the line goes missing, and it fails
if the line grows a "don't recognize" style qualifier.

Static, so the instructions stay a compile-time constant. Whether an instance
has any terms at all is reported by the tool's own description instead.

* Instruct rather than list, and page the glossary listing

Three fixes from review.

The description no longer names the terms. The old cap was on the number of
terms, but the thing being protected is the length of a description clients
truncate -- Claude Code at 2048 characters. Seventy ordinary-length terms
produced 2444 characters with no pointer to the rest, so the client cut the
tail and the model saw a partial list it had every reason to read as the whole
glossary. That is worse than no list: a model believing the glossary is covered
never calls. So the description now carries the count and an unconditional
instruction to call before answering, which is inherently bounded and asks the
model to notice nothing.

glossary() is paged. It rendered every row, term and definition, into one
block, with definition a TEXT column that nothing bounds at any layer. Now
limit and offset through the usual list envelope, defaulting to 50 and matching
browse_collection -- the closest analogue, a plain unranked listing of rows
carrying prose.

glossary(term) returns every match. term is unique case-sensitively, so "ARR"
and "arr" are two entries with two meanings, and the lookup is deliberately
case-insensitive because a model echoes a term as it read it. Taking the first
match silently picked one and gave no sign the other existed.

* Make the glossary tool description static again

The suffix reported how many terms an instance defines. The only thing that
bought was letting a model skip one call on an instance with no glossary --
a call that is cheap and self-correcting, since it returns "no terms are
defined" and the model moves on. That does not pay for a public extension
point on deftool, its load-time validation, and a query of every glossary row
on every tools/list.

So the instruction moves into the static description and the machinery goes.
registry.clj is byte-identical to its state before this branch; every change
it had carried was in service of the suffix, including the list-tools split
that existed only so tools-hash could avoid computing one.

The instruction itself is unchanged in substance and still unconditional: call
glossary() before answering any question about this instance's data, with no
qualifier about which words, because the words that most need a company
definition read as ordinary English and the question will not say which ones
are defined.

* Simplify description

* Fixes

* Read the glossary through its module API, not its db namespace

The MCP tool required metabase.glossary.db, which meant exporting another
module's raw Toucan namespace just to read from it. metabase.glossary.core is
the module's stated API namespace and says so in its own docstring; it only
lacked a read, which is why db had to be exported at all.

So core gains entries, and db leaves the module's :api. Nothing outside the
module reaches into it now.

* Match the glossary tests to the simpler unknown-term error

The unknown-term error no longer names the terms that do exist, so the two
tests asserting it did were red. They now assert what it says: the term the
caller asked for.

The third was the cleaning guard, which had been riding on that error naming a
stored term. Retargeted onto a successful lookup, where stored text still
reaches prose the model reads as the server speaking. It now covers the stored
definition as well as the term -- definitions had no cleaning coverage before,
and the definition is the longer field and the one a payload would fit in.

The listing is deliberately not covered: list-content JSON-encodes its envelope
and wraps it raw, so it is bounded by JSON escaping rather than by clean, and
asserting otherwise would have been false.
2026-09-18 13:14:17 -05:00
Bartosz Rybacki
0e83852557 GDGT-3137: accept emoji up to 10 code points as a comment reaction (#82652)
* GDGT-3137: accept emoji up to 10 code points as a comment reaction

The emoji schema counted UTF-16 units, so the England flag (7 code
points, 14 units) got a 400. Postgres and MySQL count varchar(10) in
code points and already fit every picker emoji, so only the schema and
the H2 column change. The MySQL collation set by v63 stays untouched.
2026-09-18 20:04:39 +02:00
Nathan Voxland
61d0fa5d0f Check permissions on nested cards (#82600) 2026-09-18 13:02:41 -05:00
Kevin Kelleher
504b8ea9ee Enable explorations (#82594)
* Revert "Disable explorations (#82230)"

This reverts commit c505a9a785.

* add back test improvements

* Restore up-front profile validation in the agent endpoint

The "Disable explorations" PR (#82230) also fixed an unrelated bug: a
request naming a nonexistent profile was not rejected up front. The SSE
stream opened, the turn was persisted, and the conversation-title LLM
call was made before anything failed. Reverting that PR here dropped
the fix along with the explorations changes.

This re-applies only the validation: `profile-registered?` in the
profiles registry, and a 400 "Unknown profile" check in
`streaming-request` right after the profile id is resolved and before
`start-turn!` persists anything or the title job runs. Explorations
stay registered.

---------

Co-authored-by: John Swanson <john.swanson@metabase.com>
2026-09-18 13:12:20 -04:00
Mahatthana (Kelvin) Nomsawadi
61ce133268 test: Fix failed split permissions modal test (#82757)
test: drop the wall clock from the split modal test

The test gave the metadata request a 100ms fetch-mock delay and then
asserted the loader was still showing. On a loaded CI runner the
render plus the awaits burn through that budget, the loader is gone
and getByTestId throws. Resolve the metadata response explicitly
instead, so the loading window does not depend on timing.

Fixes EMB-2405


Claude-Session: https://claude.ai/code/session_01GuH5ewGP1DzQs5jwNpTgWc

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 16:54:18 +01:00
github-automation-metabase
f24251fb4e Auto-update documentation for master (#82710)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-09-18 11:51:01 -04:00
Timofey Kachalov
7994ad35d8 Add data-apps scope to BE endpoints to prevent calling unmarked endpoints from inside a data app (#79382) 2026-09-18 19:34:58 +04:00
Timofey Kachalov
eb845c2ea6 Add version field support to data_app.yaml (#82557) 2026-09-18 18:57:25 +04:00
John Beers
3d8a3901a9 docs: update URL configuration (#82744)
[docs] Add theme keyword
2026-09-18 09:11:12 -04:00
Oleksandr Yakushev
16bd231ba7 [spec] Prevent t2.transformed/in-transforms from throwing on :model/ModelIndexValue (#79634) 2026-09-18 13:00:59 +00:00
Maksym Yakubych
89da5b2bc6 Fix unreadable low-rating data complexity badge (#82665) 2026-09-18 12:00:54 +02:00
Romeo Van Snick
91f5798258 Make SQL editor typing responsive again (DEV-3545) (#82724)
* Stop reconfiguring the SQL editor on every keystroke (DEV-3545)

* Apply native editor edits outside the keystroke (DEV-3545)

* Defer editor edits with Mantine's debounced callback
2026-09-18 19:58:46 +10:00
Jakub Chodorowicz
e49c2e60af Gdgt 3238 release sdk v2 canary1 (#82733)
release v2 canary.1
2026-09-18 11:05:32 +02:00
Mahatthana (Kelvin) Nomsawadi
496db401a1 fix: keep permissions tabs visible while data loads (#82633)
* fix: keep permissions tabs visible while data loads

The Data tab returned a bare spinner until databases, groups and
permissions loaded, so the tabs above it vanished on slow networks.
The page layout now always renders and the spinner shows in its
content area, like the Collections tab.

Fixes EMB-2392

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dg4bU1ueEbNhR2AYfm96T8

* fix: open the split permissions modal after load

The Data tab now renders its layout while data loads, so the one-time
split permissions modal could open over the spinner. Dismissing it
there turns the user setting off before the table it explains shows.
Only allow the modal once loading finishes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TuekFYoRQBgQoySZe6xB3k

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 08:05:46 +00:00
Kevin Kelleher
188c8f412f Fix failing Loki test caused by #82684 (#82713)
fix loki failure
2026-09-17 20:14:55 -07:00
Aleksandr Lesnenko
62831f5e4d infer db from query in transform_write mcp tool (#82461)
* infer db from query in transform_write mcp tool

* dedupe

* review

* fix
2026-09-17 23:20:26 +00:00
Romeo Van Snick
d5d6eba4c5 Move ErrorBoundary into common (#82624)
Claude-Session: https://claude.ai/code/session_01FeHJzstcAE34DTu4G6btjx

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 09:16:47 +10:00
Romeo Van Snick
a3b46fdd3c Return a question from the card hook, not a builder (#82602)
* Return a question from useQuestionFromCard instead of a builder

* Stop exporting the provider factory selector, which has no callers

* Memoise the connected-table link url
2026-09-18 09:16:37 +10:00
Romeo Van Snick
61c0e2915e Stop thirteen sites reaching through question.metadata() (#82552)
* Read the model's table, the list's entity type and the native editor's table from their own requests

* Take the native preview's provider from the store and the metabot table names from Lib

* Let the question carry its own metadata into derived questions and steps

* Take the pristine table URL, the native editor's databases and the last-run card off the Metadata object

* Mock the databases endpoint in the inline SQL prompt spec
2026-09-18 09:16:23 +10:00
Eric Normand
5701125c34 Per-scope checkboxes on the OAuth consent page (GHY-4555) (#82527)
* List every MCP v2 tool regardless of token scopes (GHY-4543)

tools/list filtered out tools the token lacked scope for, so a client
holding a narrow grant never saw the write, SQL or delivery tools and
had nothing to attempt or step up for. Scope stays enforced at
tools/call; the disabled-tools and client-extension filters still apply.

With scopes out of the listing, list-tools and tools-hash drop their
token-scopes parameter, and the transport's tools-hash-fn becomes
zero-arity.

* Explain MCP v2 scope denials to the model (GHY-4543)

Clients replace a scope denial with their own text ("token expired",
"Insufficient scope", "Tool execution failed"), so the model tells the
user their login expired. Server instructions now say that failure is a
missing permission, and how the user reconnects to grant it. Every tool
description ends with a static sentence naming its required permission by
its consent-screen label, identical for every token: Claude Code caches the
first description it loads for the whole session.

* Answer MCP v2 scope denials with 403 insufficient_scope (GHY-4543)

A scope denial was an HTTP 200 carrying a JSON-RPC error or an isError
result, so clients never stepped up: Claude Code only saves a step-up
scope, and mcp-remote only starts one, on a real 403 with an
insufficient_scope WWW-Authenticate challenge. The registry gate and the
four in-handler scope checks now mark their refusal, and the transport
answers a single (non-batched) message with 403 plus a challenge whose
scope is the held v2 scopes plus the required one.

* Keep a DCR client's registration scope from narrowing its step-up (GHY-4543)

Claude Code registers with the scope it read from the protected-resource
metadata, then steps up on the same client_id with the wider scope from a
403 insufficient_scope challenge. oidc-provider validates a requested scope
against the client's registered scopes, so that step-up was refused with a
400 invalid_request. Once scopes_supported is narrowed to the baseline, no
Claude Code user could reach write, SQL, or delivery.

A dynamically registered client's ceiling is now its registered scopes plus
the default grant scopes, computed on read. Registering can widen the
ceiling (a client that registers for mb:full keeps it) but never narrows
it. RFC 8707 resource narrowing is unchanged.

* Declare each MCP v2 tool's OAuth scope in securitySchemes (GHY-4543)

ChatGPT only steps up for scopes a tool declares: without per-tool
securitySchemes it re-authorizes for its narrow login scope in a loop,
and with them it re-authorizes once for held plus declared scopes.
The scheme is derived from the tool's static :scope in the cached
manifest, so it is the same for every token.

* Advertise only the baseline MCP v2 scopes to new clients (GHY-4543)

The protected-resource metadata `scopes_supported` and the 401 challenge
`scope` now list only `agent:content:read agent:resource:read`, so users
who never need write, SQL, or delivery never grant them. Claude Code and
the Claude connectors take their first-login scope from the resource
metadata; ChatGPT takes it from the challenge. All of them reach the rest
through the 403 `insufficient_scope` step-up and per-tool
`securitySchemes`.

What the resource accepts is unchanged: RFC 8707 narrowing still keeps all
six v2 scopes, and the authorization-server metadata stays wide because
Codex requests that list on every login and cannot step up.

* List every MCP v2 resource regardless of token scopes (GHY-4543)

resources/list now matches tools/list: it is token-independent, so a client can
see the resources it could step up for, and a client that pre-fetches the MCP
Apps shells before stepping up to agent:query:run does not cache them as
missing. resources/read keeps its scope gate.

* Answer MCP v2 resources/read scope denials with 403 insufficient_scope (GHY-4543)

resources/list now shows every resource, so answering a denied read with
"Resource not found" hides nothing and leaves a client with no step-up
signal. A client that pre-fetches an MCP Apps shell with a baseline token
now gets the same challenge tools/call sends, naming agent:query:run.
Unknown URIs stay -32602; inside a batch the denial is an in-band -32600.

* Lead MCP v2 tool descriptions with their required permission (GHY-4543)

Clients truncate long tool descriptions (Claude Code at 2048 characters),
so the appended "Requires the ... permission" sentence was cut from
document_write and execute_query. The model needs that sentence to name
the missing permission after a scope denial, so it now opens every
description, and the server instructions say it starts the description.

* Serve every MCP v2 resource on resources/read regardless of token scopes (GHY-4543)

Claude Desktop reads an MCP Apps shell alongside the tool call. When that
read was refused with 403 insufficient_scope, Desktop never stepped up after
the tool call's own 403; with the shell served, it stepped up automatically
and retried. The shell carries no data and, for a token without the shell's
scope, no UI credential: read-resource now withholds the credential instead
of refusing the read. Chart data stays gated by the tool call,
refresh_ui_credential, and the UI-surface route costs.

* Restore the scope check on MCP v2 data resource reads (GHY-4543)

Only the MCP Apps UI shells need to be served to any token: a refused shell
read stopped Claude Desktop stepping up after the tool call's 403. The fields
catalog carries data and has no such constraint, so reading it without
agent:resource:read again gets the 403 insufficient_scope challenge (in-band
-32600 inside a batch). UI shells stay unscoped, minting a credential only for
a token holding their scope.

* Fit MCP v2 tool descriptions under Claude Code's 2048-character limit (GHY-4543)

Claude Code truncates tool descriptions at 2048 characters, so document_write and
execute_query lost their closing guidance, including execute_query's pointer away
from execute_sql for filtered-dashboard cards. Condense both, and get_content for
margin, deferring detail to learn() topics and argument schemas. A test now holds
every listed description to the limit.

Also correct docstrings and comments left stale by the resources/read and
tools/list scope changes and by credential delivery through refresh_ui_credential.

* Spell out the scope union in the MCP v2 challenge docstring (GHY-4543)

The whitespace linter rejects the confusable Unicode character U+222A.

* Add agent:query:run to the MCP v2 baseline scopes (GHY-4543)

Claude Desktop retries a tool after a step-up over a session that does not
declare MCP Apps support, so a stepped-up visualize_query is refused and the
first chart after a step-up never embeds. Querying and charting therefore
must not require a step-up. The protected-resource metadata, the 401
challenge and default-ask-scopes now advertise agent:content:read
agent:query:run agent:resource:read; writes, raw SQL and delivery still step
up.

* Point MCP v2 chart requests at visualize_query in the server instructions (GHY-4543)

The routing line named only execute_query, and clients like Codex load tool descriptions on demand, so chart requests could miss visualize_query.

* Mint only the scopes chosen on the OAuth consent decision (GHY-4555)

The signed scope stays the offered set. The user's choice arrives in a
separate, repeatable granted_scope field and may only narrow it: a choice
outside the offer is rejected as params_tampered, and an approval that
leaves nothing granted is a 400. Offered MCP baseline scopes are always
granted. The token response already echoes the code's scope, so it now
names exactly what was granted.

* Add per-scope checkboxes to the OAuth consent page (GHY-4555)

Each offered scope is a granted_scope checkbox inside the consent form.
The six MCP v2 scopes are listed least to most harmful, with any other
scope after them in request order. The MCP baseline scopes are ticked,
disabled, and marked as always granted; every other scope, mb:full
included, starts unticked. The full-access warning now sits in the
mb:full row.

* Gate native query handles in transform_write on agent:sql:run and the kill switch (GHY-4543)

A query handle is not proof the SQL gates were spent: /api/embed-mcp/drills
stores a native query under agent:query:run alone. transform_write now runs
the same scope and mcp-execute-sql-enabled checks on a handle that resolves
to native SQL as it does on an inline native definition, matching
question_write.

* Tighten resource registration, step-up scopes, and the DCR ceiling (GHY-4543)

- register-resource! takes a closed map and strips :ui?, so a data resource
  can never be served to a token without its scope.
- step-up-scopes matches held scopes instead of looking them up, so a token
  holding a wildcard such as agent:content:* keeps the surface scopes it
  covers when a client replaces its grant with the challenged scope. It now
  takes the single required scope both callers pass.
- The default grant ceiling is applied only while dynamic registration is
  enabled, so turning registration (or MCP) off stops widening existing
  dynamic clients. The merge is now a pure function with unit tests, and an
  HTTP test pins that static clients are never widened. The legacy-client
  migration test runs with registration disabled, where the stored snapshot
  alone decides, so it exercises the migration again.

* Debounce the OAuth consent decision and require a ticked scope (GHY-4568)

The consent page gets an inline script, nonce'd and opted into a
script-src nonce so a production CSP allows it. On submit it shows a
spinner on the clicked button and disables both, carrying the decision
in a hidden input since a disabled button is not submitted, so a second
click can no longer race the first. The buttons come back when the page
is restored from the bfcache. It also keeps Authorize disabled while no
scope checkbox is ticked (GHY-4555).

* Correct MCP v2 tool descriptions, the documents skill, and server instructions (GHY-4543)

- document_write lists the valid entity-link models again.
- The documents skill said edits store new_str literally; the tool parses it
  as Markdown, as edit-replacement-parses-markdown-test pins.
- execute_query no longer claims to save cards (its handles save as cards),
  and names the previous-stage field ref shape instead of an ambiguous
  column-name string.
- The server instructions attribute the rewritten permission failure to the
  client and cover resource reads, which get the same 403.

* Tell the MCP v2 model which scopes the connection holds (GHY-4555)

The initialize instructions are now built per request from the calling token's scopes. A scoped token is told which v2 surface permissions it has and lacks, by consent-screen label; an unrestricted caller gets no list. The instructions also stop claiming the consent screen has no per-permission choices: a permission the connection lacks starts unticked, so the model warns the user to tick it.

* Note the unticked consent checkbox in the MCP v2 insufficient_scope description (GHY-4555)

Tool and data-resource 403 challenges now end their error_description with a note that the permission starts unticked on the consent screen and the user must tick it. The README describes the new consent screen and the per-connection instructions.

* Rename the agent:resource:read consent label to "Read MCP resources" (GHY-4555)

* Tighten the MCP v2 scope instructions (GHY-4555)

The general paragraph states the scope-failure guidance once; the per-connection paragraph carries only the held and missing permissions and how far to trust that list.

* Pin the MCP v2 scope-denial and UI-credential contracts in tests (GHY-4543)

Closes gaps the review found, where removing a check would stay green:
- question_write and subscription_write in-handler denials carry
  :insufficient-scope at the registry, like alert_write's.
- A denied tools/call sent as a notification is a bare 202, no challenge.
- Wildcard and unrestricted tokens get the shell credential; the fields
  catalog mints none, over the registry and over HTTP.
- tool-and-resource-scopes-agree-test asserts the credential is realized,
  since any token now reads a shell.
- advertised-scopes-are-distinct-test covers the advertised baseline, which
  is a vector and can repeat, and its docstring says so.

* Clean up MCP v2 scope-denial helpers, header quoting, and docs (GHY-4543)

- Share one scope-denial message and detail builder between tool calls and
  resource reads, one step-up challenge helper, and a common
  throw-insufficient-scope! for the four in-handler scope checks.
- quoted-string replaces " and \ instead of backslash-escaping them, since
  RFC 6750 excludes both from scope and error_description values; the 401
  challenge now quotes through it too.
- Trim docstrings to their contract and move the reasons into comments.
- The unrestricted UI credential comes from any unrestricted session (cookie,
  API key, or an mb:full token), not only cookie sessions.
- Note that the DCR ceiling applies only while registration is enabled, and
  fix stale comments, test names and strings, long lines, order-sensitive
  scope assertions, and hard-coded consent labels in tests.

* List scope IDs only in the MCP v2 instructions, have the model make the call once the user agrees, and cap instructions at 2048 characters (GHY-4555)

* Pre-tick consent scopes the same app already holds (GHY-4555)

A step-up asks for the held scopes plus the new one, and every non-baseline box started unticked, so ticking only the new scope silently dropped a held one. A scope that a live access or refresh token of the same app holds now starts ticked but stays enabled. The same app is the same client_id, the exact same https redirect, or a loopback redirect with the same host, path and client name on any port, matched against the redirect this request delivers the code to.

* Narrow the same app's tokens when a held scope is unticked (GHY-4555)

Unticking a pre-ticked scope now removes exactly that scope string from the same app's other live access and refresh tokens, so the choice holds across windows. Only scopes offered on the screen and held by the app are removed; nothing is ever added, and a token is revoked only when no scope is left. This runs at the approve decision, after the code is issued, because the authorization code row has no field to carry the declined scopes to the token exchange without a migration.

* Tell the MCP v2 model that reconnecting before a refused call won't offer the permission (GHY-4555)

* Always widen dynamic OAuth clients to the MCP scopes (GHY-4543)

Claude Code registers with the baseline scopes and steps up on the same
client_id. With dynamic registration turned off, that step-up /authorize
was answered with a raw 400. A dynamic client now always gains the six v2
MCP scopes on read, each still behind consent; the remaining agent-API
ceiling is added only while registration is enabled. Registration itself
stays blocked.

* Close the MCP v2 scope test gaps (GHY-4543)

Cover the transform_write update path's native gates, the HTTP 403
insufficient_scope challenge question_write and transform_write send from
inside their handlers, saving a drill handle minted through
/api/embed-mcp/drills, and the closed resource registration schema. Give
the locale test a positive control, merge the two tools/list
token-independence tests into one that compares the whole payload, and
drop the permission-sentence truncation test the fit and starts-with
tests already cover.

* Tidy MCP v2 scope wording and naming (GHY-4543)

Rename registry/insufficient-scope to insufficient-scope-detail so it
does not read like the transport fn of the same name. Drop the server
instructions clause telling the model to read a resource's scope off an
error clients hide. Document agent:resource:read and the handler-internal
scope checks in the module README, correct the stale comments about UI
resources gating on agent:query:run, and wrap the lines this branch left
over 120 columns.

* Let the test fallback template win over a local frontend build (GHY-4543)

The MCP Apps shell renders from the built `embed-mcp.html` when one exists,
so a worktree that has run a frontend build shadowed the inline fallback the
credential tests install. The production template has embedded no
`uiCredential` since #81041, so those tests failed locally and passed on CI,
where backend-only runs produce no build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QT1v8on6aPT2UeHCojGj8

* Tell the MCP v2 model a permission can be removed mid-session (GHY-4555)

* Drop cross-connection scope narrowing from the OAuth consent decision (GHY-4555)

Unticking a scope the same app already held used to strip it from that app's
other live access and refresh tokens. Every verified defect in this PR lived in
that path:

- the authorization code and the approved event are persisted before the
  narrowing runs, and both sit inside one `try`, so a narrowing failure answers
  400 while a redeemable code exists and the old tokens keep the scope;
- dynamic client registration is open, so an attacker can register a client
  carrying another app's https redirect -- or a loopback redirect with a copied
  `client_name`, since the port is ignored -- and strip or revoke the real app's
  tokens after phishing a single Authorize click;
- a pending authorization code is not narrowed, so redeeming one mints a
  just-removed scope straight back.

No comparable provider does this: GitHub, Google, Microsoft and Slack all leave
an existing grant alone when a new authorization asks for less. Taking a granted
permission away deserves its own flow, with its own confirmation and audit
trail; a follow-up ticket covers it.

What stays is the pre-tick: the consent page still ticks a scope the same app
already holds, so a step-up does not silently drop it, and the same-app matching
that finds those tokens is unchanged. A test now pins that an untick leaves the
app's other live tokens -- access and refresh -- exactly as they were.

Also drops the two db.clj scope-update helpers nothing else used, and corrects
two claims the removal falsifies: the MCP `initialize` instructions no longer
tell the model that another window can untick a permission mid-session (a
re-authorization without it, or a revocation, is how it goes away now), and the
MCP README says the connection's scopes are listed by scope ID rather than by
consent-screen label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Store a repeated OAuth scope once in the granted token (GHY-4555)

`scope=agent:content:write agent:content:write` was split without
deduplication, and the decision filters the grant from that list, so the
duplicate reached the token row and the token response's `scope`. The consent
page already deduplicated for display, so nothing on screen hinted at it.

Deduplicate in `scope-tokens` instead, which is the one place both the page and
the decision read the requested scopes from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Pre-tick the OAuth consent page from the app's newest live grant (GHY-4555)

The held set was the union of every live token the app had for the user. A
refresh token outlives several authorizations, so the union let an old, wider
grant keep setting the defaults: day 1 grants content:write and sql:run with a
30-day refresh token, day 5 the user re-authorizes and leaves sql:run unticked,
and on day 10 the union pre-ticks sql:run again from the day-1 token -- silently
undoing the day-5 decision every time they re-authorize.

Take the newest live grant instead, so the user's latest choice is the default.
A grant's access and refresh rows are written by one token request and share a
`created_at`, so the newest timestamp names a grant rather than a row: every row
carrying it speaks for that grant, which is what keeps the newest grant readable
once its access token has expired and only its refresh token is left. Erring
narrow is safe now that an untick only shapes the token being minted.

Also records why the loopback key ignores the port, and what that costs, next to
the branch that decides it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drop consent-page pre-ticking of already-held scopes (GHY-4555)

Every non-baseline scope now renders unticked, whatever the user already
holds. Pre-ticking needed to identify the connection being stepped up,
and the consent page cannot:

- the step-up challenge expands a wildcard grant like `agent:content:*`
  into concrete scopes, but the held check matched exact strings, so
  `content:write` started unticked and a step-up dropped it;
- refresh rotation writes new rows with a current `created_at`, so an
  older broad grant refreshed after a newer narrow one looked newest and
  re-ticked a scope the user had dropped;
- the newest app-wide grant is not necessarily the connection stepping
  up: window A holds content:write, window B authorizes later without
  it, then A steps up and gets B's defaults;
- prechecking across registrations is a consent boundary. Registration
  is unauthenticated, so a local app copying the client name and
  loopback callback path, or a client registering another app's https
  redirect, got those scopes from one generic Authorize click instead of
  the user selecting them.

Doing it properly needs grant identity carried into the authorize flow,
the redirect recorded on tokens, and wildcard-aware matching. GHY-4627
tracks that.

Removes `metabase.oauth-server.held-scopes` and the `db` helpers that
existed only for it. A test pins the removal: with live access and
refresh tokens holding `agent:content:write`, the consent page still
offers it unticked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Correct two MCP scope claims in the README (GHY-4555)

Per-tool `securitySchemes` is draft SEP-1488 (modelcontextprotocol issue
1488), supported by ChatGPT. It is not part of MCP 2025-03-26, the
version this server reports, nor of the final 2026-07-28 tools spec, so
other clients learn a missing scope from the 403 instead. The comment
above `default-ask-scopes` implied it was generally available too.

Automatic scope step-up is likewise defined in MCP 2026-07-28, so
clients are not required to implement it against this server: a scope
left unticked can trigger another step-up in clients that support it,
while others need manual reauthorization.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Drop the per-connection permission list from the MCP v2 instructions (GHY-4555)

The list caused the failure the "make the call anyway" clause was added to
fix: with it in context the model sometimes refused a write without calling
the tool, so no 403 came back, the client saved no step-up scope, and the
user's re-authorization offered only the baseline — the permission never
appeared as a checkbox. Without the list and without that clause, the model
calls the tool, reads the 403, names the permission and gives the reconnect
steps.

So the `initialize` instructions are one static string again: general
scope-failure guidance, plus one sentence that every optional permission on
the consent screen starts unticked, so the user re-ticks the ones they want
to keep. `transport.clj` goes back to a plain `:instructions` option.

* Tell the MCP v2 model the tool list is not a permission grant (GHY-4555)

Every tool is listed whatever the token holds, but a scope-filtered list is
the conventional design and the protocol has no field to signal ours. Asked
what a baseline connection could do, the model read the roster as a grant and
answered that it held every agent scope, reasoning that the server filters
tools by granted scope -- true of Metabase until the filter was removed.

Only a failed call reveals a missing permission, so say that.

* Fix counterfactual docstrings and pin the offered scope non-blank (GHY-4555)

Two docstrings stated the behavior the dedup prevents as though it were the
behavior it has, so a reader chasing the invariant learned the wrong one.
Both now say "would be stored twice".

scope-to-grant gains a postcondition. The decision endpoint answers a nil
grant with 403 params_tampered, which is only the right status while the sole
way to reach it is a choice naming an unoffered scope. That holds because
scope-to-grant throws rather than returning a blank offer; the postcondition
turns a future relaxation into a failing assertion instead of an honest
approval reported as tampering.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:34:41 -05:00
Kyle Doherty
5349fb75d7 clean up crumbs 2026-09-17 16:30:13 -04:00
Ryan Laurie
ecb8fe82b2 Merge automated PRs with the automation user's token so CI runs (#82701)
GitHub starts no workflow run for a push made with `GITHUB_TOKEN`, so
enabling auto-merge with it lands backports on release branches, and
translation updates on master, with no CI at all. Approving still uses
the other token, since a token cannot approve its own pull request.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:19:51 +00:00
Kevin Kelleher
40ebe0bb9f Fix static viz Row Chart failures on remapped columns (#82684)
* make remapping idempotent

* copy the existing remapping so it doesn't get mutated
2026-09-17 19:55:42 +00:00
Eric Normand
0700179dad GHY-4542: Reject unregistered and empty OAuth scopes; fail closed on scope-less bearer tokens (#82498)
* Reject unregistered scopes in OAuth dynamic client registration

POST /oauth/register is unauthenticated and stored any scope string the
client sent. A client's registered scopes are the ceiling /oauth/authorize
checks requests against, so a self-nominated `*` or `agent:*` could later
be requested and granted, and scope-matches? treats both as wildcards.

Registration now answers 400 invalid_client_metadata, naming the offending
scopes, when any requested scope is not a registered scope. An omitted
scope still gets the default ceiling, and `mb:full` is still accepted.

* Reject unregistered scopes at OAuth authorize

Clients that registered a wildcard such as `*` before registration
validated scopes can still request it, since /oauth/authorize only checks
a request against the client's own registration. /authorize now answers
400 invalid_scope when any requested scope is not a registered scope. The
check runs before resource narrowing, which would otherwise drop the
scope when `resource` is sent and keep it when it is not.

Both endpoints now use one error_description that points the client at
the authorization server metadata listing scopes_supported, instead of
echoing the scopes it sent.

Clients registered on v0.60-v0.61 still hold agent:database:read,
agent:table:read, and agent:metric:read, which are no longer registered;
requesting them now fails. The authorize test fixtures move from the
unregistered "profile" to agent:content:read.

* Reject OAuth authorize requests without a scope

A request to /oauth/authorize with no scope rendered a consent screen
listing no permissions and minted a token with no scopes. It now answers
400 invalid_scope whether scope is absent, empty, or whitespace. A blank
scope is dropped before the provider validates the request, so an unknown
client or mismatched redirect URI is still reported first.

The error_description names no request input and points at the
authorization server metadata listing scopes_supported.

The OAuth authorizations E2E spec drove consent without a scope; it now
requests agent:content:read.

* Reject an empty scope in OAuth dynamic client registration

A registration that sent scope as null, an empty string, or whitespace
was stored with no scopes, and such a client can never authorize now
that /oauth/authorize requires a scope. /oauth/register answers 400
invalid_client_metadata instead, with an error_description pointing at
scopes_supported.

Omitting scope still registers the client with the default ceiling, which
is what MCP clients that register without a scope rely on.

* Fail closed on OAuth bearer requests without token scopes

Nil :token-scopes means scope-unaware auth, and sessions, API keys,
the scope middleware, the agent API, and the MCP transport all let it
through as unrestricted. For an OAuth access token, only an empty set
surviving every hop kept a scope-less token from becoming unrestricted.

The session middleware no longer authenticates an access token with no
scopes, and marks requests it does authenticate with an OAuth token as
:authenticated-via-oauth?. Behind it, enforce-scope and
ensure-scopes-checked answer 403, the agent API stops defaulting nil
scopes to unrestricted, and the MCP transport answers its invalid_token
401 for an OAuth-authenticated request with no scopes.

The marker is set by the session middleware rather than inferred from an
Authorization: Bearer header, which the agent API JWT path also uses and
which a session request may carry alongside its session. Sessions, API
keys, JWT, and the MCP Apps UI credential never set it.

* Redirect OAuth authorize errors to the client per RFC 6749

GET /oauth/authorize answered every validation failure with a 400 JSON
body, which strands the user on a raw error in their browser tab and
leaves the client waiting for a callback that never arrives.

The endpoint now looks up the client and checks the redirect_uri is an
exact match for one it registered before validating anything else. If
either is missing or invalid, it keeps the non-redirect 400, as RFC 6749
section 4.1.2.1 requires. Every other failure redirects to that
registered redirect_uri with error, error_description, state, and the
RFC 9207 iss parameter, built by the library's denial redirect builder.

Error codes: invalid_scope for unregistered, missing, fully narrowed,
and unregistered-for-the-client scopes (the last was mislabeled
invalid_request); unsupported_response_type for a response_type the
client did not register; invalid_target for an invalid resource
indicator; invalid_request for PKCE problems and malformed parameters.

* Send RFC 6750 bearer challenges on scope-denial 403s and agent API 401s

RFC 6750 section 3 requires a resource server to answer a bearer token
that does not grant access with a WWW-Authenticate challenge. The scope
middleware's 403s, including the fail-closed ones for OAuth requests
without scopes, sent only a JSON body, so an OAuth client could not tell
it should re-authorize for more scope.

enforce-scope and ensure-scopes-checked now add
  Bearer error="insufficient_scope", scope="...", error_description="..."
with scope naming the endpoint's required scope, and omitted in
ensure-scopes-checked, where the endpoint declares none. The format and
value sanitizing match the MCP transport's insufficient_scope challenge.
The JSON bodies are unchanged.

The agent API's 401 for a bearer token that does not authenticate now
carries Bearer error="invalid_token".

* Answer malformed OAuth resource indicators with client errors, not 500s

A resource value that is not syntactically a URI, such as
"http://bad uri", made oidc-provider's resource check throw a
URISyntaxException rather than an ex-info. Nothing caught it, so
/oauth/authorize, /oauth/authorize/decision, and /oauth/token all
answered a client input error with a 500.

Each endpoint now checks that every resource indicator parses before
handing the request to the library:

- /oauth/authorize: once the client and redirect_uri are validated, an
  RFC 8707 invalid_target error redirect. Otherwise the existing
  no-redirect 400.
- /oauth/authorize/decision: the existing 400 invalid_request, with no
  redirect, since its parameters are untrusted until the signature
  verifies.
- /oauth/token: a 400 invalid_target.

Only URISyntaxException is caught, and only while parsing the resource
values.

* Send a plain Bearer challenge on agent API 401s without a bearer token

RFC 7235 requires a WWW-Authenticate challenge on every 401. The agent
API's missing_authorization 401 (no credentials) and
invalid_authorization_format 401 (a non-Bearer Authorization scheme)
sent only a JSON body.

Both now carry WWW-Authenticate: Bearer. There is no error attribute,
because RFC 6750 section 3.1 says a challenge to a request with no
bearer token should not include an error code. Status codes and bodies
are unchanged.

* Keep OAuth authorize errors non-redirecting

Delivering /oauth/authorize errors as a redirect to the client's
registered redirect_uri made Metabase a zero-click open redirector.
Dynamic client registration is unauthenticated, so anyone can register
a client whose redirect_uri points at their own site, and a single
/oauth/authorize link on the trusted Metabase host would then send any
logged-in user there with no consent step. RFC 9700 section 4.11 says to
answer with an error rather than redirect when the redirect URI is not
trusted.

Every error from GET /oauth/authorize is a 400 JSON body rendered in the
user's browser again, with no Location header. The login redirect for
unauthenticated users is unchanged.

The error codes stay accurate: a scope outside the client's registered
set is invalid_scope, an unregistered response_type is
unsupported_response_type, and an invalid resource indicator, including
one that is not syntactically a URI, is invalid_target. Malformed
resource indicators still cannot reach the 500 they used to.

* Drop unregistered scopes at /oauth/authorize instead of refusing

Registration and authorization see the same problem from different
sides. An error from /oauth/register reaches the client program at setup
time, where it can act on it, so that endpoint keeps rejecting
unregistered scopes outright. An error from /oauth/authorize lands in a
user's browser tab as JSON while the client waits for a callback, and it
also breaks step-up for a client that still holds a scope shipped in
v0.60-v0.61 and since removed from the registry.

/oauth/authorize now filters the requested scopes down to the registered
ones and carries on with what survives, which RFC 6749 section 3.3
allows. Filtering runs before resource narrowing, so a request behaves
the same with and without a resource indicator, and the consent page and
the minted token carry exactly the surviving scopes. Least privilege is
unchanged: an unregistered scope, including a wildcard a client
registered before registration validated scopes, still can never be
granted.

A request in which nothing survives the filter is refused with
invalid_scope and its own description, distinct from the one for a
request narrowed away by its resource indicator. A missing, empty, or
whitespace-only scope is unchanged.

* Enforce the scope rules where the authorization code is issued

The HMAC over the consent form is keyed by the CSRF token, which the
consent page prints for the user, so the user can recompute it over
whatever scope they like. It proves no third party tampered with the
form; it does not prove the scope was ever validated.
/oauth/authorize/decision is what issues the code, so it now applies the
scope rules itself instead of trusting the signature.

Both endpoints call one scope-to-grant function: refuse a missing or
blank scope, drop the scopes that are not registered, refuse when none
survive, and grant exactly what is left. A denial grants nothing and
skips it. /oauth/authorize behaves as before; a refusal at the decision
endpoint keeps its existing 400 JSON shape and never redirects.

A correctly signed form asking for a scope the consent screen never
showed was previously honored: a re-signed wildcard minted a token
carrying it, and a re-signed blank scope minted one with no scopes at
all.

* Tidy the OAuth scope and challenge code

Follows the style review. No behavior change except one described below.

- narrow-scope-to-resource's docstring described a contract it no longer
  has: that nil means both "nothing requested" and "nothing survived",
  that the caller may drop the parameter, and that narrowing can never
  turn a valid authorization into a rejected one. It returns nil when no
  requested scope survives, and the caller refuses.
- One check-resource-parseable! replaces the same pre-check written three
  times with three different ex-data shapes, and the note about why it
  exists sits on unparseable-resource! once instead of at each call site.
- invalid_target now always carries the description saying what a valid
  resource indicator looks like. It used to depend on where the error
  came from: the pre-check said so, while a relative indicator or one
  with a fragment fell back to the generic description. This is the one
  behavior change, and the authorize test that pinned the generic
  description is updated.
- The three inline invalid_client_metadata 400s in /register go through
  one helper.
- The error_description strings and their helpers move out of the
  module's core namespace, where they were public with a single caller,
  into the api namespace as private defs grouped together.
- The MCP transport's two invalid_token 401s were identical; they share
  one response now.
- The agent API's error-response takes the WWW-Authenticate challenge as
  an argument, which is what all three of its callers were adding
  afterwards.
- Dropped an inaccurate comment about parameter order matching the MCP
  transport's challenge, some rationale that belongs in line comments
  rather than docstrings, and a double negative.

* Tighten the OAuth scope and challenge tests

Follows the style review.

- quoted-string is now exercised directly with a quote, a backslash and
  non-ASCII input. The charset assertion it replaces only ever saw
  constants, so it could not fail.
- The agent API's invalid_token 401 test moves to the agent API tests,
  where the behavior lives, and its no-credentials case goes: that is
  already covered there.
- Deleted bearer-token-without-scopes-gets-no-tool-access-test. Its 401
  comes from the transport's invalid-bearer branch, because the session
  middleware declines a scope-less token before the transport sees it, so
  it passed with or without the fail-closed branch and duplicated
  bearer-bridge-refuses-token-without-scopes-test. The branch itself is
  covered by oauth-request-without-token-scopes-is-refused-by-the-
  transport-test, which invokes the handler directly.
- The DCR unregistered-scope test compares the whole error_description
  instead of a substring plus assertions that cannot fail.
- Scope-denial tests assert the exact error code each case produces
  rather than accepting either.
- A test description stated its own history; it states the behavior now.
- Wrapped lines over 120 characters.

* Declare :authenticated-via-oauth? in the request schemas

Master closed ::current-user-info and added a closed ::request schema
enumerating every key the middleware stack may attach. This branch marks
a request the OAuth bearer bridge authenticated with
:authenticated-via-oauth?, which both schemas then rejected as an extra
key: the bridge threw on its return value, so no OAuth bearer request
could authenticate.

* Describe only our own resource indicators as malformed

The error_description helper answered every invalid_target that carried no
description of its own with the one describing a malformed resource indicator.
oidc-provider raises that code without a description for a different condition:
a refresh whose resource is outside the original grant. A client sending a
well-formed absolute URI was told its URI was malformed.

The endpoint's own check now carries its description in the exception, and the
helper falls back to the endpoint's generic description for anything else. That
check also validates what it claims to describe, so a relative indicator or one
with a fragment is answered by it rather than by the library.

* Answer 401 when no JWT provider is registered

The agent API's stateless-JWT fallback dispatches auth-identity/authenticate
on :provider/jwt. That provider ships only in EE, so on OSS nothing derives
the keyword into the auth-identity hierarchy and the dispatch throws
UnsupportedOperationException. Every bearer token the OAuth bridge declines
falls through to it, so the agent API answered 500 with a stack trace instead
of the RFC 6750 invalid_token challenge it owes the request.

Treat an unregistered provider the way a disabled one is treated: the same
jwt_not_configured result, which enforce-authentication already renders as a
401 carrying the invalid_token challenge.

Also stop bearer-bridge-refuses-token-without-scopes-test from holding
with-temp's rollback-only transaction open across its four HTTP round-trips.
The request runs on a conveyed connectable, so anything in its path that
commits implicitly invalidates the savepoint and the scope exit throws
instead of rolling back.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 13:57:02 -05:00
Nathan Voxland
e96a5d884d Improve OSS include_editable_data_model logic (#82451) 2026-09-17 13:40:38 -05:00
Nathan Voxland
587b659ee7 Don't include archived cards in embedded dashboards (#82529) 2026-09-17 13:39:39 -05:00
Kyle Doherty
5073c20eb1 Metabot conversations show up in the Open list
Starting a conversation with Metabot now puts it in the rail's Open list, next
to the questions and dashboards you have open, so the sidebar panel's thread is
reachable again after you navigate away from it.

- `getStartedConversations` lists the conversations with at least one message,
  in creation order. Empty ones are skipped — an agent always holds a
  conversation, so listing those would show a row for Metabot merely existing —
  and the SQL-suggestion and explorations agents are excluded, since they answer
  in place and have no conversation page to link to.
- Rows link to /metabot/conversation/:id. The title is generated after the first
  answer, so a row starts as "New conversation" and renames itself in place when
  the title lands.
- The selector holds its result identity while ids and titles are unchanged, so
  a streaming answer does not re-fire the effect on every token.
2026-09-17 13:19:43 -04:00
Kyle Doherty
9d6d4c656e Raw data becomes a drawer with a warehouse tree
Data gets the same treatment as collections: one rail row, no section heading,
and a drawer holding the thing itself rather than a link to a page about it.

- The row reads Raw data and opens a drawer with the warehouse as a tree:
  databases, their schemas, and the tables in them. Every row links to the
  browse page it stands for, so the tree is a faster route to the same
  destinations, and any node can be expanded in place instead.
- Children are fetched only when a node is expanded, so opening the drawer costs
  one database list and nothing else.
- Tables with virtual ids (cards in the Saved Questions database) are left out:
  they have no /table/:slug route to link to.
- The Add data action moves from the old section header into the drawer header.
  `BrowseNavSection` and its spec are gone, along with the
  `expand-browse-in-nav` setting it read.
2026-09-17 13:08:57 -04:00
Andrei Simionescu
4b6744c1f4 Apply the chat system prompt to Metabot in Slack (#82639) 2026-09-17 18:06:54 +01:00
Kyle Doherty
2f02a29249 Data only in Unofficial, Getting Started collapsed, Trash in the drawer
- The Data section (Databases) moves inside the Unofficial branch. Official is
  curated content; browsing the warehouse belongs to the creation half.
- Getting Started starts collapsed, so the rail opens on the user's own work
  rather than on onboarding.
- Trash moves to the bottom of the collections drawer, where the things it holds
  were deleted from, instead of being a permanent rail row.
2026-09-17 13:02:22 -04:00
Andrei Simionescu
4c5146d536 Don't feed Metabot queries the user has no permission to run (#80083)
* BOT-1956: Fix permission-check gaps in transform/query export

Card/measure/segment export lookups routed through the loud, audit-logging
read-check even for routine filtering, one query-export call site defaulted
to the unchecked store, and a withheld query left a dangling header instead
of saying so.

* BOT-1956: Audit the permission check on client-supplied query ids

Ids arriving inline in the viewing context are a real access attempt, so the database gate
and the card ids inside the query both keep the audited read-check while stored-query export
stays quiet. export-query-for-llm now takes an explicit content store, so every call site
shows which one it picked.

* Don't audit permission refusals the caller discards (#79969)

* BOT-1956: Resolve -1337 to the real database and gate transform sources

The virtual id passed the database gate as a 404 while the export then
resolved it to the source card's real database, so the gate now resolves
first and fails closed on ids it cannot. Transform sources get the same
gate, queries and charts seeded from the viewing context keep the audit
trail when a read_resource denial withholds them, and the withheld
message no longer blames the database when a card inside the query was
the refusal.

* BOT-1956: Drop the em dashes from two docstrings

* BOT-1997: Don't feed Metabot queries the user has no permission to run

* BOT-1997: Shorten the comments on the Metabot permission checks

* BOT-1997: Close the two remaining fail-open paths in the query export

Exports now defer to the query processor's can-run-query?, the same
check the save path uses, instead of a database-wide can-query?
shortcut plus SQL analysis. For native stages that means the
database-level query-builder-and-native rule with no parsing: the
analyzer was best-effort and could return an incomplete table set with
no completeness signal, so a clean analysis never proved the query was
authorized, and unsupported drivers withheld queries from fully
authorized users. Deferring to the QP contract also keeps queries
sourced from saved questions working; those authorize through the
card's collection, not through database query permission. The
name-resolution checks (queryable tables, sandbox-visible fields,
readable cards) stay on top of the run check.

The same check now also covers conversation state. read_resource used
to export a stored query after only a database read-check, so a user
who could read the database's metadata but not run the query could
recover resolved table and field names through metabase://query/{id}
and metabase://chart/{id}. Both fetchers now share the viewing-context
formatter's exportable-query? check.

* BOT-1956: correct three stale comments

The withheld-message docstring claimed a deleted database is withheld too, when it
deliberately renders. The content-store test docstring counted three pinned properties
while listing four, and the repair comment called every pass best-effort when only the
mini-resolve lookups are.

* BOT-1997: authorize conversation queries by run permission alone

The query and chart resources read-checked the raw :database before the run-permission
check, which broke the two cases the canonical check exists for: a collection-only user
running a saved-question query got a 403 from a database they never touch, and a legacy
-1337 database id 404ed before normalization could resolve it through the source card.
The read-check is gone and the viewing-context predicate now lets normalization resolve
-1337 instead of gating on the raw id.

The source-card tests also stop granting manage-table-metadata, which had made the
database readable and hidden exactly these cases; they now run with no database
permission at all, over both the real and the -1337 id, alongside a new refusal test
for a card in an unreadable collection.

* BOT-1997: Hide transform source queries the user can't run

Reading a transform is authorized per database, but rendering its source resolves
table and field ids to names, so the details tool and the transform resource now
drop the query unless the user could run it. The viewing-context path also stops
normalizing twice and logs a query it can't permission-check at debug, instead of
letting the calculator log an error on every prompt.

* BOT-1956: make the query export gates table-granular

* BOT-1956: fail closed when a native query can't be analyzed

* BOT-1956: Fold the two query database gates into one with an audited? flag

* BOT-1997: Calculate perms inside the try, assert the 403 text in tests

* BOT-1956: Check exports with can-run-query? instead of parsing native SQL

* Build the saved-question test card with Lib instead of the deprecated mbql-query

* Use metabot.db/database-exists? instead of a direct Toucan call

* BOT-1956: Name can-run-query?'s flag for the errors it rethrows

* BOT-1956: Suppress repair's refusal auditing at its call site

* BOT-1956: Keep :client-ids out of the conversation state

The ids describe the viewing context of the request being served rather than the conversation, and final-state-part was handing them back to the client as if they were state.

* BOT-1956: Gate a query export with one Database read and one normalization

* BOT-1956: Audit a chart's refusal against the query it was built from

* BOT-1956: Make run permission the whole gate on a query export

Reading the database was checked on top of it, which withheld a saved question from a user who can read the card and run it but has no grant on the database behind it. Dropping that check also leaves one Database lookup where there were two, so a deletion landing between them can no longer escape as a 404.

* BOT-1997: Render a query we could not permission-check rather than hiding it

A query that will not normalize, or whose permissions will not calculate, was reading as a refusal and disappearing from the transform output and the conversation resources, admins included. It now comes back without a metadata provider and renders through the paths that resolve no ids to names, which is what master did with its EDN fallback.

* BOT-1997: Cover the cases the export gate decides on a transform source

* BOT-1997: cljfmt

* BOT-1997: Make the unresolved fallback actually withhold names

A query that will not parse never reached the fallback, because normalization degrades to an empty map unless it is asked to be strict, so those queries still read as refusals and disappeared. Transform rendering then threw away the checked query and its provider and resolved the source a second time through an unrestricted provider, which put resolved names back in front of the model for a query whose table or sandbox lookup had thrown. Native SQL needed its own answer either way, since its table and column names are already spelled out in the text and having no provider hides nothing, so it stays out until the check has actually run. Rendering now happens where the check's result is still in hand, and an export failure falls back to the query the caller passed rather than the normalized form.

* BOT-1956: Keep a client query's provenance for as long as the query lives

Carried over from the review on #79877, which merged before this was picked up. The ids of queries and charts the client sent were request-local, so a tool could persist one of those queries and a later turn read it back after it had left the viewing context, by which point it looked tool-written and a refusal to show it went unaudited. They live in conversation state again, declared in the state schema this time, and each turn adds to what earlier turns recorded.

* BOT-1997: Tell a check that was never needed from one that could not be made

Both came back as a query with no metadata provider, which conflated two different situations. A transform whose source database has been deleted has nothing behind it to resolve, and an admin is supposed to still read that source while repairing it, but it was being withheld like a query whose permissions had thrown. Going the other way, native SQL was only held back when the source had a single stage, because the check for it was the helper that extracts a complete query's SQL and deliberately gives up on multi-stage queries, so a native stage under an MBQL stage still printed its SQL in the unresolved fallback. The two now come back distinctly, and native is recognised by looking for a native stage anywhere in the query.

* BOT-1956: Take client ids back in the shape the app DB returns them

JSON has no sets, so state read back out of metabot_message carries the ids as a vector, and the agent entry point validates its input before memory initialization turns it back into a set. Every conversation with a stored turn therefore failed to start its next one under dev and test instrumentation. The state schema accepts either shape now and normalization settles it, and the test covers the round trip rather than handing a set straight in.

* Hide native SQL from the viewing context when perms can't be checked

* Use with-dynamic-fn-redefs in the sandboxed field transform test

* Hide native SQL from read_resource when perms can't be checked

* Treat an unresolvable database id as an unchecked query

---------

Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>
2026-09-17 18:01:18 +01:00
Kyle Doherty
3b9a1df5ac Collections move to a slide-out drawer; the rail lists what is open
The Unofficial rail stops being a place to browse collections and becomes a
place to see what you are working on, matching the prototype's explore rail.

- Collections are now one row that slides a drawer out from the right edge of
  the rail, floating over the content: the tree, other users' personal
  collections and the create action all move into it. The drawer closes on
  Escape, on clicking outside the rail, and on navigating — picking something is
  the drawer's whole job, so it gets out of the way afterwards. The
  `expand-collections-in-nav` setting is no longer read.
- Opening a question, model, metric or dashboard adds it to an Open list in the
  rail, so several things can be held at once and switched between. Closing a
  row only unlists it; the entity is untouched and reopening restores the row in
  place. State is page-session only (`openNavItems` in the app slice), like the
  section switcher.
- Models and Metrics are gone from Data, which is now just Databases. The browse
  list pages stay routed; they are reached from content rather than from a
  permanent nav row.
2026-09-17 12:40:22 -04:00
Kyle Doherty
7e9ffc83ce Remove the top app bar; rail is always shown, header owns search and account
The app bar is gone. In its place the content area gets its own header —
where you are on the left, search and the account switcher on the right —
matching the prototype, where the rail runs the full height of the window and
each page carries its own header.

- New `AppPageHeader`, rendered once above the scrolling content for every
  non-admin page, holding the collection breadcrumbs, the search button and the
  app switcher. `getIsAppBarVisible` becomes `getIsPageHeaderVisible` and keeps
  gating admin / Data Studio / Monitor / fullscreen / embedding.
- The New button moves to a footer pinned over the bottom of the rail, and the
  Metabot button and git-sync controls move into the rail, all inside the
  Unofficial half: that is the creation section, and the prototype's rail footer
  belongs to creation too.
- The logo, the sidebar toggle and the whole open/close/collapse-by-pathname
  machinery are deleted: `isNavbarOpen`, its three actions, `getIsNavbarOpen`,
  `isNavbarOpenForPathname`, `useCloseNavbarOnMount`, and every caller that
  closed the rail on mount or on entering a sidebar. The rail no longer changes
  with the entity you are looking at.
- Consumers that positioned themselves around a collapsible rail now assume it
  is always there: the bulk-action toast is always offset by the rail width, the
  detail view always subtracts it from the viewport, and the query builder
  header always uses its narrow-viewport layout.

Question lineage and the detail-view row nav went with the app bar; the
prototype puts that context in each page's own header rather than in app chrome.
`SearchBar` (the inline field used only by interactive embedding's app bar) is
left in the tree but is no longer reachable.
2026-09-17 12:29:50 -04:00
Chris Truter
e28ae43d1d Various Slackbot upload improvements (#82216)
* Tidy up slackbot CSV upload handling

Report a file that cannot be staged on disk as one failed file rather than
letting the IOException escape to the async handler, which only logs and leaves
the Slack user with no reply at all.

Take the Slack client from the caller instead of re-reading the app token per
file, fetch the uploads Database once, build the upload target in kebab-case,
and express pre-flight failures as system messages so the API namespace stops
hand-rolling its own.

* Trim the upload docstrings and comments to their contracts

Drop the note about the upload module's private `allowed-extensions`: nothing here
relies on that set, so it would go stale silently. The `csv-file?` test asserts
`txt` is not uploadable instead, which fails if anyone widens the set.

* Polish Slackbot upload handling

* Test attachment-only upload failures

* Clarify Slackbot upload return contracts

* Regenerate the modules config after dropping the Database lookup

* Tighten the upload docstrings and comments

* Close the response stream when a redirect Location will not resolve

URI.resolve throws IllegalArgumentException on a malformed Location, and redirect-target runs
in the let binding that computes target, before the cond reaches either .close branch. A Slack
file host answering with a malformed Location therefore leaked the response stream.

Returning nil instead folds the case into the existing "Unexpected response" branch, which
closes the body.

* Cover remote attachments end to end, and say nothing when there is nothing to say

A message carrying only a file stored outside Slack had no route-level coverage: the unit
tests reached remote-file? and the history message, but nothing exercised the reply path
that replaced master's refused-files-message.

A file_share event may also carry an empty :files vector, which with blank text left
handle-message-file-share posting an empty Slack message. It now posts nothing.

* Name an unsupported file type, and stop suggesting a retry for failed uploads

Retrying a failed upload fails the same way. The 415 from create-csv-upload! is the one error safe and useful to
explain, so say the contents don't look like a CSV or TSV.

* Word the unsupported file type message to cover the filename too

create-csv-upload! also answers 415 for an extension it doesn't accept, such as data.CSV, so the message can't
blame the contents.

* Return only the upload history, and describe the size limit as the bot's own

* Show the upload module's own 4xx errors to the user

* Port the upload failure message cases from #81202 to upload-file!

* Count every attached file in the Slack upload counter, under what became of it

Size rejections, unsupported and remote files, and unavailable uploads were never counted, so the error label missed the most common outcomes. Also stop formatting the model id with %d in the success log, where a nil id would have turned an upload into a reported failure.

* Point the user at the server logs when an upload fails for an unexplained reason

Everything that reaches the generic message needs an admin to read the logs, as the #81202 wording said.
2026-09-17 16:20:55 +00:00
Andrei Simionescu
d37f1a9dd3 Record which provider served each LLM call (#82280) 2026-09-17 17:19:48 +01:00
Ryan Laurie
1a32584bb8 Improve App DB Test Sharding (#82538) 2026-09-17 10:02:46 -06:00
Kyle Doherty
35bf6ca092 Official/Unofficial nav split, prototype metric & model detail pages
Ports the load-bearing parts of the consumption/creation split prototype.

Nav
- Two-segment Official/Unofficial switcher at the top of the main rail,
  backed by a `navSection` app-state slice (page-session, not persisted).
  Section is seeded from the URL for deep links, then user-driven, and
  remembers the last path per section for the life of the page.
- Official renders the Library and official-collection trees with their
  *items* hanging off each collection; Unofficial keeps Home, Bookmarks,
  the collections tree, data apps and Trash. The Data links render in both.
- Official membership is the item's own collection being marked official or
  the item living in the Library. Authority level does not inherit in
  Metabase, so neither does this. There is no authority-level search
  parameter, so the official half is filtered client-side.

Browse
- /browse/metrics and /browse/models get the prototype shell (icon + title,
  dot-separated meta, description paragraph) via a shared BrowsePageLayout,
  with the table inside a bordered panel and a header button that flips the
  section. No tabs - the nav switcher is the control.
- Both tables gain a non-sortable Owner column. Model rows now open the
  model detail page rather than the query builder.

Metric page
- The pill-tab bar is gone; the About route is the whole read view: headline
  figure with prior-period and year-over-year deltas, chart, dimension pills,
  then Definition / Breakdowns / Dimensions / Used by / History panels beside
  a sticky fact rail. The six routes stay so Data Studio's re-hosting keeps
  working, reachable through a Back to metric link.
- Used by embeds the existing EE dependency graph rather than a port of the
  prototype's own graph.

Model page
- New /model/:slug/detail with Fields / Definition / Used by / History and
  the same fact rail; the index no longer redirects to actions.

Avatars
- UserAvatar draws a deterministic DiceBear glyphs tile from a stable seed
  instead of rendering initials on a brand-coloured div, so the same person
  carries the same avatar everywhere. The bg colour plumbing it replaces is
  removed, along with the now-dead colour helpers and userColor props.

Notes
- TrendInfo/AboutVisualization are deleted: the headline card now owns the
  figure and its deltas, and printing both would have doubled the number.
- The metric dataset-value helpers moved to common/metrics so the metrics
  feature can use them without importing from browse.
- ModelHistory/ModelUsedBy are local to the models feature because module
  boundaries forbid models -> query_builder imports.
2026-09-17 11:59:28 -04:00
Chris Vandoodewaard
d3ce9de3cc Bump backend deps for open security alerts; define core-provided deps in one place (#82518)
* Bump backend deps for open Snyk alerts; define core-provided deps once

Security fixes:
- athena: netty 4.1.137.Final -> 4.1.138.Final (CVE-2026-89044, HTTP request
  smuggling in netty-codec-http). Stays on the 4.1 line because the Athena JDBC
  driver's async AWS SDK path is not binary-compatible with 4.2.x.
- snowflake: pin zstd-jni 1.5.7-16. snowflake-jdbc-thin 4.0.2 pulls 1.5.6-5,
  which has eight open out-of-bounds read alerts, all fixed in 1.5.7-14.

t-digest (CVE-2026-87822, CVE-2026-87962) is left alone deliberately: Snyk lists
no fixed version, everything from 3.1 up is affected, and it cannot be excluded
because kixi.stats.core requires kixi.stats.digest, which imports TDigest at the
namespace level. Both CVEs are in MergingDigest.fromBytes and need an
attacker-supplied serialized digest; we only ever build digests in-process and
never deserialize one, so we are not exposed. Those two alerts need a dismissal.

Per review feedback on #82261: driver uberjars are stripped of anything core
already ships (build.plugin-uberjar/prune-provided-libs!), so a driver
re-pinning a core-provided library has no effect on what loads, still lands the
artifact in that driver's generated pom.xml for Snyk to alert on, and adds
another place to bump. Replaced 22 such declarations across 7 drivers with
:exclusions on the transitive dep that drags them in, so the version is defined
only in the root deps.edn.

Verified by resolving every driver basis before and after and diffing the
effective shipped set (basis minus core-provided libs): only the two intended
security bumps move, every other driver is byte-for-byte identical. All 8
touched drivers build and pass verify-driver. Regenerated poms confirm all 15
core-provided artifacts are now declared in the root pom.xml and in zero driver
poms.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Update modules/drivers/clickhouse/deps.edn

Co-authored-by: Chris Truter <crisptrutski@users.noreply.github.com>

* Exclude httpcore5 and bcutil-jdk18on to close scan-only version mismatches

Per review on #82518:

- httpcore5: databricks-jdbc-thin 3.4.1 declares 5.3.6 and libthrift 0.24.0
  declares 5.2, while the root deps.edn pins 5.4.3. Only httpcore5-h2 was
  excluded, so httpcore5 still landed in the generated databricks pom.xml --
  and excluding it from the driver alone would have left libthrift's 5.2 in the
  standalone POM for scanners to flag. Excluded from both, matching what
  clickhouse and hive-like already do.

- bcutil-jdk18on: databricks-jdbc-thin declares 1.84 and snowflake-jdbc-thin
  4.0.2 declares 1.82, but both resolve to 1.85 transitively via the root's
  bcpkix-jdk18on 1.85. Same scan-only mismatch as bcpkix/bcprov, which were
  already excluded; added bcutil alongside them in both drivers.

No change to what either driver ships -- these artifacts are core-provided and
already stripped from the driver uberjars by prune-provided-libs!.

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 <crisptrutski@users.noreply.github.com>
2026-09-17 11:43:38 -04:00
1627 changed files with 110011 additions and 66623 deletions

View File

@@ -84,6 +84,7 @@
:metabase/validate-defendpoint-route-uses-kebab-case {:level :warning}
:metabase/validate-defendpoint-query-params-use-kebab-case {:level :warning}
:metabase/validate-logging {:level :warning}
:metabase/validate-mb-app-db-migrations-test {:level :warning}
:metabase/validate-mb-driver-tests {:level :warning}
:metabase/validate-mb-once {:level :warning}
@@ -319,6 +320,12 @@
{metabase-enterprise.entity-retrieval.core/search-unfiltered {:message "Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
metabase.entity-retrieval.core/search-unfiltered {:message "Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
metabase.entity-retrieval.mirror/search-unfiltered {:message "Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
metabase-enterprise.transform-testing.expectations.equals/->EqualsRows {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
metabase-enterprise.transform-testing.expectations.equals/map->EqualsRows {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
metabase-enterprise.transform-testing.expectations.equals/->EqualsSql {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
metabase-enterprise.transform-testing.expectations.equals/map->EqualsSql {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
metabase-enterprise.transform-testing.expectations.empty/->Empty {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
metabase-enterprise.transform-testing.expectations.empty/map->Empty {:message "Build an expectation with metabase-enterprise.transform-testing.expectations.protocol/build, over a value the schema has passed — the generated constructors skip validation"}
clojure.core/print {:message "Use metabase.util.log instead of clojure.core/print"}
clojure.core/println {:message "Use metabase.util.log instead of clojure.core/println"}
clojure.core/printf {:message "Use metabase.util.log instead of clojure.core/printf"}
@@ -617,7 +624,7 @@
metabase-enterprise.transforms-python.drivers-test/with-test-table hooks.common/with-two-bindings
metabase.api.common.internal-test/with-jetty-server hooks.common/with-one-binding
metabase.api.macros/defendpoint hooks.metabase.api.macros/defendpoint
metabase.app-db.schema-migrations-test.impl/test-migrations hooks.metabase.app-db.schema-migrations-test.impl/test-migrations
metabase.app-db.schema-migrations-test.impl/test-migrations hooks.metabase.app-db.schema-migrations-test.impl/test-migrations!
metabase.channel.render.js.pool/with-static-viz-context hooks.common/with-one-top-level-binding
metabase.channel.template.handlebars-helper/defhelper hooks.metabase.channel.template.handlebars-helper/defhelper
metabase.channel.template.handlebars-test/with-temp-template! hooks.common/with-vec-first-binding
@@ -847,7 +854,13 @@
metabase.users.models.user-test/with-groups! macros.metabase.users.models.user-test/with-groups!
metabase.util.namespaces/import-fn macros.metabase.util.namespaces/import-fn
metabase.util.namespaces/import-macro macros.metabase.util.namespaces/import-macro
metabase.xrays.related-test/with-world macros.metabase.xrays.related-test/with-world}}
metabase.xrays.related-test/with-world macros.metabase.xrays.related-test/with-world
potemkin/import-def macros.potemkin.namespaces/import-def
potemkin/import-fn macros.potemkin.namespaces/import-fn
potemkin/import-macro macros.potemkin.namespaces/import-macro
potemkin.namespaces/import-def macros.potemkin.namespaces/import-def
potemkin.namespaces/import-fn macros.potemkin.namespaces/import-fn
potemkin.namespaces/import-macro macros.potemkin.namespaces/import-macro}}
:config-in-comment
{:linters {:unresolved-symbol {:level :off}}}

View File

@@ -137,6 +137,7 @@
:uses #{actions
analytics
api
api-scope
api.eid-translation
collections
lib
@@ -317,7 +318,9 @@
api-scope
{:team "Metabot"
:api #{metabase.api-scope.core
metabase.api-scope.init}}
metabase.api-scope.data-app
metabase.api-scope.init}
:uses #{util}}
api.eid-translation
{:team "UX West"
@@ -548,6 +551,7 @@
query-processor.pivot
request
settings
startup
system
task
tiles
@@ -640,6 +644,7 @@
:ns-prefix "metabase.collections-rest"
:api #{metabase.collections-rest.api}
:uses #{api
api-scope
api.eid-translation
collections
documents
@@ -795,6 +800,7 @@
:uses #{actions
analytics
api
api-scope
api.eid-translation
app-db
channel
@@ -904,7 +910,6 @@
metabase.driver.settings
metabase.driver.sql
metabase.driver.sql-jdbc
metabase.driver.sql-jdbc.connection.pool-lock
metabase.driver.sql-jdbc.execute
metabase.driver.sql-jdbc.execute.diagnostic
metabase.driver.sql-jdbc.sync
@@ -930,6 +935,7 @@
query-processor.driver-api
settings
sql-tools
startup
system
tracing
util
@@ -999,7 +1005,7 @@
explorations
{:team "UX West"
:api #{metabase.explorations.core metabase.explorations.init metabase.explorations.schema}
:api #{metabase.explorations.api metabase.explorations.core metabase.explorations.init metabase.explorations.schema}
:uses #{analytics
analytics.interface
api
@@ -1079,7 +1085,7 @@
glossary
{:team "Gadget"
:api #{metabase.glossary.api}
:api #{metabase.glossary.api metabase.glossary.core}
:uses #{api
events
lib.schema
@@ -1104,6 +1110,7 @@
indexed-entities
{:team "UX West"
:api #{metabase.indexed-entities.api
metabase.indexed-entities.core
metabase.indexed-entities.init}
:uses #{analytics
api
@@ -1318,6 +1325,7 @@
comments
dashboards
documents
glossary
lib
lib.be
lib.metadata
@@ -1346,6 +1354,7 @@
util
util.config
warehouse-schema-overlay
warehouses
warehouses.schema}
:model-imports #{:model/Card
:model/Collection
@@ -1378,6 +1387,7 @@
mcp
models
request
server
settings
system
task
@@ -1560,6 +1570,7 @@
settings
setup
sql-tools
startup
sync
util
util.config
@@ -1594,10 +1605,7 @@
system
upload
util}
:model-imports #{:model/AuthIdentity
:model/Card
:model/Database
:model/MetabotMessage}}
:model-imports #{:model/AuthIdentity :model/Card :model/MetabotMessage}}
metrics
{:team "UX West"
@@ -1736,6 +1744,7 @@
{:team "Graphy"
:api #{metabase.native-query-snippets.api metabase.native-query-snippets.core metabase.native-query-snippets.schema}
:uses #{api
api-scope
collections
events
lib
@@ -2110,6 +2119,7 @@
:ns-prefix "metabase.queries-rest"
:api #{metabase.queries-rest.api}
:uses #{api
api-scope
api.eid-translation
collections
embedding
@@ -2195,6 +2205,7 @@
:uses #{analytics
analytics.interface
api
api-scope
app-db
appearance
audit-app
@@ -2317,7 +2328,8 @@
{:team "UX West"
:api #{metabase.revisions.api
metabase.revisions.core
metabase.revisions.init}
metabase.revisions.init
metabase.revisions.models.revision}
:uses #{api
collections
dashboards
@@ -2347,7 +2359,8 @@
:model/Measure
:model/ParameterCard
:model/Segment
:model/Transform}}
:model/Transform
:model/TransformTest}}
run-tracking
{:team "Gadget"
@@ -2389,6 +2402,7 @@
:uses #{analytics
analytics.interface
api
api-scope
app-db
appearance
audit-app
@@ -2470,6 +2484,7 @@
analytics.interface
api
api-keys
api-scope
app-db
appearance
core.initialization-status
@@ -2504,6 +2519,7 @@
metabase.session.schema
metabase.session.settings} ; TODO -- shouldn't be exposed as an API namespace
:uses #{api
api-scope
app-db
channel
events
@@ -2626,6 +2642,7 @@
server
settings
sso.auth-identity
startup
system
util
util.config}
@@ -2801,6 +2818,7 @@
premium-features
query-processor
settings
startup
util}
:model-imports #{:model/Card :model/Dashboard :model/DashboardCard}}
@@ -2810,6 +2828,7 @@
metabase.timeline.core}
:uses #{analytics
api
api-scope
collections
events
lib.schema
@@ -3046,6 +3065,7 @@
:api #{metabase.user-key-value.api
metabase.user-key-value.init}
:uses #{api
api-scope
lib.schema
util
util.config}}
@@ -3089,6 +3109,7 @@
:ns-prefix "metabase.users-rest"
:api #{metabase.users-rest.api}
:uses #{api
api-scope
appearance
collections
events
@@ -3189,6 +3210,7 @@
:api #{metabase.warehouses-rest.api}
:uses #{analytics
api
api-scope
app-db
collections
database-routing
@@ -3208,13 +3230,11 @@
sync
upload
util
util.classloader
util.config
warehouse-schema-overlay
warehouses
warehouses.schema
warehouses.secrets
enterprise/permissions}
warehouses.secrets}
:model-imports #{:model/Card
:model/Collection
:model/Database
@@ -3257,11 +3277,8 @@
settings
sync.analyze
util
util.classloader
util.config
warehouse-schema-overlay
warehouses
enterprise/permissions}
warehouses}
:model-exports #{:model/Dimension
:model/Field
:model/FieldUserSettings
@@ -3282,6 +3299,7 @@
:api #{metabase.warehouse-schema-rest.api}
:uses #{analytics
api
api-scope
app-db
collections
database-routing
@@ -3331,6 +3349,7 @@
collections
dashboards
driver
indexed-entities
lib
lib.be
lib.legacy-mbql
@@ -3355,7 +3374,6 @@
:model/Database
:model/Field
:model/ModelIndex
:model/ModelIndexValue
:model/Query
:model/Revision
:model/Segment
@@ -3584,6 +3602,7 @@
{:team "UX West"
:api #{metabase-enterprise.content-translation.routes}
:uses #{api
api-scope
content-translation
embedding
premium-features
@@ -3613,6 +3632,7 @@
metabase-enterprise.custom-viz-plugin.init
metabase-enterprise.custom-viz-plugin.settings}
:uses #{api
api-scope
events
models
premium-features
@@ -3626,6 +3646,7 @@
{:team "Embedding"
:api #{metabase-enterprise.data-apps.api metabase-enterprise.data-apps.init metabase-enterprise.data-apps.sync}
:uses #{api
api-scope
collections
lib
lib.be
@@ -4016,6 +4037,7 @@
:ns-prefix "metabase-enterprise.sandbox"
:api #{metabase-enterprise.sandbox.api.routes metabase-enterprise.sandbox.api.util metabase-enterprise.sandbox.schema}
:uses #{api
api-scope
app-db
audit-app
driver
@@ -4098,6 +4120,7 @@
:model/Timeline
:model/Transform
:model/TransformTag
:model/TransformTest
:model/User}}
enterprise/search
@@ -4332,6 +4355,31 @@
:model-exports #{:model/Tenant}
:model-imports #{:model/Collection :model/User}}
enterprise/transform-testing
{:team "Graphy"
:api #{metabase-enterprise.transform-testing.api metabase-enterprise.transform-testing.init}
:uses #{api
audit-app
driver
events
lib
lib.schema
models
premium-features
revisions
run-tracking
sql-tools
sql-tools.parsing
task
transforms.base
util
warehouses}
:model-exports #{:model/TransformTest}
:model-imports #{:model/Collection
:model/Database
:model/Transform
:model/User}}
enterprise/transforms
{:team "Graphy"
:module-exports #{enterprise/transforms.inspector

View File

@@ -0,0 +1,21 @@
(ns macros.potemkin.namespaces)
(defn- import-var-expansion
"Expansion shared by all of the `import-*` replacements below. Synthetic \"docstring\" is needed to silence
`:missing-docstring` linter."
([sym]
(import-var-expansion sym (-> sym name symbol)))
([sym new-name]
`(def ~new-name "docstring" ~sym)))
(defmacro import-def
([sym] (import-var-expansion sym))
([sym new-name] (import-var-expansion sym new-name)))
(defmacro import-fn
([sym] (import-var-expansion sym))
([sym new-name] (import-var-expansion sym new-name)))
(defmacro import-macro
([sym] (import-var-expansion sym))
([sym new-name] (import-var-expansion sym new-name)))

View File

@@ -16,8 +16,8 @@
:deprecated-namespace 95
:deprecated-var 85
:discouraged-java-method 4
:discouraged-namespace 69
:discouraged-var 130
:discouraged-namespace 68
:discouraged-var 132
:duplicate-require 1
:equals-true 1
:main-without-gen-class 1
@@ -32,7 +32,7 @@
:metabase/validate-defendpoint-query-params-use-kebab-case 48
:metabase/validate-defendpoint-route-uses-kebab-case 44
:metabase/validate-deftest 9
:missing-docstring 12
:missing-docstring 4
:missing-protocol-method 3
:non-arg-vec-return-type-hint 1
:reduce-without-init 3

View File

@@ -1,8 +1,22 @@
(ns hooks.metabase.app-db.schema-migrations-test.impl
(:require [clj-kondo.hooks-api :as hooks]))
(:require
[clj-kondo.hooks-api :as hooks]))
(defn test-migrations
[{{[_ migration-range {[binding] :children} & body] :children} :node}]
(defn- validate-mb-app-db-migrations-test!
"CI runs the namespaces tagged `:mb/app-db-migrations-test` on a shard of their own, because a count-based split
can't balance them (see `.github/workflows/app-db.yml`). An untagged one still runs, just on a shard sized for
everything else."
[{:keys [ns node]}]
(when-not (:mb/app-db-migrations-test (meta ns))
(hooks/reg-finding!
(assoc (meta node)
:message (str "Namespaces that run migration tests (test-migrations) should be marked"
" ^:mb/app-db-migrations-test [:metabase/validate-mb-app-db-migrations-test]")
:type :metabase/validate-mb-app-db-migrations-test))))
(defn test-migrations!
[{{[_ migration-range {[binding] :children} & body] :children} :node, :as x}]
(validate-mb-app-db-migrations-test! x)
(let [node* (hooks/list-node
(list*
(hooks/token-node `let)

View File

@@ -31,7 +31,8 @@
(def ^:private value-operators
"Operators whose second argument is a value rather than a column."
'#{= not= < > <= >= like not-like ilike not-ilike in not-in between})
'#{= not= != <> is-distinct-from is-not-distinct-from
< > <= >= like not-like ilike not-ilike in not-in between not-between})
(defn- marked?
"Whether `node` is an `[:auto/param v]` marker."
@@ -39,28 +40,138 @@
(and (hooks/vector-node? node)
(= :auto/param (some-> (first (:children node)) hooks/sexpr))))
(defn- distinct-by
"`coll` with only the first element for each distinct `(f element)`."
[f coll]
(->> coll (reduce (fn [[seen acc] x]
(let [k (f x)]
(if (contains? seen k) [seen acc] [(conj seen k) (conj acc x)])))
[#{} []])
second))
(def ^:private identifier-clauses
"Clause keys whose entries name a column or a table rather than carrying a value.
A symbol here is a name, and `metabase.app-db.value-guard` refuses a marker in one, so reporting
it would ask for a fix that throws at compile. `:order-by` and `:group-by` are included even
though the guard allows a marker there: it compiles to `ORDER BY ?` and loses the ordering, so
asking for one is still wrong."
#{:select :select-distinct :select-top :from :join :left-join :right-join :inner-join :full-join
:cross-join :update :insert-into :delete-from :returning :with :with-columns :using
:order-by :group-by :partition-by :window})
(def ^:private expression-entry-clauses
"Identifier clauses where index 0 of an `[expr alias]` entry is an expression, not a name.
HoneySQL binds a parameter there -- `SELECT ? AS a` -- so the guard accepts a marker, and mirrors
this set as `expression-entry-clauses` in `metabase.app-db.value-guard`. Everything after index 0
is an alias, and a bare entry is a name; the guard refuses a marker in both."
#{:select :select-distinct :select-top :returning :partition-by})
(defn- comparison-node?
"Whether `node` is a keyword-headed comparison -- the one shape inside an identifier clause that
does hold values, as in the computed projection `[[:= :engine v] :is_match]`."
[node]
(and (hooks/vector-node? node)
(let [op (some-> (first (:children node)) hooks/sexpr)]
(and (keyword? op) (contains? value-operators (symbol (name op)))))))
(declare value-nodes)
(defn- identifier-clause-value-nodes
"The value slots reachable inside an identifier clause: a subquery's own clauses, and the
arguments of a comparison. The names themselves are not candidates."
[node]
(cond
(hooks/map-node? node) (value-nodes node)
(comparison-node? node) (value-nodes node)
(hooks/vector-node? node) (mapcat identifier-clause-value-nodes (:children node))
(hooks/list-node? node) (mapcat identifier-clause-value-nodes (:children node))
:else nil))
(defn- expression-entry-value-nodes
"The value slots of an [[expression-entry-clauses]] clause: index 0 of each `[expr alias]` entry."
[node]
(when (hooks/vector-node? node)
(mapcat (fn [entry]
(when (hooks/vector-node? entry)
(when-let [expr (first (:children entry))]
;; Keep the expression AND descend, so a bare symbol is reported and a computed
;; projection or a subquery sitting there has its own values checked.
(cons expr (value-nodes expr)))))
(:children node))))
(defn- clause-value-nodes
"The value slots of the value under clause key `clause`."
[clause node]
(cond
(contains? expression-entry-clauses clause) (expression-entry-value-nodes node)
(contains? identifier-clauses clause) (identifier-clause-value-nodes node)
:else (value-nodes node)))
(defn- value-nodes
"The nodes sitting in a value slot of `node`, following the clause shapes a query map uses."
[node]
(cond
;; A query map -- walk its clause values, not its keys.
;; A query map -- walk its clause values, not its keys. A clause that names columns or tables
;; contributes only what is nested inside it.
(hooks/map-node? node)
(mapcat value-nodes (take-nth 2 (rest (:children node))))
(mapcat (fn [[k v]]
(if (hooks/keyword-node? k)
(clause-value-nodes (hooks/sexpr k) v)
(value-nodes v)))
(partition 2 (:children node)))
(hooks/vector-node? node)
(let [[head & args] (:children node)
op (some-> head hooks/sexpr)]
(cond
;; A marker IS the value slot -- never descend into its payload, or the marked value
;; gets reported as though it were bare.
(marked? node)
nil
(contains? #{:and :or :not} op)
(mapcat value-nodes args)
;; `[:= col v]`, `[:in col vs]`, `[:between col lo hi]` -- the first argument is the
;; column and the rest are values, whatever the arity.
;; column and the rest are values, whatever the arity. Keep each value AND descend into it,
;; so a subquery in a value slot -- `[:in :id {:select ... :where [:= :z z]}]` -- has its
;; own values checked too. Descending alone would drop the plain-token case.
(and (keyword? op) (contains? value-operators (symbol (name op))))
(rest args)
(mapcat #(cons % (value-nodes %)) (rest args))
;; A function-call form -- `[:lower v]` -- or a literal collection -- `[a b]` in an `:in`.
;; Both hold values, so yield the children themselves as candidates as well as
;; descending, since a token child returns nothing on its own.
:else
(mapcat value-nodes (:children node))))
(mapcat #(cons % (value-nodes %)) (:children node))))
;; A clause built conditionally -- `(when flag [:= :col v])`, `(if ... )`, `(cond-> ...)`. The
;; value slots are inside, so walk the children rather than stopping. Without this a value
;; wrapped in `when` is invisible to the check, which is a false negative in a security lint.
;;
;; An `assoc` call gets its clause context back: `(assoc q :order-by [[col dir]])` is the same
;; clause as `{:order-by [[col dir]]}`, but written outside a map literal, where the walk would
;; otherwise fall back to shape alone and report the column and the direction as values.
;; `cond->` is covered because the walk descends into the nested `assoc`.
(hooks/list-node? node)
(let [children (vec (:children node))
assoc? (= 'assoc (some-> (first children) hooks/sexpr))
;; index of a clause value -> its clause key
clauses (when assoc?
(into {} (keep-indexed (fn [i child]
(when (and (hooks/keyword-node? child)
(contains? identifier-clauses (hooks/sexpr child))
(< (inc i) (count children)))
[(inc i) (hooks/sexpr child)]))
children)))]
(mapcat (fn [i child]
(if-let [clause (get clauses i)]
(clause-value-nodes clause child)
(value-nodes child)))
(range)
children))
:else nil))
@@ -77,35 +188,102 @@
(let [f (some-> (first (:children node)) hooks/sexpr)]
(not (contains? write-fns (some-> f name symbol)))))
(defn- kv-arg-pair
"The first `:column value` pair a query call passes after its model, if any.
(def ^:private clause-keys
"Query-map clause keys. A map keyed by these is a query map rather than a map of conditions."
#{:select :select-distinct :from :where :join :left-join :right-join :inner-join :full-join
:cross-join :group-by :having :order-by :limit :offset :for :union :union-all :with
:with-columns :returning :values :set})
(defn- query-map-node?
"Whether `node` is a query map rather than a map of column/value conditions.
`(t2/delete! :model/X {:where [:= :k k]})` passes a query map, whose values [[value-nodes]]
already walks -- treating it as conditions too would report every value twice."
[node]
(boolean (some #(and (hooks/keyword-node? %) (contains? clause-keys (hooks/sexpr %)))
(take-nth 2 (:children node)))))
(def ^:private changes-map-fns
"Conditions-map fns whose arglist also ends in a changes map, so a lone trailing map is changes."
'#{update! update-or-insert!})
(def ^:private conditions-map-fns
"Calls whose first argument after the model is a map of CONDITIONS rather than a query map.
`(t2/update! :model/X {:key k} {:v 1})` filters on `k`, so it is a where-clause value -- but it
reaches the query as a plain map entry rather than a `:where` clause, so neither the query-map
walker nor the kv-arg walker sees it. It is a shape the sweep leaves in place, so it has to be
checked here."
'#{update! update-or-insert! delete!})
(defn- conditions-map-value-nodes
"The values of a conditions map passed to one of [[conditions-map-fns]]."
[f args]
(when (contains? conditions-map-fns (some-> f name symbol))
(let [after-model (->> args
(drop-while #(not (and (hooks/keyword-node? %)
(= "model" (namespace (hooks/sexpr %))))))
rest)]
;; Only the map immediately after the model is conditions. A later map is the changes map,
;; whose values are written rather than filtered on, and a written value is never marked: a
;; column's `:in` transform runs on the marker itself and stores it as data.
;; `update!`'s arglist ENDS in the changes map, so its first map is conditions only when
;; another argument follows: `(t2/update! model {:v written})` is the two-arity call whose
;; single map is CHANGES, so flagging it would ask for a marker that corrupts the write.
;; `delete!` has no changes map, so its map is always conditions.
(when-let [m (when (or (not (contains? changes-map-fns (some-> f name symbol)))
(next after-model))
(first after-model))]
(when (and (hooks/map-node? m) (not (query-map-node? m)))
(mapcat (fn [v]
;; The column comes from the map key, so an operator form here holds only
;; values -- `{:key [:in ks]}` -- exactly as a kv-arg pair does.
(if (and (hooks/vector-node? v) (not (marked? v)))
(let [[head & op-args] (:children v)
op (some-> head hooks/sexpr)]
(if (and (keyword? op) (contains? value-operators (symbol (name op))))
(mapcat #(cons % (value-nodes %)) op-args)
(cons v (value-nodes v))))
(cons v (value-nodes v))))
(take-nth 2 (rest (:children m)))))))))
(defn- kv-arg-value-nodes
"The value nodes of a call written as `:column value` pairs.
`(t2/select :model/X :locale locale)`. Several of these fns take an argument before the model --
`(t2/select-one-fn :value :model/X :key k)` -- so the pairs do not start at a fixed offset; they
start after the `:model/...` keyword. A call that does not name a literal model is not checked."
start after the `:model/...` keyword. A call that does not name a literal model is not checked.
A trailing query map is not a pair, and its values are reached by [[value-nodes]] instead. A
value that is itself an operator form -- `:id [:in ids]` -- has its own values walked, so the
collection inside is checked rather than the form."
[args]
(let [after-model (->> args
(drop-while #(not (and (hooks/keyword-node? %)
(= "model" (namespace (hooks/sexpr %))))))
rest)
[k v] after-model]
(when (and k v (hooks/keyword-node? k))
k)))
rest)]
(->> after-model
;; `partition-all` rather than `partition` so a trailing odd argument (a query map, which
;; is not a pair) is still seen and skipped by the keyword-node? test below.
(partition-all 2)
(mapcat (fn [[k v]]
(when (and v (hooks/keyword-node? k))
(if-not (hooks/vector-node? v)
[v]
(let [[head & op-args] (:children v)
op (some-> head hooks/sexpr)]
(cond
;; Already marked -- the marker is the value slot.
(marked? v) [v]
(defn- lint-kv-args!
"Register a finding for a query written as `:column value` pairs.
;; `:id [:in ids]`. Toucan folds the column in from the pair key, so
;; EVERY argument here is a value -- unlike a query-map clause, where
;; the first argument is the column.
(and (keyword? op)
(contains? value-operators (symbol (name op))))
(mapcat #(cons % (value-nodes %)) op-args)
Toucan builds those into the where clause itself, so the query's shape is not on the page: there
is nothing for a reader, this linter, or the `.sql` extraction that follows this project to read.
The same call takes a map -- `{:where [:= :locale v]}` for a select, `{:locale v}` for an
`update!` -- which is the statement, near enough to read off."
[node]
(when-let [k (kv-arg-pair (rest (:children node)))]
(hooks/reg-finding!
(assoc (meta k)
:message (format "Pass this query a map rather than `%s ...` pairs, so its shape is on the page."
(hooks/sexpr k))
:type :metabase/unsafe-app-db-query))))
:else (cons v (value-nodes v)))))))))))
(defn- lint-unmarked-values!
"Register a finding for each argument of the enclosing function that reaches a value slot unmarked.
@@ -113,7 +291,15 @@
Only a symbol is reported. A literal cannot carry a request value, and a value built inside the
function is out of reach of a check that does not follow it across a call."
[node]
(doseq [value (mapcat value-nodes (rest (:children node)))
(doseq [value (let [args (rest (:children node))
f (some-> (first (:children node)) hooks/sexpr)]
;; The three walkers overlap on some shapes -- a multi-arg operator in a kv-arg
;; reaches both `value-nodes` and `kv-arg-value-nodes` -- so dedupe by source
;; position rather than trying to keep them disjoint.
(->> (concat (mapcat value-nodes args)
(kv-arg-value-nodes args)
(conditions-map-value-nodes f args))
(distinct-by #(select-keys (meta %) [:row :col]))))
:when (and (hooks/token-node? value)
(symbol? (hooks/sexpr value))
(not (marked? value)))]
@@ -141,6 +327,5 @@
(db-namespace? (modules/config input) ns)
(not (test-file? filename))
(read-call? node))
(lint-unmarked-values! node)
(lint-kv-args! node))
(lint-unmarked-values! node))
input)

View File

@@ -0,0 +1,25 @@
(ns hooks.metabase.app-db.schema-migrations-test.impl-test
(:require
[clj-kondo.hooks-api :as hooks]
[clj-kondo.impl.utils]
[clojure.test :refer :all]
[hooks.metabase.app-db.schema-migrations-test.impl]))
(defn- finding-types!
[ns-symb]
(binding [clj-kondo.impl.utils/*ctx* {:config {:linters {:metabase/validate-mb-app-db-migrations-test {:level :warning}}}
:ignores (atom nil)
:findings (atom [])
:namespaces (atom {})}]
(hooks.metabase.app-db.schema-migrations-test.impl/test-migrations!
{:ns ns-symb
:node (hooks/parse-string (pr-str '(impl/test-migrations ["v64.2026-09-01T00:00:00"] [migrate!] (migrate!))))})
(mapv :type @(:findings clj-kondo.impl.utils/*ctx*))))
(deftest ^:synchronized require-app-db-migrations-test-tag-test
(testing "a namespace that runs migration tests must be tagged, so CI shards it with the other migration tests"
(is (= [:metabase/validate-mb-app-db-migrations-test]
(finding-types! 'metabase.some-migrations-test))))
(testing "a tagged namespace is fine"
(is (= []
(finding-types! (with-meta 'metabase.some-migrations-test {:mb/app-db-migrations-test true}))))))

View File

@@ -103,24 +103,101 @@
(is (empty? (lint-query-call '(t2/select :model/X {:where [:= :locale locale]})
'metabase.foo.db "test/metabase/foo/db_test.clj")))))
(deftest ^:parallel kv-arg-style-test
(testing "a query written as :column value pairs is flagged whatever the values are"
(are [form] (=? [{:type :metabase/unsafe-app-db-query, :message #"Pass this query a map.*"}]
(deftest ^:parallel kv-arg-style-is-not-flagged-test
(testing "call style is not linted: a query written as :column value pairs is left alone"
;; Extraction to a .sql file is semantic rather than a transcription of the HoneySQL shape, so a
;; query map is no closer to that target than kv-args are. Converting also skips Toucan's type
;; transforms, which only run in kv-arg position -- that bug shipped twice.
(are [form] (empty? (filter #(re-find #"Pass this query a map" (str (:message %)))
(lint-query-call form 'metabase.foo.db)))
'(t2/select :model/X :locale [:auto/param locale])
'(t2/select :model/X :archived false)
'(t2/select-one :model/X :id (long id))
'(t2/select-one-fn :value :model/Setting :key [:auto/param k])
'(t2/select :model/X {:where [:= :locale [:auto/param locale]]})))
(testing "an unmarked value in a kv-arg is still flagged -- only the style check went away"
(is (=? [{:type :metabase/unsafe-app-db-query, :message #".*reaches a SQL value slot unmarked.*"}]
(lint-query-call '(t2/select :model/X :locale locale) 'metabase.foo.db)))))
(deftest ^:parallel conditions-map-values-are-linted-test
(testing "a conditions map -- a shape the sweep leaves in place -- is checked"
;; `(t2/update! model {:col v} changes)` filters on v, but it is not a `:where` clause, so
;; neither the query-map walker nor the kv-arg walker used to see it. A namespace written this
;; way passed the lint vacuously.
(are [form] (=? [{:type :metabase/unsafe-app-db-query
:message #".*reaches a SQL value slot unmarked.*"}]
(lint-query-call form 'metabase.foo.db))
'(t2/update! :model/X {:key k} {:v 1})
'(t2/delete! :model/X {:key k})
'(t2/update! :model/X {:key [:in ks]} {:v 1})))
(testing "a marked or coerced conditions value is not flagged"
(are [form] (empty? (lint-query-call form 'metabase.foo.db))
'(t2/update! :model/X {:key [:auto/param k]} {:v 1})
'(t2/update! :model/X {:id (long id)} {:v 1})))
(testing "only the map right after the model is conditions -- the changes map is exempt"
;; rule 1: a written value must arrive as itself, so it is never flagged.
(is (empty? (lint-query-call '(t2/update! :model/X {:id (long id)} {:v written})
'metabase.foo.db)))))
(deftest ^:parallel subquery-values-are-linted-test
(testing "a value inside a subquery in a value slot is reached"
(is (=? [{:type :metabase/unsafe-app-db-query
:message #".*`z`.*reaches a SQL value slot unmarked.*"}]
(lint-query-call
'(t2/select :model/X {:where [:in :id {:select [:id] :from [:y] :where [:= :z z]}]})
'metabase.foo.db))))
(testing "the plain token case still works -- descending must not replace it"
(is (=? [{:message #"`v`.*reaches a SQL value slot unmarked.*"}]
(lint-query-call '(t2/select :model/X {:where [:= :k v]}) 'metabase.foo.db)))))
(deftest ^:parallel conditionally-built-clause-test
(testing "a value slot inside a conditional form is still reached"
;; `value-nodes` used to stop at a list node, so anything a `when`/`if`/`cond->` built was
;; invisible -- a false negative in a security lint. permissions/db.clj and collections/db.clj
;; both build clauses this way.
(are [form] (=? [{:type :metabase/unsafe-app-db-query
:message #".*reaches a SQL value slot unmarked.*"}]
(lint-query-call form 'metabase.foo.db))
'(t2/select :model/X {:where [:and (when k [:= :key k])]})))
(testing "both branches of an if are reached"
(is (= 2 (count (lint-query-call
'(t2/select :model/X {:where [:and (if flag [:= :key k] [:= :key other-k])]})
'metabase.foo.db)))))
(testing "a marked value inside a conditional form is not flagged"
(is (empty? (lint-query-call
'(t2/select :model/X {:where [:and (when k [:= :key [:auto/param k]])]})
'metabase.foo.db)))))
(deftest ^:parallel kv-arg-values-are-linted-test
(testing "an unmarked value in a kv-arg pair is flagged, so dropping the style check left no gap"
(are [form] (=? [{:type :metabase/unsafe-app-db-query
:message #".*reaches a SQL value slot unmarked.*"}]
(lint-query-call form 'metabase.foo.db))
'(t2/select :model/X :locale locale)
'(t2/select :model/X :archived false)
'(t2/select :model/X :locale [:auto/param locale])
'(t2/select-one :model/X :id (long id))))
(testing "one finding per call, not one per pair"
(is (= 1 (count (lint-query-call '(t2/select :model/X :locale locale :msgid msgid)
'metabase.foo.db)))))
(testing "a fn with an argument before the model is still recognised"
(is (=? [{:message #"Pass this query a map.*"}]
(lint-query-call '(t2/select-one-fn :value :model/Setting :key k) 'metabase.foo.db))))
(testing "a map query is not flagged"
'(t2/select-one :model/X :id id)
;; the pairs do not start at a fixed offset -- :value comes before the model here
'(t2/select-one-fn :value :model/Setting :key k)
;; a value inside an operator form is reached, rather than the form being treated as a value
'(t2/select :model/X :id [:in ids])))
(testing "a marked or coerced kv-arg value is not flagged"
(are [form] (empty? (lint-query-call form 'metabase.foo.db))
'(t2/select :model/X {:where [:= :locale [:auto/param locale]]})
'(t2/update! :model/X {:locale [:auto/param locale]} {:msgstr "x"}))))
'(t2/select :model/X :locale [:auto/param locale])
'(t2/select-one :model/X :id (long id))
'(t2/select :model/X :id [:in (mapv long ids)])
'(t2/select :model/X :id [:in [:auto/param ids]])))
(testing "a literal kv-arg value is not flagged"
(are [form] (empty? (lint-query-call form 'metabase.foo.db))
'(t2/select :model/X :archived false)
'(t2/select :model/X :status "pending")
'(t2/select :model/X :engine :appdb)))
(testing "one unmarked value per pair is reported"
(is (= 2 (count (lint-query-call '(t2/select :model/X :locale locale :msgid msgid)
'metabase.foo.db)))))
(testing "a call mixing kv-args with a trailing query map reports each value exactly once"
;; The two walkers run over the same arguments, so this pins that they do not double-report.
(is (= 2 (count (lint-query-call
'(t2/select :model/X :locale locale {:where [:= :other other]})
'metabase.foo.db))))))
(deftest ^:parallel write-calls-are-not-linted-for-values-test
(testing "an insert's values are written, not filtered on, so they are not flagged"
@@ -136,3 +213,112 @@
(testing "a value operator with an unexpected arity still has its args examined"
(is (seq (lint-query-call '(t2/select :model/X {:where [:= :a b c]}) 'metabase.foo.db)))
(is (seq (lint-query-call '(t2/select :model/X {:where [:between :a lo hi]}) 'metabase.foo.db)))))
(deftest ^:parallel operator-coverage-test
(testing "comparison operators HoneySQL accepts as aliases for not= are checked"
;; The set used to list only `not=`, so `[:!= :col v]` -- live in collections/db.clj -- was
;; silently unchecked even though HoneySQL binds it as `<>`.
(are [form] (=? [{:message #"`v`.*reaches a SQL value slot unmarked.*"}]
(lint-query-call form 'metabase.foo.db))
'(t2/select :model/X {:where [:!= :k v]})
'(t2/select :model/X {:where [:<> :k v]})
'(t2/select :model/X {:where [:is-distinct-from :k v]})))
(testing "a two-value operator reports both values"
(is (= 2 (count (lint-query-call '(t2/select :model/X {:where [:not-between :k lo hi]})
'metabase.foo.db)))))
(testing "a value wrapped in a function-call form is reached"
;; `[:lower v]` compiles to `LOWER(?)`, so `v` is a real bind slot.
(are [form] (seq (lint-query-call form 'metabase.foo.db))
'(t2/select :model/X {:where [:= :k [:lower v]]})
'(t2/select :model/X {:where [:= :k [:cast v :text]]})
'(t2/select :model/X :k [:lower v])))
(testing "values in a literal collection are reached"
;; `[:in :k [a b]]` binds each element -- `IN (?, ?)`.
(is (= 2 (count (lint-query-call '(t2/select :model/X {:where [:in :k [a b]]})
'metabase.foo.db))))))
(deftest ^:parallel no-duplicate-findings-test
(testing "a multi-arg operator in a kv-arg reports each value exactly once"
;; value-nodes misreads `[:between lo hi]` as a clause (lo as column), while
;; kv-arg-value-nodes reads it correctly, so the walkers overlap on `hi`. Deduping by source
;; position makes that harmless.
(is (= 2 (count (lint-query-call '(t2/select :model/X :d [:between lo hi]) 'metabase.foo.db))))
(is (= 2 (count (lint-query-call '(t2/delete! :model/X {:d [:between lo hi]})
'metabase.foo.db)))))
(testing "a query map passed where conditions go reports each value once"
;; `{:where ...}` is a query map that value-nodes already walks; treating it as conditions too
;; reported everything twice. Live in mcp/db.clj, metabot/db.clj, testing_api/db.clj.
(is (= 1 (count (lint-query-call '(t2/delete! :model/X {:where [:= :k k]}) 'metabase.foo.db))))
(is (= 2 (count (lint-query-call '(t2/update! :model/X {:where [:and [:= :a a] [:= :b b]]} {:v 1})
'metabase.foo.db))))))
(deftest ^:parallel two-arity-update-changes-map-test
(testing "update!'s lone trailing map is CHANGES, not conditions, so its values are not flagged"
;; Toucan's arglist ends in the changes map. remote_sync/db.clj's mark-all-rsos-synced! is this
;; shape; flagging `written` would ask for a marker on a written value, which the column's
;; `:in` transform stores as data.
(is (empty? (lint-query-call '(t2/update! :model/X {:v written}) 'metabase.foo.db))))
(testing "delete! has no changes map, so its lone map is always conditions"
(is (=? [{:message #"`k`.*"}]
(lint-query-call '(t2/delete! :model/X {:k k}) 'metabase.foo.db))))
(testing "update!'s FIRST map is still conditions when a changes map follows"
(is (=? [{:message #"`k`.*"}]
(lint-query-call '(t2/update! :model/X {:k k} {:v 1}) 'metabase.foo.db)))))
(deftest ^:parallel identifier-clauses-are-not-value-slots-test
(testing "a symbol naming a column or a table is not reported"
;; `value-guard` refuses a marker in these clauses, so reporting one asks for a fix that throws
;; at compile. data_studio/db.clj, task_history/db.clj and permissions/db.clj are written this
;; way. `:select` is the exception: index 0 of its entries is an expression, so it has its own
;; test in expression-entry-clauses-test.
(are [form] (empty? (lint-query-call form 'metabase.foo.db))
'(t2/query {:select [:*] :from [[union-query :subquery]]})
'(t2/query {:select [:*] :from [:t] :order-by [[:sort_key sort-direction]]})
'(t2/query {:select [:*] :from [:t] :group-by [group-column]})))
(testing "a value nested inside an identifier clause is still reported"
(are [form] (=? [{:message #"`v`.*reaches a SQL value slot unmarked.*"}]
(lint-query-call form 'metabase.foo.db))
;; a computed projection is a real comparison
'(t2/query {:select [[[:= :engine v] :is_match]] :from [:t]})
;; so is a CASE sort key
'(t2/query {:select [:*] :from [:t] :order-by [[[:case [:= :a v] 1 :else 2] :asc]]})
;; a subquery carries its own where clause
'(t2/query {:select [:*] :from [[{:select [:*] :from [:u] :where [:= :k v]} :s]]}))))
(deftest ^:parallel expression-entry-clauses-test
(testing "index 0 of an entry is a value slot, because HoneySQL binds a parameter there"
;; `{:select [[[:auto/param "a"] :k]]}` compiles to `SELECT ? AS "K"`, so `value-guard` accepts
;; a marker there. Treating the whole clause as names would hide a request value.
(are [form] (=? [{:type :metabase/unsafe-app-db-query
:message #"`v`.*reaches a SQL value slot unmarked.*"}]
(lint-query-call form 'metabase.foo.db))
'(t2/query {:select [[v :label]] :from [:t]})
'(t2/query {:select-distinct [[v :label]] :from [:t]})
'(t2/query {:insert-into [:t] :returning [[v :label]]})))
(testing "the alias, a bare entry and a table entry are names"
;; `value-guard` refuses a marker in each of these, so asking for one would break the query.
(are [form] (empty? (lint-query-call form 'metabase.foo.db))
'(t2/query {:select [[:key alias-sym]] :from [:t]})
'(t2/query {:select [cols] :from [:t]})
'(t2/query {:select [:*] :from [[tbl :x]]})))
(testing "a computed projection and a subquery entry still reach their values"
(are [form] (=? [{:message #"`v`.*"}] (lint-query-call form 'metabase.foo.db))
'(t2/query {:select [[[:= :engine v] :is_match]] :from [:t]})
'(t2/query {:select [:*] :from [[{:select [:*] :from [:u] :where [:= :k v]} :s]]}))))
(deftest ^:parallel clause-context-survives-assoc-test
(testing "a clause key in an assoc call gives its value the clause context back"
;; task_history/db.clj:174 and five other namespaces build `:order-by` this way. Without this
;; the lint asks for a marker on a column name and on a sort direction, and the fix the message
;; names loses the ordering with no error.
(are [form] (empty? (lint-query-call form 'metabase.foo.db))
'(t2/query (assoc q :order-by [[sort-column sort-direction] [:id :desc]]))
'(t2/query (cond-> q flag (assoc :order-by [[sort-column sort-direction]])))
'(t2/query (assoc q :group-by [group-column]))
'(t2/query (assoc q :select [[:key alias-sym]]))))
(testing "a value clause built the same way is still reported"
(is (=? [{:message #"`v`.*"}]
(lint-query-call '(t2/query (assoc q :where [:= :k v])) 'metabase.foo.db))))
(testing "an expression entry built the same way is still reported"
(is (=? [{:message #"`v`.*"}]
(lint-query-call '(t2/query (assoc q :select [[v :label]])) 'metabase.foo.db)))))

View File

@@ -121,6 +121,9 @@ MB_EDITION="ee"
# Frontend dev server port (for hot reload). Must match the port used by `yarn build-hot`.
# MB_FRONTEND_DEV_PORT=8080
# Frontend dev server bind address. Defaults to 127.0.0.1; set 0.0.0.0 to reach it from other devices.
# MB_FRONTEND_DEV_HOST=127.0.0.1
# 3.2 E2E Local Runner Setup
# ------------------------------------------------------------------------------
@@ -161,3 +164,46 @@ TZ="US/Pacific"
# DO NOT USE Docker Desktop. Use Orbstack instead. Metabase will fail to connect
# to the dockerized Mongo QA database otherwise.
EXPERIMENTAL_DOCKER_DESKTOP_FORCE_QEMU=1
# ==============================================================================
# 4. BACKEND DRIVER TESTS
# ==============================================================================
#
# Connection details for running backend tests against a driver, e.g.
# ./bin/test-agent --drivers=snowflake :only '[metabase.foo-test]'
# 4.1 BigQuery
# ------------------------------------------------------------------------------
# Service account with BigQuery access in the project. The test user creates and drops the test-data datasets itself.
# https://cloud.google.com/iam/docs/keys-create-delete
# MB_BIGQUERY_CLOUD_SDK_TEST_PROJECT_ID="<project-id>"
# MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON='{"type": "service_account", ...}'
# Only for database routing tests
# MB_BIGQUERY_CLOUD_SDK_TEST_SERVICE_ACCOUNT_JSON_ROUTING='{"type": "service_account", ...}'
# 4.2 Redshift
# ------------------------------------------------------------------------------
# Test data is loaded into a unique schema per test run.
# MB_REDSHIFT_TEST_HOST="<cluster>.<id>.<region>.redshift.amazonaws.com"
# Comma-separated hosts to pick one at random from, instead of MB_REDSHIFT_TEST_HOST
# MB_REDSHIFT_TEST_HOSTS=""
# MB_REDSHIFT_TEST_PORT=5439
# MB_REDSHIFT_TEST_DB="testdb"
# MB_REDSHIFT_TEST_USER="metabase_ci"
# MB_REDSHIFT_TEST_PASSWORD="<password>"
# Only for database routing tests
# MB_REDSHIFT_TEST_DB_ROUTING="dev"
# 4.3 Snowflake
# ------------------------------------------------------------------------------
# Key-pair authentication. The test user creates and drops the test-data databases itself.
# https://docs.snowflake.com/en/user-guide/key-pair-auth
# MB_SNOWFLAKE_TEST_ACCOUNT="<orgname>-<accountname>"
# MB_SNOWFLAKE_TEST_USER="<username>"
# MB_SNOWFLAKE_TEST_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
# MB_SNOWFLAKE_TEST_WAREHOUSE="<warehouse>"
# MB_SNOWFLAKE_TEST_ADDITIONAL_OPTIONS=""

View File

@@ -22,6 +22,9 @@ runs:
# `awk 'NR<=25'` rather than `head -25`: the runner ignores SIGPIPE, so `head` exiting early
# leaves `sort` with an EPIPE write error and `pipefail` fails the step. `|| true` covers `grep`
# finding no junit files.
#
# `-r --include` rather than a `target/junit/*.xml` glob: a multi-JVM run nests one directory
# per partition under `target/junit`, and a non-recursive glob matches none of it.
- name: Summarize test wall times
if: ${{ inputs.profiled == 'true' }}
continue-on-error: true
@@ -30,13 +33,13 @@ runs:
{
echo "### Slowest test namespaces (seconds)"
echo '```'
grep -h '<testsuite' target/junit/*.xml 2>/dev/null \
grep -rh --include='*.xml' '<testsuite' target/junit 2>/dev/null \
| sed -E 's|.*<testsuite name="([^"]*)" time="([^"]*)".*|\2 \1|' \
| sort -rn | awk 'NR<=25' || true
echo '```'
echo "### Slowest tests (seconds)"
echo '```'
grep -h '<testcase' target/junit/*.xml 2>/dev/null \
grep -rh --include='*.xml' '<testcase' target/junit 2>/dev/null \
| sed -E 's|.*classname="([^"]*)" name="([^"]*)" time="([^"]*)".*|\3 \1/\2|' \
| sort -rn | awk 'NR<=25' || true
echo '```'
@@ -53,9 +56,12 @@ runs:
uses: actions/upload-artifact@v7
with:
name: ${{ inputs.artifact-name }}
# Globs, because a multi-JVM run nests its junit XML one directory per partition and
# names each log `test-log-p<index>.json`. The flamegraphs stay flat, with that same
# `p<index>-` prefix.
path: |
target/clj-async-profiler/results/*flamegraph.html
target/junit/*.xml
logs/test-log.json
target/junit/**/*.xml
logs/test-log*.json
retention-days: 14
if-no-files-found: warn

View File

@@ -85,17 +85,27 @@ runs:
git checkout ${{ github.sha }}
- name: Approve and Merge the PR
# Approving takes a different token than the one that opened the PR: GitHub won't let a token
# approve its own pull request.
- name: Approve the backport PR
if: ${{ steps.create_backport_pull_request.outputs.has-conflicts == 'false' }}
run: |
gh pr review --approve "$PR_NUMBER"
gh pr merge --squash --auto "$PR_NUMBER"
run: gh pr review --approve "$PR_NUMBER"
shell: bash
env:
# uses a different token than the token that created the PR so we can approve and merge it
GH_TOKEN: ${{ inputs.github-token-2 }}
PR_NUMBER: ${{ steps.create_backport_pull_request.outputs.backport_pr_number }}
# Merging, though, has to use `github-token` rather than the `GITHUB_TOKEN` that approves: GitHub
# starts no workflow run for a push made with `GITHUB_TOKEN`, so an auto-merge enabled with it
# lands the backport on the release branch with no CI at all.
- name: Merge the backport PR
if: ${{ steps.create_backport_pull_request.outputs.has-conflicts == 'false' }}
run: gh pr merge --squash --auto "$PR_NUMBER"
shell: bash
env:
GH_TOKEN: ${{ inputs.github-token }}
PR_NUMBER: ${{ steps.create_backport_pull_request.outputs.backport_pr_number }}
- uses: ./.github/actions/notify-pull-request
if: ${{ steps.create_backport_pull_request.outputs.has-conflicts == 'true' }}
with:

View File

@@ -130,6 +130,7 @@ runs:
if [ "$PROFILE_TESTS" = "true" ]; then
echo "Profiling this run; a flamegraph will be attached to the job."
echo "profiled=true" >> "$GITHUB_OUTPUT"
ALIASES="$ALIASES:profile-tests"
BASE="$BASE:profile-tests"
fi
# An explicit selection wins over the rerun narrowing below: it was asked for by hand, on this
@@ -149,6 +150,15 @@ runs:
shell: bash
id: run-test
env:
# Both derived from test.profile-backend.yml, the sole dispatcher; its runs always profile. Inside
# a called workflow `github.event` is the caller's payload, so these resolve here rather than
# in each calling workflow. On a normal CI run there are no dispatch inputs, so these render
# `false` and empty and the job's own selection stands.
#
# ONLY_TESTS replaces the job's own test selection, so every matrix leg of the chosen job
# runs the whole list.
PROFILE_TESTS: ${{ github.event.inputs.suite != '' }}
ONLY_TESTS: ${{ github.event.inputs.tests }}
TEST_RUNNER_INDEX: ${{ inputs.runner-index }}
TEST_RUNNER_COUNT: ${{ inputs.runner-count }}
TEST_JVMS_PER_RUNNER: ${{ inputs.jvms-per-runner }}

View File

@@ -98,6 +98,15 @@ merge_outputs() {
if [ -f "$jvm_dir/logs/test-log.json" ]; then
cp -a "$jvm_dir/logs/test-log.json" "$WORKSPACE/logs/test-log-p$idx.json" || true
fi
# Flamegraphs, when the run was profiled. Flat with a `p$idx-` prefix, not nested: the
# collect-profile action globs `results/*flamegraph.html` without descending.
if [ -d "$jvm_dir/target/clj-async-profiler/results" ]; then
mkdir -p "$WORKSPACE/target/clj-async-profiler/results"
for f in "$jvm_dir"/target/clj-async-profiler/results/*flamegraph.html; do
[ -e "$f" ] || continue
cp -a "$f" "$WORKSPACE/target/clj-async-profiler/results/p$idx-$(basename "$f")" || true
done
fi
done
}
trap merge_outputs EXIT

View File

@@ -11,30 +11,6 @@ on:
type: boolean
default: false
# Run one app-DB job on demand, against the ref you dispatch on, optionally under a profiler.
# Everything else about the job -- services, matrix legs, test args, quarantine gating -- is
# whatever the normal CI run uses, because it is the same job.
workflow_dispatch:
inputs:
app-db:
description: 'Which app-DB job to run. Only this one runs; the others are skipped.'
type: choice
required: true
options:
- mariadb
- mysql
- postgres
tests:
description: 'Optional: run only these tests, comma- or space-separated, e.g. metabase.db.custom-migrations-test/some-test. Bare namespaces work too. Blank runs the job''s usual set.'
type: string
required: false
default: ''
profile:
description: 'Profile the test run and attach a flamegraph to the job. Adds sampling overhead.'
type: boolean
required: false
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-app-db
cancel-in-progress: true
@@ -45,14 +21,6 @@ concurrency:
env:
CI_CONDUCTOR_BASE_URL: ${{ vars.CI_CONDUCTOR_BASE_URL }}
CI_CONDUCTOR_WEBHOOK_SECRET: ${{ secrets.CI_CONDUCTOR_WEBHOOK_SECRET }}
# Both read by the test-driver action, which is what actually runs the tests. Set here rather than
# passed as inputs to each job so that turning either on is one line, not one line per job.
# Empty (and so off) on workflow_call, where neither input exists.
#
# ONLY_TESTS replaces the job's own test selection, so every matrix leg of the chosen job runs the
# whole list -- each app DB has three.
PROFILE_TESTS: ${{ inputs.profile }}
ONLY_TESTS: ${{ inputs.tests }}
jobs:
app-db-matrix:
@@ -64,8 +32,9 @@ jobs:
mariadb: ${{ steps.versions.outputs.mariadb }}
mysql: ${{ steps.versions.outputs.mysql }}
postgres: ${{ steps.versions.outputs.postgres }}
shards: ${{ steps.versions.outputs.shards }}
steps:
- name: Select versions to test
- name: Select versions and shards to test
id: versions
uses: actions/github-script@v9
env:
@@ -133,35 +102,42 @@ jobs:
core.setOutput(db, JSON.stringify(selected));
}
const commonExcludes = ":mb/driver-tests :mb/transforms-python-test :mb/old-migrations-test";
const shards = [
{
name: 'Enterprise Tests',
'build-static-viz': false,
'test-args': `:only '"enterprise/backend/test"' :exclude-tags '[${commonExcludes}]'`,
},
{
name: 'EE App DB Tests (Part 1)', // migrations shard
'build-static-viz': true,
'test-args': `:only '"test"' :only-tags '[:mb/app-db-migrations-test]' :exclude-tags '[${commonExcludes}]'`,
},
{
name: 'EE App DB Tests (Part 2)',
'build-static-viz': true,
'test-args': `:only '"test"' :partition/total 2 :partition/index 0 :exclude-tags '[${commonExcludes} :mb/app-db-migrations-test]'`,
},
{
name: 'EE App DB Tests (Part 3)',
'build-static-viz': true,
'test-args': `:only '"test"' :partition/total 2 :partition/index 1 :exclude-tags '[${commonExcludes} :mb/app-db-migrations-test]'`,
},
];
core.setOutput('shards', JSON.stringify(shards));
be-tests-mariadb:
needs: app-db-matrix
if: ${{ !inputs.skip && (inputs.app-db == '' || inputs.app-db == 'mariadb') }}
if: ${{ !inputs.skip && (github.event.inputs.db == '' || github.event.inputs.db == 'mariadb') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.app-db-matrix.outputs.mariadb) }}
job:
- name: Enterprise Tests
build-static-viz: false
test-args: >-
:only '"enterprise/backend/test"'
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
- name: EE App DB Tests (Part 1)
build-static-viz: true
test-args: >-
:only '"test"'
:partition/total 2
:partition/index 0
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
- name: EE App DB Tests (Part 2)
build-static-viz: true
test-args: >-
:only '"test"'
:partition/total 2
:partition/index 1
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
job: ${{ fromJSON(needs.app-db-matrix.outputs.shards) }}
services:
mysql:
@@ -175,10 +151,6 @@ jobs:
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: true
MARIADB_DATABASE: mb_test
env:
# __ADDITIONAL_EXCLUDED_TAG__: ${{ !inputs.force-run && ':mb/old-migrations-test' || '' }}
# GDGT-3126: re-enable and properly fix old migrations tests
__ADDITIONAL_EXCLUDED_TAG__: ':mb/old-migrations-test'
# actual serious env vars below
CI: 'true'
DRIVERS: mysql
MB_DB_TYPE: mysql
@@ -205,7 +177,8 @@ jobs:
# for historic reasons (I don't want to mess around with required jobs) the job name should be something like
# "be-tests-mariadb-10-6-ee (0)"
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
steps:
# Shared by every app DB job below via `*app-db-test-steps`.
steps: &app-db-test-steps
- uses: actions/checkout@v6
- name: Test ${{ matrix.version.name }}
id: test-driver
@@ -214,39 +187,18 @@ jobs:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: ${{ matrix.version.junit-name }}
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
test-args: ${{ matrix.job.test-args }}
be-tests-mysql:
needs: app-db-matrix
if: ${{ !inputs.skip && (inputs.app-db == '' || inputs.app-db == 'mysql') }}
if: ${{ !inputs.skip && (github.event.inputs.db == '' || github.event.inputs.db == 'mysql') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.app-db-matrix.outputs.mysql) }}
job:
- name: Enterprise Tests
build-static-viz: false
test-args: >-
:only '"enterprise/backend/test"'
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
- name: EE App DB Tests (Part 1)
build-static-viz: true
test-args: >-
:only '"test"'
:partition/total 2
:partition/index 0
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
- name: EE App DB Tests (Part 2)
build-static-viz: true
test-args: >-
:only '"test"'
:partition/total 2
:partition/index 1
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
job: ${{ fromJSON(needs.app-db-matrix.outputs.shards) }}
services:
mysql:
@@ -260,10 +212,6 @@ jobs:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: mb_test
env:
# __ADDITIONAL_EXCLUDED_TAG__: ${{ !inputs.force-run && ':mb/old-migrations-test' || '' }}
# GDGT-3126: re-enable and properly fix old migrations tests
__ADDITIONAL_EXCLUDED_TAG__: ':mb/old-migrations-test'
# actual serious env vars below
CI: 'true'
DRIVERS: mysql
MB_DB_TYPE: mysql
@@ -298,54 +246,20 @@ jobs:
# for historic reasons (I don't want to mess around with required jobs) the job name should be something like
# "be-tests-mariadb-10-6-ee (0)"
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
steps:
- uses: actions/checkout@v6
- name: Test ${{ matrix.version.name }}
id: test-driver
uses: ./.github/actions/test-driver
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: ${{ matrix.version.junit-name }}
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
steps: *app-db-test-steps
be-tests-postgres:
needs: app-db-matrix
if: ${{ !inputs.skip && (inputs.app-db == '' || inputs.app-db == 'postgres') }}
if: ${{ !inputs.skip && (github.event.inputs.db == '' || github.event.inputs.db == 'postgres') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.app-db-matrix.outputs.postgres) }}
job:
- name: Enterprise Tests
build-static-viz: false
test-args: >-
:only '"enterprise/backend/test"'
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
- name: EE App DB Tests (Part 1)
build-static-viz: true
test-args: >-
:only '"test"'
:partition/total 2
:partition/index 0
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
- name: EE App DB Tests (Part 2)
build-static-viz: true
test-args: >-
:only '"test"'
:partition/total 2
:partition/index 1
exclude-tag: ':mb/driver-tests :mb/transforms-python-test'
job: ${{ fromJSON(needs.app-db-matrix.outputs.shards) }}
name: "${{ matrix.version.name }} ${{ matrix.job.name }}"
env:
# __ADDITIONAL_EXCLUDED_TAG__: ${{ !inputs.force-run && ':mb/old-migrations-test' || '' }}
# GDGT-3126: re-enable and properly fix old migrations tests
__ADDITIONAL_EXCLUDED_TAG__: ':mb/old-migrations-test'
# actual serious env vars below
CI: 'true'
DRIVERS: postgres
MB_DB_TYPE: postgres
@@ -374,18 +288,7 @@ jobs:
POSTGRES_USER: mb_test
POSTGRES_DB: mb_test
POSTGRES_HOST_AUTH_METHOD: trust
steps:
- uses: actions/checkout@v6
- name: Test ${{ matrix.version.name }} (${{ matrix.job.name }})
id: test-driver
uses: ./.github/actions/test-driver
with:
build-static-viz: ${{ matrix.job.build-static-viz }}
junit-name: ${{ matrix.version.junit-name }}
artifact-suffix: ${{ matrix.job.name }}
test-args: >-
${{ matrix.job.test-args }}
:exclude-tags '[ ${{ matrix.job.exclude-tag }} ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]'
steps: *app-db-test-steps
app-db-tests-result:
needs:

View File

@@ -23,22 +23,6 @@ on:
type: boolean
default: false
# Run the backend test suite on demand, against the ref you dispatch on, optionally under a
# profiler. Everything else about the jobs -- matrix legs, tag filters, quarantine gating -- is
# whatever the normal CI run uses, because they are the same jobs.
workflow_dispatch:
inputs:
tests:
description: 'Optional: run only these tests, comma- or space-separated, e.g. metabase.query-processor-test/some-test. Bare namespaces work too. Blank runs each leg''s usual set.'
type: string
required: false
default: ''
profile:
description: 'Profile the test run and attach a flamegraph to the job. Adds sampling overhead.'
type: boolean
required: false
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-backend
cancel-in-progress: true
@@ -49,13 +33,6 @@ concurrency:
env:
CI_CONDUCTOR_BASE_URL: ${{ vars.CI_CONDUCTOR_BASE_URL }}
CI_CONDUCTOR_WEBHOOK_SECRET: ${{ secrets.CI_CONDUCTOR_WEBHOOK_SECRET }}
# Both read by the be-tests run step below. Set here rather than passed per job so that turning
# either on is one line. Empty (and so off) on workflow_call, where neither input exists.
#
# ONLY_TESTS replaces the leg's own test selection, so every matrix leg runs the whole list: the
# partition args no longer apply, and legs that hold none of the named tests run nothing.
PROFILE_TESTS: ${{ inputs.profile }}
ONLY_TESTS: ${{ inputs.tests }}
jobs:
project-checks:
@@ -374,6 +351,16 @@ jobs:
else
$BASE :exclude-tags '[:mb/driver-tests ${{ env.__ADDITIONAL_EXCLUDED_TAG__ }}]' ${{ matrix.job.test-args }}
fi
env:
# Both derived from test.profile-backend.yml, the sole dispatcher; its runs always profile. On a
# normal CI run there are no dispatch inputs, so these render `false` and empty and the
# leg's own selection stands.
#
# ONLY_TESTS replaces the leg's own test selection, so every matrix leg runs the whole
# list: the partition args no longer apply, and legs that hold none of the named tests run
# nothing.
PROFILE_TESTS: ${{ github.event.inputs.suite != '' }}
ONLY_TESTS: ${{ github.event.inputs.tests }}
# Keep this step continue-on-error because the quarantine logic controls whether it ultimately fails.
continue-on-error: true

View File

@@ -10,45 +10,6 @@ on:
description: Run the full suite regardless of the diff (master / release branches, ci:run-all label).
type: boolean
default: false
# Run one driver job on demand, against the ref you dispatch on, optionally under a profiler.
# Everything else about the job -- credentials, services, test args, quarantine gating -- is
# whatever the normal CI run uses, because it is the same job.
workflow_dispatch:
inputs:
driver:
description: 'Which driver job to run. Only this one runs; the usual decision rules are bypassed.'
type: choice
required: true
options:
- athena
- bigquery
- clickhouse
- databricks
- druid-jdbc
- h2
- mongo
- mongo-sharded-cluster
- mongo-ssl
- mysql-mariadb
- oracle
- postgres
- presto-jdbc
- redshift
- snowflake
- sparksql
- sqlite
- sqlserver
- vertica
tests:
description: 'Optional: run only these tests, comma- or space-separated, e.g. metabase.driver.bigquery-cloud-sdk-test/sync-test. Bare namespaces work too. Blank runs the job''s usual set.'
type: string
required: false
default: ''
profile:
description: 'Profile the test run and attach a flamegraph to the job. Adds sampling overhead.'
type: boolean
required: false
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-drivers
@@ -60,14 +21,6 @@ concurrency:
env:
CI_CONDUCTOR_BASE_URL: ${{ vars.CI_CONDUCTOR_BASE_URL }}
CI_CONDUCTOR_WEBHOOK_SECRET: ${{ secrets.CI_CONDUCTOR_WEBHOOK_SECRET }}
# Both read by the test-driver action, which is what actually runs the tests. Set here rather than
# passed as inputs to each job so that turning either on is one line, not one line per driver.
# Empty (and so off) on workflow_call, where neither input exists.
#
# ONLY_TESTS replaces the job's own test selection, so every matrix leg of the chosen driver runs
# the whole list -- BigQuery, for instance, has three legs.
PROFILE_TESTS: ${{ inputs.profile }}
ONLY_TESTS: ${{ inputs.tests }}
jobs:
# This job determines which driver tests should run based on PR context.
@@ -117,7 +70,7 @@ jobs:
--force-run="${FORCE_RUN}"
--pr-labels="${{ join(github.event.pull_request.labels.*.name, ',') }}"
--skip="${{ inputs.skip }}"
--only-driver="${ONLY_DRIVER}"
--only-driver="${PROFILE_DRIVER}"
)
echo "Running:"
echo "./bin/mage -driver-decisions ${MAGE_ARGS[*]}"
@@ -132,7 +85,9 @@ jobs:
env:
BASE_REF: ${{ github.event.pull_request.base.ref || github.ref_name }}
FORCE_RUN: ${{ inputs.force-run }}
ONLY_DRIVER: ${{ inputs.driver }}
# Set only by test.profile-backend.yml. Absent on a normal CI run, which leaves the decision rules
# alone; mage rejects a name that is not a driver.
PROFILE_DRIVER: ${{ github.event.inputs.db }}
shell: bash
be-tests-athena-ee:

View File

@@ -150,7 +150,7 @@ jobs:
run: NODE_ENV=development bun run build-pure:cljs
- name: Build Storybook
run: bun run build-storybook
run: bun run build-storybook:test
env:
STORYBOOK_STORIES_FILTER: ${{ needs.detect-changed-stories.outputs.story_files }}

View File

@@ -88,7 +88,7 @@ jobs:
- name: Build Storybook
env:
STORYBOOK_STORY_PATHS_FILE: ${{ steps.loki-selection.outputs.paths-file }}
run: bun run build-storybook
run: bun run build-storybook:test
- name: Run Loki Visual Tests
# Loki drops stories with parameters.loki.skip, and exits non-zero when that leaves the selection empty.

View File

@@ -14,14 +14,17 @@ jobs:
runs-on: ${{ vars.SLIM_RUNNER_KEY }}
timeout-minutes: 5
steps:
- name: Dispatch patch release for the current and previous major
- name: Dispatch patch release for the current and recent majors
uses: actions/github-script@v9
with:
script: | # js
// Nightly patches cover the current supported major and the one
// before it. release-auto.yml releases one major per dispatch.
const autoPatchVersionsCount = Number('${{ vars.AUTO_PATCH_VERSIONS_COUNT }}') || 0;
const currentRelease = Number('${{ vars.CURRENT_VERSION }}');
const majors = [currentRelease];
// [currentRelease, currentRelease - 1, ..., currentRelease - autoPatchVersionsCount]
const majors = Array.from(
{ length: autoPatchVersionsCount + 1 },
(_, i) => currentRelease - i,
);
await Promise.all(majors.map(version =>
github.rest.actions.createWorkflowDispatch({

View File

@@ -11,32 +11,6 @@ on:
default: false
description: Only run Postgres Latest to do a quick resource-efficient sanity check, intended to be set for PRs.
# Run one semantic-search job on demand, against the ref you dispatch on, optionally under a
# profiler. Everything else about the job -- services, matrix legs, test args, quarantine gating --
# is whatever the normal CI run uses, because it is the same job.
workflow_dispatch:
inputs:
suite:
description: 'Which semantic-search job to run. Only this one runs; the others are skipped.'
type: choice
required: true
options:
- h2
- mariadb
- mysql
- postgres
- appdb-mode
tests:
description: 'Optional: run only these tests, comma- or space-separated, e.g. metabase-enterprise.semantic-search.index-test/some-test. Bare namespaces work too. Blank runs the job''s usual set.'
type: string
required: false
default: ''
profile:
description: 'Profile the test run and attach a flamegraph to the job. Adds sampling overhead.'
type: boolean
required: false
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}-semantic-search
cancel-in-progress: true
@@ -47,18 +21,10 @@ concurrency:
env:
CI_CONDUCTOR_BASE_URL: ${{ vars.CI_CONDUCTOR_BASE_URL }}
CI_CONDUCTOR_WEBHOOK_SECRET: ${{ secrets.CI_CONDUCTOR_WEBHOOK_SECRET }}
# Both read by the test-driver action, which is what actually runs the tests. Set here rather than
# passed as inputs to each job so that turning either on is one line, not one line per job.
# Empty (and so off) on workflow_call, where neither input exists.
#
# ONLY_TESTS replaces the job's own test selection, so every matrix leg of the chosen job runs the
# whole list.
PROFILE_TESTS: ${{ inputs.profile }}
ONLY_TESTS: ${{ inputs.tests }}
jobs:
semantic-search-tests-h2:
if: ${{ !inputs.skip && !inputs.smoke-only && (inputs.suite == '' || inputs.suite == 'h2') }}
if: ${{ !inputs.skip && !inputs.smoke-only && (github.event.inputs.db == '' || github.event.inputs.db == 'h2') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
strategy:
@@ -102,7 +68,7 @@ jobs:
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
semantic-search-tests-mariadb:
if: ${{ !inputs.skip && !inputs.smoke-only && (inputs.suite == '' || inputs.suite == 'mariadb') }}
if: ${{ !inputs.skip && !inputs.smoke-only && (github.event.inputs.db == '' || github.event.inputs.db == 'mariadb') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
strategy:
@@ -167,7 +133,7 @@ jobs:
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
semantic-search-tests-mysql:
if: ${{ !inputs.skip && !inputs.smoke-only && (inputs.suite == '' || inputs.suite == 'mysql') }}
if: ${{ !inputs.skip && !inputs.smoke-only && (github.event.inputs.db == '' || github.event.inputs.db == 'mysql') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
strategy:
@@ -239,7 +205,7 @@ jobs:
:exclude-tags '[${{ matrix.job.exclude-tag }}]'
semantic-search-tests-postgres:
if: ${{ !inputs.skip && (inputs.suite == '' || inputs.suite == 'postgres') }}
if: ${{ !inputs.skip && (github.event.inputs.db == '' || github.event.inputs.db == 'postgres') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
strategy:
@@ -310,7 +276,7 @@ jobs:
# MB_PGVECTOR_DB_URL is deliberately unset. The dedicated-harness tests gate themselves off; the
# appdb-pgvector-mode round trip (and the URL-independent unit tests) run for real.
semantic-search-tests-appdb-mode:
if: ${{ !inputs.skip && !inputs.smoke-only && (inputs.suite == '' || inputs.suite == 'appdb-mode') }}
if: ${{ !inputs.skip && !inputs.smoke-only && (github.event.inputs.db == '' || github.event.inputs.db == 'appdb-mode') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
strategy:

View File

@@ -0,0 +1,65 @@
name: Profile Backend
# The only on-demand entry point for the backend test suites. Dispatching this workflow runs one
# job of one suite against the ref you dispatch on, under clj-async-profiler, and attaches a
# flamegraph to the job.
#
# The suite workflows below are called unchanged: services, matrix legs, test args and quarantine
# gating are whatever the normal CI run uses, because they are the same jobs. They declare nothing
# for this feature -- they read `suite`, `db` and `tests` off `github.event.inputs`, which inside a
# called workflow is this dispatch's payload.
#
# `suite` picks the workflow, `db` picks the leg within it. `db` is free text so that adding a
# driver or an app-DB leg needs no edit here; an unknown driver fails the drivers run through
# mage's `--only-driver`, and an unknown app-DB or semantic-search leg matches nothing and runs no
# jobs.
#
# Renaming an input here means updating every reader -- `grep -rn github.event.inputs .github/`.
# Nothing enforces that, deliberately: a stale name reads as empty, which runs every leg of the
# suite unprofiled, on the run you are watching. It cannot affect a normal CI run, where those
# references are empty either way.
on:
workflow_dispatch:
inputs:
suite:
description: 'Which suite to profile.'
type: choice
required: true
options:
- backend
- app-db
- drivers
- semantic-search
db:
description: 'Which leg of the suite: app-db takes mariadb, mysql or postgres; drivers takes a driver name, e.g. bigquery or mongo-ssl; semantic-search takes h2, mariadb, mysql, postgres or appdb-mode. Ignored by backend. Blank runs the whole suite -- every leg for app-db and semantic-search, mage''s usual decisions for drivers.'
type: string
required: false
default: ''
tests:
description: 'Optional: profile only these tests, comma- or space-separated, e.g. metabase.query-processor-test/some-test. Bare namespaces work too. Blank profiles the leg''s usual set.'
type: string
required: false
default: ''
# These jobs gate on `inputs.suite` -- this workflow's own input. Only the called workflows reach
# for `github.event.inputs.*`, because `inputs` there is their own `workflow_call` signature.
jobs:
backend:
if: ${{ inputs.suite == 'backend' }}
uses: ./.github/workflows/backend.yml
secrets: inherit
app-db:
if: ${{ inputs.suite == 'app-db' }}
uses: ./.github/workflows/app-db.yml
secrets: inherit
drivers:
if: ${{ inputs.suite == 'drivers' }}
uses: ./.github/workflows/drivers.yml
secrets: inherit
semantic-search:
if: ${{ inputs.suite == 'semantic-search' }}
uses: ./.github/workflows/semantic-search.yml
secrets: inherit

View File

@@ -144,12 +144,20 @@ jobs:
EOF
)"
- name: Approve and Merge the PR
run: |
gh pr review --approve "$PR_NUMBER"
gh pr merge --squash --auto "$PR_NUMBER"
# Approving takes a different token than the one that opened the PR (above, the automation
# user's): GitHub won't let a token approve its own pull request.
- name: Approve the PR
run: gh pr review --approve "$PR_NUMBER"
shell: bash
env:
# uses a different token than the token that created the PR so we can approve and merge it
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.create_pr.outputs.pr_number }}
# Merging goes back through the automation user: GitHub starts no workflow run for a push made
# with `GITHUB_TOKEN`, so an auto-merge enabled with it lands on master with no CI.
- name: Merge the PR
run: gh pr merge --squash --auto "$PR_NUMBER"
shell: bash
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
PR_NUMBER: ${{ steps.create_pr.outputs.pr_number }}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 518 KiB

View File

@@ -58,6 +58,11 @@ const config: StorybookConfig = {
name: "@storybook/react-webpack5",
options: {},
},
build: {
test: {
disableTreeShaking: false,
},
},
typescript: {
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {

View File

@@ -1,6 +1,6 @@
{
"extends": ["stylelint-config-standard", "stylelint-config-css-modules"],
"plugins": ["./frontend/lint/stylelint-rules/no-base-colors.js"],
"plugins": ["./frontend/lint/stylelint-rules/no-base-colors.mjs"],
"rules": {
"alpha-value-notation": null,
"color-function-notation": null,

106
bun.lock
View File

@@ -20,6 +20,8 @@
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.11",
"@dagrejs/dagre": "^1.1.5",
"@dicebear/core": "^10.7.0",
"@dicebear/styles": "^10.6.0",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^8.0.0",
@@ -42,7 +44,7 @@
"@modelcontextprotocol/ext-apps": "^1.3.2",
"@react-oauth/google": "^0.11.1",
"@reduxjs/toolkit": "^2.12.0",
"@snowplow/browser-tracker": "3.2.0",
"@snowplow/browser-tracker": "4.10.2",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.2",
"@tiptap/core": "^3.31.3",
@@ -809,6 +811,10 @@
"@dependents/detective-less": ["@dependents/detective-less@5.0.1", "", { "dependencies": { "gonzales-pe": "^4.3.0", "node-source-walk": "^7.0.1" } }, "sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ=="],
"@dicebear/core": ["@dicebear/core@10.7.0", "", {}, "sha512-JUOzKOd8RUByJJo8ZL+C7YE/pvEsWewfpbWUmR8OIQZWv/PvUQHnohdevySC3icyyxe6EIU/8B/rQgA42us1QA=="],
"@dicebear/styles": ["@dicebear/styles@10.6.0", "", {}, "sha512-KhxbEVI+z8TaenX58+ZhIRFBt7adggu/F+8g6ZYLu43H2/lgLuWA3Kg8vHBrCVWdfTTTt1p2wUhQomIkkypTuQ=="],
"@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="],
"@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.0", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ=="],
@@ -1461,11 +1467,11 @@
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="],
"@snowplow/browser-tracker": ["@snowplow/browser-tracker@3.2.0", "", { "dependencies": { "@snowplow/browser-tracker-core": "3.2.0", "@snowplow/tracker-core": "3.2.0", "tslib": "^2.3.0" } }, "sha512-k9oabjXpM7qIYM5CnMu1EZ/CbbffBKBK1TJTMJaf2k1F3Xplbyqx9DssN1NzgX+lH9Gc22n+yow+czRyppZOCg=="],
"@snowplow/browser-tracker": ["@snowplow/browser-tracker@4.10.2", "", { "dependencies": { "@snowplow/browser-tracker-core": "4.10.2", "@snowplow/tracker-core": "4.10.2", "tslib": "^2.3.1" } }, "sha512-nDX0I2n7AUeTL9H1aI3FDBUkFo8DZIJ7Bp1g8ne7KBCcstrkxADpo7COz9iBOIXNDnr2scEy0ia/eKmRkaZFsA=="],
"@snowplow/browser-tracker-core": ["@snowplow/browser-tracker-core@3.2.0", "", { "dependencies": { "@snowplow/tracker-core": "3.2.0", "sha1": "^1.1.1", "tslib": "^2.3.0", "uuid": "^3.4.0" } }, "sha512-20mrp4OtyzXJMrvDjDmiddKnDUjccg3jOzmK/sNIv8jwPNleHct/MTV1dBo8aTkzxR2jcwL55AqnrD4XIQYDVg=="],
"@snowplow/browser-tracker-core": ["@snowplow/browser-tracker-core@4.10.2", "", { "dependencies": { "@snowplow/tracker-core": "4.10.2", "tslib": "^2.3.1", "uuid": "^11.1.1" } }, "sha512-KX5Zu+IzslpQW7qVFKzTXupHoHgiz4UWA8A5KujcULrSm2vmDhYJGOtN0SVAOqc0TAB+xLmT9eoeDhSd+oSkKg=="],
"@snowplow/tracker-core": ["@snowplow/tracker-core@3.2.0", "", { "dependencies": { "tslib": "^2.3.0", "uuid": "^3.4.0" } }, "sha512-EsOs8HlWPp3SIFn3TSG5Eo8l/QEU0ZyHVIlvmoQT3fOdsWZVrTK9IlrWi/sA0S43tVxpHKLuiEKgmQIrQXuVyw=="],
"@snowplow/tracker-core": ["@snowplow/tracker-core@4.10.2", "", { "dependencies": { "tslib": "^2.3.1", "uuid": "^11.1.1" } }, "sha512-L49i7OGAC2qQydnWXmKJnypTyVMqCWwfhKjC8Dk9mDsQn1+yzcHs+B4BxZB+vF0pCOrjPfGMWFmQkND4cWsrGA=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
@@ -1845,8 +1851,6 @@
"@types/node": ["@types/node@22.1.0", "", { "dependencies": { "undici-types": "~6.13.0" } }, "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw=="],
"@types/node-forge": ["@types/node-forge@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ=="],
"@types/pako": ["@types/pako@2.0.4", "", {}, "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw=="],
"@types/parse-json": ["@types/parse-json@4.0.0", "", {}, "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="],
@@ -1925,7 +1929,7 @@
"@types/yoga-layout": ["@types/yoga-layout@1.9.2", "", {}, "sha512-S9q47ByT2pPvD65IvrWp7qppVMpk9WGMbVq9wbWZOHg6tnXSD4vyhao6nOSBwwfDdV2p3Kx9evA9vI+XWTfDvw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/type-utils": "8.60.0", "@typescript-eslint/utils": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.60.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg=="],
@@ -1935,7 +1939,7 @@
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.60.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/utils": "8.60.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
@@ -1943,7 +1947,7 @@
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="],
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
@@ -4023,8 +4027,6 @@
"node-fetch-h2": ["node-fetch-h2@2.3.0", "", { "dependencies": { "http2-client": "^1.2.5" } }, "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg=="],
"node-forge": ["node-forge@1.3.0", "", {}, "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA=="],
"node-int64": ["node-int64@0.4.0", "", {}, "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs="],
"node-notifier": ["node-notifier@9.0.1", "", { "dependencies": { "growly": "^1.3.0", "is-wsl": "^2.2.0", "semver": "^7.3.2", "shellwords": "^0.1.1", "uuid": "^8.3.0", "which": "^2.0.2" } }, "sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg=="],
@@ -4713,8 +4715,6 @@
"secure-keys": ["secure-keys@1.0.0", "", {}, "sha1-8MgtmKOxOah3aogIBQuCRDEIf8o="],
"selfsigned": ["selfsigned@2.4.1", "", { "dependencies": { "@types/node-forge": "^1.3.0", "node-forge": "^1" } }, "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q=="],
"semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
@@ -4745,8 +4745,6 @@
"sha.js": ["sha.js@2.4.12", "", { "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1", "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" } }, "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w=="],
"sha1": ["sha1@1.1.1", "", { "dependencies": { "charenc": ">= 0.0.1", "crypt": ">= 0.0.1" } }, "sha1-rdqnqTFo85PxnrKxUJFhjicA+Eg="],
"shadow-cljs": ["shadow-cljs@3.3.6", "", { "dependencies": { "buffer": "^6.0.3", "process": "^0.11.10", "readline-sync": "^1.4.10", "shadow-cljs-jar": "1.3.4", "source-map-support": "^0.5.21", "which": "^5.0.0", "ws": "^8.18.1" }, "bin": { "shadow-cljs": "cli/runner.js" } }, "sha512-t5TBJmy77abwlkAY7UOXa1s9H/28nTHeDVckgzEVS8ONYtDvbNCHEuBYfS7wwV9wVZVybRNb7dILWNXISJL1Nw=="],
"shadow-cljs-jar": ["shadow-cljs-jar@1.3.4", "", {}, "sha512-cZB2pzVXBnhpJ6PQdsjO+j/MksR28mv4QD/hP/2y1fsIa9Z9RutYgh3N34FZ8Ktl4puAXaIGlct+gMCJ5BmwmA=="],
@@ -5065,7 +5063,7 @@
"ts-algebra": ["ts-algebra@1.2.2", "", {}, "sha512-kloPhf1hq3JbCPOTYoOWDKxebWjNb2o/LKnNfkWhxVVisFFmMJPPdJeGoGmM+iRLyoXAR61e08Pb+vUXINg8aA=="],
"ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
@@ -5595,11 +5593,11 @@
"@snowplow/browser-tracker-core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@snowplow/browser-tracker-core/uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="],
"@snowplow/browser-tracker-core/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
"@snowplow/tracker-core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@snowplow/tracker-core/uuid": ["uuid@3.4.0", "", { "bin": { "uuid": "./bin/uuid" } }, "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="],
"@snowplow/tracker-core/uuid": ["uuid@11.1.1", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ=="],
"@storybook/addon-docs/react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="],
@@ -5683,38 +5681,32 @@
"@types/leaflet/@types/geojson": ["@types/geojson@7946.0.8", "", {}, "sha512-1rkryxURpr6aWP7R786/UQOkJ3PcpQiWkAXBmdWc7ryFWqN6a4xfK7BtjXvFBKO9LjQ+MWQSWxYeZX1OApnArA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0" } }, "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0" } }, "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw=="],
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="],
"@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="],
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="],
"@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="],
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@visx/axis/classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
@@ -6803,8 +6795,6 @@
"typedoc/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.60.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/type-utils": "8.60.0", "@typescript-eslint/utils": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.60.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw=="],
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.60.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.60.0", "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA=="],
"unbox-primitive/which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
@@ -7101,28 +7091,32 @@
"@types/jest/pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
"@typescript-eslint/parser/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0" } }, "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="],
"@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
"@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
@@ -7301,8 +7295,6 @@
"detective-typescript/@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
"detective-typescript/@typescript-eslint/typescript-estree/ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"es-get-iterator/get-intrinsic/has-proto": ["has-proto@1.0.1", "", {}, "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg=="],
"es-get-iterator/get-intrinsic/hasown": ["hasown@2.0.1", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA=="],
@@ -7809,14 +7801,6 @@
"typed-array-length/is-typed-array/which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0" } }, "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/typescript-estree": "8.60.0", "@typescript-eslint/utils": "8.60.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.60.0", "", { "dependencies": { "@typescript-eslint/types": "8.60.0", "@typescript-eslint/visitor-keys": "8.60.0" } }, "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="],
@@ -7993,11 +7977,17 @@
"@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
@@ -8395,9 +8385,7 @@
"test-exclude/glob/path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.60.0", "", {}, "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="],
"webpack-dev-middleware/schema-utils/ajv-formats/ajv": ["ajv@8.10.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw=="],
@@ -8495,7 +8483,9 @@
"@storybook/builder-webpack5/terser-webpack-plugin/schema-utils/ajv-formats/ajv": ["ajv@8.10.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw=="],
"@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -8677,9 +8667,9 @@
"test-exclude/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.2", "", { "dependencies": { "jackspeak": "^4.2.3" } }, "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="],
"typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"webpack-dev-middleware/schema-utils/ajv-formats/ajv/uri-js": ["uri-js@4.4.0", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g=="],

View File

@@ -27,7 +27,7 @@ services:
metabase:
image: ${METABASE_IMAGE:-metabase/metabase:latest}
ports:
- "${METABASE_PORT:-3000}:3000"
- "127.0.0.1:${METABASE_PORT:-3000}:3000"
environment:
MB_DB_TYPE: postgres
MB_DB_DBNAME: metabase

View File

@@ -941,8 +941,10 @@
{:extra-deps {nrepl/nrepl {:mvn/version "1.5.1"}}
:main-opts ["-m" "nrepl.cmdline" "-p" "50605"]}
;; - start a Socket REPL on port 50505 that you can connect your editor to:
:socket {:jvm-opts ["-Dclojure.server.repl={:address,\"0.0.0.0\",:port,50505,:accept,clojure.core.server/repl}"]}
;; - start a Socket REPL on port 50505 that you can connect your editor to. Bound to loopback: it has no
;; authentication, so exposing it on a network interface is remote code execution. dev/docker-compose.yml
;; overrides the address for the containerized dev app, where the published port is the boundary.
:socket {:jvm-opts ["-Dclojure.server.repl={:address,\"127.0.0.1\",:port,50505,:accept,clojure.core.server/repl}"]}
;; Liquibase CLI:
;;

View File

@@ -39,8 +39,8 @@ services:
image: clojure
container_name: metabase-app
ports:
- "3001:3000"
- "50505:50505"
- "127.0.0.1:3001:3000"
- "127.0.0.1:50505:50505"
working_dir: /dev/metabase
volumes:
- ../:/dev/metabase
@@ -58,7 +58,10 @@ services:
MB_POSTGRESQL_TEST_PASSWORD: password
MB_POSTGRESQL_TEST_PORT: 5432
MB_POSTGRESQL_TEST_USER: user
command: ["clojure", "-M:run:dev:drivers:drivers-dev:ee:ee-dev:socket"]
# The :socket alias binds the REPL to loopback inside the container, which docker's port
# forwarding can't reach, so bind it to all container interfaces here instead. The host
# side stays loopback-only via the 127.0.0.1: prefix in `ports` above.
command: ["clojure", "-J-Dclojure.server.repl={:address,\"0.0.0.0\",:port,50505,:accept,clojure.core.server/repl}", "-M:run:dev:drivers:drivers-dev:ee:ee-dev"]
profiles:
- dev-app
networks:
@@ -79,10 +82,14 @@ services:
# the browser loads the bundle from the host's :8080 — it must be
# published, not just exposed on the docker network.
ports:
- "8080:8080"
- "127.0.0.1:8080:8080"
volumes:
- ../:/dev/metabase
- ~/.bun/install/cache:/root/.bun/install/cache
environment:
# rspack binds to loopback by default; inside the container it must listen on all
# interfaces for docker's port forwarding to reach it.
MB_FRONTEND_DEV_HOST: 0.0.0.0
command:
- /bin/sh
- -c
@@ -99,7 +106,7 @@ services:
container_name: metabase-postgres-app-db
restart: always
ports:
- "5432:5432"
- "127.0.0.1:5432:5432"
environment:
POSTGRES_USER: mbuser
POSTGRES_PASSWORD: password
@@ -116,7 +123,7 @@ services:
container_name: metabase-mysql57-app-db
restart: always
ports:
- "3306:3306"
- "127.0.0.1:3306:3306"
environment:
MYSQL_USER: mbuser
MYSQL_PASSWORD: password
@@ -138,7 +145,7 @@ services:
image: metabase/qa-databases:postgres-sample-14
container_name: metabasedb_postgres_14_data
ports:
- "15432:5432"
- "127.0.0.1:15432:5432"
restart: always
profiles:
- postgres14data
@@ -149,7 +156,7 @@ services:
image: metabase/qa-databases:mysql-sample-5.7
container_name: metabasedb_mysql_5_7_data
ports:
- "23306:3306"
- "127.0.0.1:23306:3306"
restart: always
profiles:
- mysql57data
@@ -160,7 +167,7 @@ services:
image: metabase/qa-databases:mysql-sample-8
container_name: metabasedb_mysql_8_data
ports:
- "13306:3306"
- "127.0.0.1:13306:3306"
restart: always
profiles:
- mysql8data
@@ -172,7 +179,7 @@ services:
container_name: metabasedb_mongo_6_data
restart: always
ports:
- "27017:27017"
- "127.0.0.1:27017:27017"
profiles:
- mongodata
networks:
@@ -183,7 +190,7 @@ services:
container_name: vertica_data
restart: always
ports:
- "5433:5433"
- "127.0.0.1:5433:5433"
profiles:
- verticadata
networks:
@@ -195,8 +202,8 @@ services:
image: maildev/maildev:latest
container_name: maildev
ports:
- "1080:1080"
- "1025:1025"
- "127.0.0.1:1080:1080"
- "127.0.0.1:1025:1025"
profiles:
- pulse
networks:
@@ -209,7 +216,7 @@ services:
- ./idp-private-key.pem:/idp-private-key.pem
- ./idp-public-cert.pem:/idp-public-cert.pem
ports:
- "7000:7000"
- "127.0.0.1:7000:7000"
entrypoint:
- node
- app.js

View File

@@ -32,7 +32,7 @@ Upload a custom icon. SVG or PNG with a transparent background gives the cleanes
![Metabot illustrations](./images/metabot-illustrations.png)
Once you've uploaded a custom icon, this toggle appears and controls whether the default decorative illustrations show in the Metabot chat sidebar and on the AI exploration page.
This toggle controls whether the default decorative illustrations show in the Metabot chat sidebar.
## Further reading

View File

@@ -257,6 +257,38 @@ Credentials:
- **Region**. Pick one from the dropdown in **Admin > AI**. Defaults to `us-east-1`. You can also set it with the environment variable `MB_LLM_BEDROCK_REGION`.
- **Session token** (advanced). Only together with **Access key ID** and **Secret access key**. Only needed for temporary credentials. You can also set it with the environment variable `MB_LLM_BEDROCK_SESSION_TOKEN`.
### IAM permissions for Bedrock
Metabase talks to Bedrock through the mantle endpoint, `https://bedrock-mantle.{region}.api.aws`, not through `bedrock-runtime`. Mantle is a separate IAM namespace with its own actions, so a policy written against the `bedrock` prefix won't grant access. Metabase lists models and runs conversations, so it needs `bedrock-mantle:ListModels` and `bedrock-mantle:CreateInference`.
Here's a least-privilege policy that grants both:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["bedrock-mantle:ListModels", "bedrock-mantle:CreateInference"],
"Resource": "arn:aws:bedrock-mantle:*:*:project/*"
}
]
}
```
The AWS managed policy [AmazonBedrockMantleInferenceAccess](https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonBedrockMantleInferenceAccess.html) also covers both actions (along with permissions Metabase doesn't use).
If Metabase reports "AWS Bedrock credentials lack permission for this model or action", check that your policy uses the `bedrock-mantle` prefix.
### The Bedrock models you can pick depend on the region
The table above lists the models Metabase can use. The **Models** card only offers the ones Bedrock serves in the connection's region, so what you can pick depends on the region. The mantle catalog has no cross-region inference profiles, so a model that isn't served in your region can't be reached from that region at all.
If the model list is empty or shorter than you expect after connecting:
- **Check the region**: the **Region** dropdown lists every AWS region, including regions where Bedrock serves none of these models. The [AWS model cards](https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards.html) show where each model is available. For example, the GPT models are only served in US regions.
- **Check your account's data retention setting**: Bedrock marks a model unavailable when your account's data retention mode doesn't meet what that model requires. For example, [Claude Fable 5](https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-fable-5.html) requires the `aws_review` data retention mode.
## vLLM
- Provider key: `vllm`

View File

@@ -58,7 +58,7 @@ To connect a provider with your own credentials:
1. Go to **Admin > AI**.
2. Click **Add a provider**.
3. Pick your provider.
4. Fill in its credentials. The **Where do I find this?** links open the provider's key management pages in a new tab.
4. Enter your credentials. Most providers only need an **API key**. The **Where do I find this?** link opens your provider's key management page in a new tab. For what each provider needs, see [AI providers](./providers.md).
5. Click **Connect**.
If you've already copied a key, one neat thing: just paste the key anywhere on the provider grid, and Metabase selects the matching provider and fills in the key for you. Check to make sure the provider matches.

3017
docs/api.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
---
title: "IP allowlist"
version: latest
has_magic_breadcrumbs: true
show_category_breadcrumb: true
show_title_breadcrumb: true
category: "Cloud"
layout: new-docs
---
# IP allowlist
Use an IP allowlist to limit access to your Metabase instance to approved IP addresses. The allowlist applies to all access to your instance, so people outside the allowlist can't reach the API, embedded dashboards and questions, or public links.
IP allowlists are only available on Metabase Cloud [Pro or Enterprise](https://www.metabase.com/pricing) plans. To restrict access to a self-hosted Metabase, use your own firewall or load balancer, or consider [air-gapping](https://www.metabase.com/product/air-gapping).
The allowlist accepts IPv4 addresses and CIDR ranges. Metabase treats a bare IPv4 address as a `/32` range.
By default, restricting access by IP address is turned off.
To allow Metabase to reach your databases, see [IP addresses to whitelist](./ip-addresses-to-whitelist.md).
## Set up an IP allowlist
When you save an allowlist, your instance restarts. Metabase blocks requests from addresses outside the allowlist, so include the address or range you're connecting from before you save.
The allowlist doesn't apply to the Metabase Store. If you leave out your own address, you can still log in to the Store and update the allowlist.
1. Log in to your Metabase [Store account](https://store.metabase.com).
2. Navigate to **Instances**.
3. In the instance you want to restrict access to, click **Settings**.
4. Scroll to **IP allowlist**.
5. Enable the **Restrict access by IP address** toggle.
6. In the **IP/CIDR ranges** field, enter an IPv4 address or a CIDR range.
7. To add more addresses, enter them in the empty field. To add several at once, paste a list with one address or range per line.
8. Click **Save**.
9. Review the warning, then confirm. Your instance restarts.
Metabase validates each entry as you type. To save the allowlist, you must first correct or remove invalid entries.
To remove an address, click the trash icon next to the address or range, then click **Save**.
## Turn off the IP allowlist
When you turn off the IP allowlist and save, Metabase clears the saved addresses. To restrict access again, you must re-enter the addresses.
Turning off the allowlist doesn't remove authentication. People still need to sign in to your Metabase.
In your instance's **IP allowlist** settings:
1. Disable the **Restrict access by IP address** toggle.
2. Click **Save**.
## Further reading
- [IP addresses to whitelist](./ip-addresses-to-whitelist.md)
- [Changing your domain name](./custom-domain.md)

View File

@@ -29,6 +29,7 @@ Manage your Metabase Cloud instance, subscription, and billing via the [Metabase
- [Changing which region your Metabase is hosted in](./change-region.md)
- [IP addresses to whitelist](ip-addresses-to-whitelist.md)
- [IP allowlist](./ip-allowlist.md)
- [Changing your domain name](./custom-domain.md)
### Migrations

View File

@@ -147,7 +147,9 @@ config:
embedding-homepage: hidden
embedding-secret-key: null
enable-embedding-interactive: false
enable-embedding-modular: false
enable-embedding-sdk: false
enable-embedding-simple: false
enable-embedding-static: false
enable-password-login: true
enable-pivoted-exports: true
enable-public-sharing: true
@@ -163,7 +165,6 @@ config:
help-link: metabase
help-link-custom-destination: https://www.metabase.com/help/premium
hide-stacktraces: false
http-channel-allowed-networks: external-only
humanization-strategy: simple
index-update-thread-count: 2
jdbc-data-warehouse-connection-pool-checkout-timeout-ms: 0
@@ -251,7 +252,6 @@ config:
loading-message: doing-science
login-page-illustration: default
login-page-illustration-custom: null
map-tile-server-allowed-networks: null
map-tile-server-url: https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png
mcp-apps-cors-custom-origins: ''
mcp-apps-cors-enabled-clients: []
@@ -282,7 +282,6 @@ config:
notification-link-base-url: null
notification-system-event-thread-pool-size: 5
notification-thread-pool-size: 3
oidc-allowed-networks: allow-all
oidc-providers: []
oidc-user-provisioning-enabled: true
persisted-model-refresh-cron-schedule: 0 0 0/6 * * ? *
@@ -382,5 +381,4 @@ config:
use-native-pivot-tables: false
use-tenants: false
user-visibility: all
warehouse-allowed-networks: null
```

View File

@@ -731,17 +731,7 @@ Allow admins to securely embed questions and dashboards within other application
Allow admins to embed Metabase via interactive embedding?
### `MB_ENABLE_EMBEDDING_MODULAR`
- Type: boolean
- Default: `false`
- [Configuration file name](./config-file.md): `enable-embedding-modular`
Allow admins to embed Metabase via modular embedding, the SDK for React, and guest embeds?
### `MB_ENABLE_EMBEDDING_SDK [DEPRECATED]`
> DEPRECATED: 0.65.0
### `MB_ENABLE_EMBEDDING_SDK`
- Type: boolean
- Default: `false`
@@ -749,9 +739,7 @@ Allow admins to embed Metabase via modular embedding, the SDK for React, and gue
Allow admins to embed Metabase via the SDK?
### `MB_ENABLE_EMBEDDING_SIMPLE [DEPRECATED]`
> DEPRECATED: 0.65.0
### `MB_ENABLE_EMBEDDING_SIMPLE`
- Type: boolean
- Default: `false`
@@ -759,9 +747,7 @@ Allow admins to embed Metabase via the SDK?
Allow admins to embed Metabase via modular embedding?
### `MB_ENABLE_EMBEDDING_STATIC [DEPRECATED]`
> DEPRECATED: 0.65.0
### `MB_ENABLE_EMBEDDING_STATIC`
- Type: boolean
- Default: `false`
@@ -901,6 +887,7 @@ Prevent the exception middleware from including stacktraces in responses.
- Type: keyword
- Default: `external-only`
- Environment variable only: you can't set this in the Admin settings or in a [configuration file](./config-file.md).
Controls which types of hosts are allowed as HTTP channel destinations.
Options:
@@ -909,6 +896,8 @@ Options:
- allow-all (no restrictions including localhost).
.
Set this when a notification webhook must reach a host on your private network (`allow-private`) or on this machine (`allow-all`). Default is `external-only`
### `MB_HUMANIZATION_STRATEGY`
- Type: keyword
@@ -1433,7 +1422,7 @@ Backed by the bedrock connection in the admin AI settings provider list: reads a
The AWS region for Amazon Bedrock (e.g. us-east-1).
Backed by the bedrock connection in the admin AI settings provider list: reads and writes go through the llm-providers connection list, and a value set by this environment variable shadows this one field of that connection. On a self-hosted Metabase, setting only the region enables Bedrock with the AWS default credentials chain, with no access keys configured.
Backed by the bedrock connection in the admin AI settings provider list: reads and writes go through the llm-providers connection list, and a value set by this environment variable shadows this one field of that connection. On a self-hosted Metabase, setting only the region enables Bedrock with the AWS default credentials chain, with no access keys configured. [The Bedrock models you can pick depend on the region](../ai/providers.md#the-bedrock-models-you-can-pick-depend-on-the-region).
### `MB_LLM_BEDROCK_SECRET_ACCESS_KEY`
@@ -1784,6 +1773,7 @@ The custom illustration for the login page.
- Type: keyword
- Default: `null`
- Environment variable only: you can't set this in the Admin settings or in a [configuration file](./config-file.md).
Controls which networks Metabase may connect to for map tile servers.
Options:
@@ -1792,6 +1782,8 @@ Controls which networks Metabase may connect to for map tile servers.
- allow-all (no restrictions).
Defaults to external-only on Metabase Cloud and allow-private when self-hosted.
Allowed network for the map tile server.
### `MB_MAP_TILE_SERVER_URL`
- Type: string
@@ -1800,6 +1792,16 @@ Controls which networks Metabase may connect to for map tile servers.
The map tile server URL template used in map visualizations, for example from OpenStreetMaps or MapBox. This URL is visible to clients, so do not include private keys.
### `MB_MAX_UNAUTHENTICATED_REQUEST_BODY_BYTES`
- Type: integer
- Default: `4194304`
- Environment variable only: you can't set this in the Admin settings or in a [configuration file](./config-file.md).
Maximum size in bytes of an HTTP request body from a caller who is not authenticated. Larger requests are rejected with HTTP 413 before the body is read.
Applies to requests without a valid session, API key or token. The body is bounded before authentication is checked, so this is the most memory an anonymous request can make the server buffer. It covers login, setup, password reset, SSO callbacks, and public or embedded queries; none of those legitimately send more than a few hundred kilobytes. Authenticated requests are not limited.
### `MB_MCP_APPS_CORS_CUSTOM_ORIGINS`
- Type: string
@@ -2028,10 +2030,12 @@ If Metabase stops sending notifications like alerts, it may be because long-runn
- Type: keyword
- Default: `allow-all`
- [Configuration file name](./config-file.md): `oidc-allowed-networks`
- Environment variable only: you can't set this in the Admin settings or in a [configuration file](./config-file.md).
What networks are OIDC requests allowed to? Possible values: 'allow-all' (default), 'allow-private', or 'external-only'.
Set this to tighten which networks OIDC discovery and token requests may reach; it defaults to allow-all. Other values: external-only and allow-private
### `MB_OIDC_PROVIDERS`
> Only available on Metabase [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans.
@@ -2178,7 +2182,7 @@ Network timeout (in seconds) for remote git operations such as fetch, push, clon
- Default: `300000`
- [Configuration file name](./config-file.md): `remote-sync-task-time-limit-ms`
The maximum amount of time a remote sync task will be given to complete.
How long a remote sync task may go without proving its process is alive (via heartbeat, or progress on rows without one) before it is treated as dead and superseded. A slow but live task is never affected. The task itself is aborted after ten times this limit.
### `MB_REMOTE_SYNC_TOKEN`
@@ -2997,6 +3001,7 @@ Note: Users with row or column security restrictions will never see suggestions.
- Type: keyword
- Default: `null`
- Environment variable only: you can't set this in the Admin settings or in a [configuration file](./config-file.md).
Controls which networks Metabase may connect to for warehouse connections.
Options:
@@ -3006,6 +3011,8 @@ Options:
Defaults to external-only on Metabase Cloud and allow-all when self-hosted.
Also covers the SSH tunnel host and the database auth-provider URLs.
Set this when Metabase must reach a warehouse on a private network (allow-private) or on this machine (allow-all). There is no admin UI for it, and a value stored in the application database is ignored. Defaults to external-only on Metabase Cloud and allow-all when self-hosted. Metabase refuses to start if this is set to anything but one of the three policies, rather than run on a policy nobody chose.
## Other environment variables
The following environment variables can only be set via the environment. They cannot be set by the configuration file.

View File

@@ -51,7 +51,7 @@ On self-hosted Metabases, this option determines whether or not you allow [anony
{% include plans-blockquote.html feature="Collecting user data" %}
You can switch on logging of IP addresses, user agents, embed path, query parameters, and Metabot conversation metadata for people using your Metabase, both for people logging in directly to Metabase, and for people who view an embedded Metabase component in your app. If enabled, you can find this information in your [usage analytics](../usage-and-performance-tools/usage-analytics.md).
You can switch on logging of IP addresses, user agents, embed path, query parameters, the values people submit when they run an action, and Metabot conversation metadata for people using your Metabase, both for people logging in directly to Metabase, and for people who view an embedded Metabase component in your app. If enabled, you can find this information in your [usage analytics](../usage-and-performance-tools/usage-analytics.md).
By default, collection of user data is turned **off**.

View File

@@ -343,11 +343,12 @@ To configure a dashboard using its URL, you can add the following optional keywo
- `fullscreen`
- `refresh`
- `theme`
Here's an example URL:
```
https://metabase.mydomain.com/dash/2#refresh=60&fullscreen&night
https://metabase.mydomain.com/dash/2#refresh=60&fullscreen&theme=night
```
The part that says `refresh=60` sets the dashboard to automatically refresh every 60 seconds, `fullscreen` sets it to fullscreen mode. Use an ampersand, `&`, in between keywords, and make sure there's a hash, `#`, after the dashboard's ID number.

View File

@@ -53,6 +53,12 @@ There are multiple ways to run actions:
- From a [public form](./custom.md#make-public) of an action.
- From a [button on dashboard](../../../dashboards/actions.md).
## Auditing actions
{% include plans-blockquote.html feature="Usage analytics" %}
Metabase records every action run in the [Query log](../../../usage-and-performance-tools/usage-analytics-reference.md#query-log) model in your Usage analytics collection.
## Actions change data in tables, which affect models
Just something to clarify here: actions, even though they are added to models, make their changes to the underlying table that a model queries. Which means that anyone who has access to the underlying table, or to questions or other models based on that table, will be able to see the effects of an action. Tools other than Metabase that are connected to that database will also pick up these changes.

View File

@@ -33,7 +33,7 @@ Once Docker is installed, you’re ready to go.
2. Copy and paste this command, switching out `<branch-name>` for the name of the branch you’d like to test:
```bash
docker run --platform linux/amd64 -d -p 3000:3000 --name metabase-dev metabase/metabase-dev:<branch-name>
docker run --platform linux/amd64 -d -p 127.0.0.1:3000:3000 --name metabase-dev metabase/metabase-dev:<branch-name>
```
3. In your browser, navigate to `http://localhost:3000`, where you should see Metabase. It may take a minute or two to start up depending on your computer.
@@ -51,7 +51,7 @@ docker pull metabase/metabase-enterprise-head:latest
Then:
```
docker run --platform linux/amd64 -d -p 3000:3000 --name metabase metabase/metabase-enterprise-head:latest
docker run --platform linux/amd64 -d -p 127.0.0.1:3000:3000 --name metabase metabase/metabase-enterprise-head:latest
```
The “latest” tag is not automatically upgraded on your local machine, so the above commands ensure that you’re pulling the latest changes.

View File

@@ -4,8 +4,28 @@ title: Driver interface changelog
# Driver Interface Changelog
## Metabase 0.65.0
- `sql.qp/use-ctes-for-stages?` is a multi-method for drivers to opt-in to compiling multi-stage queries
with CTEs instead of nested subselects. Drivers should only do this if they satisfy all of the criteria
in the docstring of this method.
## Metabase 0.64.0
- `metabase.driver.sql.normalize/default-schema` now takes the database as well as the driver:
`[driver database]`. The schema an unqualified table reference resolves to is a property of the connection for a
driver that opens a database where others have a default schema — ClickHouse now answers with the database its
connection opened, where before it inherited `"public"` from `:sql`. `database` may be nil when the caller has none
in hand, and such a driver answers nil for it.
- New feature `:transforms/testing` -- whether the driver can run transform test suites against temp tables. Drivers
with this feature implement the new multimethods `metabase.driver/temp-table-name` `[driver]`,
`metabase.driver/compile-create-temp-table` `[driver {:keys [table query]}]`,
`metabase.driver/compile-drop-temp-table` `[driver table]`,
`metabase.driver/do-with-test-connection` `[driver database f]`,
`metabase.driver/execute-on-connection!` `[driver conn query]` and
`metabase.driver/query-on-connection` `[driver conn query {:keys [max-rows]}]`.
- `metabase.driver.sql-jdbc.execute/cancelation-poisons-connection?` `[driver]` -- whether canceling a `Statement`
leaves the `Connection` unfit for the next query, so that it must be discarded rather than returned to the
connection pool. The query processor cancels a `Statement` whenever reduction stops before the `ResultSet` runs out
@@ -70,6 +90,10 @@ title: Driver interface changelog
- `metabase.driver/compile-create-index` `[driver schema table structured]` -- compiles a `:standalone` index into
the DDL statement(s) that create it.
- `metabase.driver/humanize-index-error-message` `[driver message]` -- trims the message of an exception raised by
`fetch-table-indexes` or by index DDL before it is shown to the user. Defaults to returning it unchanged;
ClickHouse drops the `(version ...) (queryId=...)` tail its server appends.
- `metabase.driver.sql-jdbc.sync.interface/db-tables` is now a multimethod for retrieving JDBC metadata
tables. SQL JDBC drivers can override this method to customize which database objects are discovered during sync.

View File

@@ -171,7 +171,7 @@ We have a few helpers for dealing with tests involving snowplow
Some of our tests depend on the email being set up, and require a local SMTP server. We use `maildev` Docker image for that purpose. At the time of this writing the image we use is `maildev/maildev:2.1.0`. The default cypress configuration for local development will handle this for you. If you want to set it up manually, you can use this command:
```sh
docker run -d -p 1180:1080 -p 1125:1025 maildev/maildev:2.2.1
docker run -d -p 127.0.0.1:1180:1080 -p 127.0.0.1:1125:1025 maildev/maildev:2.2.1
```
The e2e maildev is published on host ports 1180 (web UI) and 1125 (SMTP) on purpose. These differ, on purpose, from the 1080/1025 used for development.

View File

@@ -648,6 +648,11 @@ The Topic column on the [Activity log](#activity-log) model takes one of:
- transform-run-canceled
- transform-run-start
- transform-run-timeout
- transform-test-create
- transform-test-delete
- transform-test-run-start
- transform-test-run-timeout
- transform-test-update
- update-permission-failure
- update-transform
- upload-append

Some files were not shown because too many files have changed in this diff Show More