@malloydata/cli 0.0.54 → 0.0.55

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 CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- process.MALLOY_CLI_VERSION="0.0.54";
2
+ process.MALLOY_CLI_VERSION="0.0.55";
3
3
  require('./cli.js')
@@ -11,12 +11,13 @@ A core design principle is that **most queries are themselves designing a new se
11
11
 
12
12
  ## Documents and Statements
13
13
 
14
- A Malloy file (`.malloy`) is a sequence of statements, optionally separated by semicolons. There are four statement types:
14
+ A Malloy file (`.malloy`) is a sequence of statements, optionally separated by semicolons. There are five statement types:
15
15
 
16
16
  - **`import`** — import sources and queries from another `.malloy` file
17
17
  - **`source:`** — define a named, reusable data source with its schema and extensions
18
18
  - **`query:`** — define a named query (source + view) for reuse
19
19
  - **`run:`** — execute a query (the "do it now" statement)
20
+ - **`given:`** — declare model-level parameters supplied at run time (experimental, see Givens)
20
21
 
21
22
  ```malloy
22
23
  import "shared_model.malloy"
@@ -96,6 +97,8 @@ join_cross: other_table on other_table.key = key -- cross join
96
97
 
97
98
  The `with` shorthand requires the joined source to have a declared `primary_key`. All joins are left outer by default. There is no right join — Malloy's graph model doesn't need one.
98
99
 
100
+ **Choosing `join_one` vs `join_many`:** Ask "for a single row in the base source, can the joined source match more than one row?" If yes → `join_many`. If no (or at most one) → `join_one`. The common mistake is reaching for `join_many` when joining a *lookup or summary table* (e.g., joining an inventory snapshot to a purchase history on a wine key). Even though the joined table may have many rows overall, if each base row resolves to *at most one* joined row, use `join_one`. Use `join_many` only when the join genuinely fans out the base rows — e.g., joining line items to orders, or notes to a wine catalog.
101
+
99
102
  When you reference a joined source's fields, you use dot notation: `carriers.nickname`, `origin_airport.state`. This is one of Malloy's most important abstractions: **the access path to nested data is identical regardless of how the nesting is physically stored.** An array of records embedded in a column, a `join_many` to a separate table, a record-typed column — all are navigated with the same dot notation. The SQL required to traverse these different physical arrangements varies wildly (unnesting arrays, LEFT JOINs, correlated subqueries, ARRAY_AGG), but Malloy hides all of that. You think about the logical shape of your data — "flights have carriers, carriers have a nickname" — and write `carriers.nickname`. The compiler figures out what SQL is needed to get there. This means you can restructure your physical schema (normalize a nested array into a separate table, or denormalize a joined table into a record column) without changing any of the Malloy that references that data.
100
103
 
101
104
  ## Fields
@@ -417,7 +420,7 @@ Not all annotations are tags. An annotation is just text. Tags are annotations t
417
420
  The character(s) immediately after `#` route the annotation to different consumers:
418
421
 
419
422
  - `# ` (hash-space) — renderer tags, parsed by the Malloy VS Code extension for visualization
420
- - `##!` — compiler directives (e.g., `##! experimental.parameters`)
423
+ - `##!` — compiler directives (e.g., `##! experimental.parameters`, `##! experimental.givens`)
421
424
  - `#"` — reserved for documentation strings
422
425
  - `#(appName)` — application-specific tags (e.g., `#(docs) hidden`)
423
426
 
@@ -441,6 +444,192 @@ tName.sub.path=value -- deep path assignment
441
444
 
442
445
  Values can be unquoted identifiers, quoted strings, numbers, or typed values prefixed with `@` (`@true`, `@false`, `@2024-01-15`).
443
446
 
447
+ ## Givens (Model-Level Parameters)
448
+
449
+ **Status: experimental, gated by `##! experimental.givens`.** Naming is provisional.
450
+
451
+ Givens are values supplied at run time that the model can reference in any expression. The motivating use case is row-level access control — a model written once with `where: x.tenant_id = $TENANT` and the tenant supplied per API call — but they also fit configuration values, session context, and any "one compiled model, many invocations with varying context" pattern.
452
+
453
+ Givens are model-wide: a single namespace, one value per name per compilation. They are *complementary to* source/query parameters (`source: foo(x :: string) is ...`), not a replacement. Use a parameter when you want two differently-bound copies of the same source side-by-side in one model; use a given when you want one value visible everywhere in the compilation.
454
+
455
+ ### Declaration
456
+
457
+ The `given:` top-level statement introduces givens, with a name, a type, and an optional default:
458
+
459
+ ```malloy
460
+ given:
461
+ TENANT :: string
462
+ MAX_ROWS :: number is 1000
463
+ CUTOFF_DATE :: date is @2024-01-01
464
+ ```
465
+
466
+ Type can be any Malloy atomic type or compound type, including `filter<T>`:
467
+
468
+ ```malloy
469
+ given:
470
+ ROLE :: string
471
+ ALLOWED_ROLES :: string[]
472
+ SESSION :: { user_id :: string, tenant :: string }
473
+ TENANT_FILTER :: filter<string>
474
+ ```
475
+
476
+ Defaults are expressions over constants and other givens. Annotations attach to given declarations the same way they attach to sources or measures.
477
+
478
+ ### Reference: the `$` sigil
479
+
480
+ Inside any expression, a given is referenced with a leading `$`:
481
+
482
+ ```malloy
483
+ source: orders_for_user is orders extend {
484
+ where: orders.tenant_id = $TENANT
485
+ }
486
+
487
+ query: recent_orders is orders_for_user -> {
488
+ where: order_date >= $CUTOFF_DATE
489
+ limit: $MAX_ROWS
490
+ }
491
+ ```
492
+
493
+ `$` appears *only* at expression references. The other three sites where a given's name appears — declaration, import, and supply (caller side) — use the bare name, because syntactic position already disambiguates. Givens share the top-level declaration namespace with sources/queries/views, so `source: x is ...` plus `given: x :: string` is a name-conflict error.
494
+
495
+ ### Set membership: `expr in $arrayGiven`
496
+
497
+ The RHS of `in` is either a parenthesized list of expressions (`in (1, 2, x, y * 7)`, same as SQL) or a given with an array value (`in $ARR`). A bare array-typed expression — a dimension, a joined array field, an inline `[a, b, c]` literal — is *not* legal on the RHS; arrays only reach the RHS via the given form.
498
+
499
+ When a given has array type, `expr in $ARR` tests `expr` against the runtime-bound array; `not in $ARR` is the negation. The left-hand side must match the array's element type (`string in $string[]`, `number in $number[]`, etc.); mismatches are translate-time errors. Records and nested arrays are out of scope.
500
+
501
+ ```malloy
502
+ given:
503
+ ALLOWED_STATES :: string[]
504
+ URGENT_STATUSES :: string[]
505
+
506
+ source: orders extend {
507
+ where: state in $ALLOWED_STATES
508
+ dimension: is_urgent is order_status in $URGENT_STATUSES
509
+ }
510
+ ```
511
+
512
+ At SQL emit, the array's contents land in a generated `IN (...)` clause. Empty or `null` arrays collapse to the obvious result (`IN` → `FALSE`, `NOT IN` → `TRUE`). NULL elements inside a non-empty array follow standard SQL `IN` semantics.
513
+
514
+ To derive a value from an array — typically a boolean gate — *without* the array itself reaching row-position SQL, use an inline given (below).
515
+
516
+ ### Inline givens
517
+
518
+ An `inline` given is evaluated at **bind time**, before SQL is emitted: its default expression runs against the resolved given values and reduces to a literal, and that literal is what reaches SQL.
519
+
520
+ ```malloy
521
+ given:
522
+ CAPABILITIES :: string[]
523
+ inline CAN_READ_ORDERS :: boolean is 'read_orders' in $CAPABILITIES
524
+ inline CAN_MUTATE :: boolean
525
+ is 'write_orders' in $CAPABILITIES or 'admin' in $CAPABILITIES
526
+
527
+ source: orders extend {
528
+ where: $CAN_READ_ORDERS -- SQL sees: WHERE ... AND TRUE (or FALSE)
529
+ }
530
+ ```
531
+
532
+ This is the **row-level access-control gate** pattern: the host supplies a capability list as a regular given, an inline given derives a boolean from it, and only the boolean — not the list — crosses into row-position SQL. The query planner sees a constant predicate.
533
+
534
+ Rules:
535
+
536
+ - An inline given **must** have a default. `inline FOO :: number` with no `is` clause is a translate-time error.
537
+ - The default may use:
538
+ - Boolean and comparison operators: `and`, `or`, `not`, `=`, `!=`, `<`, `<=`, `>`, `>=`
539
+ - The `in $array` test against another given
540
+ - Literals (string, number, boolean, null, array) and references to other givens
541
+ - The default cannot call SQL functions, reference fields, or use any operator outside that list. Disallowed operators are reported at translate time with the offending operator names.
542
+ - Inline givens are filtered out of `Model.givens` and `PreparedQuery.givens` — they're computed, not supplied — so introspection-driven UIs don't render editors for them. A caller can still shadow one by binding it explicitly (useful in tests).
543
+ - `inline` is a context-sensitive modifier, not a reserved keyword: fields, sources, views, dimensions, and joins can still be named `inline`.
544
+
545
+ ### Imports
546
+
547
+ Givens behave like every other top-level named thing under import:
548
+
549
+ - **Bare import** (`import "b.malloy"`) brings B's full export surface in, including all of B's givens, under their original names.
550
+ - **Selective import** (`import { source1 } from "b.malloy"`) brings in only what's listed. To surface a given to your callers, list it: `import { source1, MAX_ROWS } from "b.malloy"`.
551
+ - **Rename** uses the existing `LOCAL is REMOTE` form: `import { CAP is MAX_ROWS } from "b.malloy"`.
552
+
553
+ Surfacing controls *who can supply a value*, not whether internal references work. An imported source can reference a given the importer didn't surface; the reference still resolves internally, and at run time the unsurfaced given relies on its declaration-site default.
554
+
555
+ A common project convention is a shared `tenant_givens.malloy` (declaring `$TENANT`, `$USER_ROLE`, etc.) that every root file bare-imports on line 1, so the project's given contract is visible at the top of any model.
556
+
557
+ ### Satisfiability
558
+
559
+ A query referencing `$X` is satisfiable if either `$X` is in the model's namespace (so a caller can supply a value) or `$X` has a default at its declaration site. Otherwise the query is unsatisfiable and errors. Latent definitions (views, dimensions, measures) that reference `$X` are fine if no query actually invokes them — satisfiability is a property of running queries.
560
+
561
+ ### Supplying values
562
+
563
+ Values can be supplied at two layers, which compose (per-query overrides per-runtime):
564
+
565
+ **Per-runtime** — bound to a `Runtime`, applied as defaults to every query through it. Two paths:
566
+
567
+ 1. **`givensPath` in `malloy-config.json`** points at a JSON file of `name → value`:
568
+ ```jsonc
569
+ { "givensPath": "./local-givens.json" }
570
+ // or env-var indirection (resolved at config load):
571
+ { "givensPath": { "env": "GAME_STORE_GIVENS" } }
572
+ ```
573
+ The values file is a flat JSON map, keys are caller-facing surface names:
574
+ ```jsonc
575
+ { "TENANT": "acme", "USER_ROLE": "admin", "CUTOFF_DATE": "2024-01-01" }
576
+ ```
577
+
578
+ 2. **Direct on the Runtime constructor** (for per-request multi-tenant servers, tests, scripts):
579
+ ```typescript
580
+ const runtime = new Runtime({
581
+ config,
582
+ givens: { TENANT: claims.tenant_id, USER_ROLE: claims.role },
583
+ urlReader,
584
+ });
585
+ ```
586
+ Constructor values *merge over* the file at `givensPath` per-key.
587
+
588
+ **Per-query** — supplied on a single `.run({ givens: ... })` call:
589
+ ```typescript
590
+ await query.run({ givens: { STATE_FILTER: "CA", LIMIT_OVERRIDE: 50 } })
591
+ ```
592
+ Available on every compile-or-run entry point (`runtime.loadQuery(...).run(options)`, `preparedQuery.getPreparedResult(options)`, `preparedQuery.getSQL(options)`).
593
+
594
+ The resolved per-runtime values are exposed on `runtime.givens` (read-only) for diagnostics.
595
+
596
+ ### Finalized givens (security primitive)
597
+
598
+ A multi-tenant deployment usually wants `TENANT`/`USER_ROLE`/`REGION` to be runtime-bound and **un-overridable per-query** — otherwise a downstream endpoint that accidentally accepts user-controlled query params and plumbs them into `.run({ givens: ... })` becomes a tenant-leak vulnerability.
599
+
600
+ `finalizeGivens` in the config locks names at the API surface:
601
+
602
+ ```jsonc
603
+ {
604
+ "givensPath": { "env": "GAME_STORE_GIVENS" },
605
+ "finalizeGivens": ["TENANT", "USER_ROLE", "REGION"]
606
+ }
607
+ ```
608
+
609
+ Finalize doesn't change *what* a given resolves to — only *who* can supply it. A `.run({ givens: { TENANT: ... } })` for a finalized name throws at API entry (named, not silently dropped). Finalized givens are filtered out of `Model.givens` and `PreparedQuery.givens` so introspection-driven UIs don't render editors for locked names.
610
+
611
+ ### JS shapes for supplied values
612
+
613
+ Both the JSON values file and per-query `givens` maps accept the same per-type shapes:
614
+
615
+ | Malloy type | JS |
616
+ |---|---|
617
+ | `string` | string |
618
+ | `number` | `number`, `bigint`, or string (precision escape hatch) |
619
+ | `boolean` | boolean |
620
+ | `date` | ISO date string `"2024-01-15"` |
621
+ | `timestamp` (naive) | ISO string without offset — *not* a JS `Date` |
622
+ | `timestamptz` | JS `Date` or ISO string with offset (string preferred — makes TZ choice visible) |
623
+ | `T[]` | JS array |
624
+ | `{ name :: T, ... }` | JS object |
625
+ | `filter<T>` | JS string (Malloy filter expression source) |
626
+
627
+ Naive timestamp givens reject `Date` because `Date` represents a UTC instant, not a wall-clock value, and `new Date("2001-01-01T00:00:00")` silently picks up the system's local TZ. Type mismatches throw at the boundary with a path that points at the offending location (e.g., `givens.SESSION.user_id: expected string, got number`). `null` is legal for any given type.
628
+
629
+ ### Introspection
630
+
631
+ `Model.givens` and `PreparedQuery.givens` expose, to the host, the supplyable givens — for whole-model parameter editors and per-query "run this" forms respectively. Each entry carries name, type, default expression (or undefined if the caller must supply), location, and access to declaration-site annotations via `tagParse`/`getTaglines`.
632
+
444
633
  ## How a Malloy Query Becomes SQL
445
634
 
446
635
  The compilation pipeline has two phases:
@@ -11,8 +11,8 @@ This CLI exposes five tools. "Compile" means parse + typecheck + build the model
11
11
  |-----------------|----------------------------------------|--------------------------------------------------------------------------------|
12
12
  | `compile_file` | `{ uri, expand? }` | Fetch+compile a .malloy file; returns structured model with problems[]. |
13
13
  | `compile` | `{ source, base_uri?, expand? }` | Same, on an inline source string. |
14
- | `run_file` | `{ uri, name?, index? }` | Execute one run: from a file. `name` wins over `index`; else last run:. |
15
- | `run` | `{ source, base_uri? }` | Execute the final run: from an inline source. |
14
+ | `run_file` | `{ uri, name?, index?, givens? }` | Execute one run: from a file. `name` wins over `index`; else last run:. |
15
+ | `run` | `{ source, base_uri?, givens? }` | Execute the final run: from an inline source. |
16
16
  | `list_runs` | `{ uri }` | Cheap discovery: runnable run: + named queries, no model serialization. |
17
17
 
18
18
  Every tool returns a uniform `problems[]` with `{ severity, message, code, uri?, line, column, endLine?, endColumn? }`. Compile errors, import-resolution errors, and runtime SQL errors all use this shape.
@@ -79,6 +79,25 @@ Feed that whole thing to `compile`. Imports resolve against `base_uri` if suppli
79
79
  - **Mixed reduction/projection** — a single stage uses either `group_by:`/`aggregate:` OR `select:`, not both.
80
80
  - **Calculation in source** — `calculate:` (window functions) only lives in queries, never in source definitions.
81
81
 
82
+ ## Running a query that declares givens
83
+
84
+ Givens are model-level parameters (`given: TENANT :: string`, referenced as `$TENANT`). When a model declares them:
85
+
86
+ 1. `compile_file` (or `list_runs`) reports `model.givens[]` (full list with type/default) and per-run/per-query `givens: ["NAME", ...]` arrays — the names that run needs supplied.
87
+ 2. Pass values to `run`/`run_file` via the `givens` map, keyed by surface name (no `$`):
88
+
89
+ ```jsonc
90
+ { "uri": "file:///.../model.malloy", "givens": { "TENANT": "acme", "MAX_ROWS": 100 } }
91
+ ```
92
+
93
+ 3. **For per-type JS shapes** (date as ISO string, naive timestamp as ISO without offset, record as JS object, `filter<T>` as a Malloy filter source string, etc.) call `language_help("givens")` — the "JS shapes for supplied values" subsection is the canonical reference. Don't guess; the compiler validates and rejects bad shapes with a path to the offending field.
94
+
95
+ A given with a default is optional in the `givens` map. A given without a default must be supplied or the run fails with a missing-given error.
96
+
97
+ **Inline givens** (declared `inline NAME :: T is …`) are evaluated at bind time and don't take supplied values, so they are filtered out of `model.givens[]` and the per-run `givens[]` lists. If you see `$X` referenced in source but missing from introspection, it's an inline.
98
+
99
+ **Array givens with `in`.** The RHS of `in` is either a parenthesized list of expressions (`in (1, 2, x, y * 7)`, as in SQL) or a given with an array value (`in $ARR`). Use the given form (`where: state in $ALLOWED_STATES`) when the values come from outside the model — bare array expressions (a dimension, joined field, `[a, b, c]` literal) are not legal on the RHS.
100
+
82
101
  ## Before writing non-trivial Malloy
83
102
 
84
103
  Pull the `malloy-language-reference` prompt first. The language has real scoping and typing rules that the compiler will enforce; don't guess.
@@ -34346,7 +34346,7 @@ SOFTWARE.
34346
34346
  -------
34347
34347
  Package: @databricks/sql
34348
34348
  Url: https://github.com/databricks/databricks-sql-nodejs#readme
34349
- License(s): Apache 2.0
34349
+ License(s): Apache-2.0
34350
34350
  License Text:
34351
34351
  Apache License
34352
34352
  Version 2.0, January 2004
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/cli",
3
- "version": "0.0.54",
3
+ "version": "0.0.55",
4
4
  "description": "Malloy CLI",
5
5
  "bin": {
6
6
  "malloy-cli": "dist/index.js",
@@ -72,14 +72,14 @@
72
72
  "@commander-js/extra-typings": "^12.1.0",
73
73
  "@duckdb/node-api": "1.4.4-r.1",
74
74
  "@duckdb/node-bindings": "1.4.4-r.1",
75
- "@malloydata/db-bigquery": "0.0.387",
76
- "@malloydata/db-duckdb": "0.0.387",
77
- "@malloydata/db-postgres": "0.0.387",
78
- "@malloydata/db-snowflake": "0.0.387",
79
- "@malloydata/db-trino": "0.0.387",
80
- "@malloydata/malloy": "0.0.387",
81
- "@malloydata/malloy-connections": "0.0.387",
82
- "@malloydata/malloy-sql": "0.0.387",
75
+ "@malloydata/db-bigquery": "0.0.402",
76
+ "@malloydata/db-duckdb": "0.0.402",
77
+ "@malloydata/db-postgres": "0.0.402",
78
+ "@malloydata/db-snowflake": "0.0.402",
79
+ "@malloydata/db-trino": "0.0.402",
80
+ "@malloydata/malloy": "0.0.402",
81
+ "@malloydata/malloy-connections": "0.0.402",
82
+ "@malloydata/malloy-sql": "0.0.402",
83
83
  "@types/jest": "^29.2.5",
84
84
  "@types/node": "^14.11.2",
85
85
  "@typescript-eslint/eslint-plugin": "^5.48.2",