@trilogy-data/trilogy-studio-components 0.1.27 → 0.1.29

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.
Files changed (73) hide show
  1. package/dist/{AssetAutoImporter-OHphsfW9.js → AssetAutoImporter-CCbTLQ9v.js} +5 -5
  2. package/dist/{ChatCreatorModal-CuXha6Dg.js → ChatCreatorModal-C0OHgrEB.js} +1 -1
  3. package/dist/{CodeBlock-BBsaC6hX.js → CodeBlock-CS86Ipg1.js} +1 -1
  4. package/dist/{CommunityModels-CIQQhO-y.js → CommunityModels-BxJKe8_v.js} +4 -4
  5. package/dist/{ConnectionView-7Jj_Ggdi.js → ConnectionView-D-1gQNQ7.js} +4 -4
  6. package/dist/{CreateEditorFromDatasourcePopup-CrVHUWFl.js → CreateEditorFromDatasourcePopup-RkNgUdzU.js} +2 -2
  7. package/dist/{Dashboard-DxSi9W6Q.js → Dashboard-DWJPKIeQ.js} +5 -5
  8. package/dist/{DashboardImportSelector-kuDRDYi8.js → DashboardImportSelector-t6tKq5N4.js} +1 -1
  9. package/dist/{DashboardMobile-CZEgTb0R.js → DashboardMobile-CxObMuv6.js} +4 -4
  10. package/dist/{Editor-D6tdutTm.js → Editor-DD1inQuU.js} +6 -6
  11. package/dist/{GlobalChatPanel-BXZh0sdD.js → GlobalChatPanel-BArWsM8Y.js} +20 -20
  12. package/dist/{IDE-T8PmyAQ_.js → IDE-D5GKLVYn.js} +6 -6
  13. package/dist/{JobsView-Ca97MiSd.js → JobsView-By7OZgj_.js} +5 -5
  14. package/dist/{LLMChat-Cch33Yd_.js → LLMChat-B-q_7sa4.js} +2 -2
  15. package/dist/{LLMView-BDb6wD74.js → LLMView-8BKf70Ay.js} +9 -9
  16. package/dist/{LoadingView-B-xFwd2N.js → LoadingView-Da0HfQ56.js} +1 -1
  17. package/dist/{MarkdownRenderer-BWvdx0k3.js → MarkdownRenderer-CQppLv_n.js} +1 -1
  18. package/dist/{MobileIDE-DpdEQEqc.js → MobileIDE-C81gvbco.js} +12 -12
  19. package/dist/{ModelCard-DkAMP4X9.js → ModelCard-BMDgFDyQ.js} +4 -4
  20. package/dist/{ModelView-DomyhN8g.js → ModelView-C4B8qJwX.js} +4 -4
  21. package/dist/{PageLoading-BAvKGo9h.js → PageLoading-CRl9HNIg.js} +75 -75
  22. package/dist/{ResultComponent-zoOSoRLe.js → ResultComponent-C-A8sbr1.js} +4 -4
  23. package/dist/{Results-dq-SlJbU.js → Results-C1nNHU7C.js} +11 -11
  24. package/dist/{StatusIcon-AbxNvJ6p.js → StatusIcon-v2EhaU49.js} +1 -1
  25. package/dist/{StoreTokenModal-CM8WXdu1.js → StoreTokenModal-BV_gLE94.js} +1 -1
  26. package/dist/{SymbolsPane-DfaxeaPS.js → SymbolsPane-CuVCamJf.js} +1 -1
  27. package/dist/{TutorialPage-BtEJG7cT.js → TutorialPage-DGQGx3vF.js} +11 -11
  28. package/dist/{VegaLiteChart-BVyAab-o.js → VegaLiteChart-BIeSuNU8.js} +11413 -11363
  29. package/dist/{WelcomePage-BoqzYBLr.js → WelcomePage-T757Ynnn.js} +3 -3
  30. package/dist/components/VegaLiteChart.vue.d.ts.map +1 -1
  31. package/dist/components/chartControlsManager.d.ts +0 -1
  32. package/dist/components/chartControlsManager.d.ts.map +1 -1
  33. package/dist/dashboard.js +19 -19
  34. package/dist/dashboards/chartTheme.d.ts +68 -0
  35. package/dist/dashboards/chartTheme.d.ts.map +1 -0
  36. package/dist/dashboards/dashboardQueryExecutor.d.ts +9 -0
  37. package/dist/dashboards/dashboardQueryExecutor.d.ts.map +1 -1
  38. package/dist/dashboards/donutSpec.d.ts +2 -1
  39. package/dist/dashboards/donutSpec.d.ts.map +1 -1
  40. package/dist/dashboards/headlineSpec.d.ts +2 -1
  41. package/dist/dashboards/headlineSpec.d.ts.map +1 -1
  42. package/dist/dashboards/helpers.d.ts +23 -0
  43. package/dist/dashboards/helpers.d.ts.map +1 -1
  44. package/dist/dashboards/index.d.ts +2 -0
  45. package/dist/dashboards/index.d.ts.map +1 -1
  46. package/dist/dashboards/layerSpec.d.ts +4 -1
  47. package/dist/dashboards/layerSpec.d.ts.map +1 -1
  48. package/dist/dashboards/pointSpec.d.ts +2 -1
  49. package/dist/dashboards/pointSpec.d.ts.map +1 -1
  50. package/dist/dashboards/spec.d.ts +8 -1
  51. package/dist/dashboards/spec.d.ts.map +1 -1
  52. package/dist/{editorStore-CAlMJftt.js → editorStore-k4IbmW9-.js} +1094 -1094
  53. package/dist/{globalChatRuntime-CyS7vH51.js → globalChatRuntime-BWahbZxZ.js} +14 -14
  54. package/dist/{helpers-BbVlA3Pf.js → helpers-DDKx6kkf.js} +4 -4
  55. package/dist/llm/data/prompts.d.ts +1 -1
  56. package/dist/llm/data/prompts.d.ts.map +1 -1
  57. package/dist/llm/data/trilogySyntax.generated.d.ts +3 -3
  58. package/dist/llm/data/trilogySyntax.generated.d.ts.map +1 -1
  59. package/dist/{llm-ClU-2rgk.js → llm--BDQ6qkZ.js} +111 -77
  60. package/dist/llm.js +2 -2
  61. package/dist/{localStorage-CGQ1iJXt.js → localStorage-BYwXjN4M.js} +16 -16
  62. package/dist/monaco.js +2 -2
  63. package/dist/{reconcile-DXIvGwBk.js → reconcile-D-vMtlgJ.js} +1 -1
  64. package/dist/{remoteStoreSync-CuQBWpWf.js → remoteStoreSync-PLFQCOmY.js} +2 -2
  65. package/dist/{stores-LlyiwSbK.js → stores-S6dQ5cjI.js} +40 -32
  66. package/dist/stores.js +3 -3
  67. package/dist/trilogy-studio-components.css +1 -1
  68. package/dist/{useChatWithTools-m3Jn_SV-.js → useChatWithTools-Bkd9MeMh.js} +10 -10
  69. package/dist/{useDashboard-BOAuvVEw.js → useDashboard-50rwbG0x.js} +889 -889
  70. package/dist/{useGlobalChatPanel-DuP1q7_f.js → useGlobalChatPanel-JJZaHao8.js} +4 -4
  71. package/dist/views.js +11 -11
  72. package/dist/{vocabulary-CKLGILtR.js → vocabulary-DQK5jB7m.js} +3 -1
  73. package/package.json +1 -1
@@ -15448,7 +15448,7 @@ var jg = class e extends nt {
15448
15448
  let t = [...e].sort(Ag);
15449
15449
  return t.find((e) => kg(e)?.tier === "flash") || t[0] || "";
15450
15450
  }
15451
- }, Mg = "# Trilogy Syntax\n\nTrilogy statements define a semantic model or a query; collections of statements\nare written as `.preql` files. A statement returns data (SELECT) or defines\nconcepts (key | property | auto | metric), datasources, or parameters.\n\nimport <model> as <alias>; makes a model's fields available. <model> is a DOT-SEPARATED MODULE PATH relative to the project root (where trilogy.toml lives), never a file path: the file `raw/store_sales.preql` is imported as `import raw.store_sales as ss;` — replace each `/` with `.`, drop the `.preql`, keep the folder segment. When an imported model imports others (a fact with imported dimensions), those are dot paths: `enroll.student.name`. Typical usage imports ONE fact and reaches dimensions through it; nested dimensions carry ALL values (order.customers includes customers without orders).\n\nkey | property | auto | metric; define new concepts. `auto x <- ...` is a DEFINITION, not a precomputed value: each reference expands and re-evaluates in the consuming query's scope.\n\nparameter NAME TYPE [default <literal>]; — a runtime value supplied via `--param NAME=VALUE` (repeatable); append `?` to the type to allow null.\n\n## Combining models\n\n| Goal | Use |\n|---|---|\n| Typical query | no merge, no join: access all fields through dot-paths |\n| Blend two models on shared keys inside one query | scoped `subset\\|union join` |\n| Make a connection universal to a whole file | `merge` |\n| Stack subsets/channels as rows | `union(...)` |\n| Rows in A but never in B (set difference) | `except(...)` |\n| Rows present in every source (set intersection) | `intersect(...)` |\n\n### Query-scoped join\n\nA typical fact already has its dimensions merged in, so no join is needed. Blending fact models or rowset outputs takes a scoped join, placed right after the select list, which DECLARES how the key domains relate: `subset join a = b` (a's values are contained in b's, a ⊆ b; b authoritative for the key) or `union join a = b` (neither contains the other; the key is the coalesce of both sides and unmatched rows from BOTH sides are kept).\n\nTwo rules decide whether the numbers are right, so apply them before reaching for the example:\n\n- Joins NEVER drop a row and NULL keys match null-safely; there is NO inner join. An intersection is an explicit `where <optional-side attr> is not null`.\n- JOIN ON THE FULL GRAIN: `explore` prints each fact's grain as @<k1, k2>; write one clause PER key. Matching only one key of a multi-key grain causes duplication.\n\nChaining, expression keys, union/subset mixing, multi-key and self-pair shapes: trilogy agent-info syntax example scoped-join.\n\nmerge <a> into ~<b>; is the persistent (whole-file) equivalent of `subset join a = b`; plain `merge a into b;` declares EXACT domain equivalence. Standalone statements; prefer a scoped join unless the connection is universal.\n\n### union / except / intersect (row set operations)\n\n`union(...)` row-stacks self-contained select arms positionally (SQL UNION ALL) into one named rowset (`with combined as union((armA), (armB), ...) -> (out1, out2, ...)`); `except(...)` / `intersect(...)` share that arm shape but are SQL SET operators (DISTINCT rows, null-safe whole-row compare, and except subtracts later arms from the FIRST, so arm order matters). Prefer except over a multi-column `not in` for \"in A but never in B\". Each arm dedups to its own output grain BEFORE stacking, so aggregate INSIDE each arm or the total silently undersums. Examples: trilogy agent-info syntax example union-stack-channels / except-intersect-setops.\n\n## Query structure\n\nTop-level statements first (each `;`-terminated), then a query with clauses in this EXACT order — all optional except select:\n\n```\n<top-level statements>; # import / key / auto / rowset / merge / def / parameter\nwith <name> as # optional: name the select as a reusable rowset (CTE)\nwhere <row condition> # 1. filters INPUT rows, BEFORE aggregation\nselect <col>, <agg> as name, # 2. projection — grouping is AUTOMATIC by the\n # non-aggregated columns; never write GROUP BY\n subset|union join a = b (= c)? # 3. blend models; one clause per key, right after the select list\nby rollup|cube|grouping sets (...) # 4. optional multi-level grouping for the whole select\nhaving <result condition> # 5. filters aggregated/joined RESULTS\norder by <col> asc|desc # 6. sort\nlimit <n>; # 7. cap rows\n```\n\nA rowset (`with <name> as where ... select ...;`) is a standalone statement, evaluated in isolation — it does NOT respond to the consuming query's context. All its outputs are namespaced: output `abc.def` of rowset `foo` is referenced as `foo.abc.def`, and joined back like any concept (`subset join foo.key = other.key`). Alias every reused expression with `as`.\n\nFull annotated example: trilogy agent-info syntax example query-structure.\n\n### Not SQL — what to never write\n\n- **No FROM, GROUP BY, DISTINCT, SELECT \\*, or SQL-style set operators between selects.** Stack rows with `union(...)`; set difference/intersection with `except(...)`/`intersect(...)`; blend fact models with a scoped join.\n- **Grouping is automatic** by the non-aggregated fields in the SELECT — never write GROUP BY. Aggregates inherit the select's grain wherever they appear (where/select/having). Override per-aggregate with `agg(x) by <dims>`; `by *` aggregates across all data (single row). To weight a related dimension property once per fact row, project it to the fact grain first: `auto row_birth_year <- group(enroll.student.birth_year) by enroll.student_id, enroll.course_id;` then aggregate that.\n- **Output rows are deduplicated to the select grain.** To preserve legitimate duplicates (one row per matching fact), include the fact's grain keys in the select — hidden with the PREFIX `--` if they shouldn't appear in the output.\n- **Never write `distinct`.** `count(<key>)` is already distinct because keys are unique; `count_distinct(<property>)` counts distinct values of a non-key property.\n- **Count a COMPOSITE grain with `grain(...)`.** `count(grain(order_id, item.id))` counts order+item combinations; `grain(...)` is NEVER NULL. Never count one column of a multi-key grain — a coarser key counts its own distinct values and undercounts. `count_distinct(a, b)` is accepted as sugar for `count(grain(a, b))` (distinct combinations, NULL members included).\n- **An aggregate over a group with no qualifying rows is NULL, never 0.** `sum(x ? cond) by k` yields NULL for groups where nothing matches, so `= 0` matches nothing. Test emptiness with `count(key ? cond) by k = 0` or wrap: `coalesce(sum(x ? cond), 0)`.\n- **`count(1)` is invalid** — a constant does not identify rows. Use the row key, `count(grain(...))`, or `sum(1) by <grain>`.\n- **One-column expression subqueries only.** `(select ...)` is allowed where a scalar or membership set is expected (exactly one projected column) — never as a SQL-style FROM-subquery. For related-entity filters prefer the dot-path: `where enrollments.student.state = 'TN'`, not a subselect.\n- **`--` is a HIDDEN-field prefix, not a comment** (it still changes query structure). Comments use `#` only.\n- **Aliases**: always use the full dot-path (`enroll.student.id`); alias every new expression with `as`; a select alias is usable in `having`/`order by` but NOT inside other select expressions or `where`, and must not rename a field back to an existing concept name.\n\n## Filtering\n\n`where` filters input rows before aggregation; `having` filters results after aggregation/windows. The inline filter `x ? cond` filters the immediate prior expression: `sum(credits ? enrolled = true)`.\n\nAggregates/windows in WHERE do not filter each other's inputs. Use inline filters, or a staged `then where` chain: each `then where` stage's aggregates compute over only the rows passing earlier stages (`where x = 5 then where sum(y) > 10` ≡ `where x = 5 and sum(y ? x = 5) > 10`). One rule: the SAME cross-row expression cannot gate two different stages — give one an inline filter.\n\nWindows need all rows in their range: filter AFTER the window (in `having`) rather than in `where` when the window must see the full set.\n\n## Membership (semijoins)\n\n`in` / `not in` cross models without any declared relationship — the semijoin IS a scoped intersection. The right side is a concept or expression (never a subselect); build computed sets with an inline filter: `auto big_zip <- student.zip ? (count(student.id ? student.honors = true) by student.zip) > 10; ... where school.zip in big_zip`.\n\nMembership is IDENTITY matching, not SQL three-valued logic: NULL matches NULL; the result is always TRUE or FALSE (safe as a projected flag); `not in` is the exact complement (a NULL in the set never empties the result). For strict SQL behavior, add `x is not null`.\n\nA fact model contains ALL dimensional members, so `where students.id not in enroll.student_id` is typically a tautology — use `where enroll.id is null` instead. Tuple membership `(a, b) in (m.a, m.b)` requires both right-side concepts to resolve to ONE source (model or rowset); to test against a cross-model pair, stage it through a rowset first. Examples: existence-anti-join, staged-membership.\n\n## Aggregation\n\nAn aggregate without an explicit `by` is RESPONSIVE — its grain is set where it is consumed, not where it is declared (`auto avg_credits <- avg(enroll.credits);` becomes per-department when selected with `enroll.department`). Pin a grain inline: `sum(metric) by dim1, dim2 as name`; `by` accepts bare identifiers or parenthesized expressions: `avg(price) by (substring(phone, 1, 2))`.\n\nMulti-level grouping (`by rollup|cube|grouping sets (...)`) is a clause of the WHOLE select, placed before `having`: it computes every no-explicit-grain aggregate at every level in one pass (`by rollup (d1, d2)` → leaf, per-d1 subtotal, grand total). `grouping(<field>)` = 1 on rolled-up rows — use it to label levels rather than testing for NULL. Full detail: trilogy agent-info syntax example rollup.\n\n## Window functions\n\nDefault to windows for self-referential queries (period-over-period, running totals, rank, share of group). SQL-style syntax: `rank(<key>) over (partition by <g> order by <expr> desc)` (`dense_rank`/`row_number` same shape; multi-key `rank(a, b)`); `sum(x) over (partition by g order by t)` for running totals; `lag(<field>, <offset>) over (...)` / `lead(...)` for offsets (default 1). `partition by` is optional and accepts expressions.\n\n## Data types\n\nScalars: `string`, `bytes`, `bool`, `int`, `bigint`, `float`, `double`, `number`, `numeric(p,s)` (alias `decimal`; use for exact money), `date`, `datetime`, `timestamp`, `geography`, `any`. Composites: `array<T>`, `map<K, V>`, `struct<name: T, ...>`, `enum<T>[v1, v2, ...]`. Cast with `::type` or `cast(x as type)`: `\"2020-01-01\"::date`.\n\n`?` after a type marks it nullable (`property user.middle_name string?;`). `~` prefixing a concept marks the reference as a PARTIAL subset of the concept's domain — on a select item (`~customer.id`) or in `merge a into ~b` (a ⊆ b); it tells the planner this side is not authoritative.\n\n## Expression gotchas\n\n- `x ? cond` binds to a primary — wrap arithmetic: `(a - b) ? cond`, NOT `a - b ? cond`.\n- `a || b` is NULL if either side is NULL; `concat(...)` skips NULL arguments; `concat_ws(sep, ...)` joins with sep, skipping NULLs.\n- Date parts are unquoted: `date_part(d, year)`, never `'year'`; prefer `year(d)`. `date_diff(start, end, unit)` = end − start (a shipping lag is `date_diff(sold_date, ship_date, day)`).\n- Zero-argument functions still take parentheses: `current_date()`.\n- Factor repeated calculations into a `def` macro (invoked `@name(...)`) or reusable `auto` concepts.\n\n\nFunctions (the `aggregate` family doubles as the Aggregate Functions list):\naggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)\nstring: concat|concat_strict|concat_ws(<arg1>, <arg2>, ..., <argN>) ; split|strpos|contains|trim|regexp_contains|format_time|parse_time(<arg1>, <arg2>) ; len|lower|upper|ltrim|rtrim|hex(<arg1>) ; substring|replace|regexp_extract|regexp_replace(<arg1>, <arg2>, <arg3>)\ndate/time: date|datetime|timestamp|second|minute|hour|day|day_of_week|week|month|quarter|year|month_name|day_name|unix_to_timestamp(<arg1>) ; date_spine(<arg1>, <arg2>) ; current_date|current_datetime()\n date_part(<arg1>, <arg2>) e.g. date_part('2020-01-01'::date, year)\n date_truncate(<arg1>, <arg2>) e.g. date_trunc('2020-01-01'::date, month)\n date_add(<arg1>, <arg2>, <arg3>) e.g. date_add('2020-01-01'::date, month, 1)\n date_sub(<arg1>, <arg2>, <arg3>) e.g. date_sub('2020-01-01'::date, day, 1)\n date_diff(<arg1>, <arg2>, <arg3>) e.g. date_diff('2020-01-01'::date, '2020-01-02'::date, day)\n current_timestamp() e.g. now()\narray/map/struct: unnest|array_distinct|array_sum|map_keys|map_values(<arg1>) ; struct|array(<arg1>, <arg2>, ..., <argN>) ; array_sort|array_to_string(<arg1>, <arg2>) ; array_transform|array_filter|generate_array(<arg1>, <arg2>, <arg3>)\nmath: mod|round|log|power|hash(<arg1>, <arg2>) ; abs|sqrt|random|floor|ceil(<arg1>)\nconditional/cast: case|simple_case|coalesce|greatest|least(<arg1>, <arg2>, ..., <argN>) ; cast|nullif(<arg1>, <arg2>) ; isnull(<arg1>)\ngeo: geo_from_text|geo_x|geo_y|geo_centroid(<arg1>) ; geo_point|geo_distance(<arg1>, <arg2>) ; geo_transform(<arg1>, <arg2>, <arg3>)\nother: is_not_distinct(<arg1>, <arg2>)\n\nSome types carry trait metadata that adds meaning: 'latitude', 'longitude' and 'currency' are all 'float' with extra semantics.\n\nAdditional syntax examples:\nThese less-common patterns have complete, copy-pasteable examples. Do NOT guess\nthe syntax — print the full example on demand with\n`trilogy agent-info syntax example <name>`:\n- `python-datasource` - run a local Python script as a datasource: wrap a function in `trilogy.io.run`, which writes the Arrow IPC stream to stdout for you from a table, dataframe, or list of dicts; declare concepts, map script columns in `datasource (...)`, use `grain (...) file `path.py`;`, then reference locally declared concepts WITHOUT the datasource name as a prefix\n- `query-structure` - the exact clause order of a query, and how to define a rowset\n- `chart` - drawing a result rather than tabulating it: `chart layer <type> ( <role> <- <expr> as <name>, ... )` is a STATEMENT that wraps its own query - the bindings ARE the select; layers, roles, scales, reference lines and `from <select>`\n- `filtered-aggregate` - aggregating only the rows matching a condition, and counting rows correctly\n- `row-level-where-vs-having` - choosing between WHERE and HAVING for a threshold condition\n- `nested-aggregate-group-average` - the \"above 1.2x the group norm\" ask: inner `sum(x) by entity, group`, outer `avg(inner) by group`; use WHERE for the comparison when a separate output-only filter must not narrow the benchmark\n- `pivot-columns` - turn distinct values of a dimension into columns with a reusable `def` macro + filtered aggregate\n- `case-when` - `case when <cond> then <val> ... else <val> end` to relabel codes or bucket a measure into a derived value, then group by it\n- `window-period-over-period` - `lead`/`lag` over (order by ...) to compare a row to an offset period; build a scoped set with `<col> ? <cond>` and filter with `in`\n- `union-stack-channels` - stacking several sources/channels together as rows\n- `except-intersect-setops` - set difference or co-occurrence across sources\n- `scoped-join` - blending a second fact model or rowset into one query\n- `existence-anti-join` - keeping or excluding rows by whether a key matches another model\n- `set-intersect-difference` - count tuples present in / absent from other groups via per-group presence flags - the multi-column INTERSECT/EXCEPT idiom\n- `dedup-before-aggregate` - a `rowset <- select <cols>` collapses identical tuples to one row (SELECT-grain group by) before a downstream sum - the `distinct`/UNION substitute\n- `rollup` - subtotals and a grand total in one pass, and labeling the levels\n- `rank-over-rollup` - rank rollup subtotals/leaves with a SINGLE `rank(a,b) over (partition by level, parent ...)` - not separate ranks per level\n- `staged-membership` - filtering a fine-grain query by a set computed at a coarser grain\n- `correlated-exists-via-grouped-counts` - translate `EXISTS other` / `NOT EXISTS other matching` over the same model into two `count(...) by <grain>` compared in `where` (`> 1` = another exists, `= 1` = no other matches) - pin the correlation grain with `by`", Ng = "# Trilogy Syntax\n\nTrilogy statements define a semantic model or a query; collections of statements\nare written as `.preql` files. A statement returns data (SELECT) or defines\nconcepts (key | property | auto | metric), datasources, or parameters.\n\nimport <model> as <alias>; makes a model's fields available. <model> is a DOT-SEPARATED MODULE PATH relative to the project root (where trilogy.toml lives), never a file path: the file `raw/store_sales.preql` is imported as `import raw.store_sales as ss;` — replace each `/` with `.`, drop the `.preql`, keep the folder segment. When an imported model imports others (a fact with imported dimensions), those are dot paths: `enroll.student.name`. Typical usage imports ONE fact and reaches dimensions through it; nested dimensions carry ALL values (order.customers includes customers without orders).\n\nkey | property | auto | metric; define new concepts. `auto x <- ...` is a DEFINITION, not a precomputed value: each reference expands and re-evaluates in the consuming query's scope.\n\nparameter NAME TYPE [default <literal>]; — a runtime value supplied via `--param NAME=VALUE` (repeatable); append `?` to the type to allow null.\n\n## Combining models\n\n| Goal | Use |\n|---|---|\n| Typical query | no merge, no join: access all fields through dot-paths |\n| Blend two models on shared keys inside one query | scoped `subset\\|union join` |\n| Make a connection universal to a whole file | `merge` |\n| Stack subsets/channels as rows | `union(...)` |\n| Rows in A but never in B (set difference) | `except(...)` |\n| Rows present in every source (set intersection) | `intersect(...)` |\n\n### Query-scoped join\n\nA typical fact already has its dimensions merged in, so no join is needed. Blending fact models or rowset outputs takes a scoped join, placed right after the select list, which DECLARES how the key domains relate: `subset join a = b` (a's values are contained in b's, a ⊆ b; b authoritative for the key) or `union join a = b` (neither contains the other; the key is the coalesce of both sides and unmatched rows from BOTH sides are kept).\n\nTwo rules decide whether the numbers are right, so apply them before reaching for the example:\n\n- Joins NEVER drop a row and NULL keys match null-safely; there is NO inner join. An intersection is an explicit `where <optional-side attr> is not null`.\n- JOIN ON THE FULL GRAIN: `explore` prints each fact's grain as @<k1, k2>; write one clause PER key. Matching only one key of a multi-key grain causes duplication.\n\nChaining, expression keys, union/subset mixing, multi-key and self-pair shapes: trilogy agent-info syntax example scoped-join.\n\nmerge <a> into ~<b>; is the persistent (whole-file) equivalent of `subset join a = b`; plain `merge a into b;` declares EXACT domain equivalence. Standalone statements; prefer a scoped join unless the connection is universal.\n\n### union / except / intersect (row set operations)\n\n`union(...)` row-stacks self-contained select arms positionally (SQL UNION ALL) into one named rowset (`with combined as union((armA), (armB), ...) -> (out1, out2, ...)`); `except(...)` / `intersect(...)` share that arm shape but are SQL SET operators (DISTINCT rows, null-safe whole-row compare, and except subtracts later arms from the FIRST, so arm order matters). Prefer except over a multi-column `not in` for \"in A but never in B\". Each arm dedups to its own output grain BEFORE stacking, so aggregate INSIDE each arm or the total silently undersums. Examples: trilogy agent-info syntax example union-stack-channels / except-intersect-setops.\n\n## Query structure\n\nTop-level statements first (each `;`-terminated), then a query with clauses in this EXACT order — all optional except select:\n\n```\n<top-level statements>; # import / key / auto / rowset / merge / def / parameter\nwith <name> as # optional: name the select as a reusable rowset (CTE)\nwhere <row condition> # 1. filters INPUT rows, BEFORE aggregation\nselect <col>, <agg> as name, # 2. projection — grouping is AUTOMATIC by the\n # non-aggregated columns; never write GROUP BY\n subset|union join a = b (= c)? # 3. blend models; one clause per key, right after the select list\nby rollup|cube|grouping sets (...) # 4. optional multi-level grouping for the whole select\nhaving <result condition> # 5. filters aggregated/joined RESULTS\norder by <col> asc|desc # 6. sort\nlimit <n>; # 7. cap rows\n```\n\nA rowset (`with <name> as where ... select ...;`) is a standalone statement, evaluated in isolation — it does NOT respond to the consuming query's context. All its outputs are namespaced: output `abc.def` of rowset `foo` is referenced as `foo.abc.def`, and joined back like any concept (`subset join foo.key = other.key`). Alias every reused expression with `as`.\n\nFull annotated example: trilogy agent-info syntax example query-structure.\n\n### Not SQL — what to never write\n\n- **No FROM, GROUP BY, DISTINCT, SELECT \\*, or SQL-style set operators between selects.** Stack rows with `union(...)`; set difference/intersection with `except(...)`/`intersect(...)`; blend fact models with a scoped join.\n- **Grouping is automatic** by the non-aggregated fields in the SELECT — never write GROUP BY. Aggregates inherit the select's grain wherever they appear (where/select/having). Override per-aggregate with `agg(x) by <dims>`; `by *` aggregates across all data (single row). To weight a related dimension property once per fact row, project it to the fact grain first: `auto row_birth_year <- group(enroll.student.birth_year) by enroll.student_id, enroll.course_id;` then aggregate that.\n- **Output rows are deduplicated to the select grain.** To preserve legitimate duplicates (one row per matching fact), include the fact's grain keys in the select — hidden with the PREFIX `--` if they shouldn't appear in the output.\n- **Never write `distinct`.** `count(<key>)` is already distinct because keys are unique; `count_distinct(<property>)` counts distinct values of a non-key property.\n- **Count a COMPOSITE grain with `grain(...)`.** `count(grain(order_id, item.id))` counts order+item combinations; `grain(...)` is NEVER NULL. Never count one column of a multi-key grain — a coarser key counts its own distinct values and undercounts. `count_distinct(a, b)` is accepted as sugar for `count(grain(a, b))` (distinct combinations, NULL members included).\n- **An aggregate over a group with no qualifying rows is NULL, never 0.** `sum(x ? cond) by k` yields NULL for groups where nothing matches, so `= 0` matches nothing. Test emptiness with `count(key ? cond) by k = 0` or wrap: `coalesce(sum(x ? cond), 0)`.\n- **`count(1)` is invalid** — a constant does not identify rows. Use the row key, `count(grain(...))`, or `sum(1) by <grain>`.\n- **One-column expression subqueries only.** `(select ...)` is allowed where a scalar or membership set is expected (exactly one projected column) — never as a SQL-style FROM-subquery. For related-entity filters prefer the dot-path: `where enrollments.student.state = 'TN'`, not a subselect.\n- **`--` is a HIDDEN-field prefix, not a comment** (it still changes query structure). Comments use `#` only.\n- **Aliases**: always use the full dot-path (`enroll.student.id`); alias every new expression with `as`; a select alias is usable in `having`/`order by` but NOT inside other select expressions or `where`, and must not rename a field back to an existing concept name.\n\n## Filtering\n\n`where` filters input rows before aggregation; `having` filters results after aggregation/windows. The inline filter `x ? cond` filters the immediate prior expression: `sum(credits ? enrolled = true)`.\n\nAggregates/windows in WHERE do not filter each other's inputs. Use inline filters, or a staged `then where` chain: each `then where` stage's aggregates compute over only the rows passing earlier stages (`where x = 5 then where sum(y) > 10` ≡ `where x = 5 and sum(y ? x = 5) > 10`). One rule: the SAME cross-row expression cannot gate two different stages — give one an inline filter.\n\nWindows need all rows in their range: filter AFTER the window (in `having`) rather than in `where` when the window must see the full set.\n\n## Membership (semijoins)\n\n`in` / `not in` cross models without any declared relationship — the semijoin IS a scoped intersection. The right side is a concept or expression (never a subselect); build computed sets with an inline filter: `auto big_zip <- student.zip ? (count(student.id ? student.honors = true) by student.zip) > 10; ... where school.zip in big_zip`.\n\nMembership is IDENTITY matching, not SQL three-valued logic: NULL matches NULL; the result is always TRUE or FALSE (safe as a projected flag); `not in` is the exact complement (a NULL in the set never empties the result). For strict SQL behavior, add `x is not null`.\n\nA fact model contains ALL dimensional members, so `where students.id not in enroll.student_id` is typically a tautology — use `where enroll.id is null` instead. Tuple membership `(a, b) in (m.a, m.b)` requires both right-side concepts to resolve to ONE source (model or rowset); to test against a cross-model pair, stage it through a rowset first. Examples: existence-anti-join, staged-membership.\n\n## Aggregation\n\nAn aggregate without an explicit `by` is RESPONSIVE — its grain is set where it is consumed, not where it is declared (`auto avg_credits <- avg(enroll.credits);` becomes per-department when selected with `enroll.department`). Pin a grain inline: `sum(metric) by dim1, dim2 as name`; `by` accepts bare identifiers or parenthesized expressions: `avg(price) by (substring(phone, 1, 2))`.\n\nMulti-level grouping (`by rollup|cube|grouping sets (...)`) is a clause of the WHOLE select, placed before `having`: it computes every no-explicit-grain aggregate at every level in one pass (`by rollup (d1, d2)` → leaf, per-d1 subtotal, grand total). `grouping(<field>)` = 1 on rolled-up rows — use it to label levels rather than testing for NULL. Full detail: trilogy agent-info syntax example rollup.\n\n## Window functions\n\nDefault to windows for self-referential queries (period-over-period, running totals, rank, share of group). SQL-style syntax: `rank(<key>) over (partition by <g> order by <expr> desc)` (`dense_rank`/`row_number` same shape; multi-key `rank(a, b)`); `sum(x) over (partition by g order by t)` for running totals; `lag(<field>, <offset>) over (...)` / `lead(...)` for offsets (default 1). `partition by` is optional and accepts expressions.\n\n## Data types\n\nScalars: `string`, `bytes`, `bool`, `int`, `bigint`, `float`, `double`, `number`, `numeric(p,s)` (alias `decimal`; use for exact money), `date`, `datetime`, `timestamp`, `geography`, `any`. Composites: `array<T>`, `map<K, V>`, `struct<name: T, ...>`, `enum<T>[v1, v2, ...]`. Cast with `::type` or `cast(x as type)`: `\"2020-01-01\"::date`.\n\n`?` after a type marks it nullable (`property user.middle_name string?;`). `~` prefixing a concept marks the reference as a PARTIAL subset of the concept's domain — on a select item (`~customer.id`) or in `merge a into ~b` (a ⊆ b); it tells the planner this side is not authoritative.\n\n## Expression gotchas\n\n- `x ? cond` binds to a primary — wrap arithmetic: `(a - b) ? cond`, NOT `a - b ? cond`.\n- `a || b` is NULL if either side is NULL; `concat(...)` skips NULL arguments; `concat_ws(sep, ...)` joins with sep, skipping NULLs.\n- Date parts are unquoted: `date_part(d, year)`, never `'year'`; prefer `year(d)`. `date_diff(start, end, unit)` = end − start (a shipping lag is `date_diff(sold_date, ship_date, day)`).\n- Zero-argument functions still take parentheses: `current_date()`.\n- Factor repeated calculations into a `def` macro (invoked `@name(...)`) or reusable `auto` concepts.", Pg = ["aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)"], Fg = [
15451
+ }, Mg = "# Trilogy Syntax\n\nTrilogy statements define a semantic model or a query; collections of statements\nare written as `.preql` files. A statement returns data (SELECT) or defines\nconcepts (key | property | auto | metric), datasources, or parameters.\n\nimport <model> as <alias>; makes a model's fields available. <model> is a DOT-SEPARATED MODULE PATH relative to the project root (where trilogy.toml lives), never a file path: the file `raw/store_sales.preql` is imported as `import raw.store_sales as ss;` — replace each `/` with `.`, drop the `.preql`, keep the folder segment. When an imported model imports others (a fact with imported dimensions), those are dot paths: `enroll.student.name`. Typical usage imports ONE fact and reaches dimensions through it; nested dimensions carry ALL values (order.customers includes customers without orders).\n\nkey | property | auto | metric; define new concepts. `auto x <- ...` is a DEFINITION, not a precomputed value: each reference expands and re-evaluates in the consuming query's scope.\n\nparameter NAME TYPE [default <literal>]; — a runtime value supplied via `--param NAME=VALUE` (repeatable); append `?` to the type to allow null.\n\n## Combining models\n\n| Goal | Use |\n|---|---|\n| Typical query | no merge, no join: access all fields through dot-paths |\n| Blend two models on shared keys inside one query | scoped `subset\\|union\\|equal join` |\n| Make a connection universal to a whole file | `merge` |\n| Stack subsets/channels as rows | `union(...)` |\n| Rows in A but never in B (set difference) | `except(...)` |\n| Rows present in every source (set intersection) | `intersect(...)` |\n\n### Query-scoped join\n\nA typical fact already has its dimensions merged in, so no join is needed. Blending fact models or rowset outputs takes a scoped join, placed right after the select list, which DECLARES how the key domains relate: `subset join a = b` (a's values are contained in b's, a ⊆ b; b authoritative for the key), `union join a = b` (neither contains the other; the key is the coalesce of both sides and unmatched rows from BOTH sides are kept) or `equal join a = b` (ONE domain: a is an alias of b, every value on either side is on the other; the join renders inner).\n\nTwo rules decide whether the numbers are right, so apply them before reaching for the example:\n\n- Joins NEVER drop a row and NULL keys match null-safely; there is NO inner join. An intersection is an explicit `where <optional-side attr> is not null`.\n- JOIN ON THE FULL GRAIN: `explore` prints each fact's grain as @<k1, k2>; write one clause PER key. Matching only one key of a multi-key grain causes duplication.\n\nChaining, expression keys, union/subset mixing, multi-key and self-pair shapes: trilogy agent-info syntax example scoped-join.\n\n`merge subset a = b;`, `merge union a = b;` and `merge equal a = b;` are the persistent (whole-file) equivalents of the three scoped joins; `merge a into ~b;` and `merge a into b;` are older spellings of `merge subset` and `merge equal`. Standalone statements; prefer a scoped join unless the connection is universal. Use `merge union` when each side has keys the other lacks (orphaned parents, dimension rows with no facts): `merge a into b` declares one domain and drops the unmatched rows.\n\n### union / except / intersect (row set operations)\n\n`union(...)` row-stacks self-contained select arms positionally (SQL UNION ALL) into one named rowset (`with combined as union((armA), (armB), ...) -> (out1, out2, ...)`); `except(...)` / `intersect(...)` share that arm shape but are SQL SET operators (DISTINCT rows, null-safe whole-row compare, and except subtracts later arms from the FIRST, so arm order matters). Prefer except over a multi-column `not in` for \"in A but never in B\". Each arm dedups to its own output grain BEFORE stacking, so aggregate INSIDE each arm or the total silently undersums. Examples: trilogy agent-info syntax example union-stack-channels / except-intersect-setops.\n\n## Query structure\n\nTop-level statements first (each `;`-terminated), then a query with clauses in this EXACT order — all optional except select:\n\n```\n<top-level statements>; # import / key / auto / rowset / merge / def / parameter\nwith <name> as # optional: name the select as a reusable rowset (CTE)\nwhere <row condition> # 1. filters INPUT rows, BEFORE aggregation\nselect <col>, <agg> as name, # 2. projection — grouping is AUTOMATIC by the\n # non-aggregated columns; never write GROUP BY\n subset|union|equal join a = b (= c)? # 3. blend models; one clause per key, right after the select list\nby rollup|cube|grouping sets (...) # 4. optional multi-level grouping for the whole select\nhaving <result condition> # 5. filters aggregated/joined RESULTS\norder by <col> asc|desc # 6. sort\nlimit <n>; # 7. cap rows\n```\n\nA rowset (`with <name> as where ... select ...;`) is a standalone statement, evaluated in isolation — it does NOT respond to the consuming query's context. All its outputs are namespaced: output `abc.def` of rowset `foo` is referenced as `foo.abc.def`. Pairing an output with a concept outside the rowset REQUIRES a declared join on the rowset's key (`subset join foo.key = key`); without one the query is disconnected. Alternatively project the concept inside the rowset and filter or select it through the handle (`where foo.cat = 'a'`). Alias every reused expression with `as`.\n\nFull annotated example: trilogy agent-info syntax example query-structure.\n\n### Not SQL — what to never write\n\n- **No FROM, GROUP BY, DISTINCT, SELECT \\*, or SQL-style set operators between selects.** Stack rows with `union(...)`; set difference/intersection with `except(...)`/`intersect(...)`; blend fact models with a scoped join.\n- **Grouping is automatic** by the non-aggregated fields in the SELECT — never write GROUP BY. Aggregates inherit the select's grain wherever they appear (where/select/having). Override per-aggregate with `agg(x) by <dims>`; `by *` aggregates across all data (single row). To weight a related dimension property once per fact row, project it to the fact grain first: `auto row_birth_year <- group(enroll.student.birth_year) by enroll.student_id, enroll.course_id;` then aggregate that.\n- **Output rows are deduplicated to the select grain.** To preserve legitimate duplicates (one row per matching fact), include the fact's grain keys in the select — hidden with the PREFIX `--` if they shouldn't appear in the output.\n- **No `select distinct`.** `count(<key>)` is already distinct because keys are unique; `count(distinct(<property>))` counts distinct values of a non-key property. `distinct(x)` is `group(x) by x`: the distinct values of x, usable anywhere.\n- **Count a COMPOSITE grain with `grain(...)`.** `count(grain(order_id, item.id))` counts order+item combinations; `grain(...)` is NEVER NULL. Never count one column of a multi-key grain — a coarser key counts its own distinct values and undercounts.\n- **An aggregate over a group with no qualifying rows is NULL, never 0.** `sum(x ? cond) by k` yields NULL for groups where nothing matches, so `= 0` matches nothing. Test emptiness with `count(key ? cond) by k = 0` or wrap: `coalesce(sum(x ? cond), 0)`.\n- **`count(1)` is invalid** — a constant does not identify rows. Use the row key, `count(grain(...))`, or `sum(1) by <grain>`.\n- **One-column expression subqueries only.** `(select ...)` is allowed where a scalar or membership set is expected (exactly one projected column; a scalar must also be one row: an aggregate with no `by`, or `limit 1`) — never as a SQL-style FROM-subquery. For related-entity filters prefer the dot-path: `where enrollments.student.state = 'TN'`, not a subselect.\n- **`--` is a HIDDEN-field prefix, not a comment** (it still changes query structure). Comments use `#` only.\n- **Aliases**: always use the full dot-path (`enroll.student.id`); alias every new expression with `as`; a select alias is usable in `having`/`order by` but NOT inside other select expressions or `where`, and must not rename a field back to an existing concept name.\n\n## Filtering\n\n`where` filters input rows before aggregation; `having` filters results after aggregation/windows. The inline filter `x ? cond` filters the immediate prior expression: `sum(credits ? enrolled = true)`.\n\nAggregates/windows in WHERE do not filter each other's inputs. Use inline filters, or a staged `then where` chain: each `then where` stage's aggregates compute over only the rows passing earlier stages (`where x = 5 then where sum(y) > 10` ≡ `where x = 5 and sum(y ? x = 5) > 10`). One rule: the SAME cross-row expression cannot gate two different stages — give one an inline filter.\n\nWindows need all rows in their range: filter AFTER the window (in `having`) rather than in `where` when the window must see the full set.\n\n## Membership (semijoins)\n\n`in` / `not in` cross models without any declared relationship — the semijoin IS a scoped intersection. The right side is a concept or expression (never a subselect); build computed sets with an inline filter: `auto big_zip <- student.zip ? (count(student.id ? student.honors = true) by student.zip) > 10; ... where school.zip in big_zip`.\n\nMembership is IDENTITY matching, not SQL three-valued logic: NULL matches NULL; the result is always TRUE or FALSE (safe as a projected flag); `not in` is the exact complement (a NULL in the set never empties the result). For strict SQL behavior, add `x is not null`.\n\nA fact model contains ALL dimensional members, so `where students.id not in enroll.student_id` is typically a tautology — use `where enroll.id is null` instead. Tuple membership `(a, b) in (m.a, m.b)` requires both right-side concepts to resolve to ONE source (model or rowset); to test against a cross-model pair, stage it through a rowset first. Examples: existence-anti-join, staged-membership.\n\n## Aggregation\n\nAn aggregate without an explicit `by` is RESPONSIVE — its grain is set where it is consumed, not where it is declared (`auto avg_credits <- avg(enroll.credits);` becomes per-department when selected with `enroll.department`). Pin a grain inline: `sum(metric) by dim1, dim2 as name`; `by` accepts bare identifiers or parenthesized expressions: `avg(price) by (substring(phone, 1, 2))`.\n\nMulti-level grouping (`by rollup|cube|grouping sets (...)`) is a clause of the WHOLE select, placed before `having`: it computes every no-explicit-grain aggregate at every level in one pass (`by rollup (d1, d2)` → leaf, per-d1 subtotal, grand total). `grouping(<field>)` = 1 on rolled-up rows — use it to label levels rather than testing for NULL. Full detail: trilogy agent-info syntax example rollup.\n\n## Window functions\n\nDefault to windows for self-referential queries (period-over-period, running totals, rank, share of group). SQL-style syntax: `rank(<key>) over (partition by <g> order by <expr> desc)` (`dense_rank`/`row_number` same shape; multi-key `rank(a, b)`); `sum(x) over (partition by g order by t)` for running totals; `lag(<field>, <offset>) over (...)` / `lead(...)` for offsets (default 1); these walk one row per key of the field (a property walks its key's rows), while `lag(distinct(<field>)) over (...)` walks the field's distinct values (`distinct(x)` = `group(x) by x`, usable anywhere). `partition by` is optional and accepts expressions.\n\n## Data types\n\nScalars: `string`, `bytes`, `bool`, `int`, `bigint`, `float`, `double`, `number`, `numeric(p,s)` (alias `decimal`; use for exact money), `date`, `datetime`, `timestamp`, `geography`, `any`. Composites: `array<T>`, `map<K, V>`, `struct<name: T, ...>`, `enum<T>[v1, v2, ...]`. Cast with `::type` or `cast(x as type)`: `\"2020-01-01\"::date`.\n\n`?` after a type marks it nullable (`property user.middle_name string?;`). `~` prefixing a concept marks the reference as a PARTIAL subset of the concept's domain — on a select item (`~customer.id`) or in `merge a into ~b` (a ⊆ b); it tells the planner this side is not authoritative.\n\n## Expression gotchas\n\n- `x ? cond` binds to a primary — wrap arithmetic: `(a - b) ? cond`, NOT `a - b ? cond`.\n- `a || b` is NULL if either side is NULL; `concat(...)` skips NULL arguments; `concat_ws(sep, ...)` joins with sep, skipping NULLs.\n- Date parts are unquoted: `date_part(d, year)`, never `'year'`; prefer `year(d)`. `date_diff(start, end, unit)` = end − start (a shipping lag is `date_diff(sold_date, ship_date, day)`).\n- Zero-argument functions still take parentheses: `current_date()`.\n- Factor repeated calculations into a `def` macro (invoked `@name(...)`) or reusable `auto` concepts.\n\n\nFunctions (the `aggregate` family doubles as the Aggregate Functions list):\naggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)\nstring: concat|concat_strict|concat_ws(<arg1>, <arg2>, ..., <argN>) ; split|strpos|contains|trim|regexp_contains|format_time|parse_time(<arg1>, <arg2>) ; len|lower|upper|ltrim|rtrim|hex(<arg1>) ; substring|replace|regexp_extract|regexp_replace(<arg1>, <arg2>, <arg3>)\ndate/time: date|datetime|timestamp|second|minute|hour|day|day_of_week|week|month|quarter|year|month_name|day_name|unix_to_timestamp(<arg1>) ; date_spine(<arg1>, <arg2>) ; current_date|current_datetime()\n date_part(<arg1>, <arg2>) e.g. date_part('2020-01-01'::date, year)\n date_truncate(<arg1>, <arg2>) e.g. date_trunc('2020-01-01'::date, month)\n date_add(<arg1>, <arg2>, <arg3>) e.g. date_add('2020-01-01'::date, month, 1)\n date_sub(<arg1>, <arg2>, <arg3>) e.g. date_sub('2020-01-01'::date, day, 1)\n date_diff(<arg1>, <arg2>, <arg3>) e.g. date_diff('2020-01-01'::date, '2020-01-02'::date, day)\n current_timestamp() e.g. now()\narray/map/struct: unnest|array_distinct|array_sum|map_keys|map_values(<arg1>) ; struct|array(<arg1>, <arg2>, ..., <argN>) ; array_sort|array_to_string(<arg1>, <arg2>) ; array_transform|array_filter|generate_array(<arg1>, <arg2>, <arg3>)\nmath: mod|round|log|power|hash(<arg1>, <arg2>) ; abs|sqrt|random|floor|ceil(<arg1>)\nconditional/cast: case|simple_case|coalesce|greatest|least(<arg1>, <arg2>, ..., <argN>) ; cast|nullif(<arg1>, <arg2>) ; isnull(<arg1>)\ngeo: geo_from_text|geo_x|geo_y|geo_centroid(<arg1>) ; geo_point|geo_distance(<arg1>, <arg2>) ; geo_transform(<arg1>, <arg2>, <arg3>)\nother: is_not_distinct(<arg1>, <arg2>) ; prior(<arg1>) ; grain_pin(<arg1>, <arg2>, ..., <argN>)\n\nSome types carry trait metadata that adds meaning: 'latitude', 'longitude' and 'currency' are all 'float' with extra semantics.\n\nAdditional syntax examples:\nThese less-common patterns have complete, copy-pasteable examples. Do NOT guess\nthe syntax — print the full example on demand with\n`trilogy agent-info syntax example <name>`:\n- `python-datasource` - run a local Python script as a datasource: wrap a function in `trilogy.io.run`, which writes the Arrow IPC stream to stdout for you from a table, dataframe, or list of dicts; declare concepts, map script columns in `datasource (...)`, use `grain (...) file `path.py`;`, then reference locally declared concepts WITHOUT the datasource name as a prefix\n- `query-structure` - the exact clause order of a query, and how to define a rowset\n- `chart` - drawing a result rather than tabulating it: `chart layer <type> ( <role> <- <expr> as <name>, ... )` is a STATEMENT that wraps its own query - the bindings ARE the select; layers, roles, scales, reference lines and `from <select>`\n- `filtered-aggregate` - aggregating only the rows matching a condition, and counting rows correctly\n- `row-level-where-vs-having` - choosing between WHERE and HAVING for a threshold condition\n- `nested-aggregate-group-average` - the \"above 1.2x the group norm\" ask: inner `sum(x) by entity, group`, outer `avg(inner) by group`; use WHERE for the comparison when a separate output-only filter must not narrow the benchmark\n- `pivot-columns` - turn distinct values of a dimension into columns with a reusable `def` macro + filtered aggregate\n- `case-when` - `case when <cond> then <val> ... else <val> end` to relabel codes or bucket a measure into a derived value, then group by it\n- `window-period-over-period` - `lead`/`lag` over (order by ...) to compare a row to an offset period; build a scoped set with `<col> ? <cond>` and filter with `in`\n- `union-stack-channels` - stacking several sources/channels together as rows\n- `except-intersect-setops` - set difference or co-occurrence across sources\n- `scoped-join` - blending a second fact model or rowset into one query\n- `existence-anti-join` - keeping or excluding rows by whether a key matches another model\n- `set-intersect-difference` - count tuples present in / absent from other groups via per-group presence flags - the multi-column INTERSECT/EXCEPT idiom\n- `dedup-before-aggregate` - a `rowset <- select <cols>` collapses identical tuples to one row (SELECT-grain group by) before a downstream sum - the `distinct`/UNION substitute\n- `rollup` - subtotals and a grand total in one pass, and labeling the levels\n- `rank-over-rollup` - rank rollup subtotals/leaves with a SINGLE `rank(a,b) over (partition by level, parent ...)` - not separate ranks per level\n- `staged-membership` - filtering a fine-grain query by a set computed at a coarser grain\n- `correlated-exists-via-grouped-counts` - translate `EXISTS other` / `NOT EXISTS other matching` over the same model into two `count(...) by <grain>` compared in `where` (`> 1` = another exists, `= 1` = no other matches) - pin the correlation grain with `by`\n- `recursive-rollup` - walk a parent pointer (folder tree, org chart, process tree) to the top of each chain with `recurse_edge(key, parent)`, then aggregate per root", Ng = "# Trilogy Syntax\n\nTrilogy statements define a semantic model or a query; collections of statements\nare written as `.preql` files. A statement returns data (SELECT) or defines\nconcepts (key | property | auto | metric), datasources, or parameters.\n\nimport <model> as <alias>; makes a model's fields available. <model> is a DOT-SEPARATED MODULE PATH relative to the project root (where trilogy.toml lives), never a file path: the file `raw/store_sales.preql` is imported as `import raw.store_sales as ss;` — replace each `/` with `.`, drop the `.preql`, keep the folder segment. When an imported model imports others (a fact with imported dimensions), those are dot paths: `enroll.student.name`. Typical usage imports ONE fact and reaches dimensions through it; nested dimensions carry ALL values (order.customers includes customers without orders).\n\nkey | property | auto | metric; define new concepts. `auto x <- ...` is a DEFINITION, not a precomputed value: each reference expands and re-evaluates in the consuming query's scope.\n\nparameter NAME TYPE [default <literal>]; — a runtime value supplied via `--param NAME=VALUE` (repeatable); append `?` to the type to allow null.\n\n## Combining models\n\n| Goal | Use |\n|---|---|\n| Typical query | no merge, no join: access all fields through dot-paths |\n| Blend two models on shared keys inside one query | scoped `subset\\|union\\|equal join` |\n| Make a connection universal to a whole file | `merge` |\n| Stack subsets/channels as rows | `union(...)` |\n| Rows in A but never in B (set difference) | `except(...)` |\n| Rows present in every source (set intersection) | `intersect(...)` |\n\n### Query-scoped join\n\nA typical fact already has its dimensions merged in, so no join is needed. Blending fact models or rowset outputs takes a scoped join, placed right after the select list, which DECLARES how the key domains relate: `subset join a = b` (a's values are contained in b's, a ⊆ b; b authoritative for the key), `union join a = b` (neither contains the other; the key is the coalesce of both sides and unmatched rows from BOTH sides are kept) or `equal join a = b` (ONE domain: a is an alias of b, every value on either side is on the other; the join renders inner).\n\nTwo rules decide whether the numbers are right, so apply them before reaching for the example:\n\n- Joins NEVER drop a row and NULL keys match null-safely; there is NO inner join. An intersection is an explicit `where <optional-side attr> is not null`.\n- JOIN ON THE FULL GRAIN: `explore` prints each fact's grain as @<k1, k2>; write one clause PER key. Matching only one key of a multi-key grain causes duplication.\n\nChaining, expression keys, union/subset mixing, multi-key and self-pair shapes: trilogy agent-info syntax example scoped-join.\n\n`merge subset a = b;`, `merge union a = b;` and `merge equal a = b;` are the persistent (whole-file) equivalents of the three scoped joins; `merge a into ~b;` and `merge a into b;` are older spellings of `merge subset` and `merge equal`. Standalone statements; prefer a scoped join unless the connection is universal. Use `merge union` when each side has keys the other lacks (orphaned parents, dimension rows with no facts): `merge a into b` declares one domain and drops the unmatched rows.\n\n### union / except / intersect (row set operations)\n\n`union(...)` row-stacks self-contained select arms positionally (SQL UNION ALL) into one named rowset (`with combined as union((armA), (armB), ...) -> (out1, out2, ...)`); `except(...)` / `intersect(...)` share that arm shape but are SQL SET operators (DISTINCT rows, null-safe whole-row compare, and except subtracts later arms from the FIRST, so arm order matters). Prefer except over a multi-column `not in` for \"in A but never in B\". Each arm dedups to its own output grain BEFORE stacking, so aggregate INSIDE each arm or the total silently undersums. Examples: trilogy agent-info syntax example union-stack-channels / except-intersect-setops.\n\n## Query structure\n\nTop-level statements first (each `;`-terminated), then a query with clauses in this EXACT order — all optional except select:\n\n```\n<top-level statements>; # import / key / auto / rowset / merge / def / parameter\nwith <name> as # optional: name the select as a reusable rowset (CTE)\nwhere <row condition> # 1. filters INPUT rows, BEFORE aggregation\nselect <col>, <agg> as name, # 2. projection — grouping is AUTOMATIC by the\n # non-aggregated columns; never write GROUP BY\n subset|union|equal join a = b (= c)? # 3. blend models; one clause per key, right after the select list\nby rollup|cube|grouping sets (...) # 4. optional multi-level grouping for the whole select\nhaving <result condition> # 5. filters aggregated/joined RESULTS\norder by <col> asc|desc # 6. sort\nlimit <n>; # 7. cap rows\n```\n\nA rowset (`with <name> as where ... select ...;`) is a standalone statement, evaluated in isolation — it does NOT respond to the consuming query's context. All its outputs are namespaced: output `abc.def` of rowset `foo` is referenced as `foo.abc.def`. Pairing an output with a concept outside the rowset REQUIRES a declared join on the rowset's key (`subset join foo.key = key`); without one the query is disconnected. Alternatively project the concept inside the rowset and filter or select it through the handle (`where foo.cat = 'a'`). Alias every reused expression with `as`.\n\nFull annotated example: trilogy agent-info syntax example query-structure.\n\n### Not SQL — what to never write\n\n- **No FROM, GROUP BY, DISTINCT, SELECT \\*, or SQL-style set operators between selects.** Stack rows with `union(...)`; set difference/intersection with `except(...)`/`intersect(...)`; blend fact models with a scoped join.\n- **Grouping is automatic** by the non-aggregated fields in the SELECT — never write GROUP BY. Aggregates inherit the select's grain wherever they appear (where/select/having). Override per-aggregate with `agg(x) by <dims>`; `by *` aggregates across all data (single row). To weight a related dimension property once per fact row, project it to the fact grain first: `auto row_birth_year <- group(enroll.student.birth_year) by enroll.student_id, enroll.course_id;` then aggregate that.\n- **Output rows are deduplicated to the select grain.** To preserve legitimate duplicates (one row per matching fact), include the fact's grain keys in the select — hidden with the PREFIX `--` if they shouldn't appear in the output.\n- **No `select distinct`.** `count(<key>)` is already distinct because keys are unique; `count(distinct(<property>))` counts distinct values of a non-key property. `distinct(x)` is `group(x) by x`: the distinct values of x, usable anywhere.\n- **Count a COMPOSITE grain with `grain(...)`.** `count(grain(order_id, item.id))` counts order+item combinations; `grain(...)` is NEVER NULL. Never count one column of a multi-key grain — a coarser key counts its own distinct values and undercounts.\n- **An aggregate over a group with no qualifying rows is NULL, never 0.** `sum(x ? cond) by k` yields NULL for groups where nothing matches, so `= 0` matches nothing. Test emptiness with `count(key ? cond) by k = 0` or wrap: `coalesce(sum(x ? cond), 0)`.\n- **`count(1)` is invalid** — a constant does not identify rows. Use the row key, `count(grain(...))`, or `sum(1) by <grain>`.\n- **One-column expression subqueries only.** `(select ...)` is allowed where a scalar or membership set is expected (exactly one projected column; a scalar must also be one row: an aggregate with no `by`, or `limit 1`) — never as a SQL-style FROM-subquery. For related-entity filters prefer the dot-path: `where enrollments.student.state = 'TN'`, not a subselect.\n- **`--` is a HIDDEN-field prefix, not a comment** (it still changes query structure). Comments use `#` only.\n- **Aliases**: always use the full dot-path (`enroll.student.id`); alias every new expression with `as`; a select alias is usable in `having`/`order by` but NOT inside other select expressions or `where`, and must not rename a field back to an existing concept name.\n\n## Filtering\n\n`where` filters input rows before aggregation; `having` filters results after aggregation/windows. The inline filter `x ? cond` filters the immediate prior expression: `sum(credits ? enrolled = true)`.\n\nAggregates/windows in WHERE do not filter each other's inputs. Use inline filters, or a staged `then where` chain: each `then where` stage's aggregates compute over only the rows passing earlier stages (`where x = 5 then where sum(y) > 10` ≡ `where x = 5 and sum(y ? x = 5) > 10`). One rule: the SAME cross-row expression cannot gate two different stages — give one an inline filter.\n\nWindows need all rows in their range: filter AFTER the window (in `having`) rather than in `where` when the window must see the full set.\n\n## Membership (semijoins)\n\n`in` / `not in` cross models without any declared relationship — the semijoin IS a scoped intersection. The right side is a concept or expression (never a subselect); build computed sets with an inline filter: `auto big_zip <- student.zip ? (count(student.id ? student.honors = true) by student.zip) > 10; ... where school.zip in big_zip`.\n\nMembership is IDENTITY matching, not SQL three-valued logic: NULL matches NULL; the result is always TRUE or FALSE (safe as a projected flag); `not in` is the exact complement (a NULL in the set never empties the result). For strict SQL behavior, add `x is not null`.\n\nA fact model contains ALL dimensional members, so `where students.id not in enroll.student_id` is typically a tautology — use `where enroll.id is null` instead. Tuple membership `(a, b) in (m.a, m.b)` requires both right-side concepts to resolve to ONE source (model or rowset); to test against a cross-model pair, stage it through a rowset first. Examples: existence-anti-join, staged-membership.\n\n## Aggregation\n\nAn aggregate without an explicit `by` is RESPONSIVE — its grain is set where it is consumed, not where it is declared (`auto avg_credits <- avg(enroll.credits);` becomes per-department when selected with `enroll.department`). Pin a grain inline: `sum(metric) by dim1, dim2 as name`; `by` accepts bare identifiers or parenthesized expressions: `avg(price) by (substring(phone, 1, 2))`.\n\nMulti-level grouping (`by rollup|cube|grouping sets (...)`) is a clause of the WHOLE select, placed before `having`: it computes every no-explicit-grain aggregate at every level in one pass (`by rollup (d1, d2)` → leaf, per-d1 subtotal, grand total). `grouping(<field>)` = 1 on rolled-up rows — use it to label levels rather than testing for NULL. Full detail: trilogy agent-info syntax example rollup.\n\n## Window functions\n\nDefault to windows for self-referential queries (period-over-period, running totals, rank, share of group). SQL-style syntax: `rank(<key>) over (partition by <g> order by <expr> desc)` (`dense_rank`/`row_number` same shape; multi-key `rank(a, b)`); `sum(x) over (partition by g order by t)` for running totals; `lag(<field>, <offset>) over (...)` / `lead(...)` for offsets (default 1); these walk one row per key of the field (a property walks its key's rows), while `lag(distinct(<field>)) over (...)` walks the field's distinct values (`distinct(x)` = `group(x) by x`, usable anywhere). `partition by` is optional and accepts expressions.\n\n## Data types\n\nScalars: `string`, `bytes`, `bool`, `int`, `bigint`, `float`, `double`, `number`, `numeric(p,s)` (alias `decimal`; use for exact money), `date`, `datetime`, `timestamp`, `geography`, `any`. Composites: `array<T>`, `map<K, V>`, `struct<name: T, ...>`, `enum<T>[v1, v2, ...]`. Cast with `::type` or `cast(x as type)`: `\"2020-01-01\"::date`.\n\n`?` after a type marks it nullable (`property user.middle_name string?;`). `~` prefixing a concept marks the reference as a PARTIAL subset of the concept's domain — on a select item (`~customer.id`) or in `merge a into ~b` (a ⊆ b); it tells the planner this side is not authoritative.\n\n## Expression gotchas\n\n- `x ? cond` binds to a primary — wrap arithmetic: `(a - b) ? cond`, NOT `a - b ? cond`.\n- `a || b` is NULL if either side is NULL; `concat(...)` skips NULL arguments; `concat_ws(sep, ...)` joins with sep, skipping NULLs.\n- Date parts are unquoted: `date_part(d, year)`, never `'year'`; prefer `year(d)`. `date_diff(start, end, unit)` = end − start (a shipping lag is `date_diff(sold_date, ship_date, day)`).\n- Zero-argument functions still take parentheses: `current_date()`.\n- Factor repeated calculations into a `def` macro (invoked `@name(...)`) or reusable `auto` concepts.", Pg = ["aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)"], Fg = [
15452
15452
  "aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)",
15453
15453
  "string: concat|concat_strict|concat_ws(<arg1>, <arg2>, ..., <argN>) ; split|strpos|contains|trim|regexp_contains|format_time|parse_time(<arg1>, <arg2>) ; len|lower|upper|ltrim|rtrim|hex(<arg1>) ; substring|replace|regexp_extract|regexp_replace(<arg1>, <arg2>, <arg3>)",
15454
15454
  "date/time: date|datetime|timestamp|second|minute|hour|day|day_of_week|week|month|quarter|year|month_name|day_name|unix_to_timestamp(<arg1>) ; date_spine(<arg1>, <arg2>) ; current_date|current_datetime()",
@@ -15462,7 +15462,7 @@ var jg = class e extends nt {
15462
15462
  "math: mod|round|log|power|hash(<arg1>, <arg2>) ; abs|sqrt|random|floor|ceil(<arg1>)",
15463
15463
  "conditional/cast: case|simple_case|coalesce|greatest|least(<arg1>, <arg2>, ..., <argN>) ; cast|nullif(<arg1>, <arg2>) ; isnull(<arg1>)",
15464
15464
  "geo: geo_from_text|geo_x|geo_y|geo_centroid(<arg1>) ; geo_point|geo_distance(<arg1>, <arg2>) ; geo_transform(<arg1>, <arg2>, <arg3>)",
15465
- "other: is_not_distinct(<arg1>, <arg2>)"
15465
+ "other: is_not_distinct(<arg1>, <arg2>) ; prior(<arg1>) ; grain_pin(<arg1>, <arg2>, ..., <argN>)"
15466
15466
  ], Ig = [
15467
15467
  "string",
15468
15468
  "bytes",
@@ -15520,13 +15520,13 @@ var jg = class e extends nt {
15520
15520
  name: "existence-anti-join",
15521
15521
  title: "Existence / anti-join across models",
15522
15522
  summary: "keeping or excluding rows by whether a key matches another model",
15523
- body: "# `in` / `not in` against another model's key is the semi-join / anti-join\n# idiom - no manual JOIN. Use it instead of `count(...) is null` hacks.\n# Membership is IDENTITY matching and total: NULL matches NULL, the result is\n# never NULL, and `not in` is the EXACT complement of `in` - a NULL in the set\n# does not empty an anti-join (no SQL NOT-IN footgun), and NULL-keyed rows land\n# in exactly one of the two sides. For strict SQL parity, filter NULLs\n# explicitly (`where key is not null and key in other.key`).\nimport students as students;\nimport enrollments as enroll;\n\n# semi-join: majors with at least one student enrolled in 'Biology 101'\nauto bio_students <- enroll.student_id ? enroll.course = 'Biology 101';\nwhere students.id in bio_students\nselect students.major, count(students.id) as enrolled_students;\n\n# anti-join: students who never enrolled in ANY course\nwhere students.id not in enroll.student_id\nselect students.major, count(students.id) as never_enrolled;\n\n# NULLABLE FLAG: a flag that is true only when a match exists and NULL otherwise\n# (e.g. `enroll.completed` set only for graded rows) needs `is null` for the\n# \"never matched\" case - `not flag` evaluates NULL→dropped, losing those rows.\nwhere enroll.completed is null\nselect enroll.student_id;\n\n# COMPOSITE (multi-column) membership: to test a MULTI-PART key against a set,\n# wrap both sides in matching tuples - `(a, b) in (set.a, set.b)`. This matches\n# ROW-WISE (the whole tuple must appear as one row of the set), so it is NOT the\n# same as `a in set.a and b in set.b`, which matches the parts independently and\n# lets unrelated cross-pairs through. Build the set as a `rowset` selecting the\n# key columns, then test the tuple with `in` / `not in`. Prefer this over\n# concatenating the parts into a single string key.\nrowset multi_course_pairs <- select\n enroll.department as dept,\n enroll.course as course,\nhaving count_distinct(enroll.year) > 1; # (dept, course) offered in >1 year\n\nwhere (enroll.department, enroll.course)\n in (multi_course_pairs.dept, multi_course_pairs.course)\nselect enroll.student_id, count(enroll.id) as enrollments;\n"
15523
+ body: "# `in` / `not in` against another model's key is the semi-join / anti-join\n# idiom - no manual JOIN. Use it instead of `count(...) is null` hacks.\n# Membership is IDENTITY matching and total: NULL matches NULL, the result is\n# never NULL, and `not in` is the EXACT complement of `in` - a NULL in the set\n# does not empty an anti-join (no SQL NOT-IN footgun), and NULL-keyed rows land\n# in exactly one of the two sides. For strict SQL parity, filter NULLs\n# explicitly (`where key is not null and key in other.key`).\nimport students as students;\nimport enrollments as enroll;\n\n# semi-join: majors with at least one student enrolled in 'Biology 101'\nauto bio_students <- enroll.student_id ? enroll.course = 'Biology 101';\nwhere students.id in bio_students\nselect students.major, count(students.id) as enrolled_students;\n\n# anti-join: students who never enrolled in ANY course\nwhere students.id not in enroll.student_id\nselect students.major, count(students.id) as never_enrolled;\n\n# NULLABLE FLAG: a flag that is true only when a match exists and NULL otherwise\n# (e.g. `enroll.completed` set only for graded rows) needs `is null` for the\n# \"never matched\" case - `not flag` evaluates NULL→dropped, losing those rows.\nwhere enroll.completed is null\nselect enroll.student_id;\n\n# COMPOSITE (multi-column) membership: to test a MULTI-PART key against a set,\n# wrap both sides in matching tuples - `(a, b) in (set.a, set.b)`. This matches\n# ROW-WISE (the whole tuple must appear as one row of the set), so it is NOT the\n# same as `a in set.a and b in set.b`, which matches the parts independently and\n# lets unrelated cross-pairs through. Build the set as a `rowset` selecting the\n# key columns, then test the tuple with `in` / `not in`. Prefer this over\n# concatenating the parts into a single string key.\nrowset multi_course_pairs <- select\n enroll.department as dept,\n enroll.course as course,\nhaving count(distinct(enroll.year)) > 1; # (dept, course) offered in >1 year\n\nwhere (enroll.department, enroll.course)\n in (multi_course_pairs.dept, multi_course_pairs.course)\nselect enroll.student_id, count(enroll.id) as enrollments;\n"
15524
15524
  },
15525
15525
  {
15526
15526
  name: "filtered-aggregate",
15527
15527
  title: "Filtered aggregate - aggregate only the rows matching a condition",
15528
15528
  summary: "aggregating only the rows matching a condition, and counting rows correctly",
15529
- body: "# `?` is a local filter - a filter that applies only to the prior expression-\n# several differently-filtered measures can share one select. Add `by <grain>`\n# to pin an aggregate to a grain other than the select's.\nimport iris as iris;\n\nselect\n iris.species,\n avg(iris.petal_length) as avg_petal,\n avg(iris.petal_length ? iris.petal_width > 1.5) as avg_petal_wide_only,\n count(iris.petal_length ? iris.sepal_length > 6.0) as long_sepal_count,\norder by iris.species asc;\n\n# COUNTING ROWS: `count(k ? cond)` counts DISTINCT values of k - `count` is\n# distinct-by-argument. To count matching ROWS, count the model's unique grain /\n# row key (often named `row_key`), NOT a non-unique sub-key. e.g. counting line\n# items by `count(line_number ? cond)` collapses to the number of distinct line\n# numbers (1..7) and undercounts - use `count(row_key ? cond)`.\n\n# COMPOSITE GRAIN: when SEVERAL keys together identify the thing you are counting,\n# name them with `grain(...)` instead of picking one:\n# count(grain(sales.order_id, sales.item_id)) # order+item lines\n# count_distinct(grain(cust.first_name, cust.last_name, sales.date))\n# `grain(...)` is NEVER NULL, so a combination with a missing member (a customer\n# with no last name) still counts - whereas `count(last_name)` silently drops it.\n# It composes with `?` like any expression: count(grain(a, b) ? cond).\n"
15529
+ body: "# `?` is a local filter - a filter that applies only to the prior expression-\n# several differently-filtered measures can share one select. Add `by <grain>`\n# to pin an aggregate to a grain other than the select's.\nimport iris as iris;\n\nselect\n iris.species,\n avg(iris.petal_length) as avg_petal,\n avg(iris.petal_length ? iris.petal_width > 1.5) as avg_petal_wide_only,\n count(iris.petal_length ? iris.sepal_length > 6.0) as long_sepal_count,\norder by iris.species asc;\n\n# COUNTING ROWS: `count(k ? cond)` counts DISTINCT values of k - `count` is\n# distinct-by-argument. To count matching ROWS, count the model's unique grain /\n# row key (often named `row_key`), NOT a non-unique sub-key. e.g. counting line\n# items by `count(line_number ? cond)` collapses to the number of distinct line\n# numbers (1..7) and undercounts - use `count(row_key ? cond)`.\n\n# COMPOSITE GRAIN: when SEVERAL keys together identify the thing you are counting,\n# name them with `grain(...)` instead of picking one:\n# count(grain(sales.order_id, sales.item_id)) # order+item lines\n# count(distinct(grain(cust.first_name, cust.last_name, sales.date)))\n# `grain(...)` is NEVER NULL, so a combination with a missing member (a customer\n# with no last name) still counts - whereas `count(last_name)` silently drops it.\n# It composes with `?` like any expression: count(grain(a, b) ? cond).\n"
15530
15530
  },
15531
15531
  {
15532
15532
  name: "nested-aggregate-group-average",
@@ -15550,7 +15550,7 @@ var jg = class e extends nt {
15550
15550
  name: "query-structure",
15551
15551
  title: "Full query structure - every clause and where it goes (+ rowsets)",
15552
15552
  summary: "the exact clause order of a query, and how to define a rowset",
15553
- body: "# Every Trilogy query has the SAME fixed skeleton. Top-level statements come\n# first (each ends with `;`), then ONE query whose clauses appear in this exact\n# order - each clause is OPTIONAL except `select`:\n#\n# <top-level statements>; # import / key / property / auto / metric /\n# # rowset / merge / def / datasource / parameter\n# where <row condition> # 1. filter INPUT rows (BEFORE aggregation)\n# select <col>, <agg> as name, # 2. the projection - grouping is AUTOMATIC by the\n# # non-aggregated columns (never write GROUP BY)\n# <subset|union join a = b (=c)?>* # 3. blend models (zero or more joins; one per concept RIGHT AFTER the\n# # select list - the SQL-like spot)\n# having <result condition> # 4. filter on an AGGREGATED / joined RESULT\n# order by <col> asc|desc # 5. sort\n# limit <n> # 6. cap rows\nimport enrollments as enroll;\nimport students as students;\n\n# --- TOP-LEVEL DEFINITIONS (reusable; all live ABOVE the query) -------------\n# An `auto`/`metric`/`property`/`key` defines a field. It expands inline wherever\n# referenced (a macro), re-evaluated in the referencing query's scope.\nauto completed_credits <- sum(enroll.credits ? enroll.completed = true);\n\n# A ROWSET is a NAMED select: `rowset <name> <- <a full select>;`. It runs as its\n# own scoped query (its own where/select/having) and exposes its outputs as\n# `<name>.<col>`. ALIAS every column you reuse downstream with `as`.\nrowset dept_totals <- select\n enroll.department as department,\n sum(enroll.credits) as total_credits,\n;\n\n# --- THE QUERY - clauses in the fixed order above ---------------------------\nwhere enroll.year >= 2015 # 1. WHERE: per-row filter, before aggregation\nselect # 2. SELECT: grouped automatically by students.major\n students.major,\n count(enroll.id) as enrollments,\n --completed_credits, # a leading `--` HIDES a column from the output\nsubset join enroll.student_id = students.id # 3. JOIN: blend students onto enrollments (after the select list)\nhaving completed_credits > 0 # 4. HAVING: condition on an aggregated RESULT\norder by enrollments desc nulls first # 5. ORDER BY\nlimit 100; # 6. LIMIT\n\n# Reference a rowset's outputs as `<rowset>.<output>` in a later query.\nselect\n dept_totals.department,\n dept_totals.total_credits,\norder by dept_totals.total_credits desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - Joins sit RIGHT AFTER the select list (the SQL-like, preferred spot). They\n# may also appear before `select` - both parse - but keep them after the\n# select list for consistency.\n# - `where` filters INPUT rows (before aggregation). To filter on something only\n# known AFTER the join / aggregation (e.g. comparing two joined rowsets'\n# counts), select that value (hide it with `--`) and test it in `having` -\n# `having` is the post-aggregation/output filter. There is only ONE `where`.\n# - Output rows dedupe to the select grain. To keep legitimate duplicates (one\n# row per matching fact), select the fact's grain keys - hide with `--` if\n# they shouldn't appear in the output.\n# - No FROM, GROUP BY, DISTINCT, SELECT *, subqueries, or SQL-style set/JOIN\n# operators. Grouping is automatic; blend with the scoped `join` above.\n"
15553
+ body: "# Every Trilogy query has the SAME fixed skeleton. Top-level statements come\n# first (each ends with `;`), then ONE query whose clauses appear in this exact\n# order - each clause is OPTIONAL except `select`:\n#\n# <top-level statements>; # import / key / property / auto / metric /\n# # rowset / merge / def / datasource / parameter\n# where <row condition> # 1. filter INPUT rows (BEFORE aggregation)\n# select <col>, <agg> as name, # 2. the projection - grouping is AUTOMATIC by the\n# # non-aggregated columns (never write GROUP BY)\n# <subset|union|equal join a = b (=c)?>* # 3. blend models (zero or more joins; one per concept RIGHT AFTER the\n# # select list - the SQL-like spot)\n# having <result condition> # 4. filter on an AGGREGATED / joined RESULT\n# order by <col> asc|desc # 5. sort\n# limit <n> # 6. cap rows\nimport enrollments as enroll;\nimport students as students;\n\n# --- TOP-LEVEL DEFINITIONS (reusable; all live ABOVE the query) -------------\n# An `auto`/`metric`/`property`/`key` defines a field. It expands inline wherever\n# referenced (a macro), re-evaluated in the referencing query's scope.\nauto completed_credits <- sum(enroll.credits ? enroll.completed = true);\n\n# A ROWSET is a NAMED select: `rowset <name> <- <a full select>;`. It runs as its\n# own scoped query (its own where/select/having) and exposes its outputs as\n# `<name>.<col>`. ALIAS every column you reuse downstream with `as`.\nrowset dept_totals <- select\n enroll.department as department,\n sum(enroll.credits) as total_credits,\n;\n\n# --- THE QUERY - clauses in the fixed order above ---------------------------\nwhere enroll.year >= 2015 # 1. WHERE: per-row filter, before aggregation\nselect # 2. SELECT: grouped automatically by students.major\n students.major,\n count(enroll.id) as enrollments,\n --completed_credits, # a leading `--` HIDES a column from the output\nsubset join enroll.student_id = students.id # 3. JOIN: blend students onto enrollments (after the select list)\nhaving completed_credits > 0 # 4. HAVING: condition on an aggregated RESULT\norder by enrollments desc nulls first # 5. ORDER BY\nlimit 100; # 6. LIMIT\n\n# Reference a rowset's outputs as `<rowset>.<output>` in a later query.\nselect\n dept_totals.department,\n dept_totals.total_credits,\norder by dept_totals.total_credits desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - Joins sit RIGHT AFTER the select list (the SQL-like, preferred spot). They\n# may also appear before `select` - both parse - but keep them after the\n# select list for consistency.\n# - `where` filters INPUT rows (before aggregation). To filter on something only\n# known AFTER the join / aggregation (e.g. comparing two joined rowsets'\n# counts), select that value (hide it with `--`) and test it in `having` -\n# `having` is the post-aggregation/output filter. There is only ONE `where`.\n# - Output rows dedupe to the select grain. To keep legitimate duplicates (one\n# row per matching fact), select the fact's grain keys - hide with `--` if\n# they shouldn't appear in the output.\n# - No FROM, GROUP BY, DISTINCT, SELECT *, subqueries, or SQL-style set/JOIN\n# operators. Grouping is automatic; blend with the scoped `join` above.\n"
15554
15554
  },
15555
15555
  {
15556
15556
  name: "rank-over-rollup",
@@ -15558,6 +15558,12 @@ var jg = class e extends nt {
15558
15558
  summary: "rank rollup subtotals/leaves with a SINGLE `rank(a,b) over (partition by level, parent ...)` - not separate ranks per level",
15559
15559
  body: "# When ranking the rows of a ROLLUP within each hierarchy level, use ONE\n# multi-key window over the rollup output. `grouping()` gives the level; the\n# window partitions by (level, parent) so leaves rank within their parent and\n# subtotals rank among themselves - all in one pass. Do NOT write separate rank\n# concepts per level (they mis-rank across the wrong row set). The aggregates and\n# grouping() level concepts are plain - the select's `by rollup (…)` clause stamps\n# the grouping onto all of them.\nimport enrollments as enroll;\n\nauto total <- count(enroll.id);\nauto g_course <- grouping(enroll.course);\nauto g_year <- grouping(enroll.year);\nauto level <- g_course + g_year; # 0 = leaf, 1 = course subtotal, 2 = grand total\nauto parent <- case when g_year = 0 then enroll.course else null end;\nauto rnk <- rank(enroll.course, enroll.year)\n over (partition by level, parent order by total desc);\n\nselect\n enroll.course,\n enroll.year,\n total,\n level,\n --parent,\n rnk,\nby rollup (enroll.course, enroll.year)\norder by level desc nulls first, parent asc nulls first, rnk asc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTE - a composite aggregate over a rollup (a difference/ratio of sums) needs\n# NO special handling: `sum(profit) - sum(loss) as net` rolls up both operands\n# to the same levels, because `by rollup (…)` is a property of the whole select,\n# not of one aggregate.\n"
15560
15560
  },
15561
+ {
15562
+ name: "recursive-rollup",
15563
+ title: "Roll a hierarchy up to its roots with `recurse_edge`",
15564
+ summary: "walk a parent pointer (folder tree, org chart, process tree) to the top of each chain with `recurse_edge(key, parent)`, then aggregate per root",
15565
+ body: "# `recurse_edge(child, parent)` follows a parent pointer to the top of its chain.\n# For each row it returns the ROOT reached from that row (the row itself when its\n# parent is null), so grouping by it rolls every descendant up to its root.\n# Define it once as an `auto` concept in the model; queries then use it like any\n# other field.\nkey folder_id int;\nproperty folder_id.parent_id int?; # null at a top-level folder\nproperty folder_id.name string;\nproperty folder_id.size_bytes int;\nauto root_folder_id <- recurse_edge(folder_id, parent_id);\n\ndatasource folders (folder_id: folder_id, parent_id: parent_id, name: name, size_bytes: size_bytes)\ngrain (folder_id)\nquery '''\nselect 1 as folder_id, null as parent_id, 'home' as name, 10 as size_bytes\nunion all select 2, 1, 'docs', 200\nunion all select 3, 2, 'drafts', 50\nunion all select 4, null, 'tmp', 5\nunion all select 5, 4, 'cache', 900\n''';\n\n# each top-level folder with everything below it\nselect\n root_folder_id,\n count(folder_id) as folders,\n sum(size_bytes) as total_bytes\norder by total_bytes desc;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - The argument ORDER matters: the key first, then its parent pointer.\n# - A null parent ends the walk, and so does a parent with no row of its own;\n# the value is the last row reached. Stop a walk early by nulling the parent\n# in the data (e.g. at a boundary you do not want to roll across).\n# - To show the ROOT's own attributes (its name) next to the rollup, import the\n# model a second time under another name and merge the recursive concept into\n# its key: `import folders as root; merge root_folder_id into root.folder_id;`\n# then `select root.name, sum(size_bytes) ...`.\n"
15566
+ },
15561
15567
  {
15562
15568
  name: "rollup",
15563
15569
  title: "Subtotals + grand total in one pass with `by rollup (…)`",
@@ -15572,9 +15578,9 @@ var jg = class e extends nt {
15572
15578
  },
15573
15579
  {
15574
15580
  name: "scoped-join",
15575
- title: "Blend two models in one query with a scoped subset/union join",
15581
+ title: "Blend two models in one query with a scoped subset/union/equal join",
15576
15582
  summary: "blending a second fact model or rowset into one query",
15577
- body: "# A query-scoped `join` blends a second model into ONE select without editing the\n# model files (the query-local equivalent of `merge`). A join DECLARES how the\n# two keys' value domains relate - it never drops rows (rendering is always\n# row-preserving; restrict rows with explicit `is not null` filters):\n# subset join a = b -> declares a's values are contained in b's (a is the\n# narrow side, b superset, all values in a in b)\n# union join a = b -> neither domain contains the other; the key coalesces\n# and unmatched rows from EITHER side are kept\n# Place the clause(s) RIGHT AFTER the `select` list (the SQL-like spot).\nimport enrollments as enroll;\nimport students as students;\n\n# (1) SINGLE-KEY blend: bring students onto enrollments by the shared student\n# key. Enrollment student ids are a SUBSET of all student ids, so declare that;\n# majors with no enrollments are preserved with a NULL count.\nselect\n students.major,\n count(enroll.id) as enrollments,\nsubset join enroll.student_id = students.id\norder by enrollments desc nulls first\nlimit 100;\n\n# The select plus its join clause(s) is ONE statement, so it must end with `;`\n# even when no `order by`/`limit` follows the join - a quick probe that stops\n# right after the join clause still needs the terminator:\nselect\n students.major,\n count(enroll.id) as enrollments,\nsubset join enroll.student_id = students.id;\n\n# ---------------------------------------------------------------------------\n# (2) MULTI-KEY blend - JOIN ON THE FULL GRAIN. When facts share a COMPOSITE\n# grain, write ONE join clause per key. `trilogy explore` shows each fact's grain\n# as `@<k1, k2>` (e.g. a sales/returns fact keyed `@<department, course>`);\n# matching only ONE key fans out and DOUBLE-COUNTS. Two rowsets, joined on both\n# of their shared keys:\nrowset completed_by <- where enroll.completed = true\n select enroll.department as dept, enroll.course as course, count(enroll.id) as completed_cnt;\nrowset incomplete_by <- where enroll.completed = false\n select enroll.department as dept, enroll.course as course, count(enroll.id) as incomplete_cnt;\nrowset null_completed <- where enroll.completed is null\n select enroll.department as dept, enroll.course as course, count(enroll.id) as null_completed_cnt;\n\nselect\n completed_by.dept,\n completed_by.course,\n completed_by.completed_cnt,\n incomplete_by.incomplete_cnt,\n null_completed.null_completed_cnt,\nunion join completed_by.dept = incomplete_by.dept = null_completed.dept\nunion join completed_by.course = incomplete_by.course = null_completed.course\norder by completed_by.dept asc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# (3) SELF-PAIR across two periods/sets (period-over-period) - a useful shape\n# for \"this year vs last year\": build one rowset per period (each aggregated to\n# the SAME keys), filter to restrict to output to one side or the other\n# in having clause.\n\nrowset y2020 <- where enroll.year = 2020 select enroll.department as dept, count(enroll.id) as cnt;\nrowset y2021 <- where enroll.year = 2021 select enroll.department as dept, count(enroll.id) as cnt;\n\nselect\n y2020.dept,\n y2020.cnt as cnt_2020,\n y2021.cnt as cnt_2021,\n y2021.cnt - y2020.cnt as yoy_diff,\nunion join y2020.dept = y2021.dept\nhaving y2021.cnt is not null\norder by yoy_diff desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# (4) EXPRESSION join keys - a key may be ANY expression (arithmetic, an\n# aggregate, a window), not just a bare field. Here each year is matched to the\n# PRIOR year by joining on an OFFSET key (`prev_year.yr + 1`), so the prior\n# year's count lands on the current row without a self-merge. Only `=` equality\n# is supported (a key below comparison level - wrap a comparison in parens).\nrowset cur_year <- select enroll.year as yr, count(enroll.id) as cnt;\nrowset prev_year <- select enroll.year as yr, count(enroll.id) as cnt;\n\nselect\n cur_year.yr,\n cur_year.cnt as this_year,\n prev_year.cnt as prior_year,\nunion join cur_year.yr = prev_year.yr + 1\norder by cur_year.yr asc\nlimit 100;\n\n# NOTES:\n# - JOIN ON THE FULL GRAIN: one `join` clause per key in the two facts' shared\n# `@<...>` grain. Under-joining (one key of a multi-key grain) can cause\n# duplication. Chain `= c` to also pull a base key into the join\n# group so its properties stay reachable (`union join a.k = b.k = base.k`).\n# - Stacked clauses of the SAME type can share one prefix via `and`:\n# `union join a.k1 = b.k1 and a.k2 = b.k2` == two stacked `union join` clauses.\n# `and` joins distinct KEY-EQUALITY groups (not filters); `= c` chains keys into\n# ONE group - both compose: `union join a.k = b.k = base.k and a.k2 = b.k2`.\n# A COMMA never separates join groups (it is a select-list separator only):\n# `union join a.k1 = b.k1, a.k2 = b.k2` is an error - use `and`.\n# - Terminate the statement with `;` after the LAST clause, whatever that clause\n# is (join, `having`, `order by`, `limit`).\n# - A join key may be ANY expression (a computed/offset key, an aggregate, a\n# window), not only a bare field - see (4). Only `=` equality is supported.\n# - Joins NEVER drop rows: for an intersection (rows present in BOTH sides),\n# add explicit conditions - `where <side_a attr> is not null and <side_b\n# attr> is not null`. A one-sided `is not null` keeps the other side's\n# exclusive rows. Either spelling reads as \"did this side match?\": the\n# side's own JOIN KEY (`where a.k is not null`, tested per side even though\n# the projected key is the coalesced group axis) or any of its measures /\n# attributes (`where a.cnt is not null`). Same in `having`.\n# - `left`/`full`/`inner`/`right`/`cross join` are NOT valid query-scoped joins\n# (they error) - only `subset`/`union`. Map an old `left join a = b` to\n# `subset join b = a` (swap operands) and `full join a = b` to\n# `union join a = b`. A union key-group must be ALL union (can't mix with\n# subset on the SAME key; `union join a = b = c` chains one union group);\n"
15583
+ body: "# A query-scoped `join` blends a second model into ONE select without editing the\n# model files (the query-local equivalent of `merge`). A join DECLARES how the\n# two keys' value domains relate - it never drops rows (rendering is always\n# row-preserving; restrict rows with explicit `is not null` filters):\n# subset join a = b -> declares a's values are contained in b's (a is the\n# narrow side, b superset, all values in a in b)\n# union join a = b -> neither domain contains the other; the key coalesces\n# and unmatched rows from EITHER side are kept\n# equal join a = b -> ONE domain: a is an alias of b, every value on either\n# side is on the other (`merge a into b` scoped to this\n# query); the join renders inner. Two rowsets ranking\n# the same rows two ways pair this way.\n# `merge subset|union|equal a = b;` declares the same relation for every query\n# in the file.\n# Place the clause(s) RIGHT AFTER the `select` list (the SQL-like spot).\nimport enrollments as enroll;\nimport students as students;\n\n# (1) SINGLE-KEY blend: bring students onto enrollments by the shared student\n# key. Enrollment student ids are a SUBSET of all student ids, so declare that;\n# majors with no enrollments are preserved with a NULL count.\nselect\n students.major,\n count(enroll.id) as enrollments,\nsubset join enroll.student_id = students.id\norder by enrollments desc nulls first\nlimit 100;\n\n# The select plus its join clause(s) is ONE statement, so it must end with `;`\n# even when no `order by`/`limit` follows the join - a quick probe that stops\n# right after the join clause still needs the terminator:\nselect\n students.major,\n count(enroll.id) as enrollments,\nsubset join enroll.student_id = students.id;\n\n# ---------------------------------------------------------------------------\n# (2) MULTI-KEY blend - JOIN ON THE FULL GRAIN. When facts share a COMPOSITE\n# grain, write ONE join clause per key. `trilogy explore` shows each fact's grain\n# as `@<k1, k2>` (e.g. a sales/returns fact keyed `@<department, course>`);\n# matching only ONE key fans out and DOUBLE-COUNTS. Two rowsets, joined on both\n# of their shared keys:\nrowset completed_by <- where enroll.completed = true\n select enroll.department as dept, enroll.course as course, count(enroll.id) as completed_cnt;\nrowset incomplete_by <- where enroll.completed = false\n select enroll.department as dept, enroll.course as course, count(enroll.id) as incomplete_cnt;\nrowset null_completed <- where enroll.completed is null\n select enroll.department as dept, enroll.course as course, count(enroll.id) as null_completed_cnt;\n\nselect\n completed_by.dept,\n completed_by.course,\n completed_by.completed_cnt,\n incomplete_by.incomplete_cnt,\n null_completed.null_completed_cnt,\nunion join completed_by.dept = incomplete_by.dept = null_completed.dept\nunion join completed_by.course = incomplete_by.course = null_completed.course\norder by completed_by.dept asc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# (3) SELF-PAIR across two periods/sets (period-over-period) - a useful shape\n# for \"this year vs last year\": build one rowset per period (each aggregated to\n# the SAME keys), filter to restrict to output to one side or the other\n# in having clause.\n\nrowset y2020 <- where enroll.year = 2020 select enroll.department as dept, count(enroll.id) as cnt;\nrowset y2021 <- where enroll.year = 2021 select enroll.department as dept, count(enroll.id) as cnt;\n\nselect\n y2020.dept,\n y2020.cnt as cnt_2020,\n y2021.cnt as cnt_2021,\n y2021.cnt - y2020.cnt as yoy_diff,\nunion join y2020.dept = y2021.dept\nhaving y2021.cnt is not null\norder by yoy_diff desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# (4) EXPRESSION join keys - a key may be ANY expression (arithmetic, an\n# aggregate, a window), not just a bare field. Here each year is matched to the\n# PRIOR year by joining on an OFFSET key (`prev_year.yr + 1`), so the prior\n# year's count lands on the current row without a self-merge. Only `=` equality\n# is supported (a key below comparison level - wrap a comparison in parens).\nrowset cur_year <- select enroll.year as yr, count(enroll.id) as cnt;\nrowset prev_year <- select enroll.year as yr, count(enroll.id) as cnt;\n\nselect\n cur_year.yr,\n cur_year.cnt as this_year,\n prev_year.cnt as prior_year,\nunion join cur_year.yr = prev_year.yr + 1\norder by cur_year.yr asc\nlimit 100;\n\n# NOTES:\n# - JOIN ON THE FULL GRAIN: one `join` clause per key in the two facts' shared\n# `@<...>` grain. Under-joining (one key of a multi-key grain) can cause\n# duplication. Chain `= c` to also pull a base key into the join\n# group so its properties stay reachable (`union join a.k = b.k = base.k`).\n# - Stacked clauses of the SAME type can share one prefix via `and`:\n# `union join a.k1 = b.k1 and a.k2 = b.k2` == two stacked `union join` clauses.\n# `and` joins distinct KEY-EQUALITY groups (not filters); `= c` chains keys into\n# ONE group - both compose: `union join a.k = b.k = base.k and a.k2 = b.k2`.\n# A COMMA never separates join groups (it is a select-list separator only):\n# `union join a.k1 = b.k1, a.k2 = b.k2` is an error - use `and`.\n# - Terminate the statement with `;` after the LAST clause, whatever that clause\n# is (join, `having`, `order by`, `limit`).\n# - A join key may be ANY expression (a computed/offset key, an aggregate, a\n# window), not only a bare field - see (4). Only `=` equality is supported.\n# - Joins NEVER drop rows: for an intersection (rows present in BOTH sides),\n# add explicit conditions - `where <side_a attr> is not null and <side_b\n# attr> is not null`. A one-sided `is not null` keeps the other side's\n# exclusive rows. Either spelling reads as \"did this side match?\": the\n# side's own JOIN KEY (`where a.k is not null`, tested per side even though\n# the projected key is the coalesced group axis) or any of its measures /\n# attributes (`where a.cnt is not null`). Same in `having`.\n# - `left`/`full`/`inner`/`right`/`cross join` are NOT valid query-scoped joins\n# (they error) - only `subset`/`union`/`equal`. Map an old `left join a = b` to\n# `subset join b = a` (swap operands) and `full join a = b` to\n# `union join a = b`. A union key-group must be ALL union (can't mix with\n# subset on the SAME key; `union join a = b = c` chains one union group);\n"
15578
15584
  },
15579
15585
  {
15580
15586
  name: "set-intersect-difference",
@@ -15586,7 +15592,7 @@ var jg = class e extends nt {
15586
15592
  name: "staged-membership",
15587
15593
  title: "Stage a coarse-grain membership set, then filter a fine-grain query",
15588
15594
  summary: "filtering a fine-grain query by a set computed at a coarser grain",
15589
- body: "# When a query filters on a condition computed at a COARSER grain (e.g. \"items\n# sold in all 3 channels\", \"courses offered in more than one year\"), compute that\n# membership set in its own `rowset` and filter the fine-grain query with `in`.\n# Staging keeps the coarse aggregate from entangling with the fine-grain\n# aggregates (which otherwise re-aggregate / over-count).\nimport enrollments as enroll;\n\n# Coarse stage: courses offered in more than one year.\nrowset multi_year <- select\n enroll.course as course,\n --count_distinct(enroll.year) as year_count,\nhaving year_count > 1\n;\n\n# Fine stage: enrollments per course, only for those courses.\nwhere enroll.course in multi_year.course\nselect\n enroll.course,\n count(enroll.id) as enrollments,\norder by enrollments desc\nlimit 100;\n"
15595
+ body: "# When a query filters on a condition computed at a COARSER grain (e.g. \"items\n# sold in all 3 channels\", \"courses offered in more than one year\"), compute that\n# membership set in its own `rowset` and filter the fine-grain query with `in`.\n# Staging keeps the coarse aggregate from entangling with the fine-grain\n# aggregates (which otherwise re-aggregate / over-count).\nimport enrollments as enroll;\n\n# Coarse stage: courses offered in more than one year.\nrowset multi_year <- select\n enroll.course as course,\n --count(distinct(enroll.year)) as year_count,\nhaving year_count > 1\n;\n\n# Fine stage: enrollments per course, only for those courses.\nwhere enroll.course in multi_year.course\nselect\n enroll.course,\n count(enroll.id) as enrollments,\norder by enrollments desc\nlimit 100;\n"
15590
15596
  },
15591
15597
  {
15592
15598
  name: "union-stack-channels",
@@ -17531,7 +17537,36 @@ var bv = (e, t, n) => {
17531
17537
  }, kv = (e, t) => {
17532
17538
  let n = t.filter((t) => t.name !== e.xField && t.name !== e.yField);
17533
17539
  return n.length > 0 ? n[0] : t[0];
17534
- }, Av = (e, t, n) => {
17540
+ }, Av = [
17541
+ "xField",
17542
+ "yField",
17543
+ "yField2",
17544
+ "colorField",
17545
+ "sizeField",
17546
+ "groupField",
17547
+ "trellisField",
17548
+ "trellisRowField",
17549
+ "geoField",
17550
+ "annotationField"
17551
+ ], jv = ["trellisField", "trellisRowField"], Mv = (e, t) => {
17552
+ let n = e[t];
17553
+ return typeof n == "string" && n !== "" ? n : void 0;
17554
+ }, Nv = (e, t) => {
17555
+ let n = { ...e };
17556
+ for (let [e, r] of Object.entries(t)) r != null && (typeof r != "string" || r !== "") && (n[e] = r);
17557
+ let r = /* @__PURE__ */ new Set();
17558
+ for (let e of Av) {
17559
+ let n = Mv(t, e);
17560
+ n && r.add(n);
17561
+ }
17562
+ if (r.size === 0) return n;
17563
+ for (let e of Av) {
17564
+ if (Mv(t, e)) continue;
17565
+ let i = Mv(n, e);
17566
+ i && (jv.includes(e) || r.has(i)) && (e in t ? n[e] = "" : delete n[e]);
17567
+ }
17568
+ return n;
17569
+ }, Pv = (e, t, n) => {
17535
17570
  let r = {
17536
17571
  showTitle: !1,
17537
17572
  hideLegend: !1
@@ -17598,7 +17633,7 @@ var bv = (e, t, n) => {
17598
17633
  n.length > 0 && (r.trellisRowField = n[0].name), n.length > 1 && (r.trellisField = n[1].name);
17599
17634
  }
17600
17635
  return r;
17601
- }, jv = (e, t) => {
17636
+ }, Fv = (e, t) => {
17602
17637
  let n = $("numeric", t), r = $("categorical", t), i = $("temporal", t), a = $("latitude", t), o = $("longitude", t), s = $("geographic", t), c = [];
17603
17638
  if (r.length > 0 && c.push("beeswarm"), n.length === 0) return (a.length > 0 && o.length > 0 || s.length > 0) && c.push("geo-map"), c.push("headline"), c;
17604
17639
  let l = r.filter((e) => hv(e));
@@ -17608,13 +17643,13 @@ var bv = (e, t, n) => {
17608
17643
  c.push("bar"), c.push("barh"), c.push("donut");
17609
17644
  }
17610
17645
  return n.length >= 2 && c.push("point"), r.length >= 2 && n.length > 0 && c.push("heatmap"), n.length > 0 && (c.push("boxplot"), c.push("headline")), (a.length > 0 && o.length > 0 || s.length > 0) && c.push("geo-map"), c.filter((e) => cv.map((e) => e.value).includes(e));
17611
- }, Mv = (e) => Ov(e, "latitude") ? "latitude" : Ov(e, "longitude") ? "longitude" : e.traits?.some((e) => e.endsWith("state") || e.endsWith("state_short") || e.endsWith("country")) ? "geographic" : Tv(e) ? "temporal" : hv(e) ? "categorical" : Sv(e) ? "numeric" : Ev(e) ? "categorical" : "other", Nv = (e) => {
17646
+ }, Iv = (e) => Ov(e, "latitude") ? "latitude" : Ov(e, "longitude") ? "longitude" : e.traits?.some((e) => e.endsWith("state") || e.endsWith("state_short") || e.endsWith("country")) ? "geographic" : Tv(e) ? "temporal" : hv(e) ? "categorical" : Sv(e) ? "numeric" : Ev(e) ? "categorical" : "other", Lv = (e) => {
17612
17647
  let t = [];
17613
17648
  return e.forEach((e) => {
17614
- t.push(`${e.name} (${Mv(e)})`);
17649
+ t.push(`${e.name} (${Iv(e)})`);
17615
17650
  }), t.length > 0 ? t.join(", ") : "no fields";
17616
- }, Pv = (e, t, n) => {
17617
- let r = jv(e, t), a = [];
17651
+ }, Rv = (e, t, n) => {
17652
+ let r = Fv(e, t), a = [];
17618
17653
  if (n.layers && n.layers.length > 0) {
17619
17654
  let e = [], a = Array.from(t.keys());
17620
17655
  return n.layers.forEach((n, r) => {
@@ -17633,16 +17668,16 @@ var bv = (e, t, n) => {
17633
17668
  };
17634
17669
  }
17635
17670
  if (!r.includes(n.chartType)) {
17636
- let i = Nv(t);
17671
+ let i = Lv(t);
17637
17672
  return {
17638
17673
  valid: !1,
17639
17674
  chartTypeError: `Chart type "${n.chartType}" is not compatible with the available fields (${i}). Eligible chart types: ${r.join(", ")}.`,
17640
17675
  fieldErrors: [],
17641
17676
  eligibleChartTypes: r,
17642
- suggestedConfig: Av(e, t)
17677
+ suggestedConfig: Pv(e, t)
17643
17678
  };
17644
17679
  }
17645
- let o = Av(e, t, [
17680
+ let o = Pv(e, t, [
17646
17681
  "line",
17647
17682
  "bar",
17648
17683
  "barh",
@@ -17682,15 +17717,14 @@ var bv = (e, t, n) => {
17682
17717
  let e = t.get(n.xField);
17683
17718
  !Tv(e) && !Ev(e) && !Sv(e) && a.push(`For ${n.chartType} charts, xField should typically be a temporal or categorical column.`);
17684
17719
  }
17685
- let l = { ...o };
17686
- for (let [e, t] of Object.entries(n)) t != null && (typeof t != "string" || t !== "") && (l[e] = t);
17720
+ let l = Nv(o, n);
17687
17721
  return {
17688
17722
  valid: a.length === 0,
17689
17723
  fieldErrors: a,
17690
17724
  eligibleChartTypes: r,
17691
17725
  suggestedConfig: l
17692
17726
  };
17693
- }, Fv = (e) => `Chart configuration is invalid for the data:\n${[e.chartTypeError, ...e.fieldErrors].filter(Boolean).map((e) => ` - ${e}`).join("\n")}\nEligible chart types for this data: ${e.eligibleChartTypes.join(", ") || "none"}.\nSuggested auto-detected config: ${JSON.stringify(e.suggestedConfig)}\nTip: Omit chartConfig to let auto-detection choose the best chart type and field mapping.`, Iv = (e, t) => {
17727
+ }, zv = (e) => `Chart configuration is invalid for the data:\n${[e.chartTypeError, ...e.fieldErrors].filter(Boolean).map((e) => ` - ${e}`).join("\n")}\nEligible chart types for this data: ${e.eligibleChartTypes.join(", ") || "none"}.\nSuggested auto-detected config: ${JSON.stringify(e.suggestedConfig)}\nTip: Omit chartConfig to let auto-detection choose the best chart type and field mapping.`, Bv = (e, t) => {
17694
17728
  if (!e || !t.get(e)) return {};
17695
17729
  let r = t.get(e);
17696
17730
  if (!r) return {};
@@ -17715,7 +17749,7 @@ var bv = (e, t, n) => {
17715
17749
  case n.INTEGER: return {};
17716
17750
  default: return {};
17717
17751
  }
17718
- }, Lv = (e, t, n = null) => {
17752
+ }, Vv = (e, t, n = null) => {
17719
17753
  if (!e || !t.get(e)) return {};
17720
17754
  let r = t.get(e);
17721
17755
  return r ? bv(e, t, "day_of_week_name") ? { sort: [
@@ -17744,7 +17778,7 @@ var bv = (e, t, n) => {
17744
17778
  field: e,
17745
17779
  order: "ascending"
17746
17780
  } } : {};
17747
- }, Rv = (e, t) => {
17781
+ }, Hv = (e, t) => {
17748
17782
  if (!e || !t.get(e)) return "nominal";
17749
17783
  let r = t.get(e);
17750
17784
  return r ? Tv(r) ? [
@@ -17752,38 +17786,38 @@ var bv = (e, t, n) => {
17752
17786
  n.TIMESTAMP,
17753
17787
  n.INTEGER
17754
17788
  ].includes(r.type) || [n.DATE].includes(r.type) ? "temporal" : "ordinal" : Cv(r) || hv(r) ? "ordinal" : Sv(r) ? "quantitative" : "nominal" : "nominal";
17755
- }, zv = (e, t, n = {}, r = !0, i = {}) => e ? {
17789
+ }, Uv = (e, t, n = {}, r = !0, i = {}) => e ? {
17756
17790
  field: e,
17757
- type: Rv(e, t),
17791
+ type: Hv(e, t),
17758
17792
  title: dv(t.get(e)?.description || e),
17759
- ...Iv(e, t),
17793
+ ...Bv(e, t),
17760
17794
  ...n,
17761
- ...r ? Lv(e, t) : {},
17795
+ ...r ? Vv(e, t) : {},
17762
17796
  ...i.scale !== void 0 || i.zero !== void 0 ? { scale: {
17763
17797
  ...i.scale !== void 0 && { type: i.scale },
17764
17798
  ...i.zero !== void 0 && { zero: i.zero }
17765
17799
  } } : {}
17766
- } : {}, Bv = (e = 0) => e > 0 ? `_l${e}` : "", Vv = (e, t = "") => `${e}${t}`, Hv = (e = "") => ({
17800
+ } : {}, Wv = (e = 0) => e > 0 ? `_l${e}` : "", Gv = (e, t = "") => `${e}${t}`, Kv = (e = "") => ({
17767
17801
  fillOpacity: {
17768
17802
  condition: {
17769
- param: Vv("select", e),
17803
+ param: Gv("select", e),
17770
17804
  value: 1
17771
17805
  },
17772
17806
  value: .3
17773
17807
  },
17774
17808
  strokeWidth: {
17775
17809
  condition: [{
17776
- param: Vv("select", e),
17810
+ param: Gv("select", e),
17777
17811
  empty: !1,
17778
17812
  value: 2
17779
17813
  }, {
17780
- param: Vv("highlight", e),
17814
+ param: Gv("highlight", e),
17781
17815
  empty: !1,
17782
17816
  value: 1
17783
17817
  }],
17784
17818
  value: 0
17785
17819
  }
17786
- }), Uv = (e, t, n, r = "color") => {
17820
+ }), qv = (e, t, n, r = "color") => {
17787
17821
  let i = !1;
17788
17822
  return n === "quantitative" && r === "color" && (i = !0), e && e.length > 10 ? {
17789
17823
  orient: t ? "bottom" : "right",
@@ -17796,21 +17830,21 @@ var bv = (e, t, n) => {
17796
17830
  direction: t ? "horizontal" : "vertical",
17797
17831
  titleFontSize: t ? 10 : 12
17798
17832
  };
17799
- }, Wv = 15, Gv = (e, t, n, r = !1, i = "light", a = !1, o = [], s = "") => {
17800
- let c = { tickCount: Wv }, l = [], u = [], d = o || [], f = Array.from(n.entries()).filter(([e, t]) => t.traits?.includes("hex")).map(([e, t]) => e);
17833
+ }, Jv = 15, Yv = (e, t, n, r = !1, i = "light", a = !1, o = [], s = "") => {
17834
+ let c = { tickCount: Jv }, l = [], u = [], d = o || [], f = Array.from(n.entries()).filter(([e, t]) => t.traits?.includes("hex")).map(([e, t]) => e);
17801
17835
  if (t) {
17802
- let e = Rv(t, n);
17836
+ let e = Hv(t, n);
17803
17837
  c = {
17804
17838
  ...c,
17805
- ...Uv(t, r, e)
17806
- }, (e === "nominal" || e === "ordinal") && (u = Array.from(new Set(d.map((e) => String(e[t] ?? "")))).sort((e, t) => e.localeCompare(t))), l = u.slice(0, Wv);
17839
+ ...qv(t, r, e)
17840
+ }, (e === "nominal" || e === "ordinal") && (u = Array.from(new Set(d.map((e) => String(e[t] ?? "")))).sort((e, t) => e.localeCompare(t))), l = u.slice(0, Jv);
17807
17841
  }
17808
17842
  let p = (e) => (!a && Object.keys(c).length > 0 ? e.legend = c : a && (e.legend = null), e);
17809
17843
  if (t && n.get(t)) {
17810
- let e = Rv(t, n);
17844
+ let e = Hv(t, n);
17811
17845
  c = {
17812
17846
  ...c,
17813
- ...Iv(t, n),
17847
+ ...Bv(t, n),
17814
17848
  ...(e === "nominal" || e === "ordinal") && l.length > 0 ? { values: l } : {}
17815
17849
  };
17816
17850
  let r = e === "quantitative" ? { scheme: i === "light" ? "viridis" : "plasma" } : { scheme: i === "light" ? "category20c" : "plasma" };
@@ -17830,25 +17864,25 @@ var bv = (e, t, n) => {
17830
17864
  title: dv(n.get(t)?.description || t),
17831
17865
  scale: r,
17832
17866
  ...s === null ? {} : { condition: [{
17833
- param: Vv("highlight", s),
17867
+ param: Gv("highlight", s),
17834
17868
  empty: !1,
17835
17869
  value: fv
17836
17870
  }, {
17837
- param: Vv("select", s),
17871
+ param: Gv("select", s),
17838
17872
  empty: !1,
17839
17873
  value: fv
17840
17874
  }] },
17841
- ...Iv(t, n)
17875
+ ...Bv(t, n)
17842
17876
  });
17843
17877
  }
17844
17878
  return p({});
17845
- }, Kv = (e, t, n = !1, r = !1) => {
17879
+ }, Xv = (e, t, n = !1, r = !1) => {
17846
17880
  let i = { tickCount: 5 };
17847
17881
  if (e) {
17848
- let r = Rv(e, t);
17882
+ let r = Hv(e, t);
17849
17883
  i = {
17850
17884
  ...i,
17851
- ...Uv(e, n, r, "size")
17885
+ ...qv(e, n, r, "size")
17852
17886
  };
17853
17887
  }
17854
17888
  return e && t.get(e) ? ((e) => (!r && Object.keys(i).length > 0 ? e.legend = i : r && (e.legend = null), e))({
@@ -17863,7 +17897,7 @@ var bv = (e, t, n) => {
17863
17897
  };
17864
17898
  //#endregion
17865
17899
  //#region llm/importConcepts.ts
17866
- async function qv(e, t, n) {
17900
+ async function Zv(e, t, n) {
17867
17901
  let { connectionStore: r, editorStore: i, queryExecutionService: a } = e;
17868
17902
  try {
17869
17903
  if (!i?.editors[t.id]?.contents) return "No fields found in this data source.";
@@ -17900,21 +17934,21 @@ async function qv(e, t, n) {
17900
17934
  }
17901
17935
  //#endregion
17902
17936
  //#region llm/sharedToolHelpers.ts
17903
- function Jv(e) {
17937
+ function Qv(e) {
17904
17938
  return Object.values(e.connections).map((e) => e.name).join(", ");
17905
17939
  }
17906
- function Yv(e, t) {
17940
+ function $v(e, t) {
17907
17941
  return e.connections[t] || e.connectionByName(t) || null;
17908
17942
  }
17909
- async function Xv(e, t, n) {
17943
+ async function ey(e, t, n) {
17910
17944
  if (!t || typeof t != "string") return {
17911
17945
  success: !1,
17912
- error: `Connection name is required. Available connections: ${Jv(e) || "None"}`
17946
+ error: `Connection name is required. Available connections: ${Qv(e) || "None"}`
17913
17947
  };
17914
- let r = Yv(e, t);
17948
+ let r = $v(e, t);
17915
17949
  if (!r) return {
17916
17950
  success: !1,
17917
- error: `Connection "${t}" not found. Available connections: ${Jv(e) || "None"}`
17951
+ error: `Connection "${t}" not found. Available connections: ${Qv(e) || "None"}`
17918
17952
  };
17919
17953
  if (r.connected) return n?.onConnected?.(r.name, r.id), {
17920
17954
  success: !0,
@@ -17933,8 +17967,8 @@ async function Xv(e, t, n) {
17933
17967
  };
17934
17968
  }
17935
17969
  }
17936
- function Zv(e, t, n, r) {
17937
- let i = /* @__PURE__ */ new Map(), a = Yv(e, n), o = a?.id || n;
17970
+ function ty(e, t, n, r) {
17971
+ let i = /* @__PURE__ */ new Map(), a = $v(e, n), o = a?.id || n;
17938
17972
  return e.getConnectionSources(o).forEach((e) => i.set(e.alias, e.contents)), t && a && Object.values(t.editors).filter((e) => !e.deleted && e.connectionId === a.id).forEach((e) => {
17939
17973
  i.set(e.name, e.contents);
17940
17974
  }), r && r.forEach((e) => {
@@ -17945,7 +17979,7 @@ function Zv(e, t, n, r) {
17945
17979
  contents: t
17946
17980
  }));
17947
17981
  }
17948
- function Qv(e, t, n, r) {
17982
+ function ny(e, t, n, r) {
17949
17983
  let i = typeof t?.toJSON == "function" ? t.toJSON() : t, a = i?.data;
17950
17984
  if (!Array.isArray(a)) return {
17951
17985
  success: !1,
@@ -17969,17 +18003,17 @@ function Qv(e, t, n, r) {
17969
18003
  }, null, 2)
17970
18004
  };
17971
18005
  }
17972
- function $v(e, t, n) {
18006
+ function ry(e, t, n) {
17973
18007
  if (!e) return [];
17974
- let r = n ? Yv(n, t) : null;
18008
+ let r = n ? $v(n, t) : null;
17975
18009
  return Object.values(e.editors).filter((e) => e.deleted ? !1 : r ? e.connectionId === r.id : e.connection === t).sort((e, t) => e.name.localeCompare(t.name)).map((e) => ({
17976
18010
  id: e.id,
17977
18011
  name: e.name.replace(/\//g, "."),
17978
18012
  alias: ""
17979
18013
  }));
17980
18014
  }
17981
- async function ey(e, t, n, r, i) {
17982
- let a = t.getConnectionName(), o = i && a ? Yv(i, a) : null;
18015
+ async function iy(e, t, n, r, i) {
18016
+ let a = t.getConnectionName(), o = i && a ? $v(i, a) : null;
17983
18017
  if (!e || e.trim() === "") return t.setActiveImports([]), {
17984
18018
  success: !0,
17985
18019
  message: "Cleared active data source selection.",
@@ -17989,27 +18023,27 @@ async function ey(e, t, n, r, i) {
17989
18023
  success: !1,
17990
18024
  error: "No data connection selected. Please select a data connection first."
17991
18025
  };
17992
- let s = $v(r, o?.id || a, i), c = s.find((t) => t.name === e || t.name.endsWith(`.${e}`));
18026
+ let s = ry(r, o?.id || a, i), c = s.find((t) => t.name === e || t.name.endsWith(`.${e}`));
17993
18027
  if (!c) return {
17994
18028
  success: !1,
17995
18029
  error: `Import "${e}" not found. Available imports: ${s.map((e) => e.name).join(", ") || "none"}`
17996
18030
  };
17997
18031
  let l = t.getActiveImports(), u = l.length === 1 && l[0].id === c.id;
17998
18032
  u || t.setActiveImports([c]);
17999
- let d = await qv(n, c, o?.id || a);
18033
+ let d = await Zv(n, c, o?.id || a);
18000
18034
  return {
18001
18035
  success: !0,
18002
18036
  message: `${u ? `"${c.name}" is already the active data source.` : `Successfully selected "${c.name}" as the active data source.`}\n\n${d}`,
18003
18037
  triggersSymbolRefresh: !u
18004
18038
  };
18005
18039
  }
18006
- function ty(e, t, n) {
18007
- let r = e.getConnectionName(), i = n && r ? Yv(n, r) : null, a = i?.name || r;
18040
+ function ay(e, t, n) {
18041
+ let r = e.getConnectionName(), i = n && r ? $v(n, r) : null, a = i?.name || r;
18008
18042
  if (!r) return {
18009
18043
  success: !1,
18010
18044
  error: "No data connection selected. Please select a data connection first."
18011
18045
  };
18012
- let o = $v(t, i?.id || r, n), s = e.getActiveImports(), c = s.length > 0 ? s[0] : null;
18046
+ let o = ry(t, i?.id || r, n), s = e.getActiveImports(), c = s.length > 0 ? s[0] : null;
18013
18047
  return o.length === 0 ? {
18014
18048
  success: !0,
18015
18049
  message: `No data sources available on connection "${a}".`
@@ -18018,7 +18052,7 @@ function ty(e, t, n) {
18018
18052
  message: `${c ? `Current active data source: ${c.name}\n\n` : "No data source currently selected.\n\n"}Available data sources for connection "${a}":\n${o.map((e) => `- ${e.name}${c?.id === e.id ? " (active)" : ""}`).join("\n")}\n\nUse select_active_import to select a data source.`
18019
18053
  };
18020
18054
  }
18021
- async function ny(e, t, n, r, i, a) {
18055
+ async function oy(e, t, n, r, i, a) {
18022
18056
  if (!i || typeof i != "string" || i.trim() === "") return {
18023
18057
  success: !1,
18024
18058
  error: "Query is required and must be a non-empty string",
@@ -18026,13 +18060,13 @@ async function ny(e, t, n, r, i, a) {
18026
18060
  };
18027
18061
  if (!r || typeof r != "string") return {
18028
18062
  success: !1,
18029
- error: `Connection name is required. Available connections: ${Jv(t) || "None"}`,
18063
+ error: `Connection name is required. Available connections: ${Qv(t) || "None"}`,
18030
18064
  query: i
18031
18065
  };
18032
- let o = Yv(t, r);
18066
+ let o = $v(t, r);
18033
18067
  if (!o) return {
18034
18068
  success: !1,
18035
- error: `Connection "${r}" not found. Available connections: ${Jv(t) || "None"}`,
18069
+ error: `Connection "${r}" not found. Available connections: ${Qv(t) || "None"}`,
18036
18070
  query: i
18037
18071
  };
18038
18072
  if (!o.connected) return {
@@ -18040,7 +18074,7 @@ async function ny(e, t, n, r, i, a) {
18040
18074
  error: `Connection "${o.name}" is not connected. Use connect_data_connection first.`,
18041
18075
  query: i
18042
18076
  };
18043
- let s = Zv(t, n, o.id, a), c = (a || []).map((e) => ({
18077
+ let s = ty(t, n, o.id, a), c = (a || []).map((e) => ({
18044
18078
  name: e.name,
18045
18079
  alias: e.alias || ""
18046
18080
  })), l = {
@@ -18077,13 +18111,13 @@ async function ny(e, t, n, r, i, a) {
18077
18111
  }
18078
18112
  //#endregion
18079
18113
  //#region llm/editorRefinementToolExecutor.ts
18080
- function ry() {
18114
+ function sy() {
18081
18115
  return {
18082
18116
  artifactRegistry: /* @__PURE__ */ new Map(),
18083
18117
  lastQueryResults: { value: null }
18084
18118
  };
18085
18119
  }
18086
- var iy = class {
18120
+ var cy = class {
18087
18121
  queryExecutionService;
18088
18122
  connectionStore;
18089
18123
  editorStore;
@@ -18092,7 +18126,7 @@ var iy = class {
18092
18126
  lastQueryResults;
18093
18127
  constructor(e, t, n, r, i) {
18094
18128
  this.queryExecutionService = e, this.connectionStore = t, this.editorContext = n, this.editorStore = r || null;
18095
- let a = i ?? ry();
18129
+ let a = i ?? sy();
18096
18130
  this.artifactRegistry = a.artifactRegistry, this.lastQueryResults = a.lastQueryResults;
18097
18131
  }
18098
18132
  async executeToolCall(e, t) {
@@ -18311,7 +18345,7 @@ var iy = class {
18311
18345
  };
18312
18346
  let t = this.editorContext.getCurrentResults?.() ?? this.lastQueryResults.value ?? null;
18313
18347
  if (t && t.headers) {
18314
- let n = Pv(t.data, t.headers, e);
18348
+ let n = Rv(t.data, t.headers, e);
18315
18349
  if (!n.valid) {
18316
18350
  let e = n.suggestedConfig;
18317
18351
  try {
@@ -18319,7 +18353,7 @@ var iy = class {
18319
18353
  } catch {}
18320
18354
  return {
18321
18355
  success: !1,
18322
- error: "Auto-corrected to default configuration.\n" + Fv(n)
18356
+ error: "Auto-corrected to default configuration.\n" + zv(n)
18323
18357
  };
18324
18358
  }
18325
18359
  }
@@ -18418,7 +18452,7 @@ var iy = class {
18418
18452
  }
18419
18453
  getArtifactRows(e, t, n) {
18420
18454
  let r = this.artifactRegistry.get(e);
18421
- return r ? Qv(e, r.data, t, n) : {
18455
+ return r ? ny(e, r.data, t, n) : {
18422
18456
  success: !1,
18423
18457
  error: `Artifact "${e}" not found. Only artifacts created by run_query or run_active_editor_query in this session are available.`
18424
18458
  };
@@ -18445,7 +18479,7 @@ var iy = class {
18445
18479
  }
18446
18480
  }
18447
18481
  async connectDataConnection(e) {
18448
- return Xv(this.connectionStore, e);
18482
+ return ey(this.connectionStore, e);
18449
18483
  }
18450
18484
  serializeDatabaseTree(e, t, n) {
18451
18485
  let r = this.connectionStore.connectionByName(this.editorContext.connectionName), i = r?.databases || [], a = (e ? i.filter((t) => t.name === e) : i).map((e) => {
@@ -18486,8 +18520,8 @@ var iy = class {
18486
18520
  };
18487
18521
  }
18488
18522
  buildEditorExtraContent() {
18489
- return Zv(this.connectionStore, this.editorStore, this.editorContext.connectionName);
18523
+ return ty(this.connectionStore, this.editorStore, this.editorContext.connectionName);
18490
18524
  }
18491
18525
  };
18492
18526
  //#endregion
18493
- export { N_ as $, C as $t, dv as A, nt as At, tv as B, Ke as Bt, Rv as C, jg as Ct, Bv as D, ht as Dt, wv as E, vt as Et, iv as F, O as Ft, P_ as G, Ee as Gt, ev as H, Ie as Ht, uv as I, Xe as It, q_ as J, Ve as Jt, K_ as K, Fe as Kt, lv as L, Ye as Lt, sv as M, et as Mt, ov as N, Qe as Nt, Vv as O, ft as Ot, av as P, $e as Pt, j_ as Q, D as Qt, rv as R, qe as Rt, Lv as S, Mg as St, Dv as T, wt as Tt, J_ as U, He as Ut, Q_ as V, ze as Vt, $_ as W, Ue as Wt, O_ as X, We as Xt, V_ as Y, Le as Yt, D_ as Z, Ce as Zt, jv as _, Jg as _t, ny as a, re as an, g_ as at, bv as b, Rg as bt, ty as c, ge as cn, p_ as ct, yv as d, b as dn, c_ as dt, ee as en, k_ as et, Gv as f, h as fn, n_ as ft, Av as g, d as gn, Yg as gt, Kv as h, f as hn, Kg as ht, Xv as i, S as in, S_ as it, cv as j, Ze as jt, Pv as k, ot as kt, ey as l, pe as ln, i_ as lt, Hv as m, l as mn, r_ as mt, ry as n, te as nn, w_ as nt, Qv as o, E as on, o_ as ot, zv as p, p as pn, e_ as pt, W_ as q, Be as qt, Zv as r, T as rn, C_ as rt, $v as s, ve as sn, a_ as st, iy as t, w as tn, A_ as tt, fv as u, _e as un, s_ as ut, $ as v, qg as vt, gv as w, Og as wt, Iv as x, Lg as xt, Fv as y, Hg as yt, nv as z, Ge as zt };
18527
+ export { j_ as $, D as $t, Rv as A, ot as At, nv as B, Ge as Bt, Hv as C, Mg as Ct, Wv as D, vt as Dt, wv as E, wt as Et, av as F, $e as Ft, $_ as G, Ue as Gt, Q_ as H, ze as Ht, iv as I, O as It, W_ as J, Be as Jt, P_ as K, Ee as Kt, uv as L, Xe as Lt, cv as M, Ze as Mt, sv as N, et as Nt, Nv as O, ht as Ot, ov as P, Qe as Pt, D_ as Q, Ce as Qt, lv as R, Ye as Rt, Vv as S, Lg as St, Dv as T, Og as Tt, ev as U, Ie as Ut, tv as V, Ke as Vt, J_ as W, He as Wt, V_ as X, Le as Xt, q_ as Y, Ve as Yt, O_ as Z, We as Zt, Fv as _, d as _n, Yg as _t, oy as a, S as an, S_ as at, bv as b, Hg as bt, ay as c, ve as cn, a_ as ct, yv as d, _e as dn, s_ as dt, C as en, N_ as et, Yv as f, b as fn, c_ as ft, Pv as g, f as gn, Kg as gt, Xv as h, l as hn, r_ as ht, ey as i, T as in, C_ as it, dv as j, nt as jt, Gv as k, ft as kt, iy as l, ge as ln, p_ as lt, Kv as m, p as mn, e_ as mt, sy as n, w as nn, A_ as nt, ny as o, re as on, g_ as ot, Uv as p, h as pn, n_ as pt, K_ as q, Fe as qt, ty as r, te as rn, w_ as rt, ry as s, E as sn, o_ as st, cy as t, ee as tn, k_ as tt, fv as u, pe as un, i_ as ut, $ as v, Jg as vt, gv as w, jg as wt, Bv as x, Rg as xt, zv as y, qg as yt, rv as z, qe as zt };