@malloydata/malloyyo 0.2.38 → 0.2.40
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/dist/index.js +65 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -208,6 +208,7 @@ var contentFiles = {
|
|
|
208
208
|
"develop/connection-setup.md": '---\ndescription: Setting up a data connection (malloy-config.json)\n---\n\n# Setting up a data connection\n\nA model reaches its data through a **connection** declared in\n`malloy-config.json` at the **root of the model** (next to `index.malloy`). This\nis Malloy\'s standard connection config \u2014 the full reference, with every\nconnector\'s properties, is at\n<https://docs.malloydata.dev/documentation/setup/config>. The essentials:\n\n## The file\n\n```json\n{\n "connections": {\n "mydb": { "is": "duckdb" }\n }\n}\n```\n\n- A connection has a **name** (the key) and a type (`is`). Sources refer to it by\n that name \u2014 `source: x is mydb.table("orders")` or `mydb.sql("SELECT \u2026")` \u2014 so\n the name in the config must match the name in the model.\n- Supported types (`is`): `duckdb` (incl. MotherDuck), `bigquery`, `postgres`,\n `mysql`, `snowflake`, `databricks`, `trino`, `presto`. Each has its own\n properties \u2014 see the full docs.\n\n## Default connections (and when they apply)\n\nSetting `"includeDefaultConnections": true` makes one connection available for\n**each registered database type, named by the type** \u2014 a `duckdb` connection\nnamed `duckdb`, a `postgres` named `postgres`, and so on. Each uses that\nconnector\'s default settings, which for several backends means picking up\ncredentials from the environment (e.g. BigQuery\'s application-default\ncredentials \u2014 see the per-connector setup docs). Connections you name explicitly\nin `connections` always win; the defaults only fill in types you didn\'t list.\n\n```json\n{\n "includeDefaultConnections": true,\n "connections": { "warehouse": { "is": "postgres", "host": "\u2026" } }\n}\n```\n\nmalloyyo has one rule worth knowing:\n\n- **No `malloy-config.json` at all \u2192 defaults are ON.** `duckdb` just works with\n zero setup.\n- **Write a `malloy-config.json` and they turn OFF** unless you add\n `"includeDefaultConnections": true`. A config that only defines, say, a\n `postgres` connection will report *No connection named "duckdb"* if a source\n still references `duckdb`.\n\nThis is deliberate \u2014 your local connections then resolve exactly the way the\npublished server\'s will, rather than silently leaning on a default that would not\nexist in production. (It differs from `malloy-cli`, which forces the defaults on\nunconditionally.)\n\n*No connection named* has one cause that is **not** about your config: if more\nthan one copy of `@malloydata/malloy` is installed, the connectors register with\none copy and malloyyo reads another, and every connection looks missing. malloyyo\ndetects that and says so instead \u2014 if it doesn\'t mention duplicate copies, the\nproblem really is the config. To check by hand: `npm ls @malloydata/malloy`.\n\nGive a connection a **custom name** \u2014 not the bare type default \u2014 whenever you\nhave more than one connection of the same type, or need non-default parameters.\n\n## DuckDB and local files (the common case)\n\nDuckDB can either open a **DuckDB database file** or read **local data files**\n(CSV, Parquet, \u2026) directly.\n\n**A pre-loaded database file** \u2014 point `databasePath` at a `.duckdb` file and\nreference its tables by name (an absolute path is safest):\n\n```json\n{ "connections": { "warehouse": { "is": "duckdb", "databasePath": "/data/warehouse.duckdb" } } }\n```\n\n**Local files, by path** \u2014 read a CSV or Parquet file straight into a source.\nThese paths are **project-relative** \u2014 resolved against the model root, so they\nsurvive publishing:\n\n source: my_csv is duckdb.table(\'data/my_file.csv\')\n source: my_parquet is duckdb.table(\'data/my_file.parquet\')\n\n`.table()` names a single file. When you need something it can\'t express \u2014 a\nglob, a union, any SQL \u2014 wrap it in `.sql()` (its paths are project-relative too):\n\n source: payments is duckdb.sql(\n "SELECT * FROM read_parquet(\'data/payments-*.parquet\')"\n )\n\nThe default `duckdb` connection is **in-memory** \u2014 nothing persists between runs;\nyour data lives in the files (or the `databasePath` database) you read.\n\n**MotherDuck:** a DuckDB connection against an `md:` database; set the\n`MOTHERDUCK_TOKEN` environment variable.\n\n## Secrets \u2014 keep them out of the file\n\nAny property value may be written as `{ "env": "VAR_NAME" }`. It resolves from\n`process.env.VAR_NAME` when the connection opens, so passwords and tokens never\nget committed:\n\n```json\n{\n "connections": {\n "analytics": {\n "is": "postgres",\n "host": "db.internal",\n "databaseName": "analytics",\n "username": "reader",\n "password": { "env": "PG_PASSWORD" }\n }\n }\n}\n```\n\n(The non-secret property names here are illustrative \u2014 each connector\'s exact\nproperties are in the full docs. The `{ "env": \u2026 }` form is the part that\nmatters: it works for any value.)\n\n## malloyyo specifics\n\n- **One file, one place.** Only the `malloy-config.json` at the model root is\n read \u2014 there is no walk-up to parent directories.\n- **Local override:** a `malloy-config-local.json` (do **not** commit it)\n **replaces** `malloy-config.json` entirely when present \u2014 your private variant\n for local credentials or a different database.\n- **The same file ships to production.** Publishing uploads this exact\n `malloy-config.json`, so it must resolve the same way locally and on the\n server \u2014 put anything environment-specific behind `{ "env": \u2026 }` rather than\n hard-coding it. That is what makes the local test window faithful to\n production.\n- Edits are picked up **without a restart** \u2014 the server re-reads the file when\n it changes.\n\n## When a connection will not resolve\n\nFix the connection **first**: a broken connection yields an empty schema and then\na cascade of misleading `field-not-found` errors \u2014 ignore those and fix the\nconnection. A fast check is to compile a probe and see if it alone compiles:\n\n source: _probe is mydb.sql("SELECT 1 AS one")\n',
|
|
209
209
|
"develop/getting-started.md": '---\ndescription: Getting started \u2014 build a Malloy model step by step\n---\n\n# Building a Malloy model, step by step\n\nA model is a `malloy-config.json` (the connection to the data) and an\n`index.malloy` (the published query surface), optionally with other `.malloy`\nfiles that `index.malloy` imports. You edit these with your own file tools; the\nMCP tools compile, inspect, and test what you wrote. Never read `.malloy` as\ntext \u2014 compiling a bare source is how you read a table\'s schema.\n\n## 1. Verify the connection first\n\nConfirm the connection named in `malloy-config.json` resolves \u2014 compile a\nthrowaway probe inline with `compile` (no file needed):\n\n source: _probe is CONN.sql("SELECT 1 AS one")\n\nIf it compiles, the connection is good. If not, fix the connection / config\nbefore going further \u2014 a broken connection produces an empty schema and then a\ncascade of misleading `field-not-found` errors; ignore the cascade and fix the\nconnection. Call `yo_help("develop/connection-setup")` for how to set up or repair a connection.\n\n## 2. Identify the tables the model needs\n\nIf you are unsure which tables matter, ask the fox \u2014 they own the data and know\nwhere it lives.\n\n## 3. Get a base source per table\n\nA base is "what\'s in the table and what\'s computable from it" \u2014 no joins.\nDiscover the schema by compiling a bare stub inline with `compile`:\n\n source: users_base is CONN.table("users")\n\n`compile` returns the full column list + types \u2014 that is your schema browser.\nThen write the base into its own file and iterate with `compile_file`, adding\nonly the dimensions and measures intrinsic to that one table:\n\n // users_base.malloy\n source: users_base is CONN.table("users") extend {\n measure: user_count is count()\n }\n\nIf the data lives in files rather than database tables \u2014 common when the\nconnection is DuckDB \u2014 DuckDB lets you name a file path as the table (a\nproject-relative path):\n\n source: users_base is CONN.table(\'data/users.parquet\')\n\nOnly drop to a `.sql()` block when a single file-as-table can\'t express what you\nneed \u2014 e.g. globbing or unioning several files:\n\n source: users_base is CONN.sql("SELECT * FROM read_parquet(\'data/users-*.parquet\')")\n\n(`read_parquet` there is DuckDB SQL, not Malloy \u2014 see `yo_help("develop/connection-setup")`.)\n\n## 4. Assemble index.malloy \u2014 the published surface\n\nImport the bases, join them into the consumer-facing sources, and explicitly\nexport what consumers may query:\n\n import "users_base.malloy"\n import "orders_base.malloy"\n\n source: users is users_base extend {\n join_many: orders is orders_base on id = orders.user_id\n }\n source: orders is orders_base extend { }\n\n export { users, orders }\n\n**Export discipline** \u2014 the model is a published artifact, so be deliberate about\nits public surface:\n\n- Imported names are private. Base sources stay internal scaffolding unless you\n export them.\n- Without an `export` statement, everything you define is public. Add one and the\n surface becomes explicit: only the names you list \u2014 defined or imported \u2014 are\n public.\n- Hide intermediates. A staging source you define only so other sources can build\n on it should not be exported.\n- The export list is the consumer\'s menu \u2014 exactly what the test window and real\n consumers can query, nothing more.\n\n## The loop\n\nEdit a file \u2192 `compile_file` \u2192 fix `problems[]` \u2192 `query` (pass the model file\'s\npath as `ref`; `execute: false` validates first and reports the givens the query\nneeds, supplied via `givens`). Query text is restricted \u2014 `import`, `given:`\ndeclarations, `connection.table`/`connection.sql`, raw SQL, and `##!` flags\nbelong in the model file, not the query. When `compile`/`compile_file` returns\n`formatted: false`, call `prettify` and save its output. Use project-relative\ndata paths, not absolute \u2014 they resolve against the project root and survive\npublishing the model.\n',
|
|
210
210
|
"develop/working-with-models.md": "---\ndescription: Working with an existing Malloy model\n---\n\n# Working with an existing model\n\nAn existing model is an `index.malloy` (plus any `.malloy` files it imports) and\na `malloy-config.json`.\n\n## Understand the model\n\n- **Read `malloy-config.json` directly** \u2014 it is JSON, so read it as text. It\n lists the connection(s) the model queries against; `yo_help(\"develop/connection-setup\")` explains\n the format (and how to set one up or repair it).\n- **Do NOT read `.malloy` as text \u2014 compile it.** `compile_file` returns the\n structured model: each source with its fields, joins, views, and named queries,\n plus `problems[]`. That is how you describe what is in the model. (Compiling a\n bare source \u2014 no `extend` block \u2014 likewise reads a raw table's schema.)\n\n## The loop\n\nEdit a file \u2192 `compile_file` \u2192 fix `problems[]` \u2192 `query`. Pass the model file's\npath as `ref`; `execute: false` validates first and reports the givens the query\nneeds (supplied via `givens`). Query text is restricted \u2014 `import`, `given:`\ndeclarations, `connection.table`/`connection.sql`, raw SQL, and `##!` flags\nbelong in the model file, not the query. When `compile`/`compile_file` returns\n`formatted: false`, run `prettify` and save its output. Use project-relative data\npaths, not absolute.\n",
|
|
211
|
+
"explore/charting-results.md": "---\ndescription: Answer with a chart \u2014 the render tags, how channels are chosen, and when a chart beats a table\n---\n\n# Charting a result\n\nA query result renders as a table unless you tag it. One line above the query\nturns it into a chart, and the tag travels with the result \u2014 so a shared link or\na saved query draws the same picture later.\n\n```malloy\n# bar_chart\nrun: order_items -> {\n group_by: brand is inventory_items.product_brand\n aggregate: total_sales\n order_by: total_sales desc\n limit: 10\n}\n```\n\n**A tag on its own line attaches to the thing on the NEXT line.** That is the\nwhole placement rule, and it is also the trap:\n\n```malloy\n// WRONG \u2014 the tag attaches to `birth_year`, not to the query.\n// Malloy compiles this, runs it, returns rows, and silently draws a TABLE.\nrun: baby_names -> {\n where: name = 'James' | 'Michael'\n # line_chart { x=birth_year y=total_babies series=name }\n group_by: birth_year\n group_by: name\n aggregate: total_babies\n}\n```\n\n```malloy\n// RIGHT \u2014 above `run:`, so it attaches to the query.\n# line_chart { x=birth_year y=total_babies series=name }\nrun: baby_names -> {\n where: name = 'James' | 'Michael'\n group_by: birth_year\n group_by: name\n aggregate: total_babies\n}\n```\n\nNothing warns you. The tag simply does not appear in the result's annotations,\nand the renderer has no chart to draw. If you tagged a query and got a table,\nthis is why, before anything else.\n\nRelated: `yo_help dashboards/charts` goes deeper on the dimension-counting rule\nand on sorting a named axis; this topic is about answering a question with a\npicture.\n\n## Choose the shape from the question\n\n| The question is about\u2026 | Use | The x axis is |\n|---|---|---|\n| ranking or comparing categories | `# bar_chart` | the category |\n| change over time | `# line_chart` | the time field |\n| whether two measures relate | `# scatter_chart` | the first measure |\n| variation across US states | `# shape_map` | the state name |\n\nIf none of those is what was asked, **leave it a table**. A chart of eight\ncolumns is worse than the eight columns, and a bar chart whose bars are all the\nsame height says \"no pattern\" at the cost of the reader's attention.\n\n## Name the channels\n\n```malloy\n# bar_chart { x=brand y=total_sales }\n# bar_chart { x=nickname y=flight_count series=destination }\n# line_chart { x=order_month y=['sales', 'cost'] }\n```\n\nLeft unset they are inferred: **x** takes a time dimension if there is one and\notherwise the first dimension, **y** takes the first aggregate, and \u2014 the rule\nthat catches people \u2014 **any leftover dimension becomes a colour series**.\n\nSo a result with two `group_by` fields and only `x` named will grow a legend you\ndid not ask for, and one with three or more untagged dimensions is refused\noutright:\n\n> Too many dimensions. A bar chart can have at most 2 dimensions: 1 for the x\n> axis, and 1 for the series.\n\nAggregates are exempt \u2014 a result may carry as many measures as you like; only\nthose named in `y` are drawn.\n\n**`# hidden` does not exempt a dimension from that count.** It hides a column in\na table; a hidden `group_by` is still a dimension and will still be promoted to a\nseries.\n\n*If you need a sort key that is not a channel, make it a MEASURE* \u2014\n`aggregate: sort_key is min(month_number)` \u2014 because measures can never be\npromoted. That is the standard fix for \"label it January, order it first\".\n\n## Properties worth knowing\n\n| | |\n|---|---|\n| `size` | `spark`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, or `size.width` / `size.height` in pixels |\n| `stack` | `# bar_chart.stack` \u2014 stack instead of group |\n| `zero_baseline` | line charts: force the y axis to include zero |\n| `series.limit` | how many series before the rest are dropped (bar 20, line 12) |\n| `title`, `subtitle` | a heading on the chart itself |\n| `x.limit` | cap the categories drawn |\n\nChannels can also be tagged on the fields instead of in the block, which reads\nbetter when the query is long:\n\n```malloy\n# bar_chart\nrun: flights -> {\n group_by:\n # series\n destination\n # x\n carriers.nickname\n aggregate:\n # y\n flight_count\n}\n```\n\n## Scatter and maps take fields in ORDER\n\n`# scatter_chart` reads them positionally: **x, y, colour, size, shape**. There\nare no channel names to set, so the order of your `group_by`/`aggregate` clauses\nis the encoding.\n\n`# shape_map` is **US states only**, and wants **state name first, value\nsecond**. `# segment_map` takes `lat1, lon1, lat2, lon2, colour`.\n\n## Format the numbers\n\nA chart with raw floats on the axis is harder to read than the table it\nreplaced. These attach to a field, not to the chart:\n\n```malloy\naggregate:\n # currency=usd2m\n total_sales\n # percent\n share_of_sales\n # number=\"#,##0\"\n order_count\n```\n\n`# duration`, `# data_volume`, `# link` and `# image` follow the same shape.\n\n## Check it drew\n\nA chart that compiles can still refuse to render \u2014 \"too many dimensions\" happens\nat draw time, not compile time. If you ran the query and the caller sees a table\nwhere you expected a chart, the tag did not take: check that it is on its own\nline directly above the query, and that you have not left a spare dimension to be\npromoted.\n",
|
|
211
212
|
"explore/how-to.md": '---\ndescription: Extended instructions working with this MCP server\n---\n# General Notes\nMalloy is a combined semantic layer/query language. It describes data and analysis, and it can generate and execute SQL.\n\nYou answer questions from published Malloy semantic models. A model publishes sources and queries.\n\n* Some tools return problems[] to indicate invalid Malloy. problems may have a `help_topic` field \u2014 call `yo_help(help_topic)` for detailed guidance.\n* `yo_help()` with no topic will show an index which include error explanations, examples of Malloy syntax for common patterns, and a language reference manual (Malloy syntax is still evolving).\n* Tools that inspect Malloy code return objects with schemas, among other things. An entry with a name which requires `back-tick-quoting` (reserved word, special characters), will have `must_quote: true`\n* When limiting queries, do ranking, top-N, and member selection in Malloy, not in client code. Results are byte-budgeted: oversized results are truncated (the response says so and may link the full result). Reading aggregated rows is better analysis and the only way to see everything.\n* Compose your answer from what the model publishes. When composing a query, you can make new sources, extending existing sources with measures dimensions and joins. If the model\'s surface genuinely cannot answer a question, that is useful signal about the model.\n\n# Answering A Question\nTo answer a question you need to see what sources are available which pertain to the question.\n\n`list_sources` (when available) \u2014 see the sources you can query, grouped by model, with each model\'s named queries. If you already know the source, go straight to describe_source.\n\n# Build the Query\n* New to a pattern? `yo_help("explore/query-examples")` \u2014 the handful of Malloy query shapes (views, the workhorse group_by/aggregate, filtered aggregates, `all()`, `extend:`, `select:`, `nest:`) that cover almost every question, with the SQL habits that are wrong in Malloy.\n* `describe_source(source, model_ref)` \u2014 always describe a source before querying it (`model_ref` optional when the name is unique). Returns:\n * `described_source` \u2014 the source\'s `dimensions` (columns), `measures`, and `views` (the author\'s saved queries). A dimension\'s `type` is a scalar or a nested record (`origin.city`); an array column has no `type` \u2014 it shows up as a `joins` entry at its `path`.\n * `joins` \u2014 keyed by path, the arrays and source-joins this source reaches. `fans_out` marks a path that fans out. Each entry is one of: `{ source }` (fields in `join_source_map`), `{ source_def }` (an anonymous source\'s fields, inline), or an array `{ is_array, source_def }` \u2014 a record array\'s fields are used directly (`parcels.sku`), a scalar array\'s element is `each` (`tags.each`). To write a reference, use the entry\'s `quoted_path` if it has one, else the key.\n * `join_source_map` \u2014 the named sources those `{ source }` joins resolve to, deduped.\n * In its own content block, the source\'s raw Malloy, for anything the structured output above doesn\'t cover.\n* `query(source: "...", malloy: "run: source -> { ... }", execute: false)` \u2014 validate without running; it returns the SQL. Iterate until clean. (`model_ref` optional, needed only when the source name is ambiguous.)\n* Some queries accept parameters (givens). More info if needed: yo_help("language/givens-model-level-parameters")\n\n# Run the query\n* Pass a plain-English question with EVERY query, describing what that specific query answers. Queries are recorded/shared independently. Don\'t try to group related queries.\n* query(source: "...", malloy: "run: source -> { ... }", question: "...") \u2014 run it; get the rows.\n\n# Displaying Results\n* Lead with a natural-language restatement of the query \u2014 a short heading works well.\n* A successful query comes back with an ltool_link \u2014 {text, url}, already assembled. It opens this exact query so the user can keep exploring, or share the insight. Follow the data with a markdown link, [\u2197 text](url).\n* When it helps the reader add a short note on how you got the answer: the Malloy logic (filters, grouping, aggregation, ordering, pipeline stages), and any post-processing done outside Malloy.',
|
|
212
213
|
"explore/query-examples.md": "---\ndescription: Worked query examples \u2014 the handful of Malloy shapes that cover almost every question. Read this before writing a query from scratch.\n---\n# Query Examples\n\nAlmost every query is one of the shapes below. Examples use a `flights` source\n(dimensions like `carrier`, `distance`, `origin`, `destination`, `state`,\n`dep_delay`, `is_small_plane`; measures `flight_count`, `total_distance`).\nSubstitute the real fields from `describe_source`.\n\nTwo rules first: do ordering, limiting, and ranking **in Malloy**, not in client\ncode \u2014 and reuse what the source already publishes before writing your own.\n\n## Run a saved view\n\nThe source may already answer the question. A view is invoked by name:\n\n```malloy\nrun: flights -> by_carrier\n```\n\nRefine a saved view in place with `+ { \u2026 }` \u2014 no need to rewrite it:\n\n```malloy\nrun: flights -> by_carrier + { where: state = 'CA' }\n```\n\n## The workhorse query\n\nWhen no view fits, write a stage. The vast majority of queries are this shape \u2014\n`group_by` the dimensions, `aggregate` the **measures the source already\ndeclares**, `where` to filter, `order_by` to rank. Reuse the measures\n`describe_source` lists (here `flight_count`); don't re-derive an aggregate the\nsource already defines (`count()`):\n\n```malloy\nrun: flights -> {\n group_by: destination\n aggregate: flight_count\n where: state = 'CA'\n order_by: flight_count desc\n limit: 10\n}\n```\n\nEvery clause is `keyword:` (note the colon, including `order_by:`).\n\n### Aggregate moves\n\n**Filtered aggregate** \u2014 `measure { where: \u2026 }` filters *that one number* only,\nindependent of the stage `where:`. Two kinds of `where`: stage-level (which rows\nenter the query) vs aggregate-level (which rows a single aggregate counts).\n\n```malloy\nrun: flights -> {\n group_by: carrier\n aggregate:\n flight_count\n percent_late is flight_count { where: dep_delay > 15 } / flight_count * 100\n}\n```\n\n**Aggregates from scratch** \u2014 *only when the source has no measure for what you\nneed*, define your own with `is`:\n\n```malloy\nrun: flights -> {\n group_by: carrier\n aggregate:\n flight_count is count() -- row count\n destination_count is count(destination) -- DISTINCT destinations\n total_distance is distance.sum() -- field-first aggregation\n}\n```\n\n**Percent of total with `all()`** \u2014 `all(expr)` ignores the `group_by:` to give\nthe grand total, so a share is `part / all(part)`:\n\n```malloy\nrun: flights -> {\n group_by: carrier\n aggregate:\n flight_count\n pct_of_total is flight_count / all(flight_count)\n}\n```\n\n(`all(expr, dim)` totals within a subgroup \u2014 it takes the **alias from\n`group_by:`**, not a dotted path.)\n\n### Declare once, reuse \u2014 `extend:`\n\nWhen an expression repeats in a query, define it locally in an `extend:` block:\n\n```malloy\nrun: flights -> {\n extend: {\n measure: total_distance is distance.sum()\n dimension: state_first_letter is substr(state, 1, 1)\n }\n group_by: state_first_letter\n aggregate:\n total_distance\n small_plane_distance is total_distance { where: is_small_plane }\n}\n```\n\n**Composing aggregates.** You can't reference one aggregate from another in the\nsame stage (`aggregate: a is \u2026, b is a/2` fails \u2014 *\"'a' is not defined\"*). To\nbuild a value *from* other aggregates \u2014 a ratio, a share \u2014 define the parts as\nmeasures in `extend:` (measures **can** reference each other), then use them:\n\n```malloy\nrun: flights -> {\n extend: {\n measure:\n late_flights is flight_count { where: dep_delay > 15 }\n pct_late is late_flights / flight_count\n }\n group_by: carrier\n aggregate: late_flights, pct_late\n order_by: pct_late desc\n}\n```\n\nTo rank by a computed value, name it (in `aggregate:` or `extend:`) and\n`order_by:` that name \u2014 `order_by:` takes an output field name, never a raw\nexpression, and there is no `derive:` step.\n\n## Flat detail rows \u2014 `select:`\n\nTo fetch raw rows instead of aggregating, use `select:` (the columns to return):\n\n```malloy\nrun: flights -> {\n select: id, carrier, origin, destination, distance\n where: distance > 1000\n order_by: distance desc\n limit: 100\n}\n```\n\nA stage is **either** a reduction (`group_by:` / `aggregate:`) **or** a projection\n(`select:`) \u2014 never both in the same stage. And `select:` **cannot** be combined\nwith `nest:` in any way; nesting belongs to reductions.\n\n## Nesting \u2014 a sub-table per row\n\n`nest:` attaches a whole query to each row of the outer one. This is where\nanswers get their depth (a per-row ranked breakdown):\n\n```malloy\nrun: flights -> {\n group_by: origin\n aggregate: flight_count\n nest: by_carrier is {\n group_by: carrier\n aggregate: flight_count\n order_by: flight_count desc\n limit: 5\n }\n}\n```\n\n**Listing top-N detail rows in a nest \u2014 `group_by:`, not `select:`.** A nest is a\nreduction, so to nest raw rows (not an aggregate) list the columns with\n`group_by:` (`select:` is not allowed inside a nest):\n\n```malloy\nrun: flights -> {\n group_by: carrier\n aggregate: flight_count\n nest: longest_flights is {\n group_by: origin, destination, distance\n order_by: distance desc\n limit: 5\n }\n}\n```\n\n## Multi-stage \u2014 aggregate, then aggregate again (`->`)\n\nA second `->` runs another stage over the **output** of the first. Reach for it\nwhen you need to aggregate an aggregate \u2014 e.g. the **peak** of a per-period\ntotal. You can't write `flight_count.max()` (that's an aggregate of an aggregate\n\u2014 it errors); compute the per-period total in one stage, then take the max in the\nnext:\n\n```malloy\nrun: flights -> {\n group_by: carrier, dep_year\n aggregate: flights_that_year is flight_count\n} -> {\n group_by: carrier\n aggregate: peak_year is flights_that_year.max()\n}\n```\n\nIn the second stage `flights_that_year` is an ordinary column (the first stage's\noutput), so `.max()` is valid. The same shape filters or re-ranks already-\naggregated rows.\n\n## Make a cell a clickable deep link \u2014 `# link`\n\nWhen the answer is \"here's the row, go look at it in the source system\", tag a\n`group_by:`/`select:` field with `# link` so its cell renders as a hyperlink\n(in the shareable ltool view and in dashboards). Three forms:\n\n```malloy\nrun: flights -> {\n # link -- the value IS a full URL\n group_by: page is concat('https://wikipedia.org/wiki/', origin)\n}\n```\n\n```malloy\nrun: flights -> {\n # link { url_template='https://www.flightsfrom.com/$$' } -- $$ = this cell's value\n group_by: origin\n}\n```\n\nLink to a value *other* than the one displayed with `field=`, and hide the raw\nid with `# hidden` so only the label shows:\n\n```malloy\nrun: flights -> {\n # link { url_template='https://crm.example.com/person/$$' field=person_id }\n group_by: person_name\n # hidden\n group_by: person_id\n}\n```\n\n`$$` is substituted anywhere in the template (`.../$$-SJC` works). Sibling\n`# image { url_template=\u2026 width= height= alt= }` renders the cell as an inline\nimage instead. Deep links open in a new browser tab.\n\n## SQL habits that are WRONG in Malloy\n\n| You'd write in SQL | Malloy |\n| --- | --- |\n| `COUNT(DISTINCT x)` | `count(x)` \u2014 `count(distinct x)` is a deprecated error |\n| `SUM(x)` | `x.sum()` |\n| `SUM(x) OVER ()` | `all(x)` |\n| `COUNT(*) FILTER (WHERE c)` / `CASE WHEN` | `count() { where: c }` |\n| `SELECT \u2026 GROUP BY \u2026` | `group_by:` + `aggregate:` (one stage is reduction OR `select:`, never both) |\n",
|
|
213
214
|
"explore/restricted-queries.md": "# Restricted Query Explanation\n\nThe `query` tool runs your Malloy against a **published model**. You have that\nmodel's entire published surface to work with \u2014 and you can build on it. The\nmodel is an inentionally curated subset of the data available in the\ndatabase.\n\n## You can\n\n- Use everything the model defines: its **sources, dimensions, measures, views,\n joins, and named queries**. `describe_source` shows exactly what's there.\n- **Run a named query and refine it** \u2014\n `run: top_carriers + { where: dep_year = 2024 }`.\n- **Define your own** dimensions, measures, and **your own sources and joins** \u2014\n as long as they are *derived from the model's sources*. You are not limited to\n the author's fields; compose new ones from them.\n- Reference the model's `$NAME` givens and supply values via the `givens` map on\n the `query` call (use `execute: false` to discover which a query needs).\n- Use a model field that was itself defined with raw SQL \u2014 the author vouched\n for the model's own definitions.\n\n## What is \"Restricted\"\n\nIf you see `restricted-construct-forbidden`, the query used something that\nreaches *outside* the published model: pulling in another file (`import`),\nopening a raw connection (`connection.table(...)` / `connection.sql(...)`),\nwriting raw SQL (`name!type(...)` or the `sql_*` functions), declaring new\n`given:`s, or setting `##!` compiler flags.\n\nThe fix is never to work around it \u2014 express the answer in terms of what the\nmodel publishes (define derived sources, joins, dimensions, and measures from\nthe model's sources). If something fundamental is missing, that's feedback for\nthe model's author.\n",
|
|
@@ -2880,7 +2881,7 @@ function clearCreds(url6) {
|
|
|
2880
2881
|
}
|
|
2881
2882
|
|
|
2882
2883
|
// package.json
|
|
2883
|
-
var version = "0.2.
|
|
2884
|
+
var version = "0.2.40";
|
|
2884
2885
|
|
|
2885
2886
|
// src/http.ts
|
|
2886
2887
|
var USER_AGENT = `malloyyo/${version}`;
|
|
@@ -2927,15 +2928,34 @@ async function registerClient(registrationEndpoint, redirectUri) {
|
|
|
2927
2928
|
if (!res.ok) throw new Error(`client registration failed: ${res.status} ${await res.text()}`);
|
|
2928
2929
|
return (await res.json()).client_id;
|
|
2929
2930
|
}
|
|
2931
|
+
function browserless(platform = process.platform, env = process.env) {
|
|
2932
|
+
if (platform === "darwin" || platform === "win32") return false;
|
|
2933
|
+
return !env.DISPLAY && !env.WAYLAND_DISPLAY;
|
|
2934
|
+
}
|
|
2930
2935
|
function openBrowser(url6) {
|
|
2931
2936
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url6]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url6]] : ["xdg-open", [url6]];
|
|
2932
2937
|
try {
|
|
2933
|
-
spawn(cmd, args, { stdio: "ignore", detached: true })
|
|
2938
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
2939
|
+
child.on("error", () => {
|
|
2940
|
+
});
|
|
2941
|
+
child.unref();
|
|
2934
2942
|
} catch {
|
|
2935
2943
|
}
|
|
2936
2944
|
}
|
|
2945
|
+
function listenTarget(env = process.env) {
|
|
2946
|
+
const raw = env.MALLOYYO_OAUTH_PORT;
|
|
2947
|
+
let port = 0;
|
|
2948
|
+
if (raw !== void 0 && raw !== "") {
|
|
2949
|
+
port = Number(raw);
|
|
2950
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
2951
|
+
throw new Error(`MALLOYYO_OAUTH_PORT must be a port number between 1 and 65535, got "${raw}"`);
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
return { host: env.MALLOYYO_OAUTH_HOST || "127.0.0.1", port };
|
|
2955
|
+
}
|
|
2937
2956
|
function awaitRedirect(state) {
|
|
2938
|
-
return new Promise((resolveServer) => {
|
|
2957
|
+
return new Promise((resolveServer, rejectServer) => {
|
|
2958
|
+
const { host, port: wanted } = listenTarget();
|
|
2939
2959
|
let resolveCode;
|
|
2940
2960
|
let rejectCode;
|
|
2941
2961
|
const code = new Promise((res, rej) => {
|
|
@@ -2960,13 +2980,33 @@ function awaitRedirect(state) {
|
|
|
2960
2980
|
if (ok) resolveCode(got);
|
|
2961
2981
|
else rejectCode(new Error(err ?? "state mismatch or missing code"));
|
|
2962
2982
|
});
|
|
2963
|
-
|
|
2983
|
+
let listening = false;
|
|
2984
|
+
server.on("error", (err) => {
|
|
2985
|
+
clearTimeout(timer);
|
|
2986
|
+
const detail = err.code === "EADDRINUSE" ? `${host}:${wanted} is already in use \u2014 set MALLOYYO_OAUTH_PORT to a free port` : err.message;
|
|
2987
|
+
const failure = new Error(`could not start the sign-in listener: ${detail}`);
|
|
2988
|
+
rejectCode(failure);
|
|
2989
|
+
if (!listening) {
|
|
2990
|
+
void code.catch(() => {
|
|
2991
|
+
});
|
|
2992
|
+
rejectServer(failure);
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
server.listen(wanted, host, () => {
|
|
2996
|
+
listening = true;
|
|
2964
2997
|
const port = server.address().port;
|
|
2965
|
-
resolveServer({
|
|
2998
|
+
resolveServer({
|
|
2999
|
+
port,
|
|
3000
|
+
code,
|
|
3001
|
+
close: () => {
|
|
3002
|
+
clearTimeout(timer);
|
|
3003
|
+
server.close();
|
|
3004
|
+
}
|
|
3005
|
+
});
|
|
2966
3006
|
});
|
|
2967
3007
|
});
|
|
2968
3008
|
}
|
|
2969
|
-
async function login(baseUrl) {
|
|
3009
|
+
async function login(baseUrl, opts = {}) {
|
|
2970
3010
|
const ep = await discover(baseUrl);
|
|
2971
3011
|
const { verifier, challenge } = pkce();
|
|
2972
3012
|
const state = crypto.randomBytes(16).toString("base64url");
|
|
@@ -2984,11 +3024,24 @@ async function login(baseUrl) {
|
|
|
2984
3024
|
scope: "mcp",
|
|
2985
3025
|
state
|
|
2986
3026
|
}).toString();
|
|
2987
|
-
|
|
2988
|
-
|
|
3027
|
+
if (opts.noBrowser || browserless()) {
|
|
3028
|
+
console.log(`Visit this URL to sign in:
|
|
3029
|
+
|
|
2989
3030
|
${authUrl.toString()}
|
|
2990
3031
|
`);
|
|
2991
|
-
|
|
3032
|
+
if (!process.env.MALLOYYO_OAUTH_PORT) {
|
|
3033
|
+
console.log(
|
|
3034
|
+
"Note: sign-in redirects back to this machine on a random port.\n In a container, set MALLOYYO_OAUTH_PORT and MALLOYYO_OAUTH_HOST=0.0.0.0,\n and publish that port, so the browser can reach the redirect.\n"
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
console.log("Waiting for sign-in to complete\u2026");
|
|
3038
|
+
} else {
|
|
3039
|
+
console.log("Opening your browser to sign in\u2026");
|
|
3040
|
+
console.log(`If it doesn't open, visit:
|
|
3041
|
+
${authUrl.toString()}
|
|
3042
|
+
`);
|
|
3043
|
+
openBrowser(authUrl.toString());
|
|
3044
|
+
}
|
|
2992
3045
|
const authCode = await code;
|
|
2993
3046
|
const res = await apiFetch(ep.token_endpoint, {
|
|
2994
3047
|
method: "POST",
|
|
@@ -5388,9 +5441,9 @@ async function status(target, opts) {
|
|
|
5388
5441
|
console.log(` version ${s.version ?? "?"}` + (git?.sha ? ` ${git.branch}@${shortSha(git.sha)}` : ""));
|
|
5389
5442
|
console.log(` ${s.compileError ? `\u2717 ${s.compileError}` : `\u2713 compiled ${s.compiledAt ?? ""}`}`);
|
|
5390
5443
|
}
|
|
5391
|
-
async function loginCmd(target) {
|
|
5444
|
+
async function loginCmd(target, opts) {
|
|
5392
5445
|
const inst = resolveInstance(resolve3("."), target);
|
|
5393
|
-
await login(inst.url);
|
|
5446
|
+
await login(inst.url, { noBrowser: opts.browser === false });
|
|
5394
5447
|
console.log(`\u2713 logged in to ${inst.name} (${inst.url})`);
|
|
5395
5448
|
}
|
|
5396
5449
|
async function logoutCmd(target) {
|
|
@@ -5399,7 +5452,7 @@ async function logoutCmd(target) {
|
|
|
5399
5452
|
}
|
|
5400
5453
|
var program = new Command();
|
|
5401
5454
|
program.name("malloyyo").description("Publish Malloy models to a Malloyyo instance").version(version);
|
|
5402
|
-
program.command("login").argument("[target]", "target name or instance URL (optional if the config has one target)").description("sign in to an instance in your browser (stores a token)").action(loginCmd);
|
|
5455
|
+
program.command("login").argument("[target]", "target name or instance URL (optional if the config has one target)").option("--no-browser", "print the sign-in URL instead of launching a browser").description("sign in to an instance in your browser (stores a token)").action(loginCmd);
|
|
5403
5456
|
program.command("logout").argument("[target]", "target name or instance URL (optional if the config has one target)").description("forget the stored token for an instance").action(logoutCmd);
|
|
5404
5457
|
program.command("publish").argument(
|
|
5405
5458
|
"[target]",
|