@metabase/cli 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -115
- package/dist/add-collection-C9BdVBs2.mjs +11 -0
- package/dist/{add-collection-C_iovi9i.mjs → add-collection-DQjTlDNF.mjs} +12 -5
- package/dist/archive-B3qiL-kK.mjs +40 -0
- package/dist/{archive-DdaP94H3.mjs → archive-BXzghEQX.mjs} +10 -9
- package/dist/{archive-WaEW85NB.mjs → archive-CBGKzEAl.mjs} +9 -8
- package/dist/{archive-BKPO8lEO.mjs → archive-CuVk8iwN.mjs} +10 -9
- package/dist/archive-DTN9tLGT.mjs +40 -0
- package/dist/{archive-Dvzrmdbk.mjs → archive-De8jzzq7.mjs} +10 -9
- package/dist/auth-D9eAyVoG.mjs +19 -0
- package/dist/{body-XtR7-uCO.mjs → body-tcURGnGh.mjs} +4 -4
- package/dist/{branches-XUY4JY-X.mjs → branches-CIGkjXIk.mjs} +11 -7
- package/dist/{cancel-BrUVO_ax.mjs → cancel-pPsvgJ0Z.mjs} +9 -8
- package/dist/{cancel-task-oXheTOB6.mjs → cancel-task-BLGE4UlL.mjs} +12 -8
- package/dist/capabilities-7e9MgquN.mjs +29 -0
- package/dist/card-DDDrWcDU.mjs +20 -0
- package/dist/{card-CQxvHeyP.mjs → card-ezYiriML.mjs} +1 -1
- package/dist/{cards-CONTTAG9.mjs → cards-Dq3nx_9n.mjs} +8 -7
- package/dist/cli.mjs +264 -44
- package/dist/collection-DkEvCDar.mjs +20 -0
- package/dist/{predicates-CGO17Q15.mjs → command-augment-BH9qgQ5u.mjs} +66 -14
- package/dist/{create-Cs2xntFG.mjs → create-9DBTkbMq.mjs} +18 -16
- package/dist/{create-kYpjobrq.mjs → create-B1dyuL9Y.mjs} +16 -10
- package/dist/create-BIphz0kO.mjs +52 -0
- package/dist/create-BcgoukG4.mjs +50 -0
- package/dist/create-BdPoSk_7.mjs +50 -0
- package/dist/{create-DZxUeqdf.mjs → create-BrUqxreg.mjs} +11 -10
- package/dist/create-DHscDhRd.mjs +50 -0
- package/dist/{create-Dq25vsMu.mjs → create-aPaUEGdr.mjs} +11 -10
- package/dist/{create-branch-D5u14AxL.mjs → create-branch-DGoc9CUU.mjs} +12 -8
- package/dist/{create-Ca9lIDwP.mjs → create-w3mQg9n4.mjs} +11 -10
- package/dist/{credentials-BIQ1cEzM.mjs → credentials-qryRLUed.mjs} +12 -11
- package/dist/current-task-DZM28rnr.mjs +40 -0
- package/dist/{dashboard-CnMD04PQ.mjs → dashboard-B4bn3z6t.mjs} +2 -1
- package/dist/dashboard-BLf1RZlk.mjs +21 -0
- package/dist/{database-vvig8k4x.mjs → database-BXiue1in.mjs} +1 -1
- package/dist/{database-BSvzYlRe.mjs → database-Ce1gOJF7.mjs} +3 -3
- package/dist/db-CWTFe_FZ.mjs +22 -0
- package/dist/{delete-VTAS9EUt.mjs → delete-BPaFdHZP.mjs} +11 -8
- package/dist/{delete-CUx6RT9e.mjs → delete-BrJOotpW.mjs} +11 -8
- package/dist/{remove-C6bS0Z6w.mjs → delete-FFj1xQWO.mjs} +25 -20
- package/dist/{delete-runtime-DfFMWJJ6.mjs → delete-runtime-uuYbd4k2.mjs} +9 -7
- package/dist/{delete-table-DzUneMKe.mjs → delete-table-CNupWUO0.mjs} +11 -8
- package/dist/{deprovision-CpJfGgCt.mjs → deprovision-BNr9fPDY.mjs} +18 -12
- package/dist/{dirty-nkAOXxgC.mjs → dirty-BCkNOY8c.mjs} +11 -7
- package/dist/{docker-D5FTIoD0.mjs → docker-Ds252Mwc.mjs} +4 -4
- package/dist/{translate-Cqsd0Px5.mjs → eid-CLY5X0Uw.mjs} +43 -23
- package/dist/error-ZsFeevV2.mjs +191 -0
- package/dist/{export-BWvY7X_G.mjs → export-CgHgWW3I.mjs} +20 -18
- package/dist/{field-B3gvaqpK.mjs → field-E0IBy4Uw.mjs} +12 -3
- package/dist/field-LL6W_c-c.mjs +18 -0
- package/dist/{fields-dH16G5UV.mjs → fields-RkRWU-u9.mjs} +9 -8
- package/dist/{get-D96QEU49.mjs → get-42tJ7BNp.mjs} +8 -7
- package/dist/{get-C6SR3A9t.mjs → get-B8l4t4Pz.mjs} +10 -9
- package/dist/{get-BnBRKHr7.mjs → get-B9kwSs6U.mjs} +8 -7
- package/dist/{get-7macOPAI.mjs → get-Bo4Cpd_c.mjs} +7 -7
- package/dist/{get-BcqxMVC1.mjs → get-C9O_aEGo.mjs} +8 -7
- package/dist/{get-CKxlhMy1.mjs → get-CRvbChoX.mjs} +8 -7
- package/dist/{get-B7i_nYJB.mjs → get-CTDqioaj.mjs} +8 -7
- package/dist/{get-D8e_RzZ0.mjs → get-CiZrZJLt.mjs} +10 -9
- package/dist/{get-B08K82JV.mjs → get-CvmqPN30.mjs} +8 -7
- package/dist/{get-CACaBFLt.mjs → get-DmzgSgrl.mjs} +9 -8
- package/dist/{get-R7OaVL_t.mjs → get-DsqGHNHN.mjs} +8 -7
- package/dist/get-run-CBwcRc8E.mjs +38 -0
- package/dist/{get-DNN1X2gN.mjs → get-sMpa-X4E.mjs} +9 -8
- package/dist/{get-DAWofnzK.mjs → get-y17zJMnU.mjs} +8 -7
- package/dist/git-sync-CrWTo3YX.mjs +28 -0
- package/dist/{has-remote-changes-BAnIXQXU.mjs → has-remote-changes-CfRidwXT.mjs} +13 -8
- package/dist/{import-CfdPEMng.mjs → import-BZV0Z2KR.mjs} +21 -18
- package/dist/{input-BQ-BZA8h.mjs → input-cMSEqISy.mjs} +7 -4
- package/dist/is-dirty-CPzOnnH6.mjs +10 -0
- package/dist/{is-dirty-CZWcG0vj.mjs → is-dirty-hKcB4OH9.mjs} +9 -4
- package/dist/{items-DqwahOKf.mjs → items-C94eW2Yd.mjs} +10 -9
- package/dist/key-vkNkH82H.mjs +17 -0
- package/dist/license-B37055sr.mjs +17 -0
- package/dist/{list-L63TpX1t.mjs → list-B0V7FeL2.mjs} +7 -7
- package/dist/{list-Bkd7Nbds.mjs → list-BFlzLGlw.mjs} +7 -6
- package/dist/{list-yxVAE1S7.mjs → list-BJXaGk-z.mjs} +7 -6
- package/dist/{list-BqNMpIXy.mjs → list-BS_Bxejg.mjs} +9 -8
- package/dist/{list-J277Qtki.mjs → list-BmHoYJr7.mjs} +7 -6
- package/dist/list-C-oZe1_p.mjs +173 -0
- package/dist/{list-ViT2KWhv.mjs → list-CF1pMN4S.mjs} +7 -6
- package/dist/{list-BpNU1neq.mjs → list-CU6sOfI-.mjs} +9 -7
- package/dist/{list-oftHLFbE.mjs → list-CqN4gvCk.mjs} +9 -7
- package/dist/{list-DBOYoJtA.mjs → list-DUXdt0XI.mjs} +10 -6
- package/dist/{list-D41gfkKb.mjs → list-DfDZr55C.mjs} +10 -8
- package/dist/{list-vF4EneaE.mjs → list-DrINpVLM.mjs} +7 -6
- package/dist/{list-DJcGwJ4W.mjs → list-DuSoEk_J.mjs} +9 -8
- package/dist/{list-CQkDqphl.mjs → list-HS15y_WN.mjs} +7 -6
- package/dist/login-enh9Yimb.mjs +181 -0
- package/dist/{logout-DD4q5whi.mjs → logout-BWLPLDh8.mjs} +13 -11
- package/dist/{logs-Ci3mJE2z.mjs → logs-Cu3QtvPs.mjs} +9 -8
- package/dist/{manifest-CGM7XNLC.mjs → manifest-BNh0Lw6p.mjs} +15 -15
- package/dist/{measure-BEQfnLdN.mjs → measure-Bt3InQsA.mjs} +2 -2
- package/dist/measure-CDlEPFtB.mjs +19 -0
- package/dist/{metadata-BDat-jN9.mjs → metadata-BTTEBWdS.mjs} +10 -9
- package/dist/{metadata-29_qlqbz.mjs → metadata-D2TxboMm.mjs} +9 -8
- package/dist/parse-enum-CrEWOhuY.mjs +11 -0
- package/dist/{parse-id-CysSaCbf.mjs → parse-id-0_tOPvfI.mjs} +1 -1
- package/dist/{parse-ref-D1yeDOn8.mjs → parse-ref-DKag6a6I.mjs} +1 -1
- package/dist/{parse-schemas-B10n01ez.mjs → parse-schemas-D-qVLl4z.mjs} +2 -2
- package/dist/{path-DLByFMMA.mjs → path-C8GrBdgT.mjs} +7 -7
- package/dist/{poll-p9Y7-JEQ.mjs → poll-4eoh5J0r.mjs} +2 -2
- package/dist/{poll-task-BQe0NvJZ.mjs → poll-task-51WRdugU.mjs} +19 -20
- package/dist/{preflight-CvFu0Cct.mjs → preflight-BhsErYz3.mjs} +4 -4
- package/dist/{process-zJeVJZTM.mjs → process-CM7Uu5q_.mjs} +1 -1
- package/dist/{prompt-DgDNy_Pc.mjs → prompt-CFKoys7k.mjs} +3 -1
- package/dist/provision-Chf86BF0.mjs +83 -0
- package/dist/{ps-BxQdpkr5.mjs → ps-CEYtsKBj.mjs} +5 -3
- package/dist/ps-CIDwaubS.mjs +11 -0
- package/dist/{query-C7zTlFJA.mjs → query-BBCAF-tG.mjs} +17 -14
- package/dist/{query-CFH4nBlK.mjs → query-DYVBnu9d.mjs} +12 -9
- package/dist/query-result-ABPLz6I4.mjs +19 -0
- package/dist/{remove-BuWxx3hY.mjs → remove-2yInufA6.mjs} +14 -12
- package/dist/remove-collection-CBAHz0Dk.mjs +44 -0
- package/dist/{render-DuoDUTVL.mjs → render-0_GsapXa.mjs} +51 -23
- package/dist/render-khznBlla.mjs +32 -0
- package/dist/{rescan-values-DabyRYQ_.mjs → rescan-values-cfTSNQZo.mjs} +12 -11
- package/dist/{run-Cl-9RtC4.mjs → run-qgdEJv-I.mjs} +17 -15
- package/dist/{runs-BH6s1Zao.mjs → runs-BFIIH4GL.mjs} +9 -8
- package/dist/{runtime-CDu6fykq.mjs → runtime-Duawf5lE.mjs} +653 -428
- package/dist/{schema-tables-i58wp_p3.mjs → schema-tables-C2xM3dho.mjs} +9 -8
- package/dist/{schemas-_m8RYRl9.mjs → schemas-BP7xiktH.mjs} +7 -6
- package/dist/{search-DObOsjbP.mjs → search-DYP3lOlq.mjs} +12 -16
- package/dist/segment-B6HnNGDs.mjs +19 -0
- package/dist/{set-CwVWeAsi.mjs → set-DpRQqdo7.mjs} +13 -11
- package/dist/{set-CJA9dpK6.mjs → set-Tt-ioa4L.mjs} +14 -13
- package/dist/setting-DUa96KF3.mjs +17 -0
- package/dist/{setup-DqBOe3HZ.mjs → setup-BPlllnim.mjs} +10 -9
- package/dist/{skills-CHU7uuDU.mjs → skills-BkregMyb.mjs} +2 -2
- package/dist/{skills-C2rTVj0n.mjs → skills-SqbPo0BI.mjs} +3 -3
- package/dist/snippet-dJ68tGsl.mjs +19 -0
- package/dist/{start-CfruN4wF.mjs → start-DJZA67WF.mjs} +70 -38
- package/dist/{stash-CWuXKSZq.mjs → stash-C89zNKxo.mjs} +21 -18
- package/dist/{status-D-RYZB9G.mjs → status-B1EJ_jv0.mjs} +16 -9
- package/dist/status-BNvFPemM.mjs +100 -0
- package/dist/status-D5wSqYV_.mjs +34 -0
- package/dist/{stop-D8Hr4cKX.mjs → stop-5rCLmkCQ.mjs} +16 -11
- package/dist/{summary-Lt2XLBK9.mjs → summary-Cihbx0Qs.mjs} +8 -7
- package/dist/{sync-schema-BDElSynU.mjs → sync-schema-C3odu0ZH.mjs} +12 -11
- package/dist/table-J2f0STnB.mjs +19 -0
- package/dist/{table-B-PYcgGb.mjs → table-qDD2kApF.mjs} +1 -1
- package/dist/{transform-job-BrhOLO4M.mjs → transform-job-DjhoJbiV.mjs} +1 -1
- package/dist/transform-job-OW4SDhsQ.mjs +19 -0
- package/dist/transform-q1LYWQtW.mjs +24 -0
- package/dist/{tree-DfvjDjmk.mjs → tree-mvq9gM9w.mjs} +7 -6
- package/dist/{update-DzgXF082.mjs → update-BoIiuC70.mjs} +16 -14
- package/dist/{update-qnFY5IuC.mjs → update-C0jP0AKT.mjs} +12 -11
- package/dist/{update-zp7pCBZH.mjs → update-CbBnHz42.mjs} +16 -14
- package/dist/{update-B0bjPqKC.mjs → update-CtOo3LsX.mjs} +13 -12
- package/dist/{update-CVxOxmt6.mjs → update-DCrOQ1PW.mjs} +17 -15
- package/dist/{update-BYduslhn.mjs → update-DEZayTb4.mjs} +20 -15
- package/dist/{update-BgcroYkF.mjs → update-DwRxdflw.mjs} +12 -11
- package/dist/{update-D9Z8cL7h.mjs → update-Rr4usmCo.mjs} +12 -11
- package/dist/{update-DuA8-cCq.mjs → update-VvKMnwsM.mjs} +16 -14
- package/dist/{update-dashcard-CQ3kmmss.mjs → update-dashcard-DFvIz8Qj.mjs} +12 -11
- package/dist/{update-CqnDMNtZ.mjs → update-tRparnUs.mjs} +18 -16
- package/dist/{upgrade-CIgTr2CG.mjs → upgrade-D-Rl_fH9.mjs} +16 -29
- package/dist/{url-B5MgZXzg.mjs → url-BB6jeNQj.mjs} +11 -10
- package/dist/{uuid-CJz9TmHI.mjs → uuid-BSVUk8u2.mjs} +8 -6
- package/dist/{validate-CB0bu50i.mjs → validate-dPEOnOf8.mjs} +2 -1
- package/dist/{validate-query-CavIA0Q2.mjs → validate-query-CYvOP8Ld.mjs} +3 -3
- package/dist/values-D1RJE4H6.mjs +45 -0
- package/dist/verify-A7BWfBPZ.mjs +79 -0
- package/dist/{wait-BFqBlg0y.mjs → wait-B17I_pWy.mjs} +2 -2
- package/dist/{wait-tDp9ZOou.mjs → wait-DK5QDZ8n.mjs} +14 -10
- package/dist/{wait-flags-CN-e9zNq.mjs → wait-flags-DlfbIXHw.mjs} +20 -9
- package/dist/{workspace-credentials-4lIxxz4g.mjs → workspace-credentials-8CBMQJFz.mjs} +2 -2
- package/dist/workspace-ri6r3zWo.mjs +25 -0
- package/dist/{yaml-ECiog374.mjs → yaml-Gv6wRFMF.mjs} +1 -1
- package/package.json +2 -1
- package/skill-data/core/SKILL.md +55 -453
- package/skill-data/git-sync/SKILL.md +1 -1
- package/skill-data/mbql/SKILL.md +156 -0
- package/skill-data/mbql/references/operators.md +253 -0
- package/skill-data/transform/SKILL.md +2 -40
- package/skill-data/viz/SKILL.md +137 -0
- package/skill-data/viz/references/settings.md +312 -0
- package/skill-data/workspace/SKILL.md +45 -63
- package/skills/metabase-cli/SKILL.md +5 -26
- package/dist/add-collection-ucsyAMkV.mjs +0 -11
- package/dist/api-key-BENHbTbV.mjs +0 -13
- package/dist/auth-DICRtJDy.mjs +0 -19
- package/dist/card-l-UmrUIo.mjs +0 -20
- package/dist/collection-oV0olVY-.mjs +0 -19
- package/dist/command-augment-D9pI9Vbh.mjs +0 -11
- package/dist/create-CrUq6sib.mjs +0 -125
- package/dist/create-D3Z878yr.mjs +0 -50
- package/dist/create-Le3Bqn7b.mjs +0 -48
- package/dist/create-V-q2rU0T.mjs +0 -48
- package/dist/create-swbIXdo5.mjs +0 -48
- package/dist/current-task-DCq7rk9V.mjs +0 -36
- package/dist/dashboard-hbKDd36X.mjs +0 -20
- package/dist/db-qVK6NsdB.mjs +0 -22
- package/dist/eid-CDFXX_6H.mjs +0 -13
- package/dist/field-C0LE7RQI.mjs +0 -18
- package/dist/flag-pair-Fmcdkrfx.mjs +0 -17
- package/dist/get-run-CwFuR4Uw.mjs +0 -36
- package/dist/git-sync-DV7YjniX.mjs +0 -28
- package/dist/is-dirty-LxVbm2C5.mjs +0 -10
- package/dist/key-CCJdVWKc.mjs +0 -12
- package/dist/license-Cb6ewEJO.mjs +0 -17
- package/dist/list-DV6CONhp.mjs +0 -55
- package/dist/login-D1nZwgKv.mjs +0 -192
- package/dist/measure-XhJuL77y.mjs +0 -19
- package/dist/package-DFUprkSZ.mjs +0 -85
- package/dist/provision-BP-b4Are.mjs +0 -77
- package/dist/ps-Bk6unzaX.mjs +0 -11
- package/dist/remove-collection-Bc4roCq0.mjs +0 -38
- package/dist/segment-DfxZdJmR.mjs +0 -19
- package/dist/setting-Czy4ws6h.mjs +0 -18
- package/dist/snippet-BCY4KHBU.mjs +0 -19
- package/dist/status-1oUnw803.mjs +0 -56
- package/dist/status-J9HIDcA5.mjs +0 -32
- package/dist/table-BwX3Ib5f.mjs +0 -19
- package/dist/transform-iaAi37V0.mjs +0 -24
- package/dist/transform-job-Bemonf82.mjs +0 -19
- package/dist/values-BXN6tx1i.mjs +0 -36
- package/dist/workspace-BBsT0H0g.mjs +0 -24
- /package/dist/{body-flags-BK7J6Daz.mjs → body-flags-D7q87Btw.mjs} +0 -0
- /package/dist/{collection-B3sPXRLs.mjs → collection-Bcy8cWYH.mjs} +0 -0
- /package/dist/{paginate-CTSfuYiF.mjs → paginate-BexjkjbY.mjs} +0 -0
- /package/dist/{revision-message-flag-oyq2xrDU.mjs → revision-message-flag-DY29-cgz.mjs} +0 -0
- /package/dist/{segment-BMrUBz94.mjs → segment-DhBmcr_E.mjs} +0 -0
- /package/dist/{setting-CTaAeMci.mjs → setting-BzCng1Ub.mjs} +0 -0
- /package/dist/{snippet-CSWqkslB.mjs → snippet-bi_0XbNT.mjs} +0 -0
- /package/dist/{transform-DR4ejuPM.mjs → transform-BKahefz_.mjs} +0 -0
- /package/dist/{workspace-DUfqhPm5.mjs → workspace-D8HtUN0y.mjs} +0 -0
|
@@ -8,7 +8,7 @@ allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
|
|
|
8
8
|
|
|
9
9
|
Metabase content (cards, dashboards, transforms, snippets, collections, …) can live in a git repo as YAML and round-trip in and out of a Metabase instance via the `git-sync` verbs. The instance is configured with a `remote-sync-*` settings block (URL, branch, token, type read-only/read-write); the CLI drives the sync tasks against `/api/ee/remote-sync/*`.
|
|
10
10
|
|
|
11
|
-
This skill covers the import/export workflow. The general flag conventions and auth setup live in the `core` skill (`mb skills get core`). To author content YAML by hand,
|
|
11
|
+
This skill covers the import/export workflow. The general flag conventions and auth setup live in the `core` skill (`mb skills get core`). To author content YAML by hand: the per-resource clause and settings shapes mirror the API form — query bodies follow the `mbql` skill, `visualization_settings` follow the `viz` skill — except the portable YAML uses **name-based** references (e.g. `[Sample Database, PUBLIC, ORDERS, TOTAL]`, and entity-ids for cross-entity FKs) where the API form uses numeric ids. For the on-disk folder layout, model new files on what the synced repo already contains.
|
|
12
12
|
|
|
13
13
|
## Adding / removing a directory (collection) to sync
|
|
14
14
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mbql
|
|
3
|
+
description: Author Metabase MBQL 5 query bodies for the `mb` CLI — the only hand-authorable query format. Covers the JSON shape (lib/type mbql/query, flat stages, numeric ids), the "options object always second" clause rule, lib/uuid minting, the print-schema → dry-run → run validation loop, where MBQL 5 is consumed (mb query, card dataset_query, transform source.query, measure/segment definition), the flat-vs-legacy-envelope footgun, and naming aggregation output columns. Load whenever building or fixing an MBQL query by hand — "write an MBQL query", "create a card from MBQL", "the dataset_query is wrong", "fix the validation errors", "aggregate and group by", "order by the count", or any `--dry-run` / `mb query` work.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MBQL 5
|
|
8
|
+
|
|
9
|
+
MBQL 5 is the **only query format you can author by hand** with confidence — it has a bundled JSON Schema, so the CLI pre-flight-validates it before sending. Legacy MBQL 4 and native SQL are accepted but **not** schema-validated (see "Other formats" below).
|
|
10
|
+
|
|
11
|
+
Prefer MBQL over native SQL: MBQL is portable across warehouse engines and the CLI can validate it. Reach for native only when the query needs something MBQL can't express.
|
|
12
|
+
|
|
13
|
+
The general flag conventions, body-input precedence, and output flags live in the `core` skill (`mb skills get core`).
|
|
14
|
+
|
|
15
|
+
## The shape
|
|
16
|
+
|
|
17
|
+
A query is a flat object — `lib/type`, a numeric `database` id, and an ordered `stages` array. No recursive `source-query` nesting; multi-step queries are sibling stages.
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"lib/type": "mbql/query",
|
|
22
|
+
"database": 1,
|
|
23
|
+
"stages": [
|
|
24
|
+
{
|
|
25
|
+
"lib/type": "mbql.stage/mbql",
|
|
26
|
+
"source-table": 7,
|
|
27
|
+
"aggregation": [["count", { "lib/uuid": "<mint via mb uuid>" }]],
|
|
28
|
+
"breakout": [["field", { "temporal-unit": "month", "lib/uuid": "<mint>" }, 22]]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- **Numeric ids only.** `database`, `source-table`, and field ids are integers from `mb database list` / `mb table get <id> --include fields`. (The portable YAML representation under git-sync uses _names_ like `[Sample Database, PUBLIC, ORDERS]`; the CLI's `/api/dataset` form uses numeric ids — don't mix them.)
|
|
35
|
+
- **First stage** carries `source-table` (a table id) or `source-card` (a saved card). Later stages omit both and read the previous stage's output columns by name.
|
|
36
|
+
- `source-card` references a saved card by entity id; downstream fields are referenced by column name (string), not a field id.
|
|
37
|
+
|
|
38
|
+
## The one rule that trips everyone: options object is **second**
|
|
39
|
+
|
|
40
|
+
Every clause is `[op, {options}, ...args]`. The options object is element **1**, args follow.
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
["field", { "base-type": "type/Text", "lib/uuid": "<mint>" }, 1779] // field id is THIRD
|
|
44
|
+
["count", { "lib/uuid": "<mint>" }] // no args
|
|
45
|
+
["sum", { "lib/uuid": "<mint>" }, ["field", { "lib/uuid": "<mint>" }, 42]]
|
|
46
|
+
["=", { "lib/uuid": "<mint>" }, ["field", { "lib/uuid": "<mint>" }, 1779], "delivered"]
|
|
47
|
+
["asc", { "lib/uuid": "<mint>" }, ["field", { "lib/uuid": "<mint>" }, 42]]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The legacy MBQL 4 field shape `["field", id, opts]` (id second) is **rejected** here. A slot-1 violation surfaces from `--dry-run` as `must be the field options object` / `must be the clause options object` at `/stages/0/<verb>/<n>/1`.
|
|
51
|
+
|
|
52
|
+
The same `[op, {options}, …]` rule holds for `aggregation`, `breakout` (a list of field refs), `filters` (implicitly ANDed; nest an explicit `["or", {}, …]` for OR), `order-by`, `expressions`, and join `conditions`.
|
|
53
|
+
|
|
54
|
+
## UUIDs: mint them, never invent them
|
|
55
|
+
|
|
56
|
+
Every clause options object carries a `lib/uuid` (UUID v4). The schema enforces RFC 4122 strictly, so placeholders (`"a1"`, `"uuid-1"`, `"agg-uuid-001"`) fail pre-flight with `must be a UUID v4 (RFC 4122) — run \`mb uuid\` …`. The same applies to native template-tag ids and any other `format: "uuid"` slot.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
mb uuid --count 5 --json # → ["…","…","…","…","…"] — mint exactly what you need, in one call
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Workflow: count the slots (one per clause options object), `mb uuid --count <N> --json`, substitute each minted value as you build the JSON. Never copy a UUID from docs, a prior query, or another session.
|
|
63
|
+
|
|
64
|
+
**Aggregation/expression refs are the only legitimate reuse.** To reference an aggregation downstream (in `order-by` or a later stage), use `["aggregation", {options}, "<uuid>"]` where the third arg is the **string** `lib/uuid` of the target aggregation — the same minted value, by string equality. A numeric position fails with `must be the target aggregation's lib/uuid (string), not a numeric position`. Expression refs work the same way but key off the expression's name string.
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
"aggregation": [["count", { "lib/uuid": "AGG_UUID" }]],
|
|
68
|
+
"order-by": [["desc", { "lib/uuid": "ORDER_UUID" },
|
|
69
|
+
["aggregation", { "lib/uuid": "REF_UUID" }, "AGG_UUID"]]]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
(`AGG_UUID` appears twice and must be the _same_ minted value; `ORDER_UUID` and `REF_UUID` are distinct.)
|
|
73
|
+
|
|
74
|
+
## Authoring loop: print-schema → dry-run → run
|
|
75
|
+
|
|
76
|
+
`mb query` is the canonical authoring surface. Three modes:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
mb query --print-schema --profile <n> > /tmp/mbql-schema.json # 1. fetch the schema
|
|
80
|
+
mb query --file q.json --dry-run --profile <n> # 2. validate, no network
|
|
81
|
+
mb query --file q.json --profile <n> --json # 3. validate + run
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- `--print-schema` emits `{ schema, defs }` where `defs` carries `id.yaml` / `parameter.yaml` / `ref.yaml` / `temporal_bucketing.yaml` keyed by the path used in the schema's `$ref`s. Read it first for any non-trivial query — cheaper than guess-and-fail.
|
|
85
|
+
- `--dry-run` validates and emits `{ ok, errors: [{ path, message }] }`. Exit `0` valid, `2` invalid. No request sent. Iterate until `ok: true`.
|
|
86
|
+
- run (no flag) validates, then on success sends to `/api/dataset`. On validation failure it writes the same envelope, exits `2`, and **never sends**.
|
|
87
|
+
|
|
88
|
+
`path` is a JSON Pointer into the body (`/stages/0/aggregation/0`); `message` is the validator error. Exit codes: `0` valid + ran, `2` validation failed / malformed body, `1` server-side error after a valid pre-flight.
|
|
89
|
+
|
|
90
|
+
`--skip-validate` bypasses the pre-flight and sends as-is — use only when the bundled schema disagrees with what the server actually accepts (drift / false negative). Mutually exclusive with `--dry-run`. The same flag exists on `card create/update` and `transform create/update`.
|
|
91
|
+
|
|
92
|
+
## Where MBQL 5 is consumed
|
|
93
|
+
|
|
94
|
+
The same body and the same pre-flight apply everywhere a query is embedded. Each pre-flights only when the value is MBQL 5 (`lib/type: "mbql/query"`); legacy shapes skip it; `--skip-validate` bypasses.
|
|
95
|
+
|
|
96
|
+
| Command | MBQL 5 lives at | Notes |
|
|
97
|
+
| --------------------------------------- | ---------------------------------------------- | ------------------------------------------- |
|
|
98
|
+
| `mb query` | the whole body | ad-hoc run against `/api/dataset` |
|
|
99
|
+
| `card create` / `card update` | `dataset_query` | a **flat** `mbql/query` — see footgun below |
|
|
100
|
+
| `transform create` / `transform update` | `source.query` (when `source.type` is `query`) | materializes to a warehouse table |
|
|
101
|
+
| `measure create` / `measure update` | `definition` | exactly one `aggregation`, no `filters` |
|
|
102
|
+
| `segment create` / `segment update` | `definition` | filter macro tied to a table |
|
|
103
|
+
|
|
104
|
+
## Footgun: `dataset_query` is the flat mbql/query, not a legacy envelope
|
|
105
|
+
|
|
106
|
+
The most common mistake. The legacy MBQL 4 shape `{ "type": "query", "database": N, "query": {…} }` looks similar but is wrong for MBQL 5. `dataset_query` (and `source.query`, and `definition`) **is the `mbql/query` value itself**:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
"dataset_query": {
|
|
110
|
+
"lib/type": "mbql/query",
|
|
111
|
+
"database": 2,
|
|
112
|
+
"stages": [{ "lib/type": "mbql.stage/mbql", "source-table": 190,
|
|
113
|
+
"aggregation": [["count", { "lib/uuid": "<mint>" }]] }]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
No `type:"query"` wrapper, no `query:` nesting. If you wrap MBQL 5 inside a legacy envelope the CLI rejects it pre-send with a `ConfigError` (no `--skip-validate` gets it past). If it ever reached the server it would store silently and fail at run time with `Initial MBQL stage must have either :source-table or :source-card`.
|
|
118
|
+
|
|
119
|
+
## Other formats skip pre-flight
|
|
120
|
+
|
|
121
|
+
Anything that is not `lib/type: "mbql/query"` is sent as-is and normalized server-side:
|
|
122
|
+
|
|
123
|
+
- **Legacy MBQL 4** — `{ "type": "query", "database": N, "query": { "source-table": T, … } }`
|
|
124
|
+
- **Native SQL** — `{ "type": "native", "database": N, "native": { "query": "SELECT …" } }`
|
|
125
|
+
|
|
126
|
+
`mb query --file probe.json` runs these directly; `--dry-run` on them returns `{ ok: true, errors: [] }`. Don't author MBQL 4 by hand — if you need a legacy or complex query, build it in the Metabase UI and pull the body with `mb card get <id> --full --json` / `mb transform get <id> --full --json`.
|
|
127
|
+
|
|
128
|
+
## Naming aggregation output columns
|
|
129
|
+
|
|
130
|
+
Default MBQL 5 aggregations materialize as `count`, `count_where`, `avg`, `avg_2`, `sum`, … — fine for an ad-hoc run, ugly when the output is a transform target table or a card column. Set `name` (becomes the warehouse column name) and `display-name` (the UI header) in the aggregation's options:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
[
|
|
134
|
+
"count",
|
|
135
|
+
{ "lib/uuid": "<mint>", "name": "shipments_shipped", "display-name": "Shipments shipped" }
|
|
136
|
+
]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Operator reference
|
|
140
|
+
|
|
141
|
+
The full operator vocabulary — filter operators (`=`, `!=`, `<`, `between`, `contains`, `is-null`, …), aggregation functions (`count`, `sum`, `avg`, `distinct`, `count-where`, `share`, …), expression operators (arithmetic, string, temporal), temporal-bucketing units, and binning strategies — lives in this skill's `references/operators.md`, in the CLI's numeric-id form. Load it on demand rather than dumping the schema:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
mb skills get mbql --full # appends references/operators.md to this body
|
|
145
|
+
mb skills path mbql # → the skill dir; then Read references/operators.md
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`mb query --print-schema` is the exhaustive-but-heavy fallback (the full JSON Schema, ~1600 lines). The cheat-sheet covers the vocabulary; the `--dry-run` loop settles any disagreement.
|
|
149
|
+
|
|
150
|
+
## Don't
|
|
151
|
+
|
|
152
|
+
- Don't invent, hard-code, or copy `lib/uuid` values — `mb uuid` every slot at author time.
|
|
153
|
+
- Don't put the options object anywhere but slot 1, and don't use the legacy `["field", id, opts]` order.
|
|
154
|
+
- Don't wrap an MBQL 5 body in `{type:"query", query:…}` — `dataset_query` / `source.query` / `definition` is the flat `mbql/query`.
|
|
155
|
+
- Don't author MBQL 4 by hand — build it in the UI and pull it with `… get <id> --full --json`.
|
|
156
|
+
- Don't skip the `--dry-run` loop on a non-trivial query — it's free and exact.
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# MBQL 5 operator reference
|
|
2
|
+
|
|
3
|
+
The complete clause vocabulary the bundled schema accepts, in the CLI's API/numeric
|
|
4
|
+
form. The clause _structure_ and the slot-1-options rule are in the SKILL.md body —
|
|
5
|
+
this file is the catalog of which operators exist and their arguments.
|
|
6
|
+
|
|
7
|
+
**Reading the tables.** Every clause is `[op, {options}, ...args]`. Below, `{…}`
|
|
8
|
+
abbreviates the slot-1 options object, which always carries `lib/uuid` (mint with
|
|
9
|
+
`mb uuid`) plus any operator-specific option noted in the row. Field refs are numeric:
|
|
10
|
+
`["field", {…}, <field-id>]`. Everything here passes `mb query --dry-run`; when in
|
|
11
|
+
doubt, that loop is the authority.
|
|
12
|
+
|
|
13
|
+
> Relative date filters use **`time-interval`** / **`relative-time-interval`** (below),
|
|
14
|
+
> not bare date literals. If you pulled a query from the UI that contains
|
|
15
|
+
> `relative-datetime` or `absolute-datetime`, the bundled schema does not yet model
|
|
16
|
+
> those — send it with `--skip-validate` rather than rewriting it.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Filter operators
|
|
21
|
+
|
|
22
|
+
A stage's `filters` is a list of boolean clauses, implicitly ANDed. Nest an explicit
|
|
23
|
+
`["or", {…}, …]` for OR.
|
|
24
|
+
|
|
25
|
+
### Logical
|
|
26
|
+
|
|
27
|
+
| Op | Args | Notes |
|
|
28
|
+
| ----- | ------------------ | ----------------------------------------------------- |
|
|
29
|
+
| `and` | 2+ boolean clauses | Logical AND (usually implicit via the `filters` list) |
|
|
30
|
+
| `or` | 2+ boolean clauses | Logical OR |
|
|
31
|
+
| `not` | 1 boolean clause | Logical NOT |
|
|
32
|
+
|
|
33
|
+
### Comparison
|
|
34
|
+
|
|
35
|
+
| Op | Args | Notes |
|
|
36
|
+
| ----------------- | -------------------------------------------------------- | ----------------------- |
|
|
37
|
+
| `=` | field, 1+ values | Multi-value = IN |
|
|
38
|
+
| `!=` | field, 1+ values | Multi-value = NOT IN |
|
|
39
|
+
| `<` `>` `<=` `>=` | 2 orderable | |
|
|
40
|
+
| `between` | field, min, max | Inclusive |
|
|
41
|
+
| `inside` | lat-field, lon-field, lat-max, lon-min, lat-min, lon-max | Geographic bounding box |
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
["between", {…}, ["field", {…}, 12], 10, 100]
|
|
45
|
+
["=", {…}, ["field", {…}, 7], "Widget", "Gadget"]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Null / empty
|
|
49
|
+
|
|
50
|
+
| Op | Args | Notes |
|
|
51
|
+
| ----------- | ------------------- | --------------------- |
|
|
52
|
+
| `is-null` | 1 expression | |
|
|
53
|
+
| `not-null` | 1 expression | |
|
|
54
|
+
| `is-empty` | 1 string expression | NULL or `""` |
|
|
55
|
+
| `not-empty` | 1 string expression | not NULL and not `""` |
|
|
56
|
+
|
|
57
|
+
### String match
|
|
58
|
+
|
|
59
|
+
N-ary (multiple values OR'd). Accept a `case-sensitive` option (default `true`) in the
|
|
60
|
+
options object.
|
|
61
|
+
|
|
62
|
+
| Op | Args |
|
|
63
|
+
| ------------------ | ----------------- |
|
|
64
|
+
| `contains` | field, 1+ strings |
|
|
65
|
+
| `does-not-contain` | field, 1+ strings |
|
|
66
|
+
| `starts-with` | field, 1+ strings |
|
|
67
|
+
| `ends-with` | field, 1+ strings |
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
["contains", { "lib/uuid": "<mint>", "case-sensitive": false }, ["field", {…}, 9], "widget"]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Temporal
|
|
74
|
+
|
|
75
|
+
| Op | Args | Notes |
|
|
76
|
+
| ------------------------ | ---------------------------------------------------------- | --------------------------------------------------- |
|
|
77
|
+
| `time-interval` | temporal-field, n, unit | `n` = integer, or `"current"` / `"last"` / `"next"` |
|
|
78
|
+
| `relative-time-interval` | temporal-field, value, bucket, offset-value, offset-bucket | interval with offset |
|
|
79
|
+
|
|
80
|
+
Units (truncation only): `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
|
|
81
|
+
`month`, `quarter`, `year`.
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
["time-interval", {…}, ["field", {…}, 22], -30, "day"] // last 30 days
|
|
85
|
+
["time-interval", {…}, ["field", {…}, 22], "current", "month"]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Segment
|
|
89
|
+
|
|
90
|
+
| Op | Args | Notes |
|
|
91
|
+
| --------- | ---------- | ------------------------- |
|
|
92
|
+
| `segment` | segment id | Reference a saved segment |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Aggregation functions
|
|
97
|
+
|
|
98
|
+
A stage's `aggregation` is a list of aggregation clauses.
|
|
99
|
+
|
|
100
|
+
| Op | Args | Notes |
|
|
101
|
+
| ----------------------- | -------------------------- | -------------------------------- |
|
|
102
|
+
| `count` | none, or 1 expression | with arg: count non-NULL |
|
|
103
|
+
| `sum` `avg` `min` `max` | 1 numeric/orderable | |
|
|
104
|
+
| `distinct` | 1 expression | count of distinct values |
|
|
105
|
+
| `cum-count` | none or 1 expression | running count |
|
|
106
|
+
| `cum-sum` | 1 numeric | running sum |
|
|
107
|
+
| `stddev` `var` `median` | 1 numeric | |
|
|
108
|
+
| `percentile` | numeric, p (0.0–1.0) | |
|
|
109
|
+
| `count-where` | 1 boolean clause | |
|
|
110
|
+
| `sum-where` | numeric, boolean clause | |
|
|
111
|
+
| `distinct-where` | expression, boolean clause | |
|
|
112
|
+
| `share` | 1 boolean clause | proportion 0–1 |
|
|
113
|
+
| `metric` | metric id | reference a saved metric card |
|
|
114
|
+
| `measure` | measure id | reference a saved measure (v59+) |
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
["count", {…}]
|
|
118
|
+
["sum", {…}, ["field", {…}, 42]]
|
|
119
|
+
["count-where", {…}, [">", {…}, ["field", {…}, 42], 100]]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Naming** — set `name` (warehouse column) and/or `display-name` (UI header) in the
|
|
123
|
+
options object: `["sum", { "lib/uuid": "<mint>", "name": "revenue", "display-name": "Revenue" }, …]`.
|
|
124
|
+
|
|
125
|
+
**Window function** — `offset` is only valid inside `aggregation`:
|
|
126
|
+
|
|
127
|
+
| Op | Args | Notes |
|
|
128
|
+
| -------- | ------------- | ------------------------------------------------- |
|
|
129
|
+
| `offset` | expression, n | value n rows before (negative) / after (positive) |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Expression operators
|
|
134
|
+
|
|
135
|
+
Used in `expressions` (named, via `lib/expression-name` in options) and inline.
|
|
136
|
+
|
|
137
|
+
### Arithmetic
|
|
138
|
+
|
|
139
|
+
| Op | Args | Notes |
|
|
140
|
+
| --- | ---------------------------------- | -------------------- |
|
|
141
|
+
| `+` | 2+ numeric, or temporal + interval | |
|
|
142
|
+
| `-` | 1+ numeric, or temporal − interval | unary = negation |
|
|
143
|
+
| `*` | 2+ numeric | |
|
|
144
|
+
| `/` | 2+ numeric | always returns float |
|
|
145
|
+
|
|
146
|
+
### Math
|
|
147
|
+
|
|
148
|
+
| Op | Args |
|
|
149
|
+
| ---------------------------- | -------------- |
|
|
150
|
+
| `abs` `ceil` `floor` `round` | 1 numeric |
|
|
151
|
+
| `power` | base, exponent |
|
|
152
|
+
| `sqrt` `exp` `log` | 1 numeric |
|
|
153
|
+
|
|
154
|
+
### String
|
|
155
|
+
|
|
156
|
+
| Op | Args |
|
|
157
|
+
| ---------------------------------- | ------------------------------- |
|
|
158
|
+
| `concat` | 2+ expressions |
|
|
159
|
+
| `substring` | str, start (1-indexed), length? |
|
|
160
|
+
| `replace` | str, find, replace |
|
|
161
|
+
| `regex-match-first` | str, regex |
|
|
162
|
+
| `split-part` | str, delimiter, position |
|
|
163
|
+
| `trim` `ltrim` `rtrim` | 1 string |
|
|
164
|
+
| `upper` `lower` | 1 string |
|
|
165
|
+
| `length` | 1 string |
|
|
166
|
+
| `host` `domain` `subdomain` `path` | 1 URL string |
|
|
167
|
+
|
|
168
|
+
### Temporal
|
|
169
|
+
|
|
170
|
+
| Op | Args | Notes |
|
|
171
|
+
| ----------------------------------------------------------------------------------- | ------------------------------- | -------------------------- |
|
|
172
|
+
| `now` | none | datetime |
|
|
173
|
+
| `today` | none | date |
|
|
174
|
+
| `interval` | amount, unit | a temporal interval |
|
|
175
|
+
| `datetime-add` | temporal, amount, unit | |
|
|
176
|
+
| `datetime-subtract` | temporal, amount, unit | |
|
|
177
|
+
| `datetime-diff` | datetime1, datetime2, unit | |
|
|
178
|
+
| `convert-timezone` | temporal, target-tz, source-tz? | |
|
|
179
|
+
| `get-year` `get-quarter` `get-month` `get-day` `get-hour` `get-minute` `get-second` | 1 temporal | integer component |
|
|
180
|
+
| `get-day-of-week` | temporal, mode? | mode `iso`/`us`/`instance` |
|
|
181
|
+
| `get-week` | temporal, mode? | mode `iso`/`us`/`instance` |
|
|
182
|
+
| `temporal-extract` | temporal, unit, mode? | generic extraction |
|
|
183
|
+
| `month-name` `quarter-name` `day-name` | 1 integer | name from number |
|
|
184
|
+
|
|
185
|
+
Add/subtract/interval units: `year`, `quarter`, `month`, `week`, `day`, `hour`,
|
|
186
|
+
`minute`, `second`, `millisecond`. `datetime-diff` units: same minus `millisecond`.
|
|
187
|
+
`temporal-extract` units: `year-of-era`, `quarter-of-year`, `month-of-year`,
|
|
188
|
+
`week-of-year-iso`, `week-of-year-us`, `week-of-year-instance`, `day-of-month`,
|
|
189
|
+
`day-of-week`, `day-of-week-iso`, `hour-of-day`, `minute-of-hour`, `second-of-minute`.
|
|
190
|
+
|
|
191
|
+
### Type conversion
|
|
192
|
+
|
|
193
|
+
| Op | Args |
|
|
194
|
+
| --------- | ----------------- |
|
|
195
|
+
| `integer` | string or numeric |
|
|
196
|
+
| `float` | string |
|
|
197
|
+
| `text` | 1 expression |
|
|
198
|
+
|
|
199
|
+
### Conditional
|
|
200
|
+
|
|
201
|
+
| Op | Args | Notes |
|
|
202
|
+
| ---------- | ------------------------------ | ---------------------------------------------------- |
|
|
203
|
+
| `case` | `[[cond, value], …]`, default? | if/then/else; default is the trailing positional arg |
|
|
204
|
+
| `if` | same as `case` | alias for `case` |
|
|
205
|
+
| `coalesce` | 2+ expressions | first non-null |
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
["case", { "lib/uuid": "<mint>", "lib/expression-name": "Tier" },
|
|
209
|
+
[[[">", {…}, ["field", {…}, 14], 100], "Premium"],
|
|
210
|
+
[["<=", {…}, ["field", {…}, 14], 20], "Budget"]],
|
|
211
|
+
"Standard"]
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
(`case`'s first arg is a list of `[condition, value]` pairs; the optional 4th
|
|
215
|
+
positional arg is the default.)
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## References (within clauses)
|
|
220
|
+
|
|
221
|
+
| Ref | Shape | Notes |
|
|
222
|
+
| ----------- | ------------------------------------ | ------------------------------------------------------------------------------ |
|
|
223
|
+
| field | `["field", {…}, <field-id>]` | numeric id; options may carry `base-type`, `temporal-unit`, `binning` |
|
|
224
|
+
| expression | `["expression", {…}, "<name>"]` | by the expression's `lib/expression-name` string |
|
|
225
|
+
| aggregation | `["aggregation", {…}, "<agg-uuid>"]` | 3rd arg is the **string** `lib/uuid` of the target aggregation, not a position |
|
|
226
|
+
|
|
227
|
+
## Field option: temporal bucketing
|
|
228
|
+
|
|
229
|
+
`temporal-unit` in a field ref's options buckets a datetime.
|
|
230
|
+
|
|
231
|
+
- Truncation: `default`, `millisecond`, `second`, `minute`, `hour`, `day`, `week`,
|
|
232
|
+
`month`, `quarter`, `year`.
|
|
233
|
+
- Extraction (returns an integer): `minute-of-hour`, `hour-of-day`, `day-of-week`,
|
|
234
|
+
`day-of-week-iso`, `day-of-month`, `day-of-year`, `week-of-year`, `week-of-year-iso`,
|
|
235
|
+
`month-of-year`, `quarter-of-year`, `year-of-era`, `second-of-minute`.
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
["field", { "lib/uuid": "<mint>", "temporal-unit": "month" }, 22]
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## Field option: binning
|
|
242
|
+
|
|
243
|
+
`binning` in a field ref's options groups a numeric/coordinate column.
|
|
244
|
+
|
|
245
|
+
| `strategy` | Extra property | Notes |
|
|
246
|
+
| ----------- | -------------------- | -------------------------------- |
|
|
247
|
+
| `num-bins` | `num-bins` (integer) | fixed number of equal-width bins |
|
|
248
|
+
| `bin-width` | `bin-width` (number) | fixed bin width |
|
|
249
|
+
| `default` | — | Metabase chooses |
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
["field", { "lib/uuid": "<mint>", "binning": { "strategy": "num-bins", "num-bins": 10 } }, 14]
|
|
253
|
+
```
|
|
@@ -17,47 +17,9 @@ A transform has two halves:
|
|
|
17
17
|
- `source` — the query to run (`type: "query"`, with `query.type` of `native` or `mbql`).
|
|
18
18
|
- `target` — the warehouse destination (`type: "table"`, with `database`, `schema`, `name`).
|
|
19
19
|
|
|
20
|
-
Native SQL is the simplest source and the easiest to author by hand. MBQL is what the Metabase UI emits and is
|
|
20
|
+
Native SQL is the simplest source and the easiest to author by hand (see "Create + run" below). MBQL is what the Metabase UI emits and is more verbose; pull a sample with `mb transform get <id> --full --json` if you need its shape.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
**Mint UUIDs for `lib/uuid` slots before assembling the body — never invent, hard-code, or reuse them.** Every clause options object carries a `lib/uuid` (UUID v4); the bundled schema enforces RFC 4122 format strictly, so placeholder strings fail `--dry-run`. Workflow: count the slots, run `mb uuid --count <N> --json`, substitute each minted value into its slot. The examples below use `<UUID:label>` sentinels (NOT valid UUIDs) so the assembly step is unambiguous — replace each sentinel with a freshly-minted UUID before sending. Same `<UUID:label>` token must be replaced with the same minted UUID (used for aggregation-ref ↔ aggregation pairing); distinct sentinels get distinct UUIDs.
|
|
25
|
-
|
|
26
|
-
**Clause shape: opts always second, args after.** Every clause is `[op, {options}, ...args]`. Field refs are `["field", {options}, fieldId]` (id third), not the legacy MBQL 4 shape `["field", id, opts]`. The same rule holds for aggregations, filters, order-by — the options object never moves out of slot 1.
|
|
27
|
-
|
|
28
|
-
## MBQL 5 aggregations: name your output columns
|
|
29
|
-
|
|
30
|
-
Default MBQL 5 aggregations materialize as `count`, `count_where`, `count_where_2`, `avg`, `avg_2`, `sum`, … — ugly when the result is a transform target. Pass `name` and `display-name` in the aggregation's options object to control them. Mint 4 UUIDs (`mb uuid --count 4 --json`) for the slots below before assembling:
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
["count",
|
|
34
|
-
{"lib/uuid": "<UUID:agg-shipped>", "name": "shipments_shipped", "display-name": "Shipments shipped"}]
|
|
35
|
-
|
|
36
|
-
["count-where",
|
|
37
|
-
{"lib/uuid": "<UUID:agg-delivered>", "name": "shipments_delivered", "display-name": "Shipments delivered"},
|
|
38
|
-
["=", {"lib/uuid": "<UUID:eq-filter>"},
|
|
39
|
-
["field", {"base-type": "type/Text", "lib/uuid": "<UUID:status-field>"}, 1779],
|
|
40
|
-
"delivered"]]
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
The `name` value becomes the warehouse column name on the materialized table. The `display-name` is the column header in the UI.
|
|
44
|
-
|
|
45
|
-
## MBQL 5 order-by referencing an aggregation
|
|
46
|
-
|
|
47
|
-
Order by an aggregation column with an `["aggregation", {…}, "<aggregation-uuid>"]` ref — the third arg is the **string UUID** of the target aggregation's `lib/uuid`, **not** its numeric position. The aggregation's own `lib/uuid` and the ref's third element must be the same minted UUID (string equality); the order-by clause itself and the ref clause each carry their own separate `lib/uuid` in their options. Mint 3 UUIDs and substitute — note that `<UUID:agg-count>` appears twice and gets the same minted value:
|
|
48
|
-
|
|
49
|
-
```json
|
|
50
|
-
"aggregation": [
|
|
51
|
-
["count", {"lib/uuid": "<UUID:agg-count>"}]
|
|
52
|
-
],
|
|
53
|
-
"order-by": [
|
|
54
|
-
["desc", {"lib/uuid": "<UUID:order-desc>"},
|
|
55
|
-
["aggregation", {"lib/uuid": "<UUID:agg-ref>"},
|
|
56
|
-
"<UUID:agg-count>"]]
|
|
57
|
-
]
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
A numeric index (`["aggregation", {…}, 0]`) fails pre-flight with `must be the target aggregation's lib/uuid (string), not a numeric position` at `/stages/0/order-by/0/2/2`.
|
|
22
|
+
For an **MBQL 5** `source.query` (`lib/type: "mbql/query"`), the body shape, the "options object is always second" clause rule, UUID minting, aggregation/order-by refs, naming aggregation output columns, and the `--print-schema` → `--dry-run` validation loop are all in the `mbql` skill — **`mb skills get mbql`**. The MBQL-5 pre-flight on `transform create`/`update` is documented there too (legacy MBQL 4 and native sources skip it). For a transform target, naming your aggregation output columns matters more than usual — a bare `count` / `avg_2` becomes the warehouse column name; see the `mbql` skill's "Naming aggregation output columns".
|
|
61
23
|
|
|
62
24
|
## Create + run (native SQL)
|
|
63
25
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: viz
|
|
3
|
+
description: Author Metabase `visualization_settings` and pick the right `display` for cards via the `mb` CLI. Covers the display → settings-namespace map (graph.*, pie.*, funnel.*, scalar.*, table.*, …), the column-name-vs-numeric-field-id rule, the `column_settings` JSON-string-key footgun, worked API-form examples per chart family, and the pull-from-UI escape hatch for complex charts. Load whenever shaping how a card renders by hand — "create a bar chart", "make this a line chart", "format this column as currency", "set the pie dimension and metric", "the card renders as a table instead of a chart", "add conditional formatting", or any `visualization_settings` work.
|
|
4
|
+
allowed-tools: Read, Write, Edit, Bash, AskUserQuestion
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Visualization settings
|
|
8
|
+
|
|
9
|
+
A card has two presentation fields alongside its `dataset_query`:
|
|
10
|
+
|
|
11
|
+
- **`display`** — the chart type (`bar`, `line`, `pie`, `scalar`, `table`, …). One closed set of values; pick from the enum below.
|
|
12
|
+
- **`visualization_settings`** — a free-form map whose keys are **namespaced by `display`** (`graph.*` for bar/line, `pie.*` for pie, …). The server stores almost anything and **silently ignores keys that don't apply** to the chosen `display`.
|
|
13
|
+
|
|
14
|
+
The MBQL pre-flight does **not** validate `visualization_settings` — there is no `--skip-validate` to fail past, because nothing checks it. A `display` typo or a misnamed key is accepted by the API and the card just renders as a default table or drops the setting. So **the feedback loop is read-back, not pre-flight**: after `card create`/`update`, confirm with `mb card get <id> --full --json` (or open the card) that it rendered as intended.
|
|
15
|
+
|
|
16
|
+
The general flag conventions and body-input precedence live in the `core` skill (`mb skills get core`); the `dataset_query` body itself is the `mbql` skill's job (`mb skills get mbql`). This skill is only about how the result is displayed.
|
|
17
|
+
|
|
18
|
+
## `display` decides everything
|
|
19
|
+
|
|
20
|
+
`display` selects which setting namespace is read. Pick `display` first, then only the matching namespace's keys matter.
|
|
21
|
+
|
|
22
|
+
| `display` | Settings namespace(s) | Key columns are named by |
|
|
23
|
+
| ------------------------------------------- | --------------------------------------------------------- | ------------------------ |
|
|
24
|
+
| `bar` `line` `area` `combo` `row` `scatter` | `graph.*`, `series_settings`, `column_settings` | output column **name** |
|
|
25
|
+
| `waterfall` | `graph.*` + `waterfall.*` | output column name |
|
|
26
|
+
| `boxplot` | `graph.*` + `boxplot.*` | output column name |
|
|
27
|
+
| `pie` | `pie.*`, `column_settings` | output column name |
|
|
28
|
+
| `scalar` `number` | `scalar.*` | output column name |
|
|
29
|
+
| `smartscalar` | `scalar.comparisons` | output column name |
|
|
30
|
+
| `funnel` | `funnel.*` | output column name |
|
|
31
|
+
| `gauge` | `gauge.*` | — |
|
|
32
|
+
| `map` | `map.*` | output column name |
|
|
33
|
+
| `pivot` | `pivot_table.*`, `table.*` | output column name |
|
|
34
|
+
| `sankey` | `sankey.*` | output column name |
|
|
35
|
+
| `table` | `table.*`, `column_settings` | output column name |
|
|
36
|
+
| `object` `list` | `column_settings` | output column name |
|
|
37
|
+
| `progress` | `progress.goal`, `progress.color` (sparse — pull from UI) | — |
|
|
38
|
+
| `heading` `text` `link` `iframe` `action` | dashcard-only `virtual_card` (see references) | — |
|
|
39
|
+
|
|
40
|
+
Closed `display` enum (card-level): `table`, `bar`, `line`, `area`, `row`, `pie`, `scalar`, `smartscalar`, `number`, `combo`, `pivot`, `funnel`, `map`, `scatter`, `waterfall`, `progress`, `gauge`, `object`, `list`, `sankey`, `boxplot`. (`heading`/`text`/`link`/`iframe`/`action` are dashcard virtuals, not standalone cards.) An unknown value is accepted by the API but renders nothing useful — typos like `bargraph`/`linechart` are the most common cause of a "why is my chart blank" report.
|
|
41
|
+
|
|
42
|
+
## The rule that trips everyone: settings name **output columns**, by name
|
|
43
|
+
|
|
44
|
+
`graph.dimensions`, `graph.metrics`, `pie.dimension`, `pie.metric`, `scalar.field`, `funnel.metric`, `map.latitude_column`, … all take **output column-name strings** — the names the query _produces_, not field ids. A `count` aggregation outputs the column `count`; a breakout on a field outputs that field's name; a named aggregation outputs its `name`. These strings are **identical in the API form and the portable (git-sync) form** — no numeric-vs-name footgun here.
|
|
45
|
+
|
|
46
|
+
So the names you put in `visualization_settings` come from the query's output, not from `mb field`/`mb table`. If you set `name` on an aggregation (see the `mbql` skill, "Naming aggregation output columns"), use that same string here.
|
|
47
|
+
|
|
48
|
+
## Minimum-viable settings per chart family (API form)
|
|
49
|
+
|
|
50
|
+
Each example is the `visualization_settings` block to pair with the given `display` on a `card create`/`update` body. The `dataset_query` is elided — build it per the `mbql` skill. The output columns referenced (`CATEGORY`, `count`) are whatever the query's breakout/aggregation produce.
|
|
51
|
+
|
|
52
|
+
**Bar / line / area** — one dimension on the x-axis, one or more metrics:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
"display": "bar",
|
|
56
|
+
"visualization_settings": {
|
|
57
|
+
"graph.dimensions": ["CATEGORY"],
|
|
58
|
+
"graph.metrics": ["count"]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
(Switch `display` to `line` or `area` with the same `graph.*` keys. Multiple metrics: `"graph.metrics": ["count", "sum"]`. Stacked: add `"stackable.stack_type": "stacked"`.)
|
|
63
|
+
|
|
64
|
+
**Pie** — one dimension, one metric:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
"display": "pie",
|
|
68
|
+
"visualization_settings": { "pie.dimension": "CATEGORY", "pie.metric": "count" }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Scalar** (single big number) — the field to surface:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
"display": "scalar",
|
|
75
|
+
"visualization_settings": { "scalar.field": "count" }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Table** — column order/visibility plus per-column formatting:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
"display": "table",
|
|
82
|
+
"visualization_settings": {
|
|
83
|
+
"table.columns": [
|
|
84
|
+
{ "name": "CATEGORY", "enabled": true },
|
|
85
|
+
{ "name": "count", "enabled": true }
|
|
86
|
+
],
|
|
87
|
+
"column_settings": {
|
|
88
|
+
"[\"name\",\"count\"]": { "column_title": "Orders" }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
An **empty** `"visualization_settings": {}` is valid for any `display` — Metabase falls back to sensible defaults (it auto-picks dimensions/metrics for a simple aggregate). Set keys only to override the defaults.
|
|
94
|
+
|
|
95
|
+
## `column_settings`: the JSON-string-key footgun
|
|
96
|
+
|
|
97
|
+
`column_settings` is a map **whose keys are themselves JSON-encoded arrays** — so inside a JSON body the inner quotes must be escaped. The key is a _string_, never an object.
|
|
98
|
+
|
|
99
|
+
- **Prefer the name form:** `["name", "<output column name>"]` → in a JSON body, `"[\"name\",\"count\"]"`. This is the canonical key Metabase writes (`getColumnKey`), and it's **identical in API and portable form**. Use it unless you have a reason not to.
|
|
100
|
+
- **Ref form (legacy order!):** `["ref", ["field", <id>, <opts>]]`. The inner field ref here uses the **legacy MBQL-4 order** `["field", id, options]` (id **second**) — _not_ the MBQL-5 order `["field", {options}, id]` you use in `dataset_query`. In the API form `<id>` is the **numeric** field id; the portable form uses a name path. Because the order differs from MBQL 5, this form is easy to get wrong — reach for the name form instead.
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
"column_settings": {
|
|
104
|
+
"[\"name\",\"TOTAL\"]": { "number_style": "currency", "currency": "USD", "decimals": 2 },
|
|
105
|
+
"[\"name\",\"CREATED_AT\"]": { "date_style": "MMMM D, YYYY" }
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The exhaustive per-column key list (number/date/link formatting, `view_as`, click behavior) is in the references file.
|
|
110
|
+
|
|
111
|
+
## Escape hatch: pull a real card instead of authoring from scratch
|
|
112
|
+
|
|
113
|
+
For anything beyond a single dimension + metric — combo charts, conditional formatting, pivot splits, click behavior, series colors — the cheapest **correct** path is to build it once in the Metabase UI and copy the result:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
mb card get <id> --full --json | jq '.visualization_settings'
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Paste that block into your `card create`/`update` body. The server produced it, so it's guaranteed valid for that `display`. This beats guessing keys from memory, and it's token-cheap (no schema dump).
|
|
120
|
+
|
|
121
|
+
## Full key catalog
|
|
122
|
+
|
|
123
|
+
The body above covers the high-frequency 90%. The complete per-display key tables — every `graph.*`/`pie.*`/`table.*`/… key, series settings, conditional formatting rules, pivot splits, the full `column_settings` formatting vocabulary, virtual-card (heading/text/link/iframe) settings, and click behavior — live in this skill's references file. Load it on demand, not by default:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
mb skills get viz --full # appends references/settings.md to this body
|
|
127
|
+
mb skills path viz # → the skill dir; then Read references/settings.md
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Don't
|
|
131
|
+
|
|
132
|
+
- Don't invent `display` values (`bargraph`, `linechart`, `histogram`) — use the closed enum; the API accepts the typo and renders nothing.
|
|
133
|
+
- Don't put numeric field ids in `graph.dimensions`/`pie.metric`/`scalar.field` etc. — they take **output column-name strings**.
|
|
134
|
+
- Don't write a `column_settings` key as an object — it's a JSON **string** (`"[\"name\",\"COL\"]"`), inner quotes escaped.
|
|
135
|
+
- Don't use the MBQL-5 field-ref order inside a `column_settings` `["ref", …]` key — that key uses the **legacy** `["field", id, opts]` order. Prefer the `["name", …]` form and sidestep it.
|
|
136
|
+
- Don't expect a pre-flight to catch viz mistakes — there is none. Verify by reading the card back.
|
|
137
|
+
- Don't hand-author complex charts when you can pull a working `visualization_settings` from a UI-built card.
|