Compare commits

...

312 Commits

Author SHA1 Message Date
Aleksandr Lesnenko
476c4d1951 fixes 2026-07-31 14:56:32 -04:00
Aleksandr Lesnenko
03a914711d worktrees ux 2026-07-31 14:56:21 -04:00
Alexander Polyankin
91228287d8 Wait for the stash task id before polling for it
Starting the poll on matchPending raced /stash: the branch is created before the
export task is, so the first poll read the previous task, saw a terminal status
and stopped -- leaving the two remaining remote-sync specs waiting on a second
poll that never came. Keying off matchFulfilled with a task_id present mirrors
what the settings-update listener already does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 01:05:05 -04:00
Alexander Polyankin
2e57ac1f2c Start the sync progress state when a stash begins
The branch-and-push flows now make a single POST /stash instead of create-branch
followed by export, but the listener middleware only knew about exportChanges.
Nothing dispatched taskStarted, so the current-task poll stayed skipped, the
progress modal never opened, and three remote-sync e2e specs waited for a
request that was never made.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 00:35:53 -04:00
Alexander Polyankin
9c0996ff39 Keep worktree_id out of revision snapshots, and stop a test leaking transforms-enabled
A revert must never move content between worktrees, so worktree_id joins the
excluded columns for card, dashboard, dashcard and dashboard-tab revisions.

The worktree transform test wrapped itself in with-temporary-setting-values for
transforms-enabled. That setting has a derived getter, so under premium features
without :hosting it reads false, and the restore writes that back as an explicit
false -- disabling transforms for every test that ran afterwards in the same
JVM. Granting :hosting lets the getter answer true on its own, with nothing to
restore. The namespace is also marked synchronous, matching its siblings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 00:32:12 -04:00
Alexander Polyankin
50a13f0c6a Let worktree_id through the transform response schema and two lint gates
TransformResponse is a closed map, so every transform endpoint started
answering 400 once the model gained worktree_id -- which took out all twelve
Transforms Python driver jobs and the Druid one.

document-serdes-spec-test asserts the skip list verbatim, and two git commit
messages the sync writes to the branch trip no-literal-metabase-strings; those
are branch history, not UI text, so they get the directive rather than
getApplicationName.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 00:00:24 -04:00
Alexander Polyankin
e631be91fd Put worktree_id back on every worktree-scoped table
The previous commit narrowed worktree_id to Collection and derived everything
else from the collection a row lives in. That does not hold up: content needs
to answer which worktree it is in without a join, so the column goes back on
all thirteen tables, propagated from the parent at insert and immutable after,
with snippets, transforms and transform tag assignments back in scope.

What does not come back is the worktree_id_helper generated column and the
reworked entity_id uniques. entity_id stays globally unique, and
remote_sync_worktree_remapping keeps the per-worktree identity instead.

The reusable hooks live in OSS metabase.remote-sync.core, and the read gate now
mirrors the editable? family: collection-readable? / model-readable? are plain
OSS functions with an unconditional rule -- a row with a worktree_id is
admin-only, whatever the remote-sync settings say. On the write side the EE
editable? checks stop answering an unconditional true for worktree content and
answer is-superuser? instead, and the parent-collection perms set falls through
to the existing ___no-remote-sync-access string for both cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 23:41:34 -04:00
Alexander Polyankin
d8875eb846 Scope worktrees to collections and remap entity ids through a table
worktree_id was on thirteen tables and propagated down the content tree,
which needed a hook on every model, an immutability guard on every model, and
a generated column plus a reworked entity_id unique on every table just so a
worktree could hold its own copy of an entity the main app already has.

Put it on Collection alone. A worktree materializes collections; everything it
checks out lives inside one, so the collection answers which worktree any row
belongs to. Collections take their worktree_id from their parent on create and
can never change it, and that is the whole propagation story.

entity_id stays globally unique. remote_sync_worktree_remapping pairs the id
the branch knows an entity by with the id of the row the worktree checked it
out into, and serdes reads the pair in both directions: an export writes the
source id, a load resolves it back to the worktree's own row -- and to nothing
at all when the worktree has not checked the entity out yet, which is what
makes the load insert a fresh copy rather than overwrite the main app's. A row
created inside a worktree has no branch id, so its first export mints one.

Extraction is scoped the same way: Collection by its own column, everything
with a collection_id by an anti-join on it.

Transforms, transform tags and snippets leave the feature's scope; a worktree
push no longer collects them as export roots.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 23:18:20 -04:00
Alexander Polyankin
ec63f86f9b Revert "Make the main app a worktree row and let the database enforce containment"
This reverts commit 0fafa0c876.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 22:48:15 -04:00
Alexander Polyankin
0fafa0c876 Make the main app a worktree row and let the database enforce containment
worktree_id was nullable, which meant a composite (parent_id, worktree_id)
foreign key could not enforce anything: with a null in the key the check is
skipped entirely, so main-app rows were free to sit inside worktree
collections. Model the main app as a worktree row instead -- the one flagged
is_default, with a null branch since it follows the remote-sync-branch setting
-- and make worktree_id NOT NULL everywhere.

That makes the containment rule a foreign key. Thirteen composite FKs point
each child at its parent's (id, worktree_id), so content cannot be attached to
a parent in another worktree, or moved across worktrees, at all. Where the
parent link is NOT NULL the check is unconditional; where it is nullable it
covers everything except root-level content.

It also retires the worktree_id_helper generated columns. They existed only to
give the entity_id uniques a non-null discriminator, which worktree_id now is,
so the composite uniques are plain (entity_id, worktree_id) -- and with them go
the four dialect-specific DDL variants per table, the after-select hooks that
stripped the column, its serdes skip entries, and its cmd/copy special case.

check-parent-same-worktree stays only on the six models a user can reparent
from the UI, so those moves still answer 400 rather than surfacing a constraint
violation; everything else relies on the foreign key.

Dropping the single-column entity_id uniques moves to its own migration file
running before this one, with one block per dialect covering all eleven tables
rather than a changeset each, and a rollback that recreates them with explicit
names so the next migration can drop them portably.

Verified on H2, Postgres 15, MySQL 8 and MariaDB 10.6.
2026-07-30 18:25:13 -04:00
Alexander Polyankin
dbf0cb6a4a Build the worktree test's transform source with lib 2026-07-30 14:35:33 -04:00
Alexander Polyankin
6e6fb4b641 Pin worktree membership through serdes and keep worktree transforms out of runs
- every worktree-scoped model declares worktree_id and worktree_id_helper in its
  serialization spec, so the columns are accounted for per model rather than
  exempted wholesale in the spec-completeness test
- a worktree_id in incoming data is never honoured: a load stamps the scope it is
  pulling into and extraction is scoped to it, so the plain serdes API only ever
  reads and writes main-app content
- transforms checked out into a worktree cannot be run, by hand or from a job or
  DAG plan; their tags can still be edited
- transform dependencies resolve within the transform's own worktree
- worktree queries move from the model namespace into the API that uses them
2026-07-30 14:27:23 -04:00
Alexander Polyankin
e4ddbcf187 Leave worktree transforms out of the transform list
GET /api/transform gains include-worktrees, false by default: a transform checked
out into a worktree only shows up when the caller asks for it, the same way
worktree collections do.
2026-07-30 13:58:32 -04:00
Alexander Polyankin
e543555d35 Scope collection items to their collection's worktree
Browsing a collection's children now matches the parent collection's worktree
instead of taking an include_worktrees flag: a worktree collection lists its own
worktree's collections, a main-app one never lists worktree collections.
include_worktrees stays where it belongs, on the listing of all collections.
2026-07-30 13:51:33 -04:00
Alexander Polyankin
a18b1695ca Add remote-sync worktrees
A worktree is a checkout of a git branch materialized alongside the main app: its
content lives in the same tables tagged with a nullable worktree_id (NULL = main
app), so several branches' content can coexist in one instance.

- migration: remote_sync_worktree table; worktree_id on the collection-based synced
  content tables, remote_sync_object and remote_sync_task; a generated
  worktree_id_helper column backing per-worktree (entity_id, worktree) uniqueness
- worktree_id is never ambient: a pull sets it on the root collections it
  materializes, everything under them inherits it from its parent, and it can never
  be changed afterwards. metabase.remote-sync.core holds the helpers each model calls
- worktree content is admin-only, to create as well as to read and write: the gate
  sits in the collection perms layer, so every collection-based model inherits it
- listings leave worktree collections out unless asked for them (include_worktrees,
  like include_library)
- serdes matches entity ids within the worktree being loaded (serdes/*worktree-id*)
- EE: RemoteSyncWorktree model + admin-only CRUD under /api/ee/remote-sync/worktree;
  worktree_id/worktree-id params on the pull, push and status endpoints; the sync
  ledger and task history are scoped per worktree
- branch endpoints are now GET /branch and POST /branch, and creating a branch no
  longer switches the instance onto it - the FE creates-and-continues via stash
2026-07-30 13:46:09 -04:00
Andrei Chernykh
e4b496929a chore: viz registry and dashboard viz types (#78840)
* Type the visualization registry as components or bare definitions

* Convert the dashboard viz register module to TypeScript and use typed named exports for Action and IFrameViz
2026-07-30 09:29:46 -07:00
Denis Berezin
37a387ab60 UXW-4572 Disable greeting when conceal settings is on (#78973) 2026-07-30 19:24:31 +03:00
Denis Berezin
130d93223d UXW-4935 Fix long metric names overflow in browse/metrics table (#78958)
* UXW-4935 Fix long metric names overflow in browse/metrics table

* Add e2e check
2026-07-30 19:23:07 +03:00
Timothy S. Dean
7d1b01bac5 SQL Server: create :type/Text columns as nvarchar(max) instead of text (#78861)
`text` is non-Unicode, and it cannot be compared, sorted, or grouped. It's also deprecated (https://learn.microsoft.com/en-us/sql/t-sql/data-types/ntext-text-and-image-transact-sql?view=sql-server-ver17). `nvarchar(max)` is unbounded, Unicode, and not deprecated.

Affects transform target tables. Existing tables keep their text columns until a transform recreates them.
2026-07-30 10:07:27 -06:00
Alexander Polyankin
9d391efca1 Strip large literal lists (IN, tuple IN, ARRAY) before GraalPy SQL parsing (#79028)
* Strip large literal IN lists before GraalPy SQL parsing

Large literal IN lists (like large VALUES clauses) blow GraalPy's ~114KB/AST-node
memory cost and its 30s call timeout: a 300k-item list that sqlglot parses in
1.7s on CPython times out at 30s on GraalPy, poisoning a pooled context on every
background dependency-analysis pass over the offending query.

Extend the JVM-side pre-processing to also replace literal-only IN lists with
more than 100 items with `IN (NULL)`. Only lists consisting purely of literals
(numbers, quoted strings, NULL/TRUE/FALSE) are stripped; column references,
function calls, subqueries, and bind parameters disqualify a list, since
stripping those would change analysis results or parameter counts. Lists nested
inside unstripped IN subqueries are still found.

All callers get the same handling: the stripping happens in the one shared
strip-large-literal-lists (renamed from strip-large-values) used by every
analysis entry point, and is-single-stmt-of-type? keeps the #74284 restore
behavior — validate the stripped SQL, return the original text — so
impersonated queries still execute with their literal lists intact.

With the fix, field-references on a 300k-item IN list drops from a 30s timeout
to ~80ms.

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

* Simplify literal-list stripping to a single shared pass

Review cleanup: collapse the two per-keyword scanners into one walk over the
SQL driven by a combined VALUES|IN pattern with small per-keyword handlers,
extract the quote/doubled-quote scanning into a shared skip-string-literal
(also fixing count-top-level-commas, which didn't handle doubled-quote
escapes), and replace the substring-allocating literal-only-list? check with a
single index-based early-exit pass, so IN (SELECT ...) is rejected without
scanning or copying the subquery region. No-strip calls now return the
original string identity.

field-references on a 300k-item IN list drops from ~80ms to ~27ms; all
existing stripping tests pass unchanged.

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

* Add exhaustive tests for literal-list stripping

Cover threshold boundaries (100 vs 101) for both VALUES and IN, every accepted
literal shape, a disqualifier grid (17 non-literal items x 3 positions),
byte-for-byte preservation of surrounding SQL, nesting (IN inside VALUES
tuples, stripped-VALUES swallowing nested lists, deep IN-subquery chains),
idempotency, original-instance identity on the no-strip path, malformed input
(unbalanced parens, unterminated strings, empty lists), keyword false
positives (margin, JOIN, INT, quoted identifiers), and an end-to-end pass of
every analysis entry point over stripped SQL.

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

* Also strip large ARRAY literals and literal tuple IN lists

Two more static-data shapes reproduce the GraalPy 30s timeout / severe CPU
burn: id = ANY(ARRAY[...]) (the Postgres idiom ORMs emit instead of IN; 200k
items hit the 30s timeout) and composite-key tuple lists
(a, b) IN ((1, 1), (2, 2), ...) (50k tuples burned 21s).

Generalize the flat literal-list scanner to a parameterized closing delimiter
and reuse it for ARRAY[...] -> ARRAY[NULL], and add a tuple-list scanner that
requires every tuple to be literal-only, stripping to IN ((NULL, ...)) with
the first tuple's arity. Subscript access (arr[1], arr[1:5]), subquery tuples,
and any non-literal item still disqualify a list. The is-single-stmt-of-type?
restore behavior covers both new shapes.

After the change: tuple-IN 50k tuples 21.6s -> 525ms; ANY(ARRAY[200k])
30s timeout -> 1.1s. CASE WHEN chains and UNION ALL literal tables remain
unstripped (no lossless strip exists for them).

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

* Treat double quotes as a disqualifier in literal-list scanning

In identifier-quoting dialects (Postgres, Snowflake, ...) "x" inside an IN
list or ARRAY literal is a column reference, not a string, so stripping a
large list of double-quoted items would drop real field references from
analysis. The literal-list scan is dialect-agnostic, so be conservative: only
single-quoted strings count as literals; a bare double quote leaves the list
untouched. skip-string-literal still skips both quote styles where it is used
for paren/comma balance scanning, where either interpretation is a
skip-region.

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

* Use [[strip-threshold]] docstring links instead of backticks

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

* Fix Eastwood reflection warning: cast close-ch to primitive char

(= ch close-ch) compared a primitive char against the untyped close-ch
parameter, which Eastwood flags as an unresolvable Util.equiv call.

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

* Ignore keyword matches in strings/comments; reject empty list elements

Review follow-ups:

The keyword matcher previously rewrote text inside string literals and
comments — SELECT 'IN (0, ..., 149)' became SELECT 'IN (NULL)', and an
unbalanced quote inside such a region could corrupt the statement. The walker
now gap-scans between matches with a string/comment-aware cursor
(code-region-end) and copies any match that sits inside '...', "...",
-- ..., or /* ... */ through untouched.

The flat-list scanner also accepted empty elements, so invalid SQL like
IN (,,,) with >100 commas was rewritten into valid IN (NULL), masking syntax
errors from validation. Both the flat and tuple scanners now require content
in every element and reject leading/trailing/consecutive commas.

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

* Simplify IN/ARRAY stripping to one simple-list scanner

Stripping feeds fail-soft analysis only, and execution paths restore the
original SQL, so the defensive machinery accumulated from review rounds
(literal word allow-list, tuple/arity validation, empty-element checks,
string/comment-aware outer scanning) bought correctness for cases that don't
matter at the cost of real complexity. Replace it all with one rule: a list is
strippable when, from its opening ( or [, it contains only digits, signs,
single-quoted strings, commas, whitespace, and balanced parens (which covers
tuple lists with no special logic), and holds at least 100 commas. Flat IN,
tuple IN, and ARRAY all collapse into a single ~20-line scanner and one
decision fn; NULL/TRUE/FALSE items now conservatively block stripping. VALUES
keeps its pre-existing count-based path. The best-effort contract is stated in
the public docstring.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 11:58:31 -04:00
Timothy S. Dean
8f6204a41f Consolidate SQL string literal escaping (#78859)
Seven sites across four drivers were escaping SQL string literals the same way. This consolidates, and there's a single place to warn not to use escape-sql to sanitize user input.

No behavior changes.
2026-07-30 09:27:40 -06:00
Timothy S. Dean
4a260bbabb BigQuery: map java.math.BigInteger to INT64 in query parameters (#78844)
MBQL treats `java.math.BigInteger` and `clojure.lang.BigInt` interchangeably. Only the Clojure class was in the parameter table, so a BigInteger fell through in one of my tests to `:default` and bound as a string.
2026-07-30 09:27:03 -06:00
Nemanja Glumac
36b8479c2a Bump the init timeout to 120s (#79014) 2026-07-30 16:17:16 +02:00
Nemanja Glumac
ad16b000c8 Remove superfluous flaky e2e test (#79026) 2026-07-30 16:13:33 +02:00
Stas Gavrylov
477deaced9 Improve frontend TypeScript skill loading (#79009)
Update frontend skills to improve typescript-write referencing
2026-07-30 15:56:08 +03:00
Chris Truter
4fa4f822e6 Add pgvector store health metrics (#78668) 2026-07-30 12:34:26 +00:00
Egor Grushin
91c76f0f41 Ghy 4210 show the actual output destination for a transform run (#78874)
* Show the output schema and table for a transform run

Adds a TransformOutput component that renders the run's target as `schema › table`
breadcrumbs with a copy button. Both crumbs link out when they exist: the schema to
the data model, the table to the query builder. Wired into the run sidebar
("Output table" row) and the job run sidebar ("Output:" line).

Also fixes the job run sidebar not shrinking below its content width (missing
min-width: 0), which let long table names push it past the viewport.

* fix tests

* drop newTab prop, always open output links in new tab

* addressing PR's comments

* fix race condition
2026-07-30 13:20:06 +01:00
Chris Vandoodewaard
0efdb86767 add missing env variables so S3 path resolves (#79016)
* add missing env variables so S3 path resolves

* fix linting error
2026-07-30 07:43:57 -04:00
Phoomparin Mano
6db26cc0df chore(sdk): bump sdk version and use 64-alpha dist tag for data apps (#79011)
* bump sdk version with 64-alpha dist tag for data apps

* simplify message on why you should not use 63-stable
2026-07-30 17:37:32 +07:00
Romeo Van Snick
588be2a1eb Make metabase/router a basic-tier module (#78529)
The three preceding PRs removed every shared-tier import from the module: the
route guards, the redux location mirror, and the styled-Link re-export. All that
is left is metabase/utils, which basic may import.

basic rather than lib: router is a React component library, the same shape as
metabase/ui, and nothing in basic/* imports it, so lib would only widen what
lib/utils is allowed to reach for no gain.
2026-07-30 20:27:07 +10:00
Nicolò Pretto
24720615ab fix(sdk): sdk crashes when resizing column (#78794)
* fix(sdk): sdk crashes when resizing column

* do the same on ExecutionOutputTable too

* reword test to make it better

* make sure cols/rows are not new arrays every render when output is undefined
2026-07-30 12:18:20 +02:00
github-automation-metabase
e225496665 chore(sdk): bump SDK version on master to 0.64.0-alpha.0 (#79000)
chore(sdk): bump SDK version to 0.64.0-alpha.0 (alpha)
2026-07-30 10:02:15 +00:00
Stas Gavrylov
850763504b Derive the Tabs value type from the tab values (#78934)
* Derive Tabs value type from the tab values

Mantine types the Tabs value and onChange argument as `string | null`. Make
TabsProps generic over the tab value so callers get their own union back in
onChange instead of a bare string.

* Drop Tabs onChange type workarounds at call sites

Every `<Tabs>` call site now annotates its own tab union instead of casting or
re-narrowing the change value: the six `as SomeTab` casts (CommentsSidesheet,
PythonEditorResults, GroupLimitsSettingsSection, McpAnalyticsPage,
CliAnalyticsPage, ConversationStatsPage), the four in the admin/permissions and
tenants listings, and the handlers that laundered `string | null` through a
lookup or a literal comparison (AddDataModal, NotificationsTabs, TableSection,
the date pickers, TabPanel). AddDataModal's `isValidTab` guard existed only for
this and is gone; DatasetFieldMetadataSidebar's TAB map is now `as const`.

* Cleanup

* Apply suggestions from code review

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>

---------

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
2026-07-30 12:09:05 +03:00
Romeo Van Snick
55ab9b7e38 Make metabase/router side-effect free (#78882)
* Mark metabase/router as side-effect free for rspack

* Route deep metabase/router imports through the barrel

* Let static-viz import the router barrel instead of a deep module

* Guard the side-effect-free contract and scope the include to the directory
2026-07-30 19:01:09 +10:00
Ngoc Khuat
e415e8d14c Revert "Serialize card last_used_at so imports don't reset it" (#78991) 2026-07-30 08:33:40 +00:00
Romeo Van Snick
4a793a1294 Stop re-exporting the app's styled Link from metabase/router (#78528)
metabase/router/Link.ts re-exported metabase/common/components/Link so every
routing import had one home. But that Link is not a routing primitive: it wraps
RouterLink with a Tooltip from metabase/ui, a CSS module, and disabled/variant
props, so the barrel dragged shared/common and basic/ui into the router module
and looped back into router/router-link.

Call sites import it from metabase/common/components/Link again. RouterLink and
RouterLinkProps stay in metabase/router, which is where the primitive belongs.

The boundaries linter never flagged this: element-types checks imports, not
'export ... from'. So the edge had to go rather than be waived, or a lower tier
would have been green while the dependency was still there.
2026-07-30 17:28:40 +10:00
Arik Aronov
245e44542a new team member - enthym_aa (#78935) 2026-07-30 10:14:03 +03:00
Alexander Polyankin
70c3f15684 Fix N+1 query patterns with permissions (#78919)
* Fix N+1 query patterns in GET /api/database and GET /api/native-query-snippet

GET /api/database?saved=true&include=tables issued ~6-7 app-DB queries per
saved question (one metabase_database select-one plus a field select and
4-5 hydrations each) — ~3000 queries on instances with a few hundred
eligible cards:

- card->virtual-table: only fetch the database when include-database? is
  set, instead of unconditionally per card
- extract cards->card-id->metadata-fields so the result_metadata fields of
  all cards are fetched and hydrated in one batch (reused by
  batch-fetch-card-query-metadatas), and pass it from cards-virtual-tables
- ids-of-dbs-that-support-source-queries: one select instead of
  select-pks-set + per-id select-one

GET /api/native-query-snippet ran user-has-any-perms-of-type? — an
uncached EXISTS against data_permissions — once per snippet via
mi/can-read?, even though the answer is per-user, not per-snippet:

- user-has-any-perms-of-type? now primes the request-scoped permissions
  cache for all databases (the same pattern query-creation-capabilities
  already uses on GET /api/user/current) and answers by scanning the
  cached entries; repeated calls in one request are pure cache hits
- prime-all-dbs-cache records completeness with an ::all-dbs sentinel in
  the existing :db-ids set so the database id list is fetched only once
- the uncached EXISTS path is kept for cross-user checks and
  disable-perms-cache

Note: the cache prime excludes permission rows for inactive tables, so a
grant existing only on a deactivated table no longer satisfies
user-has-any-perms-of-type?.

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

* Track full-prime state under a dedicated :all-dbs-primed? cache key

Move the all-databases-primed marker out of the :db-ids set (where it sat
as a ::all-dbs sentinel among real database IDs) into its own
:all-dbs-primed? key on *permissions-for-user*. prime-db-cache now
preserves unrelated keys by assoc-ing instead of reset!-ing the atom.

Make prime-all-dbs-cache public with an optional pre-fetched-ids arity,
and switch query-creation-capabilities — the one caller that manually
primed all databases — to use it, so the full-prime work is shared with
user-has-any-perms-of-type? within a request.

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

* Simplify prime-all-dbs-cache: single arity, :all-dbs? key

Drop the pre-fetched-ids arity — the function always fetches the database
ID list itself — and rename the completeness marker from :all-dbs-primed?
to :all-dbs?.

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

* Align :all-dbs? docstring continuation line

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

* Add call-count regression tests for the two fixed N+1 endpoints

Both tests measure warm app-DB call counts via t2/with-call-count at two
entity counts and assert the count doesn't scale:

- GET /api/database?saved=true&include=tables with 2 vs 18 eligible cards;
  allows the 1-per-card toucan2 identity-query overhead from
  do-after-select but fails on the pre-batching one-select-per-card
  behavior (verified failing against the pre-fix code).
- GET /api/native-query-snippet with 2 vs 10 snippets as a non-admin;
  fails on the per-snippet permission EXISTS check (verified failing
  against the pre-fix code).

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

* Use mt/with-model-cleanup in call-count tests

Replaces the try/finally + manual delete pattern, so fixture rows are
cleaned up even when insertion or the warm-up request throws (addresses
review feedback).

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

* Rework any-perms-of-type caching to a dedicated request-scoped cache

Instead of priming the full permissions cache for every database and
scanning it, cache per [user-id perm-type] the set of database IDs
granting the most permissive value, loaded across all databases in a
single query. This answers every :exclude-db-ids variant from one small
fetch and keeps semantics identical to the uncached query.

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

* perms cache

* nil fix

* fix

* current state

* cleanup

* Test that inactive-table-only grants are ignored by aggregate perm checks

user-has-any-perms-of-type? previously counted grants existing only on
inactive tables (its raw EXISTS query had no active-table filter); now
that it is served by the distinct-perms cache it ignores them, matching
every other cached check. This is intentional — cover it with a test.

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

* Apply code-review fixes to the permissions cache rework

- Restore batch priming in with-can-run-adhoc-query via a new public
  prime-full-perms-cache (table-level checks otherwise load the cache
  one database at a time, up to 2 queries per database on card lists)
- Scope the non-cached distinct-perms load to one database instead of
  scanning all of the user's permission rows per call
- Extract the shared FROM/JOIN/WHERE of the two cache queries into
  perm-rows-query-base so their row sets cannot drift apart
- Simplify the full-perms interner binding and drop a dead fnil and a
  redundant double-check inside the distinct-perms swap!
- Select only :id and :engine in ids-of-dbs-that-support-source-queries
  (full rows decrypt :details per database, twice per request)
- Own the batched virtual-table Field fetch in a new
  cards->virtual-tables entry point; the helper is private again
- Use mt/with-dynamic-fn-redefs in permission-caches-load-per-type-test
- Document that the request caches are point-in-time snapshots

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

* Treat cached false as a hit in fast-interner

or-based lookups skip false, so interning false re-ran canon-fn and
putIfAbsent on every call (result was still correct). Use if-some so
presence, not truthiness, decides the hit.

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

* Document why distinct perms load all databases and why the uncached interner exists

Capture the review-thread answers in the docstrings: distinct-perms
preloads every database because most consumers ask cross-database
questions, and full-perms builds a per-call interner even without a
request cache because group values are highly repetitive.

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

* Batch-prime the full perms cache in the remaining per-table check loops

Endpoint tracing found three more loops that fill the full permissions
cache one database at a time now that priming is on demand: the
include=tables branch of GET /api/database (per-table can-read?/
can-query?/can-write?), batch-fetch-query-metadatas* (per-table
can-read? behind dashboard/card query_metadata), and the EE
filter-tables-by-data-model-perms loop. Each now batch-primes the perm
types it checks, one query per type instead of up to 3 per database.

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

* Simplify perms caches to all-types loading; rename to table/schema caches

Per-type bookkeeping wasn't worth it — nearly every consumer needs
several permission types, and there are only a handful. Both caches now
load every permission type at once: the table-level cache (was
*full-perms-cache*) per database, the schema-level cache (was
*distinct-perms-cache*, table_id collapsed so schema is the finest
granularity it answers) for all databases in a single query on first
use. prime-table-perms-cache takes just database IDs and no-ops for
superusers, whose checks never read the cache.

Also batch-prime the remaining per-table check loops found by a
repo-wide audit: search result filtering, GET /api/table, recent views
(also reusing the already-fetched table instead of re-selecting it),
typed-schemas table listings, and the segment/measure list endpoints.

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

* Drop the search prime and scope the recents prime to actual table recents

Benchmarks on a 10M-row data_permissions Postgres (200k rows for the
test user) showed both primes regressing their endpoints: search went
40ms to 840ms because priming all databases loads every permission row
of the user on each request while matching results only touch a few
databases, and recents primed the whole instance because table-recents
returns every table when given no IDs. Search now relies on lazy per-db
loads; table-recents returns nothing for empty IDs so the prime no-ops
naturally.

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

* Load the schema perms cache per database, with a priming function

Benchmarks showed the all-databases SELECT DISTINCT costing ~230ms per
request at 200k user rows, taxing endpoints that ask about one database
(dashcard query 228ms -> 445ms). The schema cache now mirrors the table
cache: single-database questions load one database at a time (indexed,
all permission types at once), loops over many databases call
prime-schema-perms-cache, and cross-database questions load everything
once, recorded under :all-dbs? so repeated checks stay one query.

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

* Load perms caches via next.jdbc/plan instead of toucan2 rows

JFR allocation profiling showed toucan2's per-row machinery (row delays,
transient instances, realize maps) dominating the cache loads' share of
request allocation. plan reads columns straight off the ResultSet with
no per-row maps; the folds are unchanged. Runs on the current toucan2
connection so transactional writes stay visible. On the 10M-row bench
this cuts per-request allocation ~10-15% further (e.g. /api/user/current
570MB on master -> 376MB) and 50-90ms of latency on load-heavy
endpoints. Note plan queries are invisible to t2/with-call-count, so
one test now spies on the load fn instead of counting calls.

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

* Store perms caches as mutable Java collections, skipping persistent copies

The folds now build nested HashMaps/HashSets that the caches store
as-is: no per-row persistent update-in churn and no whole-structure
persistent copy at the end. Clojure's lookup/seq functions read Java
collections directly, so consumers are unchanged. Only the two outer
levels (perm-type, db-id) are ConcurrentHashMaps that later loads extend
via putAll of missing databases; everything below is written once by its
load and then only read. Numeric keys are canonicalized to Long at both
fold and lookup time — JDBC hands back Integer for int columns and Java
maps compare boxed keys with equals, not Clojure's equiv. The entry
interner is gone with the persistent copy; instances with thousands of
identical per-database profiles lose that sharing (#76077), which is the
trade-off for the allocation win.

On the 10M-row Postgres bench (200k rows for the user) vs master:
/api/user/current 570 -> 268 MB allocated per request, GET /api/database
?saved=true&include=tables 1773 -> 1032 MB and 4.1s -> 2.5s,
GET /api/table 1152 -> 865 MB and 1.9s -> 0.9s.

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

* Answer cross-database permission questions from a light per-function cache

Instances with thousands of databases and groups make loading tuples
for every database (schema cache with :all-dbs?) scale badly: the
distinct tuple set alone reaches hundreds of thousands of rows. The
questions that actually span databases only need one value per database
for one permission type, so most-permissive-database-permission-for-user
(signature unchanged) and user-has-any-perms-of-type? now share
*most-permissive-perms-cache*: {user-id {perm-type {db-id value}}},
loaded by one indexed query returning distinct (database, value) pairs
and coalesced per database. The :all-dbs? machinery is gone and
query-creation-capabilities needs no priming — its per-database loop
hits the cache after one query.

On the 10M-row bench this takes GET /api/user/current from 722ms/268MB
allocated to 556ms/182MB — 5MB above the response-validation floor
measured with all permission checks short-circuited (master: 833ms/570MB).

Also reorganize the caching section (atoms, then loaders, then the
functions using them; coalesce moves above since a loader needs it) and
relax the xrays call-count expectation: permission-cache loads go
through next.jdbc and are invisible to t2/with-call-count.

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

* Coalesce permissions in SQL and give every check its own per-function cache

Each permission-checking function now owns one atom, one hand-written
aggregate query, and a cache structure fitting exactly its question,
organized per function. Values map to ranks (value-rank-case) and
GROUP BY collapses tables, duplicate values and — via two nested levels
of aggregation for the most-restrictive-within-group-then-most-
permissive-across-groups questions — group identity too, so result sets
scale with tables/schemas/databases rather than raw row counts
(dbs x groups x tables x values). A bucket's [min max] rank pair
reconstructs coalesce exactly, verified exhaustively per permission
type including :perms/view-data's special rule; full-schema keeps its
per-group db-level union by expanding db-level rows into every schema
inside its query. native-download delegates to full-db-permission
(identical semantics), user-has-any derives from the most-permissive
cache.

With the row-count multiplier gone the loads are back to plain t2/query
and persistent Clojure collections: the next.jdbc/plan machinery, the
mutable Java collections, the Long key canonicalization, and the
interner are all gone, and cache loads are visible to
t2/with-call-count again. No public API changes; the batch primer is
now prime-db-perms-cache and warms every per-database cache.

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

* Drop a docstring reference to the removed full-perm-rows

The ~15x realize-before-key-access observation stands on its own; the
function it pointed at no longer exists now that the permission caches
load SQL-side aggregates.

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

* Rewrite app-db NOT IN (SELECT ...) queries as NOT EXISTS anti-joins

NOT IN (subquery) degrades to a per-row subplan on Postgres once the
subquery result exceeds work_mem. Rewrites all runtime occurrences to
correlated NOT EXISTS (or a literal comparison where the subquery was
0/1 rows). MySQL keeps the double-wrapped subquery in
delete-database-fields! since it cannot reference the DELETE target
table in a subquery.

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

* Keep is_audit predicate as NOT EXISTS in set-default-database-permissions!

Tests create audit DBs with random ids, so the fixed audit-db-id
constant is not equivalent to the is_audit = true subquery here.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 00:24:50 -04:00
Romeo Van Snick
76e2e9ff2b Migrate enterprise plugin pages off withRouteProps (#78426) 2026-07-30 13:01:29 +10:00
Ngoc Khuat
0f1f3ce5ef Snowflake: stop over-escaping schema names in the dynamic-table probe (#78807)
Snowflake: don't over-escape schema names in the dynamic-table probe

The FK sync path pre-escaped schema/table names (`_` -> `\_`) before handing
them to `dynamic-table?`, which uses the schema as an identifier in
`IN SCHEMA <db>.<schema>`. `"RAW\_DATA"` is not a real schema, so Snowflake
threw for every table in any underscore-named schema and we logged a full
stack trace each time.

`dynamic-table?` now takes raw names and escapes only the `LIKE` argument,
where `_` is actually a wildcard. Escaping for the JDBC metadata call moved
to the call site. Also log the caught exception without the stack trace,
since it fires once per table.

Fixes #78541

Co-authored-by: da <paoliniluis@gmail.com>
2026-07-30 02:23:06 +00:00
Fraser
70ada33ee2 Move api plugin slots into api (#78747) 2026-07-30 10:40:18 +12:00
github-automation-metabase
40c64b039c Auto-update documentation for master (#78972)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-07-29 22:32:21 +00:00
Maksym Yakubych
0af1565243 Generate a fresh title for a forked Metabot conversation (#78379)
* Generate a fresh title for a forked conversation from its first new message

* Show "Forked conversation" instead of "New conversation" in fork header

* Rename first-valid-user-message to first-non-forked-user-message

* Align map value columns in metabot conversation schemas (PR review nits)
2026-07-29 19:16:33 -03:00
Maksym Yakubych
7e7ad719c0 feat: fork a Metabot conversation at an assistant message (#78261)
* feat: fork a Metabot conversation at an assistant message

* feat: surface Metabot conversation fork origin in analytics

Record forked_from_conversation_id on the conversation and
forked_from_message_id on each copied message, expose them through the
metabot-analytics API and audit views, and link a fork back to its
original thread.

* Update git_branch icon

* Show source conversation title in fork boundary and header

* Revert fork boundary to the attempt where the fork happened

Showing the fork boundary divider on every regenerated attempt was
misleading: the fork only happened on the inherited attempt, so the
divider matches that single message again.

* Refine fork boundary rendering and tests

* Exclude forked (copied) messages from v_metabot_messages

* Address PR review: unify message-action guard, extract ForkBoundary, assert fork toasts via UI

* Reuse createMockMetabotConversationDetail for the fork test fixture

* Replace fork props on Messages with a renderAfterMessage slot; move ForkBoundary to EE

* Drop the redundant UndoListing now that the metabot test setup mounts it

* Index and FK-constrain forked_from_conversation_id so deleting an origin nulls the link

* Split forked messages out of usage metrics with new_message_count

v_metabot_conversations gains new_message_count, which leaves out the
messages a fork copied in from its origin. message_count keeps counting
the whole thread, so the conversations list, the detail page and the
shipped model all report what the reader can actually see.

The usage stats charts aggregate the new column instead, which makes
SUM(new_message_count) reconcile with COUNT(*) FROM v_metabot_messages.
metabot-stats leaves copies out of :metabot-queries and :metabot-users
for the same reason — copies inherit role/ai_proxied and are stamped
with a fresh created_at, so a fork was reporting LLM calls that never
happened.

* Don't offer to fork while a turn is streaming

Only the streaming message's actions were hidden, so earlier assistant
messages kept their fork button mid-turn. Forking then swapped the
conversation out from under an in-flight stream whose reducers are keyed
by agentId alone, so the old turn's remaining deltas rendered inside the
fork.

Withholding onFork while isDoingScience is set is preferable to
cancelling the request: the user asked to branch off an earlier message,
not to stop the response, and loadConversation already avoids cancelling
so an aborted turn isn't recorded.

* Realign the analytics schema maps and fix a migration author

The fork columns are longer than the alignment column the surrounding
maps used. ConversationDetail had already drifted into two alignment
columns before this branch; both maps are now uniform.
2026-07-29 19:16:33 -03:00
Denis Berezin
b5c15c5002 UXW-4935 Hover effect for timeline event chips (#78865)
* UXW-4935 Hover effect for timeline event chips

* Remove vertical line from bar on hover

* Review fix

* Fix e2e tests
2026-07-30 01:14:28 +03:00
Fraser
b9fe183aa5 Invert markdown and email JSX rendering out of the formatting engine (#78880)
Extends the dynamic JSX formatter pattern from #77658 (which did links)
to markdown and email. formatValue and formatEmail now call a registered
renderer through the registry instead of importing ReactMarkdown/Mustache
and ExternalLink directly; the renderers live in ui.tsx and are wired up
at app boot by registerJsxFormatting (called from registerVisualizations).

This keeps the react-markdown/micromark/mustache stack and the mailto
ExternalLink out of the static-viz bundle, which imports the formatting
engine through shared echarts code but never registers the JSX renderers
(so jsx formatting degrades to plain text there, as it already did for
links). No behaviour change in the app.

Static-viz bundle: 3,450,947 -> 3,297,087 bytes (~150KB, 4.5%).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 10:07:37 +12:00
Jeff Bruemmer
edae5380bb docs - filter out read-only settings for config (#78970)
filter out read-only settings for config
2026-07-29 18:03:12 -04:00
Ben Grabow
bb4fa3fcf4 GHY-4215 Ignore invalidated-at when checking for early refresh (#78945)
Ignore invalidated-at when checking for early refresh
2026-07-29 13:32:45 -05:00
Timofey Kachalov
96e41d0d70 Hide data apps nav items if user does not have a token feature (#78946) 2026-07-29 22:22:28 +04:00
Egor Grushin
537113ca37 Gdgt 2911 theres no way to see current job progress on the job page (#78879)
show last job run transform statuses on the job page
2026-07-29 17:31:13 +01:00
Nemanja Glumac
891bb18a18 Add the sandbox-restricted-fields passthrough to metrics permissions (#78937)
metabase.metrics.core re-exports sandbox-restricted-fields from metabase.metrics.permissions, and metabase.metabot.tools.entity-details calls it through that API. The passthrough to metabase.permissions.metric was missing, so importing the re-export threw at load and broke every namespace downstream of metabase.metrics.core.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 12:25:14 -04:00
github-automation-metabase
794245fb42 Auto-update documentation for master (#78857)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-07-29 12:02:31 -04:00
dpsutton
66994aac46 Fix driver quarantine gate never failing the job (#78866) 2026-07-29 17:30:12 +02:00
Nemanja Glumac
d99fede8e2 Assert the skipped-dependent tally in the transform job failure email test (#78930)
Transform failure emails list root-cause failures by name and summarize dependents that never ran as a count, so the test asserts the tally instead of the dependent's name.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:29:49 +02:00
Denis Berezin
7eaf406eb1 Add Metric Dimensions (#76623)
* Add Metric Dimensions section

* [metrics] Field IDs are not unique! Multiple FKs to same table.

Fixes a few places where the field ID alone is assumed to be a unique
key for dimension mappings.

* [metrics] Add basic CRUD API for metric v2 dimensions

Fixes UXW-4631.
Fixes UXW-4633.

* [metrics] Add `/api/metric/$ID/dimension/set-default` endpoint

Fixes UXW-4632.

This sets the dimension of a Metric v2 with the specified
`dimension_id` (a body parameter) to be the `:default true`.
All other dimensions have `:default` removed, so there is at
most one default dimension for any metric.

* [metrics] Metric dimensions create dependencies on the tables

Fixes UXW-4636.

* [metrics] Serdes support for curated Metrics v2 dimensions

Fixes UXW-4634.

Previously `:dimensions` and `:dimension_mappings` were marked as
`:skip` on serdes since the dimensions were always synced on read.
Now that they're curated and only synced when blank, we need to
properly serialize dimensions and mappings.

* Adjust Metric dimensions UI + connect to api

* Add dimensions reorder with dnd

* Add metric dimensions usage in metric viewer

* Change Metric Viewer to group dimensions by type

* Implement shared dimensions logic, add e2e tests

* Use default dimension in Metric Viewer

* Align headings in panels / Make trash and add buttons the same height

* Move and simplify the Set as default button

* Make about page use the default curated dimension in the preview chart

* [dimensions] Bump `:card_schema` to 24; backfill `:dimensions` in old style

Fixes UXW-4808.

This gives pre-existing v2 Metrics the full `breakoutable-columns` as
their dimensions, rather than just the main group as is now the
default. That preserves the old behaviour for the dimensions of the
metrics.

* Respect default metric dimensions on dashboards / Add projectDimension lib function (#77910)

* Respect default metric dimensions on dashboards

* Consolidate metric dashboard E2E coverage

* Remove default time dimension from metric editor

* Implement a few design tweaks in Dimensions tab UI

* Use curated metric dimensions on dashboard metric card filters

* Add dimension picker to metric About page

* Auto-pick default dimensions for new metrics

* Additional cleanup to the About page dimension select element

* Expand all metrics in See all dimensions

* Remove table names from metric filters

* Don't show sub-headers in 'See all' dimension view in Metric Viewer

* Remove metric overview tab

* [UXW-4859] Handle empty curated dimension list and removed default dimension edge cases

* Fix default viz type when adding metric to dashboard

* Remove the 'See all' option on explorer page for single metric visualization

* [UXW-4863] Don't use custom display name as field reference in dimension detail column

* [backend] Fix persistence of upgraded metric dimensions

* [UXW-4880] Re-add Overview tab, show curated dimensions

* [UXW-4848] Make sure we show all shared dimensions in the Shared Dimensions view, if the underlying fields match

* [UXW-4771] Make pinned metric in collection respect default curated dimension

* Update e2e tests to account for PR updates [part 1]

* Seed explicitly joined metric dimensions

* Fix metric dimension serialization dependencies

* Fix/adjust backend unit tests

* Fix metric dimension lint issues

* Run metrics with saved card permissions

* Update e2e tests to account for PR updates [part 2]

* Update kondo ratchet for metric serialization test

* Update metric permissions e2e expectation [part 3]

* Fix BE sandbox unit test

* Prefix upgraded metric dimensions with connected table names (#78410)

[UXW-4896] Prefix upgraded metric dimension names

* Address FE issues from July 22 reviews

* use curated dimensions on series breakout list / fix dimension display name on breakout legend and dimension pill bar

* Optimize getMetricMappingOptions

* Adjust about page dimension select margin-bottom and dropdown positining

* Fix curated metric dimension selection across duplicate foreign key paths

* Improve logic to show correct dimension label in the CenterControls button

* Exclude orphaned dimensions from runtime metric metadata

* Address Ethan's comments / Clean up

* Use metadata field cache when checking user permission / Improve field validation when adding dimension

* Address latest reviews

* Update serialization baseline card_schema 23 -> 24

* Tighten docstrings and comments per review feedback

Remove caller references, ticket refs, and usage context from docstrings
and comments; describe contracts instead.

---------

Co-authored-by: Braden Shepherdson <braden@metabase.com>
Co-authored-by: Anderson <anderson@metabase.com>
Co-authored-by: Anderson <ands.alves.nunes@gmail.com>
2026-07-29 12:24:41 -03:00
Sloan Sparger
94408e998f [Metabot] Non-reasoning model loading state (#78855)
handle non-reasoning models better
2026-07-29 10:22:50 -05:00
Sloan Sparger
776e1a0c61 [Metabot] Reasoning UI (#78417)
* chain of thought ui

* self-review pass

* agent review

* height fix

* move reasoning narration gate out of shared snippet

* pr feedback
2026-07-29 10:22:50 -05:00
Sloan Sparger
207a7bdb06 [Metabot] Improved streamed reasoning metadata (#78416)
* stamp tool display titles and search results onto the stream

* self-review

* agent review

* avoid deprecated mbql-query macro

* expect search_results data event in integration test
2026-07-29 10:22:49 -05:00
Sloan Sparger
209d1ce72a [Metabot] Stream reasoning tokens (#78415)
* stream provider reasoning through the aisdk pipeline

* self-review

* agent review

* pr feedback
2026-07-29 10:22:49 -05:00
Andrei Chernykh
f670615026 chore: Convert the last auth JS file to TypeScript (#78863)
* Convert the last auth JS file to TypeScript

* cleanup test

---------

Co-authored-by: Uladzimir Havenchyk <uladzimir@metabase.com>
2026-07-29 08:02:56 -07:00
github-automation-metabase
e75f249d70 Stabilize SSO group-mapping delete flake by serializing setting round-trips (#78907)
* Stabilize SSO group-mapping delete flake by re-reading settled state

Selecting groups for a mapping optimistically patches the settings cache,
but the setting update also invalidates and refetches session properties.
A trailing stale refetch can revert the last-added group after the test's
per-selection checkbox assertion has already passed, leaving the mapping
with only one non-admin group. The delete then fires a single deleteGroup
request and cy.wait times out waiting for the second.

Re-load the auth settings page between adding groups and deleting the
mapping, and assert the row shows its full group summary before acting.
The fresh page load aborts in-flight refetches and reads a single settled
server state, so every mapped group is present when the delete fires.

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

* Serialize SSO group-mapping edits against their setting round-trips

Adding a group to a mapping PUTs the group-mappings setting (built from the
widget's current mappings cache) and invalidates session properties, which
triggers a background refetch. The widget optimistically patches the cache,
but a trailing stale refetch can revert the just-added group; the next
selection then builds its PUT from that stale set and overwrites the previous
group server-side (last-write-wins). The mapping ends up with fewer groups
than the test expects, so fewer deleteGroup requests fire and cy.wait times
out waiting for the second one.

Wait for each mapping mutation's PUT and its trailing session-properties
refetch to settle before the next interaction, in createMapping,
addGroupsToMapping (per group), and deleteMappingWithGroups, so every edit is
built from a fully committed round-trip. deleteGroup/clearGroup invalidate
only permissions-group, so updateSetting is the sole source of session
refetches and the waits stay aligned one-to-one.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 17:00:59 +02:00
Chris Vandoodewaard
27f5824c42 Remove broken assertion from StrategyEditorForDatabases tests (#78916)
cleanup unused import
2026-07-29 10:47:45 -04:00
Chris Truter
2edc737aea Fix metadata enrichment permissions for linked tables (#78896)
* Fix metadata leaks for unreadable tables

* Close metric enrichment permission gaps

* Respect sandboxed metric dimensions
2026-07-29 16:32:01 +02:00
Anderson
6a045e90d6 Make sorting consistent in small dashboard tables when including NULL text values (#78701)
* Fix dashboard sorting for small text datasets

* Preserve automatic sorting for non-text columns
2026-07-29 11:25:21 -03:00
Chris Truter
113f912ad2 Make the library index's authorization contract explicit (#78897) 2026-07-29 13:53:44 +00:00
Sébastien
e1fb469d57 Preserve passed size in SDK question loading and error states (#78906) 2026-07-29 15:41:47 +02:00
Nicolò Pretto
f8bdb80c12 refactor eajs bridge (#78570)
* fix: scope Metabase host marker to main app

* refactor eajs bridge
2026-07-29 12:53:20 +00:00
Chris Vandoodewaard
024244e68e add LTS Docker tag to releases (#77885)
* update getExtraTagsForVersion to append -lts if isLts is set

* add ability to check if a version is an LTS version

* set islts

* update function to determine if we should tag with lts

* add condition for multiple LTS releases so we only tag the latest

* fix actionlint errors

* update lts tag to include major version

* await result of getExtraTagsForVersion

* fix type

* fix merge conflict, don't throw error
2026-07-29 08:26:53 -04:00
Chris Vandoodewaard
71b85f3710 account for the case where something changes in github/workflows and it isn't a workflow (#78904) 2026-07-29 08:26:34 -04:00
Nemanja Glumac
3fd695cba6 Fix broken backend test assertions (#78902) 2026-07-29 12:03:45 +00:00
Oleh K
95e367f533 Remove color-mix from theme color definitions (#78665)
* GDGT-2851 Remove color-mix from theme color definitions

color-mix can only be resolved by a browser, so any theme color using it
breaks static viz, which renders on the server (see UXW-4690, where the
gauge failed on background_page-tertiary-inverse).

Per design's proposal, introduce a new orion-95 stop and use solid scale
values for the page backgrounds:

- dark: page primary/secondary/tertiary -> orion 95/100/110
- light: page primary/secondary/tertiary inverse -> orion 95/100/110

The remaining mixes are collapsed to their literal equivalents:
shadow-default to a plain rgba, and tooltip-background-focused to the
next stop down from tooltip-background.

The mixes that derive from whitelabel colors (core-brand, core-filter,
core-summarize) are left alone, since their values aren't known ahead of
time. A unit test now guards against reintroducing color-mix anywhere
else in the theme definitions.

* Update Loki Snapshots

* Remove var() references and non-hsla literals from theme definitions

Static viz feeds theme colors straight into Color(), which only parses
literals, so a var() throws there exactly like a color-mix does — even
when it points at a perfectly ordinary color. Collapse the remaining
var() references to the underlying base color, and normalize the few
hex and bare-keyword values to hsla so every theme color is a literal.

The guard test drops the transitive resolver it no longer needs and just
asserts that every non-whitelabel color is an hsla literal.

* Fix regression in pivot table

* Empty commit to trigger CI

* Manually update pivot table loki snapshot

* Update e2e colour assertions for the new background tokens

Dark background_page-primary moved from orion-100 to the new orion-95,
so anything asserting rgb(7, 23, 34) for it now expects rgb(12, 28, 39).

The dark mode body assertion was matching color(srgb 0.0204 0.06792
0.0996) — Chrome's computed form for the old color-mix on
background_page-secondary. That token is a plain hsla now, so Chrome
reports it as rgb(7, 23, 34).

* Fix e2e

* Fix e2e

* Revert changes in assertDarkMode

* Fix oss e2e spec

* Revert SDK theming assertions to the deprecated background token

The SDK theme presets resolve colours through the first entry of
SDK_TO_MAIN_APP_COLORS_MAPPING, and for `background` that is the
deprecated `background-primary`, which still points at orion-100.
`background_page-primary` sits at index 1 and never gets read, so
table-root is unaffected by the move to orion-95.

* Address review feedback on the colour theme changes

Move the gold and upsell-gem literals into base-colors, since each was
repeated across both theme files. Drop the intermediate staticColors
binding in the guard test in favour of a second filter, and drop the
`?? {}` on theme.colors, which is a required field.

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-07-29 13:06:47 +02:00
Nicolò Pretto
307c7c9e54 Fix SDK test concurrency collision (#78898) 2026-07-29 12:33:40 +02:00
github-automation-metabase
29cefea9f5 Fix click-behavior multi-stage e2e flake: settle mapping list between selections (#78602)
* Fix click-behavior multi-stage e2e flake: settle mapping list between selections

The "navigating to questions with filters applied in every stage" test sets 11
click-target mappings sequentially, using index-based selectors (.first(),
.last(), .eq(2)) against the unset-click-mappings list. That list re-renders as
each mapping moves from the unset to the set section, but there was no barrier
between steps, so an index-based lookup could resolve mid-re-render and land on
the wrong target — yielding a stable-but-wrong filter set (a duplicated
"Product → Title" filter replacing the dropped "Reviews - Product → Reviewer"
one, which is exactly the assertion that fails).

Extract source selection into selectClickMappingSource(), which asserts the
source picker popover has closed before returning, so every subsequent
index-based lookup runs against a settled list.

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

* Fix click-mapping settle barrier to assert popover closed correctly

The previous settle barrier used `H.popover().should("not.exist")`, but the
`H.popover()` helper internally chains `.filter(":visible").should("be.visible")`,
so the assertion is self-contradictory: it requires a visible popover to exist
and then asserts it does not, failing deterministically the moment the source
selection closes the popover.

Assert the popover is gone with `cy.get(H.POPOVER_ELEMENT).should("not.exist")`
— the idiomatic pattern used across the e2e suite — so the mapping list still
settles between index-based selections without the contradictory visibility check.

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

* Scope click-mapping settle barrier to the visible popover

The unset-click-mappings list re-renders as each mapping moves unset->set.
A bare `cy.get(POPOVER_ELEMENT).should("not.exist")` can never settle in the
click-behavior sidebar: it keeps ~60 mantine popover/combobox dropdowns
mounted-but-hidden, so they are continuously found in the DOM. Filter to
`:visible` so the barrier only waits for the open source picker to close.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 12:11:38 +02:00
Nicolò Pretto
d140a6f451 Drop conventional commit PR title reminder (#78888) 2026-07-29 11:34:24 +02:00
Timofey Kachalov
b909e55cdb Split guest-embed.cy.spec.ts into describe blocks for OSS and EE (#78830) 2026-07-29 13:34:21 +04:00
Nicola Mometto
5005f99c38 avoid cascade list of errors reported in emails (#78674)
* don't cascade all errors on a failed job run

* test
2026-07-29 10:10:38 +01:00
Nicola Mometto
2979a1f2e6 fix checkpoint field id serdes (#78666)
* reg test

* fix: route through export/import-field-fk

* fix: add field serdes dependency

* fix
2026-07-29 10:10:25 +01:00
Timofey Kachalov
692f047b92 Move Data Apps under data-apps-preview token feature (#78800) 2026-07-29 13:05:03 +04:00
Sébastien
d8f7ae6a91 Add CLI usage analytics (#78216) 2026-07-29 08:47:33 +02:00
Romeo Van Snick
bfbedff78a Stop passing query objects in navigation descriptors (#78796)
* Export queryToSearch and searchToQuery from metabase/router

* Serialize the query with queryToSearch at the navigation call sites

* Read search, not query, off a navigation descriptor

* Drop query from LocationDescriptorObject
2026-07-29 15:41:55 +10:00
Romeo Van Snick
24354fd2f4 Warn on PRs that grow the main app's initial bundle (#78652)
* Warn on PRs that grow the main app's initial bundle

* Fix actionlint findings in the touched workflows
2026-07-29 12:20:41 +10:00
Romeo Van Snick
3488d15252 Retire the state.routing slice (#78189)
* Thread QB popState off the routing slice

* Thread dashboard edit-mode off the routing slice

* Thread Metabot profile selection off the routing slice

* Thread document Metabot context off the routing slice

* Migrate component location readers to useLocation

* Retire the state.routing slice
2026-07-29 12:12:24 +10:00
Andrei Chernykh
17782894af Convert getExpandedCollectionsById to TypeScript and fix the stale ExpandedCollection type (#77538)
* Convert getExpandedCollectionsById to TypeScript and fix the stale ExpandedCollection type

* Type fake collection ids as literals instead of casting to CollectionId, dropping the String() workaround
2026-07-28 17:20:15 -07:00
Alexander Polyankin
e312b48757 Audit log statements (#78841)
* Audit log statements: no stack traces, no queries/user input, no pretty-printing

- Replace throwable args to log macros with (ex-message e) so ex-data and
  stack traces never reach the logs (~450 sites)
- Remove SQL/MBQL queries, prepared-statement params, HTTP request/response
  bodies, PII, and user-authored names from log output (IDs kept)
- Replace u/pprint-to-str in log calls with pr-str, or drop the value
- Silence sqlglot parser warnings in the GraalPy bootstrap (they quote raw
  chunks of customer SQL to stderr)
- Rewrite leftover functional colors (hsl/hsla/rgb/rgba) to hex in static-viz
  SVG output so Batik never errors (and stack-dumps) on them

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

* Use logf variants for exception messages; restore serdes --full-stacktrace

- Convert print-style `(log/warn "msg:" (ex-message e))` calls to
  `(log/warnf "msg: %s" (ex-message e))`
- Serdes import/export logs the full stack trace again when the
  --full-stacktrace CLI option / full_stacktrace API param is passed

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

* Update swap-clauses tests for clause-free warning messages

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

* Update log-content test assertions for the new logging policy

Log calls no longer attach throwables (entries have :e nil) and append
ex-message text to the message instead; body/secret leak tests now assert
the payload never appears in log output.

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

* Address review comments

- rethrow-api-error!: update docstrings — response body is never logged,
  it travels on the thrown ex-data
- expand-macros: log the segment filter-clause count once instead of an
  identical trace line per clause
- settings cache: log the whole SQLException message chain (the old
  print-sql-exception-chain walked .getNextException; ex-message alone
  dropped the chained causes)

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 19:32:29 -04:00
Fraser
d6b078baa5 Remove settings slice (#75983) 2026-07-29 10:25:25 +12:00
Uladzimir Havenchyk
a1909a240b chore: convert column settings to typescript (#70302) 2026-07-28 22:23:10 +00:00
Andrei Chernykh
8699c85e9b chore: Convert the last audit_app JS files to TypeScript (#78745)
* Convert the last audit_app JS files to TypeScript

* Address review comments
2026-07-28 15:17:08 -07:00
Maksym Yakubych
e4705cad9b Move flaky usage-auditing e2e coverage into unit tests (BOT-1909) (#78838) 2026-07-28 19:02:10 -03:00
appleby
2a503cffab Metabot Enable Opus 5 (#78695)
Add Claude Opus 5 to the anthropic openrouter and bedrock whitelists

- anthropic   claude-opus-5
- openrouter  anthropic/claude-opus-5
- bedrock     anthropic.claude-opus-5
2026-07-28 14:28:43 -07:00
da
5854e3e83f Edits on docs about how to run python transforms in self hosted deployments (#78098)
Add docs about how to run python transforms in self hosted deployments
2026-07-28 16:51:09 -04:00
Michael Eby
44e66e883b docs: add invite to view a dashboard or question (#78167)
* docs: add invite to view a dashboard or question

* Edits from Jeff
2026-07-28 16:50:57 -04:00
Egor Grushin
0374e1244c GDGT-2768 GDGT-2769 remove deprecated TableColumnFingerprintInfo and TableColumnInfo (#78519) 2026-07-28 21:24:19 +01:00
Alexander Polyankin
ae68432f30 Health check per engine (#78813) 2026-07-28 13:27:52 -04:00
Andrei Chernykh
0a9e61f44f Scope the invite group picker to the groups with access to the shared item (#77898)
* Scope the invite group picker to the groups with access to the shared item

* Imply All Users membership only for invites with a target, and mock the scoped groups endpoint in share menu specs

* Show all groups when inviting to view and mark which ones can access the item

* Make the invite picker's group section labels visible and tidy up UserForm's membership mapping

* Generalize the group picker into ordered labeled sections with group a11y roles, keeping invite-target semantics in UserForm

* Use the compact Alert size for the invite picker's no-access warning
2026-07-28 09:59:57 -07:00
Alexander Polyankin
7c58c0539f Perms - t2/reducible-query + per-row realize + fold (#78811) 2026-07-28 12:27:35 -04:00
technomancy
3133454b99 Suppress api-key-missing exception when failing to generate sample question (#78733)
Suppress api-key-missing exception when failing to generate sample question.

Not having an API key is the default situation and should not result
in an error being logged.
2026-07-28 18:13:49 +02:00
Ben Grabow
b6083e9e20 Temp pin mysql latest (#78812)
* Add MySQL 9.0 app db test suite

* Add MySQL 9.0 app db test suite, disable latest
2026-07-28 18:13:31 +02:00
Stas Gavrylov
f3f72896b7 GDGT-2910: Rename Model caching log to Model persistence log (#78787)
* GDGT-2910: Rename Model caching log to Model persistence log

* GDGT-2910: Renamed route and components to ModelPersistence*
2026-07-28 19:08:39 +03:00
github-automation-metabase
89a870bbae Auto-update documentation for master (#78731)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-07-28 12:05:25 -04:00
Timofey Kachalov
f923c031c2 Enhance DevToolBar (#78539) 2026-07-28 19:48:50 +04:00
Chris Truter
836d5568ac Support library-retrieval using pgvector on the appdb (#78682)
* Let library retrieval use pgvector on the app db

The tables were bare-named, so the drop-and-rebuild in ensure-tables! would have
resolved against the application database's default schema. That is what kept
the feature dedicated-only.

Table names now come from the pgvector mode, the way semantic search picks
between its two index-metadata configs: bare for a dedicated store, so no
existing index moves, and qualified by a library_retrieval schema when sharing
the app db. Its own schema, not semantic search's, whose migration drops every
table in it.

Raw SQL interpolating a qualified name into "%s" would read it as one
identifier, so those sites go through quote-table.

The sync task's scheduling gate stays boot-safe: a dedicated URL, or the license
plus a Postgres app db, decided without resolving the mode. available? checks
the license before the store for the same reason.

* Cover the app-db round trip end to end

Opt-in like the semantic-search one, and picked up by the same appdb-mode CI
job. Asserts the tables land in library_retrieval, the application schema is
untouched, retrieval returns the indexed entity, and a semantic-search wipe
leaves the library index alone -- the reason the schema is separate.

* Check our own schema before reporting the store ready

Semantic search's predicate answers for semantic_search. A role that can use
that schema but cannot create library_retrieval passed it, and then failed every
reconcile inside ensure-tables!. Probe for our schema too: present, or creatable
in a rolled-back transaction, cached because available? runs on every write
nudge and search.

* Gate the round trip on the capability the feature checks

Gating on semantic search's check let the harness pass where a role that can use
semantic_search but not create library_retrieval fails in production.

* Check schema privileges, not schema existence

A role with USAGE but not CREATE passed the existence check and then failed
creating tables. Ask Postgres for both privileges instead, and only fall back to
the rolled-back CREATE SCHEMA when the schema isn't there at all.

The whole probe is inside the try now: the existence query wasn't, so a
transient database failure could throw out of request-entity-sync!, which the
write hooks call fire-and-forget.

* Put the dedicated index in the schema too

A dedicated store kept the index in the default schema, which is exactly what
semantic search's migration wipes -- so enabling or migrating semantic search
dropped the library index and silently re-embedded the whole library. It fires
whenever the stored schema version trails the code, not just on first init.

Both modes now use the library_retrieval schema, so default-tables and
app-db-tables collapse into one. An index built before the schema existed is
moved into it with ALTER TABLE ... SET SCHEMA, keeping its rows; the move runs
before the IF NOT EXISTS creates, which would otherwise leave an empty table
beside it. Creating the schema now needs CREATE on the dedicated database, so a
failure says so.

* Drop only semantic search's own tables in a dedicated store

A dedicated store's default schema is shared -- library retrieval keeps its
index there until it moves -- so wiping every table in it could drop that index
before entity retrieval had a chance to adopt it. Both run as background jobs,
so nothing ordered them.

Scoped to the index_/dlq_/repair_ prefixes, and only in dedicated mode: the
module's own schema in app-db mode holds nothing else, so a reset there is still
free to clear leftovers. The app-db sentinel refusal still reads every table in
scope, since an app db is recognized by tables we would never drop.

* Match the names semantic search generates, not their prefixes

A cohabitant in a shared dedicated store is free to call something
index_history, which the prefix check would have dropped. Match the control
tables exactly, index tables through index-table-name? -- the same contract
orphan cleanup uses -- and DLQ and repair tables by their generated shapes.

Also covers the adoption path end to end: a legacy unqualified index with a row
in it, moved by ensure-tables! and still holding the row afterwards. Asserted
there rather than after the reconcile, which GCs documents the library no longer
implies.

* Stop the availability tests depending on the app db

available?-gates-test left the real schema probe live while stubbing only
the pgvector mode. The probe is Postgres-only SQL, so it threw and read as
unusable on the H2 and MySQL runs, and the case failed there.

The round trip's legacy table declares a vector(4) column before
ensure-tables! runs, and the support check verifies CREATE EXTENSION in a
transaction it rolls back, so nothing has installed the type by then.

* Answer review on the dedicated-store reset

Rename ours? to semantic-search-table?, and point the two docstring
references at namespaces the reader can resolve.

Cover the case where a legacy bare table and a qualified one both exist:
adoption has to leave them alone, since SET SCHEMA onto an occupied name
errors and would abort the whole ensure-tables! transaction.

* Latch the app-db schema check once it passes

The check catches everything and returns false, so on a plain TTL a single
transient app-db failure took retrieval offline for five minutes: available?
went false, search returned nothing and the write nudge no-oped.

A confirmed yes now sticks for the life of the process. Only a no keeps
re-checking, so a privilege granted while we run is still picked up.
2026-07-28 17:24:48 +02:00
lbrdnk
0a5aa515e6 Add eval-run triggers (#77960)
* Stub the evals trigger

* use ref arg

* dummy to trigger build

* Clean up

* Use appropriate token

* Simplify pr message

* Use .evals_ref for default ref

* Comment cleanup

* Adjust ref

* Use evals branch

* Use general automation token

* readme for .evals_branch

* Use main of evals for label trigger

* Revert "dummy to trigger build"

This reverts commit e6502c6ff076a47d94eb829b516fc45b37cd7680.

* whitespace

* Address review remarks

* Use ai benchmark naming so we can trigger from pr

* Address review comments

* Remove now redundant branch pinning

* Review remarks
2026-07-28 17:12:51 +02:00
Anderson
c48e0783ee Fix double vertical scrollbar in admin settings when the deprecated driver notice is visible (#78687)
Fix double vertical scrollbar in admin when the driver deprecation notice is shown
2026-07-28 11:38:06 -03:00
Anderson
7e2f3f6f67 Keep browse table tooltips anchored while hover actions disappear (#78622)
Fix browse card tooltip teleport
2026-07-28 11:37:55 -03:00
Nick Fitzpatrick
d42f057d2e add no cross db join message to Data Picker (#78673) 2026-07-28 11:33:43 -03:00
Chris Truter
426d1b617c Stop sending MariaDB-only timeout syntax to MySQL 26 (#78802) 2026-07-28 16:05:37 +02:00
Riley Thompson
e1c2730e9b Remove unneeded bigquery parent in plugin yaml (#78732)
remove unneeded bigquery parent in plugin yaml
2026-07-28 13:30:44 +00:00
Kevin Kelleher
6e9d52021c Fix broken duplication of inline dashboard filters (#78549)
* properly duplicate inline parameters

* add tests

* some comments for clarity
2026-07-28 13:00:42 +00:00
Nick Fitzpatrick
60eae45519 Slack PDFs - send using channels query param for DMs (#78263) 2026-07-28 08:10:40 -03:00
Chris Vandoodewaard
efb763bef8 use graphQL to collect issues and prs for a given milestone (#78496) 2026-07-28 07:09:44 -04:00
Chris Vandoodewaard
463870fb3c set backport pr to draft if there are conflicts (#78591) 2026-07-28 07:09:27 -04:00
Nicola Mometto
04407a3c35 support lookback windows for incremental transforms (#78212)
* feat: add lookback

* validate lookback

* feat: apply lookback

* tests

* fix

* fix test

* feat: only support lookback on temporal checkpoints

* update test

* FE

wip

* add :type/Time test

* add test with watermark row removed

* refactor to avoid double get-in

* only support date/datetime

* tests

* fix: support coerced cols

* add more tests

* refactor

* more tests

* test for coerced instant

* use utils in FE too

* chorec

* further validate

* use lib utils

* use strict comparison

* allow clearing lookback

* handle race
2026-07-28 09:58:56 +00:00
Chris Truter
d3528c7502 Stop the H2 app-db fixture from baking in your encryption key (#78696) 2026-07-28 17:32:24 +08:00
Jakub Chodorowicz
2c7b1c9608 cleanup init code (#78675) 2026-07-28 11:22:38 +02:00
Uladzimir Havenchyk
ee7bd843ce chore: migrate registerVisualization to typescript (#70306)
* chore: migrate registerVisualization to typescript

* chore: drop 'as unknown as Visualization' cast in custom viz plugin loading

applyDefaultVisualizationProps now takes the plain component and returns
the typed Visualization via Object.assign. The SDK's date_style format
option is widened to string | null to match the ColumnSettings value the
host actually passes to checkRenderable.
2026-07-28 10:41:37 +02:00
Nemanja Glumac
14681e7dcf Cover sql filter's error message behavior with the lower level tests (#78692)
* Unit test the native editor run button's unmapped-tag tooltip

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

* Unit test the Save button's unmapped-tag tooltip and disabled state

Covers both dimension and temporal-unit template tags.

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

* Cover temporal-unit tags in the canRun template tag validation test

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

* Drop the unmapped field filter e2e test in favour of unit coverage

Every assertion it made is now covered by NativeQueryEditorRunButton and
ViewTitleHeader unit tests.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:27:08 +12:00
Fraser
56eaca30d6 Remove dead modules where the folders no longer exist (#78754) 2026-07-28 16:15:12 +12:00
Ben Grabow
fc9441add0 Prevent serving overly stale query cache results (#78689)
* Prevent serving overly stale results

* Refresh cache results slightly before they expire
2026-07-27 22:27:28 -05:00
Romeo Van Snick
a3f7259763 Repoint the facade location hooks at react-router v7 and free Palette from route props (#78669)
* Read useLocation and useParams straight from react-router v7

* Re-export react-router's Outlet instead of shimming it through context

* Disable the command palette by path instead of route props

* Drop the now-unused route props plumbing
2026-07-28 12:57:54 +10:00
dpsutton
0ed95e05f6 Unshared appdb connections + detached audit cluster lock (fix first-boot stall) (#78276)
* Add mdb/with-unshared-connection: appdb connections exempt from ambient reuse

A connection holding fragile long-lived state -- a streaming result set
(postgres portal/cursor), or FOR UPDATE row locks -- must not be picked
up by ambient connection reuse: a borrower that inherits it via
*current-connectable* and commits destroys that state. This is the
failure mode behind the first-boot hang in #78238, the revert (#77048)
of appdb streaming (#76645), and "cursor/portal does not exist" errors
when field updates run mid-sync-stream.

Built on toucan2's new unshared-connection! (camsaul/toucan2#212): the
connection is used when passed explicitly -- (t2/reducible-query conn
...), (t2/with-transaction [t conn] ...) -- but never bound as
*current-connectable*, so toucan calls that resolve their connection
ambiently run (and commit) on other pooled connections.

mdb/with-unshared-connection is checkout -> mark -> body -> close; in
every other respect the connection is ordinary JDBC, configured by the
body (e.g. .setAutoCommit false to stream). The pg test reproduces the
exact #77048 failure on an unmarked control connection (portal killed by
a nested commit mid-reduction) and shows the unshared connection
streaming to completion.

deps.edn temporarily pins toucan2 to the PR sha via :git/sha; a
follow-up commit will switch back to :mvn/version once a toucan2
release includes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f

* Use released toucan2 1.0.572 for unshared-connection support

camsaul/toucan2#212 merged and released; drop the :git/sha pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f

* Hold the audit DB cluster lock on a detached connection (fixes boot stall)

Since #76551 the audit install/load/sync pipeline runs inside a cluster
lock whose SELECT FOR UPDATE ties lock lifetime to transaction lifetime:
the whole pipeline became one long transaction on the lock's connection.
The serdes load's ~3,600 per-entity nested transactions piled up as
savepoints and subtransaction XIDs, and every row lock was held until a
single commit at the end -- stalling first boot for 10-25+ minutes and
blocking concurrent work like config-file database provisioning (#78238).

with-cluster-lock now supports :detached? -- the lock rows are held on a
dedicated unshared connection (toucan2 unshared-connection!) while the
body runs on ordinary pooled connections, committing incrementally in
its own short transactions. Locks release at body completion (commit) or
throw (pool check-in rollback). Re-acquiring a lock this thread already
holds detached is a no-op in both detached and transactional form;
upgrading a detached :share hold to :exclusive throws instead of
self-deadlocking; :retry-transient? is rejected (a re-run would
double-apply incrementally-committed work).

The audit pipeline opts in: per-entity serdes isolation (#74412) is
restored, and a crash mid-pipeline is repaired by the next boot (the
checksum only advances on completion; reconcile runs every boot), which
is what makes incremental commit safe here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f

* Address review: confine detached retry to acquisition; repair dialect-sync window

Review findings (galdre), all majors addressed:

- Major 1: retry/with-retry wrapped the whole detached body, so a
  retryable-looking error thrown by the body (e.g. a nested cluster lock
  timing out inside the serdes load) re-ran an incrementally-committed
  body and then surfaced as "Failed to obtain cluster lock", which
  ensure-audit-db-installed! swallows as benign contention. An acquired?
  volatile now confines both the retry predicate and the acquisition-
  failure wrapper to the pre-acquisition phase in detached mode; body
  errors propagate raw with intact cause chains.
- Major 2: a failed or killed engine-changed dialect sync was never
  repaired once the content checksum had advanced. New durable setting
  audit-db-dialect-sync-pending: set before the sync attempt, cleared on
  success, a third sync trigger on every boot.
- Major 3: the mysql/h2 engine-flip visibility window under detached
  mode is documented as a KNOWN WINDOW at adjust-audit-db-to-source!
  (restores pre-#76551 semantics; overlay-based resolution is follow-up).
- Majors 4-6: reviewer's contract tests adopted (throw-path lock
  release, check-in rollback, behavioral pin on :detached?, crash-mid-
  load self-heal, plus the two Major-1 reproductions); public docstrings
  updated to describe all three lock modes and the upgrade throw.

Minors: unified cross-mode reentrancy registry with eager share->
exclusive upgrade validation (fail-fast in any detached interleaving;
legal same-transaction upgrade preserved); h2 read->write upgrade throws
instead of hanging; lock-row INSERT gets a query timeout (bounds the
N-node cold-start pile-up); install-database! and each reconcile group
are single transactions; unshared checkout goes through *application-db*
so the restore-time read-lock gate applies; registry conveyance and
explicit-tx-on-unshared-connection caveats documented; dual kondo
lint-as entries; test hygiene (finally cleanup, nil ambient contract).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f

* Right-size the engine-flip visibility comment

The committed, briefly-visible engine flip is this pipeline's
longstanding behavior; only #76551's single-transaction era hid it for
one release. A neutral note fits better than a warning block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f

* Tighten comments to constraints; drop review-context narration

Comments now state what the code can't show (crash windows, why raw
JDBC, why the marker precedes the sync) and drop justification written
for reviewers: issue-number history, caller name-drops, repeated
tellings of the retry-confinement story, and cost reassurances.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f

* Make the detached lock a bespoke entry point instead of a with-cluster-lock mode

Reviewer pressure on the :detached? option had generalized
with-cluster-lock into a two-mode API whose cross-mode interleavings all
needed guarding: a held-locks registry, mode-upgrade validation, an h2
upgrade guard, and an acquired? volatile threading retry confinement
through the shared machinery — all protecting call patterns nothing in
the tree performs.

with-detached-cluster-lock is now its own function: exclusive-only,
single lock, documented non-reentrant. Structure replaces machinery —
the body sits outside the acquisition retry, so body errors are never
retried nor rewrapped as acquisition failure (no volatile), and
same-name re-entry throws via a two-line dynamic-set guard instead of a
registry (nested *different* locks, which the audit body does take,
work unchanged). with-cluster-lock returns to its previous shape; the
lock-row INSERT keeps its new query timeout, which bounds the
multi-node cold-start pile-up in both modes. Error shape is unchanged,
so audit-lock-contention? still classifies correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7FfdpzW5pr7CX9aNwa98f

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 16:54:55 -07:00
Alexander Polyankin
84a9434f08 Compact in-memory format for the per-request data-permissions cache (#78723)
* Compact in-memory format for the per-request data-permissions cache

The request-scoped cache in metabase.permissions.models.data-permissions
previously stored raw data_permissions rows as full Toucan instances, keyed
{user-id {perm-type {db-id [row ...]}}}. On instances with thousands of
databases and users in many groups a single prime-db-cache call could
materialize over a million instances (hundreds of MB retained per request,
plus the matching GC churn).

Store a compact entry per (perm-type, db-id) instead:

    {:groups {group-id perm-value}                           ; db-level rows
     :tables {table-id [schema-name {group-id perm-value}]}} ; table-level rows

and intern the group->value maps at load time: on big instances they are
typically identical across most databases, so the retained size becomes
proportional to the number of distinct permission profiles instead of
groups * databases.

Also fetch only the columns the checks actually consume (group_id, perm_type,
db_id, table_id, schema_name, perm_value) instead of p.*, and fix an O(n^2)
already-cached filter in prime-db-cache.

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

* small fix

* Use metabase.util.performance helpers in the cache hot path

perf/concat + perf/mapv in entry-group-value-pairs. Deliberately NOT
perf/update-vals for the interning pass: it skips replacing values that
are = to the old ones, which would undo the interning.

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

* Drop perf helpers from entry-group-value-pairs

perf/concat over an already-built vector is just (into a b), and reduce-kv
iterates the map faster than mapv over entry seqs. Plain into + reduce-kv,
one less require.

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

* Address code-review findings on the compact permissions cache

- Duplicate data_permissions rows (no unique constraint on
  group/type/db/table) no longer silently last-win: cache slots promote to
  a set when duplicate rows disagree, so every value reaches the consumers'
  coalescing logic exactly as raw rows did. Regression test added.
- schema_name is stored per table-level row ({:schema .. :value ..})
  instead of first-row-wins per table, since it is denormalized and rows
  for one table can disagree.
- The interner now lives in the *permissions-for-user* atom, so value
  sharing spans every prime in the request (db-at-a-time priming
  previously got a fresh pool per call), and it reuses
  metabase.util.memoize/fast-interner instead of a bespoke pool.
- rows->cache-entry returns nil for empty rows so cached and non-cached
  get-permissions branches agree; the non-cached branch skips interning.
- schema-permission-for-user and most-permissive-database-permission-for-user
  now go through entry-group-value-pairs instead of hand-rolled traversals.
- Dropped the impossible-nil guards in prime-db-cache.

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

* fix

* Address CodeRabbit nitpicks

- gid->slot -> group-id->slot (no abbreviations)
- native-download-permission-for-user reuses most-restrictive-per-group
  instead of re-implementing the grouping inline

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

* Rename local intern -> interner (Eastwood local-shadows-var)

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

* Explain the slot rationale in conj-slot/slot-seq docstrings

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

* simplify slots

* Document interning with a worked example in the cache docstrings

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014V8QbHjoEkJ8eZLG13sYUe

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: dan sutton <dan@dpsutton.com>
2026-07-27 19:29:20 -04:00
Sloan Sparger
b2651a0af3 [Metabot] fix conversation scroll calc issue (#78642) 2026-07-27 17:30:29 -05:00
Tyler Plude
3c9fd3afe9 [BOT-1901] Fix missing type info on metric & measure refs (#78520)
* annotate metric and measure ref types

* fix tests

* pr comments
2026-07-27 15:09:46 -07:00
Alexander Polyankin
1ec45cf46f Remove the workspaces feature (#78617) 2026-07-27 17:27:57 -04:00
Chris Vandoodewaard
43497b47bd disable cross version tests temporarily (#78724)
* disable workflow

* add a helpful note
2026-07-27 16:33:22 -04:00
Andrei Chernykh
f275c0735d Restyle the sharing popover with Copy link and Embed buttons above the actions list (#78584)
* Restyle the sharing popover with Copy link and Embed buttons above the actions list

* Reserve the copy button width with hidden pseudo-elements instead of inline styles
2026-07-27 13:12:22 -07:00
Chris Vandoodewaard
cfcf9fc18b don't tag author on Slack since Github doesn't populate author login info (#78569)
call Github if login unset, also fix lookup function
2026-07-27 15:46:46 -04:00
Timur Mashara
f07aac4b88 New storage upsell 2 (#78162)
* Revert "🤖 backported "Revert 'GRO-554 update storage upsell (#76052)'" (#78041)"

This reverts commit b067da1824.

* introduce gsheets and csv panel states / new hooks for attached dwh

* update storage setup error view + conditions to show / setup timeout

* remove skipPollingIfUnfocused, increase polling timeout
2026-07-27 19:16:38 +00:00
Maz Ameli
694e3466a6 Swap placement of dashboard Subscriptions action with Auto-refresh action (#78063)
* swap subscriptions action  with auto-refresh action

* hide Subscriptions for non-admins if slack and email aren't set up

* small adjustments

* e2e adjustments

* more e2e adjustments

---------

Co-authored-by: Nick Fitzpatrick <nickfitz.582@gmail.com>
2026-07-27 16:15:23 -03:00
Timothy S. Dean
1ce2f7a546 Bump netty (#78641) 2026-07-27 13:01:34 -06:00
technomancy
660ea06251 MBQL5ize snowflake and bigquery drivers. (#76769)
* MBQL5ize snowflake and bigquery drivers.

* bigquery fixes

* fix test-bigquery-log

---------

Co-authored-by: Riley Thompson <rileythompson99@gmail.com>
Co-authored-by: Riley Thompson <riley@metabase.com>
2026-07-27 14:47:33 -04:00
Chris Truter
a317fbf1b8 Fix legacy semantic search startup validation (#78700) 2026-07-27 18:15:46 +00:00
metamben
00637eef75 Make the experiment setting available (#78698)
* Make the experiment setting available
* Enable analytics on the JVM
2026-07-27 18:08:36 +00:00
Andrei Chernykh
166175677e Convert the legacy chart legend components to TypeScript function components (#78258)
* Convert the legacy chart legend components to TypeScript function components

* Share the legend prop types and extract a typed getChartLayout helper with tests
2026-07-27 10:00:19 -07:00
Eric Normand
eeb40ed109 Don't drop entities whose parameter values-source Card was deleted (#78611)
Deleting a Database hard-deletes its Cards with raw SQL, but nothing
rewrites the `parameters` JSON on Cards/Dashboards that used one of
those Cards as a dropdown source. The parameter keeps a
`values_source_config.card_id` pointing at a row that is gone.

On export that made `*export-fk*` throw `:target-not-found`, which
`log-and-extract-one` swallows as a skip — silently dropping the whole
Card or Dashboard from the archive.

Export each parameter on its own now, and when one carries a card
values-source that can no longer be resolved, emit it without
`:values_source_type`/`:values_source_config`. That is the same shape
the app writes when a source Card is archived, so the parameter comes
back as a plain dropdown over its connected fields.

A source Card that merely isn't part of the export is unaffected —
`*export-fk*` only throws when the row is absent, so those still fail
dependency validation as before.

Fixes GHY-3259
2026-07-27 11:57:04 -05:00
Chris Truter
187f4df352 Give the raw core_user insert an entity_id (#78691)
The helper inserts straight into the table to bypass the model's transforms,
which bypasses the hook that fills in entity_id too. #78452 made that column NOT
NULL, so every App DB Tests job has failed on it since.
2026-07-27 12:13:27 -04:00
Riley Thompson
7a73a7a68a Move sqlserver and oracle to mbql5 (#77414)
* sqlserver mbql5

* oracle mbql5

* fix sqlserver tests

* fix test failures

* fix sqlserver test errors

* clean up

* kondo, test fix

* small changes
2026-07-27 11:13:00 -04:00
John Swanson
2c82f4332c Eagerly load tenant auth-provider; fail tenant SSO logins loudly (UXW-4898) (#78492)
* Eagerly load tenant auth-provider; fail tenant SSO logins loudly (UXW-4898)

Root cause: metabase-enterprise.tenants.auth-provider — the login! method
that validates a login's tenant claim and stamps :tenant_id onto the user
data — was never eagerly loaded. The JWT/SAML providers only `derive` from
its dispatch keyword, which does not load the namespace, so on a freshly
started instance the tenant step was silently absent from the login!
method chain until unrelated traffic (e.g. /api/user/current hydrating
user attributes) lazily loaded it. A JWT provisioning login in that window
created the user with tenant_id = nil even though the token carried a
valid @tenant, the tenant-group assignment failed and was swallowed, and
the login returned 200 — permanently wedging the account (every later
login 403s with "Cannot add tenant claim to internal user").

Fix, plus defense in depth so no future tenant-drop can wedge an account:

- Add metabase-enterprise.tenants.init requiring the auth-provider, wired
  into metabase-enterprise.core.init, so the tenant login step is always
  registered at startup.
- create-user! throws instead of silently dropping :tenant_id when the
  sso-user-fields field list would strip a tenant assignment stamped by
  the upstream tenant flow.
- sync-group-memberships*! no longer swallows real tenant/group mismatch
  errors (bad pairs whose :group-is-tenant? is non-nil); the login fails
  loudly instead. Nonexistent groups keep log-and-continue. This also
  covers EMB-2118 (JWT omitting @tenant with a mapped tenant group).
- The login! provisioning chain (tenant creation, user create/update,
  group sync) runs in one transaction, so a failed login leaves no
  half-provisioned user row behind.

* Fix cljfmt formatting in jwt_test.clj
2026-07-27 05:50:16 -07:00
Romeo Van Snick
54a2ef930b Migrate data studio pages off withRouteProps (#78423) 2026-07-27 21:58:10 +10:00
Chris Truter
4e307d23bd Raise CI timeout for driver tests hitting their cap under load (#78537) 2026-07-27 13:52:38 +02:00
Romeo Van Snick
ac2f1ead36 Adopt v7 navigation semantics, drop the v3 parity guards (#78653)
* Stop the location-syncing call sites from replacing the url they just read

* Cover the documents /new to /new warning with a unit test

* Adopt v7 navigation semantics, drop the v3 parity guards
2026-07-27 20:41:25 +10:00
Romeo Van Snick
357e2739bd Stop the router from importing metabase/redux (#78527)
The v7 provider mirrored every location into state.routing by calling
useDispatch itself, which is what kept metabase/router bound to the store.

createLocationMirror takes a plain dispatch function instead, so the router
publishes locations without knowing redux exists. app.js and the test harness
supply it as RouterProvider's onLocationChange prop. V7ReduxBridge is renamed
V7RouterBridge now that nothing in it is redux-specific.

metabase/router now passes a basic-tier boundaries trial with zero violations.
2026-07-27 20:07:40 +10:00
Sébastien
0e9c0eab62 Fix SDK Storybook chunk loading broken after #75985 (#78664) 2026-07-27 10:58:22 +02:00
Egor Grushin
9f9ee277cc style(sdk-bundle): addressing SDK error comments (#78548)
* addressing SDK error comments

* wrap SdkError stories with PublicComponentWrapper
2026-07-27 09:51:43 +01:00
Kamil Mielnik
9896c4aee5 GDGT-2564 Webhook destination cannot be saved from the UI (#78334) 2026-07-27 13:49:23 +07:00
Timofey Kachalov
76d6c139d4 Add Vite Data App Dev App e2e tests (#78454) 2026-07-27 09:09:00 +04:00
Romeo Van Snick
03b8f5cefc Move route guards out of metabase/router (#78526)
Guards are authorization policy, not routing primitives. They were the largest
source of shared-tier imports inside metabase/router (redux, plugins, and four
selector modules), blocking the module from dropping to a lower tier.

The generic factory and the app-wide auth/setup guards move to a new shared
metabase/route-guards module. The monitor and data-studio guards move into their
own features, which lets getMonitorRoutes and getDataStudioRoutes stop taking
them as parameters.

Router violations under a basic-tier trial go from 13 to 3; the rest are the
redux location mirror in the v7 provider.
2026-07-27 10:36:48 +10:00
Romeo Van Snick
158902de0f Migrate core route pages off withRouteProps (#78427) 2026-07-27 10:15:57 +10:00
Romeo Van Snick
5a4ca22140 Migrate admin pages off withRouteProps (#78425) 2026-07-27 08:20:32 +10:00
Ben Grabow
93f9a2fa2c Reset undecryptable rows to an empty value instead of aborting the entire decryption process (#78383) 2026-07-24 22:03:46 +00:00
github-automation-metabase
eac0d3fcc3 Auto-update documentation for master (#78631)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-07-24 21:31:55 +00:00
Nathan Voxland
39c1100a2f Improve queue handling when there is no quartz available (#78145) 2026-07-24 16:27:15 -05:00
Nemanja Glumac
a77b7d2ed8 Simplify settings error e2e tests (#78614)
* Add GeoJSON error mocks and cover map load failures in unit tests

`setupGeoJSONEndpoint` could only produce a valid FeatureCollection, so the
error branches of the custom map flow had no unit coverage.

- `setupMalformedGeoJSONEndpoint` returns a 200 with a payload the client-side
  validator in `metabase/api/geojson` rejects, since those checks run in
  `transformResponse` and need the payload to reach the client.
- `setupGeoJSONEndpointWithError` returns a non-2xx with a bare string body,
  matching what `api-exception-response` sends for an `ex-info` carrying only
  a status code.
- `createMockProjectedGeoJSONFeatureCollection` supplies projected coordinates
  so every bounds corner falls outside lat/lng range.

Covers the invalid-location, missing-features, and out-of-bounds errors in
CustomGeoJSONWidget.

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

* Drop map GeoJSON error e2e tests in favour of unit coverage

All three asserted error strings are now covered more cheaply. The two
backend-owned messages are pinned by metabase.geojson.api-test against mocked
HTTP, and the modal rendering them is covered by CustomGeoJSONWidget. The two
client-side validation messages come from transformResponse in
metabase/api/geojson, which the e2e tests reached with the backend fully
stubbed or bypassed.

Two of them also drove real outbound requests from CI — one to www.metabase.com
and one to a file on the public repo's master branch.

projected.geojson had no other consumer; the equivalent payload is now built by
createMockProjectedGeoJSONFeatureCollection.

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

* Drop admin settings e2e tests that duplicate existing unit coverage

Each of these asserts the same condition as a unit test that already exists,
not merely an adjacent one:

- the HTTPS redirect check failure message is asserted in HttpsOnlyWidget
  ("should display an error message if the site is not accessible via https")
- the billing info fetch failure is asserted in LicenseAndBillingSettings
  ("renders error for billing info for store managed billing and info request
  fails")
- the absent license input for hosted instances is asserted in
  LicenseAndBillingSettings ("does not render an input when token has hosting
  feature enabled")

mockBillingTokenFeatures and its feature-key constants had no remaining
callers.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 22:50:45 +02:00
Nemanja Glumac
107df96fbd Harden the glob in cross-version test scripts (#77182) 2026-07-24 22:15:20 +02:00
Jeff Bruemmer
0e9bec40f0 docs - transform updates (#78376) 2026-07-24 19:48:44 +00:00
Nick Fitzpatrick
d6285436bf Uxw 4246 bulk actions in the library (#76558)
* Bulk Actions in Library

* adjust e2e

* pr feedback

* final CI adjustments
2026-07-24 16:20:25 -03:00
Chris Truter
0d2b723687 Recover invalid semantic search HNSW indexes (#78428) 2026-07-24 20:45:17 +02:00
technomancy
25b687f674 Make snowflake-with-dbname-in-details-gets-synced-test tolerant of failed cleanup (#78578)
Make snowflake-with-dbname-in-details-gets-synced-test tolerant of failed cleanup.
2026-07-24 18:30:23 +00:00
appleby
1e6ea2b1f4 Remove :doc false from user configurable llm defsettings (#78586)
The following `:visibility :internal` settings remain `:doc false`
- ai-service-base-url
- llm-proxy-base-url
- llm-anthropic-api-version
2026-07-24 18:25:40 +00:00
Eric Normand
65f07f2d11 Persist disabling a collection from remote sync (GHY-4189) (#78493)
* Persist disabling a collection from remote sync (GHY-4189)

Disabling a collection deleted its RemoteSyncObject rows outright, so the
removal left no dirty state to push and the collection's files stayed on the
remote. A later pull then re-imported the collection (its YAML still carried
is_remote_synced: true), re-enabling it.

Mark the collection's and its contents' RemoteSyncObject rows as "removed"
instead — a pending deletion the next export pushes to the remote — while
dropping rows still in "create" (never pushed, so nothing to delete remotely).

* Don't delete contents when a collection is re-synced before the push

Disabling a collection records a pending removal on its contents'
RemoteSyncObject rows. Re-enabling it re-tracked only the collection's own row,
so the contents stayed marked for deletion and the next export removed their
files from the remote.

Clear the recorded removal when a collection is re-synced before that removal
was pushed. Restore to "update" rather than "synced": edits made while the
collection was un-synced are not tracked, so the entity must be re-serialized
for the remote to be guaranteed to match local.

* Stop core-test leaking RemoteSyncObject rows between tests

Disabling a collection now marks its RemoteSyncObject rows rather than deleting
them, so rows seeded by a test outlive it and leak dirty state into every later
test. The old delete-on-disable behavior had been doing this cleanup by
accident.

Reset the table around each test with the clean-object fixture.

* Assert the pull succeeds in the disable round-trip test

The test discarded impl/import!'s result, so an errored pull would leave the
collection un-synced for the wrong reason and the following assertion would
pass vacuously.

* Re-check eligibility at write time so a pending removal isn't resurrected

An event handler computes eligibility from its event payload, then writes the
RemoteSyncObject row much later. A disable committing in between left the
handler writing a tracked status over the "removed" status the disable had
recorded, so the entity stayed on the remote after being un-synced.

Re-check eligibility against current DB state before letting a tracked status
overwrite a pending removal, and let the removal stand when it no longer holds.

* Row-lock the sync entry so the un-sync race has no remaining window

Re-checking eligibility at write time narrowed the race but did not close it: a
disable could still commit between the re-check and the update, and the losing
write would take effect.

Row-lock the entry for the transaction so a tracking write and a concurrent
un-sync settle in a fixed order instead of losing one of the two: whichever
locks first commits, and the other observes that result — the un-sync re-marking
the row, or the eligibility re-check seeing the entity leave the synced set.

The race test drives the disable on its own thread and unparks the handler
without waiting for it, since the parked handler holds the row lock the disable
needs. Its assertion holds for either order, so the choreography only biases
which one is exercised.

* Address review: honest race coverage, insert-path guard, tighter asserts

- Split the concurrency test into two, each parking the handler at the point
  that exercises one ordering: disable-first (guarded by the write-time
  eligibility re-check) and handler-first (guarded by the row lock, which the
  disable now provably blocks on). The prior single test parked on the plain
  existing-entry read, so it only ever exercised the re-check ordering while its
  comment claimed lock contention.
- Re-check eligibility in the insert branch too: a stale tracked-status event
  that finds no row inserts a fresh one, a phantom the row lock cannot cover.
  This narrows, but cannot fully close, that window.
- Round-trip pull test asserts = :success rather than not= :error, so a
  :conflict result can no longer slip through.
- restore-removed-rsos! docstring no longer claims it only clears removals from
  record-removed-rsos!; it clears any 'removed' row in the subtree.

* Re-track never-pushed contents when a collection is re-synced

Disabling a collection drops its contents' never-pushed 'create' RemoteSyncObject
rows (the remote never had them, so nothing to delete there). Re-enabling only
restored the rows that were marked 'removed', so a card added-but-never-pushed
before the disable was left with no row — and the next incremental export, seeing
no dirty row for it, silently omitted it.

On re-enable, insert a 'create' row for every eligible content entity in the
re-enabled subtree that has none, so the next export pushes it. The
RemoteSyncObject table only records pending changes, so already-synced content is
legitimately untracked; re-marking it 'create' re-serializes it harmlessly (a
'create' onto its own path is a no-op).
2026-07-24 12:58:10 -05:00
Ben Grabow
1c2e735ab0 GHY-4103 Repair stale template tags on serdes import (#77897) 2026-07-24 11:15:35 -05:00
Mahatthana (Kelvin) Nomsawadi
bb6977debd feat: simplify SDK release process and checklist maintenance (#77537)
* scaffold: two-step SDK release process (EMB-1518)

Replaces the hardcoded version/dist-tag computation in
release-embedding-sdk.yml with a new sdkRelease metadata block in
package.template.json (distTag, tagAsLatest), plus a new
sdk-version-bump-pr.yml workflow that bumps the version via `npm version`
and opens a PR labeled sdk-release-trigger for review. Merging that PR
triggers the release workflow directly.

Unverified: scaffolded per EMB-1518 ahead of Nicolo/Roman's review, since
this can only really be exercised by triggering a real SDK release. See
.claude/kelvin/2026-07-09-emb-1518-simplify-sdk-release-process-and-checklist-maintenance/
for the plan and open questions (particularly around distTag semantics for
stable/patch/minor releases).

* fix: correct distTag comment, <major>-stable is real (EMB-1518)

npm dist-tag ls @metabase/embedding-sdk-react confirms 51-stable
through 62-stable are already live, actively-used tags - not a new
concept as the prior comment assumed. Resolves the one open question
this scaffold couldn't settle from the proposal text alone.

* fix: address review cycle 1 findings (EMB-1518)

Fixes release-breaking and correctness issues found in review cycle 1
of the SDK release process scaffold:

- beta release_type now defaults to <major>-beta dist-tag instead of
  the literal (non-existent) "beta" tag
- validate branch/release_type combinations that the old
  getNextSdkVersion used to hard-error on (master requires alpha,
  release branches reject alpha)
- pull_request trigger on release-embedding-sdk.yml now scoped to
  package.template.json via a paths filter, and the concurrency group
  no longer cancels an in-flight release on unrelated PR closures
- readme-PR follow-up steps now skip cleanly when the readme was
  already up to date, instead of failing and firing a false alert
- tagAsLatest now always resets to false rather than carrying forward
  a prior true, and the actual value is shown in the summary/PR body
- version-bump branch name includes the run id to avoid collisions on
  retry
- renamed a step whose name didn't match what it does

See the "Review cycle 1 (fable)" section and the new "Fixes applied
for review cycle 1" subsection in the plan file for full detail on
each finding and its disposition.

* fix: address review cycle 2 findings + simplify version bump (EMB-1518)

Cycle-2 findings: add a preminor release_type so master's next-major
alpha bump no longer needs a hand-made PR (#9); make the Slack
failure notification independent of an artifact upload that may
never have run (#10); guard against two open version-bump PRs racing
each other (#11); stop interpolating the free-text branch input
directly into shell scripts (#12).

Also replaces the npm-version rename dance (rename to package.json,
bump, rename back, plus a .bak copy) with direct calls to the semver
package the bump logic already wraps, writing the result via jq
instead - no functional change, removes the dead .bak file (#13).

* fix: address review cycle 3 findings + rename resolve-release job (EMB-1518)

Fixes findings #14-#16 from the third fable review cycle:
- #14: the failure Slack alert's messageId interpolation was unquoted,
  rendering a JS syntax error whenever send-slack-notification-start was
  skipped (the exact scenario cycle-2's fix #10 was meant to cover). Now
  quoted, and falls back to sendSlackMessage (new top-level message)
  instead of sendSlackReply when there's no thread to reply to.
- #15: finished the cycle-2 injection-hardening pass by routing the two
  remaining inline branch-value usages (workflow-summary's heredoc,
  publish-npm's gh pr create --base) through env: instead of ${{ }}.
- #16: added a semver validity pre-check before the bump step, so an old
  release branch's 0.N.x placeholder version fails with a clear ::error::
  instead of a silent exit 1 from the semver CLI.

#17 (minor's version-collision caveat on old release branches) is left as
an open question for Nicolo/Roman rather than removed - documented with a
comment and Judgment call #6. #18 (merge-commit-sha race) is deferred -
it fails loudly rather than silently, lower priority than the others.

Also renames the resolve-release job to prepare-release throughout
release-embedding-sdk.yml, per explicit request (not a review finding).

See the plan file's "Fixes applied for review cycle 3" section for the
full verification detail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2Va4ryWuZbQvmUt2N85ec

* feat: auto-compute tagAsLatest from vars.LATEST_VERSION (EMB-1518)

Resolves Judgment call #2: the SDK version-bump workflow previously
always defaulted tagAsLatest to false because there was no reliable
way to know if a branch is the current "latest" major. The repo
already exposes vars.LATEST_VERSION (major version of the most recent
gold/stable release) for exactly this, so patch/minor bumps now set
tagAsLatest=true when MAJOR_VERSION matches it, false otherwise
(including when the variable is unset). alpha/beta/preminor stay
false. The PR review gate still allows overriding the computed value
before merging.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2Va4ryWuZbQvmUt2N85ec

* refactor: use github.ref_name instead of inputs.branch (EMB-1518)

github.ref_name is already set by GitHub's "Use workflow from" ref
picker on every workflow_dispatch run, so the custom inputs.branch
free-text field was duplicating a selection that was already made -
and could silently diverge from it. Switch every functional read of
inputs.branch to github.ref_name in both release workflows; keep the
branch input field itself, but make it explicitly non-functional
(required: false, no default, description points at the ref picker).

type: choice was considered and rejected again here: it would close
the typo risk but can't accept an unregistered branch, breaking
one-off custom-branch releases without editing the workflow file per
release - exactly the maintenance burden EMB-1518 removes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2Va4ryWuZbQvmUt2N85ec

* fix: remove dead readme-update mechanism (EMB-1518)

update_readme's two sed patterns match zero occurrences in the current
README (restructured by 15f7d3d20e, no version-pinned content remains),
so publish-npm's readme-follow-up-PR step has been a silent no-op on
every release, not just re-runs. Deleted the entire dead block (7 steps,
the env vars and pull-requests:write permission it alone used) and the
now-unused update_readme function.

* fix: drop minor release_type option (EMB-1518)

`semver -i minor` on a release branch jumps to the next major's
version line, colliding with that major's own branch. `patch` already
covers graduation and `preminor` already covers the legitimate
next-major jump (scoped to master). Drop `minor` outright rather than
add compensating validation - resolves review-cycle-3 finding #17 /
Judgment call #6.

* fix: add explicit reviewer request for embedding team on bump PR (EMB-1518)

CODEOWNERS is deliberately stripped on release branches (a repo-wide
practice to cut auto-backport review noise, unrelated to this
workflow), so the bump PR's "code owner review is automatic" comment
was false there - it got zero reviewers requested on release
branches. EMB-1000 documented the identical gap in the old system's
follow-up PR. Fix targets the metabase/embedding team (not a named
individual - the old system's hardcoded reviewer has since left).

* fix: restrict release types by branch, add custom releases (EMB-1518)

A one-off branch could publish as the stable release. Validation only
guarded master and release-x.*, so `patch` on e.g. sdk-data-apps stripped
the prerelease id (0.62.5-data-apps.0 -> 0.62.5), computed the
<major>-stable dist-tag, and - when the major matched vars.LATEST_VERSION
- set tagAsLatest, shipping a feature branch to npm as the stable release.
Only check-git-tag stood in the way, and only if that version had already
shipped.

Each branch class now gets only the release types that make sense for it:
master takes alpha/preminor, release branches take beta/patch, any other
branch takes custom. tagAsLatest is additionally gated on a release-x.*
branch, so nothing published from master or a one-off branch can take the
npm `latest` tag.

The custom type keeps one-off branches (esbuild, data-apps) releasable
under that restriction: prerelease_id is set on the first cut and becomes
both the version's prerelease id and the <major>-<id> dist-tag, matching
the shape already live on npm (56-esbuild, 63-data-apps). Later bumps
leave it empty and semver reuses the id from the committed version.

Also drops the dead release_utils.bash (zero functions, zero callers),
replaces both free-text dispatch inputs with single-option choices that
cannot be typed into, points the release workflow's form at the bump PR,
and removes the scaffold banner comments.

* fix: read vars.LATEST_MAJOR_VERSION, not LATEST_VERSION (EMB-1518)

The variable is LATEST_MAJOR_VERSION - it is what release.yml already
reads to decide Docker's `latest` tag, and what the release checklist has
a github admin bump each cycle. `vars.LATEST_VERSION` does not exist, so
tagAsLatest would have computed false on every bump, including at gold,
without any error - the fallback for an unset variable is false.

* refactor: make the release concurrency group readable (EMB-1518)

Same three cases as before, but each arm now names what it produces
(sdk-release-<branch> for a dispatch or a merged bump PR, sdk-release-noop-<run id>
for a closed PR this workflow ignores), and the comment above maps them in
the order the expression tries them.

The noop arm is not decoration: concurrency is evaluated before any job's
`if:` runs, so a closed PR that prepare-release skips would still enter the
release group without it. cancel-in-progress stays false so a release is
never interrupted mid-publish.

Groups are prefixed rather than bare because concurrency groups are
repo-wide, not per-workflow.

* refactor: build the release scripts once, in their own job (EMB-1518)

The Slack jobs each got at release/dist/index.cjs a different way:
send-slack-notification-start checked out and built it (then uploaded it as
an artifact), send-slack-notification-failure built its own second copy, and
send-slack-notification-successful downloaded the artifact. The duplicate
build existed because the failure job has to fire even when the metadata read
fails - in which case the start job never ran, so its artifact never existed.

A build-release-scripts job now builds it once and uploads it, and all three
Slack jobs download it. The job hangs off prepare-release only, so the
artifact is there no matter how early the release fails - which removes the
reason the second build existed.

* fix: quote the Slack messageId in the success notification (EMB-1518)

A Slack ts is a string. Interpolated bare it becomes a JS number literal and
drops trailing zeros - 1783602196.214000 renders as 1783602196.214, and the
reply targets a thread that does not exist. Pre-existing on master; the
failure job already quoted it, so this makes the two consistent.

* refactor: resolve the release branch in one expression (EMB-1518)

The env: BRANCH indirection was injection hardening from when the value was
the free-text `inputs.branch`. It is github.ref_name now - a real branch
name, same as github.event.pull_request.base.ref, which the next line already
inlined - so it guarded nothing and made the step asymmetric.

base.ref is empty on a dispatch, so `base.ref || ref_name` picks the right one
without branching on github.event_name.

* refactor: drop the duplicate-bump-PR pre-flight (EMB-1518)

It guarded a case the design already handles safely. Two open bump PRs
against the same branch both compute from the branch's committed version, so
both propose the same next version: the first to merge publishes and tags it,
and the second dies at check-git-tag with "Tag ... already exists" (npm would
reject the republish too). Nothing is published twice, and the error says
exactly what happened.

Getting there needs someone to dispatch the bump twice and leave the first PR
open, with both PRs sitting labelled in the list. That is not worth an API
call and 16 lines on every bump.

* chore: trim the bump step's comments (EMB-1518)

* refactor: drop the semver validity pre-check (EMB-1518)

Every supported release branch (57 through 63) already carries a valid
semver, and the new process never writes a `0.N.x` placeholder - gold is a
patch bump (0.63.0-beta.3 -> 0.63.0). The placeholder only ever existed
transiently under the old checklist, which rewrote it back to a concrete
version after each release.

Also saves an npx invocation on every bump.

* chore: spell out PRERELEASE_ID in the bump workflow (EMB-1518)

* refactor: spell out the semver call in each release type (EMB-1518)

alpha and beta shared an arm that passed --preid="$RELEASE_TYPE", and patch
passed -i "$RELEASE_TYPE" - you had to know the variable's value to know what
command ran. Each arm now names its own call.

The release_type description now leads with the phase (before gold, at gold,
after gold), which is what the person dispatching actually knows. It also
corrects preminor: master moves to the next major *after gold*, not at branch
cut - the 62 cycle did it in the after-gold checklist (#75487, 0.62.0-alpha ->
0.63.0-alpha), and 61 before it (#74279).

* docs: stop calling the bump PR a review gate (EMB-1518)

Nothing blocks on approval: the bump PR is not auto-merged but it is not
gated on a reviewer either, and workflow_dispatch can publish with no PR at
all. The computed distTag/tagAsLatest are defaults made visible and editable
- whoever dispatched the bump has already decided.

* docs: note EMB-2095 (auto-merge the bump PR) in the workflow (EMB-1518)

* docs: tighten the tagAsLatest comment (EMB-1518)

* refactor: collapse the tagAsLatest case into one condition (EMB-1518)

* docs: say what the sdkRelease write does (EMB-1518)

* refactor: drop the label-creation step (EMB-1518)

The sdk-release-trigger label is repo-level, so it only ever needed creating
once - it now exists. Re-running `gh label create ... || true` on every bump
swallowed every error (including auth) to no purpose.

* fix: fail on a leftover bump branch instead of side-stepping it (EMB-1518)

The run-id suffix made every retry a fresh branch, so an abandoned bump left
a branch on origin forever and the name was unpredictable. The branch is now
named after the version, and an existing one is an error: an earlier bump to
the same version was never merged, which is worth knowing before opening a
second PR alongside it.

* refactor: drop the no-backport label step (EMB-1518)

Backporting is opt-in - auto-backport.yml only fires on a `backport` label -
and backport-reminder.yml only demands a decision label when a PR touches
docs/**. The bump PR touches one JSON file, so no-backport guarded nothing.

Keeps the reviewer rationale, moved next to the --reviewer flag it explains.

* feat: show the target branch and computed values in the bump PR body (EMB-1518)

The PR body did not say which branch it targeted, which is the first thing
you want to know when a release PR shows up. Branch, release type, version,
dist-tag and tagAsLatest are now a table at the top.

* fix: pin semver and validate prerelease_id (EMB-1518)

npx --yes semver resolved whatever the registry served at dispatch time, on
a job that installs no dependencies - so release-critical version math could
change under us while package.json already pins ^7.6.3. Pinned to semver@7.

prerelease_id was passed to semver unchecked. An id like `DataApps` bumped
fine and then died three steps later in the dist-tag read-back, whose regex
only matches [a-z][a-z0-9-]*, with an error blaming the computed version
instead of the input. It is now rejected up front, with a message naming the
rule.

* fix: take semver's version from package.json (EMB-1518)

Pinning the workflow to semver@7 fixed the drift but created a second
version to keep in step. Read the range the repo already pins instead
(devDependencies.semver, currently ^7.6.3) and hand it to npx, so bumping
semver in package.json is the only place it happens.

* refactor: rename bump workflow input to read_first (EMB-1518)

* feat: reject prerelease_id on non-custom release types (EMB-1518)

* refactor: drop the preminor bump-only warning from the PR body (EMB-1518)

* refactor: drop the duplicate rerun wording in tag-exists error (EMB-1518)

* fix: reject reserved alpha/beta as a custom prerelease_id (EMB-1518)

* refactor: tidy and clarify comments in the SDK bump workflow (EMB-1518)

* fix: satisfy actionlint in the SDK release workflow (EMB-1518)

* feat: extract embedding SDK release logic into a tested helper (EMB-1518)

* refactor: remove dead embedding SDK helpers from version-helpers (EMB-1518)

* fix: drop trailing blank line in the SDK bump workflow (EMB-1518)

* refactor: run the SDK version bump from a tested helper, not inline bash (EMB-1518)

* docs: note the env block guards github-script against injection (EMB-1518)

* refactor: drop the unused LATEST_MAJOR_VERSION fallback in the bump script (EMB-1518)

* feat: show the target branch in the bump PR title (EMB-1518)

* docs: drop the manual-edit invitation from the bump PR body (EMB-1518)

* refactor: tidy the SDK release helper and its tests (EMB-1518)

* refactor: read the SDK release metadata via the tested helper (EMB-1518)

* refactor: env-route the branch name in the bump PR title (EMB-1518)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

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

* refactor: simplify SDK release concurrency group (EMB-1518)

Collapse the three-case concurrency expression to one per-branch group, and
switch every multi-item needs array to a block sequence.

The dropped noop group guarded a compound case: GitHub keeps one pending run
per group and cancels any earlier one, so a closed PR touching the template
could evict a release waiting to start. That needs two same-branch releases
in flight at once, so we take the simpler expression. The clean fix is
queue: max, which actionlint cannot parse yet.

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 16:34:19 +01:00
Riley Thompson
a08f7ff918 Show confirmation modal when uploading CSV to model with duplicates (#77636)
* show warning modal

* address pr comments
2026-07-24 13:37:47 +00:00
github-automation-metabase
fdace56d22 Deflake transform revision history spec (#78272)
* Deflake transform revision history spec

The "view and revert transform revisions" test changed the transform
name twice, each firing a "Transform name updated" success toast that
lives for the default 5s undo timeout. Unlike every other update flow in
this spec, these toasts were never dismissed. On a fast runner the test
reached the failed-revert error toast within that window, so a lingering
success toast coexisted with it and H.undoToast() (a single-element
findByTestId on [data-testid="toast-undo"]) failed with "Found multiple
elements".

Dismiss each success toast right after it appears, matching the existing
convention used elsewhere in this file, so exactly one toast is present
when the error toast is asserted.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Nemanja <31325167+nemanjaglumac@users.noreply.github.com>
2026-07-24 14:55:37 +02:00
github-automation-metabase
8b52336966 Auto-update documentation for master (#78517)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-07-24 07:54:47 -04:00
Sébastien
0795d60b21 Extract a shared AdminDataTable for admin list tables (sortable + paginated) (#78136) 2026-07-24 12:35:17 +02:00
Timofey Kachalov
5fe045c2ab Add an endpoint that outputs Data App Dev diagnostic reports for an agent (#78358) 2026-07-24 13:23:00 +04:00
Chris Truter
1ba3a7c050 Add semantic search health checks (#78299)
* Add semantic search health checks

* Split HNSW recovery from semantic health

* Use HoneySQL literals for repair queries

* Use dynamic redefs in semantic search test
2026-07-24 10:24:15 +02:00
Ngoc Khuat
36fa06a4bd Serialize card last_used_at so imports don't reset it (#78338) 2026-07-24 06:44:37 +00:00
Chris Truter
a323e1c883 Add NLQ retrieval health checks (#78298) 2026-07-24 08:31:00 +02:00
Egor Grushin
a9731f6e65 style(sdk-bundle): Alert component redesign (#78260)
* redesign Alert, remove deprecated Alert component

* fix styling so far

* update alert stories

* additional changes

* small fix

* refactor Alert config

* address PR's comments

* chores

* Update Loki Snapshots

* addressing the designing feedback in places

* addressing the designing feedback in the Alert itself

* add loki tests

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-07-24 08:08:28 +02:00
Romeo Van Snick
a3e82fb537 Replace TreeTable getRowHref with a renderRowLink render prop (#78523) 2026-07-24 14:36:03 +10:00
Romeo Van Snick
14a65406f1 Fix dashcard #scrollTo auto-scroll (#78522)
* Auto-scroll to a dashcard whenever it becomes the scrollTo target, and keep the scroll until the grid settles

* Drop a stray brace from the auto-scroll spec's first visit URL

* Simplify the auto-scroll correction to a fixed frame window
2026-07-24 14:15:46 +10:00
Fraser
1591e74409 Fix module stats counting (#78184) 2026-07-24 15:53:41 +12:00
Romeo Van Snick
dee31666b9 Migrate transforms pages off withRouteProps (#78422)
* Migrate monitor tools off withRouteProps

* Migrate metric pages off withRouteProps

* Migrate JobInfoApp off withRouteProps

* Migrate transforms pages off withRouteProps
2026-07-24 13:51:17 +10:00
Romeo Van Snick
bd7af8355e Migrate monitor and metric pages off withRouteProps (#78421)
* Migrate monitor tools off withRouteProps

* Migrate metric pages off withRouteProps

* Migrate JobInfoApp off withRouteProps
2026-07-24 12:48:12 +10:00
Romeo Van Snick
24220fe191 Delete the react-router v3 engine (#78305)
* Delete the react-router v3 engine

* Sever the react-router package dependency

* Give the SDK data-app router its own browser history on v7

* Rename the react-router-v7 alias to react-router

* Rename getDataAppHistory to getRawBrowserHistory, drop dead getCurrentHistory
2026-07-24 08:56:17 +10:00
dpsutton
f35f7450a6 Scope migration existence preconditions to their table (~46s off every full pg migration) (#78403)
Scope existence preconditions to their table; lint against name-only checks

A foreignKeyConstraintExists precondition with only foreignKeyName (or
indexExists with only indexName) forces Liquibase to snapshot the entire
schema to answer the check -- ~2s per precondition on a large postgres
appdb. 27 such preconditions (24 FK, 3 index; 21 in one v59 workspace
block) added ~46s to every full migration: 50.2s of total changeset time,
of which 24 changesets cost 45.9s and the other 1,182 cost 3.7s.

Add the scoping key to all 27. Measured on a fresh postgres migration:
total changeset time drops 50.2s -> 5.1s, with zero changesets over 1s.

Editing released changesets' preconditions is safe: Liquibase checksums
cover the changes, not preConditions. Verified two ways -- ChangeSet
.generateCheckSum is byte-identical before/after for all 27 (v9
checksums), and a clone of an already-migrated appdb re-validates
cleanly against the edited files.

lint-migrations-file: generalize require-primary-key-exists-has-table-name
into require-table-scoped-existence-preconditions, covering
primaryKeyExists/indexExists/uniqueConstraintExists (tableName) and
foreignKeyConstraintExists (foreignKeyTableName), walking the full
precondition tree in both its flat-map and list forms.


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

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 16:11:37 -05:00
Maksym Yakubych
089b82338c Make AI auditing Conversations table horizontally scrollable (#78378) 2026-07-23 16:06:15 -05:00
Oleksandr Yakushev
f63af2b18c [perf] Implement containsKey for SnakeHatingMap (#78097) 2026-07-24 00:04:13 +03:00
Riley Thompson
a37f29e737 Match remapped fields with ID or name based fields refs (#78246)
* apply fix

* add tests

* keep key structure the same

* update test

* improve fix

* fix bad field id refs test

* add sandboxing test
2026-07-23 21:01:26 +00:00
Chris Truter
8112036b95 Add shared search index health metrics (#78297)
* Add embedder circuit breaker and recovery

* Add embedder breaker edge-case tests

* Avoid noisy circuit-open embedding errors

* Add shared search index health metrics
2026-07-23 22:57:15 +02:00
Ethan McCue
dec394cf5e Allow config files to be named with a .yaml extension if no .yml file is found. (#77823)
* Fall back to .yaml if .yml not found

Update docs to mention `.yaml` support

* Fall back to .yaml if .yml not found

Update docs to mention `.yaml` support

* Update file.clj

* Fix tests

* Address feedback
2026-07-23 20:30:32 +00:00
Chris Truter
020b02932d Add embedder circuit breaker and recovery (#78296)
* Add embedder circuit breaker and recovery

* Add embedder breaker edge-case tests

* Avoid noisy circuit-open embedding errors
2026-07-23 22:04:03 +02:00
Alexander Polyankin
9fc9f53a1f entity_id is always present; drop the identity-hash machinery (#78452) 2026-07-23 19:29:42 +00:00
technomancy
9ccac4efed Drop field usage table (#78408)
* Drop field_usage table as it is no longer used.

* Stop saving preprocessed_query in metadata since it's not used any more.

This used to be used for saving off field usage, but that hasn't been
a thing for a very long time.
2026-07-23 18:46:00 +00:00
Ngoc Khuat
7b60d66387 Limit body-only dashboard subscription results (#78431) 2026-07-23 17:37:14 +00:00
AlessioLaiso
3360008053 Make model-illustration.svg work well in both light and dark mode (#68072)
* Make model-illustration.svg work well in both light and dark mode

* whitelabel svg

---------

Co-authored-by: Nick Fitzpatrick <nickfitz.582@gmail.com>
2026-07-23 14:36:37 -03:00
Andrei Chernykh
f24e4bed1f Migrate PinMap to a typed function component and extract a pure getPoints (#78067)
* Migrate PinMap to a typed function component and extract a pure getPoints

* Type the pin metric as nullable, derive MapSettings from VisualizationSettings, and add createMockVisualizationProps

* Trim comments, replace the map ref cast with an instanceof guard, and cover instance clearing
2026-07-23 10:13:44 -07:00
John Swanson
07b9ec8ffa Don't block dashboard saves on pre-existing foreign internal cards (#78186)
* Don't block dashboard saves on pre-existing foreign internal cards

UXW-4870: the guard added in #77236 (reject assigning a dashcard's card
to a question internal to another dashboard) checked updated dashcards
unconditionally, so any dashboard with a pre-existing foreign internal
card association became unsavable — every edit, even a resize, returned
400 "Invalid Request.".

Scope the guard to newly-introduced card ids: card ids the dashboard
already references (as a dashcard's card or in a series) are
grandfathered, so existing associations never block saving — including
moving such a card to a different dashcard in the same save. Newly
assigning a question internal to another dashboard is still rejected.

* Clarify arg names in internal-dashcard guard

Rename the guard's params to existing-dashboard/new-dashcards to make
clear the grandfather set comes from stored state while the checked
dashcards are the incoming save.
2026-07-23 10:08:33 -07:00
Timofey Kachalov
8ee3e5213c Update @slack/web-api to version 8.0.0 (#78438) 2026-07-23 20:53:32 +04:00
Timofey Kachalov
a2d673e0fe Enumerate data apps by reading git tree objects (#78114) 2026-07-23 20:05:12 +04:00
Alex Yarosh
4ea4064511 docs: sample database (#78390)
sample database
2026-07-23 10:37:38 -04:00
Michael Eby
55598bf0bd docs: update screenshots (#78363)
* docs: update screenshots

* Optimized 7 images, saved 357.6 KB (51.0% smaller)

---------

Co-authored-by: Metabase Automation <github-automation@metabase.com>
2026-07-23 10:08:53 -04:00
John Swanson
c5b6a38acb Guest embed filter dropdowns should use the router database (UXW-4881) (#78235)
* Guest embed filter dropdowns should use the router database (UXW-4881)

PR #68952 made guest-embed query execution bypass DB routing by wrapping it
in with-database-routing-off, but the parameter-values endpoints (dropdown
values, search, remapping) didn't get the same treatment: chain-filter runs
through the QP, where attach-destination-db-middleware throws "Anonymous
users cannot access a database with routing enabled."

- Wrap the query-running sections of the four param helpers in
  metabase.embedding-rest.api.common in with-database-routing-off, fixing
  all six /embed/{card,dashboard}/:token/params/* endpoints plus their
  preview_embed equivalents.
- Wrap the guest-embed tiles endpoints, which have the same anonymous-user
  bug.
- Wrap preview_embed's query-execution endpoints (query, dashcard, pivot,
  tiles) so the admin's embed preview shows exactly what the published
  embed will show (router DB data) instead of routing via the previewing
  admin's user attribute.
- Replace the manual *current-user-permissions-set*/*is-superuser?*
  bindings in the param helpers with the equivalent request/as-admin.

Public sharing has the same gap but whether routed DBs should work over
public links at all is an open product question, so it is intentionally
left out.

* Consolidate embed routing-off wraps into the shared execution helpers

Review feedback: rather than a macro that each endpoint must remember to
use, move with-database-routing-off inside the shared execution helpers in
metabase.embedding-rest.api.common (process-query-for-card-with-params,
process-query-for-dashcard, process-tiles-query-for-dashcard, and a new
process-tiles-query-for-card wrapper), which every /api/embed and
/api/preview_embed endpoint funnels through. Endpoints built on these
helpers now bypass routing automatically, so a future endpoint can't
reintroduce this class of bug.

Removes all per-endpoint wraps from embed.clj (including the two original
ones from #68952) and preview_embed.clj; the invariant now lives in one
file with one explanatory comment.
2026-07-23 06:33:37 -07:00
Nick Fitzpatrick
0669d80f2b Scope measurement container css vars (#78225) 2026-07-23 10:27:32 -03:00
Oleh K
b7d519320b Prevent scrollbar flicker from cell tooltips in TreeTable (#78337)
* Fix tooltip scrollbar flicker in TreeTable (GDGT-2822)

Hovering a cell tooltip in a virtualized TreeTable while scrolling made a
page-level vertical scrollbar flash on and off. The tooltip is portaled to
<body> and positioned with real top/left insets, and with the document root
sized to the exact viewport height, a tooltip positioned past the fold grew
the document and flashed a scrollbar.

Portal cell tooltips into the table's own scroll container instead of <body>,
so a tooltip positioned past the fold is clipped by the (already-scrolling)
table rather than stretching the page. This is wired through a small
TooltipPortalTargetProvider context so the metabase/ui Tooltip wrapper picks
up the target automatically — no per-tooltip wiring, and any TreeTable with
cell tooltips benefits (e.g. the notifications admin table).

* Address PR comment

* Fix Tooltip wrapper not forwarding ref
2026-07-23 15:16:47 +02:00
Chris Truter
26c003f858 Harden health inspector check persistence (#78295) 2026-07-23 15:06:00 +02:00
Jakub Chodorowicz
be2a5e4ea1 GDGT-2890: Fix custom-viz template .gitignore: ignore dist/ and *.tgz​ (#78355)
add dist and tgz to gitignored files
2026-07-23 14:57:19 +02:00
Nick Fitzpatrick
528ae0a381 Update table loading skeleton to use patterns (#78361) 2026-07-23 09:43:59 -03:00
Nick Fitzpatrick
4323943627 Filter out other users personal collections from search in DataStep (#78353) 2026-07-23 09:43:48 -03:00
Stas Gavrylov
b5849c9944 Monitor (#76806)
* Add monitor dir in metabase-enterprise

* GDGT-2676: Migrate Dependency Diagnostics under Monitor space

* GDGT-2677: Migrate Admin -> Tools under Monitor space

* GDGT-2676: Fix paths

* GDGT-2676: Create separate high-level directories for migrated features

* GDGT-2657, GDGT-2686: Monitor space + migrate Dependency Diagnostics (#76527)

* GDGT-2657: Introduce Monitor space layout and routing

* GDGT-2686: Migrate Dependency Diagnostics to Monitor

* Monitor: Add Monitor plugin. Cleanup + fixes

* Monitor: Update graphy ownership path for Dependency Diagnostics

* Monitor: Cleanup PR

* GDGT-2697: Update hotkeys hints for Toggle navbar

* GDGT-2684: Address Copilot review comments

* GDGT-2684: Address PR comments

* GDGT-2684: Rename SpaceLayout to AreaLayout. Split to separate files

* GDGT-2684: Refactor route guards + add tests

* GDGT-2684: Move Admin Tools pages to the Monitor space (#76606)

* GDGT-2684: Move Admin Tools to Monitor space

* Monitor: Extract diagnostics sidebar into shared Monitor sidebar outlet

Add a useMonitorSidebar context to MonitorContent so Monitor pages can
render sidebars in a dedicated right-hand region outside the padded,
scrollable main content area. Rewire Dependency Diagnostics to render its
detail sidebar through this outlet, and promote the resizable shell
(SidebarResizableBox) into shared monitor/components.

* Monitor: Add MonitorHeaderTitle component for consistent page headers

Introduce a shared MonitorHeaderTitle matching the small secondary text
style used by Data Studio breadcrumbs, and apply it across Monitor pages:
Dependency Diagnostics header, Jobs, Logs, Model cache log, Task list/runs,
Task and task-run details, Erroring questions, and Alerts management.

* GDGT-2684: Cleanup

* GDGT-2684: Update monitoring tools access permissions

* GDGT-2684: Add handling for not-found routes

* GDGT-2684: Fix not-found routes handling in Data Studio

* GDGT-2684: Extend route tests. Minor styling refactoring

* GDGT-2684: Refactor Alerts Management sidebar

* GDGT-2684: Move old SidebarResizableBox to ModelSibar

* GDGT-2684: Cleanup

* GDGT-2684: Refactor Sidebar solution to use Portal

* GDGT-2684: Fix Monitor Tools E2E specs (#76886)

* Format monitor router files after the master merge

* Import Route from metabase/router facade in AreaTab spec

Fixes the no-restricted-imports lint error banning direct react-router imports.

* GDGT-2713: Update Tasks page styling (#77088)

* GDGT-2713: Update Tasks page styling

Restyle the Monitor Tasks and Runs pages to match Dependency diagnostics:

- Replace the SettingsSection card + default tabs shell with the Monitor
  frame (fixed-height page, pills tabs; only the table scrolls)
- Rebuild TasksTable and TaskRunsTable on the TreeTable data-grid
  (sticky header, virtualized rows, skeleton loading, a11y labels)
- Move filters into a Group bar and pagination below the table with a
  total results count
- Keep the 2-state sort contract (always sorted, desc/asc) on top of
  TanStack's 3-state cycle; asc-first on all sortable columns
- Add MonitorHeaderTabs (Monitor-owned pills tabs, no data-studio
  dependency) and extract the shared LinkTab router-link tab to
  metabase/common/components

* GDGT-2713: Fix e2e test

* Migrate LinkTab to the metabase/router facade

* Remove unused Link import from PaneHeader

The Tasks styling switched the tabs to LinkTab, leaving Link unused.

* Address PR review comments and fix entity_name typing

Use strict undefined comparisons instead of loose null checks where
values can only be undefined (optional props, RTK Query error,
destructured SortingState), and strict null comparisons where the
value can only be null (Task.db_id).

Also fix TaskRun/RunEntity entity_name typing: the backend always
assocs this key (nil when the entity lookup misses), so it's
string | null on the wire, not optional.

* GDGT-2712: Update Jobs page styling (#77100)

* GDGT-2712: Update Jobs page styling

Restyle the Monitor Jobs page to match Dependency diagnostics:

- Rebuild the page on the Monitor frame (fixed-height page; only the
  jobs table scrolls, with a sticky header)
- Render the scheduler info in a Code block instead of a bare pre
  inside a settings card
- Rebuild the jobs table on the TreeTable data-grid (skeleton loading,
  empty state, a11y label, keyboard row activation); row click still
  opens the job triggers modal
- Move JobInfoApp into a folder and add unit coverage; add
  TaskInfo/Job API mocks and a task-info server mock

* Make the Monitor sidebar outlet resizable

Move the resizing behavior from the Dependency diagnostics sidebar into
the shared Monitor Sidebar so every Monitor view gets it without
duplication:

- Relocate SidebarResizableBox from metabase-enterprise/monitor into
  MonitorLayout and wrap the Sidebar portal content with it
- Sidebar API: optional resizable (default true) with containerWidth
  required while resizable, optional defaultWidth (512 by default);
  consumers measure their content area with useElementSize and pass it
- Protect the Monitor content area from text selection while dragging
  inside Sidebar instead of per-view
- Simplify DiagnosticsSidebar/DependencyDiagnostics accordingly and
  drop the now-empty metabase-enterprise/monitor/components tree
- Make the Alerts management detail sidebar resizable (fills the
  resizable box, keeps its 560px default width)

* GDGT-2712: Jobs page styling fixes

* Replace the job triggers modal with a Monitor sidebar

Clicking a job on the Jobs page now opens the job's triggers in the
resizable Monitor sidebar outlet instead of a modal:

- New JobTriggersSidebar renders each trigger as a vertical key/value
  block (all fields the modal table had), with a divider between
  triggers and a close action that returns to /monitor/jobs
- The /monitor/jobs/:jobKey route stays, so deep links and back/forward
  keep working; JobInfoApp reads the jobKey param and renders the
  sidebar, measuring its content area for the resize constraint
- Delete JobTriggersModal; add a Trigger API mock
- Give the scheduler info Code block the old card padding and keep it
  from shrinking

* Apply cross-review fixes for the jobs sidebar work

- Render trigger details as a dl/dt/dd list for proper label/value
  semantics (no visual change)
- Cover the shared Sidebar resize side effect (user-select class on the
  Monitor container across drag start/stop/unmount)
- Cover keyboard row activation, all trigger attributes with null
  placeholders and the may-fire-again 'No' branch, and the no-triggers
  empty state

* GDGT-2712: Cleanup

* GDGT-2712: Add distinct error message when job is not found

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

* Migrate JobInfoApp to the metabase/router facade

* Disable text selection on body while resizing the sidebar

Address review comment: dragging the resize handle otherwise causes
the browser to continuously select/deselect page text, since the
sidebar is portaled and doesn't share a DOM ancestor with the main
content to scope a CSS class to.

---------

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

* GDGT-2711: Update Logs page styling (#77160)

* GDGT-2711: Extract LogsViewer to common Monitor components

* GDGT-2711: Update Logs page styling

* GDGT-2711: Cleanup

* GDGT-2711: Address PR comments

* GDGT-2711: Address PR review comments

Rename LogsViewer's process prop to processUUID, drop the unused
logs-content id, and remove a few comments flagged as redundant.

* GDGT-2661: Update Erroring questions page (styling + refactoring) (#77167)

* GDGT-2661: Restyle Erroring questions page

Rewrite the EE /monitor/errors page off the legacy audit_app viz onto the
Monitor recipe used by the other Monitor tools pages, keeping the same
backend audit query:

- TreeTable with the 11 audit columns, 2-state sortable headers, and native
  row selection; "Rerun Selected" moved to the standard bottom BulkActionBar
- Dependency-diagnostics-style debounced filter inputs
- Page no longer scrolls; the table body is the sole scroll region
- Pagination outside the table with a total count (new bad-table-count audit
  query), hidden until the data has loaded, and synced to a ?page URL param
- Requests abort the previous in-flight one on paging/sorting/filtering

Replaces the AuditParameters/AuditTable/ErrorMode stack; deletes the old
ErrorOverview and its click-mode.

* Remove dead legacy audit-table v1 code

The Erroring questions page was the last consumer of the Audit v1 table
stack. With it rebuilt on TreeTable, these are unreferenced and removable:
AuditTable, QuestionLoadAndDisplay, AuditTableVisualization (the last
"audit-table" display), AuditParameters, the bad_table card def, and
AuditMode. The rest of audit_app (unsubscribe UI, InsightsLink,
metabot-analytics) is untouched.

* Fix duplicate/incorrect cards in the erroring-questions audit query

The latest_qe CTE joined query_execution to the per-card MAX(started_at)
subquery on started_at alone, so executions across cards that shared a
timestamp fanned out — surfacing duplicate rows (and cards whose latest run
didn't actually error). Join on card_id as well. Regression test covers a
shared-timestamp collision.

* GDGT-2661: Address review feedback

- Surface count-query failures in the page error state so a failing count
  can't silently hide pagination
- Filter bar reflows (wrap) instead of clipping at narrow widths / zoom
- Skeleton renders the checkbox column to avoid a layout shift on load
- Centralize the internal-dataset-query cast to a single boundary
- Add tests: count db/collection filter clauses (backend), 202-body
  data.error path, real filter-reset assertion, rerun failure recovery

* GDGT-2661: Fold pagination total into the rows query via COUNT(*) OVER()

The page ran two /api/dataset requests per view — the bad-table rows and a
separate bad-table-count — each recomputing the expensive latest_qe scan over
all query executions. Add a COUNT(*) OVER () total_count column to bad-table so
the full filtered total rides on the rows response, and drop the count query.

Halves the round-trips and eliminates the second latest_qe scan (~2.1s -> ~1.4s
of DB work on the Stats dump); the window count adds no measurable overhead
since it runs over the already-materialized result set before LIMIT.

* Migrate ErrorOverview to the metabase/router facade

* GDGT-2661: Return erroring-questions total_count at the response root

The bad-table query computes COUNT(*) OVER () as a column, which repeated the
total on every row. Strip that column via an xform and surface the value once
as a top-level total_count on the response; the FE reads it from there.

* GDGT-2661: Non-blocking per-row rerun with loader in erroring questions

Rerun now runs in the background per card instead of blocking on the whole
batch: clicking Rerun Selected closes the bulk bar immediately and fires each
request independently, so more questions can be queued while slow ones run.
A rerunning row shows a loader in place of its checkbox, is excluded from
Select all, and its loader clears when the request settles; the list refetches
once the last rerun finishes.

TreeTable gains a generic isRowLoading prop and makes the whole checkbox cell
a click target so a near-miss no longer navigates to the row.

Also hoist total_count onto the Dataset type and drop the local helper.

* Extract shared generic sorting helpers for monitor tables

The monitor tables (erroring questions, tasks, dependency diagnostics) each
duplicated getSortingState/getSorting/isSortColumn. Extract them into a generic
metabase/utils/sorting module keyed by the column type. getSorting is overloaded
so the un-sort behaviour is type-driven: pass the current sort for always-sorted
tables (direction flips) or omit it to allow an unsorted result.

Refactor the three monitor tables onto the shared helpers.

* Update erroring questions E2E selectors for the new table

Point the tests at the TreeTable-based ErrorOverview: erroring-questions-table
container, erroring-question rows, and the always-enabled filters / bulk bar
that closes on rerun instead of a disabled button.

* Bind *driver* when reducing audit internal queries

The MySQL timestamp reader calls results-timezone-id, whose driver arg is
malli-validated as a keyword in tests. reduce-results* bound the timezone
override but not *driver*, so it was nil and audit queries returning timestamps
errored on MySQL/MariaDB. Bind *driver* to the app-db driver alongside the
override.

* GDGT-2661: Address PR comments

* GDGT-2661: Merge erroring questions input into one, extend to support name search

* GDGT-2661: Fix checkbox cell width in rows

* GDGT-2661: Fix e2e test

* GDGT-2661: Address PR comments

* GDGT-2710: Update Model cache log page styling (#77277)

* GDGT-2713: Update Model cache log page styling

Restyle to match Dependency diagnostics: the page no longer scrolls
vertically, only the table does. Replace the hand-rolled AdminS.ContentTable
with a metabase/ui TreeTable (skeleton while loading, empty state, refresh
action), and drop the emotion-styled ModelCacheRefreshJobs.styled.tsx in
favor of a CSS module.

Model and Collection columns are the two stretching columns (fill remaining
width) and support client-side sorting, since the underlying /api/persist
endpoint has no sort param.

* Unify monitor tables on 3-state sortable headers

Erroring questions and Tasks forced a 2-state flip-forever cycle by passing
the current sort back into getSorting(); drop that so a 3rd click clears the
header's sort arrow and falls back to the page's default sort, matching
Dependency diagnostics and Notifications.

The default-sorted column on each page needs sortDescFirst matching its
default direction (desc), otherwise the very first click on that column is a
no-op: TanStack's per-column cycle treats desc as the removable/last state
when sortDescFirst is false, so a column that starts at desc goes straight to
removed on click one.

* Make all applicable columns sortable in Model cache log and Jobs

Both are fully-loaded, unpaginated lists, so enabling client-side sorting on
the remaining columns is safe. Model cache log's action column stays
unsortable (no header text or data). Tasks and Task runs are left as-is:
their backends only support a fixed set of sort columns (or none), and
sorting the rest client-side would silently reorder only the current page.

* GDGT-2710: Address PR comments

* GDGT-2709: Update Alerts management page styling (#77364)

* GDGT-2709: Update Alerts management page styling

Restyle to match Dependency diagnostics: the page no longer scrolls
vertically, only the table does. Move pagination outside the table into a
plain footer and show the total result count via PaginationControls'
showTotal.

* Restrict Alerts management to admins only

The /api/notification/admin* endpoints are superuser-gated on the backend,
but the frontend route and nav link were reachable by anyone with the
broader can_access_monitoring permission (shared with the other Monitor
Tools pages), so a non-admin holding it could reach the page while every
API call inside it 403'd. Add a dedicated canAccessAlertsManagement
selector and route guard so the route/nav link match the backend's
admin-only restriction.

* Fix misleading test name in MonitorLayout spec

The test asserts the link is hidden for a non-admin analyst, not that it's
shown for an admin.

* GDGT-2794: Update Task/Run details page styling

Extract the "Back to Tasks"/"Back to Runs" breadcrumb into a shared
MonitorBackLink component, styled and positioned like other Monitor
page headers. Wrap both detail pages in the same non-scrolling shell
used elsewhere in Monitor so only the content area scrolls internally,
instead of the whole page scrolling behind the floating AppSwitcher.

* Extract MonitorPageContent to replace SettingsSection in Monitor

SettingsSection carries admin-page title/description logic that Monitor
pages don't use. MonitorPageContent keeps the same card styling with none
of that logic, dropping the last two /monitor call sites still importing
from metabase/admin.

* GDGT-2709: Fix layout in Tasks Details page

* GDGT-2709: Refactor NotificationsTable row selection logic. Fix selection with spacebar

* GDGT-2791: Abort superseded in-flight list requests in Monitor (#77694)

* GDGT-2791: Abort superseded in-flight list requests in Monitor

Changing page/sort/filter on a Monitor list page left the previous, often
expensive, list request running to completion on the server. Subscribed RTK
useXQuery hooks only abort when their last subscriber unmounts, so introduce
a shared useAbortableQuery hook built on lazy triggers (whose promise exposes
.abort()) that cancels the superseded request on arg change, unmount, and
skip transitions, and suppresses the abort error so cancellations never flash
the error view.

Caching mirrors useQuery: cache-first by default, refetchOnMountOrArgChange
to opt into always-fetch; manual refetch() always hits the network. Every
converted call site keeps its pre-migration caching semantics.

Converted: TaskListPage, TaskRunsPage, TaskRunEntityPicker,
ModelCacheRefreshJobs, NotificationsAdminPage (main list),
DependencyDiagnostics, BrokenDependentsSection, and ErrorOverview (its
one-line useAbortableAdhocQuery wrapper is inlined and hooks.ts deleted).

* GDGT-2791: Cleanup

* GDGT-2791: Fix type narrowing in TaskRunEntityPicker

* GDGT-2791: Clamp pagination range to total

* GDGT-2791: Test that tag invalidation does not refetch superseded args

* GDGT-2791: Scope query aborts per hook instance via injected cache key

* GDGT-2791: Migrate _refetchDeps to the shared __rtkCacheKey mechanism

* GDGT-2791: Refine baseQuery typings. Cleanup

* GDGT-2791: Replace interface in params declarations with type. Refine BaseQuery params type

* GDGT-2832: Group Monitor navbar items into sub-menus (#77706)

* GDGT-2832: Move AreaContent to AreaLayout and normalize Monitor page spacing

* GDGT-2832: Group Monitor navbar items into sub-menus

* GDGT-2832: Cleanup. Fix AreaTab outline overflow

* GDGT-2832: Match Alerts Management tab spacing to Dependency Diagnostics

* GDGT-2832: Replace collapsible navbar groups with static labelled sections

* GDGT-2832: Update Monitor views headings to match navbar. Fix e2e test

* Monitor section design tweaks (#78007)

* icon and spacing updates

* remove info icon from background tasks page heading

* improve empty states

* GDGT-2810: Enable sorting in all Monitor views (#77842)

* GDGT-2810: Enable sorting on all Background tasks and Task runs columns

* GDGT-2810: Remove bold styling from first Monitor table columns

* GDGT-2810: Fix reflection lint warnings in task history API test

* GDGT-2810: Qualify task_run/task_history filter columns

The entity_name and task_count sorts add joins (report_card and
report_dashboard both have their own entity_id column), so unqualified
filter columns become ambiguous: /api/task/runs?entity-id=N&sort_column=entity_name
returned a 500. Qualify the filter columns and add a regression test
combining filters with the derived-column sorts.

* GDGT-2810: Cleanup

* GDGT-2810: Use kebab-case sort params on the task runs endpoint

* GDGT-2810: Extract shared useSortingStateChange hook for TreeTable sorting

* GDGT-2810: Extract shared page/sort URL param parsers

* GDGT-2810: Fix e2e tests

---------

Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>

* GDGT-2843: Integrate Monitor upsell pages (#77948)

* GDGT-2843: Migrate Monitor upsell pages into monitor scope, link to /upgrade/monitor

* GDGT-2843: Tighten monitor module boundaries to standard shared tier

* GDGT-2843: Change Monitor app icon from gauge to pulse

* GDGT-2843: Update pulse icon and Monitor logo

* GDGT-2843: Address PR comments

* GDGT-2843: Fix e2e tests

* Fix Monitor code review findings (#78106)

* Fix Monitor code review findings

* Remove obsolete dependencies redirects

* Monitor review: Cleanup

* Address review comments

* Cleanup

* Remove Dependency diagnostics link from Data Studio nav

It already lives under Monitor (/monitor/dependency-diagnostics); the
Data Studio route just redirects there now.

---------

Co-authored-by: Kamil Mielnik <kamil@kamilmielnik.com>
Co-authored-by: YB Tan <tybmeta@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Maz Ameli <maz@metabase.com>
Co-authored-by: Ngoc Khuat <qn.khuat@gmail.com>
2026-07-23 13:48:39 +03:00
Ngoc Khuat
d3456cc4d1 Only execute attached cards for attachment-only dashboard subscriptions (#78352) 2026-07-23 10:25:35 +00:00
Fraser
32e4c39378 Remove video recording (#78321) 2026-07-23 22:08:04 +12:00
YB
3bef91fb42 [GDGT-2145] map more dedicated fields and leave them out of additional opts (#78412)
map dedicated fields and leave them out of additional opts
2026-07-23 16:20:40 +08:00
Romeo Van Snick
5461b53d55 Flip the unit-test harness to the v7 router engine (#78306)
* WIP: start flipping unit-test harness to v7

Default routerEngine to v7 and render bare (non-Route) children under a
catch-all route so v7's <Routes> renders them. role=link nav failures still
to diagnose. Suite not yet green.

* Give v7 tests a history handle and drop DOM-wrapped route config

The harness now owns the v7 memory history and hands specs a v3-shaped adapter
(getCurrentLocation/push/replace/goBack/listen), so the ~42 specs that drive and
assert navigation through the returned history keep working on v7.

Route config wrapped in a <div> is a v3 artifact that v7's <Routes> cannot read;
swap those two for fragments.

* Pin the v3-vs-v7 conformance suites to the v3 engine

They exist to prove the facade on v3 matches real v7 and are built from raw v3
route config, so they must not follow the harness default now that it is v7.

* Resolve a relative initial route in the v7 test history

* Buffer navigations dispatched before the v7 router registers

* Carry route props onto the v7 matched-route stubs

* Fix the v48 /browse/:dbId-:slug legacy redirect on v7

* Render facade routes in renderHookWithProviders on v7

* Migrate unit specs off v3-only routing behavior for the v7 flip

* Drop buffered v7 navigations when the router unmounts

* Default use-v7-router to true
2026-07-23 12:25:13 +10:00
Romeo Van Snick
dbeb963735 Remove use-v7-router flag from docs (#78389)
* Remove use-v7-router flag from docs

* Turn the flag of by default
2026-07-23 09:58:06 +10:00
Alexander Polyankin
8f92d99055 Replace timeout-prone fk_target_field_id cleanup migrations with NOT EXISTS (#78382) 2026-07-22 18:56:49 -04:00
Romeo Van Snick
3ee16f6e79 Re-home history@3 types onto the metabase/router facade (#78190)
* Re-home history@3 types onto the metabase/router facade

* Format the re-homed import blocks

* Merge duplicate router imports and drop unused lint directives

* Re-home the router-listen spec's history Location type
2026-07-23 07:44:27 +10:00
Maksym Yakubych
8ff4bd113d feat: remove navigate_to (#75646) 2026-07-22 20:14:25 +00:00
github-automation-metabase
6af601cba0 Stabilize treemap breadcrumb drill test (#78303)
Wait for the DebouncedFrame to settle via ensureChartIsActive() before
clicking a group tile to drill in. While the frame transitions it carries
pointer-events: none, so the drill click was silently swallowed and the
breadcrumb button never rendered.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:31:30 -06:00
Riley Thompson
cdac81f0cc Dont add previous stage sql as source query for cached query sql (#78252)
* dont add extra select for persisted query

* add repro tests

* refactor test

* pull our add-source-query helper

* remove unneeded mt/dataset

* test clean up

* add comment
2026-07-22 18:59:23 +00:00
Andrei Chernykh
628eee6db4 Migrate AuditTableVisualization to a typed function component (#78196)
* Migrate AuditTableVisualization to a typed function component

* Fix Erroring Questions column sorting by forwarding sorting props through Visualization

* Extract typed viz definition to drop casts per review
2026-07-22 09:55:39 -07:00
dpsutton
6482737957 GHY-4016: Replace Field YAMLs with FieldUserSettings in git sync (PR … (#76843)
* GHY-4016: Replace Field YAMLs with FieldUserSettings in git sync (PR 1/3)

Git sync currently exports one Field YAML per database column. For a
typical customer this is ~6396 files — 84% of the entire repo. Most of
that data (active, database_type, position, dimensions, ...) is sync
metadata that git sync has no business tracking; the user-curated part
lives in FieldUserSettings.

This PR switches git sync to export FieldUserSettings instead of Fields.
Only fields with user-curated metadata get a YAML at all.

Measured against the stats-remote-sync repo (196 published tables,
~20 columns each):

  Import:  43s → 14s  (3x faster, 7659 → 1263 files)
  Export:  6.3s → 2.0s  (3x faster, 2155 → 239 entities)
  Files:   7659 → 1263  (84% reduction)

For a large customer (563k files, 522k Fields), ingest time alone drops
from 116s to 7s.

Full Field serdes is unchanged — non-git-sync export/import is
unaffected. FieldUserSettings already had complete serdes support
(generate-path, storage-path, load-find-local, make-spec); this PR
wires it into the git sync paths.

Changes:
- `serdes/descendants "Table"`: branches on `:user-edits-only` opts.
  When set, emits `["FieldUserSettings" field_id]` only for fields with
  a FUS row; otherwise emits `["Field" field_id]` as before.
- `spec.clj`: named `git-sync-extract-opts` constant (with
  `:user-edits-only true`); public `pk-col` helper for the non-standard
  FieldUserSettings PK (`:field_id` not `:id`).
- `extract.clj` (OSS): local `pk-col` for `:where [:in pk-col ids]`
  (module boundary prevents depending on EE spec).
- `impl.clj`: `extract-chunk` uses `spec/pk-col` for FieldUserSettings.
- `models.clj`: adds `"FieldUserSettings"` to `data-model-in-collection`.
- `events.clj`: removes `:model/Field` from `register-events-for-spec!`
  so field-update events no longer create Field RSOs. A dedicated
  `::field-update-event` handler creates FieldUserSettings RSOs only
  when a FUS row exists. This completes the incremental path: full
  export uses `descendants` + `:user-edits-only`; incremental export
  uses FUS RSO rows.

Tests assert on the full RSO table so stray Field RSOs fail immediately.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Address PR review: defensive get for dynamic pk-col, fix content-hash test for FieldUserSettings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* don't need maps, k/v pairs work

* Declare :model/FieldUserSettings module boundary export/import

remote-sync references :model/FieldUserSettings in events.clj, so it must
be exported by warehouse-schema and imported by enterprise/remote-sync.

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

* Remove unused f1-id binding in extract test (kondo)

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

* Test that Field and FieldUserSettings are serdes descendants leaf nodes

Cements the design discussed in review: Table's descendants method is the
only source of field-level entries, and the :user-edits-only branch is
exclusive — you get Field refs or FieldUserSettings refs, never both via
a transitive Field -> FieldUserSettings edge.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-22 10:09:52 -05:00
Nicola Mometto
30021f1abd Transform run dag be (#76974)
* WIP: dag run BE

* split into new table

* run utils

* remove dag changes on job runs

* new dag run model

* abstract over parent-run

* separate dag model/api

* factor common job/dag code in a coordinated-run primitive

* fixes

* dag test

* fix migration

* kondo

* missing resolution

* exclude model

* kondo config

* fix test

* fix: exclude for deser

* fixes

* fix: exclude for deser

* fix: missing fk

* random ids

* chore: rename

* feat: add unioned runs

* remove dead endpoints

* api tests

* extend filter semantics

* trim docstrings

* kondo

* gap: allow canceling job runs

* share code

* fix: job start should return proper job id

* fix: mig to 64

* simplify

* simplify further

* simplify

* dead code

* add unique constraint on dag runs

* tighten schemas

* tighten permissions

* fix test

* relax schema

* fix: set null on delete

* handle deleted jobs/transforms for dag runs

* appease the sql linter

* fix test

* simplify rollbacks

* simplify callsite

* fix tests from bad rebase

* fix: fallback when transform is deleted

* add transform_count

* fix tests

* log with context

* varchar 32 for status

* Add UI for running transform with upstream or downstream (#77367)

* GDGT-2625 wip: reprocess DAG UI for transforms

- Split "Run now" button (upstream/downstream) + confirmation modal
- runDag / dag-transforms / cancel-dag API + analytics event
- Tabbed Runs page: transform-graph runs (default) + individual transform runs
- Graph runs table (Run/Type cols), filter bar (Type/Transform/Started/Status),
  sidebar with member transforms; shapes aligned to bronsa's /api/transform/runs
- List + members still served from a mock (transform-graph-runs.mock) via queryFn

* GDGT-2625 wip: align graph runs to /api/transform/runs, sidebar + cancel

- Reshape TransformGraphRun to backend RunSummaryResponse (run_type/entity_id/name)
- Filters mirror Transform runs: Type/Transform/Started/Ended/Status/Trigger
- Sidebar: title = Run name, Type as first metadata row, cancel section below
  metadata (dag/job/transform), confirm modal; job-run cancel endpoint wired
- Mock covers job/dag/transform in all states (succeeded/started/failed/timeout)
- Reorder columns: Status/Trigger after Duration

* GDGT-2625 wip: sidebar header entity links

Add "View this job" / "View this transform" (+ dependency graph for
transforms) action links to the graph-run sidebar header, mirroring the
individual transform-runs sidebar.

* GDGT-2625 wip: migrate graph runs to real /api/transform/runs

- Swap list + member queryFn mocks for real queries (list endpoint, dag/job
  member drill-down; standalone runs synthesized). Delete the mock fixture.
- Rewrite page spec against real endpoints via fetch-mock + new mock factories.
- Reword DAG run confirm modal (drop "in this order", bullet list) since members
  can run in parallel.

* Adjust wording

* GDGT-2625 wip: scheduled state + cleanup

- Show a "scheduled" run state on the transform page after a DAG reprocess is
  triggered, so the queued transform gives feedback instead of looking idle
- Drop dead DAG-run types/tag-providers/mock helpers left from earlier iterations

* Address code-review findings for reprocess DAG UI

Correctness:
- Migrate transforms.cy.spec.ts to /runs/individual (the /runs route now
  renders the graph-runs page, not the individual run table)
- Gate the "Run all" DAG confirm button on the mutation, preventing a
  double-click from triggering two DAG runs
- Toast when run-dag returns a null dag_run_id instead of silently no-op
- Stop the "Scheduled" banner from polling forever when the member-runs
  request errors
- Keep the graph-run sidebar open when the selected run scrolls off the
  current page during polling
- Invalidate transform state on cancelDagRun/cancelJobRun so member
  transforms' last_run refreshes after a cancel

Cleanup:
- Translate whole phrases (with context) for DAG run names
- Replace a typecast with a type guard in TypeFilterWidget
- Memoize parsed params and filter options on the graph-runs page
- Drop the unused direction field from the scheduled-DAG-run state
- Convert the sidebar's IIFE switch to ts-pattern match

* Handle deleted entities in graph runs; fix run-list cache invalidation

- Graph runs table: grey a deleted job/transform's run name (italic +
  "has been deleted" tooltip), keeping the row clickable
- Sidebar: keep showing member transforms for deleted DAG and standalone
  runs (fetched by dag-run id / synthesized from the run); hide the section
  only for deleted job runs, whose members can't be fetched without the
  (now-null) job id
- api/transform.ts: invalidate the run lists on deleteTransform (a
  transform's runs survive its deletion) and cancelCurrentTransformRun, so
  the Runs pages reflect the change instead of relying on polling
- Trim narrating comments from the transform API types

* Cleanup

* Fix transforms cancel e2e: click renamed "Cancel run" confirm button

The transform-page cancel confirmation button was renamed from "Yes" to
"Cancel run", but two cancelation tests still clicked "Yes". The cancel was
never confirmed, so the slow (pg_sleep) transform kept running and held a lock
on the writable DB, hanging the next test's resetWritableDb and failing the
suite. Point those two clicks at the new label (the individual-runs-page cancel
still uses "Yes" and is left unchanged).

* Avoid type cast in RunSection spec to satisfy no-unjustified-type-casts

Replace `body as string` with a typeof guard when reading the run-dag request
body, which the new lint rule flags.

* Show dependency count in the graph-run label

Use bronsa's new transform_count to label DAG runs as
"{n} dependencies › {name}" (upstream) / "{name} › {n} dependents"
(downstream), n = transform_count - 1, joined by a small chevron icon.
Bold the job/transform name; fall back to the plain name when the count is
absent (job/standalone runs, or DAG runs predating the column).

* Replace runs tabs with Detailed view switch; polish graph-run name rendering

- Swap the two runs tabs for a Detailed view switch that toggles grouped
  (/runs) and detailed (/runs/individual) views in place; add RunsPage shell
  and DetailedViewSwitch, remove RunTabs
- Render the sidebar header run name like the table (dimmed deps count +
  chevron), with wider gap and aligned font size/weight
- Keep the Detailed view switch label on one line
- Bump graph-runs page size to 25 to match the individual runs page
- Fix stale react-router-redux import

* Preserve shared filters across Detailed-view toggle; drop deprecated CSS util

Toggling the Detailed-view switch previously called the run-list URL helpers
with no arguments, discarding the active filters. Forward the params common to
both views (statuses, transform ids, start/end time, run methods) via a new
CommonRunListParams/pickCommonRunListParams helper; page and sort are dropped
since the two views paginate differently and use incompatible sort columns.

Also replace the deprecated CS.overflowHidden global utility class in the new
graph-run table with an inline overflow style prop.

* Track Detailed-view toggle in transform runs analytics

Emit a transform_runs_view_toggled simple event from the DetailedViewSwitch
toggle handler with the resulting view mode (detailed/grouped), so both the
grouped and individual run-list call sites are covered.

* Fix reprocess run status: don't re-show Scheduled after a member completes

- Derive the DAG scheduled state from this transform's own member run:
  a member run row only exists once the transform starts, so its presence
  means the transform already ran this cycle. Prevents isScheduled from
  re-arming (and masking the completed status) after this transform's run
  finishes while sibling DAG members are still running
- Keep the graph-run sidebar polling while the top-level run is canceling,
  mirroring isPollingNeeded

* Cleanup

* Address PR review nits from Uladzimir

- Extract isActiveRunStatus helper and use it for the duplicated
  started/canceling checks (RunButton, both isPollingNeeded, sidebar
  shouldPoll, RunSection dagFinished)
- DetailedViewSwitch: replace styles prop with a CSS module class
- Convert renderRunName/renderRunEntityName to RunName/RunEntityName
  components
- Drop redundant String() around an already-string cell value
- Cover RunButton onRun click and the run-options menu show/hide

* Adapt runs routes to the react-router migration on the rebased base

The base migrated transforms routes to element=/withRouteProps with
Outlet-based layouts. Make RunsPage an Outlet layout and render the runs
list pages via element in the specs.

* wip

---------

Co-authored-by: Oleh K <oleh@metabase.com>
2026-07-22 15:24:52 +01:00
Chris Vandoodewaard
76c36ef709 stop actionlint from linting deleted workflow files (#78342)
update file-paths filter for actionlint to ignore deleted files
2026-07-22 10:07:37 -04:00
Kamil Mielnik
70abeae733 GDGT-1933 Dashboard actions prefetch sends parameter values in URL query string (#77939) 2026-07-22 13:53:56 +00:00
Sébastien
6b1809dca1 Resolve tenant/personal collection sentinels in the question save path (#78104) 2026-07-22 15:22:07 +02:00
Timofey Kachalov
35444ee8a5 Add ability to report near membrane sandboxing blocks to a listener (#78344) 2026-07-22 17:04:12 +04:00
Timofey Kachalov
1152608228 refactor(sdk): Restructure the data-app dev server (#78251) 2026-07-22 15:04:52 +04:00
Timofey Kachalov
226175c492 Match CSP port semantics in the data-app sandbox allowlist (#78249) 2026-07-22 14:42:32 +04:00
Timofey Kachalov
7519d07ec5 refactor(sdk): Export SdkThemeProvider from the SDK bundle (#78247) 2026-07-22 14:38:53 +04:00
Romeo Van Snick
8adef47af6 Enable the react-router v7 engine by default, with the fixes it needs (#78199)
* Treat a v7 Link with an empty `to` as a button, not a navigation

* Make a v7 replace to the current URL a no-op so URL-sync effects settle

* Provide router.listen on the v7 engine and keep the router shim stable

* Serialize a v3 location query into the search string on v7

* Fix lint and type issues in the v7 router shim

* Keep the v7 matched-route branch stable across renders

* Republish the v7 splat param under v3's `splat` name

* Enable the react-router v7 engine by default

* Commit the v7 location update synchronously instead of in a transition

* Mirror the v7 location into redux from the history subscription

* Format the v7 router provider

* Sort query keys when serializing a v3 location query

* Match v3 query encoding and resolve bare link paths against the root

* Keep v3 query encoding and leave absolute link urls alone

* Always push when a v7 link targets the current url
2026-07-22 20:02:24 +10:00
Benoit Vinay
075482040b Updates to the license page (#78036)
* Updates to the license page

* Misc

* Unit tests updated
2026-07-22 10:55:11 +02:00
Nicola Mometto
fad1ba4fd7 test: bump transform-run timeouts for slow drivers (#78202)
* test: bump transform-run timeouts for slow drivers

BigQuery transform executions routinely take 50-70s in CI

* share timeout
2026-07-22 08:45:47 +00:00
Fraser
ce14a4a511 Clear old CodeQL caches (#78108) 2026-07-22 20:29:20 +12:00
Romeo Van Snick
001485bb4b Keep async vendor chunks out of the main app's initial bundle (#78304) 2026-07-22 13:32:12 +10:00
Chris Truter
73c6fa1778 Fix migration-version linter for the new backport label semantics (#78281) 2026-07-22 00:42:36 +00:00
da
5cfbe5423c Update pdfbox version from 3.0.5 to 3.0.8 (#78230) 2026-07-21 19:38:26 -03:00
Nathan Voxland
074f2a6e93 Additional transform gates (#76630) 2026-07-21 17:17:40 -05:00
Jeff Bruemmer
3e2c3661be docs - remove 63 workflow (#78265)
* remove 63 workflow

* update latest to target 63
2026-07-21 17:16:25 -04:00
Sloan Sparger
428c6be070 [Metabot] Animated markdown streaming (#78046)
* working solution

* finish

* code comment changes

* small animation adjustment

* respect reduced motion, fix tests
2026-07-21 20:15:26 +00:00
Nick Fitzpatrick
d188001d3a Loading Skeletons in Entity Picker (#78160)
* Loading Skeletons in Entity Picker

* update recent and search panels too

* renaming "LoadingSpinner" and adjusting imports
2026-07-21 16:40:01 -03:00
Uladzimir Havenchyk
c8f693f5da feat: use id-slug url for table row details (#78126)
* feat: use id-slug url for table row details

* update e2e test
2026-07-21 21:57:00 +03:00
Sloan Sparger
f8ae33f741 [Metabot] User conversation history + resumable conversations (#77549)
* conversation list endpoint

* add rtkquery endpoints

* generate conversation title

* better title generation logic, poll for title if not recieved before end of first message

* render conversation title in ui and show conversation list

* allow resuming historical conversations

* show exploration conversation history

* refresh current conversation ui for 409 case

* surface conversation titles in usage analytics

* add /metabot/conversation/:id route

* remove pending title data part, assume from lack of title data part we need to poll, fix loading a conversation that is mid-response

* title backfill job

* rename title event

* FE prevent polling multiple times for conversation title

* improve title generation prompt

* ci fixes

* automated review feedback

* e2e test fixes

* reduce diff size

* self-review

* Address PR review feedback

- api: pass profile-id through valid-usage-profile-id when ensuring a
  conversation title, matching the backfill job
- title backfill: gate readiness on check-usage-limits! (instance-level)
  so the job pauses and reschedules cleanly when a usage limit is hit
- title backfill: bound the title future .get with a 60s timeout so a
  stuck generation can't block the backfill loop indefinitely
2026-07-21 18:37:22 +00:00
Robert Kosara
830717374c Delete auditing tools page, redirect to usage analytics (#78241) 2026-07-21 17:12:23 +00:00
Maksym Yakubych
ccfc4fe280 feat: improve saving the charts (#76757) 2026-07-21 17:05:31 +00:00
Oleh K
bf0d55aac5 Add Metabot option to the Transforms New menu (#78134)
* Add Metabot option to the Transforms New menu (GDGT-1773)

Adds a "Metabot" entry as the first option in the Transforms "New" menu.
Clicking it opens the data-studio Metabot sidebar with a pre-seeded
"Create a transform that " prompt (not submitted), gated on Metabot access.

Also fixes the tiptap prompt input so a programmatically seeded prompt
places the caret at the end instead of the start (autofocus "end" plus a
caret-to-end after external setContent), which also improves the retry path.

* Cleanup

* Cleanup unnecessary code

* Test CreateTransformMenu Metabot item without mocking

Render the real CreateTransformMenu + Metabot via the shared metabot test
setup so the menu item, permission gating, and prompt seeding are exercised
end to end instead of through stubbed hooks.
2026-07-21 16:42:10 +00:00
Chris Vandoodewaard
916a3b8231 update the backport announcement (#77819) 2026-07-21 12:13:44 -04:00
Oleh K
8e45662740 Restyle Pill component to match Figma design (#77086)
* wip: Restyle Pill to Figma capsule spec

- Full-round 999px capsule, background-primary fill + shadow-xs ring
- 12px/500 text (text-primary), disabled -> text-disabled
- Remove button 12px, opacity 0.5 -> 1 on pill hover
- size 'sm' default + label slot override
- Add Storybook showcase matrix story
- Remove dead transforms components/TagList (unused)

* Adjust styles for existing Pill usages

* Add md size to Pill; center label/remove slots

- New md size (14px text, 6/12px padding, 28px tall) via .root[data-size=md]
- Flex-center the label and remove slots
- Adopt md in AddMemberRow and ReorderableTagsInput; drop dead radius=xl

* Apply design feedback to Pill padding and shadow

- sm vertical padding 2px (20px tall)
- sm removable right padding 6px; md removable right padding 8px
- Update shadow-xs token to match Figma (0.5px ring + 1px/3px drop)
2026-07-21 16:32:13 +02:00
Nicola Mometto
40e185b47f fix: use explicit ids when inserting test products on Snowflake (#78201)
Reuse the insert path added for ClickHouse in #77921
2026-07-21 14:38:23 +01:00
Chris Truter
e3a24b8db9 Bump bb pgsql pod to 0.1.6 to remove Rosetta requirement (#78200)
Bump babashka postgresql pod to 0.1.6 so mage runs on Apple Silicon

The 0.1.0 pod manifest has no macos-aarch64 entry -- only macos-amd64.
Babashka downloads that x86_64 artifact and caches it under the aarch64 path
anyway, so on an Apple Silicon machine without Rosetta every mage command dies
before doing any work:

  Cannot run program ".../mac_os_x/aarch64/pod-babashka-postgresql":
  Exec failed, error: 86 (Bad CPU type in executable)

Clearing the pod cache does not help -- the re-download is the same x86_64
binary. 0.1.5 is the first release with a real macos-aarch64 artifact; this
bumps to the current 0.1.6, which resolves to a native arm64 executable.

0.1.6 keeps every platform 0.1.0 shipped (macos-amd64, linux-amd64,
windows-amd64) and adds macos-aarch64 and linux-aarch64, so no one loses
coverage. `pg/execute!` is the only pod fn used anywhere in the repo, and its
signature is unchanged.

Verified on arm64: the cached binary is now `Mach-O 64-bit executable arm64`,
`mage.bot.setup-worktree` loads, and `./bin/mage fix-kondo-ratchets` runs to
completion.

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:17:08 +00:00
Romeo Van Snick
07909c841f Restore navigation blocking on the v7 engine and fix v7 Link navigation (#78103)
* Restore navigation blocking on the v7 engine via a blocking history

* Render the v7 Link on the v7 engine so links navigate in modals

* Scope v7 leave hooks to their route so within-route navigation is allowed

* Don't let a v7 Link with no destination navigate on click

* Type the app Link's `to` as optional to match the runtime contract

* Apply activeClassName on the v7 engine via NavLink
2026-07-21 19:09:17 +10:00
Kamil Mielnik
a3f5a5bf8a GDGT-2845 Fix Table to map dropdown search with more than 2 letters (#77946) 2026-07-21 09:00:20 +00:00
Oleh K
ec0a9ceaea Fix crashes and broken behavior in the native transform variables sidebar (#78047)
* Hide settings tab in TagEditorSidebar if there is no tags

* Don't crash query metadata fetch on un-normalizable queries

batch-fetch-query-metadata filtered empty queries before normalization,
but normalize-query returns {} when a query can't be normalized (e.g. a
native query with an incomplete field-filter/table template tag). That {}
then failed batch-fetch-query-metadata*'s strict ::query schema, 400ing the
whole request and tripping the query editor error boundary. Filter empties
after normalization too so an unnormalizable query just yields no metadata.

* Hide template-tag 'Try it' examples in transforms editor

The template-tag help sidebar's 'Try it' buttons load an example query
against the Sample Database. Transforms can't target the Sample Database,
so clicking 'Try it' switched the transform to an invalid database and left
the editor in a broken state. Add a canUseSampleDatabase QueryEditor option
(default on) that transforms set to false, which withholds the sample
database id so TagEditorHelp hides the 'Try it' buttons.

* Add regression tests for transform variables sidebar fixes

- batch-fetch-query-metadata skips queries that normalize to {} instead of
  400ing the whole request (the P1 crash)
- TagEditorHelp hides the 'Try it' examples when no sample database is available
- TagEditorSidebar hides the Settings/Help tabs until the query has a variable

* Address PR review feedback

- Simplify batch-fetch-query-metadata with (keep (comp not-empty normalize-query))
- Replace the hidden-tabs settings behavior with an empty state in the Settings
  tab (no more layout shift / auto-switch to Help); use the shared EmptyState
- Make sampleDatabaseId a required, DatabaseId-typed setup option
- Add a NativeQuerySidebar test asserting 'Try it' is hidden for transforms
  (canUseSampleDatabase=false) and shown otherwise
- Re-add the getEditorOptions canUseSampleDatabase assertion
2026-07-21 10:52:42 +02:00
Kamil Mielnik
07cb2f0a6c GDGT-2872 Revert #77695 to allow custom widget popovers (#78124) 2026-07-21 15:32:23 +07:00
Ngoc Khuat
3e9ff6e040 Bump schema-change test post-run waits on Redshift (#77945)
Bump schema-change test post-run waits to 30s on Redshift
2026-07-21 08:27:45 +00:00
Phoomparin Mano
6569bc1271 test(sdk-package): split useMetabaseQuery hook tests into smaller test suites (#78109)
* split use-metabase-query.unit.spec.tsx into multiple files

* split test fixtures from setup.tsx

* move useMetabaseQuery tests into tests folder

* rename useMetabaseQuery hook test

* remove unused query test fixture fields

* rename mock dataset query helper

* rename bundle exports

* chore: tiny formatting changes

* test: rewrite stale query results test

* test: extract public api tests to type and runtime tests
2026-07-21 05:54:16 +00:00
Anderson
a492c1091b Hide existing metrics from the metric mini picker (#78054)
* Hide metrics from the metric mini picker

* Select metrics from Browse all in E2E test

* Preserve metric picker restrictions
2026-07-20 22:33:19 -03:00
Anderson
cda8f7d301 Keep long chart tooltip values readable within the viewport (#77947)
* Wrap long chart tooltip values

* Constrain chart tooltip height
2026-07-20 22:33:10 -03:00
Andrei Chernykh
2e2b4e1298 Convert the Table visualization from a class to a function component (#78055)
* Convert the Table visualization from a class to a function component

* Stabilize the Table question so async metadata updates don't rebuild columns mid-hover

* Type useSyncedQuestion via VisualizationProps so the SDK d.ts doesn't leak internal types
2026-07-20 16:31:53 -07:00
Alexander Polyankin
7413348d68 Revert "Async workspace provisioning" (#78172) 2026-07-20 22:38:17 +00:00
Chris Vandoodewaard
71d66af5a2 Fix failure in "Re-run Flaky Workflows" workflow (#78135)
* use sendSlackMessage in workflow, update method to accept attachments and blocks

* pass slack token
2026-07-20 16:44:35 -04:00
Nathan Voxland
0de6d6ee2b Better model namespace requiring. (#76571) 2026-07-20 15:39:02 -05:00
Nick Fitzpatrick
c2decd2881 Library in Data Reference (#76917)
* Library in Data Reference

* conditional library option, adjusting table lists

* fix test
2026-07-20 17:32:41 -03:00
Chris Truter
5672dd99c6 Burn down redundant kondo ignores and stamp the false positives (#77787) 2026-07-20 20:25:02 +00:00
Romeo Van Snick
f8230a5b56 Add the react-router v7 engine behind a flag (#78020)
* Author route paths in v7 syntax, shim to v3 in the facade

* Add use-v7-router flag and frontend engine reader

* Add flag-gated react-router v7 engine (provider, tree mapper, bridge)

* Re-point redux navigation and location to the v7 router

* Support both router engines in renderWithProviders

* Switch the app mount to the v7 router behind the flag

* Point setRouteLeaveHook no-op comment at DEV-2374
2026-07-21 06:06:17 +10:00
Nathan Voxland
bfec5d9f26 Don't log user names or emails on authentication (#75539) 2026-07-20 15:01:02 -05:00
Ryan Laurie
096ce217b5 Chain the publish off of the auto-release, not the tag (#78161)
* chain the publish off of the auto-release, not the tag

* disallow auto release on forks

* thank you actionlint

* fail early

* today i learned
2026-07-20 13:33:32 -06:00
Nathan Voxland
a5ee670889 Better validate-setting logging and test handling (#77623) 2026-07-20 14:29:57 -05:00
Michael Eby
628f06a22c docs: fix markdown example link (#78146) 2026-07-20 14:08:25 -04:00
Ben Grabow
cbaff9c03e QUE2-756: Failed cache refresh (e.g. result too large) leaves refresh lease held and keeps serving stale cache (#78132)
* Delete stale cache entries after successfully running a query that shouldn't be saved to cache.
2026-07-20 17:39:25 +00:00
Alexander Polyankin
e1a1e69911 Async workspace provisioning (#78091) 2026-07-20 13:38:02 -04:00
Phoomparin Mano
b041c02f5a Move MCP url to the top and update inline origins copy (#78102)
* move mcp url field and inline origins copy

* copywriting improvements for allowed origins
2026-07-21 01:36:18 +08:00
Chris Vandoodewaard
e93496629b remove unnecessary uses of skipOn (#78048)
* remove some unnecessary uses of skipOn

* try running the slow x-ray test

* use standard @skip tag instead
2026-07-20 13:26:15 -04:00
Alexander Polyankin
9c2758cc0d Revoke schema-level grants when destroying Redshift workspace isolation (#78094)
* Revoke schema-level grants when destroying Redshift workspace isolation

destroy-workspace-isolation! discovered schemas to revoke via
svv_relation_privileges, which only surfaces relation-level grants: a granted
input schema with no tables (or whose tables were dropped after provisioning)
kept its schema-level USAGE grant, and DROP USER then failed with 'user ...
cannot be dropped because the user has a privilege on some object'. Union in
svv_schema_privileges so schema-only grants are revoked too.

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

* Rename workspace+det to workspace in the new destroy test

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

* Run Redshift workspace teardown statements individually, not as a JDBC batch

A failure now surfaces as the plain server error instead of the
BatchUpdateException wrapper ('Batch entry N ... Call getNextException to see
other errors in the batch.'); the transaction still aborts as a whole on the
first error.

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

* Revert "Run Redshift workspace teardown statements individually, not as a JDBC batch"

This reverts commit 4bf2d241b5.

* Unwrap BatchUpdateException in Redshift workspace teardown

Keep the batch (there can be many granted schemas, two revokes each) but
rethrow .getNextException so failures read as the plain server error instead
of 'Batch entry N ... Call getNextException to see other errors in the batch.'

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

* remove comment

* Unwrap BatchUpdateException across all workspace-isolation batch sites

New driver.u/batch-exception util returns the per-statement exception hiding
under .getNextException (or the throwable unchanged). Applied to every
workspace init/grant/destroy executeBatch site (h2, mysql, postgres,
clickhouse, redshift) — init sites unwrap before scrub-exceptions — so
failures read as the plain server error instead of 'Batch entry N ... Call
getNextException to see other errors in the batch.'

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

* Keep the batch exception on init-workspace-isolation! failures

The init statements carry secrets and pgjdbc echoes the aborted statement in
the BatchUpdateException message — scrub-exceptions redacts it, and the
GHY-3660 test (init-workspace-isolation-never-exposes-password-test) relies on
that redacted CREATE USER echo as proof the run reached the statement.
Unwrapping to the inner server error discarded the echo. grant/destroy keep
the unwrap: their statements carry no secrets and the plain server error is
the better message there.

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

* Unwrap the batch envelope on init-workspace-isolation! too

All workspace batch sites now rethrow the underlying per-statement exception;
init scrubs the unwrapped exception so passwords stay out of the message. The
GHY-3660 test now proves the flow reached CREATE USER by asserting the
server's permission error instead of the redacted batch echo (which no longer
exists).

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

* Test batch-envelope removal for grant! and password scrubbing after unwrap

- driver-level: a failed grant-workspace-read-access! must not surface a
  BatchUpdateException (h2 runs locally; pg/mysql/clickhouse in driver CI;
  non-batching impls pass trivially)
- unit: scrub-exceptions composed with batch-exception redacts the password
  in the unwrapped per-statement message

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

* Batch init/grant/destroy on every SQL workspace driver

Redshift grant, and all three Snowflake / SQL Server workspace methods, now
run their statements as JDBC batches like the other drivers — one round trip,
per-statement exceptions unwrapped via driver.u/batch-exception (init unwraps
before scrub-exceptions). Redshift grants commit before grant! returns, so a
subsequent describe-database from another connection still sees the
privileges; the destroy pre-loop for foreign-grantor default-priv rows stays
per-statement on purpose (each must survive the others failing).

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

* Fold Redshift foreign-grantor revokes into the destroy batch

The per-statement autocommit pre-loop is gone: one transactional batch now
covers the FOR USER default-priv revokes, relation/schema revokes, and the
drops. A stale discovery row fails the whole destroy, which is fine — the
workspace layer records the failure and the deprovision retry re-discovers
fresh state.

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

* Escape workspace passwords in DDL like every other interpolated value

CREATE/ALTER USER statements interpolated the generated password raw into the
SQL string on postgres, redshift, h2, clickhouse, and snowflake (mysql and
sqlserver already escaped). Escape with sql.u/escape-sql :ansi (quote
doubling, valid on all five engines) and scrub both the raw and escaped forms
from failure messages.

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

* Unwrap batch exceptions from all JDBC connectors; fix sqlserver batch var scope

The grant-envelope test caught two real issues in driver CI:
- MariaDB's connector chains the per-statement exception via .getCause and
  mssql-jdbc chains nothing at all, so batch-exception now falls back to the
  cause and, failing that, rebuilds a plain SQLException with the same
  message/SQLState/error code.
- mssql-jdbc executes a Statement batch in one T-SQL scope, so the two
  sqlserver destroy statements both DECLAREing @sql collided; the variable
  names are now unique.

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

* Type-hint the rebuilt SQLException ctor to avoid reflection

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

* Return the batch exception as is when nothing is chained

mssql-jdbc's BatchUpdateException message is already the plain server error —
only pgjdbc adds the 'Batch entry N ... getNextException' wording. So the
unwrap falls back to the original exception instead of synthesizing a
SQLException, and the driver test asserts the envelope wording stays out of
the message rather than checking the exception class.

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

* Interpolate the escaped password in Snowflake workspace user DDL

CREATE/ALTER USER spliced the raw password while every other workspace
driver uses the ANSI-escaped value; the escaped form was already
computed and scrubbed here.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 13:17:10 -04:00
Uladzimir Havenchyk
3246bebfe4 fix: allow schemas with slash in the name (#77928)
* fix: allow schemas with slash in the name

* implement Bronsa's approach

* fixup

* fix comments

* fixup

* fixup

* fixup
2026-07-20 16:30:10 +00:00
Timofey Kachalov
b0ba83e74a Remove deleted apps on sync (#78040) 2026-07-20 20:16:17 +04:00
Chris Vandoodewaard
55554106ae ping users on Slack for failures on master/release branches (#77973)
* ping users on Slack for failures on master/release branches

* fix linting errors
2026-07-20 11:11:34 -04:00
dpsutton
64f32ddea7 Truncate data_type in BigQuery describe-fields SQL to prevent sync OOM (#78069)
A STRUCT column's INFORMATION_SCHEMA data_type spells out its entire
nested schema, and the COLUMN_FIELD_PATHS LEFT JOIN repeats it on every
nested-leaf row -- O(n^2) bytes for a column with n leaves. Dynamic-key
STRUCTs (e.g. Cloud Functions log-sink tables whose JSON payloads use
user IDs as keys) reach thousands of leaves with ~270KB type strings;
one such table produced ~4,000 rows x 271KB = 1.09GB buffered in
sync-fields!'s partition-by, OOMing the instance mid-sync.

Only short type-string prefixes are ever consumed
(raw-type->database+base-type dispatches on prefixes up to 10 chars and
stores at most RANGE<TIMESTAMP> verbatim), so SUBSTR(..., 1, 200) in the
query -- applied server-side, before the wire -- is behaviorally
identical while collapsing response size, parse cost, and the buffered
partition (1.09GB -> ~2.8MB for the table above).

The test needs no dataset or network: it captures the compiled SQL via
*process-native* and asserts both data_type columns are truncated.
2026-07-20 09:57:01 -05:00
Nicola Mometto
67de65ca3b fix clickhouse parameter rewriting on sql-tools (#78110)
* sql-tools: fix placeholde rewriting for clickhouse

* add regression test
2026-07-20 14:31:32 +00:00
Uladzimir Havenchyk
e270803b78 fix: use new router syntax (#78119) 2026-07-20 10:16:14 -04:00
Chris Vandoodewaard
d518c62a90 move global unit tests to component folders (#78058)
* remove the last test file from the legacy structure

* convert test table into format of newer tests

* remove test for undefined since this isn't a valid type for RowValue
2026-07-20 09:57:54 -04:00
Michael Eby
35cd24c2fe docs: replace markdown links (#78043) 2026-07-20 09:46:53 -04:00
Nemanja Glumac
7801008876 Restore the backend tests dependency on static-viz (#78112) 2026-07-20 15:12:55 +02:00
metabase-bot[bot]
6ab02a5d6d [chore] Update E2E test timings with recent data (#78095)
Update E2E test timings with averaged data

Collected timing data from 7 days of test runs.
All specs with timing data were included (simple averages).

🤖 Generated automatically by update-e2e-timings workflow

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GitHub Actions <actions@github.com>
2026-07-20 14:40:40 +02:00
Uladzimir Havenchyk
1a6755d898 Permalinks for dbs, schemas and tables (#77274)
* handle database named permalinks (#76794)

* handle database named permalinks

* fixup

* keep urls in the bar

* add tests

* fixup

* fixup

* address review

* handle schema name permalinks (#76807)

* handle schema named permalinks

* keep urls in the bar

* fixup

* address review

* handle table named permalinks (#76814)

* handle database named permalinks (#76794)

* handle database named permalinks

* fixup

* keep urls in the bar

* add tests

* fixup

* fixup

* address review

* handle schema name permalinks (#76807)

* handle schema named permalinks

* keep urls in the bar

* fixup

* address review

* handle table named permalinks

* fixup

* empty

* collapse table permalink routes into one optional-segment route

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

* address review

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* add permalink buttons (#77213)

* empty

* add permalink buttons

* fix eslint

* use named URLs in browser instead of id-slug (#77421)

* use named URLs in browser instead of id-slug

* fix linting

* add a comment

* address review

* add tests to permalinks (#77841)

* add tests to permalinks

* fixup

* fixup

* fixup

* fixup

* drop some tests

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 15:36:10 +03:00
Timofey Kachalov
7c3311ba35 Improve loader overlay for a data app (#78111) 2026-07-20 16:26:28 +04:00
Nick Fitzpatrick
9cb27d3986 Fix leaking plural template (#77606)
* Fix leaking plural template

* PR feedback
2026-07-20 09:01:51 -03:00
Chris Truter
57e0ad66ea Add kondo ignore ratchets (#77773) 2026-07-20 13:25:59 +02:00
Kamil Mielnik
74ec111ffe Prevent claude from inserting // Unjustified type cast. FIXME comments (#78035) 2026-07-20 17:02:44 +07:00
Sébastien
ccd7e61184 Set showDashboardQuestions to false by default on SDK CollectionBrowser (#78038) 2026-07-20 11:29:16 +02:00
Egor Grushin
184c415f6e GDGT-2578 Replace TokenField (#77768)
* migrate TokenField

* fix RemappedOption type for Autocomplete ComboboxStringItem

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

* address RecipientPicker review: memoize maps, share lookup/label helpers

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

* fix single-value search: accept top match on Enter

Autocomplete only completes on Enter when an option is highlighted, but
search options load async so none is. Restore old TokenField behavior via
onKeyDown.

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

* fix letters swallowing

* addressing the PR's comments

* added missing tests

* introduce Autocomplete + refactoring

* apply filter by clicking on enter

* fix test

* refactor autocomplete

* refactor autocomplete

* fix edge case + added unit tests

* fix type

* fix test

* fix test

* get rid of lint suppression + allow autoselect on open for multiautocomplete

* revert autoselect destructuring, restore eslint suppression (tests failing)

* explain exhaustive-deps suppression in MultiAutocomplete effect

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:59:28 +01:00
Romeo Van Snick
53c503ff0f Remove withRouter from production and delete its public export (#78019)
* Remove withRouter from production components

* Delete the public withRouter export

* Type the route-leave hook for PlainRoute and drop the route casts
2026-07-20 15:27:00 +10:00
Romeo Van Snick
a9396323f5 Author route paths in v7 syntax, shim to v3 (#78079)
Author route paths in v7 syntax, shim to v3 in the facade
2026-07-20 14:48:00 +10:00
Romeo Van Snick
04000cef07 Restructure permissions routes for v7 matching (#78021)
Restructure permissions routes to v7-compatible depth siblings
2026-07-20 03:38:37 +00:00
Romeo Van Snick
638f7e7f86 Convert the route tree to element/Outlet (#77805)
* Add element/Outlet + useRoute support to the router facade

* Convert root route tree to element/Outlet

* Convert admin route tree to element/Outlet

* Convert data-studio, models and metrics route trees to element/Outlet

* Convert transforms, account, monitor and public route trees to element/Outlet

* Convert enterprise route trees to element/Outlet and drop dead library segment routes

* Convert tenants admin route tree to element/Outlet

* Convert redirect, createEntityIdRedirect and modalRoute to element

* Ban component= on routes with eslint

* Drop component= from the facade Route type and convert remaining routes

* Drop the component= eslint rule, the facade type now enforces it

* Remove the unused withOutlet helper

* Assert the required tenantId route param instead of coercing to NaN

* Bridge element={null} to a nothing-rendering component, matching v7

* Memoize router context value so element routes re-render once per navigation

* Convert data_apps and impersonation routes merged from master to element
2026-07-20 10:34:33 +10:00
Tyler Plude
8610bfc613 [BOT-1871] Fix portable_entity_id being used for read_resource (#78074)
* fix portable_entity_id being used for read_resource

* remove entity_details references
2026-07-18 15:34:13 -07:00
Egor Grushin
8caa13a827 Remove accidentally committed __config.yml with credentials (#78084) 2026-07-18 22:07:39 +01:00
Timofey Kachalov
806e4b756c Data Apps Beta (#74840)
* initial proof-of-concept for data apps

POC 2

POC 3

POC 4

Revert "POC 4"

This reverts commit 1f62e574dc.

POC 5

POC 6

* Expose all SDK components to child realm

* Setup app publishing and serving

* Refactor distortion helpers

* Adjust skill

* Add useQuestionQuery SDK hook (#74900)

Co-authored-by: Timofei <timofei@metabase.com>

* Mock METABASE_EMBEDDING_SDK_BUNDLE for data app sdk hooks/components

* Adjust skill for TS project

* Update skill to clearly mention TS

* Add blocked APIs list to skill

* Prototype schema exports and typed hooks for custom data apps (#74978)

* add prototype for schema generation

* add formatting for generated schemas

* add skill file

* useQuestionQuery should take only ID based on @sanex3339 feedback

* add segments, measures, tables and useMetabaseQuery hook

* Add useMetabaseQuery to endowments

* Fix queryDataset

---------

Co-authored-by: Timofei <timofei@metabase.com>

* Add ability to update app bundle

* Simplify hook

* Add spinner instruction

* Mode Data Apps to OSS

* Instruct to not not use blocked event handlers

* Improve API, add styles isolation, add router components (#75418)

* Prepare skills for data app template + adjust router components (#75490)

* Fixes after rebase

* Move migration to v63

* Move migration to v63 #2

* Fix config.edn

* Fix BE tests

* Fix team name

* Fix duplicated config

* Adjust version text in skill

* Fix data app bundle uploading after legacy client removal

* Remove `actions` from config edn

* Fix failing test

* Fix failing test #2

* Add custom actions for data apps (#75627)

* Fix imports

* Fix type annotations

* Adjust migration

* Adjust permissions

* Fixed iframe security

* Data Apps publishing via remote repository POC (#75702)

* Update gitignore with data-app.html

* [PoC] Type safety and modularity improvements for custom data apps (#75381)

* allow separate libraries in schemas

* add metric type checking

* add collections parameter

* check that segments and measures belongs to table in useMetabaseQuery

* require less type information for measures

* require less type information for segments

* use metric dataset endpoint when querying measures in metrics

* add validation for breakout buckets

* fix filter operation type scoping

* expose filter type helper for nicer autocompletions

* update skill file

* add breakout schema helper for better type errors

* update breakout schema helper to accept binning and bucket

* move fields that are not used in compile and runtime to comments to shorten type errors

* enforce data truthfulness in skill

* enforce semantic layer richness in skill

* handle dimension key collisions mapped from many tables

* update skill description

* prevent hallucinated measures and breakouts

* fix field serialization

* add guidance on using filter matcher

* add skill guidance on long results and units

* accept query object in StaticQuestion and InteractiveQuestion

* extract use-metabase-query to multiple files

* update skill file to use sdk question by default

* update skill file to avoid hallucinated derived calculations

* add new functions into sandbox

* kondo and cljfmt lint fixes

* update CSP rules to allow style tags for data apps

* address review feedback on scoping and metric filters

* address code review feedback

* Add aggregations support for breakouts in query hooks (#75723)

* add aggregations support

* add additional aggregation helpers

* Add metrics support in useMetabaseQueryObject hook (#75728)

* add metric queries

* indicate metric support in skill file for useMetabaseQueryObject

* add new version for release

* fix query dataset tests

* update release workflow temporarily

* update skill description

* update skill to use more generic examples

* hotfix: allow esm usage in vite dev mode

* skill file update for table caveat

* update sandbox

* fix type errors in useMetabaseQuery

* add v0.63.2 to release workflow

* update skill file to use the 63-data-apps version

* Move data apps to the EE directory and under `data-apps` token feature (#75871)

* remove skill file alias

* Add error handling for data apps

* Add spinner while iframe is loading

* Add OSS/no token upsell banner placeholder for data apps (#75893)

* ensure a fixed height is set if container has height

* Allow including everything in data and metric library in schema endpoint (#75944)

* add filter for including library and data collections

* address review on skill file

* remove id field from rendered comment

* remove useQuestionQuery and useMetricQuery hook

* Improve env files lookup in skills (#75955)

* Allow `unsafe-eval` for data app iframe (#76004)

* Fix lint

* Fixes after rebase

* Fix duplicated test

* Improve Providers and typesafety (#75934)

* chore: reformat single expression functions to arrow

* Use entity id in questionCollections when generating typed schemas (#76230)

* allow using entity ids for question collection filter

* guide the agent towards using entity ids

* add failing test for invalid entity id

* Make custom viz work in data apps (#76237)

* Allow blob: images in data-app CSP (EMB-1923)

SDK custom visualizations load their icon assets via blob: URLs (to avoid
the cross-origin auth issue), but the /embed/data-app/ route allowed
'unsafe-eval' without the blob: scheme in img-src, so icons were blocked
under a production CSP. Add blob: to img-src scoped to the data-app iframe
route only; the main app img-src stays strict.

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

* EMB-1880: support for `providerProps`  in data apps

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Reduce schema bloat in dimensions of generated metrics when many tables are involved (#76003)

* Add useQuestionQuery SDK hook (#74900)

Co-authored-by: Timofei <timofei@metabase.com>

* Prototype schema exports and typed hooks for custom data apps (#74978)

* add prototype for schema generation

* add formatting for generated schemas

* add skill file

* useQuestionQuery should take only ID based on @sanex3339 feedback

* add segments, measures, tables and useMetabaseQuery hook

* Add useMetabaseQuery to endowments

* Fix queryDataset

---------

Co-authored-by: Timofei <timofei@metabase.com>

* [PoC] Type safety and modularity improvements for custom data apps (#75381)

* allow separate libraries in schemas

* add metric type checking

* add collections parameter

* check that segments and measures belongs to table in useMetabaseQuery

* require less type information for measures

* require less type information for segments

* use metric dataset endpoint when querying measures in metrics

* add validation for breakout buckets

* fix filter operation type scoping

* expose filter type helper for nicer autocompletions

* update skill file

* add breakout schema helper for better type errors

* update breakout schema helper to accept binning and bucket

* move fields that are not used in compile and runtime to comments to shorten type errors

* enforce data truthfulness in skill

* enforce semantic layer richness in skill

* handle dimension key collisions mapped from many tables

* update skill description

* prevent hallucinated measures and breakouts

* fix field serialization

* add guidance on using filter matcher

* add skill guidance on long results and units

* accept query object in StaticQuestion and InteractiveQuestion

* extract use-metabase-query to multiple files

* update skill file to use sdk question by default

* update skill file to avoid hallucinated derived calculations

* add new functions into sandbox

* kondo and cljfmt lint fixes

* update CSP rules to allow style tags for data apps

* address review feedback on scoping and metric filters

* address code review feedback

* Add aggregations support for breakouts in query hooks (#75723)

* add aggregations support

* add additional aggregation helpers

* Add metrics support in useMetabaseQueryObject hook (#75728)

* add metric queries

* indicate metric support in skill file for useMetabaseQueryObject

* add new version for release

* fix query dataset tests

* remove useQuestionQuery and useMetricQuery hook

* Data Apps publishing via remote repository POC (#75702)

* avoid duplicating dimension fields

namespace dimensions by table

add dimension id to mapping

pick from table fields for only allowed dimensions

fix incorrect type guards

allow querying metric using question as source

remove createMetabaseQuery from public interface

add sourceFieldId to metric

* doc generation type fixes

---------

Co-authored-by: Timofei <timofei@metabase.com>

* Add allowed hosts handling for data apps (#76094)

* Update version

* Properly include data app changes in `pull changes` modal (#76294)

* Move app template into main repo, handle skill versioning flow (#76288)

* feat(data-apps): allow passing down a card with viz settings (#75993)

support card prop in sdk question component

* Rename data app env variables and read them only in dev mode (#76604)

* Add custom data apps layout with sliding toolbar (#76600)

* Remove unused helper

* Dev env Sandboxing, Human Readable Errors and Copy Helper (#76597)

* Revert wrong rebase changes

* Improve creating filtering interfaces in custom data apps (#76401)

* update skill files to guide filter usages

* update skill file to prevent filtering mistakes

* move filtering ui patterns into a separate file

* do not treat mb_replace_me as valid creds

* use combobox for filters

* pass title false to sdk question

* combobox trigger before typing

* Add missing exported types

* Apply visualizations and visualization settings from SDK package (#76659)

* ask the agent to pull possible visualizations

* ask the agent to use card prop

* teach the agent when to use viz settings

* Support query-only SDK cards

* Remove cross references between data apps skills (#76661)

Remove data app skill cross references

* Move Data App Dev Vite App setup into SDK package (#76695)

* Don't navigate on ad-hoc question chart clicks (#76751)

* Fix sandboxing crashes and update skill file mistakes (#76858)

Fix data app sandboxing and skill guidance

* Block form submissions in data apps via CSP (#76882)

* Fix SDK theming hover text colors in generated data apps (#77083)

update guidance on colors

* Remove SDK question query prop for data apps use cases (#77080)

* Remove SDK question query prop

* update tests to use card prop instead

* Adopt Lib.createTestQuery for table sources when querying in data apps (#75957)

* Add base metabase-lib query implementation

* switch to Lib.createTestQuery DSL

* Load query metadata at runtime for metabase-lib instead of hard-coding (#76874)

* load query metadata dynamically

* accept id-only source references

* Wait for SDK login before resolving query metadata

* refactor: use useAsyncFn to handle states

* pass source-name to createTestQuery

* Add metric aggregations for useMetabaseQuery via Lib.createTestQuery (#76947)

* add metric sources for useMetabaseQuery via Lib.createTestQuery

* use MetabaseQueryObject instead of opaque type marker

* Fix metric schema and join source validation

* add metric library filtering to skill files

* make metric an aggregation expression instead of join sources

* mark tableId as never in joined fields

* export public MetabaseQueryObject type

* use camelCase for source name and field id

* Add initial data app e2e setup (#77101)

* Add sorting and fields to useMetabaseQuery via Lib.createTestQuery (#76966)

* add sorting, limiting and fields to useMetabaseQuery

* refactor: cleanup sorting and aggregations

* Fix metric order by validation

* Support saved questions for useMetabaseQuery via Lib.createTestQuery (#77073)

* add saved questions as useMetabaseQuery input

* remove extra operators for saved cards

* Add unit fe/be tests for data apps (#76979)

* Fix lint

* Use modern api in query-dataset.ts

* Fix BG color

* Fix execute-action tests

* Update SDK-data-apps version

* Move migrations to v64 folder

* Adjust router components to use metabase/router

* Allow opening a data app for any signed user

* Rename data app skills (#77266)

* Update DataAppLayout.tsx `go back` button logic

* Harden data apps skills and action validation errors (#77261)

* harden data apps skills and actions error validation

* do not display models as questions in data apps

* clarify action result types

* Agents should report errors when models, actions or questions cannot be schema generated (#77260)

* return an error if models or saved questions are invalid

* return an error if actions are invalid

* update skill file guidance on model generation

* fix model boundary

* Add data apps admin UI + routing/sandboxing/isolation e2e (#77271)

* Rename data-apps paths to /apps (#77298)

* Include all action-included models in the typed schema when a mutation is specified (#77291)

* add includeModels=true flag to include all models

* omit model columns and models without actions

* remove extra metadata

* Adjust admin UI for Data Apps (#77420)

* Add more e2e/unit tests for Data Apps (#77524)

* Update bun.lock

* Fix errors after rebase

* Parallelize dts rollup (#77663)

* revert EMB-1840 measures support in metric dataset endpoint

* chore: add data app module (#77705)

* test ci

* apply code review suggestion

* Setup separate entries for client and node dev data app entries (#77687)

* Fix type casts lint errors (#77696)

* Adjust data apps setup + add proper analytics setup (#77604)

* Extract common and scope helpers + tests for typed schema module (#77403)

* refactor: extract common and query param helpers

* refactor: use kebab-case for query params

* refactor: cleanup schema mapping

ci: fix module config

* refactor: rename schema scope helpers to scope.clj

* refactor: extract scope_test.clj

* Extract typed schema rendering helpers to render.clj (#77703)

* extract schema render function

* move runtime compact test out

* remove javascript and json endpoints from schema generator

* refactor: cleanup javascript.clj and render.clj

* Extract typed schema generators for tables and questions (#77729)

* refactor: extract schema generator modules

* remove question-details from model-schemas

* fix: batch queries in table and question schema generator

* remove N+1 in typed schema field table lookup

* Extract typed schema generators for models and actions (#77739)

* extract model schema generator

* batch model action querying to avoid n+1 queries

* Extract typed schema generator for metrics (#77806)

* Extract typed schema generators for models and actions (#77739)

* extract model schema generator

* batch model action querying to avoid n+1 queries

* extract metric schema generator

* test: simplify fields in metric_test fixtures

* refactor: clean up metric schema generation

* remove n+1 from metric field dimension read

* disable default temporal dimension breakout

* surface errors if metric schema cannot be built

* revert metrics/api.clj to master to remove outdated comments

* Fix routes

* Take a data app's slug from its directory name (#77688)

* Merge data-apps specific migrations into one

* ci: fix failing backend test and cljfmt lint

* Add a beta badge in data apps admin page heading (#77877)

marks the data app as beta

* Revert "Merge data-apps specific migrations into one"

This reverts commit 383a5fa28d.

* Revert "Revert "Merge data-apps specific migrations into one""

This reverts commit 948d904b62.

* ci: actionlint fix

* Fix data apps route

* Use getSubpathSafeUrl in attachIframeUrlMirror

* Fix comment

* Do not send apps with sync error for token check

* Add data app listing sidebar section for non-admins (#77977)

* show data apps in sidebar for non-admins

* add app icon to data apps

* use available=true to filter no sync error and enabled apps

* fix navbar keys in data apps

* test: use non-admin user to assert non-admin response

* fix toucan query in data apps listing endpoint

* re-export entity detail helpers and result column to metabot core

* open data apps in new tab from the sidebar

* Prevent race conditions in responses of useMetabaseQuery hook (#78023)

prevent stale responses in useMetabaseQuery

* ci: remove tools.util from module declaration

* Merge migrations into a single one

* reset release-embedding-sdk.yml to master

---------

Co-authored-by: Phoomparin Mano <poom@metabase.com>
Co-authored-by: Phoomparin Mano <phoomparin@gmail.com>
Co-authored-by: Nicolò Pretto <info@npretto.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:42:50 +01:00
Egor Grushin
8dd86422fe GDGT-2577 Replace deprecated SchedulePicker component (#77911)
* Replace deprecated Schedule

* addressing styling issues

* addressing typing issues

* addressing missing tests

* addressing converting the component to an until

* addressing cron test coverage

* fix e2e subscription tests for Schedule migration

- target Mantine Select via data-testid (select-frequency/select-frame)
  instead of findByDisplayValue (dupe visible+hidden inputs)
- lowercase frequency option labels
- assert token popover not.exist (unmounts on Escape)

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 17:23:59 +02:00
Fraser
f051eb94a5 Compile e2e CLJS in parallel with the uberjar build (#78017) 2026-07-18 15:12:22 +12:00
Timothy S. Dean
3df3238697 Consistency pass over database routing invariants (#78066)
Some cleanup of some false alarms I encountered in test environments while working on a different issue. Destination databases (those with `router_database_id` set) are reached only through their router: metadata, sync, permissions, etc. Only execution goes against the destination's connection. A `data_permissions` row pointing at a destination is never consulted -- it's already actively stripped.

There are test utilities that helpfully create in-test permissions for all databases created, so in a test environment it would look exactly like the invariant was being violated, but that was only because the test utilities deviated from production behavior. This brings the test behaviors into line with production (no permissions get written).

There should be no change in production behavior in this PR, but as a bonus, I figured that a safety guard filtering rows out on reads is not as good as filtering them out on reads *and* failing loudly on writes. I also added guards that a normal database can't be turned into a destination database, which was a behavior tests were relying on for easier setup (but again, the tests then deviated once more from production behavior).
2026-07-17 20:39:45 -06:00
Rodrigo López Dato
3a00f09bfd Disable Liquibase usage analytics (#77992)
Since 4.28 Liquibase ships an opt-out analytics feature that, on the first
migration run, fetches config from config-endpoint.liquibase.com and sends
anonymized usage events.
2026-07-17 23:28:40 +00:00
github-automation-metabase
f5bbb2bfde Auto-update documentation for master (#78062)
Update generated docs (master)

Co-authored-by: github-actions <github-actions@github.com>
2026-07-17 21:27:21 +00:00
Mihael Konjević
898f75a2b1 Parse :absolute-datetime string literals in repr resolve (#76436)
* Parse :absolute-datetime string literals in repr resolve

Metabot's representations repair pass wraps bare ISO `between` bounds as
`[:absolute-datetime {} "2024-01-01" :day]` to satisfy the lib `:between`
temporal schema, leaving the literal as a string. That form is schema-valid
(so it passes construct_notebook_query validation), but the JVM query
processor expects a real java.time value — optimize-temporal-filters throws
on a bare string, and the QP's own wrap-value-literals only parses temporal
strings that arrive as raw values, not ones already wrapped in
:absolute-datetime.

Parse those string literals into java.time values in resolve-query, where the
portable repr form becomes QP-ready pMBQL, alongside the existing
annotate-field-types pass.

* Address review: tighten docstrings, use update over assoc

- parseable-temporal-string?: contract-first two-line docstring
- parse-absolute-datetime-literals: move the "why" into inline comments at
  the walk, keep the docstring to contract + idempotency
- use (update node 2 u.date/parse) instead of assoc

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

* Address review: year/year-month, fail-fast, export round-trip

- parse year/year-month :absolute-datetime literals too (u.date/parse maps
  them to LocalDate), so they are QP-ready instead of crashing later
- fail fast on a structurally-ISO but invalid literal (e.g. "2024-13-45"):
  re-throw as an :agent-error? ex-info so the LLM gets a clean message
- export: format java.time values back to ISO-8601 strings in
  portable-repr-form so resolve -> export round-trips to the documented
  string form
- tests: per-shape parsing (date/datetime/offset/year/year-month),
  idempotency, invalid-literal, and export round-trip

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

* Move :absolute-datetime string parsing to the QP layer

Revert the resolve-layer parsing and instead teach wrap-value-literals to
parse string-valued :absolute-datetime clauses, not just raw values. The
repair pass wraps bare `between` bounds as `[:absolute-datetime {} "2024-01-01"
:day]`; wrap-value-literals previously only parsed temporal strings that
arrived as raw values, so a pre-wrapped string slipped through and
optimize-temporal-filters threw on the bare string.

The QP is the right layer: wrap-value-literals runs at execution time, where
both the report timezone (bound) and the comparison field's type are
available — so the parse is type- and timezone-aware for free, and the stored
query keeps its portable ISO-string form (no export round-trip to invert).

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

* Fail fast on malformed :absolute-datetime literals in resolve

Add a resolve-side validation pass that rejects temporal literals which pass
the schema's structural regex but are not real dates (e.g. "2024-13-45"), so
the agent gets an actionable error at construct time instead of a failure at
query execution.

Validation only — it does not coerce. The string -> java.time coercion stays
in the QP's wrap-value-literals, where the field type and report timezone are
available. Accepts every valid :absolute-datetime shape: date, datetime,
offset-datetime, year, and year-month.

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

* Preserve unit and wrap mixed :between bounds for string :absolute-datetime

When re-parsing a string-valued :absolute-datetime in wrap-value-literals:

- Carry the clause's own temporal unit through instead of discarding it. The
  comparison field's bucket still wins, but for an unbucketed field the literal's
  unit is now the fallback (rather than :default), so a year/year-month literal
  like [:absolute-datetime {} "2024" :year] keeps its bucket instead of silently
  collapsing to a single instant.
- Wrap each :between bound independently so a mixed bound (string :absolute-datetime
  on one side, :now / :relative-datetime / an already-parsed :absolute-datetime on
  the other) still coerces the wrappable side instead of falling through unwrapped.

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

* Guard comparison literals and tidy :absolute-datetime validation

- wrap-value-literals: guard the field position in the :=/:!=/<…> arms so a
  comparison with a wrappable literal on both sides can't bind a literal as the
  field and leave it unparsed; it now falls through unchanged.
- resolve: validate :absolute-datetime literals via lib.walk/walk-clauses
  (no tree rebuild, shares clause-of-type? detection with the QP) instead of
  clojure.walk/postwalk.
- Move cross-subsystem rationale out of the string-valued-absolute-datetime
  docstring into an inline comment.
- Add a test covering the two-literal comparison fall-through.

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

* Parse fieldless :absolute-datetime comparisons and bound bad-literal errors

Wrap a string-valued :absolute-datetime on both sides of a comparison
(no field to wrap against) so each literal is parsed with its own unit
instead of leaving a bare string for later middleware to choke on.

Truncate the untrusted literal before it lands in the agent-facing
exception message and ex-data, so a pathological value can't flood logs.

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

* Treat :default field unit as unbucketed for :absolute-datetime literal fallback

A temporal field can carry :temporal-unit :default (add-default-temporal-unit
stamps it on :temporal/requires-default-unit drivers, and an LLM can author it
directly). The unit-fallback guard only skipped a nil unit, so a :default field
dropped the literal's own unit and collapsed e.g. [:absolute-datetime {} "2024"
:year] to a single :default-bucketed instant. Treat :default as unbucketed too.

* Validate :absolute-datetime literals with plain walk to keep resolve gate friendly

validate-absolute-datetime-literals walked the query with lib.walk/walk-clauses,
a mu/defn whose input schema validates the whole query against ::lib.schema/query.
Under test instrumentation that threw a raw "Invalid input" error on an otherwise-
malformed query (e.g. an :offset in :expressions), pre-empting the friendlier
query-not-runnable agent error the downstream canRun gate is meant to surface.
Use clojure.walk/postwalk and a bare [:absolute-datetime _ s _] shape check, matching
the sibling annotate-field-types pass, so only the literals are inspected.

* Use match/match-many for :absolute-datetime literal validation, add mixed-between raw-value test

Addresses PR review feedback: swap the manual walk/postwalk in
validate-absolute-datetime-literals for match/match-many, and cover a
:between with a raw (unwrapped) string bound plus a :relative-datetime
bound in wrap_value_literals_test.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 23:16:14 +02:00
Alexander Polyankin
05fa507ed7 Workspaces cleanup (#77875) 2026-07-17 17:08:49 -04:00
Nathan Voxland
d1f282210c Improving LLM error handling (#78003)
- Handle mid-stream failures better
- Better retry detection + handling
- Detect invalid json better
2026-07-17 15:17:44 -05:00
Eric Normand
f0ce91f74c Fix Remote Sync push hang: honest stalled UI + meaningful export progress (GHY-4132) (#77904)
* Show honest "still working" state when a Git sync push stalls (GHY-4132)

A large push can stop reporting progress while the export is still running; the
backend then reports the task as :timed-out with ended_at nil. The push modal
treated that as neither success nor error and stayed frozen on "Exporting
content…" until the page was refreshed.

Render a distinct stalled state instead: drop the frozen progress bar and show
"Still working. No update for N minutes." (internationalized, singular/plural
aware). The counter derives from last_progress_report_at and stays fresh via the
existing 2s current-task poll — no separate ticker.

* Add throttled monotonic progress reporter for remote-sync tasks (GHY-4132)

* Ramp remote-sync export progress across plan + serialize phases (GHY-4132)

* Report remote-sync export commit checkpoint before push (GHY-4132)

* Report remote-sync push progress via JGit ProgressMonitor (GHY-4132)

* Fix throttle, dead 0.66 write, and push liveness in export progress (GHY-4132)

commit-staged! was passed a pre-forced report closure, so every push
ProgressMonitor tick bypassed the 10s throttle and hammered the DB. Force only
the one-shot commit checkpoint (0.8) explicitly in finish-commit!, and let
push ticks go through the throttled reporter.

The forced 0.66 serialize-boundary write ran after commit-staged! returned,
by which point the 0.8 checkpoint (and push ticks) had already advanced the
monotonic reporter past it, so it was silently dropped. Move it to the end of
each export's stage-fn, before the checkpoint/push run.

The push monitor only reported during the exactly-titled "Writing objects"
phase with a known positive total; a non-English JVM locale renames that
title and JGit may report an unknown (zero) total, so the push phase could go
silent for its whole duration and re-trip the stale-progress timeout. Report
on every update tick regardless of phase/total, falling back to the 0.8
push-start fraction — repeated equal values are monotonic no-ops but still
bump last_progress_report_at, throttled to at most once per 10s.

* Use dayjs diff for the stalled-progress minute counter (GHY-4132)
2026-07-17 20:12:29 +00:00
Nick Fitzpatrick
a8234bd97c Overhaul pie chart static viz. Fix leaking HSLA values (#77303)
* Overhaul pie chart static viz. Fix leaking HSLA values

* PR feedback:

* label column adjustment to match previous behavior

* fe lint
2026-07-17 16:35:43 -03:00
Jeff Bruemmer
55e1aa315d docs - 2FA (#77883) 2026-07-17 15:06:45 -04:00
Eric Normand
8c87c3489a Expose search's ranked set and page-hydration as separate steps (#78005)
* Expose search's ranked set and page-hydration as separate steps

`search` already ran in two stages: rank the permission-filtered result set
(capped at max-filtered-results), then slice that set to the requested page
and hydrate only those rows. The comment on `search-results` spells out why
the slice comes first — hydration does per-row work, including the expensive
`can_write` check, so it should only touch rows that are actually returned.

Both stages were sealed inside `search`, so a caller holding several ranked
sets had no way to combine them and paginate the combination. Its only option
is to ask `search` for N hydrated pages and merge those, which hydrates N
pages to return one, and — because each page is sliced from its own ranking
before merging — cannot page the merged order correctly at all.

Split the two stages into `ranked-results` and `search-results` and export
both from the module API. `search` is now their composition and behaves
identically; no call site changes.

The motivating caller is metabot's multi-query search, which runs a search per
query and fuses the rankings with RRF. Fusing at the ranked-results seam lets
it hydrate only the page it returns, page the fused order coherently, and
report a total for the fused set.

* Remove comments from search core.clj

Removed comments explaining the purpose of `ranked-results` and `search-results`.

* kick ci
2026-07-17 13:53:48 -05:00
Rodrigo López Dato
7297aaaa8a Clarify what SLO does (#77761) 2026-07-17 14:40:10 -03:00
Jeff Bruemmer
eb8d806ffe docs - custom viz sdk (#77974) 2026-07-17 12:53:23 -04:00
Andrei Chernykh
f08dbabf1b Extract Table viz definition into a typed definition file (#77983) 2026-07-17 09:40:50 -07:00
Nick Fitzpatrick
1ec6f1d2d3 Add missing monthsListControl style to DateInput (#77627) 2026-07-17 13:24:30 -03:00
Andrei Chernykh
8fd7008e90 Convert sandboxes and advanced_permissions plugin modules to TypeScript (#77487)
* Convert sandboxes and advanced_permissions plugin modules to TypeScript

* Address review comments: remove typecasting from sandboxes actions and unit spec
2026-07-17 09:16:29 -07:00
Timur Mashara
144a139152 Revert 'GRO-554 update storage upsell (#76052)' (#78034) 2026-07-17 12:08:44 -03:00
Sébastien
d7a87295df Add pagination and sorting to MCP analytics tool calls (#77599) 2026-07-17 16:42:55 +02:00
Nick Fitzpatrick
cd9d166077 Fix trend viz in FE PDFs (#77612) 2026-07-17 11:08:38 -03:00
Sébastien
95267311e5 Hide dashboard questions in CollectionBrowser by default (#77941) 2026-07-17 14:35:14 +02:00
Cam Saul
74af0a368d [MongoDB] remove :metadata/key-constraints driver feature flag (#77659)
[MongoDB] remove `:metadata/key-constraints` driver feature flag (incorrectly set)
2026-07-17 12:52:04 +02:00
3719 changed files with 133464 additions and 74021 deletions

View File

@@ -13,7 +13,7 @@ allowed-tools: Read, Grep, Bash, Glob, Skill
**Primary standard: the [`typescript-write`](../typescript-write/SKILL.md) skill.** Load it first — it defines the authoring rules this review enforces, alongside `frontend/CLAUDE.md` and `docs/developers-guide/frontend.md`.
Adherence to `typescript-write` is the **highest-priority** review dimension: rank any violation of its provisions above all other findings. Treat its **no-`any` hard rule** (no explicit *or* implicit `any` in new code) as **blocking**, and verify it with the LSP rather than by eye.
Adherence to `typescript-write` is the **highest-priority** review dimension: rank any violation of its provisions above all other findings. Treat its **no-`any` hard rule** (no explicit *or* implicit `any` in new code) as **blocking**. Use TypeScript LSP tools to inspect inferred types when available; otherwise rely on type-checking and linting.
Review in this priority order:

View File

@@ -13,7 +13,7 @@ description: Write TypeScript and JavaScript code following Metabase coding stan
- **New code must not introduce `any`, explicit or implicit.** No `any` annotations, no `as any` / `as unknown as`, no untyped parameters or returns that infer `any`, no implicitly-`any` destructures or array/object literals.
- **Untyped third-party / boundary values** must be typed at the boundary (a declared type, `unknown` + type guard, or a small typed wrapper) — never let `any` propagate inward.
- **Mandatory LSP verification.** After writing or editing any TS/TSX, inspect the changed symbols with the TypeScript Language Server (hover to read inferred types; `goToDefinition` to confirm sources) and run the project type-check.
- **Mandatory type verification.** Before finishing a TS/TSX change, run `bun run type-check-pure`. If TypeScript LSP tools are available, also inspect changed symbols with hover and go-to-definition; otherwise skip LSP check.
## Type tightening
@@ -26,6 +26,7 @@ description: Write TypeScript and JavaScript code following Metabase coding stan
- **Avoid non-null assertions (`!`)**. Prefer a guard, early return, or `?.`. Use `!` only when non-nullness is provably true and localized, with a comment.
- **No redundant runtime coercion** — don't wrap already-typed values in `Number()` / `String()` / `Boolean()`.
- **Type guards belong in `frontend/src/metabase-types/guards/`**. Do not redefine them locally.
- **A cast you can't avoid needs a real justification comment.** The `metabase/no-unjustified-type-casts` rule accepts any preceding comment — state the actual reason the cast is safe. NEVER write the legacy `// Unjustified type cast. FIXME` placeholder; it exists only on casts that predated the rule, and copying it sneaks an unjustified cast past the linter. If you can't articulate why the cast is correct, the cast is wrong — fix the types.
## Type modeling

View File

@@ -179,6 +179,8 @@
clojure.core.async/take!
clojure.core.async/to-chan!
clojure.core.async/to-chan!!
;; only writes .clj-kondo/ratchets.edn, from a :once fixture that finishes before the parallel tests
dev.kondo-ratchet/fix!
metabase.ai-tracing.core/record!
metabase.api.macros.defendpoint.tools-manifest/assert-optional-fields-nullable!
metabase.channel.core/send!
@@ -298,7 +300,10 @@
metabase.query-processor.util/normalize-token {:namespaces ["metabase.*"]}}}
:discouraged-var
{clojure.core/print {:message "Use metabase.util.log instead of clojure.core/print"}
{metabase-enterprise.entity-retrieval.core/search-unfiltered {:message "Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
metabase.entity-retrieval.core/search-unfiltered {:message "Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
metabase.entity-retrieval.mirror/search-unfiltered {:message "Permission bypass risk: library-index hits are unfiltered — resolve each entity and drop what the current user can't read"}
clojure.core/print {:message "Use metabase.util.log instead of clojure.core/print"}
clojure.core/println {:message "Use metabase.util.log instead of clojure.core/println"}
clojure.core/printf {:message "Use metabase.util.log instead of clojure.core/printf"}
clojure.core/prn {:message "Use metabase.util.log instead of clojure.core/prn"}
@@ -457,6 +462,7 @@
metabase-enterprise.mfa.enrollment-flow-test/with-fresh-user-session! clojure.core/fn
metabase-enterprise.mfa.login-flow-test/with-enrolled-rasta! clojure.core/fn
metabase-enterprise.mfa.recovery-codes-test/with-confirmed-enrollment! clojure.core/fn
metabase-enterprise.remote-sync.worktree-pull-test/with-worktree clojure.core/let
metabase-enterprise.serialization.metadata-file-import.wire-format-test/with-tiny-fixture! clojure.core/fn
metabase-enterprise.serialization.test-util/with-dbs clojure.core/fn
metabase-enterprise.serialization.test-util/with-random-dump-dir clojure.core/let
@@ -465,6 +471,8 @@
metabase.actions.test-util/with-actions clojure.core/let
metabase.transforms.test-util/with-transform-cleanup! clojure.core/let
metabase.api.common/let-404 clojure.core/let
metabase.app-db.connection/with-unshared-connection clojure.core/fn
metabase.app-db.core/with-unshared-connection clojure.core/fn
metabase.app-db.custom-migrations.util/with-temp-scheduler! clojure.core/fn
metabase.app-db.custom-migrations/define-migration clj-kondo.lint-as/def-catch-all
metabase.app-db.custom-migrations/define-reversible-migration clj-kondo.lint-as/def-catch-all
@@ -524,6 +532,7 @@
metabase.test/with-temp-empty-app-db clojure.core/let
metabase.test/with-temp-file clojure.core/let
metabase.test/with-user-in-groups clojure.core/let
metabase.transforms-rest.api.transform-runs-test/with-runs-fixture! clojure.core/fn
metabase.upload.impl-test/with-upload-table! clojure.core/let
metabase.util/for-map clojure.core/for
metabase.util.files/with-open-path-to-resource clojure.core/let
@@ -622,6 +631,7 @@
metabase.lib.test-util.macros/mbql-query hooks.metabase.test.data/mbql-query
metabase.lib.test-util.macros/mbql-5-query hooks.metabase.test.data/mbql-query
metabase.lib.test-util.macros/with-testing-against-standard-queries hooks.metabase.lib.test-util.macros/with-testing-against-standard-queries
metabase.metrics.api-dimension-test/with-seeded-metric hooks.common/with-one-binding
metabase.model-persistence.api-test/with-persistence-setup! hooks.common/with-one-top-level-binding
metabase.model-persistence.api-test/with-setup! hooks.common/with-one-top-level-binding
metabase.model-persistence.test-util/with-persistence-enabled! hooks.metabase.model-persistence.test-util/with-persistence-enabled!
@@ -736,6 +746,7 @@
clojurewerkz.quartzite.schedule.simple/schedule macros.quartz/simple-schedule
clojurewerkz.quartzite.triggers/build macros.quartz/build-trigger
metabase-enterprise.database-routing.e2e-test/with-temp-dbs! macros.metabase-enterprise.database-routing.e2e-test/with-temp-dbs!
metabase-enterprise.database-routing.e2e-test/with-routing-setup! macros.metabase-enterprise.database-routing.e2e-test/with-routing-setup!
metabase-enterprise.impersonation.util-test/with-impersonations! macros.metabase-enterprise.impersonation.util-test/with-impersonations!
metabase-enterprise.query-reference-validation.api-test/with-test-setup! macros.metabase-enterprise.query-reference-validation.api-test/with-test-setup!
metabase-enterprise.sandbox.test-util/with-gtaps! macros.metabase-enterprise.sandbox.test-util/with-gtaps!

View File

@@ -55,9 +55,9 @@
;; Set of `:model/X` keywords defined by this module that it allows others to reference. By default all are private.
;; If you know what you're doing, we support an :any keyword, but let's prefer explicit sets for now.
;;
;; enterprise/workspaces
;; transforms
;; {...
;; :model-exports #{:model/Workspace :model/WorkspaceMergeTransform}}
;; :model-exports #{:model/Transform :model/TransformRun}}
;;
;; ## `model-imports`:
;;
@@ -106,11 +106,11 @@
parameters
queries
query-processor
remote-sync
search
settings
util
warehouses
workspaces}
warehouses}
:model-exports #{:model/Action}
:model-imports #{:model/Card
:model/Collection
@@ -147,6 +147,7 @@
config
events
models
permissions
util}
:model-imports #{:model/Card
:model/Collection
@@ -157,8 +158,7 @@
agent-api
{:team "Metabot"
:api #{metabase.agent-api.api
metabase.agent-api.init}
:api #{metabase.agent-api.api metabase.agent-api.init metabase.agent-api.usage}
:uses #{ai-tracing
api
auth-identity
@@ -170,6 +170,7 @@
lib-be
llm
metabot
premium-features
queries
query-permissions
query-processor
@@ -184,7 +185,8 @@
:model/DashboardTab
:model/Database
:model/Table
:model/User}}
:model/User}
:model-exports #{:model/AgentApiCallLog}}
agent-lib
{:team "Metabot"
@@ -632,6 +634,7 @@
permissions
premium-features
queries
remote-sync
request
revisions
settings
@@ -723,6 +726,7 @@
public-sharing
queries
query-processor
remote-sync
search
settings
staleness
@@ -823,6 +827,7 @@
queries
query-permissions
query-processor
remote-sync
revisions
search
util
@@ -851,10 +856,8 @@
metabase.driver.sql-jdbc.execute
metabase.driver.sql-jdbc.execute.diagnostic
metabase.driver.sql-jdbc.sync
;; TODO (lbrdnk 2025/12/17): We should revisit addition of the following line. I've added it to make
;; tests pass as we are using it directly with ee/workspaces module. I believe later, when workspaces
;; are in better state quality wise, it should become part of the driver api and that should be used
;; instead of calling directly into the normalize module.
;; TODO (lbrdnk 2025/12/17): We should revisit addition of the following line. Ideally this would
;; become part of the driver api rather than callers reaching directly into the normalize module.
metabase.driver.sql.normalize
metabase.driver.sql.query-processor
metabase.driver.sql.util
@@ -878,10 +881,7 @@
tracing
util
warehouse-schema
warehouses
;; `metabase.driver.sync/include-schema?` skips workspace iso schemas
;; on parent (non-workspace-mode) instances. See GHY-3489.
workspaces}
warehouses}
:model-imports #{:model/Database :model/Field :model/Table}}
driver-api
@@ -1048,6 +1048,7 @@
health-inspector
{:team "Querying Platform"
:api #{metabase.health-inspector.api
metabase.health-inspector.core
metabase.health-inspector.init}
:uses #{api
lib
@@ -1303,7 +1304,8 @@
metabase.metabot.core
metabase.metabot.init
metabase.metabot.self
metabase.metabot.settings}
metabase.metabot.settings
metabase.metabot.tools.entity-details}
:friends #{agent-api
enterprise/metabot
enterprise/metabot-analytics
@@ -1323,11 +1325,13 @@
documents
driver
entity-retrieval
events
interestingness
legacy-mbql
lib
lib-be
llm
metrics
models
native-query-snippets
notification
@@ -1335,6 +1339,8 @@
permissions
premium-features
pulse
queries
query-permissions
query-processor
request
search
@@ -1360,6 +1366,7 @@
:model/DashboardCard
:model/DashboardTab
:model/Database
:model/Document
:model/Field
:model/Glossary
:model/Measure
@@ -1376,21 +1383,18 @@
metabase.metrics.core}
:uses #{api
collections
events
lib
lib-be
lib-metric
models
parameters
permissions
premium-features
query-processor
request
server
util}
:model-imports #{:model/Card
:model/Field
:model/Measure
:model/Table}}
:model-imports #{:model/Card :model/Field :model/Measure}}
model-persistence
{:team "Graphy"
@@ -1413,8 +1417,7 @@
task
task-history
tracing
util
workspaces}
util}
:model-exports #{:model/PersistedInfo}
:model-imports #{:model/Card :model/Database}}
@@ -1444,6 +1447,7 @@
:model/Field
:model/Measure
:model/NativeQuerySnippet
:model/RemoteSyncWorktreeRemapping
:model/Segment
:model/Table
:model/User}}
@@ -1584,6 +1588,7 @@
{:team "UX West"
:api #{metabase.permissions.core
metabase.permissions.init
metabase.permissions.metric
metabase.permissions.schema}
:uses #{api
app-db
@@ -1608,6 +1613,7 @@
:model/PermissionsRevision}
:model-imports #{:model/Collection
:model/Database
:model/Field
:model/Table
:model/User}}
@@ -1622,7 +1628,9 @@
request
settings
util}
:model-imports #{:model/DataPermissions
:model-imports #{:model/Card
:model/Dashboard
:model/DataPermissions
:model/Database
:model/PermissionsGroup
:model/PermissionsGroupMembership
@@ -1786,6 +1794,7 @@
pulse
query-permissions
query-processor
remote-sync
request
search
settings
@@ -1874,7 +1883,6 @@
metabase.query-processor.interface
metabase.query-processor.metadata
metabase.query-processor.middleware.constraints
metabase.query-processor.middleware.enterprise
metabase.query-processor.middleware.permissions
metabase.query-processor.parameters.dates
metabase.query-processor.parameters.operators
@@ -1909,6 +1917,7 @@
legacy-mbql
lib
lib-be
lib-metric
model-persistence
models
parameters
@@ -1923,8 +1932,7 @@
system
tracing
users
util
workspaces}
util}
:model-imports #{:model/Card
:model/DashboardCardSeries
:model/Database
@@ -1938,7 +1946,8 @@
metabase.remote-sync.init}
:uses #{api
models
premium-features}
premium-features
util}
:model-imports #{:model/Collection}}
request
@@ -2011,6 +2020,7 @@
metabase.search.core
metabase.search.engine
metabase.search.in-place.scoring
metabase.search.index-health
metabase.search.ingestion
metabase.search.init
metabase.search.scoring
@@ -2025,6 +2035,7 @@
collections
config
events
health-inspector
lib
lib-be
models
@@ -2294,8 +2305,7 @@
tracing
util
warehouse-schema
warehouses
workspaces}
warehouses}
:model-imports #{:model/Database
:model/Field
:model/FieldValues
@@ -2392,6 +2402,7 @@
collections
events
models
remote-sync
util}
:model-exports #{:model/Timeline}
:model-imports #{:model/Collection :model/User}}
@@ -2442,6 +2453,7 @@
:friends #{models
enterprise/transforms-python}
:model-exports #{:model/Transform
:model/TransformDagRun
:model/TransformJob
:model/TransformJobRun
:model/TransformJobTransformTag
@@ -2477,8 +2489,7 @@
premium-features
query-processor
sync
util
workspaces}
util}
:friends #{transforms
enterprise/transforms-python}}
@@ -2499,12 +2510,36 @@
transforms-base
util}
:model-imports #{:model/Transform
:model/TransformDagRun
:model/TransformJob
:model/TransformJobRun
:model/TransformJobTransformTag
:model/TransformRun
:model/TransformTag}}
typed-schemas
{:team "Embedding"
:api #{metabase.typed-schemas.api}
:uses #{actions
api
collections
lib
lib-be
metabot
metrics
models
permissions
system
types
util}
:model-imports #{:model/Action
:model/Card
:model/Collection
:model/Database
:model/Field
:model/Measure
:model/Table}}
types
{:team "Querying Platform"
:api #{metabase.types.core
@@ -2531,8 +2566,7 @@
settings
sync
util
warehouse-schema
workspaces}
warehouse-schema}
:model-imports #{:model/Card
:model/Database
:model/Field
@@ -2687,6 +2721,7 @@
warehouses}
:model-exports #{:model/Dimension
:model/Field
:model/FieldUserSettings
:model/FieldValues
:model/Table}
:model-imports #{:model/Card
@@ -2784,13 +2819,6 @@
:model/Field
:model/Table}}
workspaces
{:team "Graphy"
:api #{metabase.workspaces.core
metabase.workspaces.table-remapping}
:uses #{premium-features
util}}
xrays
{:team "Graphy"
:api #{metabase.xrays.api
@@ -2864,8 +2892,7 @@
setup
sync
users
util
enterprise/workspaces}
util}
:model-imports #{:model/ApiKey :model/Database :model/User}}
enterprise/advanced-permissions
@@ -2892,6 +2919,17 @@
:model/Sandbox
:model/Table}}
enterprise/agent-api
{:team "Metabot"
:api #{metabase-enterprise.agent-api.init}
:uses #{agent-api
analytics
metabot
premium-features
task
util}
:model-imports #{:model/AgentApiCallLog}}
enterprise/analytics
{:team "UX West"
:api #{}
@@ -2902,7 +2940,7 @@
enterprise/scim
enterprise/semantic-search
enterprise/sso}
:model-imports #{:model/Database :model/Sandbox :model/Workspace}}
:model-imports #{:model/Database :model/Sandbox}}
enterprise/api
{:team "UX West"
@@ -2991,20 +3029,6 @@
:model/QueryCache
:model/QueryExecution}}
enterprise/checker
{:team "Graphy"
;; todo: add ability to prevent usage (transitive even) of app db
:api #{}
:uses #{classloader
lib
models
plugins
query-processor
sql-tools
util
enterprise/dependencies}
:model-imports #{:model/Database}}
enterprise/cloud-add-ons
{:team "Cloud"
:api #{metabase-enterprise.cloud-add-ons.api}
@@ -3072,6 +3096,17 @@
:api #{}
:uses #{premium-features}}
enterprise/data-apps
{:team "Gadget"
:api #{metabase-enterprise.data-apps.api
metabase-enterprise.data-apps.init
metabase-enterprise.data-apps.sync}
:uses #{api
models
premium-features
settings
util}}
enterprise/data-complexity-score
{:team "Metabot"
:api #{metabase-enterprise.data-complexity-score.api
@@ -3142,15 +3177,13 @@
settings
util
warehouse-schema
warehouses
workspaces}
warehouses}
:model-exports #{:model/DatabaseRouter}
:model-imports #{:model/Database :model/Transform}}
enterprise/dependencies
{:team "Graphy"
:api #{metabase-enterprise.dependencies.analysis
metabase-enterprise.dependencies.api
:api #{metabase-enterprise.dependencies.api
metabase-enterprise.dependencies.core
metabase-enterprise.dependencies.init
metabase-enterprise.dependencies.models.dependency}
@@ -3231,9 +3264,12 @@
{:team "Metabot"
:api #{metabase-enterprise.entity-retrieval.init}
:uses #{analytics-interface
app-db
collections
entity-retrieval
health-inspector
premium-features
search
task
util
enterprise/semantic-search}
@@ -3420,14 +3456,17 @@
task
task-history
util
enterprise/data-apps
enterprise/serialization
enterprise/transforms-python}
:model-exports #{:model/RemoteSyncTask}
:model-imports #{:model/Card
:model-exports #{:model/RemoteSyncTask :model/RemoteSyncWorktreeRemapping}
:model-imports #{:model/Action
:model/Card
:model/Collection
:model/Dashboard
:model/Document
:model/Field
:model/FieldUserSettings
:model/Measure
:model/NativeQuerySnippet
:model/PythonLibrary
@@ -3512,8 +3551,7 @@
settings
system
users
util
enterprise/serialization}
util}
:model-imports #{:model/ApiKey
:model/PermissionsGroup
:model/PermissionsGroupMembership
@@ -3554,7 +3592,9 @@
metabase-enterprise.semantic-search.core
metabase-enterprise.semantic-search.db.datasource
metabase-enterprise.semantic-search.embedding
metabase-enterprise.semantic-search.init}
metabase-enterprise.semantic-search.embedding-health
metabase-enterprise.semantic-search.init
metabase-enterprise.semantic-search.util}
:uses #{activity-feed
analytics
analytics-interface
@@ -3564,6 +3604,7 @@
config
connection-pool
events
health-inspector
lib-be
llm
metabot
@@ -3581,12 +3622,7 @@
enterprise/serialization
{:team "Graphy"
:api #{metabase-enterprise.serialization.api
metabase-enterprise.serialization.cmd
metabase-enterprise.serialization.core
metabase-enterprise.serialization.init
metabase-enterprise.serialization.schema
metabase-enterprise.serialization.v2.backfill-ids}
:api #{metabase-enterprise.serialization.api metabase-enterprise.serialization.cmd metabase-enterprise.serialization.core}
:friends #{enterprise/memoize-monitor}
:uses #{analytics
api
@@ -3602,9 +3638,7 @@
search
server
setup
sync
util
warehouse-schema}
util}
:model-imports :bypass}
enterprise/snippet-collections
@@ -3669,7 +3703,7 @@
enterprise/tenants
{:team "Admin Webapp"
:api #{metabase-enterprise.tenants.api}
:api #{metabase-enterprise.tenants.api metabase-enterprise.tenants.init}
:uses #{api
audit-app
auth-identity
@@ -3748,33 +3782,7 @@
premium-features
upload
util}
:model-imports #{:model/Database :model/Table}}
enterprise/workspaces
{:team "Graphy"
:api #{metabase-enterprise.workspaces.api
metabase-enterprise.workspaces.core
metabase-enterprise.workspaces.init}
:uses #{api
app-db
cache
driver
lib
models
permissions
premium-features
query-processor
sample-data
server
settings
sql-tools
util
workspaces
enterprise/serialization}
:model-exports #{:model/Workspace}
:model-imports #{:model/Database
:model/Table
:model/User}}}
:model-imports #{:model/Database :model/Table}}}
;; namespaces matching these patterns (with `re-find`) are excluded from the module linter. Since regex literals
;; aren't allowed in EDN just used the `(str <regex>)` version i.e. two slashes instead of one.

View File

@@ -3,3 +3,8 @@
(defmacro with-temp-dbs! [bindings & body]
`(let [~@(interleave bindings (repeat nil))]
~@body))
(defmacro with-routing-setup! [[router-binding destinations] & body]
`(let [~router-binding nil
~@(interleave (map first destinations) (repeat nil))]
~@body))

47
.clj-kondo/ratchets.edn Normal file
View File

@@ -0,0 +1,47 @@
;; Per-linter budgets for inline `:clj-kondo/ignore` forms.
;; metabase.core.kondo-ratchet-test fails when the budgets drift from the actual counts.
;; `./bin/mage fix-kondo-ratchets` lowers budgets to match the tree; local test runs do it
;; automatically. Raising a budget, or adding one for a new linter (`--seed`), is a hand edit
;; to defend in your PR.
;; :all is the vector-less ignore form, which suppresses every linter on the next form.
{:ignore-counts {:aliased-namespace-symbol 3
:all 50
:case-symbol-test 1
:clojure-lsp/unused-public-var 9
:def-fn 1
:deprecated-namespace 95
:deprecated-var 81
:discouraged-java-method 3
:discouraged-namespace 80
:discouraged-var 106
:equals-true 1
:main-without-gen-class 1
:metabase/defsetting-namespace 7
:metabase/disallow-hardcoded-driver-names-in-tests 34
:metabase/discourage-millis-duration 1
:metabase/i-like-making-cams-eyes-bleed-with-horrifically-long-tests 13
:metabase/modules 8
:metabase/namespace-name 4
:metabase/prefer-with-dynamic-fn-redefs 27
:metabase/test-helpers-use-non-thread-safe-functions 17
:metabase/validate-defendpoint-has-response-schema 441
:metabase/validate-defendpoint-query-params-use-kebab-case 40
:metabase/validate-defendpoint-route-uses-kebab-case 44
:metabase/validate-deftest 8
:missing-docstring 10
:missing-protocol-method 3
:non-arg-vec-return-type-hint 1
:reduce-without-init 3
:redundant-fn-wrapper 1
:syntax 2
:unquote-not-syntax-quoted 1
:unresolved-namespace 3
:unresolved-require 1
:unresolved-symbol 4
:unsorted-required-namespaces 1
:unused-alias 1
:unused-binding 9
:unused-excluded-var 2
:unused-namespace 1
:unused-private-var 8
:unused-value 1}}

View File

@@ -153,6 +153,7 @@
"build."
"i18n." ; bin/i18n
"lint-migrations-file-test"
"mage."
"main-test" ; bin/release-list
"metabase.deps-edn-test"
"metabase.driver."

3
.github/CODEOWNERS vendored
View File

@@ -10,6 +10,7 @@ src/metabase/query_processor @metabase/core-backend-querying-platform
src/metabase/graph @metabase/graphy
enterprise/backend/src/metabase_enterprise/dependencies @metabase/graphy
enterprise/frontend/src/metabase-enterprise/dependencies @metabase/graphy
enterprise/frontend/src/metabase-enterprise/monitor/dependency-diagnostics @metabase/graphy
# @metabase/embedding
@@ -49,5 +50,5 @@ docs/installation-and-operation/commands.md # auto-generated
src/metabase/analytics/prometheus.clj @metabase/cloud-ops
# @metabase/metabot-frontend
enterprise/frontend/src/metabase-enterprise/metabot @metabase/metabot-frontend
enterprise/frontend/src/metabase-enterprise/metabot @metabase/metabot-frontend
frontend/src/metabase/metabot @metabase/metabot-frontend

View File

@@ -1,10 +1,11 @@
name: Embedding SDK Bundle Size Check
name: Bundle Size Check
description: |
Measures app + embedding SDK bundle sizes from the current and base-ref
uberjars (reusing the builds that already ran) plus their bundle-stats
artifacts, and flags changes to the chunked SDK's reachable, gzipped size
beyond the threshold. Skips when the base-ref artifacts aren't available
rather than rebuilding from source (which would compare across pipelines).
artifacts, and flags gzipped size changes beyond the threshold for the main
app's initial bundle and the chunked SDK's reachable total. Skips when the
base-ref artifacts aren't available rather than rebuilding from source (which
would compare across pipelines).
inputs:
edition:
@@ -18,12 +19,18 @@ inputs:
required: true
outputs:
status:
description: "Bundle size status: increased, decreased, or stable"
value: ${{ steps.compare.outputs.status }}
size_change_percent:
description: "size change percent (positive = increase, negative = decrease)"
value: ${{ steps.compare.outputs.size_change_percent }}
app_status:
description: "App initial bundle status: increased, decreased, or stable"
value: ${{ steps.compare.outputs.app_initial_status }}
app_size_change_percent:
description: "App initial bundle size change percent (positive = increase, negative = decrease)"
value: ${{ steps.compare.outputs.app_initial_size_change_percent }}
sdk_status:
description: "Chunked SDK bundle status: increased, decreased, or stable"
value: ${{ steps.compare.outputs.embedding_sdk_chunked_total_status }}
sdk_size_change_percent:
description: "Chunked SDK bundle size change percent (positive = increase, negative = decrease)"
value: ${{ steps.compare.outputs.embedding_sdk_chunked_total_size_change_percent }}
runs:
using: "composite"
@@ -52,13 +59,20 @@ runs:
BASE_SHA: ${{ inputs.base-ref-commit }}
THRESHOLD: ${{ inputs.threshold-percent }}
MEASURE_BUNDLES: app,embedding-sdk-legacy,embedding-sdk-chunked
# The gated bundles, as "<bundle>/<kind>". Each one's step outputs are
# named after its key (see outputPrefix in compare.js), which is what the
# action outputs above read.
GATES: app/initial,embedding-sdk-chunked/total
run: |
set -uo pipefail
skip() {
echo "::notice::Embedding SDK bundle-size check skipped: $1"
echo "status=stable" >> "$GITHUB_OUTPUT"
echo "size_change_percent=0" >> "$GITHUB_OUTPUT"
echo "::notice::Bundle-size check skipped: $1"
for gate in ${GATES//,/ }; do
prefix=$(printf '%s' "$gate" | tr -c 'a-zA-Z0-9' '_')
echo "${prefix}_status=stable" >> "$GITHUB_OUTPUT"
echo "${prefix}_size_change_percent=0" >> "$GITHUB_OUTPUT"
done
exit 0
}
@@ -97,4 +111,4 @@ runs:
MEASURE_ROOT=temp/base node ./.github/scripts/measure-bundle-sizes.js > temp/base-sizes.json \
|| { echo "::error::Failed to measure base bundle sizes"; exit 1; }
node ./.github/actions/sdk-bundle-size-check/compare.js temp/current-sizes.json temp/base-sizes.json
node ./.github/actions/bundle-size-check/compare.js temp/current-sizes.json temp/base-sizes.json

View File

@@ -2,8 +2,11 @@
// Diffs the bundle sizes produced by measure-bundle-sizes.js for the current
// build and the base ref. Every (bundle, kind) is compared strictly against the
// same (bundle, kind) — never one bundle type against another — using gzipped
// (served) bytes. The gate is driven by the chunked SDK's reachable total, which
// is what the SDK actually delivers.
// (served) bytes.
//
// The report covers every measured bundle; the gates are the subset that drives
// PR comments, named as "<bundle>/<kind>" (e.g. "app/initial",
// "embedding-sdk-chunked/total").
//
// compareBundles is pure (data in, data out) so it can be unit-tested; main()
// is the thin I/O wrapper that reads the size files, prints the report and sets
@@ -16,14 +19,48 @@ const mb = bytes => (bytes / 1024 / 1024).toFixed(2);
const percentOf = (currentBytes, baseBytes) =>
baseBytes ? Math.trunc(((currentBytes - baseBytes) * 100) / baseBytes) : 0;
// Output names are derived from the gate key, so the workflow reads
// `app_initial_status`, `embedding_sdk_chunked_total_status`, and so on.
const outputPrefix = gateKey => gateKey.replace(/[^a-zA-Z0-9]+/g, "_");
/**
* Compare two measurement sets and decide the gate. Returns a human-readable
* `report` (always), and exactly one of:
* - `gate` { status, percent }: the chunked SDK total moved by `percent`.
* - `skip` reason: the gate is non-actionable (report stable, no comment).
* Evaluate one gate. Returns the key plus exactly one of:
* - `status` / `percent`: the bundle moved by `percent`.
* - `skip` reason: the gate is non-actionable (reported stable, no comment).
* - `error` reason: the gate inputs are missing (hard failure).
*/
function compareBundles({ current, base, threshold }) {
function evaluateGate({ current, base, threshold, gateKey }) {
const [bundle, kind] = gateKey.split("/");
const gateCurrent = pick(current, bundle, kind);
const gateBase = pick(base, bundle, kind);
if (!gateCurrent || !gateBase || !gateBase.gzipBytes) {
return { gateKey, error: `Could not find ${bundle} ${kind} (gzip) in both builds` };
}
// Both sides must measure the bundle the same way. A base ref built before the
// reachableAssets enrichment collapses its total to the initial set, so
// comparing it against the current reachable total reports a phantom jump
// (~30%). Skip until the base ref also carries reachable stats.
if (Boolean(gateCurrent.reachable) !== Boolean(gateBase.reachable)) {
return {
gateKey,
skip:
`"${gateKey}" measured differently on each side ` +
`(current reachable=${Boolean(gateCurrent.reachable)}, base reachable=${Boolean(gateBase.reachable)}); ` +
`the base ref predates the reachable-chunk stats. Resolves once this change is on the base ref.`,
};
}
const percent = percentOf(gateCurrent.gzipBytes, gateBase.gzipBytes);
const status = percent > threshold ? "increased" : percent < -threshold ? "decreased" : "stable";
return { gateKey, status, percent };
}
/**
* Compare two measurement sets and evaluate every gate. Returns a
* human-readable `report` over all measured bundles plus one entry per gate.
*/
function compareBundles({ current, base, threshold, gateKeys }) {
const report = ["=== Bundle sizes (gzipped, current vs base) ==="];
const keys = [...new Set([...current, ...base].map(keyOf))].sort();
for (const key of keys) {
@@ -39,34 +76,10 @@ function compareBundles({ current, base, threshold }) {
report.push(`${key}: ${mb(currentRow.gzipBytes)}MB vs ${mb(baseRow.gzipBytes)}MB (${percent}%, ${diff} bytes)`);
}
// Gate on the chunked SDK's reachable total (gzipped) — what the SDK ships.
const gateCurrent = pick(current, "embedding-sdk-chunked", "total");
const gateBase = pick(base, "embedding-sdk-chunked", "total");
if (!gateCurrent || !gateBase || !gateBase.gzipBytes) {
return { report, error: "Could not find embedding-sdk-chunked total (gzip) in both builds" };
}
// Both sides must measure "total" the same way. A base ref built before the
// reachableAssets enrichment collapses its total to the initial set, so
// comparing it against the current reachable total reports a phantom jump
// (~30%). Skip until the base ref also carries reachable stats (i.e. once
// this change is on the base).
if (Boolean(gateCurrent.reachable) !== Boolean(gateBase.reachable)) {
return {
report,
skip:
`chunked "total" measured differently on each side ` +
`(current reachable=${Boolean(gateCurrent.reachable)}, base reachable=${Boolean(gateBase.reachable)}); ` +
`the base ref predates the reachable-chunk stats. Resolves once this change is on the base ref.`,
};
}
const percent = percentOf(gateCurrent.gzipBytes, gateBase.gzipBytes);
const status = percent > threshold ? "increased" : percent < -threshold ? "decreased" : "stable";
return { report, gate: { status, percent } };
return { report, gates: gateKeys.map(gateKey => evaluateGate({ current, base, threshold, gateKey })) };
}
module.exports = { compareBundles };
module.exports = { compareBundles, outputPrefix };
const setOutput = (name, value) => {
if (process.env.GITHUB_OUTPUT) {
@@ -77,32 +90,42 @@ const setOutput = (name, value) => {
function main() {
const [, , currentPath, basePath] = process.argv;
const threshold = Number(process.env.THRESHOLD ?? 2);
const gateKeys = (process.env.GATES ?? "embedding-sdk-chunked/total").split(",").map(key => key.trim());
const current = JSON.parse(fs.readFileSync(currentPath, "utf8"));
const base = JSON.parse(fs.readFileSync(basePath, "utf8"));
const { report, gate, skip, error } = compareBundles({ current, base, threshold });
const { report, gates } = compareBundles({ current, base, threshold, gateKeys });
console.log(report.join("\n"));
console.log("");
if (error) {
console.error(`::error::${error}`);
for (const gate of gates) {
const prefix = outputPrefix(gate.gateKey);
if (gate.error) {
// One broken gate must not hide the others, so report them all and fail
// the step at the end.
console.error(`::error::${gate.error}`);
continue;
}
if (gate.skip) {
// Treat the gate as non-actionable: report stable so no regression comment
// is posted, mirroring the shell skip() for missing artifacts.
console.log(`::notice::Bundle-size gate skipped: ${gate.skip}`);
setOutput(`${prefix}_status`, "stable");
setOutput(`${prefix}_size_change_percent`, "0");
continue;
}
console.log(`Gate: ${gate.gateKey} ${gate.percent}% (threshold ${threshold}%) → ${gate.status}`);
setOutput(`${prefix}_status`, gate.status);
setOutput(`${prefix}_size_change_percent`, gate.percent);
}
if (gates.some(gate => gate.error)) {
process.exit(1);
}
if (skip) {
// Treat the gate as non-actionable: report stable so no regression comment
// is posted, mirroring the shell skip() for missing artifacts.
console.log(`::notice::Embedding SDK bundle-size gate skipped: ${skip}`);
setOutput("status", "stable");
setOutput("size_change_percent", "0");
process.exit(0);
}
console.log("");
console.log(`Gate: embedding-sdk-chunked total ${gate.percent}% (threshold ${threshold}%) → ${gate.status}`);
setOutput("status", gate.status);
setOutput("size_change_percent", gate.percent);
}
if (require.main === module) {

View File

@@ -0,0 +1,84 @@
const { compareBundles, outputPrefix } = require("./compare");
const SDK_GATE = "embedding-sdk-chunked/total";
const APP_GATE = "app/initial";
const total = (gzipBytes, over = {}) => ({
bundle: "embedding-sdk-chunked",
kind: "total",
gzipBytes,
reachable: true,
...over,
});
const appInitial = gzipBytes => ({ bundle: "app", kind: "initial", gzipBytes });
const compare = ({ current, base, threshold = 2, gateKeys = [SDK_GATE] }) =>
compareBundles({ current, base, threshold, gateKeys });
describe("outputPrefix", () => {
it("turns a gate key into a step-output prefix", () => {
expect(outputPrefix(APP_GATE)).toBe("app_initial");
expect(outputPrefix(SDK_GATE)).toBe("embedding_sdk_chunked_total");
});
});
describe("compareBundles", () => {
it("gates on the chunked SDK total and flags an increase past the threshold", () => {
const { gates, report } = compare({ current: [total(110)], base: [total(100)] });
expect(gates).toEqual([{ gateKey: SDK_GATE, status: "increased", percent: 10 }]);
expect(report[0]).toContain("current vs base");
});
it("flags a decrease and stays stable within the threshold", () => {
expect(compare({ current: [total(90)], base: [total(100)] }).gates[0]).toMatchObject({
status: "decreased",
percent: -10,
});
expect(compare({ current: [total(101)], base: [total(100)] }).gates[0]).toMatchObject({
status: "stable",
percent: 1,
});
});
it("evaluates each gate against its own bundle", () => {
const { gates } = compare({
current: [total(100), appInitial(110)],
base: [total(100), appInitial(100)],
gateKeys: [APP_GATE, SDK_GATE],
});
expect(gates).toEqual([
{ gateKey: APP_GATE, status: "increased", percent: 10 },
{ gateKey: SDK_GATE, status: "stable", percent: 0 },
]);
});
it("skips when the two sides measured a gate differently (reachable mismatch)", () => {
const { gates } = compare({
current: [total(130, { reachable: true })],
base: [total(100, { reachable: false })],
});
expect(gates[0].status).toBeUndefined();
expect(gates[0].skip).toContain("predates the reachable-chunk stats");
});
it("errors when a gate bundle is missing on either side, without hiding the others", () => {
const { gates } = compare({
current: [appInitial(100)],
base: [appInitial(100)],
gateKeys: [APP_GATE, SDK_GATE],
});
expect(gates[0]).toEqual({ gateKey: APP_GATE, status: "stable", percent: 0 });
expect(gates[1].error).toContain("embedding-sdk-chunked total");
});
it("reports a bundle/kind present on only one side without erroring", () => {
const { report } = compare({ current: [total(100), appInitial(50)], base: [total(100)] });
expect(report.some(line => line.includes("present only in current build"))).toBe(true);
});
});

View File

@@ -57,14 +57,12 @@ runs:
#${ORIGINAL_PULL_REQUEST_NUMBER}
> [!IMPORTANT]
> Manual conflict resolution is required.
> Checkout the branch and run \`./backport.sh\` script. Force push your changes after cherry-picking. Check the box below when done.
> Checkout the branch and run \`./backport.sh\` script. Force push your changes after cherry-picking. Mark the PR as ready for review once conflicts have been addressed.
Conflicts:
\`\`\`shell
${CONFLICT_LIST}
\`\`\`
- [ ] Conflicts resolved
END
)
else
@@ -74,7 +72,12 @@ runs:
git push -u origin ${BACKPORT_BRANCH}
BACKPORT_PR_URL=$(gh pr create --base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
PR_ARGS=(--base "${TARGET_BRANCH}" --head "${BACKPORT_BRANCH}" --label "was-backported" --assignee "${{ github.event.pull_request.user.login }}" --title "🤖 backported \"${ORIGINAL_TITLE}\"" --body "${PR_BODY}")
if [ "$CONFLICTS" -gt 0 ]; then
PR_ARGS+=(--draft)
fi
BACKPORT_PR_URL=$(gh pr create "${PR_ARGS[@]}")
BACKPORT_PR_NUMBER=${BACKPORT_PR_URL##*/}
echo "backport_pr_number=$BACKPORT_PR_NUMBER" >> $GITHUB_OUTPUT

View File

@@ -1,12 +1,11 @@
name: Notify Pull Request
description: Post a comment on the pull request, optionally replacing the previous one with the same message.
inputs:
remove-previous-comment:
required: false
type: boolean
default: false
add-new-comment:
required: false
type: boolean
default: true
message:
required: true

View File

@@ -1,65 +0,0 @@
const { compareBundles } = require("./compare");
const total = (gzipBytes, over = {}) => ({
bundle: "embedding-sdk-chunked",
kind: "total",
gzipBytes,
reachable: true,
...over,
});
describe("compareBundles", () => {
it("gates on the chunked SDK total and flags an increase past the threshold", () => {
const { gate, report } = compareBundles({
current: [total(110)],
base: [total(100)],
threshold: 2,
});
expect(gate).toEqual({ status: "increased", percent: 10 });
expect(report[0]).toContain("current vs base");
});
it("flags a decrease and stays stable within the threshold", () => {
expect(compareBundles({ current: [total(90)], base: [total(100)], threshold: 2 }).gate).toEqual({
status: "decreased",
percent: -10,
});
expect(compareBundles({ current: [total(101)], base: [total(100)], threshold: 2 }).gate).toEqual({
status: "stable",
percent: 1,
});
});
it("skips when the two sides measured total differently (reachable mismatch)", () => {
const result = compareBundles({
current: [total(130, { reachable: true })],
base: [total(100, { reachable: false })],
threshold: 2,
});
expect(result.gate).toBeUndefined();
expect(result.skip).toContain("predates the reachable-chunk stats");
});
it("errors when the gate bundle is missing on either side", () => {
const result = compareBundles({
current: [{ bundle: "app", kind: "total", gzipBytes: 1 }],
base: [total(100)],
threshold: 2,
});
expect(result.gate).toBeUndefined();
expect(result.error).toContain("embedding-sdk-chunked total");
});
it("reports a bundle/kind present on only one side without erroring", () => {
const { report } = compareBundles({
current: [total(100), { bundle: "app", kind: "total", gzipBytes: 50 }],
base: [total(100)],
threshold: 2,
});
expect(report.some(line => line.includes("present only in current build"))).toBe(true);
});
});

View File

@@ -99,7 +99,6 @@ runs:
&& steps.run-test.outcome == 'failure'
&& !(inputs.driver-status == 'info'
&& !(github.ref_name == 'master' || startsWith(github.ref_name, 'release-x')))
continue-on-error: ${{ inputs.quarantine-engine != 'ci-conductor' }}
uses: ./.github/actions/quarantine-gate
with:
adapter: backend

View File

@@ -8,8 +8,8 @@ default: &default
- "bin/**/!(*.md)"
actionlint:
- ".github/workflows/*.yml"
- ".github/workflows/*.yaml"
- added|modified: ".github/workflows/*.yml"
- added|modified: ".github/workflows/*.yaml"
shared_sources: &shared_sources
- "src/**/*.cljc"
@@ -54,11 +54,6 @@ frontend_sources: &frontend_sources
- "enterprise/frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
- "frontend/!(test)**/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
# Used only in .github/workflows/conventional-commit-pr-title-reminder.yml
frontend_embedding_sources:
- "enterprise/frontend/src/{embedding,embedding-*}/**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
- "enterprise/frontend/src/metabase-enterprise/{embedding,embedding-*}**/{!(*.spec),}.{js,jsx,ts,tsx,css}"
# Used only in .github/workflows/embedding-sdk-labels-reminder.yml
frontend_embedding_sdk_package_sources:
- "enterprise/frontend/src/embedding-sdk-package/**/!(*.spec).{js,jsx,ts,tsx,css}"

View File

@@ -1,109 +0,0 @@
const TYPES = new Set([
"feat",
"fix",
"perf",
"docs",
"style",
"refactor",
"test",
"build",
"ci",
]);
/**
* @param title {string}
*/
function isConventionalTitle(title) {
const match = /^(?<type>\w+)\((?<rawScopes>[^)]+)\):/.exec(title);
if (!match) {
return false;
}
const [, type, rawScopes] = match;
const scopes = rawScopes
.split(",")
.map((scope) => scope.trim())
.filter(Boolean);
return TYPES.has(type) && scopes.length > 0;
}
/** @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments */
async function updateCommentForEmbedding({ github, context }) {
const githubUsername = context.payload.sender.login;
const comment = `${CONVENTIONAL_COMMENT_IDENTIFIER}
@${githubUsername} You have modified embedding code. Please make sure the PR title follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) style.
Here are all the supported types:
- \`feat\`
- \`fix\`
- \`perf\`
- \`docs\`
- \`style\`
- \`refactor\`
- \`test\`
- \`build\`
- \`ci\`
Please also make sure to include a scope.
For example, these are valid PR titles:
- \`feat(sdk-bundle): Add interactive dashboard component\`
- \`fix(sdk-package): Fixed MetabaseProviderPropsStore logic\`
- \`fix(sdk-package, embed-js): Fix API\``;
return updateComment(comment, { github, context });
}
/**
* @param comment {string}
* @param {import('@types/github-script').AsyncFunctionArguments} AsyncFunctionArguments
*/
async function updateComment(comment, { github, context }) {
const owner = context.repo.owner;
const repo = context.repo.repo;
const pullRequestNumber = context.issue.number;
const comments = await github.rest.issues.listComments({
owner,
repo,
issue_number: pullRequestNumber,
});
const existingComment = getExistingConventionalCommitReminderComment(
comments.data,
);
if (existingComment) {
return await github.rest.issues.updateComment({
owner,
repo,
comment_id: existingComment.id,
body: comment,
});
} else {
return await github.rest.issues.createComment({
owner,
repo,
issue_number: pullRequestNumber,
body: comment,
});
}
}
const CONVENTIONAL_COMMENT_IDENTIFIER =
"<!---conventional comment identifier-->";
/**
* @param comments {{body: string}[]}
*/
function getExistingConventionalCommitReminderComment(comments) {
for (const comment of comments) {
if (comment.body.startsWith(CONVENTIONAL_COMMENT_IDENTIFIER)) {
return comment;
}
}
}
exports.isConventionalTitle = isConventionalTitle;
exports.updateCommentForEmbedding = updateCommentForEmbedding;

View File

@@ -1,45 +0,0 @@
const { isConventionalTitle } = require("./utils");
describe("isConventionalTitle", () => {
it("returns true for a simple conventional commit", () => {
expect(isConventionalTitle("feat: add new feature")).toBe(false);
});
it("returns true for a commit with a scope", () => {
expect(isConventionalTitle("fix(api): correct API bug")).toBe(true);
});
it("returns true for a commit with multiple scopes, comma separated", () => {
expect(isConventionalTitle("feat(build,deps): update dependencies")).toBe(
true,
);
});
it("returns true for a commit with multiple scopes, comma separated with space", () => {
expect(isConventionalTitle("feat(build, deps): update dependencies")).toBe(
true,
);
});
it("returns false for a commit with round brackets in a message", () => {
expect(isConventionalTitle("feat: (something) update dependencies")).toBe(
false,
);
});
it("returns false for missing colon", () => {
expect(isConventionalTitle("feat add feature")).toBe(false);
});
it("returns false for invalid type", () => {
expect(isConventionalTitle("invalidType: some message")).toBe(false);
});
it("returns false for empty message", () => {
expect(isConventionalTitle("feat:")).toBe(false);
});
it("returns false for empty input", () => {
expect(isConventionalTitle("")).toBe(false);
});
});

7
.github/team.json vendored
View File

@@ -124,6 +124,13 @@
"losrebellos",
"tmashara"
]
},
{
"name": "Security Engineering",
"label": ".Team/SecurityEngineering",
"members": [
"enthym-aa"
]
}
]
}

View File

@@ -29,6 +29,7 @@ jobs:
filters: .github/file-paths.yaml
list-files: shell
- name: Setup actionlint
if: steps.changes.outputs.actionlint == 'true'
id: download
shell: bash
run: |
@@ -39,4 +40,5 @@ jobs:
bash <(curl -fsSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.12/scripts/download-actionlint.bash)
fi
- name: Run actionlint
if: steps.changes.outputs.actionlint == 'true'
run: ${{ steps.download.outputs.executable }} --color --verbose ${{ steps.changes.outputs.actionlint_files }}

View File

@@ -1,21 +1,172 @@
name: AI Benchmark
# Runs the ai-benchmark E2E test suite against a Metabase instance.
#
# NOTE: This is a stub. The real workflow — which builds the benchmark Docker
# image, boots Metabase + Postgres with a preloaded data dump, runs the
# benchmark suite, and uploads results as artifacts — is being developed on a
# feature branch. This stub exists on master so that the "Run workflow" button
# is available in the GitHub Actions UI, which lets us dispatch the real
# workflow against feature branches during development.
run-name: "dispatch evals · ${{ github.event_name == 'pull_request' && format('smoke PR#{0}', github.event.pull_request.number) || (inputs.pr != '' && format('PR#{0}', inputs.pr) || (inputs.ref != '' && inputs.ref || 'master')) }}"
on:
pull_request:
types: [labeled]
workflow_dispatch:
inputs:
pr:
description: "Metabase PR number (sets ref to the PR head)"
required: false
default: ""
ref:
description: "Metabase ref"
required: false
default: ""
scope:
description: "Scope"
required: true
type: choice
options: [smoke, full, "all (EXPENSIVE)"]
default: smoke
sample:
description: "Sample (0 = full suite)"
required: false
default: "5"
evals_branch:
description: "Evals branch"
default: main
concurrency:
group: evals-dispatch-${{ (github.event.label.name == 'evals/smoke' && github.event.pull_request.number) || github.run_id }}
cancel-in-progress: true
jobs:
placeholder:
name: Placeholder
runs-on: ubuntu-latest
unsupported:
if: >-
github.event_name == 'pull_request'
&& github.event.label.name == 'evals/smoke'
&& github.event.pull_request.head.repo.full_name != github.repository
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
steps:
- name: Stub
run: echo "ai-benchmark workflow stub — real implementation pending"
- run: |
echo "::error::evals/smoke only runs on same-repo PRs; this PR's head is on a fork"
exit 1
announce:
if: >-
github.event_name == 'pull_request'
&& github.event.label.name == 'evals/smoke'
&& github.event.pull_request.head.repo.full_name == github.repository
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
outputs:
comment_id: ${{ steps.post.outputs.comment_id }}
steps:
- name: Comment (building)
id: post
uses: actions/github-script@v9
with:
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
script: |
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const body = `🤖 **Evals smoke** — ⏳ building the image… ([logs](${runUrl}))`;
const { owner, repo } = context.repo;
const issue_number = context.payload.pull_request.number;
// Drop earlier verdicts first: they describe shas that are no longer head, and a stale
// green one reads as this PR passing. The prefix survives eval-run's rewrites.
const existing = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number });
for (const c of existing.filter(c => c.body.startsWith('🤖 **Evals'))) {
await github.rest.issues.deleteComment({ owner, repo, comment_id: c.id }).catch(() => {});
}
const { data } = await github.rest.issues.createComment({ owner, repo, issue_number, body });
core.setOutput('comment_id', String(data.id));
uberjar:
needs: [announce]
if: >-
github.event_name == 'pull_request'
&& github.event.label.name == 'evals/smoke'
&& github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/workflows/uberjar.yml
secrets: inherit
smoke:
needs: [announce, uberjar]
if: >-
github.event_name == 'pull_request'
&& github.event.label.name == 'evals/smoke'
&& github.event.pull_request.head.repo.full_name == github.repository
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
env:
COMMENT_ID: ${{ needs.announce.outputs.comment_id }}
steps:
- name: Dispatch smoke eval-run
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
PR: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
EVALS_BRANCH: main
run: |
gh api repos/metabase/evals/actions/workflows/eval-run.yml/dispatches \
-f ref="$EVALS_BRANCH" \
-f "inputs[pr]=$PR" \
-f "inputs[ref]=$HEAD_SHA" \
-f "inputs[scope]=smoke" \
-f "inputs[surfaces]=all" \
-f "inputs[sample]=5" \
-f "inputs[evals_branch]=$EVALS_BRANCH" \
-f "inputs[comment_id]=$COMMENT_ID"
finalize:
# If the build or dispatch fell over, eval-run never took over the comment — leave a
# failure verdict instead of a comment stuck at "building the image…".
needs: [announce, uberjar, smoke]
if: always() && needs.announce.result == 'success' && needs.smoke.result != 'success'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
env:
COMMENT_ID: ${{ needs.announce.outputs.comment_id }}
SMOKE_RESULT: ${{ needs.smoke.result }}
UBERJAR_RESULT: ${{ needs.uberjar.result }}
steps:
- name: Comment (build/dispatch failed)
uses: actions/github-script@v9
with:
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
script: |
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
// A concurrency-cancelled retrigger lands here too; don't stamp it as an infra failure.
const cancelled = [process.env.SMOKE_RESULT, process.env.UBERJAR_RESULT].includes('cancelled');
const body = cancelled
? `🤖 **Evals smoke** — ⚪ cancelled before evals started ([logs](${runUrl}))`
: `🤖 **Evals smoke** — ❌ build or dispatch failed before evals started ([logs](${runUrl}))`;
// The replacement run deletes this comment on its way in, so a concurrency cancel can
// land here with nothing to patch. Losing the stamp is fine; failing the job isn't.
await github.rest.issues.updateComment({
owner: context.repo.owner, repo: context.repo.repo,
comment_id: Number(process.env.COMMENT_ID), body })
.catch(e => core.warning(`could not update comment (deleted?): ${e.message}`));
// eval-run only consumes the label once it has taken the run over, so anything failing
// before the dispatch leaves it applied — and `labeled` only fires on a transition, which
// makes re-applying it to retry a no-op. Hand it back.
await github.rest.issues.removeLabel({
owner: context.repo.owner, repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
name: 'evals/smoke',
}).catch(() => {});
dispatch:
if: github.event_name == 'workflow_dispatch'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
steps:
- name: Dispatch eval-run
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
EVALS_BRANCH: ${{ github.event.inputs.evals_branch }}
PR: ${{ inputs.pr }}
REF: ${{ inputs.ref }}
SCOPE: ${{ inputs.scope }}
SAMPLE: ${{ inputs.sample }}
run: |
gh api repos/metabase/evals/actions/workflows/eval-run.yml/dispatches \
-f ref="$EVALS_BRANCH" \
-f "inputs[pr]=$PR" \
-f "inputs[ref]=$REF" \
-f "inputs[scope]=$SCOPE" \
-f "inputs[sample]=${SAMPLE:-5}" \
-f "inputs[evals_branch]=$EVALS_BRANCH"

View File

@@ -148,12 +148,21 @@ jobs:
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
- name: MySQL Latest
junit-name: be-tests-mysql-latest-ee
image: mysql:latest
- name: MySQL 9
junit-name: be-tests-mysql-9-ee
image: mysql:9
env:
enable-ssl-tests: 'true'
enable-aws-iam-tests: 'true'
# `mysql:latest` now resolves to MySQL 26.x, which fails in a way that floods the log
# until the runner cancels the job. A cancelled job can't be soft-failed with
# continue-on-error, so the leg is disabled until 26.x is supported.
# - name: MySQL Latest
# junit-name: be-tests-mysql-latest-ee
# image: mysql:latest
# env:
# enable-ssl-tests: 'false'
# enable-aws-iam-tests: 'false'
job:
- name: Enterprise Tests
build-static-viz: false

88
.github/workflows/bundle-size.yml vendored Normal file
View File

@@ -0,0 +1,88 @@
name: Bundle Size
# Warns on PRs that move a shipped bundle's gzipped size past the threshold,
# comparing the PR's uberjar against the base ref's. Both gates are measured in
# a single job: downloading and extracting two uberjars is the expensive part,
# so adding a bundle costs a comment step, not another job.
on:
workflow_call:
inputs:
skip:
description: Whether to skip the check (e.g. no front-end sources changed, or the uberjar failed to build).
required: true
type: boolean
concurrency:
group: bundle-size-${{ github.workflow }}-${{ github.head_ref && github.ref || github.run_id }}
cancel-in-progress: true
jobs:
bundle-size-check:
name: "Bundle size check"
if: ${{ !inputs.skip }}
continue-on-error: true
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
env:
BUNDLE_SIZE_THRESHOLD_PERCENT: 2
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/bundle-size-check
id: compare
with:
edition: ee
base-ref-commit: ${{ github.event.pull_request.base.sha }}
threshold-percent: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}
- name: Comment on PR if app bundle size has increased
uses: ./.github/actions/notify-pull-request
if: success() && steps.compare.outputs.app_status == 'increased'
with:
remove-previous-comment: true
add-new-comment: true
message: "⚠️ Main app initial bundle size (gzipped) increased by ${{ steps.compare.outputs.app_size_change_percent }}% compared to the ${{ github.base_ref }} branch (threshold: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}%). This is the JavaScript every user downloads before the app renders, so check whether the new code can be loaded lazily instead."
include-log: false
- name: Comment on PR if app bundle size has decreased
uses: ./.github/actions/notify-pull-request
if: success() && steps.compare.outputs.app_status == 'decreased'
with:
remove-previous-comment: true
add-new-comment: true
message: "🎉 Main app initial bundle size (gzipped) decreased by ${{ steps.compare.outputs.app_size_change_percent }}% compared to the ${{ github.base_ref }} branch. Nice work!"
include-log: false
- name: Comment on PR if SDK bundle size has increased
uses: ./.github/actions/notify-pull-request
if: success() && steps.compare.outputs.sdk_status == 'increased'
with:
remove-previous-comment: true
add-new-comment: true
message: "⚠️ Embedding SDK chunked bundle size (gzipped) increased by ${{ steps.compare.outputs.sdk_size_change_percent }}% compared to the ${{ github.base_ref }} branch (threshold: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}%). Please contact Embedding team."
include-log: false
- name: Send notification to Slack via Zapier
if: success() && steps.compare.outputs.sdk_status == 'increased'
shell: bash
env:
ZAPIER_WEBHOOK_URL: ${{ secrets.EMBEDDING_ZAPIER_WEBHOOK_URL }}
SIZE_CHANGE_PERCENT: ${{ steps.compare.outputs.sdk_size_change_percent }}
BUNDLE_SIZE_THRESHOLD_PERCENT: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
curl -s -X POST "$ZAPIER_WEBHOOK_URL" \
-d "⚠️ Embedding SDK chunked bundle size (gzipped) increased by ${SIZE_CHANGE_PERCENT}% (threshold: ${BUNDLE_SIZE_THRESHOLD_PERCENT}%)
PR #${PR_NUMBER} ${PR_TITLE} (${PR_URL})"
- name: Comment on PR if SDK bundle size has decreased
uses: ./.github/actions/notify-pull-request
if: success() && steps.compare.outputs.sdk_status == 'decreased'
with:
remove-previous-comment: true
add-new-comment: true
message: "🎉 Embedding SDK chunked bundle size (gzipped) decreased by ${{ steps.compare.outputs.sdk_size_change_percent }}% compared to the ${{ github.base_ref }} branch. Nice work!"
include-log: false

View File

@@ -0,0 +1,83 @@
# CodeQL's overlay analysis saves a ~400MB "base database" cache on every master run, keyed by commit SHA.
#
# Only the newest entry is ever restored: CodeQL looks its cache up by key *prefix* and takes the most recent match,
# so everything behind it is dead weight. This job keeps the newest few and deletes the rest.
name: CodeQL Cache Cleanup
on:
schedule:
- cron: "0 4 * * *" # daily at 04:00 UTC
workflow_dispatch:
inputs:
keep:
description: "How many of the newest CodeQL caches to keep"
required: false
default: "2"
dry-run:
description: "List what would be deleted without deleting it"
type: boolean
required: false
default: false
concurrency:
group: cache-cleanup-codeql
cancel-in-progress: false
permissions:
actions: write # required to delete cache entries
contents: read
jobs:
cleanup:
# don't run the schedule on forks
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
env:
GH_TOKEN: ${{ github.token }}
KEEP: ${{ github.event.inputs.keep || '2' }}
DRY_RUN: ${{ github.event.inputs.dry-run || 'false' }}
PREFIX: codeql-overlay-base-database-
steps:
- name: Delete superseded CodeQL caches
shell: bash
run: |
set -euo pipefail
# created_at is ISO-8601, so a reverse lexicographic sort is newest-first.
gh api --paginate "/repos/${GITHUB_REPOSITORY}/actions/caches?per_page=100" \
--jq ".actions_caches[] | select(.key | startswith(\"${PREFIX}\")) | \"\(.created_at)\t\(.id)\t\(.size_in_bytes)\t\(.key)\"" \
| sort -r > all.tsv || true
found=$(wc -l < all.tsv | tr -d ' ')
echo "Found $found cache(s) matching '${PREFIX}'; keeping the newest $KEEP."
if [ "$found" -eq 0 ]; then
echo "Nothing to do." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
tail -n +$((KEEP + 1)) all.tsv > doomed.tsv || true
doomed=$(wc -l < doomed.tsv | tr -d ' ')
freed=0
while IFS=$'\t' read -r created id size key; do
[ -z "${id:-}" ] && continue
echo " deleting $key ($((size / 1000000))MB, created $created)"
if [ "$DRY_RUN" = "true" ]; then
echo " (dry run — not deleted)"
else
# A cache can vanish between listing and deleting (concurrent eviction); don't fail the job over it.
gh api --method DELETE "/repos/${GITHUB_REPOSITORY}/actions/caches/${id}" >/dev/null \
|| echo " (already gone, skipping)"
fi
freed=$((freed + size))
done < doomed.tsv
{
echo "### CodeQL cache cleanup"
echo ""
echo "- matched: **$found**"
echo "- kept (newest): **$KEEP**"
echo "- deleted: **$doomed**"
echo "- reclaimed: **$((freed / 1000000)) MB**${DRY_RUN:+ (dry run: $DRY_RUN)}"
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -1,56 +0,0 @@
name: Conventional commit PR Title Reminder
on:
pull_request:
branches:
- master
jobs:
files-changed:
name: Check which files changed
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 3
outputs:
frontend_embedding_sources: ${{ steps.changes.outputs.frontend_embedding_sources }}
steps:
- uses: actions/checkout@v6
- name: Test which files changed
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-paths.yaml
pr-title-reminder:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
permissions:
pull-requests: write
needs: [ files-changed ]
if: |
!cancelled() &&
needs.files-changed.outputs.frontend_embedding_sources == 'true'
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
- name: Check if the PR title is already following Conventional Commits style
uses: actions/github-script@v9
id: is-conventional-commit
with:
result-encoding: string
script: | # js
const { isConventionalTitle } = require(".github/scripts/conventional-commit-pr-title-reminder/utils.js")
const pullRequestTitle = context.payload.pull_request.title
return isConventionalTitle(pullRequestTitle)
- name: Remind PR authors to use conventional commit in the title
if: ${{ steps.is-conventional-commit.outputs.result != 'true' }}
uses: actions/github-script@v9
with:
script: | #js
const { updateCommentForEmbedding } = require(".github/scripts/conventional-commit-pr-title-reminder/utils.js")
await updateCommentForEmbedding({github, context})

View File

@@ -115,7 +115,7 @@ jobs:
base-ref: ${{ env.BASE_REF }}
head-ref: ${{ env.HEAD_REF }}
migrations-mysql-latest-ee:
migrations-mysql-9-ee:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 15
env:
@@ -128,7 +128,7 @@ jobs:
MB_MYSQL_TEST_USER: root
services:
mysql:
image: mysql:latest
image: mysql:9
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: mb_test
@@ -141,6 +141,35 @@ jobs:
base-ref: ${{ env.BASE_REF }}
head-ref: ${{ env.HEAD_REF }}
# `mysql:latest` now resolves to MySQL 26.x, which fails in a way that floods the log until the
# runner cancels the job. A cancelled job can't be soft-failed with continue-on-error, so this
# job is disabled until 26.x is supported.
# migrations-mysql-latest-ee:
# runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
# timeout-minutes: 15
# env:
# CI: 'true'
# MB_DB_TYPE: mysql
# MB_DB_HOST: localhost
# MB_DB_PORT: 3306
# MB_DB_DBNAME: mb_test
# MB_DB_USER: root
# MB_MYSQL_TEST_USER: root
# services:
# mysql:
# image: mysql:latest
# env:
# MYSQL_ALLOW_EMPTY_PASSWORD: true
# MYSQL_DATABASE: mb_test
# ports:
# - "3306:3306"
# steps:
# - uses: actions/checkout@v6
# - uses: ./.github/actions/cross-branch-migration-test
# with:
# base-ref: ${{ env.BASE_REF }}
# head-ref: ${{ env.HEAD_REF }}
migrations-mariadb-10-6-ee:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 15

View File

@@ -12,15 +12,16 @@ on:
required: true
type: string
pull_request:
paths:
- "cross-version/**"
- ".github/workflows/cross-version.yml"
- ".github/scripts/build-cross-version-matrix.js"
# NOTE: this workflow is temporarily disabled until the cross version tests are fixed.
# pull_request:
# paths:
# - "cross-version/**"
# - ".github/workflows/cross-version.yml"
# - ".github/scripts/build-cross-version-matrix.js"
schedule:
# Run nightly at 3 AM UTC
- cron: "0 3 * * *"
# schedule:
# # Run nightly at 3 AM UTC
# - cron: "0 3 * * *"
jobs:
# Generate matrix for scheduled/PR runs

View File

@@ -250,8 +250,7 @@ jobs:
const message = [
`:announcement-animated: ${eng} The v${next} release branch has been cut.`,
`- \`backport\`s will go to v${next} now`,
`- If you want to go back to v${current}, you'll need to add the double-backport tag`,
`Add the \`backport\` label to backport changes to all supported release branches.`,
].join('\n');
await sendSlackMessage({

View File

@@ -1,22 +0,0 @@
name: Auto-update documentation (release)
on:
schedule:
- cron: "0 23 * * 1-5" # Every Monday through Friday at 11 PM UTC
workflow_dispatch:
inputs:
target_branch:
description: "Target branch for documentation update"
required: false
default: "release-x.63.x"
type: string
jobs:
update-release-docs:
if: ${{ github.event_name != 'schedule' || github.repository == 'metabase/metabase' }}
name: Call documentation update for release branch
uses: ./.github/workflows/docs-generate-base.yml
with:
target_branch: ${{ inputs.target_branch || 'release-x.63.x' }}
secrets:
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}

View File

@@ -8,7 +8,7 @@ on:
target_branch:
description: "Target branch for documentation update"
required: false
default: "release-x.62.x"
default: "release-x.63.x"
type: string
jobs:
@@ -17,6 +17,6 @@ jobs:
name: Call documentation update for release branch
uses: ./.github/workflows/docs-generate-base.yml
with:
target_branch: ${{ inputs.target_branch || 'release-x.62.x' }}
target_branch: ${{ inputs.target_branch || 'release-x.63.x' }}
secrets:
automation_token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}

View File

@@ -34,6 +34,7 @@ on:
- Mongo 5 (SSL)
- Mongo (latest)
- MySQL
- MySQL 9
- MySQL (latest)
- Oracle 18
- Oracle 21
@@ -338,6 +339,35 @@ jobs:
junit-name: 'be-tests-mysql-8-0-ee'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mysql-9-ee:
if: inputs.driver == 'MySQL 9'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 90
env:
CI: 'true'
DRIVERS: mysql
MB_DB_TYPE: mysql
MB_DB_HOST: localhost
MB_DB_PORT: 3306
MB_DB_DBNAME: mb_test
MB_DB_USER: root
MB_MYSQL_TEST_USER: root
services:
mysql:
image: mysql:9
ports:
- "3306:3306"
env:
MYSQL_ALLOW_EMPTY_PASSWORD: true
MYSQL_DATABASE: mb_test
steps:
- uses: actions/checkout@v6
- name: Test MySQL driver (9)
uses: ./.github/actions/test-driver
with:
junit-name: 'be-tests-mysql-9-ee'
test-args: ":only ${{ inputs.test }} :times ${{ inputs.times }}"
be-tests-mysql-latest-ee:
if: inputs.driver == 'MySQL (latest)'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}

View File

@@ -127,7 +127,7 @@ jobs:
--skip="${{ inputs.skip }}"
)
echo "Running:"
echo "./bin/mage -driver-decisions ${MAGE_ARGS[@]}"
echo "./bin/mage -driver-decisions ${MAGE_ARGS[*]}"
echo ""
# First call: show human-readable output for debugging
./bin/mage -driver-decisions "${MAGE_ARGS[@]}"
@@ -135,7 +135,7 @@ jobs:
echo "Want to force-run a specific driver? Add a ci:run-<driver> or ci:run-all-drivers label to your PR."
echo "Run './bin/mage -driver-decisions -h' locally for the full list of drivers and labels."
# Second call: output only key=value lines for GITHUB_OUTPUT
./bin/mage -driver-decisions "${MAGE_ARGS[@]}" --github-output-only >> $GITHUB_OUTPUT
./bin/mage -driver-decisions "${MAGE_ARGS[@]}" --github-output-only >> "$GITHUB_OUTPUT"
env:
BASE_REF: ${{ github.event.pull_request.base.ref || github.ref_name }}
IS_MASTER_OR_RELEASE: ${{ github.ref_name == 'master' || startsWith(github.ref_name, 'release-x') }}
@@ -193,14 +193,13 @@ jobs:
trunk-api-token: ${{ secrets.TRUNK_API_TOKEN }}
previous-step-outcome: ${{ steps.test-driver.outputs.test-outcome }}
driver-status: ${{ needs.determine-driver-skips.outputs.athena-status }}
report-variant: ${{ matrix.job.name }}
run-trunk: ${{ vars.QUARANTINE_ENGINE != 'ci-conductor' }}
be-tests-bigquery-cloud-sdk-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.bigquery-should-run == 'true' || needs.determine-driver-skips.outputs.bigquery-quarantine-conflict == 'true' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 60
timeout-minutes: 70
strategy:
fail-fast: false
matrix:
@@ -279,7 +278,7 @@ jobs:
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: ${{ matrix.job.name }} ${{ matrix.version.name }}
logs-artifact-suffix: ${{ matrix.job.name }}
be-tests-clickhouse-ee:
needs: determine-driver-skips
@@ -366,7 +365,7 @@ jobs:
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: ${{ matrix.job.name }} ${{ matrix.version.name }}
logs-artifact-suffix: ${{ matrix.job.name }}
be-tests-druid-jdbc-ee:
needs: determine-driver-skips
@@ -424,7 +423,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.databricks-should-run == 'true' || needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
timeout-minutes: 50
strategy:
fail-fast: false
matrix:
@@ -500,7 +499,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.databricks-should-run == 'true' || needs.determine-driver-skips.outputs.databricks-quarantine-conflict == 'true' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
timeout-minutes: 50
strategy:
fail-fast: false
matrix:
@@ -669,12 +668,21 @@ jobs:
env:
enable-ssl-tests: 'false'
enable-aws-iam-tests: 'false'
- name: MySQL Latest
junit-name: be-tests-mysql-latest-ee
image: mysql:latest
- name: MySQL 9
junit-name: be-tests-mysql-9-ee
image: mysql:9
env:
enable-ssl-tests: 'true'
enable-aws-iam-tests: 'true'
# `mysql:latest` now resolves to MySQL 26.x, which fails in a way that floods the log
# until the runner cancels the job. A cancelled job can't be soft-failed with
# continue-on-error, so the leg is disabled until 26.x is supported.
# - name: MySQL Latest
# junit-name: be-tests-mysql-latest-ee
# image: mysql:latest
# env:
# enable-ssl-tests: 'false'
# enable-aws-iam-tests: 'false'
job:
- name: Driver Tests
test-args: >-
@@ -1088,7 +1096,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.postgres-should-run == 'true' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
timeout-minutes: 50
strategy:
fail-fast: false
matrix:
@@ -1240,7 +1248,7 @@ jobs:
port: ${{ env.MB_PRESTO_JDBC_TEST_PORT }}
timeout-minutes: 5
- name: Create temp cacerts file based on bundled JDK one
run: cp $JAVA_HOME/lib/security/cacerts /tmp/cacerts-with-presto-ssl.jks
run: cp "$JAVA_HOME/lib/security/cacerts" /tmp/cacerts-with-presto-ssl.jks
- name: Capture Presto server self signed CA
run: |
while [[ ! -s /tmp/presto-ssl-ca.pem ]]; do
@@ -1288,7 +1296,7 @@ jobs:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.redshift-should-run == 'true' || needs.determine-driver-skips.outputs.redshift-quarantine-conflict == 'true' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 40
timeout-minutes: 50
strategy:
fail-fast: false
matrix:
@@ -1377,13 +1385,13 @@ jobs:
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: ${{ matrix.job.name }} ${{ matrix.version.name }}
logs-artifact-suffix: ${{ matrix.job.name }}
be-tests-snowflake-ee:
needs: determine-driver-skips
if: ${{ needs.determine-driver-skips.outputs.snowflake-should-run == 'true' || needs.determine-driver-skips.outputs.snowflake-quarantine-conflict == 'true' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 60
timeout-minutes: 70
strategy:
fail-fast: false
matrix:
@@ -1465,7 +1473,7 @@ jobs:
uses: ./.github/actions/cleanup-python-runner
with:
upload-logs: ${{ steps.test-driver.outcome == 'failure' }}
logs-artifact-suffix: ${{ matrix.job.name }} ${{ matrix.version.name }}
logs-artifact-suffix: ${{ matrix.job.name }}
be-tests-sparksql-ee:
needs: determine-driver-skips
@@ -1708,9 +1716,11 @@ jobs:
REGISTRY="$(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com"
echo "docker_username=AWS" >> $GITHUB_OUTPUT
echo "docker_password=$PASSWORD" >> $GITHUB_OUTPUT
echo "registry=$REGISTRY" >> $GITHUB_OUTPUT
{
echo "docker_username=AWS"
echo "docker_password=$PASSWORD"
echo "registry=$REGISTRY"
} >> "$GITHUB_OUTPUT"
outputs:
registry: ${{ steps.login-ecr.outputs.registry }}
docker_username: ${{ steps.login-ecr.outputs.docker_username }}

View File

@@ -64,6 +64,11 @@ jobs:
# workspace where the gate step runs; an absolute path anchored to the
# workspace makes both agree regardless of cwd.
QUARANTINE_FAILURES_FILE: ${{ github.workspace }}/target/quarantine-failures.jsonl
# Cypress records video for every spec but the passing specs' videos are
# deleted in after:spec so recording is ~5-10% slowdown per shard and almost
# never yields an artifact. Failure screenshots are unaffected, and the
# stress-test workflows have their own config and keep video.
CYPRESS_VIDEO: "false"
# Sensitive env vars (tokens, secrets) starting with `CYPRESS_` are accessed via async `cy.env()`
# Example: `cy.env(["FOO"]).then(({ FOO }) => ...)`
# For non-sensitive config, use `Cypress.expose()` in cypress config instead (synchronous)
@@ -123,9 +128,20 @@ jobs:
- name: Get failed specs
if: github.run_attempt != '1'
run: echo "specs=$(cat failed-specs)" >> $GITHUB_ENV
run: echo "specs=$(cat failed-specs)" >> "$GITHUB_ENV"
# run-tests.yml compiles this in parallel with the uberjar build. Downloading it is ~10x faster than compiling it here.
# Callers without that job (stress tests, coverage shards, cross-run re-runs) fall through to compiling as before.
- name: Download pre-built CLJS
id: prebuilt-cljs
uses: actions/download-artifact@v8
continue-on-error: true
with:
name: e2e-cljs-${{ github.event.pull_request.head.sha || github.sha }}
path: target/cljs_dev
- name: Compile CLJS
if: steps.prebuilt-cljs.outcome != 'success'
run: bun run build-pure:cljs
- name: Run Metabase

View File

@@ -156,9 +156,9 @@ jobs:
REPO_URL="https://github.com/metabase/${REPO_NAME}.git"
echo "Checking if branch '${SAMPLE_APP_BRANCH_NAME}' exists in ${REPO_URL}..."
if git ls-remote --exit-code --heads "$REPO_URL" "${SAMPLE_APP_BRANCH_NAME}"; then
echo "sample_app_branch_exists=true" >> $GITHUB_OUTPUT
echo "sample_app_branch_exists=true" >> "$GITHUB_OUTPUT"
else
echo "sample_app_branch_exists=false" >> $GITHUB_OUTPUT
echo "sample_app_branch_exists=false" >> "$GITHUB_OUTPUT"
fi
- name: Prepare and launch Sample Apps
@@ -360,6 +360,7 @@ jobs:
test_suite:
[
"vite-6-host-app-e2e",
"vite-6-data-app-host-app-e2e",
"next-15-app-router-host-app-e2e",
"next-15-pages-router-host-app-e2e",
"angular-20-host-app-e2e",

View File

@@ -23,10 +23,6 @@ on:
description: "Whether to run host and sample apps compatibility tests."
required: true
type: boolean
bundle-size-check:
description: "Whether to run bundle size check."
required: true
type: boolean
skip:
description: Whether to skip running tests due to one of the artifacts failing to build.
required: true
@@ -90,13 +86,15 @@ jobs:
shell: bash
run: |
set -o pipefail
# SC2016: the '$a' and '$aEOF' are sed commands, not shell variables.
# shellcheck disable=SC2016
bun run embedding-sdk:docs:generate:pure --emit none 2>&1 \
| tee /dev/stdout \
| tee >( tail -n 10 \
| sed '1s/^/output<<EOF\n/' \
| sed '$aEOF' \
>> "$GITHUB_OUTPUT" )
exit ${PIPESTATUS[0]}
exit "${PIPESTATUS[0]}"
- name: Comment on PR if 'not included in the documentation' error happens
uses: ./.github/actions/notify-pull-request
with:
@@ -128,59 +126,8 @@ jobs:
uses: ./.github/workflows/embedding-sdk-host-sample-apps-tests.yml
secrets: inherit
embedding-sdk-bundle-size-check:
if: inputs.bundle-size-check
continue-on-error: true
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
env:
BUNDLE_SIZE_THRESHOLD_PERCENT: 2
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/sdk-bundle-size-check
id: compare
with:
edition: ee
base-ref-commit: ${{ github.event.pull_request.base.sha }}
threshold-percent: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}
- name: Comment on PR if bundle size has increased
uses: ./.github/actions/notify-pull-request
if: success() && steps.compare.outputs.status == 'increased'
with:
remove-previous-comment: true
add-new-comment: true
message: "⚠️ Embedding SDK chunked bundle size (gzipped) increased by ${{ steps.compare.outputs.size_change_percent }}% compared to the ${{ github.base_ref }} branch (threshold: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}%). Please contact Embedding team."
include-log: false
- name: Send notification to Slack via Zapier
if: success() && steps.compare.outputs.status == 'increased'
shell: bash
env:
ZAPIER_WEBHOOK_URL: ${{ secrets.EMBEDDING_ZAPIER_WEBHOOK_URL }}
SIZE_CHANGE_PERCENT: ${{ steps.compare.outputs.size_change_percent }}
BUNDLE_SIZE_THRESHOLD_PERCENT: ${{ env.BUNDLE_SIZE_THRESHOLD_PERCENT }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
curl -s -X POST "$ZAPIER_WEBHOOK_URL" \
-d "⚠️ Embedding SDK chunked bundle size (gzipped) increased by ${SIZE_CHANGE_PERCENT}% (threshold: ${BUNDLE_SIZE_THRESHOLD_PERCENT}%)
PR #${PR_NUMBER} ${PR_TITLE} (${PR_URL})"
- name: Comment on PR if bundle size has decreased
uses: ./.github/actions/notify-pull-request
if: success() && steps.compare.outputs.status == 'decreased'
with:
remove-previous-comment: true
add-new-comment: true
message: "🎉 Embedding SDK chunked bundle size (gzipped) decreased by ${{ steps.compare.outputs.size_change_percent }}% compared to the ${{ github.base_ref }} branch. Nice work!"
include-log: false
embedding-sdk-tests-result:
needs:
- embedding-sdk-bundle-size-check
- embedding-sdk-cli-snippets-type-check
- embedding-sdk-docs-snippets-type-check
- embedding-sdk-api-documentation-generation-validation

View File

@@ -0,0 +1,85 @@
name: Kondo Ratchets Self-Heal
# Stale budgets in .clj-kondo/ratchets.edn (above the actual ignore counts) fail CI. On PRs
# labelled kondo-ratchets-self-healing, this commits the lowered budgets to the branch;
# unlabelled PRs rely on local test runs to tighten the file.
#
# Two jobs so the automation token never shares an environment with PR-controlled code: the
# ratchet-down job runs ./bin/mage from the PR branch and only exports the fixed file as an
# artifact; the push job starts from a clean runner, executes nothing from the PR, and pushes.
permissions:
contents: read
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
paths:
- "**.clj"
- "**.cljc"
- "**.cljs"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
ratchet-down:
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
# fork PRs don't get the automation token, so we couldn't push to them anyway
if: >-
contains(github.event.pull_request.labels.*.name, 'kondo-ratchets-self-healing')
&& github.event.pull_request.head.repo.full_name == github.repository
outputs:
changed: ${{ steps.diff.outputs.changed }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: false
- name: Lower ignore budgets to match the source tree
run: ./bin/mage fix-kondo-ratchets
- name: Check for changes
id: diff
run: |
if git diff --quiet .clj-kondo/ratchets.edn; then
echo "budgets already tight"
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/upload-artifact@v7
if: steps.diff.outputs.changed == 'true'
with:
name: ratchets-edn
path: .clj-kondo/ratchets.edn
# dot-directories are excluded by default, which would ship an empty artifact
include-hidden-files: true
push:
needs: ratchet-down
if: needs.ratchet-down.outputs.changed == 'true'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.ref }}
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: ratchets-edn
path: .clj-kondo/
- name: Commit to the PR branch
env:
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
REPO: ${{ github.repository }}
# METABASE_AUTOMATION_USER_TOKEN so the self-healing commit triggers CI
# (a GITHUB_TOKEN push would not)
AUTOMATION_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
run: |
git config --global user.name "Metabase Automation"
git config --global user.email "github-automation@metabase.com"
git add .clj-kondo/ratchets.edn
git commit -m "Ratchet down kondo ignore budgets" --no-verify
git push "https://x-access-token:${AUTOMATION_TOKEN}@github.com/${REPO}.git" "HEAD:$PR_BRANCH"

View File

@@ -24,15 +24,42 @@ jobs:
check-migration-versions:
if: github.event_name == 'pull_request'
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
timeout-minutes: 10 # the `backport`-label path builds the release scripts
steps:
- uses: actions/checkout@v6
with:
filter: blob:none
fetch-depth: 0
# The `backport` label backports to every supported release line (see auto-backport.yml),
# so migrations on such a PR must sit in the oldest supported line's directory. Resolve
# that floor with the same getSupportedMajors the backport workflow uses; skip the
# release-script build entirely for PRs without the label.
- name: Prepare release scripts
if: contains(github.event.pull_request.labels.*.name, 'backport')
uses: ./.github/actions/prepare-release-scripts
- name: Compute oldest supported backport target
id: oldest-supported
if: contains(github.event.pull_request.labels.*.name, 'backport')
uses: actions/github-script@v9
env:
AWS_S3_STATIC_BUCKET: ${{ vars.AWS_S3_STATIC_BUCKET }}
AWS_REGION: ${{ vars.AWS_REGION }}
with:
script: | # js
const { getSupportedMajors } = require('${{ github.workspace }}/release/dist/index.cjs');
const targets = await getSupportedMajors();
console.log('supported backport targets', targets);
if (!targets.length || !targets.every(Number.isInteger)) {
throw new Error(`getSupportedMajors() returned no usable majors: ${JSON.stringify(targets)}`);
}
return Math.min(...targets);
- name: Check migration versions
env:
BASE_REF: ${{ github.base_ref }}
CURRENT_VERSION: ${{ vars.CURRENT_VERSION }}
PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
OLDEST_SUPPORTED_VERSION: ${{ steps.oldest-supported.outputs.result }}
run: bin/check-migration-versions

View File

@@ -19,40 +19,7 @@ jobs:
run: bun install --frozen-lockfile
- name: get module boundary stats
id: get-stats
run: |
NAMED_MODULES=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const named = elements.filter(e => !['shared/other', 'app/misc', 'other'].includes(e.type));
console.log(named.length);
")
ENFORCED_MODULES=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const enforced = new Set(elements.filter(e => e.enforceOutgoing).map(e => e.type));
console.log(enforced.size);
")
NAMED_DIRS=$(node --input-type=module -e "
import { elements } from './frontend/lint/module-boundaries.mjs';
const dirs = new Set();
for (const el of elements) {
const m = el.pattern.match(/^frontend\/src\/metabase\/(\w[\w-]*)\//)
if (m) dirs.add(m[1]);
}
for (const d of dirs) console.log(d);
")
TOTAL_DIRS=$(ls -d frontend/src/metabase/*/ | xargs -I{} basename {} | wc -l | tr -d ' ')
NAMED_COUNT=$(echo "$NAMED_DIRS" | grep -c . || true)
UNMODULED=$(($TOTAL_DIRS - $NAMED_COUNT))
VIOLATIONS=$(bun run module-boundaries 2>&1 | grep -o '✖ [0-9]* problems' | grep -o '[0-9]*' || echo "0")
echo "$NAMED_MODULES modules, $ENFORCED_MODULES enforced, $UNMODULED unmoduled folders, $VIOLATIONS violations"
echo "NAMED_MODULES=$NAMED_MODULES" >> $GITHUB_OUTPUT
echo "ENFORCED_MODULES=$ENFORCED_MODULES" >> $GITHUB_OUTPUT
echo "UNMODULED=$UNMODULED" >> $GITHUB_OUTPUT
echo "VIOLATIONS=$VIOLATIONS" >> $GITHUB_OUTPUT
run: node frontend/lint/module-boundaries-stats.mjs | tee -a "$GITHUB_OUTPUT"
- name: Upload stats
uses: actions/github-script@v9
env:

View File

@@ -34,6 +34,12 @@ jobs:
commit: ${{ steps.compute.outputs.commit }}
should_release: ${{ steps.compute.outputs.should_release }}
steps:
- name: Check if running on a fork
run: |
if [ "${{ github.repository }}" != "metabase/metabase" ]; then
echo "This workflow should not run on forks."
exit 1
fi
- uses: actions/checkout@v6
with:
sparse-checkout: |
@@ -125,7 +131,7 @@ jobs:
core.setOutput('should_release', 'false');
- name: Write job summary
if: always()
if: always() && github.repository == 'metabase/metabase'
shell: bash
run: |
{
@@ -157,3 +163,12 @@ jobs:
with:
version: ${{ needs.prepare.outputs.version }}
commit: ${{ needs.prepare.outputs.commit }}
auto-publish-release:
name: Auto-publish release ${{ inputs.version }}
needs: [prepare, trigger-release]
uses: ./.github/workflows/release.yml
secrets: inherit
with:
version: ${{ needs.prepare.outputs.version }}
commit: ${{ needs.prepare.outputs.commit }}

View File

@@ -1,46 +1,103 @@
# Step 2 of the SDK release process: merging a version-bump PR (opened by
# sdk-version-bump-pr.yml, labeled `sdk-release-trigger`) publishes the version
# and dist-tag committed in package.template.json's `sdkRelease` block.
#
# workflow_dispatch publishes the same way, from the branch chosen in the ref
# picker, without a PR. It reads the same committed sdkRelease block and never
# bumps the version - so it is not how you cut a release. Use it only to publish
# a version that is already committed, when the bump PR cannot do it.
#
# To retry a failed run, re-run the failed jobs on that run rather than
# dispatching a fresh one: if publish-npm already succeeded, a fresh run would
# stop at check-git-tag on the existing tag.
name: Release Metabase Embedding SDK for React
run-name: Release SDK from ${{ inputs.branch }}
run-name: Release SDK from ${{ github.event_name == 'workflow_dispatch' && github.ref_name || github.event.pull_request.base.ref }}
on:
workflow_dispatch:
inputs:
branch:
description: "Branch we want to release the SDK from (to release from other branches use the workflow from those branches)"
read_first:
description: |
⚠️ To cut a release, use the "SDK Version Bump PR" workflow instead — it bumps the version and opens a PR; merging that PR publishes.
This publishes the version already committed in package.template.json. It never bumps. Pick the branch in "Use workflow from" above.
Use it only when neither the bump PR nor re-running the failed jobs will do.
type: choice
required: true
default: "master"
required: false
options:
- master
- Publish the committed version from the branch picked in "Use workflow from" above
# Step 2 of the new release process: merging a version-bump PR (opened by
# sdk-version-bump-pr.yml, labeled `sdk-release-trigger`) triggers a release
# of whatever branch that PR targeted. See `prepare-release` below.
#
# The `paths` filter keeps this from creating a run for every PR closed
# anywhere in the repo - version-bump PRs always touch package.template.json,
# so this is a reliable pre-filter ahead of the label/merge check below.
pull_request:
types: [closed]
paths:
- enterprise/frontend/src/embedding-sdk-package/package.template.json
concurrency:
# We want to ensure only one job is running at a time per branch because
# there could be a conflict when updating the readme file.
group: ${{ github.workflow }}-${{ inputs.branch }}
cancel-in-progress: true
# One release at a time per branch; base.ref is empty on workflow_dispatch, so
# it falls back to the ref picker's branch. Never cancel in progress - a queued
# release that turns out stale fails loudly at check-git-tag.
#
# Non-release closed PRs queue here too and skip in seconds. GitHub keeps only
# one pending run per group and cancels any earlier one, so one could in theory
# evict a release waiting to start; that needs two same-branch releases at once.
# `queue: max` would fix it, but actionlint cannot parse it (rhysd/actionlint#654).
group: sdk-release-${{ github.event.pull_request.base.ref || github.ref_name }}
cancel-in-progress: false
jobs:
# Gates the whole workflow: either a manual workflow_dispatch, or a merged
# PR carrying the sdk-release-trigger label. Every other job depends
# (directly or transitively) on this one, so if it's skipped the rest of
# the run is skipped too - no separate `if:` needed on downstream jobs.
prepare-release:
name: Prepare release trigger
if: |
github.event_name == 'workflow_dispatch' ||
(github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'sdk-release-trigger'))
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
outputs:
branch: ${{ steps.resolve.outputs.branch }}
steps:
# base.ref is only set on a pull_request event; on a dispatch it is empty,
# so this falls through to the branch picked in the ref picker.
- name: Resolve target branch
id: resolve
run: echo "branch=${{ github.event.pull_request.base.ref || github.ref_name }}" >> "$GITHUB_OUTPUT"
workflow-summary:
name: Log inputs
needs: prepare-release
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
steps:
- name: Generate workflow summary
env:
BRANCH: ${{ needs.prepare-release.outputs.branch }}
run: |
tee -a $GITHUB_STEP_SUMMARY << EOF
tee -a "$GITHUB_STEP_SUMMARY" << EOF
**Inputs:**
- \`branch\`: ${{ inputs.branch }}
- \`branch\`: $BRANCH
- \`trigger\`: ${{ github.event_name }}
triggered by: @${{ github.event.sender.login }}
EOF
send-slack-notification-start:
name: "Send Slack notification: SDK release started"
needs: determine-sdk-version
# Builds release/dist/index.cjs once, for every Slack job to download. It hangs
# off prepare-release only - not off the metadata read or anything downstream -
# so the artifact exists even when the release fails at its very first step,
# which is exactly when send-slack-notification-failure needs it.
build-release-scripts:
name: Build release scripts
needs: prepare-release
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
outputs:
slackMessageId: ${{ steps.send-slack-message-start.outputs.result }}
steps:
- uses: actions/checkout@v6
with:
@@ -51,12 +108,28 @@ jobs:
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Cache build scripts
- name: Upload release scripts
uses: actions/upload-artifact@v7
with:
name: build-scripts
path: release/dist/index.cjs
send-slack-notification-start:
name: "Send Slack notification: SDK release started"
needs:
- build-release-scripts
- read-sdk-release-metadata
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
outputs:
slackMessageId: ${{ steps.send-slack-message-start.outputs.result }}
steps:
- name: Retrieve release scripts
uses: actions/download-artifact@v8
with:
name: build-scripts
path: release/dist/
- name: Send build start message
id: send-slack-message-start
uses: actions/github-script@v9
@@ -66,7 +139,7 @@ jobs:
result-encoding: string
script: | # js
const { mentionUserByGithubLogin, sendSlackMessage, githubRunLink } = require('${{ github.workspace }}/release/dist/index.cjs');
const sdkVersion = "${{ needs.determine-sdk-version.outputs.sdk_version }}";
const sdkVersion = "${{ needs.read-sdk-release-metadata.outputs.sdk_version }}";
const title = `:rocket: *SDK release: ${sdkVersion}* :rocket:`
const runLink = githubRunLink(
@@ -87,23 +160,22 @@ jobs:
send-slack-notification-failure:
name: "Send Slack notification: SDK release failed"
needs:
[
"determine-sdk-version",
"send-slack-notification-start",
"check-git-tag",
"test",
"build-sdk",
"publish-npm",
"tag-latest",
"git-tag",
]
- build-release-scripts
- read-sdk-release-metadata
- send-slack-notification-start
- check-git-tag
- test
- build-sdk
- publish-npm
- tag-latest
- git-tag
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
env:
slackMessageId: ${{ needs.send-slack-notification-start.outputs.slackMessageId }}
if: ${{ always() && contains(needs.*.result, 'failure') }}
steps:
- name: Retrieve build scripts
- name: Retrieve release scripts
uses: actions/download-artifact@v8
with:
name: build-scripts
@@ -115,8 +187,9 @@ jobs:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: | # js
const { githubRunLink, sendSlackReply } = require('${{ github.workspace }}/release/dist/index.cjs');
const sdkVersion = "${{ needs.determine-sdk-version.outputs.sdk_version }}";
const { githubRunLink, sendSlackReply, sendSlackMessage } = require('${{ github.workspace }}/release/dist/index.cjs');
const sdkVersion = "${{ needs.read-sdk-release-metadata.outputs.sdk_version }}";
const messageId = "${{ env.slackMessageId }}";
const runLink = githubRunLink(
`:x: ${sdkVersion} Release has failed`,
@@ -124,23 +197,34 @@ jobs:
context.repo.owner,
context.repo.repo,
);
sendSlackReply({
channelName: 'team-embedding-releases',
message: runLink,
messageId: ${{ env.slackMessageId}},
broadcast: true
});
if (messageId) {
sendSlackReply({
channelName: 'team-embedding-releases',
message: runLink,
messageId,
broadcast: true,
});
} else {
sendSlackMessage({
channelName: 'team-embedding-releases',
message: runLink,
});
}
send-slack-notification-successful:
name: "Send Slack notification: SDK release successful"
needs:
["determine-sdk-version", "send-slack-notification-start", "publish-npm"]
- build-release-scripts
- read-sdk-release-metadata
- send-slack-notification-start
- publish-npm
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 5
env:
slackMessageId: ${{ needs.send-slack-notification-start.outputs.slackMessageId }}
steps:
- name: Retrieve build scripts
- name: Retrieve release scripts
uses: actions/download-artifact@v8
with:
name: build-scripts
@@ -153,7 +237,7 @@ jobs:
with:
script: | # js
const { githubRunLink, sendSlackReply } = require('${{ github.workspace }}/release/dist/index.cjs');
const sdkVersion = "${{ needs.determine-sdk-version.outputs.sdk_version }}";
const sdkVersion = "${{ needs.read-sdk-release-metadata.outputs.sdk_version }}";
const runLink = githubRunLink(
`:partydeploy: ${sdkVersion} Release is complete :partydeploy:`,
@@ -164,93 +248,104 @@ jobs:
sendSlackReply({
channelName: 'team-embedding-releases',
message: runLink,
messageId: ${{ env.slackMessageId}},
messageId: "${{ env.slackMessageId }}",
broadcast: true
});
determine-sdk-version:
# Reads the version + release metadata straight from package.template.json
# instead of computing them (the version and sdkRelease.distTag/tagAsLatest
# are already correct on this branch, because sdk-version-bump-pr.yml wrote
# them there and a human reviewed them before merging).
read-sdk-release-metadata:
name: Read SDK release metadata
needs: prepare-release
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
timeout-minutes: 5
outputs:
sdk_version: ${{ fromJson(steps.new-sdk-version.outputs.result).version }}
major_version: ${{ fromJson(steps.new-sdk-version.outputs.result).majorVersion }}
pre_release_label: ${{ fromJson(steps.new-sdk-version.outputs.result).preReleaseLabel }}
sdk_version: ${{ steps.metadata.outputs.sdk_version }}
major_version: ${{ steps.metadata.outputs.major_version }}
dist_tag: ${{ steps.metadata.outputs.dist_tag }}
tag_as_latest: ${{ steps.metadata.outputs.tag_as_latest }}
steps:
- name: Check out the code using the provided branch
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
ref: ${{ needs.prepare-release.outputs.branch }}
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Get current SDK version
id: current-sdk-version
run: |
VERSION=$(jq -r .version ./enterprise/frontend/src/embedding-sdk-package/package.template.json)
echo "sdk_current_version=$VERSION" >> $GITHUB_OUTPUT
- name: Get next SDK patch or pre-release version
id: new-sdk-version
- name: Read version and sdkRelease metadata
id: metadata
uses: actions/github-script@v9
with:
script: | #js
const { getNextSdkVersion } = require('${{ github.workspace }}/release/dist/index.cjs');
script: | # js
const { readSdkReleaseMetadata } = require('${{ github.workspace }}/release/dist/index.cjs');
const branch = "${{ inputs.branch }}";
const currentVersion = "${{ steps.current-sdk-version.outputs.sdk_current_version }}";
let metadata;
try {
metadata = readSdkReleaseMetadata({
packageTemplatePath:
'enterprise/frontend/src/embedding-sdk-package/package.template.json',
});
} catch (error) {
core.setFailed(error.message);
return;
}
const result = getNextSdkVersion(branch, currentVersion);
return {
version: result.version,
preReleaseLabel: result.preReleaseLabel,
majorVersion: result.majorVersion
};
core.setOutput('sdk_version', metadata.version);
core.setOutput('major_version', metadata.majorVersion);
core.setOutput('dist_tag', metadata.distTag);
core.setOutput('tag_as_latest', String(metadata.tagAsLatest));
- name: Append workflow summary
run: |
tee -a $GITHUB_STEP_SUMMARY << EOF
**Derived inputs:**
tee -a "$GITHUB_STEP_SUMMARY" << EOF
**SDK release metadata (from package.template.json):**
- \`current_version\`: "${{ steps.current-sdk-version.outputs.sdk_current_version }}"
- \`next_version\`: "${{ fromJson(steps.new-sdk-version.outputs.result).version }}"
- \`major_version\`: "${{ fromJson(steps.new-sdk-version.outputs.result).majorVersion }}"
- \`version\`: "${{ steps.metadata.outputs.sdk_version }}"
- \`major_version\`: "${{ steps.metadata.outputs.major_version }}"
- \`distTag\`: "${{ steps.metadata.outputs.dist_tag }}"
- \`tagAsLatest\`: "${{ steps.metadata.outputs.tag_as_latest }}"
EOF
check-git-tag:
needs: determine-sdk-version
needs:
- prepare-release
- read-sdk-release-metadata
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
env:
tag: embedding-sdk-${{ needs.determine-sdk-version.outputs.sdk_version }}
tag: embedding-sdk-${{ needs.read-sdk-release-metadata.outputs.sdk_version }}
steps:
- name: Check out the code using the provided branch
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
ref: ${{ needs.prepare-release.outputs.branch }}
fetch-depth: 0
fetch-tags: true
- name: check tags
run: |
if [ $(git tag -l | grep $tag$) ]
if git tag -l | grep -q "${tag}$"
then
echo "::error::Tag '${{ env.tag }}' already exists. If you expect to run this workflow with the same tag, please remove the tag first and rerun this workflow again."
echo "::error::Tag '${{ env.tag }}' already exists. If you expect to run this workflow with the same tag, please remove the tag first and rerun this workflow."
exit 1
else
exit 0
fi
test:
needs: check-git-tag
needs:
- prepare-release
- check-git-tag
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
steps:
- name: Check out the code using the provided branch
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
ref: ${{ needs.prepare-release.outputs.branch }}
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
@@ -262,14 +357,16 @@ jobs:
run: bun run embedding-sdk:test-unit
build-sdk:
needs: [test, determine-sdk-version]
needs:
- prepare-release
- test
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
steps:
- name: Check out the code using the provided branch
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
ref: ${{ needs.prepare-release.outputs.branch }}
fetch-depth: 0
fetch-tags: true
@@ -279,11 +376,9 @@ jobs:
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Bump published npm package version
# NOTE: this should happen before "Build SDK bundle" as we inject SDK version into the code during build step
run: |
./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ needs.determine-sdk-version.outputs.sdk_version }}
# No version bump here anymore: package.template.json's version is
# already correct on this branch (bumped by sdk-version-bump-pr.yml
# before merge), so we build with it as-is.
- name: Build SDK package
run: bun run build-embedding-sdk-package
@@ -297,94 +392,22 @@ jobs:
path: ./resources/embedding-sdk
publish-npm:
needs: [build-sdk, determine-sdk-version]
needs:
- prepare-release
- build-sdk
- read-sdk-release-metadata
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
permissions:
id-token: write # OIDC handshake for npm trusted publishing
actions: read # actions/download-artifact reads the Actions API via GITHUB_TOKEN
pull-requests: write # gh pr review --approve step uses GITHUB_TOKEN
env:
sdk_version: ${{ needs.determine-sdk-version.outputs.sdk_version }}
steps:
- name: Check out branch to prepare for a SDK version bump PR
uses: actions/checkout@v6
with:
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
# when we created version update PR, it would only have diff from 2 files SDK readme and SDK package.json template
ref: ${{ inputs.branch}}
- name: Setup git user
run: |
git config --global user.email "github-automation@metabase.com"
git config --global user.name "Metabase Automation"
- name: Update readme
if: inputs.branch != 'master'
run: |
bash ./bin/embedding-sdk/release_utils.bash update_readme ${{ env.sdk_version }}
- name: Bump published npm package version
run: |
bash ./bin/embedding-sdk/release_utils.bash update_package_json_template ${{ env.sdk_version }}
- name: Create a PR updating readme + published version (using GitHub Metabase Automation account)
run: |
git checkout -b update-sdk-version-${{ env.sdk_version }}
if [ "${{ inputs.branch }}" = "master" ]; then
COMMIT_MSG="chore(sdk): Update published npm version to ${{ env.sdk_version }}"
PR_TITLE="chore(sdk): Update SDK version to ${{ env.sdk_version }}"
PR_BODY="Update published npm package version to ${{ env.sdk_version }}"
else
COMMIT_MSG="docs(sdk): Update Readme version references and published npm version to ${{ env.sdk_version }}"
PR_TITLE="docs(sdk): Update SDK version to ${{ env.sdk_version }}"
PR_BODY="Update Readme version references and published npm package version to ${{ env.sdk_version }}"
fi
git commit -a -m "$COMMIT_MSG"
git push origin HEAD
gh pr create --base "${{ inputs.branch }}" \
--assignee "${GITHUB_ACTOR}" \
--title "$PR_TITLE" \
--body "$PR_BODY"
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Edit PR adding useful information (using Metabase bot app)
# metabase/embedding team is added automatically as code owner of updated `package.template.json` file
run: |
gh pr edit --add-reviewer albertoperdomo --add-label no-backport
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Auto approve PR (using GitHub Actions account)
run: |
gh pr review --approve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enable Pull request auto-merge (using GitHub Metabase Automation account)
run: |
gh pr merge --auto --squash
env:
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Retrieve build SDK package artifact
uses: actions/download-artifact@v8
with:
name: metabase-sdk
path: sdk
- name: Compute master tag
id: compute-tag
run: |
if [ -n "${{ needs.determine-sdk-version.outputs.pre_release_label }}" ]; then
echo "masterTag=${{ needs.determine-sdk-version.outputs.pre_release_label }}" >> $GITHUB_OUTPUT
else
echo "masterTag=nightly" >> $GITHUB_OUTPUT
fi
# Trusted publishing requires npm >= 11.5.1 and Node >= 22.14.0.
# Node 22.x line bundles npm 10.x, so pin Node 24.x which ships npm 11.x.
- name: Setup Node.js for npm publish with trusted publishing
@@ -396,34 +419,41 @@ jobs:
- name: Publish to NPM
working-directory: sdk
run: |
TAG="${{ steps.compute-tag.outputs.masterTag }}"
TAG="${{ needs.read-sdk-release-metadata.outputs.dist_tag }}"
echo "Resolved npm tag: $TAG"
npm publish --tag "$TAG"
# When a newer release branch goes gold, remove this entire job from this branch.
tag-latest:
name: Tag npm release as latest
# Pre-gold: remove `if: false` once this branch becomes gold.
if: false
needs: [publish-npm, determine-sdk-version]
# Driven by package.template.json's sdkRelease.tagAsLatest instead of a
# hardcoded `if: false` that had to be hand-edited (uncommented) once a
# branch went gold - see sdk-version-bump-pr.yml, which is where a human
# sets this to true when appropriate.
if: ${{ needs.read-sdk-release-metadata.outputs.tag_as_latest == 'true' }}
needs:
- publish-npm
- read-sdk-release-metadata
uses: ./.github/workflows/embedding-sdk-tag-latest.yml
with:
version: ${{ needs.determine-sdk-version.outputs.sdk_version }}
version: ${{ needs.read-sdk-release-metadata.outputs.sdk_version }}
secrets: inherit
git-tag:
needs: [publish-npm, determine-sdk-version]
needs:
- prepare-release
- publish-npm
- read-sdk-release-metadata
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 20
env:
tag: embedding-sdk-${{ needs.determine-sdk-version.outputs.sdk_version }}
stable_tag: embedding-sdk-${{ needs.determine-sdk-version.outputs.major_version }}-stable
tag: embedding-sdk-${{ needs.read-sdk-release-metadata.outputs.sdk_version }}
stable_tag: embedding-sdk-${{ needs.read-sdk-release-metadata.outputs.major_version }}-stable
steps:
- name: Check out the code using the provided branch
uses: actions/checkout@v6
with:
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
ref: ${{ inputs.branch }}
ref: ${{ needs.prepare-release.outputs.branch }}
- name: Setup git user
run: |
@@ -436,7 +466,7 @@ jobs:
git push origin ${{ env.tag }}
- name: Create and push SDK stable version tag
if: ${{ startsWith(inputs.branch, 'release-x.') }}
if: ${{ startsWith(needs.prepare-release.outputs.branch, 'release-x.') }}
run: |
git tag -a -f ${{ env.stable_tag }} -m "Tagging SDK stable version ${{ env.stable_tag }}"
git push origin -f ${{ env.stable_tag }}

View File

@@ -36,6 +36,8 @@ jobs:
- name: Get Release Version
uses: actions/github-script@v9
env:
# required by getMilestoneIssues to configure GraphQL client
GITHUB_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
DOCKERHUB_OWNER: test_owner
DOCKERHUB_REPO: test_repo
AWS_S3_DOWNLOADS_BUCKET: test_bucket

View File

@@ -138,19 +138,45 @@ jobs:
- name: Check JAR version properties
run: |
# ensure actual jar checksum matches checksum file
grep -q $(sha256sum ./metabase.jar) SHA256.sum && echo "checksums match" || (echo "checksum mismatch" && exit 1)
jar_checksum=$(sha256sum ./metabase.jar | cut -d ' ' -f 1)
if grep -q "$jar_checksum" SHA256.sum; then
echo "checksums match"
else
echo "checksum mismatch"
exit 1
fi
# ensure stored commit-id file matches input commit
grep -q ${{ inputs.commit }} COMMIT-ID && echo "commit ids match" || (echo "commit-id mismatch" && exit 1)
if grep -q "${{ inputs.commit }}" COMMIT-ID; then
echo "commit ids match"
else
echo "commit-id mismatch"
exit 1
fi
# ensure version.properties partial commit matches commit input
grep -q $(cut -c 1-7 <<< ${{ inputs.commit }}) version.properties && echo "version.properties commit matches" || (echo "version.properties commit mismatch" && exit 1)
if grep -q "$(cut -c 1-7 <<< '${{ inputs.commit }}')" version.properties; then
echo "version.properties commit matches"
else
echo "version.properties commit mismatch"
exit 1
fi
# ensure version.properties version number matches input version (without v0/v1 to make it ee/oss agnostic)
grep -q $(cut -c 3-99 <<< ${{ inputs.version }}) version.properties && echo "version.properties version number matches" || (echo "version.properties version number mismatch" && exit 1)
if grep -q "$(cut -c 3-99 <<< '${{ inputs.version }}')" version.properties; then
echo "version.properties version number matches"
else
echo "version.properties version number mismatch"
exit 1
fi
# ensure the build is not a snapshot build
grep -q "SNAPSHOT" version.properties && (echo "jar is a snapshot" && exit 1) || echo "jar is not a snapshot build"
if grep -q "SNAPSHOT" version.properties; then
echo "jar is a snapshot"
exit 1
else
echo "jar is not a snapshot build"
fi
- name: Upload Uberjar as artifact
uses: actions/upload-artifact@v7
with:
@@ -244,7 +270,7 @@ jobs:
run:
curl -L http://${{ vars.AWS_S3_DOWNLOADS_BUCKET }}.s3.${{ vars.AWS_REGION }}.amazonaws.com/${{ steps.version_path.outputs.result }}/metabase.jar -o metabase-downloaded.jar
- name: Verify Checksum
run: grep -q $(sha256sum ./metabase-downloaded.jar) SHA256.sum && echo "checksums match" || exit 1
run: grep -q "$(sha256sum ./metabase-downloaded.jar | cut -d ' ' -f 1)" SHA256.sum && echo "checksums match" || exit 1
containerize-oss:
needs: check-version
@@ -255,7 +281,7 @@ jobs:
commit: ${{ inputs.commit }}
repo: ${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_REPO }}
tag: ${{ needs.check-version.outputs.oss }}
release: "true"
release: ${{ 'true' }}
containerize-ee:
needs: check-version
@@ -266,7 +292,7 @@ jobs:
commit: ${{ inputs.commit }}
repo: ${{ vars.DOCKERHUB_OWNER }}/${{ vars.DOCKERHUB_REPO }}-enterprise
tag: ${{ needs.check-version.outputs.ee }}
release: "true"
release: ${{ 'true' }}
push-tags:
needs: [verify-s3-download, containerize-oss, containerize-ee, check-version]
@@ -317,6 +343,9 @@ jobs:
- name: Get extra tags
id: taglist
uses: actions/github-script@v9
env:
AWS_S3_STATIC_BUCKET: ${{ vars.AWS_S3_STATIC_BUCKET }}
AWS_REGION: ${{ vars.AWS_REGION }}
with:
result-encoding: string
script: | # js
@@ -325,7 +354,7 @@ jobs:
const version = '${{ inputs.version }}';
const latestMajorVersion = '${{ vars.LATEST_MAJOR_VERSION }}';
const extra_tags = getExtraTagsForVersion({ version, latestMajorVersion });
const extra_tags = await getExtraTagsForVersion({ version, latestMajorVersion });
console.log({ extra_tags, latestMajorVersion });
core.setOutput('extra_tags', JSON.stringify(extra_tags));
@@ -445,6 +474,9 @@ jobs:
uses: ./.github/actions/prepare-release-scripts
- name: Publish draft release notes to github
uses: actions/github-script@v9
env:
# required by getMilestoneIssues to configure GraphQL client
GITHUB_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
with:
github-token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
result-encoding: string

View File

@@ -15,20 +15,30 @@ jobs:
env:
BRANCH_NAME: ${{ github.event.workflow_run.head_branch }}
AUTHOR_NAME: ${{ github.event.workflow_run.head_commit.author.name }}
AUTHOR_LOGIN: ${{ github.event.workflow_run.head_commit.author.username }}
steps:
- name: Generate job summary
run: |
echo "# ${{ github.event.workflow_run.name }} workflow failed! :x:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "View the failed run attempt (#${{ github.event.workflow_run.run_attempt }}) using the following link:" >> $GITHUB_STEP_SUMMARY
echo "${{ github.event.workflow_run.html_url }}" >> $GITHUB_STEP_SUMMARY
{
echo "# ${{ github.event.workflow_run.name }} workflow failed! :x:"
echo ""
echo "View the failed run attempt (#${{ github.event.workflow_run.run_attempt }}) using the following link:"
echo "${{ github.event.workflow_run.html_url }}"
} >> "$GITHUB_STEP_SUMMARY"
- uses: actions/setup-node@v6.4.0
- uses: actions/checkout@v6
with:
node-version: lts/Jod # 22.x.x
- run: npm install @slack/web-api
sparse-checkout: |
.github
release
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Trigger a re-run
uses: actions/github-script@v9
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
with:
script: | # js
const MAX_ATTEMPTS = 1;
@@ -36,7 +46,8 @@ jobs:
const FAILED_RUN_URL = "${{ github.event.workflow_run.html_url }}";
const FAILED_RUN_NAME = "${{ github.event.workflow_run.name }}";
const BREAKING_COMMIT = "${{ github.event.workflow_run.head_sha }}";
const AUTHOR = process.env.AUTHOR_NAME;
let AUTHOR = process.env.AUTHOR_NAME;
let AUTHOR_LOGIN = process.env.AUTHOR_LOGIN;
const BRANCH = process.env.BRANCH_NAME;
if (ATTEMPT <= MAX_ATTEMPTS) {
@@ -55,8 +66,7 @@ jobs:
}
} else if (!BRANCH.includes('backport-')) {
// notify slack of repeated failure
const { WebClient } = require('@slack/web-api');
const slack = new WebClient('${{ secrets.SLACK_BOT_TOKEN }}');
const { mentionUserByGithubLogin, sendSlackMessage } = require('${{ github.workspace }}/release/dist/index.cjs');
// find out which jobs failed
const { owner, repo } = context.repo;
@@ -76,6 +86,24 @@ jobs:
return;
}
// the workflow_run payload omits the github login when the commit
// author isn't matched to a github account, so look it up from
// the commit itself
if (!AUTHOR_LOGIN) {
try {
const { data: commit } = await github.rest.repos.getCommit({
owner,
repo,
ref: BREAKING_COMMIT,
});
AUTHOR_LOGIN = commit.author?.login ?? '';
AUTHOR = AUTHOR || commit.commit?.author?.name || '';
} catch (error) {
console.log(`Could not look up the author of ${BREAKING_COMMIT}: ${error.message}`);
}
}
const failedJobsList = failedJobs.map(job => ({
"type": "rich_text_section",
"elements": [
@@ -87,9 +115,9 @@ jobs:
]
}));
await slack.chat.postMessage({
channel: 'engineering-ci',
text: 'Failing tests',
await sendSlackMessage({
channelName: 'engineering-ci',
message: 'Failing tests',
blocks: [
{
"type": "header",
@@ -107,7 +135,7 @@ jobs:
"type": "section",
"text": {
"type": "mrkdwn",
"text": `Commit <https://github.com/metabase/metabase/commit/${BREAKING_COMMIT}|${BREAKING_COMMIT.slice(0,7)}> by ${AUTHOR} has failing <${FAILED_RUN_URL}|${FAILED_RUN_NAME}> tests on <https://github.com/${owner}/${repo}/commits/${BRANCH}|\`${BRANCH}\`>`
"text": `Commit <https://github.com/metabase/metabase/commit/${BREAKING_COMMIT}|${BREAKING_COMMIT.slice(0,7)}> by ${AUTHOR} (${mentionUserByGithubLogin(AUTHOR_LOGIN)}) has failing <${FAILED_RUN_URL}|${FAILED_RUN_NAME}> tests on <https://github.com/${owner}/${repo}/commits/${BRANCH}|\`${BRANCH}\`>`
},
},
{

View File

@@ -93,13 +93,63 @@ jobs:
uses: ./.github/workflows/uberjar.yml
secrets: inherit
# Compile CLJS in parallel with the uberjar and pass it to each e2e group.
# The compile is doesn't depend on MB_EDITION
e2e-cljs:
name: Compile CLJS for e2e
needs: [create-test-plan]
if: ${{ !cancelled() && needs.create-test-plan.result == 'success' && needs.create-test-plan.outputs.e2e_all == 'true' }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v6
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Compile CLJS
run: bun run build-pure:cljs
- name: Upload compiled CLJS
uses: actions/upload-artifact@v7
with:
name: e2e-cljs-${{ github.event.pull_request.head.sha || github.sha }}
path: target/cljs_dev
if-no-files-found: error
# Uploads fail when an artifact of this name already exists in the run
# replace it rather than fail on a re-run.
overwrite: true
static-viz-files-changed:
name: Check whether static-viz files changed
if: ${{ !contains(github.event.pull_request.labels.*.name,'ci skip') && !contains(github.event.pull_request.labels.*.name,'minimal-tests') }}
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
outputs:
static_viz: ${{ steps.static_viz.outputs.static_viz }}
steps:
- uses: actions/checkout@v6
- name: Prepare front-end environment
uses: ./.github/actions/prepare-frontend
- name: Prepare back-end environment
uses: ./.github/actions/prepare-backend
- name: Build static-viz frontend
run: bun run build-static-viz
env:
MB_EDITION: ee
- name: Check for static viz changes
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: static_viz
with:
token: ${{ github.token }}
filters: .github/static-viz-sources.yaml
backend-tests:
if: ${{ !cancelled() && needs.create-test-plan.result == 'success' }}
needs: [create-test-plan]
needs: [create-test-plan, static-viz-files-changed]
uses: ./.github/workflows/backend.yml
secrets: inherit
with:
skip: ${{ needs.create-test-plan.outputs.backend_all != 'true' }}
skip: ${{ needs.create-test-plan.outputs.backend_all != 'true' && needs.static-viz-files-changed.outputs.static_viz != 'true' }}
semantic-search-tests:
needs: create-test-plan
@@ -178,7 +228,6 @@ jobs:
uses: ./.github/workflows/embedding-sdk.yml
secrets: inherit
with:
bundle-size-check: ${{ github.event_name == 'pull_request' && needs.uberjar.result == 'success' && needs.create-test-plan.outputs.frontend_sources == 'true' }}
cli-snippets-type-check: ${{ needs.create-test-plan.outputs.frontend_sources == 'true' }}
docs-snippets-type-check: ${{ needs.create-test-plan.outputs.embedding_documentation == 'true' }}
documentation-validation: ${{ needs.create-test-plan.outputs.frontend_sources == 'true' }}
@@ -186,6 +235,14 @@ jobs:
host-sample-apps-tests: ${{ needs.create-test-plan.outputs.embedding_sdk_host_sample_apps == 'true' }}
skip: ${{ needs.embedding-sdk-package.result != 'success' || needs.uberjar.result != 'success' }}
bundle-size:
needs: [uberjar, create-test-plan]
if: always() && !cancelled()
uses: ./.github/workflows/bundle-size.yml
secrets: inherit
with:
skip: ${{ github.event_name != 'pull_request' || needs.uberjar.result != 'success' || needs.create-test-plan.outputs.frontend_sources != 'true' }}
pr-env:
needs: [uberjar]
if: |

View File

@@ -0,0 +1,180 @@
# Step 1 of the SDK release process: bump the version (and the sdkRelease
# metadata that controls the npm dist-tag / `latest` promotion) in
# package.template.json, then open a PR labeled `sdk-release-trigger`.
# Merging that PR is what actually triggers a release, via the
# `pull_request` trigger on release-embedding-sdk.yml.
name: SDK Version Bump PR
run-name: Bump Embedding SDK version (${{ inputs.release_type }}) on ${{ github.ref_name }}
on:
workflow_dispatch:
inputs:
read_first:
description: "Bumps the version on the branch chosen in 'Use workflow from' above."
type: choice
required: false
options:
- Pick the branch in "Use workflow from" above
release_type:
description: |
Before gold:
• beta — release branch: publish a beta
At gold:
• patch — release branch: graduate the beta to 0.N.0
• preminor — master: move master to the next major
After gold:
• patch — release branch: ship a patch (0.N.1, 0.N.2, ...)
Any time:
• alpha — master: bump the development version
• custom — one-off branch (esbuild, data-apps): publish under its own prerelease id
type: choice
required: true
options:
- alpha
- beta
- preminor
- patch
- custom
prerelease_id:
description: "release_type=custom only, and only on the FIRST cut of a one-off branch (e.g. data-apps). Becomes the prerelease id in the version (0.62.5-data-apps.0) and the npm dist-tag (62-data-apps). Leave empty on later bumps - the id is reused from the current version."
type: string
required: false
concurrency:
# one version-bump in flight per branch at a time, to avoid racing PRs
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: false
jobs:
bump-version:
name: Bump SDK version and open review PR
runs-on: ${{ vars.DEFAULT_RUNNER_KEY }}
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
env:
package_template: enterprise/frontend/src/embedding-sdk-package/package.template.json
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
token: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
- name: Setup git user
run: |
git config --global user.email "github-automation@metabase.com"
git config --global user.name "Metabase Automation"
- name: Prepare release scripts
uses: ./.github/actions/prepare-release-scripts
- name: Compute version and sdkRelease metadata
id: metadata
uses: actions/github-script@v9
# Pass inputs via env, not inlined `${{ }}` in the script: github-script
# interpolates expressions into the JS source before running it, so a
# free-form input like prerelease_id could inject code. env values are
# read at runtime and can't escape into the code.
env:
BRANCH: ${{ github.ref_name }}
RELEASE_TYPE: ${{ inputs.release_type }}
PRERELEASE_ID: ${{ inputs.prerelease_id }}
LATEST_MAJOR_VERSION: ${{ vars.LATEST_MAJOR_VERSION }}
PACKAGE_TEMPLATE: ${{ env.package_template }}
with:
script: | # js
const { applySdkVersionBump } = require('${{ github.workspace }}/release/dist/index.cjs');
let result;
try {
result = applySdkVersionBump({
packageTemplatePath: process.env.PACKAGE_TEMPLATE,
branch: process.env.BRANCH,
releaseType: process.env.RELEASE_TYPE,
prereleaseId: process.env.PRERELEASE_ID,
latestMajorVersion: process.env.LATEST_MAJOR_VERSION,
});
} catch (error) {
core.setFailed(error.message);
return;
}
core.setOutput("previous_version", result.previousVersion);
core.setOutput("new_version", result.newVersion);
core.setOutput("major_version", result.majorVersion);
core.setOutput("dist_tag", result.distTag);
core.setOutput("tag_as_latest", String(result.tagAsLatest));
- name: Append workflow summary
env:
BRANCH: ${{ github.ref_name }}
run: |
tee -a "$GITHUB_STEP_SUMMARY" << EOF
**SDK version bump:**
- \`branch\`: $BRANCH
- \`release_type\`: ${{ inputs.release_type }}
- \`previous_version\`: ${{ steps.metadata.outputs.previous_version }}
- \`new_version\`: ${{ steps.metadata.outputs.new_version }}
- \`distTag\` (default, review before merging): ${{ steps.metadata.outputs.dist_tag }}
- \`tagAsLatest\` (auto-computed from \`vars.LATEST_MAJOR_VERSION\`, review before merging): ${{ steps.metadata.outputs.tag_as_latest }}
triggered by: @${{ github.event.sender.login }}
EOF
- name: Commit version bump
env:
BUMP_BRANCH: sdk-version-bump-${{ steps.metadata.outputs.new_version }}
run: |
# A leftover branch means an earlier bump to this same version was
# abandoned. Say so, rather than pushing a near-duplicate alongside it.
if git ls-remote --exit-code --heads origin "$BUMP_BRANCH" >/dev/null 2>&1; then
echo "::error::'$BUMP_BRANCH' already exists on origin - an earlier bump to this version was never merged. Close its PR, delete the branch, then re-run."
exit 1
fi
git checkout -b "$BUMP_BRANCH"
git add "$package_template"
git commit -m "chore(sdk): bump SDK version to ${{ steps.metadata.outputs.new_version }} (${{ inputs.release_type }})"
git push origin HEAD
- name: Open version-bump PR (using GitHub Metabase Automation account)
id: open-pr
env:
BRANCH: ${{ github.ref_name }}
GH_TOKEN: ${{ secrets.METABASE_AUTOMATION_USER_TOKEN }}
run: |
# TODO (EMB-2095) auto-approve and auto-merge this PR once the computed
# version/distTag/tagAsLatest have proven out over a few real releases.
# Nothing gates on review today - merging is only manual as a seatbelt.
#
# --reviewer tags the team so a release is visible, not because anything
# waits on a review. It needs the automation PAT: GITHUB_TOKEN cannot
# resolve an org team.
gh pr create --base "$BRANCH" \
--assignee "${GITHUB_ACTOR}" \
--reviewer metabase/embedding \
--label sdk-release-trigger \
--title "chore(sdk): bump SDK version on $BRANCH to ${{ steps.metadata.outputs.new_version }}" \
--body "$(cat <<'BODY'
Bumps the Embedding SDK version in `package.template.json` and updates its `sdkRelease` metadata.
| | |
|---|---|
| Branch | `${{ github.ref_name }}` |
| Release type | `${{ inputs.release_type }}` |
| Version | `${{ steps.metadata.outputs.previous_version }}` → `${{ steps.metadata.outputs.new_version }}` |
| npm dist-tag | `${{ steps.metadata.outputs.dist_tag }}` |
| Tag as `latest` | `${{ steps.metadata.outputs.tag_as_latest }}` |
`distTag` and `tagAsLatest` are computed from the branch and release type. `tagAsLatest` is `true` only when patching the current gold major (`vars.LATEST_MAJOR_VERSION`).
Merging this PR (with the `sdk-release-trigger` label) triggers [release-embedding-sdk.yml](../actions/workflows/release-embedding-sdk.yml), which builds, publishes to npm, and tags the release.
_Opened by the [SDK Version Bump PR workflow](../actions/workflows/sdk-version-bump-pr.yml), triggered by @${{ github.event.sender.login }}._
BODY
)"

View File

@@ -162,10 +162,17 @@ jobs:
junit-name: semantic-search-tests-mysql-8-0
mysql-image: mysql:8.0
pgvector-image: pgvector/pgvector:pg17
- name: MySQL Latest with pgvector pg17
junit-name: semantic-search-tests-mysql-latest
mysql-image: mysql:latest
- name: MySQL 9 with pgvector pg17
junit-name: semantic-search-tests-mysql-9
mysql-image: mysql:9
pgvector-image: pgvector/pgvector:pg17
# `mysql:latest` now resolves to MySQL 26.x, which fails in a way that floods the log
# until the runner cancels the job. A cancelled job can't be soft-failed with
# continue-on-error, so the leg is disabled until 26.x is supported.
# - name: MySQL Latest with pgvector pg17
# junit-name: semantic-search-tests-mysql-latest
# mysql-image: mysql:latest
# pgvector-image: pgvector/pgvector:pg17
job:
- name: Semantic Search Tests
build-static-viz: false

View File

@@ -140,8 +140,8 @@ jobs:
run: |
RELEASE_BRANCH=${{ steps.release_branch.outputs.result }}
git checkout $RELEASE_BRANCH
git branch --contains ${{ inputs.commit }} | grep -q $RELEASE_BRANCH \
&& echo "Commit found in correct release branch" \
(git branch --contains ${{ inputs.commit }} | grep -q $RELEASE_BRANCH \
&& echo "Commit found in correct release branch") \
|| (echo "Commit not found in correct release branch" && exit 1)
git checkout -
@@ -196,7 +196,7 @@ jobs:
version='${{ needs.check-version.outputs.oss }}'
fi
echo "VERSION=$version" >> $GITHUB_ENV
echo "VERSION=$version" >> "$GITHUB_ENV"
- name: Setup Java
uses: actions/setup-java@v5.2.0
with:
@@ -215,9 +215,9 @@ jobs:
echo "Old version.properties:"
cat version.properties
sed -i "s/^tag=.*/tag=$VERSION/" version.properties
echo "\n\nNew version.properties:"
printf "\n\nNew version.properties:"
cat version.properties
echo "\n\nUpdating metabase.jar with new version.properties"
printf "\n\nUpdating metabase.jar with new version.properties"
jar uf metabase.jar version.properties
- name: Store commit's SHA-1 hash
@@ -280,12 +280,3 @@ jobs:
with:
commit: ${{ inputs.commit }}
version: ${{ inputs.version }}
auto-publish-release:
name: Auto-publish release ${{ inputs.version }}
needs: run-pre-release-tests
uses: ./.github/workflows/release.yml
secrets: inherit
with:
version: ${{ inputs.version }}
commit: ${{ inputs.commit }}

2
.gitignore vendored
View File

@@ -89,6 +89,7 @@ cypress.env.json
/resources/frontend_client/app/embedding-sdk.js.map
/resources/frontend_client/app/embedding-sdk/
/resources/frontend_client/tmp-*/
/resources/frontend_client/data-app.html
/resources/frontend_client/embed.html
/resources/frontend_client/embed-sdk.html
/resources/frontend_client/embed-mcp.html
@@ -150,6 +151,7 @@ mise.local.toml
**/.clj-kondo/*
!**/.clj-kondo/README.md
!**/.clj-kondo/config.edn
!**/.clj-kondo/ratchets.edn
!**/.clj-kondo/config/
!**/.clj-kondo/src/
!**/.clj-kondo/test/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 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: 392 KiB

After

Width:  |  Height:  |  Size: 392 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,27 @@
import { Comment, Converter } from "typedoc";
const TAG = "@notExported";
export function load(app) {
app.options.getDeclaration("blockTags").defaultValue?.push?.(TAG);
app.converter.on(Converter.EVENT_RESOLVE_END, (context) => {
const project = context.project;
const intentional = new Set(app.options.getValue("intentionallyNotExported"));
for (const id in project.reflections) {
const ref = project.reflections[id];
const tags = ref.comment?.blockTags ?? [];
for (const tag of tags) {
if (tag.tag === TAG) {
const name = Comment.combineDisplayParts(tag.content).trim();
if (name) {
intentional.add(name);
}
}
}
}
app.options.setValue("intentionallyNotExported", [...intentional]);
});
}

View File

@@ -10,6 +10,7 @@ const config = {
"typedoc-plugin-dt-links",
"typedoc-plugin-redirect",
"./typedoc-plugin-replace-text.js",
"./typedoc-plugin-not-exported.js",
],
entryPoints: ["../resources/embedding-sdk/dist/index.d.ts"],
router: "structure",

View File

@@ -77,6 +77,36 @@ It piggybacks on a running dev nREPL (~5s) and auto-spawns a JVM if none is runn
the four generated keys; structural changes it can't safely make (a new module needs a human `:team`, or
modules need reordering) are printed as `WARNING:` lines for you to resolve by hand.
## Kondo Ignore Ratchets
`.clj-kondo/ratchets.edn` records, per linter, how many inline `:clj-kondo/ignore` forms the backend source
tree may contain. `metabase.core.kondo-ratchet-test` fails when the budgets drift from the actual counts,
in either direction. Prefer fixing the underlying warning over adding an ignore.
Budget too high (you removed ignores): a local run of the test tightens the file for you — commit the
change. PRs labelled `kondo-ratchets-self-healing` get the lowered budgets committed to the branch by CI.
To tighten by hand (babashka, no JVM; a no-op prints `unchanged`):
```bash
./bin/mage fix-kondo-ratchets
```
Budget too low (you added an ignore): the task only raises a budget when told to. If the ignore is
genuinely required, run `./bin/mage fix-kondo-ratchets --seed :the-linter` and defend the increase in the
PR.
Introducing a new linter: `./bin/mage kondo-insert-ignores :the-linter` inserts an ignore at every site it
flags, then `./bin/mage fix-kondo-ratchets --seed :the-linter` records the budget — no big-bang cleanup.
To burn debt down, `./bin/mage kondo-redundant-ignores` lists ignores that are no longer needed (slow:
full kondo run). Kondo's redundancy report can't see hook-linter warnings, so `--fix` re-lints after
removing, puts any still-working ignore back exactly as it was, and stamps it with a `[kondo-keep]`
comment; marked sites are skipped on later runs. That verification needs a clean starting point, so
files with pre-existing lint findings are excluded from the sweep and reported. `--fix --audit` rechecks the
marked sites too, removing any that have become truly redundant along with their stamped marker
comments (a marker trailing on a code line is left for a hand fix). `[kondo-keep]` can also be added
by hand to protect an ignore whose exact form matters — it only counts on the line directly above the
ignore, or trailing on the ignore's own line.
## Tool Preferences
If `clojure-mcp` tools are available, prefer them over shell-based alternatives for Clojure development.

31
bb.edn
View File

@@ -1,7 +1,8 @@
{:min-bb-version "1.12.212"
;; we put path as bin, and everything is in the ./mage subdirectory,
;; so the namespaces are mage.cli, mage.format, etc.
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src"]
;; dev/src is here for tasks whose logic is shared with JVM tests (e.g. dev.kondo-ratchet)
:paths ["mage/src" "mage/test" "bin/lint-migrations-file/src" "dev/src"]
;; use Git version of dep temporarily until a new release > 0.16.4 is cut
:deps {dev.weavejester/cljfmt {:git/sha "075eae26e3c8dad433b88f05927bcd50e3571328"
:git/url "https://github.com/weavejester/cljfmt"} ;; run cljfmt from bb
@@ -12,7 +13,7 @@
org.babashka/json {:mvn/version "0.1.6"} ;; json parsing
table/table {:mvn/version "0.5.0"} ;; table printing
}
:pods {org.babashka/postgresql {:version "0.1.0"}}
;; No :pods key on purpose: tasks lazily load only what they need.
:tasks
{:requires [[mage.cli :as cli]
[mage.color :as c]]
@@ -274,6 +275,32 @@
:requires [[mage.modules]]
:task (task! (mage.modules/cli-fix-config (cli/parse! (current-task))))}
fix-kondo-ratchets
{:doc "Lower the per-linter ignore budgets in .clj-kondo/ratchets.edn to match the source tree."
:examples [["./bin/mage fix-kondo-ratchets" "Ratchet budgets down (never raises one; over-budget linters are only warned about)"]
["./bin/mage fix-kondo-ratchets --seed :some-linter" "Also set :some-linter's budget to its actual count, for landing a new linter"]]
:options [["-s" "--seed LINTER" "Set LINTER's budget to the actual count (may add or raise it)"]]
:requires [[dev.kondo-ratchet]]
:task (task! (dev.kondo-ratchet/fix! (:options parsed)))}
kondo-redundant-ignores
{:doc "Report inline kondo ignores that are no longer necessary (slow: full kondo run)."
:examples [["./bin/mage kondo-redundant-ignores" "Scan the backend tree for removable ignores"]
["./bin/mage kondo-redundant-ignores --fix" "Also remove them from source (restores + marks [kondo-keep] any that re-trigger)"]
["./bin/mage kondo-redundant-ignores --fix --audit" "Recheck [kondo-keep] sites too, unmarking any now truly redundant"]]
:options [["-f" "--fix" "Remove the candidate ignores from source"]
["-a" "--audit" "Include sites marked [kondo-keep] instead of skipping them"]]
:requires [[mage.kondo-ratchet]]
:task (task! (mage.kondo-ratchet/redundant-ignores parsed))}
kondo-insert-ignores
{:doc "Insert inline ignores at every site a linter flags -- for landing a new linter without a big-bang fix."
:examples [["./bin/mage kondo-insert-ignores :metabase/new-linter" "Insert ignores everywhere the linter fires, then seed its budget"]
["./bin/mage kondo-insert-ignores :equals-true test/metabase/pulse" "Only under the given paths"]]
:arg-schema [:sequential [:string {:name "linter-then-paths"}]]
:requires [[mage.kondo-ratchet]]
:task (task! (mage.kondo-ratchet/insert-ignores (:arguments parsed)))}
ls
{:doc "List all mage public and private tasks"
:examples [["./bin/mage ls" "List all tasks"]]

View File

@@ -169,7 +169,6 @@
(doseq [^File file (file-seq source-path)
:when (not (directory? file))
:when
#_{:clj-kondo/ignore [:discouraged-var]}
(or (str/ends-with? (str/lower-case file) "yaml")
(str/ends-with? (str/lower-case file) "yml"))]
(when verbose (out/safe-println "Zipping file:" file))

View File

@@ -27,7 +27,6 @@
(deftest ^:parallel backend-message?
(testing "messages present in any .clj and .cljc files are detected as backend messages"
#_{:clj-kondo/ignore [:equals-true]}
(are [source-references expected] (= expected
(i18n/backend-message? {:source-references source-references}))
;; Simple .clj and .cljc files with and without line numbers

View File

@@ -3,6 +3,7 @@ set -euo pipefail
# Requires: BASE_REF, CURRENT_VERSION (for master)
# Optional: PR_LABELS (comma-separated, for backport handling)
# OLDEST_SUPPORTED_VERSION (required when PR_LABELS contains `backport`)
if [[ -z "${BASE_REF:-}" ]]; then
echo "ERROR: BASE_REF is not set." >&2
@@ -39,30 +40,53 @@ elif [[ "$BASE_REF" == "master" ]]; then
fi
echo "CURRENT_VERSION: ${CURRENT_VERSION}"
matches=0
for l in backport single-backport double-backport triple-backport; do
if [[ ",$labels," == *",$l,"* ]]; then
# N.B. not ((matches++)) — that exits 1 when matches=0 under set -e
matches=$((matches + 1))
# Backport labels shift the target version back so migrations land on the earliest release
# branch the backport will reach. `backport` targets every supported release line (see
# .github/workflows/auto-backport.yml), so its floor is the oldest supported major from
# version-info.json, passed in as OLDEST_SUPPORTED_VERSION. The legacy depth labels keep
# their fixed offsets from CURRENT_VERSION.
candidates=()
if [[ ",$labels," == *",backport,"* ]]; then
if [[ -z "${OLDEST_SUPPORTED_VERSION:-}" ]]; then
echo "ERROR: PR has the 'backport' label but OLDEST_SUPPORTED_VERSION is not set." >&2
exit 1
fi
done
if ! [[ "$OLDEST_SUPPORTED_VERSION" =~ ^[1-9][0-9]*$ ]]; then
echo "ERROR: OLDEST_SUPPORTED_VERSION must be a positive integer, got: '${OLDEST_SUPPORTED_VERSION}'" >&2
exit 1
fi
echo "OLDEST_SUPPORTED_VERSION: ${OLDEST_SUPPORTED_VERSION}"
candidates+=("$OLDEST_SUPPORTED_VERSION")
fi
[[ ",$labels," == *",single-backport,"* ]] && candidates+=("$CURRENT_VERSION")
[[ ",$labels," == *",double-backport,"* ]] && candidates+=($((CURRENT_VERSION - 1)))
[[ ",$labels," == *",triple-backport,"* ]] && candidates+=($((CURRENT_VERSION - 2)))
constraint="="
version=$((CURRENT_VERSION + 1))
# Backport labels shift the target version back so migrations land on the correct release branch.
[[ ",$labels," == *",backport,"* ]] && version=$((CURRENT_VERSION))
[[ ",$labels," == *",single-backport,"* ]] && version=$((CURRENT_VERSION))
[[ ",$labels," == *",double-backport,"* ]] && version=$((CURRENT_VERSION - 1))
[[ ",$labels," == *",triple-backport,"* ]] && version=$((CURRENT_VERSION - 2))
# ${arr[@]+...} guards the empty-array case, which trips `set -u` on bash < 4.4
for candidate in ${candidates[@]+"${candidates[@]}"}; do
[[ "$candidate" -lt "$version" ]] && version=$candidate
done
[[ $matches -gt 1 ]] && echo "NOTE: Multiple backport labels detected; using the earliest version (v${version})." >&2
[[ ${#candidates[@]} -gt 1 ]] && echo "NOTE: Multiple backport labels detected; using the earliest version (v${version})." >&2
else
echo "Target branch '${BASE_REF}' is not master or a release branch — skipping."
exit 0
fi
# Per-feature migration directories (resources/migrations/NNN/) start at v60. Older versions keep a
# single shared file (resources/migrations/0NN_update_migrations.yaml), and pre-60 release branches
# only load those shared files (endsWithFilter: "_update_migrations.yaml"), so a per-feature file
# targeting a pre-60 version would be silently ignored there.
DIR_LAYOUT_FLOOR=60
expected_dir=$(printf "%03d" "$version")
echo "Expected directory: ${constraint} ${expected_dir}"
if [[ "$version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "Expected location: resources/migrations/${expected_dir}_update_migrations.yaml (per-feature directories start at v${DIR_LAYOUT_FLOOR})"
else
echo "Expected directory: ${constraint} ${expected_dir}"
fi
echo ""
# Check that new migration files are in the correct directory
@@ -84,9 +108,16 @@ while IFS= read -r file; do
dir_version=$((10#$dir))
dir=$(printf "%03d" "$dir_version")
if [[ "$constraint" == "=" && "$dir_version" -ne "$version" ]] ||
if [[ "$dir_version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "ERROR: ${file} targets v${dir_version}, but per-feature directories start at v${DIR_LAYOUT_FLOOR} — a pre-${DIR_LAYOUT_FLOOR} changeset goes in resources/migrations/${dir}_update_migrations.yaml" >&2
errors=$((errors + 1))
elif [[ "$constraint" == "=" && "$dir_version" -ne "$version" ]] ||
[[ "$constraint" == "<=" && "$dir_version" -gt "$version" ]]; then
echo "ERROR: ${file} is in ${dir} but expected ${constraint} ${expected_dir}" >&2
if [[ "$version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "ERROR: ${file} is in ${dir} but the changeset belongs in resources/migrations/${expected_dir}_update_migrations.yaml" >&2
else
echo "ERROR: ${file} is in ${dir} but expected ${constraint} ${expected_dir}" >&2
fi
errors=$((errors + 1))
fi
done <<<"$changed_files"
@@ -98,19 +129,24 @@ if [[ "$errors" -gt 0 ]]; then
echo "Why: Migration files must be in the directory matching the target release version." >&2
echo "A misplaced migration misrepresents when it was introduced and will break downgrades spanning more than one version." >&2
echo "" >&2
if [[ "$constraint" == "<=" ]]; then
echo "To fix: move the file(s) to resources/migrations/${expected_dir}/ or any earlier version directory." >&2
if [[ "$version" -lt "$DIR_LAYOUT_FLOOR" ]]; then
echo "To fix: append the changeset(s) to resources/migrations/${expected_dir}_update_migrations.yaml." >&2
echo "Pre-${DIR_LAYOUT_FLOOR} releases only load the shared per-version files, not NNN/ directories." >&2
elif [[ "$constraint" == "<=" ]]; then
echo "To fix: move the file(s) to resources/migrations/${expected_dir}/ or any earlier version directory (>= 0${DIR_LAYOUT_FLOOR})." >&2
else
echo "To fix: move the file(s) to resources/migrations/${expected_dir}/." >&2
fi
echo "" >&2
echo "If this PR will be backported, ensure the correct backport label is applied" >&2
echo "(backport, single-backport, double-backport, or triple-backport) and re-run CI, even if you" >&2
echo "plan to create the backport PRs manually. Base the label on the oldest" >&2
echo "release you're targeting, and simply close any unwanted PRs." >&2
echo "and re-run CI, even if you plan to create the backport PRs manually:" >&2
echo " - backport: all supported release lines (the check targets the oldest one)" >&2
echo " - single-backport: the current release only" >&2
echo " - double-backport / triple-backport: 1 or 2 releases before the current one" >&2
echo "Base the label on the oldest release you're targeting, and simply close any unwanted PRs." >&2
echo "" >&2
echo "If you are backporting more than 3 releases back, or this is an exceptional" >&2
echo "circumstance, you will need to either update this script or force merge." >&2
echo "If none of the labels matches the releases you're targeting, or this is an" >&2
echo "exceptional circumstance, you will need to either update this script or force merge." >&2
exit 1
fi

View File

@@ -1,12 +1,15 @@
#!/usr/bin/env node
/* eslint-env node */
/* eslint-disable import/no-commonjs, import/order, no-console */
/* eslint-disable import/order */
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
const glob = require("glob");
const { Extractor, ExtractorConfig } = require("@microsoft/api-extractor");
const SDK_DIST_DIR_PATH = path.resolve("./resources/embedding-sdk/dist");
const API_EXTRACTOR_BIN_PATH = path.resolve(
"./node_modules/@microsoft/api-extractor/bin/api-extractor",
);
/*
* This script replaces all custom aliases in Embedding SDK generated ".d.ts" files so that this imports could be resolved
@@ -26,10 +29,52 @@ const REPLACES_MAP = {
cljs: "target/cljs_release",
};
const API_EXTRACTOR_CONFIG_PATH = path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-api-extractor.json",
);
const DTS_ROLLUPS = [
{
name: "main",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/index.d.ts",
),
},
{
name: "data-app",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-data-app-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/data-app.d.ts",
),
},
{
name: "data-app-dev",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-data-app-dev-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/data-app-dev.d.ts",
),
},
{
name: "data-app-dev-config",
configPath: path.join(
__dirname,
"../../enterprise/frontend/src/embedding-sdk-package/embedding-sdk-data-app-dev-config-api-extractor.json",
),
entryPointPath: path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/data-app-dev.config.d.ts",
),
},
];
const getLogger = (prefix) => {
const verbose = process.env.SDK_FIXUP_VERBOSE_LOGS === "true";
@@ -110,7 +155,7 @@ const removeUnresolvedReexports = (filePath) => {
}
};
const fixupTypesAfterCompilation = ({ isWatchMode }) => {
const fixupTypesAfterCompilation = async ({ isWatchMode }) => {
log("Fixing SDK d.ts files...");
const dtsFilePaths = glob.sync(`${SDK_DIST_DIR_PATH}/**/*.d.ts`);
@@ -123,45 +168,97 @@ const fixupTypesAfterCompilation = ({ isWatchMode }) => {
console.log("[dts fixup] Done!");
if (!isWatchMode) {
generateDtsRollup();
await generateDtsRollup();
}
};
const generateDtsRollup = () => {
const generateDtsRollup = async () => {
// Dts rollup logger
const { log, error } = getLogger("dts rollup");
log("Generate dts rollup...");
const dtsRollupEntryPointPath = path.resolve(
path.join(
__dirname,
"../../resources/embedding-sdk/dist/enterprise/frontend/src/embedding-sdk-package/index.d.ts",
),
);
const dtsRollupEntryPointPathExist = fs.existsSync(dtsRollupEntryPointPath);
const existingEntryPointCount = DTS_ROLLUPS.filter(({ entryPointPath }) =>
fs.existsSync(path.resolve(entryPointPath)),
).length;
if (!dtsRollupEntryPointPathExist) {
if (existingEntryPointCount === 0) {
log("It looks like dts rollup is already generated, skipping...");
return;
}
const apiExtractorConfig = ExtractorConfig.loadFileAndPrepare(
API_EXTRACTOR_CONFIG_PATH,
if (existingEntryPointCount !== DTS_ROLLUPS.length) {
DTS_ROLLUPS.forEach(({ entryPointPath, name }) => {
const resolvedEntryPointPath = path.resolve(entryPointPath);
if (!fs.existsSync(resolvedEntryPointPath)) {
error(
`Dts rollup entry point for ${name} does not exist: ${resolvedEntryPointPath}`,
);
}
});
process.exitCode = 1;
return;
}
let hasExtractorFailure = false;
// The extractors are independent (each reads the shared compiled d.ts tree
// and writes its own rollup), so run them concurrently. Output is buffered
// per extractor and printed on completion so parallel runs don't interleave.
const runApiExtractor = ({ configPath, name }) =>
new Promise((resolve) => {
const child = spawn(
process.execPath,
[
API_EXTRACTOR_BIN_PATH,
"run",
"--local",
"--verbose",
"--config",
path.resolve(configPath),
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
let output = "";
child.stdout.on("data", (chunk) => (output += chunk));
child.stderr.on("data", (chunk) => (output += chunk));
child.on("close", (status, signal) =>
resolve({ name, status, signal, output }),
);
});
const results = await Promise.all(
DTS_ROLLUPS.filter(({ entryPointPath, name }) => {
if (fs.existsSync(path.resolve(entryPointPath))) {
return true;
}
error(
`Dts rollup entry point for ${name} does not exist: ${path.resolve(entryPointPath)}`,
);
hasExtractorFailure = true;
return false;
}).map(runApiExtractor),
);
const extractorResult = Extractor.invoke(apiExtractorConfig, {
localBuild: true,
showVerboseMessages: true,
});
for (const { name, status, signal, output } of results) {
log(`--- api-extractor: ${name} ---`);
process.stdout.write(output);
if (!extractorResult.succeeded) {
error(
`API Extractor completed with ${extractorResult.errorCount} errors` +
` and ${extractorResult.warningCount} warnings`,
);
if (status !== 0 || signal) {
error(
`API Extractor failed for ${name}` +
(signal ? ` with signal ${signal}` : ""),
);
hasExtractorFailure = true;
}
}
if (hasExtractorFailure) {
process.exitCode = 1;
return;
}
log("API Extractor completed successfully");
@@ -220,7 +317,7 @@ const isWatchMode = process.argv.includes("--watch");
const run = async () => {
// when running on a clean state and with --watch, the folder might not exist yet
await waitForFolder(SDK_DIST_DIR_PATH);
fixupTypesAfterCompilation({ isWatchMode });
await fixupTypesAfterCompilation({ isWatchMode });
if (isWatchMode) {
console.log("\n\n\n");

View File

@@ -42,10 +42,16 @@ function generateSdkPackage() {
);
const sdkPackageTemplateJsonContent = JSON.parse(sdkPackageTemplateJson);
// `sdkRelease` is release-process metadata (distTag/tagAsLatest) read by
// .github/workflows/release-embedding-sdk.yml — it must never ship inside
// the published npm package.json.
const { sdkRelease: _sdkRelease, ...publishableTemplateJsonContent } =
sdkPackageTemplateJsonContent;
const todayDate = new Date().toJSON().slice(0, 10).replaceAll("-", "");
const mergedContent = {
...sdkPackageTemplateJsonContent,
...publishableTemplateJsonContent,
version: maybeCommitHash
? `${sdkPackageTemplateJsonContent.version}-${todayDate}-${maybeCommitHash}`
: sdkPackageTemplateJsonContent.version,

View File

@@ -1,11 +0,0 @@
#!/bin/bash
function update_readme() {
sed -i -E 's|(embedding-sdk-)[0-9.]+|\1'"$1"'|' enterprise/frontend/src/embedding-sdk-package/README.md
sed -i -E 's|(http://downloads.metabase.com/sdk/v)[0-9.]+|\1'"$1"'|' enterprise/frontend/src/embedding-sdk-package/README.md
}
function update_package_json_template() {
sed -i -E 's/("version": ").*"/\1'"$1"'"/' enterprise/frontend/src/embedding-sdk-package/package.template.json
}
$1 $2

View File

@@ -207,16 +207,42 @@
(str/join ", " bad-ids))
{:invalid-ids (vec bad-ids)}))))))
(defn require-primary-key-exists-has-table-name
"Ensures that all primaryKeyExists preconditions specify a tableName."
(def ^:private table-scoped-preconditions
"Existence preconditions mapped to the key that scopes them to a single table. Without that key,
Liquibase answers the check by snapshotting the whole schema — ~2 seconds per precondition on a
large postgres appdb, which is how 27 name-only preconditions once added ~50s to every full
migration."
{:primaryKeyExists :tableName
:indexExists :tableName
:uniqueConstraintExists :tableName
:foreignKeyConstraintExists :foreignKeyTableName})
(defn- unscoped-preconditions
"Walk a preConditions tree (maps nested under and/or/not, in either flat-map or list form) and
return [precondition-key required-key] pairs for every table-scoped existence check that is
missing its table key."
[form]
(cond
(map? form) (concat
(for [[precondition required] table-scoped-preconditions
:let [check (get form precondition)]
:when (and (map? check) (not (contains? check required)))]
[precondition required])
(mapcat unscoped-preconditions (vals form)))
(sequential? form) (mapcat unscoped-preconditions form)
:else nil))
(defn require-table-scoped-existence-preconditions
"Ensures that primaryKeyExists/indexExists/uniqueConstraintExists preconditions specify a
tableName, and foreignKeyConstraintExists a foreignKeyTableName."
[change-log]
(doseq [{:keys [changeSet]} change-log
:when changeSet
:let [s (pr-str (:preConditions changeSet))]
:when (and (str/includes? s ":primaryKeyExists")
(not (str/includes? s ":tableName")))]
:let [[precondition required] (first (unscoped-preconditions (:preConditions changeSet)))]
:when precondition]
(throw (validation-error
(format "Migration '%s' has primaryKeyExists precondition without tableName" (:id changeSet))
(format "Migration '%s' has %s precondition without %s: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
(:id changeSet) (name precondition) (name required))
{:id (:id changeSet)}))))
(s/def ::changeSet
@@ -292,7 +318,7 @@
(require-no-bare-blob-or-text-types change-log)
(require-no-bare-boolean-types change-log file)
(require-no-datetime-type change-log file)
(require-primary-key-exists-has-table-name change-log)
(require-table-scoped-existence-preconditions change-log)
(let [{:keys [changeSet]
:as all} (group-by only-key change-log)]
(when-not (set/subset? (set (keys all)) #{:property :objectQuotingStrategy :changeSet :logicalFilePath})
@@ -368,17 +394,14 @@
(println)
#_:clj-kondo/ignore
(printf "Error in %s:\t%s\n" (:file (ex-data e)) (.getMessage e))
#_:clj-kondo/ignore
(printf "Details:\n\n %s" (with-out-str (pprint/pprint (dissoc (ex-data e) ::validation-error))))
(println))
(do
(pprint/pprint (Throwable->map e))
(println (.getMessage e))))
#_:clj-kondo/ignore
(System/exit 1))
(catch #_:clj-kondo/ignore
Throwable e
(pprint/pprint (Throwable->map e))
(println (.getMessage e))
#_:clj-kondo/ignore
(System/exit 1))))

View File

@@ -398,3 +398,40 @@
:remarks "none"
:type "timestamp with time zone"}}]}}]
:rollback nil))))))
(deftest ^:parallel require-table-scoped-existence-preconditions-test
(testing "foreignKeyConstraintExists without foreignKeyTableName is rejected"
(is-thrown-with-error-info?
"Migration 'v49.2024-01-01T10:30:00' has foreignKeyConstraintExists precondition without foreignKeyTableName: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
{:id "v49.2024-01-01T10:30:00"}
(validate
(mock-change-set
:preConditions [{:onFail "MARK_RAN"}
{:not [{:foreignKeyConstraintExists {:foreignKeyName "fk_my_table_other_id"}}]}]))))
(testing "foreignKeyConstraintExists with foreignKeyTableName passes"
(is (= :ok
(validate
(mock-change-set
:preConditions [{:onFail "MARK_RAN"}
{:not [{:foreignKeyConstraintExists {:foreignKeyName "fk_my_table_other_id"
:foreignKeyTableName "my_table"}}]}])))))
(testing "indexExists without tableName is rejected, including in flat-map not form"
(is-thrown-with-error-info?
"Migration 'v49.2024-01-01T10:30:00' has indexExists precondition without tableName: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
{:id "v49.2024-01-01T10:30:00"}
(validate
(mock-change-set
:preConditions [{:not {:indexExists {:indexName "idx_my_table_other_id"}}}]))))
(testing "indexExists with tableName passes"
(is (= :ok
(validate
(mock-change-set
:preConditions [{:not {:indexExists {:indexName "idx_my_table_other_id"
:tableName "my_table"}}}])))))
(testing "primaryKeyExists without tableName is still rejected (previous rule subsumed)"
(is-thrown-with-error-info?
"Migration 'v49.2024-01-01T10:30:00' has primaryKeyExists precondition without tableName: without it Liquibase snapshots the entire schema to answer the check (~2s per precondition on a large postgres appdb)"
{:id "v49.2024-01-01T10:30:00"}
(validate
(mock-change-set
:preConditions [{:primaryKeyExists {:primaryKeyName "pk_my_table"}}])))))

149
bun.lock
View File

@@ -67,7 +67,6 @@
"@tiptap/starter-kit": "^3.2.0",
"@tiptap/suggestion": "^3.2.0",
"@types/js-yaml": "^4.0.9",
"@types/react-router": "3.0.27",
"@uiw/react-codemirror": "^4.23.6",
"@visx/axis": "^3.10.1",
"@visx/clip-path": "^3.3.0",
@@ -102,7 +101,6 @@
"formik": "^2.4.5",
"frimousse": "^0.3.0",
"fuse.js": "^7.1.0",
"history": "3",
"html2canvas-pro": "^2.0.2",
"icepick": "2.4.0",
"iframe-resizer": "~4.3.2",
@@ -118,6 +116,7 @@
"leaflet-draw": "^0.4.9",
"lodash.debounce": "^4.0.8",
"lodash.orderby": "^4.6.0",
"marked": "18.0.6",
"merge-refs": "^1.3.0",
"mustache": "^2.3.2",
"normalizr": "^3.0.2",
@@ -145,7 +144,6 @@
"react-qr-code": "^2.0.15",
"react-redux": "^9.1.2",
"react-resizable": "^3.1.3",
"react-router": "3",
"react-transition-group": "^4.4.5",
"react-use": "^17.5.1",
"react-virtualized": "9.22.6",
@@ -153,6 +151,7 @@
"redux-undo": "^1.1.0",
"rehype-external-links": "^3.0.0",
"remark-gfm": "^4.0.0",
"remend": "1.3.0",
"reselect": "^5.1.0",
"resize-observer-polyfill": "^1.5.1",
"server-text-width": "^1.0.2",
@@ -277,6 +276,7 @@
"@types/tether": "^1.4.6",
"@types/underscore": "^1.13.0",
"@types/use-sync-external-store": "^1.5.0",
"@vitejs/plugin-react": "^6.0.2",
"ajv": "^8.5.0",
"babel-loader": "^9.1.2",
"babel-plugin-ttag": "^1.8.16",
@@ -378,7 +378,7 @@
"raf": "^3.4.0",
"react-docgen-typescript-plugin": "^1.0.6",
"react-refresh": "^0.14.2",
"react-router-v7": "npm:react-router@7.18.1",
"react-router": "7.18.1",
"reg-cli": "^0.17.7",
"semver": "^7.6.3",
"shadow-cljs": "^3.3.5",
@@ -402,6 +402,9 @@
"typedoc-plugin-redirect": "^1.2.0",
"typescript-eslint": "8.60.0",
"typescript-plugin-css-modules": "^5.1.0",
"vite": "^8.0.16",
"vite-plugin-css-injected-by-js": "^3.5.2",
"vite-plugin-svgr": "^5.2.0",
"web-streams-polyfill": "^4.1.0",
"webpack": "5.105.0",
"webpack-bundle-analyzer": "^4.10.1",
@@ -1275,6 +1278,8 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.34.0", "", {}, "sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA=="],
"@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="],
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.38.0", "", { "os": "android", "cpu": "arm" }, "sha512-lTN4//sgYywK8ulQo7a/EZVzOTGomGQv2IG/7tMYdqTV3xN3QTqWpXcZBGUzaicC4B882N+5zJLYZ37IWfUMcg=="],
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.38.0", "", { "os": "android", "cpu": "arm64" }, "sha512-XbVgqR1WsIcCkfxwh2tdg3M1MWgR23YOboW2nbB8ab0gInNNLGy7cIAdr78XaoG/bGdaF4488XRhuGWq67xrzA=="],
@@ -1439,6 +1444,40 @@
"@remirror/core-constants": ["@remirror/core-constants@3.0.0", "", {}, "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
"@rspack/binding": ["@rspack/binding@2.0.3", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "2.0.3", "@rspack/binding-darwin-x64": "2.0.3", "@rspack/binding-linux-arm64-gnu": "2.0.3", "@rspack/binding-linux-arm64-musl": "2.0.3", "@rspack/binding-linux-x64-gnu": "2.0.3", "@rspack/binding-linux-x64-musl": "2.0.3", "@rspack/binding-wasm32-wasi": "2.0.3", "@rspack/binding-win32-arm64-msvc": "2.0.3", "@rspack/binding-win32-ia32-msvc": "2.0.3", "@rspack/binding-win32-x64-msvc": "2.0.3" } }, "sha512-4exVNhGhW5RFHjK87XeTKbkA/qAgI5NHJlT1jNqiJv0gcUXLqTOEU3w7f8+f9zUo4JMFvPc0c9veOi4M19YYTg=="],
"@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@2.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4UyCjLJwU/WxR6K1/gG4u3+jUsoaRHJ5rNu9fto/UbvrItwdlVNULChAApqZFw6mcSetMddSjSICeuj5pSB6sA=="],
@@ -1969,8 +2008,6 @@
"@types/react-resizable": ["@types/react-resizable@3.0.4", "", { "dependencies": { "@types/react": "*" } }, "sha512-+QguN9CDfC1lthq+4noG1fkxh8cqkV2Fv/Mu3mdknCCBiwwNLecnBdk1MmNNN7uJpT23Nx/aVkYsbt5NuWouFw=="],
"@types/react-router": ["@types/react-router@3.0.27", "", { "dependencies": { "@types/history": "^3", "@types/react": "*" } }, "sha512-Sq4bfTZ8TuHFTHXyzA3tu/BTGm/d4B8CwtOK4qOyJNoJMkY87qMOazkBizMHNpszVR1+kYOCNLjPdh/wAD7ESw=="],
"@types/react-textarea-autosize": ["@types/react-textarea-autosize@4.3.6", "", { "dependencies": { "@types/react": "*" } }, "sha512-cTf8tCem0c8A7CERYbTuF+bRFaqYu7N7HLCa6ZhUhDx8XnUsTpGx5udMWljt87JpciUKuUkImKPEsy6kcKhrcQ=="],
"@types/react-transition-group": ["@types/react-transition-group@4.4.5", "", { "dependencies": { "@types/react": "*" } }, "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA=="],
@@ -2157,6 +2194,8 @@
"@visx/vendor": ["@visx/vendor@3.5.0", "", { "dependencies": { "@types/d3-array": "3.0.3", "@types/d3-color": "3.1.0", "@types/d3-delaunay": "6.0.1", "@types/d3-format": "3.0.1", "@types/d3-geo": "3.1.0", "@types/d3-interpolate": "3.0.1", "@types/d3-scale": "4.0.2", "@types/d3-time": "3.0.0", "@types/d3-time-format": "2.1.0", "d3-array": "3.2.1", "d3-color": "3.1.0", "d3-delaunay": "6.0.2", "d3-format": "3.1.0", "d3-geo": "3.1.0", "d3-interpolate": "3.0.1", "d3-scale": "4.0.2", "d3-time": "3.1.0", "d3-time-format": "4.1.0", "internmap": "2.0.3" } }, "sha512-yt3SEZRVmt36+APsCISSO9eSOtzQkBjt+QRxNRzcTWuzwMAaF3PHCCSe31++kkpgY9yFoF+Gfes1TBe5NlETiQ=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="],
"@vitest/expect": ["@vitest/expect@2.0.5", "", { "dependencies": { "@vitest/spy": "2.0.5", "@vitest/utils": "2.0.5", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" } }, "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA=="],
"@vitest/pretty-format": ["@vitest/pretty-format@2.1.8", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ=="],
@@ -2681,8 +2720,6 @@
"create-hmac": ["create-hmac@1.1.7", "", { "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", "inherits": "^2.0.1", "ripemd160": "^2.0.0", "safe-buffer": "^5.0.1", "sha.js": "^2.4.8" } }, "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg=="],
"create-react-class": ["create-react-class@15.7.0", "", { "dependencies": { "loose-envify": "^1.3.1", "object-assign": "^4.1.1" } }, "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng=="],
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
"cron-expression-validator": ["cron-expression-validator@1.0.20", "", {}, "sha512-g0osBTdp+1ryDw2vzlG6UpDPaa4fO94ZChF2R0lEnRurbuUEL74XEVX7xZJ13m4Mq/gb3ni6UQu8+Oqt+eocsw=="],
@@ -2905,7 +2942,7 @@
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-newline": ["detect-newline@3.1.0", "", {}, "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA=="],
@@ -3119,7 +3156,7 @@
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
@@ -3401,8 +3438,6 @@
"headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="],
"history": ["history@3.3.0", "", { "dependencies": { "invariant": "^2.2.1", "loose-envify": "^1.2.0", "query-string": "^4.2.2", "warning": "^3.0.0" } }, "sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw="],
"hmac-drbg": ["hmac-drbg@1.0.1", "", { "dependencies": { "hash.js": "^1.0.3", "minimalistic-assert": "^1.0.0", "minimalistic-crypto-utils": "^1.0.1" } }, "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
@@ -3853,6 +3888,30 @@
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"lines-and-columns": ["lines-and-columns@1.1.6", "", {}, "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA="],
@@ -3953,7 +4012,7 @@
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
"marked": ["marked@18.0.6", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w=="],
"marky": ["marky@1.2.5", "", {}, "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q=="],
@@ -4629,8 +4688,6 @@
"qs": ["qs@6.15.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw=="],
"query-string": ["query-string@4.3.4", "", { "dependencies": { "object-assign": "^4.1.0", "strict-uri-encode": "^1.0.0" } }, "sha1-u7aTucqRXCMlFbIosaArYJBD2+s="],
"querystring": ["querystring@0.2.0", "", {}, "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="],
"querystring-es3": ["querystring-es3@0.2.1", "", {}, "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="],
@@ -4713,9 +4770,7 @@
"react-resizable": ["react-resizable@3.1.3", "", { "dependencies": { "prop-types": "15.x", "react-draggable": "^4.5.0" }, "peerDependencies": { "react": ">= 16.3", "react-dom": ">= 16.3" } }, "sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw=="],
"react-router": ["react-router@3.2.6", "", { "dependencies": { "create-react-class": "^15.5.1", "history": "^3.0.0", "hoist-non-react-statics": "^3.3.2", "invariant": "^2.2.1", "loose-envify": "^1.2.0", "prop-types": "^15.7.2", "react-is": "^16.13.0", "warning": "^3.0.0" } }, "sha512-nlxtQE8B22hb/JxdaslI1tfZacxFU8x8BJryXOnR2RxB4vc01zuHYAHAIgmBkdk1kzXaA25hZxK6KAH/+CXArw=="],
"react-router-v7": ["react-router@7.18.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg=="],
"react-router": ["react-router@7.18.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
@@ -4811,6 +4866,8 @@
"remedial": ["remedial@1.0.8", "", {}, "sha512-/62tYiOe6DzS5BqVsNpH/nkGlX45C/Sp6V+NtiN6JQNS1Viay7cWkazmRkrQrdFj2eshDe96SIQNIoMxqhzBOg=="],
"remend": ["remend@1.3.0", "", {}, "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw=="],
"remove-trailing-spaces": ["remove-trailing-spaces@1.0.8", "", {}, "sha512-O3vsMYfWighyFbTd8hk8VaSj9UAGENxAtX+//ugIst2RMk5e03h6RoIS+0ylsFxY1gvmPuAY/PO4It+gPEeySA=="],
"renderkid": ["renderkid@3.0.0", "", { "dependencies": { "css-select": "^4.1.3", "dom-converter": "^0.2.0", "htmlparser2": "^6.1.0", "lodash": "^4.17.21", "strip-ansi": "^6.0.1" } }, "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg=="],
@@ -4869,6 +4926,8 @@
"robust-predicates": ["robust-predicates@3.0.2", "", {}, "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg=="],
"rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="],
"rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="],
"rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="],
@@ -5093,8 +5152,6 @@
"strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
"strict-uri-encode": ["strict-uri-encode@1.1.0", "", {}, "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM="],
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
"string-length": ["string-length@6.0.0", "", { "dependencies": { "strip-ansi": "^7.1.0" } }, "sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg=="],
@@ -5461,6 +5518,12 @@
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
"vite": ["vite@8.1.4", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.5", "postcss": "^8.5.16", "rolldown": "~1.1.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ=="],
"vite-plugin-css-injected-by-js": ["vite-plugin-css-injected-by-js@3.5.2", "", { "peerDependencies": { "vite": ">2.0.0-0" } }, "sha512-2MpU/Y+SCZyWUB6ua3HbJCrgnF0KACAsmzOQt1UvRVJCGF6S8xdA3ZUhWcWdM9ivG4I5az8PnQmwwrkC2CAQrQ=="],
"vite-plugin-svgr": ["vite-plugin-svgr@5.2.0", "", { "dependencies": { "@rollup/pluginutils": "^5.3.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, "peerDependencies": { "vite": ">=3.0.0" } }, "sha512-qj2eAKF8C6PZWemVTvQA0xgQIcP1hHU6Buh7fl6BhvayWwnuxE+z417miKxeDvRWbDrupQ1oK99hfxElopJ3sQ=="],
"vitest-axe": ["vitest-axe@0.1.0", "", { "dependencies": { "aria-query": "^5.0.0", "axe-core": "^4.4.2", "chalk": "^5.0.1", "dom-accessibility-api": "^0.5.14", "lodash-es": "^4.17.21", "redent": "^3.0.0" } }, "sha512-jvtXxeQPg8R/2ANTY8QicA5pvvdRP4F0FsVUAHANJ46YCDASie/cuhlSzu0DGcLmZvGBSBNsNuK3HqfaeknyvA=="],
"vm-browserify": ["vm-browserify@1.1.2", "", {}, "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ=="],
@@ -5473,8 +5536,6 @@
"walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="],
"warning": ["warning@3.0.0", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w="],
"watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="],
"watskeburt": ["watskeburt@5.0.3", "", { "bin": { "watskeburt": "dist/run-cli.js" } }, "sha512-g9CXukMjazlJJVQ3OHzXsnG25KFYgSgKMIyoJrD8ggr0DbS9UNF7OzIqWmmKKBMedkxj3T01uqEaGnn+y7QhMA=="],
@@ -5815,6 +5876,8 @@
"@nicolo-ribaudo/eslint-scope-5-internals/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"@parcel/watcher/detect-libc": ["detect-libc@1.0.3", "", { "bin": { "detect-libc": "./bin/detect-libc.js" } }, "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="],
"@pmmmwh/react-refresh-webpack-plugin/source-map": ["source-map@0.7.3", "", {}, "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="],
"@redocly/cli/ajv": ["@redocly/ajv@8.17.4", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-BieiCML/IgP6x99HZByJSt7fJE4ipgzO7KAFss92Bs+PEI35BhY7vGIysFXLT+YmS7nHtQjZjhOQyPPEf7xGHA=="],
@@ -5835,6 +5898,16 @@
"@redocly/respect-core/colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
"@rollup/pluginutils/@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@rollup/pluginutils/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"@rushstack/node-core-library/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
"@rushstack/node-core-library/fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
@@ -6055,12 +6128,8 @@
"@vue/compiler-core/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"@vue/compiler-core/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-sfc/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
"@vue/compiler-sfc/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@vue/compiler-sfc/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"@vue/compiler-sfc/postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
@@ -6953,9 +7022,7 @@
"react-remove-scroll-bar/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"react-router/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-router-v7/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"react-router/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
"react-style-singleton/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
@@ -6985,6 +7052,8 @@
"redoc/classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
"redoc/marked": ["marked@4.3.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A=="],
"reduce-css-calc/balanced-match": ["balanced-match@0.4.2", "", {}, "sha512-STw03mQKnGUYtoNjmowo4F2cRmIIxYEGiMsjjwla/u5P1lxadj/05WkNaFjNiKTgJkj8KiXbgAiRTmcQRwQNtg=="],
"reduce-function-call/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
@@ -7007,8 +7076,12 @@
"ripemd160/hash-base": ["hash-base@3.1.0", "", { "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", "safe-buffer": "^5.2.0" } }, "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA=="],
"rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
"rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
"rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
"router/path-to-regexp": ["path-to-regexp@8.4.0", "", {}, "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg=="],
"rxjs/tslib": ["tslib@2.6.2", "", {}, "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="],
@@ -7197,6 +7270,12 @@
"vfile-message/@types/unist": ["@types/unist@3.0.2", "", {}, "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="],
"vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"vite/postcss": ["postcss@8.5.16", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg=="],
"vite/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
"vitest-axe/aria-query": ["aria-query@5.0.0", "", {}, "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg=="],
"vitest-axe/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
@@ -7463,6 +7542,14 @@
"@redocly/cli/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@rolldown/binding-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@rolldown/binding-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@storybook/addon-docs/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"@storybook/addon-webpack5-compiler-babel/babel-loader/find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="],
@@ -8331,6 +8418,8 @@
"unbox-primitive/which-boxed-primitive/is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
"vite/postcss/nanoid": ["nanoid@3.3.15", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA=="],
"webpack-dev-middleware/mime-types/mime-db": ["mime-db@1.51.0", "", {}, "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g=="],
"webpack-dev-middleware/schema-utils/@types/json-schema": ["@types/json-schema@7.0.9", "", {}, "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="],
@@ -8517,6 +8606,8 @@
"@redocly/cli/glob/minimatch/brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@storybook/addon-webpack5-compiler-babel/babel-loader/find-cache-dir/pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="],
"@storybook/addon-webpack5-compiler-babel/babel-loader/schema-utils/@types/json-schema": ["@types/json-schema@7.0.9", "", {}, "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="],

View File

@@ -74,6 +74,8 @@ if [[ ! -d "$SPECS_DIR" ]]; then
closest=""
closest_dist=999
for dir in "$REPO_ROOT"/e2e/cross-version/[0-9]*/; do
# Skip the literal pattern when the glob matches no numbered folders
[[ -d "$dir" ]] || continue
v=$(basename "$dir")
(( v < MAJOR )) && continue
dist=$(( v - MAJOR ))

View File

@@ -89,6 +89,8 @@ run_e2e() {
if [[ ! -d "$specs_dir" ]]; then
local closest="" closest_dist=999
for dir in "$REPO_ROOT"/e2e/cross-version/[0-9]*/; do
# Skip the literal pattern when the glob matches no numbered folders
[[ -d "$dir" ]] || continue
local v=$(basename "$dir")
(( v < major )) && continue
local dist=$(( v - major ))

View File

@@ -80,7 +80,7 @@
hiccup/hiccup {:mvn/version "2.0.0"} ; HTML templating
inflections/inflections {:mvn/version "0.15.0"} ; Clojure/Script library used for prularizing words
instaparse/instaparse {:mvn/version "1.5.0"} ; Make your own parser
io.github.camsaul/toucan2 {:mvn/version "1.0.571"}
io.github.camsaul/toucan2 {:mvn/version "1.0.572"}
io.github.eerohele/pp {#_#_:git/tag "2024-11-13.77" ; super fast pretty-printing library
:git/sha "ffc4edc482c057cd9412c62f018a41e9140c618b"
:git/url "https://github.com/eerohele/pp"}
@@ -126,7 +126,7 @@
{:mvn/version "2.26.1"} ; allows the custom json logging format
org.apache.logging.log4j/log4j-slf4j2-impl
{:mvn/version "2.26.1"} ; allows the slf4j2 API to work with log4j 2
org.apache.pdfbox/pdfbox {:mvn/version "3.0.5"} ; PDF generation (backend dashboard PDF prototype)
org.apache.pdfbox/pdfbox {:mvn/version "3.0.8"} ; PDF generation (backend dashboard PDF prototype)
org.apache.poi/poi {:mvn/version "5.5.1"} ; Work with Office documents (e.g. Excel spreadsheets) -- newer version than one specified by Docjure
org.apache.poi/poi-ooxml {:mvn/version "5.5.1"
:exclusions [org.bouncycastle/bcpkix-jdk15on

View File

@@ -158,7 +158,6 @@
(let [h2-dbs (t2/select :model/Database :engine :h2)
in-memory? (fn [db] (some-> db :details :db (str/starts-with? "mem:")))
can-connect? (fn [db]
#_:clj-kondo/ignore
(binding [driver.settings/*allow-testing-h2-connections* true]
(try
(driver/can-connect? :h2 (:details db))

View File

@@ -0,0 +1,252 @@
(ns dev.kondo-ratchet
"Ratchet on inline kondo ignore forms.
Per-linter budgets live in `.clj-kondo/ratchets.edn`.
`metabase.core.kondo-ratchet-test` fails when they drift from the tree;
`./bin/mage fix-kondo-ratchets` lowers budgets, never raises them.
Loaded by both the bb task and the JVM test, so keep it dependency-free."
{:clj-kondo/config '{:linters {:discouraged-var {clojure.core/println {:level :off}}}}}
(:require
[clojure.edn :as edn]
[clojure.java.io :as io]
[clojure.string :as str]))
(set! *warn-on-reflection* true)
(def ratchets-file
"The budgets file, relative to the repo root."
".clj-kondo/ratchets.edn")
(def ^:private source-roots
["src" "test" "enterprise" "modules/drivers" "dev" "bin" "mage"])
(def ^:private source-extensions
[".clj" ".cljc" ".cljs"])
;; Concatenated so this file never contains a literal ignore marker.
(def ^:private ignore-marker
(str ":clj-kondo" "/ignore"))
;; `#_{... [:some-linter]}`, `^{... [:some-linter]}`, and the prefix-less attr-map form
;; `(ns foo {... [:some-linter]})`; the vector may span lines. The lazy tail after the vector runs to the
;; map's own closing brace, so extra keys still count and removal spans the whole form; a nested-brace
;; value stops the match at the vector instead.
(def ^:private vector-form-re
(re-pattern (str "(?:(?:#_|\\^)\\s*)?\\{\\s*" ignore-marker "\\s*\\[([^\\]]*)\\](?:[^{}]*?\\})?")))
;; Bare `#_kw` / `^kw` with no linter vector: suppresses every linter on the next form.
(def ^:private bare-form-re
(re-pattern (str "(?:#_\\s*|\\^)" ignore-marker "(?![\\w./-])")))
(defn mask-strings-and-comments
"`content` with string-literal and line-comment interiors replaced by spaces, newlines kept.
Same length as the input, so offsets and line numbers carry over.
Ignore forms inside strings (test fixtures) or commented-out code must not count."
[content]
(let [sb (StringBuilder. ^String content)
n (count content)]
(loop [i 0, state :code]
(if (>= i n)
(str sb)
(let [c (.charAt sb i)]
(case state
:code (case c
\" (recur (inc i) :string)
\; (do (.setCharAt sb i \space)
(recur (inc i) :comment))
;; char literal: never treat the next char as a delimiter
\\ (recur (+ i 2) :code)
(recur (inc i) :code))
:string (case c
\" (recur (inc i) :code)
\\ (do (.setCharAt sb i \space)
(when (< (inc i) n)
(when-not (= (.charAt sb (inc i)) \newline)
(.setCharAt sb (inc i) \space)))
(recur (+ i 2) :string))
\newline (recur (inc i) :string)
(do (.setCharAt sb i \space)
(recur (inc i) :string)))
:comment (if (= c \newline)
(recur (inc i) :code)
(do (.setCharAt sb i \space)
(recur (inc i) :comment)))))))))
(defn- linter-keywords
[vector-contents]
(map (comp keyword #(subs % 1))
(re-seq #":[A-Za-z][A-Za-z0-9*+!?<>=._/-]*" vector-contents)))
(defn line-linters
"Linter keywords suppressed by inline ignore forms on `line`.
The bare vector-less form counts as `:all`.
Like [[scan]], ignore forms inside string literals or line comments don't count."
[line]
(let [masked (mask-strings-and-comments line)]
(concat (mapcat (comp linter-keywords second) (re-seq vector-form-re masked))
(repeat (count (re-seq bare-form-re masked)) :all))))
(defn- offset->line
"1-based line number of character offset `i` in `content`."
[content i]
(inc (count (filter #(= % \newline) (subs content 0 i)))))
(defn- matches-with-offsets
"Like re-seq, but returns `{:start _, :end _, :linters [...]}` for each match of `re` in `masked`."
[re masked bare?]
(let [m (re-matcher re masked)]
(loop [acc []]
(if (.find m)
(recur (conj acc {:start (.start m)
:end (.end m)
:linters (if bare? [:all] (vec (linter-keywords (.group m 1))))}))
acc))))
(defn ignore-matches
"Inline ignore matches in `content`, in file order:
`{:start _, :end _, :line _, :linters [...]}` with character offsets and a 1-based line.
Matches inside string literals or line comments are excluded."
[content]
(let [masked (mask-strings-and-comments content)]
(->> (concat (matches-with-offsets vector-form-re masked false)
(matches-with-offsets bare-form-re masked true))
(sort-by :start)
(map #(assoc % :line (offset->line masked (:start %)))))))
(defn scan
"Occurrences of inline ignore forms under `roots` (relative to the repo root).
Returns `{:file \"src/...\", :line 42, :linters [...]}` maps.
Forms inside string literals or line comments don't count."
([]
(scan source-roots))
([roots]
(for [root roots
^java.io.File f (file-seq (io/file root))
:when (and (.isFile f)
(some #(str/ends-with? (.getPath f) %) source-extensions))
:let [content (slurp f)]
:when (str/includes? content ignore-marker)
m (ignore-matches content)]
{:file (.getPath f)
:line (:line m)
:linters (:linters m)})))
(defn actual-counts
"Per-linter occurrence counts for `occurrences`, as returned by [[scan]]."
[occurrences]
(frequencies (mapcat :linters occurrences)))
(defn- sorted-by-str
[kvs]
(into (sorted-map-by #(compare (str %1) (str %2))) kvs))
(defn drift
"Linters whose count in `occurrences` differs from its budget in `recorded` (absent = 0, either side).
Returns `{linter {:recorded _, :actual _}}`, plus `:examples` (up to 5 `file:line`) when over budget."
[recorded occurrences]
(let [actual (actual-counts occurrences)]
(sorted-by-str
(for [linter (into (set (keys actual)) (keys recorded))
:let [budget (get recorded linter 0)
n (get actual linter 0)]
:when (not= budget n)]
[linter (cond-> {:recorded budget, :actual n}
(> n budget)
(assoc :examples (->> occurrences
(filter #(some #{linter} (:linters %)))
(map #(str (:file %) ":" (:line %)))
(take 5)
vec)))]))))
(defn read-ratchets
"Parsed contents of [[ratchets-file]], with empty defaults when the file doesn't exist."
[]
(merge {:ignore-counts {}}
(when (.exists (io/file ratchets-file))
(edn/read-string (slurp ratchets-file)))))
(def ^:private header
(str ";; Per-linter budgets for inline `" ignore-marker "` forms.\n"
";; metabase.core.kondo-ratchet-test fails when the budgets drift from the actual counts.\n"
";; `./bin/mage fix-kondo-ratchets` lowers budgets to match the tree; local test runs do it\n"
";; automatically. Raising a budget, or adding one for a new linter (`--seed`), is a hand edit\n"
";; to defend in your PR.\n"
";; :all is the vector-less ignore form, which suppresses every linter on the next form.\n"))
(defn render
"Text of the ratchets file for the `{:ignore-counts _}` map `ratchets`.
Byte-stable: [[fix!]] idempotency and the file-hygiene test depend on it."
[{:keys [ignore-counts]}]
(let [counts-indent (apply str (repeat (count "{:ignore-counts {") \space))]
(str header
"{:ignore-counts "
(if (empty? ignore-counts)
"{}"
(let [entries (sort-by (comp str first) ignore-counts)
width (apply max (map (comp count str first) entries))]
(str "{"
(str/join (str "\n" counts-indent)
(for [[linter n] entries]
(format (str "%-" width "s %d") (str linter) n)))
"}")))
"}\n")))
(defn lowered-counts
"`recorded` with each budget lowered to its actual count; entries with no ignores left are dropped.
Linters in `seeded` get their budget set to the actual count outright — the explicit escape hatch for
landing a new linter. Otherwise never raises a budget, never adds one."
[recorded actual seeded]
(into (sorted-by-str
(for [linter seeded
:when (pos? (get actual linter 0))]
[linter (get actual linter)]))
(keep (fn [[linter budget]]
(let [n (get actual linter 0)]
(cond
(contains? (set seeded) linter) nil
(zero? n) nil
(< n budget) [linter n]
:else [linter budget]))))
recorded))
(defn change-report
"The lines [[fix!]] prints: lowered/dropped/seeded budgets, plus warnings for anything over budget."
[{:keys [ignore-counts]} occurrences seeded]
(let [actual (actual-counts occurrences)]
(concat
(for [linter seeded
:let [n (get actual linter 0)]]
(if (pos? n)
(format "seeded %s at %d" linter n)
(format "WARNING: %s has no inline ignores -- nothing to seed" linter)))
(for [[linter budget] (sort-by (comp str first) (apply dissoc ignore-counts seeded))
:let [n (get actual linter 0)]
:when (not= n budget)]
(cond
(zero? n) (format "dropped %s (no ignores left)" linter)
(< n budget) (format "lowered %s %d -> %d" linter budget n)
:else (format "WARNING: %s is over budget (%d recorded, %d actual) -- remove ignores, or accept them all with `--seed %s`"
linter budget n linter)))
(for [[linter n] (sort-by (comp str first) (apply dissoc actual (concat seeded (keys ignore-counts))))]
(format "WARNING: %s has %d ignores but no budget entry -- seed one with `./bin/mage fix-kondo-ratchets --seed %s`"
linter n linter)))))
(defn fix!
"Rewrite [[ratchets-file]]: lower budgets and normalize formatting.
`--seed LINTER` (`{:seed \"...\"}` here) sets that budget to the actual count, adding or raising it.
Prints the [[change-report]], or `unchanged` on a no-op."
([]
(fix! nil))
([{:keys [seed]}]
(let [{:keys [ignore-counts] :as ratchets} (read-ratchets)
occurrences (scan)
seeded (if seed [(keyword (str/replace-first seed #"^:" ""))] [])
actual (actual-counts occurrences)
text (render {:ignore-counts (lowered-counts ignore-counts actual seeded)})
file (io/file ratchets-file)
old (when (.exists file) (slurp file))]
(run! println (change-report ratchets occurrences seeded))
(if (= old text)
(println "unchanged")
(do (spit file text)
(println (str "wrote " ratchets-file)))))))

View File

@@ -182,6 +182,7 @@ Metabase's reference documentation.
- [Managing people and groups](./people-and-groups/managing.md)
- [Password complexity](./people-and-groups/changing-password-complexity.md)
- [Session expiration](./people-and-groups/changing-session-expiration.md)
- [Two-factor authentication](./people-and-groups/two-factor-authentication.md)
- [Google Sign-In](./people-and-groups/google-sign-in.md)
- [LDAP](./people-and-groups/ldap.md)
- [API keys](./people-and-groups/api-keys.md)

View File

@@ -76,7 +76,7 @@ If something goes wrong, Metabase will display the error message it received fro
### Example `UPDATE` action
You could write an action that would update the `plan` column for a record in the `invoices` table in the Sample Database:
You could write an action that would update the `plan` column for a record in the `invoices` table:
```sql
{% raw %}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -15,7 +15,7 @@ System prompts let you customize instructions for Metabot. You can write a separ
- **Natural language queries**: query-builder questions generated from natural language.
- **SQL generation**: SQL written from prompts (sidebar or [inline](./metabot.md#inline-sql-editing)).
Like text cards on dashboards, prompts support [Markdown](https://www.metabase.com/learn/metabase-basics/querying-and-dashboards/dashboards/markdown).
Like text cards on dashboards, prompts support [Markdown](../dashboards/markdown.md).
## Prompt tips

3895
docs/api.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@ Metabase Cloud only works with:
- [Metabase's officially supported databases](../databases/connecting.md#connecting-to-supported-databases) (with the exception of SQLite and H2).
Metabase Cloud doesn't support [community database drivers](../developers-guide/community-drivers.md), or file-based databases (SQLite and H2), because there's currently no file storage available.
Metabase Cloud doesn't support [community database drivers](../developers-guide/community-drivers.md), or file-based databases (SQLite), because there's currently no file storage available.
## Limited custom certificate support

View File

@@ -6,9 +6,9 @@ title: "Configuration file"
{% include plans-blockquote.html feature="Loading from a configuration file" self-hosted-only="true" %}
On self-hosted [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans, Metabase supports initialization on launch from a config file named `config.yml`. The config file should be located at:
On self-hosted [Pro](https://www.metabase.com/product/pro) and [Enterprise](https://www.metabase.com/product/enterprise) plans, Metabase supports initialization on launch from a config file. The config file should be located at:
- The current directory (the directory where the running Metabase JAR is located).
- A file named `config.yml` or `config.yaml` in the current directory (the directory where the running Metabase JAR is located).
- The path specified by the [`MB_CONFIG_FILE_PATH`](./environment-variables.md#mb_config_file_path) environment variable.
The settings in the config file work the same as if you'd set the settings in the Admin Settings in your Metabase. Settings defined in this configuration file will update any existing settings. If, for example, a database already exists (that is, you'd already added it via the initial set up or **Admin settings** > **Databases**, Metabase will update the database entry based on the data in the config file). Which means: if you define a setting in the config file, and then later change that setting in your Metabase application, keep in mind that the config file will overwrite that change whenever Metabase restarts. Let's reiterate that in a blockquote:

View File

@@ -210,8 +210,26 @@ config:
ldap-user-filter: (&(objectClass=inetOrgPerson)(|(uid={login})(mail={login})))
ldap-user-provisioning-enabled: true
license-token-missing-banner-dismissal-timestamp: []
llm-anthropic-api-base-url: https://api.anthropic.com
llm-anthropic-api-key: null
llm-anthropic-model: claude-opus-4-5-20251101
llm-azure-api-base-url: null
llm-azure-api-key: null
llm-bedrock-access-key-id: null
llm-bedrock-region: us-east-1
llm-bedrock-secret-access-key: null
llm-bedrock-session-token: null
llm-connection-timeout-ms: 5000
llm-max-tokens: 4096
llm-metabot-provider: anthropic/claude-sonnet-4-6
llm-openai-api-base-url: https://api.openai.com
llm-openai-api-key: null
llm-openai-model: gpt-5.4
llm-openrouter-api-base-url: https://openrouter.ai/api
llm-openrouter-api-key: null
llm-rate-limit-per-ip: 100
llm-rate-limit-per-user: 20
llm-request-timeout-ms: 60000
load-analytics-content: true
loading-message: doing-science
login-page-illustration: default
@@ -242,9 +260,10 @@ config:
persisted-model-refresh-cron-schedule: 0 0 0/6 * * ? *
persisted-models-enabled: false
premium-embedding-token: null
query-caching-early-refresh-ratio: 0.1
query-caching-max-kb: 2000
query-caching-max-ttl: 3024000.0
queue-backend: quartz
queue-backend: null
queue-max-retries: 5
queue-no-listener-max-age-ms: 86400000
redirect-all-requests-to-https: false

View File

@@ -1287,6 +1287,14 @@ When set to `true`, users who log in via LDAP will automatically get a Metabase
The array of last two ISO8601 dates when an admin dismissed the license token missing banner.
### `MB_LLM_ANTHROPIC_API_BASE_URL`
- Type: string
- Default: `https://api.anthropic.com`
- [Configuration file name](./config-file.md): `llm-anthropic-api-base-url`
The Anthropic API base URL.
### `MB_LLM_ANTHROPIC_API_KEY`
- Type: string
@@ -1295,6 +1303,78 @@ The array of last two ISO8601 dates when an admin dismissed the license token mi
The Anthropic API Key.
### `MB_LLM_ANTHROPIC_MODEL`
- Type: string
- Default: `claude-opus-4-5-20251101`
- [Configuration file name](./config-file.md): `llm-anthropic-model`
The Anthropic model to use.
### `MB_LLM_AZURE_API_BASE_URL`
- Type: string
- Default: `null`
- [Configuration file name](./config-file.md): `llm-azure-api-base-url`
The base URL of the Azure resource's OpenAI- or Anthropic-compatible surface, e.g. `https://<resource>.services.ai.azure.com/openai`.
### `MB_LLM_AZURE_API_KEY`
- Type: string
- Default: `null`
- [Configuration file name](./config-file.md): `llm-azure-api-key`
The API key for the Azure resource hosting your models.
### `MB_LLM_BEDROCK_ACCESS_KEY_ID`
- Type: string
- Default: `null`
- [Configuration file name](./config-file.md): `llm-bedrock-access-key-id`
The AWS Access Key ID for Amazon Bedrock.
### `MB_LLM_BEDROCK_REGION`
- Type: string
- Default: `us-east-1`
- [Configuration file name](./config-file.md): `llm-bedrock-region`
The AWS region for Amazon Bedrock (e.g. us-east-1).
### `MB_LLM_BEDROCK_SECRET_ACCESS_KEY`
- Type: string
- Default: `null`
- [Configuration file name](./config-file.md): `llm-bedrock-secret-access-key`
The AWS Secret Access Key for Amazon Bedrock.
### `MB_LLM_BEDROCK_SESSION_TOKEN`
- Type: string
- Default: `null`
- [Configuration file name](./config-file.md): `llm-bedrock-session-token`
The AWS Session Token for Amazon Bedrock. Only needed for temporary credentials.
### `MB_LLM_CONNECTION_TIMEOUT_MS`
- Type: integer
- Default: `5000`
- [Configuration file name](./config-file.md): `llm-connection-timeout-ms`
Connection timeout in milliseconds for LLM API requests.
### `MB_LLM_MAX_TOKENS`
- Type: integer
- Default: `4096`
- [Configuration file name](./config-file.md): `llm-max-tokens`
Maximum tokens for LLM responses.
### `MB_LLM_METABOT_PROVIDER`
- Type: string
@@ -1303,6 +1383,70 @@ The Anthropic API Key.
The AI provider and model for Metabot. Format: provider/model-name, e.g. `anthropic/claude-haiku-4-5`, `openai/gpt-5.4`, `openrouter/anthropic/claude-haiku-4.5`.
### `MB_LLM_OPENAI_API_BASE_URL`
- Type: string
- Default: `https://api.openai.com`
- [Configuration file name](./config-file.md): `llm-openai-api-base-url`
The OpenAI API base URL.
### `MB_LLM_OPENAI_API_KEY`
- Type: string
- Default: `null`
- [Configuration file name](./config-file.md): `llm-openai-api-key`
The OpenAI API Key.
### `MB_LLM_OPENAI_MODEL`
- Type: string
- Default: `gpt-5.4`
- [Configuration file name](./config-file.md): `llm-openai-model`
The OpenAI Model (e.g. 'gpt-5.5', 'gpt-5.4-mini').
### `MB_LLM_OPENROUTER_API_BASE_URL`
- Type: string
- Default: `https://openrouter.ai/api`
- [Configuration file name](./config-file.md): `llm-openrouter-api-base-url`
The OpenRouter API base URL used for Chat Completions.
### `MB_LLM_OPENROUTER_API_KEY`
- Type: string
- Default: `null`
- [Configuration file name](./config-file.md): `llm-openrouter-api-key`
The OpenRouter API Key.
### `MB_LLM_RATE_LIMIT_PER_IP`
- Type: integer
- Default: `100`
- [Configuration file name](./config-file.md): `llm-rate-limit-per-ip`
Maximum SQL generation requests per IP address per minute.
### `MB_LLM_RATE_LIMIT_PER_USER`
- Type: integer
- Default: `20`
- [Configuration file name](./config-file.md): `llm-rate-limit-per-user`
Maximum SQL generation requests per user per minute.
### `MB_LLM_REQUEST_TIMEOUT_MS`
- Type: integer
- Default: `60000`
- [Configuration file name](./config-file.md): `llm-request-timeout-ms`
Socket timeout in milliseconds for LLM API requests.
### `MB_LOAD_ANALYTICS_CONTENT`
- Type: boolean
@@ -1580,6 +1724,14 @@ Allow persisting models into the source database.
Token for premium features. Go to the MetaStore to get yours!
### `MB_QUERY_CACHING_EARLY_REFRESH_RATIO`
- Type: double
- Default: `0.1`
- [Configuration file name](./config-file.md): `query-caching-early-refresh-ratio`
Refresh cached results this fraction of their cache duration before they expire, so requests keep being served from cache instead of waiting for a recomputation. Set to 0 to only refresh once results have expired.
### `MB_QUERY_CACHING_MAX_KB`
- Type: integer
@@ -1599,9 +1751,9 @@ The absolute maximum time to keep any cached query results, in seconds.
### `MB_QUEUE_BACKEND`
- Type: string
- Default: `quartz`
- Default: `null`
Which queue backend to use. Valid values: `quartz`, `memory`.
Which queue backend to use. Valid values: `quartz`, `memory`. When unset, defaults to `quartz`, or to `memory` when the task scheduler is disabled (`MB_DISABLE_SCHEDULER`).
### `MB_QUEUE_MAX_RETRIES`
@@ -1881,8 +2033,7 @@ on your IdP, this usually looks something like `http://www.example.com/141xkex60
- Default: `null`
- [Configuration file name](./config-file.md): `saml-identity-provider-slo-uri`
This is the URL where your users go to logout of your identity provider. Depending on which IdP you're
using, this usually looks like `https://your-org-name.example.com` or `https://example.com/app/my_saml_app/abc123/sso/slo`.
If SAML single logout (SLO) is enabled, Metabase will make an HTTP-Redirect SLO request to this endpoint when a user logs out of Metabase.
### `MB_SAML_IDENTITY_PROVIDER_URI`
@@ -1933,7 +2084,7 @@ Absolute path to the Keystore file to use for signing SAML requests.
- Default: `false`
- [Configuration file name](./config-file.md): `saml-slo-enabled`
Is SAML Single Log Out enabled?
If enabled, Metabase will redirect users to your configured SAML Single Logout endpoint when they log out of Metabase.
### `MB_SAML_USER_PROVISIONING_ENABLED`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -82,7 +82,7 @@ You can use [Markdown](https://commonmark.org/help/) to format the text in your
![Result](images/result.png)
To learn more, see [Fun with Markdown in your dashboards](https://www.metabase.com/learn/metabase-basics/querying-and-dashboards/dashboards/markdown).
To learn more, see [Format text with Markdown](./markdown.md).
### Including variables in text cards
@@ -321,12 +321,14 @@ See [Caching per dashboard](../configuring-metabase/caching.md#dashboard-caching
## Sharing dashboards with public links
If your Metabase administrator has enabled [public sharing](../embedding/public-links.md) on a saved question or dashboard, you can go to that question or dashboard and click on the **sharing** icon to find its public links.
If your Metabase administrator has enabled [public sharing](../embedding/public-links.md) on a saved question or dashboard, you can go to that question or dashboard and click on the **Share** icon to find its public links.
![Share icon](./images/share-icon.png)
Public links can be viewed by anyone, even if they don't have access to Metabase. You can also use the public embedding code to embed your question or dashboard in a simple web page or blog post. Check out examples of simple apps with embedded dashboards in our [embedding-reference-apps repository](https://github.com/metabase/embedding-reference-apps). To learn more about [embedding](../embedding/introduction.md), check out our article on [How to use Metabase to deliver analytics to your customers](https://www.metabase.com/learn/metabase-basics/embedding/overview), as well as an article on how to combine branding, Single Sign-On, full app embedding, and row and column security to deliver [multi-tenant, self-service analytics](https://www.metabase.com/learn/metabase-basics/embedding/multi-tenant-self-service-analytics).
Admins can also [create an account](../people-and-groups/managing.md#creating-an-account) for someone and point them to a specific dashboard or question. On a dashboard or question, click the **Sharing** icon and select **Invite someone to view this**.
## Exporting results from a dashboard
You can export the entire dashboard as PDF, or export results of individual questions on the dashboard. See [Exporting results](../questions/exporting-results.md).

View File

@@ -79,7 +79,7 @@ To nest a bulleted item under another bulleted item, indent the nested item with
To add a link, put the link text in square brackets, followed by the URL in parentheses:
```text
[the internet's finest webpage](https://www.metabase.com)
[the internet's finest webpage](https://www.example.com)
```
This renders as a clickable link: [the internet's finest webpage](https://www.metabase.com).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -34,7 +34,7 @@ People in the Data Analyst group will have table metadata and data structure acc
_Data Studio > Tables_
You can see all tables in all databases connected to your Metabase in **Data Studio > Tables**, together with their owner, row count, and published state. For now, Metabase only displays row counts for PostgreSQL tables.
You can see all tables in all databases connected to your Metabase in **Data Studio > Tables**, together with their owner, visibility layer, row count, and published state. For now, Metabase only displays row counts for PostgreSQL tables.
You can search for table names, but the search will only match beginnings of words in table names. So for example, if you search for "base", results will include names like "Baseball stats" and "All your base are belong to us", but the results won't include tables like "Metabase secrets".

View File

@@ -37,7 +37,7 @@ With basic transforms, you can:
- **Metabase Cloud**: Basic transform functionality on Metabase Cloud - Starter, Pro, or Enterprise - comes with an additional small fee per successful transform run, see [Pricing](https://www.metabase.com/pricing).
Only people logged in with an email of a [Metabase Store admin](../../cloud/accounts-and-billing.md#add-managers) (not just Metabase _instance_ admins) can [enable basic transforms](./transforms-overview.md#enable-transforms).
Only people logged in with an email of a [Metabase Store admin](../../cloud/accounts-and-billing.md#add-managers) (not just Metabase _instance_ admins) can [enable basic transforms](./transforms-overview.md#enable-transforms).
### Cancel basic transforms
@@ -65,6 +65,8 @@ The Advanced transforms add-on comes with an additional charge per successful tr
To enable Advanced transforms functionality, you need to have [Basic transforms](#basic-transforms) already, see [Enable basic transforms](#enable-basic-transforms).
For the full self-hosted setup see [Set up transforms on a self-hosted Metabase](transforms-overview.md#set-up-transforms-on-a-self-hosted-metabase).
There are two ways to enable Advanced transforms:
- **From your Metabase instance**: you can navigate to a feature requiring advanced transforms (like Python transforms or transform inspector), and follow the prompts to upgrade.

View File

@@ -38,7 +38,7 @@ Jobs have two components: schedule and tags.
- **Schedule** determines when the job will be executed: daily, hourly, etc. You can specify a custom cron schedule (e.g. "Every weekday at 9:05 AM"). The times are given in your Metabase's system timezone.
- **Tags** determine _which_ transforms a job runs, not when the job runs. For example, you can create a `Weekdays` tag, add that tag to a few transforms, then create a job that runs all the transforms with the `Weekdays` tag every weekday at 9:05AM.
Jobs will run all transforms tagged with any of the tags, plus any transforms that the tagged transforms depend on, see [Jobs will run all dependent transforms](#jobs-will-run-all-dependent-transforms).
Jobs will run all transforms tagged with any of the tags, plus any transforms that the tagged transforms depend on that aren't already up to date, see [Jobs include all dependent transforms](#jobs-include-all-dependent-transforms).
You can see which transforms a job will run and in which order on the job's page.
@@ -104,13 +104,22 @@ To delete a job:
2. Find the job you want to delete and click the **three dots** icon to the right of the job's name.
3. Select **Delete**.
## Jobs will run all dependent transforms
## Jobs include all dependent transforms
If one transform depends on another, Metabase will run the dependency first, even if that transform isn't tagged in the job. So if transform B depends on A, Metabase will first run A, even if A doesn't have a tag.
If one transform depends on another, Metabase adds the dependency to the job and runs it first, even if that transform isn't tagged in the job. So if transform B depends on A, Metabase makes sure A is up to date before running B.
This means that you can explicitly tag transform A to run daily, and transform B hourly, but because transform B depends on transform A, transform A will _also_ run hourly (in addition to daily), despite not having the tag.
## Jobs skip dependencies that are already up to date
You can see which transforms a job will run (and in which order) on the job's page in **Data Studio > Jobs**.
Metabase won't re-run a dependency that's still fresh. That way, a transform pulled into a frequent job doesn't get rebuilt more often than its own schedule calls for. So, for example, if you tag transform A `daily`, and transform B `hourly`, and hourly B depends on daily A, then A runs on its own daily schedule. The hourly job that runs B won't rerun A every hour.
Metabase skips a dependency when the dependency:
- Has its own tags and jobs, and none of those jobs' schedules have come due since the dependency last ran successfully.
- Has no tags at all, and it's already run successfully at least once.
## See which transforms a job will run
The job's page in **Data Studio > Jobs** lists every transform the job will run and in which order. The **Notes** column tells you when Metabase will skip a dependency, and flags dependencies that have no schedule of their own.
## Runs
@@ -120,4 +129,11 @@ You can see all past and current transform runs (both manual and scheduled) by g
You can click on any transform run to see more details about the run, like the error logs. To go to the transform definition from the transform run page, click on the icon next the transform name in the right sidebar.
The "Tags" column in the **Runs** table will only show the transform's specific tags. But the run might not have anything to do with those tags. Another job with different tags could have run the transform because [jobs will run _all_ dependent transforms](#jobs-will-run-all-dependent-transforms).
The "Tags" column in the **Runs** table will only show the transform's specific tags. But the run might not have anything to do with those tags. Another job with different tags could have run the transform because [jobs include all dependent transforms](#jobs-include-all-dependent-transforms).
## Emails about failed transforms
If your Metabase is [set up to send email](../../configuring-metabase/email.md), Metabase will let people know when transforms fail.
- **Individual transform failures**: when transforms fail during a scheduled job run, Metabase emails the last person to edit each failed transform (or the transform's creator, or admins if neither is active). Each email covers a single job run.
- **Daily digest of job failures**: each morning, Metabase emails all admins a summary of the scheduled job runs that failed or timed out the previous day. Manual runs aren't included in the digest, and Metabase skips the email entirely if no scheduled runs failed.

View File

@@ -11,7 +11,7 @@ To run Python transforms from a self-hosted Metabase, you'll need to configure a
## Prerequisites
- Docker installed and running.
- Docker installed and running, or an infrastructure that can run containers.
- **Self-hosted Metabase Pro or Enterprise license** (Python Runner requires a Pro/Enterprise plan).
- For production: an S3-compatible storage bucket (AWS S3, MinIO, etc.).
@@ -69,13 +69,13 @@ docker network create metabase-network
docker run -d \
--network metabase-network \
-e AUTH_TOKEN=your-secure-token-here \
--name python-runner metabase/python-runner:latest
--name python-runner --hostname python-runner metabase/python-runner:latest
docker run -d \
--network metabase-network \
-p 3000:3000 \
-e MB_PYTHON_RUNNER_URL=http://python-runner:5000 \
-e MB_PYTHON_RUNNER_API_TOKEN=your-secure-token-here \
-e MB_PYTHON_RUNNER_API_TOKEN=<your-secure-token-here> \
-e MB_PYTHON_STORAGE_S_3_ENDPOINT=https://s3.amazonaws.com \
-e MB_PYTHON_STORAGE_S_3_BUCKET=your-bucket-name \
-e MB_PYTHON_STORAGE_S_3_REGION=us-east-1 \
@@ -97,82 +97,27 @@ docker run -d \
These settings can also be configured in the Metabase UI at **Admin** > **Settings** > **Python Runner**. Note that environment variables take precedence over UI settings.
| Variable | Description |
| ----------------------------------------- | ---------------------------------------------------------------------------------- |
| `MB_PYTHON_RUNNER_URL` | URL where Metabase can reach the Python Runner (e.g., `http://python-runner:5000`) |
| `MB_PYTHON_RUNNER_API_TOKEN` | Authentication token. Must match `AUTH_TOKEN` in the Python Runner. |
| `MB_PYTHON_STORAGE_S_3_ENDPOINT` | S3 endpoint URL. |
| `MB_PYTHON_STORAGE_S_3_BUCKET` | S3 bucket name for storing Python artifacts |
| `MB_PYTHON_STORAGE_S_3_REGION` | AWS region (e.g., `us-east-1`) |
| `MB_PYTHON_STORAGE_S_3_ACCESS_KEY` | S3 access key |
| `MB_PYTHON_STORAGE_S_3_SECRET_KEY` | S3 secret key |
| `MB_PYTHON_STORAGE_S_3_PATH_STYLE_ACCESS` | (Optional) Set to `true` for S3-compatible services like MinIO or LocalStack |
| Variable | Description |
| ------------------------------------------ | ---------------------------------------------------------------------------------- |
| `MB_PYTHON_RUNNER_URL` | URL where Metabase can reach the Python Runner (e.g., `http://python-runner:5000`) |
| `MB_PYTHON_RUNNER_API_TOKEN` | Authentication token. Must match `AUTH_TOKEN` in the Python Runner. |
| `MB_PYTHON_STORAGE_S_3_ENDPOINT` | S3 endpoint URL. |
| `MB_PYTHON_STORAGE_S_3_CONTAINER_ENDPOINT` | S3 endpoint seen from the runner container, if different from the main endpoint. |
| `MB_PYTHON_STORAGE_S_3_BUCKET` | S3 bucket name for storing Python artifacts |
| `MB_PYTHON_STORAGE_S_3_REGION` | AWS region (e.g., `us-east-1`) |
| `MB_PYTHON_STORAGE_S_3_ACCESS_KEY` | S3 access key |
| `MB_PYTHON_STORAGE_S_3_SECRET_KEY` | S3 secret key |
| `MB_PYTHON_STORAGE_S_3_PATH_STYLE_ACCESS` | (Optional) Set to `true` for S3-compatible services like MinIO or LocalStack |
`MB_PYTHON_STORAGE_S_3_CONTAINER_ENDPOINT` is the host Metabase signs into the presigned URLs the runner uploads to and downloads from. Set it when the runner resolves storage by a different hostname than Metabase does.
## Using Docker Compose
Docker Compose simplifies managing multiple containers. Below are example configurations for different scenarios.
Below are example configurations for different scenarios.
### Simple setup with LocalStack
### Self-hosted storage with MinIO
For simple setup and local testing, you can use LocalStack to simulate S3:
```yml
name: metabase-python-runner
services:
metabase:
image: metabase/metabase-enterprise:latest
ports:
- "3000:3000"
environment:
- MB_PYTHON_RUNNER_URL=http://python-runner:5000
- MB_PYTHON_RUNNER_API_TOKEN=your-secure-token-here
- MB_PYTHON_STORAGE_S_3_ENDPOINT=http://localstack:4566
- MB_PYTHON_STORAGE_S_3_BUCKET=metabase-python-runner
- MB_PYTHON_STORAGE_S_3_REGION=us-east-1
- MB_PYTHON_STORAGE_S_3_PATH_STYLE_ACCESS=true
- MB_PYTHON_STORAGE_S_3_ACCESS_KEY=test
- MB_PYTHON_STORAGE_S_3_SECRET_KEY=test
depends_on:
localstack-init:
condition: service_completed_successfully
python-runner:
condition: service_started
python-runner:
image: metabase/python-runner:latest
environment:
- AUTH_TOKEN=your-secure-token-here
localstack:
image: localstack/localstack:latest
environment:
- SERVICES=s3
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
interval: 5s
timeout: 5s
retries: 10
localstack-init:
image: localstack/localstack:latest
entrypoint:
["/bin/sh", "-c", "awslocal s3 mb s3://metabase-python-runner || true"]
environment:
- AWS_ENDPOINT_URL=http://localstack:4566
depends_on:
localstack:
condition: service_healthy
```
Then run:
```bash
docker compose up -d
```
### Production setup
For production, use an external S3-compatible storage service:
[MinIO](https://min.io/) is an S3-compatible server you can run yourself, which makes it a common pick for self-hosted Metabase. This Compose file runs Metabase, the Python runner, MinIO, and a short-lived container that creates the bucket Metabase will use — neither MinIO nor Metabase creates that bucket for you.
```yml
name: metabase-python-runner
@@ -184,12 +129,16 @@ services:
environment:
- MB_PYTHON_RUNNER_URL=http://python-runner:5000
- MB_PYTHON_RUNNER_API_TOKEN=${AUTH_TOKEN}
- MB_PYTHON_STORAGE_S_3_ENDPOINT=${S3_ENDPOINT}
- MB_PYTHON_STORAGE_S_3_BUCKET=${S3_BUCKET}
- MB_PYTHON_STORAGE_S_3_REGION=${S3_REGION:-us-east-1}
- MB_PYTHON_STORAGE_S_3_ACCESS_KEY=${S3_ACCESS_KEY}
- MB_PYTHON_STORAGE_S_3_SECRET_KEY=${S3_SECRET_KEY}
- MB_PYTHON_STORAGE_S_3_ENDPOINT=http://minio:9000
- MB_PYTHON_STORAGE_S_3_CONTAINER_ENDPOINT=http://minio:9000
- MB_PYTHON_STORAGE_S_3_BUCKET=metabase-python-runner
- MB_PYTHON_STORAGE_S_3_REGION=us-east-1
- MB_PYTHON_STORAGE_S_3_PATH_STYLE_ACCESS=true
- MB_PYTHON_STORAGE_S_3_ACCESS_KEY=${MINIO_ROOT_USER}
- MB_PYTHON_STORAGE_S_3_SECRET_KEY=${MINIO_ROOT_PASSWORD}
depends_on:
minio-init:
condition: service_completed_successfully
python-runner:
condition: service_started
@@ -197,21 +146,58 @@ services:
image: metabase/python-runner:latest
environment:
- AUTH_TOKEN=${AUTH_TOKEN}
minio:
image: quay.io/minio/minio:latest
command: server /data --console-address ":9001"
environment:
- MINIO_ROOT_USER=${MINIO_ROOT_USER}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
volumes:
- minio-data:/data
minio-init:
image: quay.io/minio/mc:latest
depends_on:
minio:
condition: service_started
entrypoint:
- /bin/sh
- -c
- |
until mc alias set local http://minio:9000 "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}" >/dev/null 2>&1; do
echo "waiting for minio..."; sleep 2;
done
mc mb --ignore-existing local/metabase-python-runner
volumes:
minio-data: {}
```
Create a `.env` file with your configuration:
Both S3 endpoint variables point at `http://minio:9000` because Metabase and the runner share the Compose network and reach MinIO by the same hostname. `MB_PYTHON_STORAGE_S_3_CONTAINER_ENDPOINT` sets the host Metabase signs into the presigned URLs the runner uploads to and downloads from — if your runner resolves storage by a different hostname than Metabase does, set this to the runner's view of it.
Create a `.env` file:
```bash
AUTH_TOKEN=your-secure-token-here
S3_ENDPOINT=https://s3.amazonaws.com
S3_BUCKET=metabase-python-runner
S3_REGION=us-east-1
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
```
Then run:
Then bring everything up and try a transform:
```bash
docker compose up -d
```
1. Put a strong shared secret in `AUTH_TOKEN` (the runner and Metabase have to agree on it):
```bash
openssl rand -hex 32
```
2. Start the stack:
```bash
docker compose up -d
```
3. You'll need a Pro or Enterprise license with the [Advanced transforms add-on](addons.md). In Metabase, [enable transforms](transforms-overview.md#enable-transforms), then [create a Python transform](python-transforms.md#create-a-python-transform) and click **Run**.
4. Open **Data Studio > Jobs > Runs** and find your run. If it failed, the run's logs will usually tell you whether Metabase couldn't reach the runner, or the runner couldn't reach MinIO.

View File

@@ -108,7 +108,7 @@ To use functions or classes from your Python library:
## Incremental Python transforms
By default, Metabase will process all the data in all input tables, drop the existing target table (if one exists), and create a new table with the processed data. You can tell Metabase to only write **new** data to your target table by marking your transform as incremental.
By default, Metabase will process all the data in all input tables, drop the existing target table (if one exists), and create a new table with the processed data. You can tell Metabase to only process **new** data by marking your transform as incremental.
### Prerequisites for incremental transforms
@@ -120,7 +120,8 @@ To make a Python transform incremental:
1. Go to the transform's page in **Data studio > Transforms**.
2. Switch to **Settings** tab.
3. In **Column to check for new values**, select the column in one of the source tables that Metabase should check to determine which values are new. Only some columns are eligible. See [prerequisites for incremental transforms](./transforms-overview.md#prerequisites-for-incremental-transforms).
3. In **Field to check for new values**, select the field in one of the source tables that Metabase should scan to determine which records are new or changed. Only some fields are eligible. See [prerequisites for incremental transforms](./transforms-overview.md#prerequisites-for-incremental-transforms).
4. (Optional) To update matching rows instead of appending new ones, [add a merge key](./transforms-overview.md#add-merge-keys-to-upsert-rows).
## Current limitations of Python transforms

View File

@@ -92,7 +92,7 @@ See [Run a transform](transforms-overview.md#run-a-transform). You'll see logs f
## Incremental query transforms
By default, on every transform run after the first one, Metabase will process all the data in all input tables, then drop the existing target table, and create a new table with the processed data. You can tell Metabase to only write **new** data to your target table by marking your transform as incremental.
By default, on every transform run after the first one, Metabase will process all the data in all input tables, then drop the existing target table, and create a new table with the processed data. You can tell Metabase to only process **new** data by marking your transform as incremental.
### Prerequisites for incremental transforms
@@ -102,6 +102,8 @@ Your data has to have certain structure for incremental transforms to work. See
For a transform to run incrementally, you'll need to pick a column ("checkpoint") that Metabase needs to check for new values. Then, behind the scenes, Metabase will add a filter around your transform query that will filter the results of the query for values greater than the last written checkpoint value.
If you set a [merge key](transforms-overview.md#add-merge-keys-to-upsert-rows), Metabase updates the target rows that match the key instead of adding new ones.
### Make a query transform incremental
#### Use table variables for incremental SQL transforms
@@ -152,7 +154,8 @@ To make a query transform incremental:
1. Go to the transform's page in **Data studio > Transforms**.
2. Switch to **Settings** tab.
3. In **Column to check for new values**, select the column in one of the source tables that Metabase should check to determine which values are new. Only some columns are eligible. See [prerequisites for incremental transforms](./transforms-overview.md#prerequisites-for-incremental-transforms).
3. In **Field to check for new values**, select the field in one of the source tables that Metabase should scan to determine which records are new or changed. Only some fields are eligible. See [prerequisites for incremental transforms](./transforms-overview.md#prerequisites-for-incremental-transforms).
4. (Optional) To update matching rows instead of appending new ones, [add a merge key](./transforms-overview.md#add-merge-keys-to-upsert-rows).
## Convert models to transforms

View File

@@ -56,6 +56,16 @@ Permission configuration for transform depends on your plan.
- To **see** the list of transforms on your instance, people need to be able to access Data Studio, so they need to be either an Admin or a member of the special [Data Analyst group](../../people-and-groups/managing.md).
- To **execute** transforms on a database, people need to be either Admins on belong to the special [Data Analyst group](../../people-and-groups/managing.md). Additionally people need to have the [Transform permissions](../../permissions/data.md) for that database.
## Set up transforms on a self-hosted Metabase
If you run Metabase yourself, here's the path to working transforms:
1. **Check your plan.** [Basic transforms](addons.md#basic-transforms) (query-based) are included on self-hosted Metabase. [Advanced transforms](addons.md#advanced-transforms) — Python transforms, the [transform inspector](transform-inspector.md), and [writable connections](../../databases/writable-connection.md) — need a self-hosted Pro or Enterprise plan with the Advanced transforms add-on.
2. **Connect a database that can write.** Transforms create and replace tables in your database, so the database user needs create, drop, and write privileges. See [Database users, roles, and privileges](../../databases/users-roles-privileges.md). For cleaner isolation, point the transform at a [writable connection](../../databases/writable-connection.md). Only some databases [support transforms](#databases-that-support-transforms).
3. **For Python transforms, set up a runner.** Query-based transforms need nothing extra — they run inside your database. Python transforms run in a separate execution environment, so you'll point Metabase at a [self-hosted Python runner](python-runner.md) backed by S3-compatible storage (AWS S3, MinIO, and so on).
4. **[Enable transforms](#enable-transforms)** in Data Studio.
5. **Create and run a transform.** [Create a query-based transform](query-transforms.md#create-a-query-based-transform) or a [Python transform](python-transforms.md#create-a-python-transform), run it manually, and check the result under **Runs**. Once it works, [schedule it with jobs](jobs-and-runs.md).
## Enable transforms
Before you can start writing transforms, you'll need to enable transforms in your Metabase instance.
@@ -220,13 +230,17 @@ Metabase will track transform dependencies and execute transforms in a reasonabl
On Metabase Pro or Enterprise plans, you can see the transform dependencies graph by going to **Data Studio > Transforms** and go to the **Dependencies** tab.
If a job includes a transform that depends on a table created by another transform, then the job will run all the tagged transforms and their dependencies, even if they lack tags, see [Jobs and runs](jobs-and-runs.md) for more information.
If a job includes a transform that depends on a table created by another transform, then the job will run all the tagged transforms, plus any of their [dependencies that aren't already up to date](jobs-and-runs.md#jobs-include-all-dependent-transforms).
## Incremental transforms
_Data Studio > Transforms > Settings_
Incremental transforms only append new data since the previous transform run. For example, you might have new transaction data coming in every day, and run the transform nightly. With each run, the incremental transform would only write the rows added after the previous run the night before.
Incremental transforms only process the data that's new since the previous transform run. For example, you might have new transaction data coming in every day, and run the transform nightly. With each run, the incremental transform would only handle the rows added after the previous run the night before.
By default, Metabase appends those rows to the target table. If your source tables track changes to a record over time, you can use an incremental strategy by setting a [merge key](#add-merge-keys-to-upsert-rows) so that Metabase updates the existing rows instead of adding duplicate rows.
In this case, the checkpoint field decides which rows count as new, and the merge key decides whether Metabase appends those rows or updates the matching ones.
### Prerequisites for incremental transforms
@@ -234,6 +248,30 @@ Incremental transforms only append new data since the previous transform run. Fo
- The checkpoint column has to have increasing values, like a sequential ID or timestamp column. Metabase will determine what "new" data is by looking for values that are _greater than_ already-written checkpoint values.
- Your schema is stable, meaning that the structure of the tables is not going to change from run to run.
### Add merge keys to upsert rows
By default, Metabase will append rows when outputting a transform. You can add a merge key to upsert (update or append) instead.
Why you'd want to add a merge key: some source tables get a new row each time a record changes. So for example the same order shows up once as `created`, again as `paid`, and again as `shipped`. If you append those rows, you end up with three rows for the same order. That may be what you want. But if instead you want to update these records in place, you can select one or more columns as merge keys, like an order ID column. If a merge key is set, Metabase will first try to update existing records that match the key. If no match is found, Metabase will append a new record.
To add a merge key:
1. Go to the transform's page in **Data studio > Transforms**.
2. Switch to the **Settings** tab and turn on **Only process new data**.
3. In **Merge key**, choose the columns that identify a record. If the target table already exists, you'll pick from a list of its columns. If the transform hasn't run yet, there's no table to pick from, so type each column name and press comma or enter. In that case, type the column's name in the database, which may differ from the display name you'd see in the list.
The merge key refers to columns in the _target_ table (the columns your transform outputs), not columns in the _source_ tables. If identifying a record requires a combination of columns, like an `id` plus a `region`, pick more than one column as merge keys.
Merge keys need a checkpoint field that increases every time a row is written, like an `updated_at` timestamp, otherwise the transform never sees the change.
### Reprocess all data to rebuild the target table
Once an incremental transform has run, its **Settings** tab shows **Last processed [checkpoint field]**, followed by the highest checkpoint value Metabase has written so far. That value is how Metabase knows where to pick up, since each run only handles rows past it.
To clear that value and reprocess all rows, click **Reprocess all data**. The next run will then rebuild the target table from scratch, processing every row instead of only the new ones. The run doesn't start right away; you'll need to run the transform manually, or wait for the transform's next scheduled run.
Changing the checkpoint field also resets the stored value, so a transform will rebuild from scratch on its next run after you pick a different field to use for checkpointing.
### Make a transform incremental
Incremental transforms work differently for query-based transforms and Python transforms, so see [incremental query transforms](query-transforms.md#incremental-query-transforms) and [incremental Python transforms](./python-transforms.md#incremental-python-transforms) for more information.

View File

@@ -48,7 +48,7 @@ The databases listed below have official drivers maintained by the Metabase team
If you don't see your database listed here, see [Community drivers](../developers-guide/community-drivers.md).
As of version 46.6.4, Metabase [no longer supports H2 connections](https://www.metabase.com/blog/vulnerability-post-mortem). But Metabase still ships with an H2 database to include an embedded application database, as well as to provide some sample data out of the box.
As of version 46.6.4, Metabase [no longer supports H2 connections](https://www.metabase.com/blog/vulnerability-post-mortem). But Metabase still ships with an H2 database to include an embedded application database.
## Connecting to databases hosted by a cloud provider

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