@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.
- package/dist/{AssetAutoImporter-OHphsfW9.js → AssetAutoImporter-CCbTLQ9v.js} +5 -5
- package/dist/{ChatCreatorModal-CuXha6Dg.js → ChatCreatorModal-C0OHgrEB.js} +1 -1
- package/dist/{CodeBlock-BBsaC6hX.js → CodeBlock-CS86Ipg1.js} +1 -1
- package/dist/{CommunityModels-CIQQhO-y.js → CommunityModels-BxJKe8_v.js} +4 -4
- package/dist/{ConnectionView-7Jj_Ggdi.js → ConnectionView-D-1gQNQ7.js} +4 -4
- package/dist/{CreateEditorFromDatasourcePopup-CrVHUWFl.js → CreateEditorFromDatasourcePopup-RkNgUdzU.js} +2 -2
- package/dist/{Dashboard-DxSi9W6Q.js → Dashboard-DWJPKIeQ.js} +5 -5
- package/dist/{DashboardImportSelector-kuDRDYi8.js → DashboardImportSelector-t6tKq5N4.js} +1 -1
- package/dist/{DashboardMobile-CZEgTb0R.js → DashboardMobile-CxObMuv6.js} +4 -4
- package/dist/{Editor-D6tdutTm.js → Editor-DD1inQuU.js} +6 -6
- package/dist/{GlobalChatPanel-BXZh0sdD.js → GlobalChatPanel-BArWsM8Y.js} +20 -20
- package/dist/{IDE-T8PmyAQ_.js → IDE-D5GKLVYn.js} +6 -6
- package/dist/{JobsView-Ca97MiSd.js → JobsView-By7OZgj_.js} +5 -5
- package/dist/{LLMChat-Cch33Yd_.js → LLMChat-B-q_7sa4.js} +2 -2
- package/dist/{LLMView-BDb6wD74.js → LLMView-8BKf70Ay.js} +9 -9
- package/dist/{LoadingView-B-xFwd2N.js → LoadingView-Da0HfQ56.js} +1 -1
- package/dist/{MarkdownRenderer-BWvdx0k3.js → MarkdownRenderer-CQppLv_n.js} +1 -1
- package/dist/{MobileIDE-DpdEQEqc.js → MobileIDE-C81gvbco.js} +12 -12
- package/dist/{ModelCard-DkAMP4X9.js → ModelCard-BMDgFDyQ.js} +4 -4
- package/dist/{ModelView-DomyhN8g.js → ModelView-C4B8qJwX.js} +4 -4
- package/dist/{PageLoading-BAvKGo9h.js → PageLoading-CRl9HNIg.js} +75 -75
- package/dist/{ResultComponent-zoOSoRLe.js → ResultComponent-C-A8sbr1.js} +4 -4
- package/dist/{Results-dq-SlJbU.js → Results-C1nNHU7C.js} +11 -11
- package/dist/{StatusIcon-AbxNvJ6p.js → StatusIcon-v2EhaU49.js} +1 -1
- package/dist/{StoreTokenModal-CM8WXdu1.js → StoreTokenModal-BV_gLE94.js} +1 -1
- package/dist/{SymbolsPane-DfaxeaPS.js → SymbolsPane-CuVCamJf.js} +1 -1
- package/dist/{TutorialPage-BtEJG7cT.js → TutorialPage-DGQGx3vF.js} +11 -11
- package/dist/{VegaLiteChart-BVyAab-o.js → VegaLiteChart-BIeSuNU8.js} +11413 -11363
- package/dist/{WelcomePage-BoqzYBLr.js → WelcomePage-T757Ynnn.js} +3 -3
- package/dist/components/VegaLiteChart.vue.d.ts.map +1 -1
- package/dist/components/chartControlsManager.d.ts +0 -1
- package/dist/components/chartControlsManager.d.ts.map +1 -1
- package/dist/dashboard.js +19 -19
- package/dist/dashboards/chartTheme.d.ts +68 -0
- package/dist/dashboards/chartTheme.d.ts.map +1 -0
- package/dist/dashboards/dashboardQueryExecutor.d.ts +9 -0
- package/dist/dashboards/dashboardQueryExecutor.d.ts.map +1 -1
- package/dist/dashboards/donutSpec.d.ts +2 -1
- package/dist/dashboards/donutSpec.d.ts.map +1 -1
- package/dist/dashboards/headlineSpec.d.ts +2 -1
- package/dist/dashboards/headlineSpec.d.ts.map +1 -1
- package/dist/dashboards/helpers.d.ts +23 -0
- package/dist/dashboards/helpers.d.ts.map +1 -1
- package/dist/dashboards/index.d.ts +2 -0
- package/dist/dashboards/index.d.ts.map +1 -1
- package/dist/dashboards/layerSpec.d.ts +4 -1
- package/dist/dashboards/layerSpec.d.ts.map +1 -1
- package/dist/dashboards/pointSpec.d.ts +2 -1
- package/dist/dashboards/pointSpec.d.ts.map +1 -1
- package/dist/dashboards/spec.d.ts +8 -1
- package/dist/dashboards/spec.d.ts.map +1 -1
- package/dist/{editorStore-CAlMJftt.js → editorStore-k4IbmW9-.js} +1094 -1094
- package/dist/{globalChatRuntime-CyS7vH51.js → globalChatRuntime-BWahbZxZ.js} +14 -14
- package/dist/{helpers-BbVlA3Pf.js → helpers-DDKx6kkf.js} +4 -4
- package/dist/llm/data/prompts.d.ts +1 -1
- package/dist/llm/data/prompts.d.ts.map +1 -1
- package/dist/llm/data/trilogySyntax.generated.d.ts +3 -3
- package/dist/llm/data/trilogySyntax.generated.d.ts.map +1 -1
- package/dist/{llm-ClU-2rgk.js → llm--BDQ6qkZ.js} +111 -77
- package/dist/llm.js +2 -2
- package/dist/{localStorage-CGQ1iJXt.js → localStorage-BYwXjN4M.js} +16 -16
- package/dist/monaco.js +2 -2
- package/dist/{reconcile-DXIvGwBk.js → reconcile-D-vMtlgJ.js} +1 -1
- package/dist/{remoteStoreSync-CuQBWpWf.js → remoteStoreSync-PLFQCOmY.js} +2 -2
- package/dist/{stores-LlyiwSbK.js → stores-S6dQ5cjI.js} +40 -32
- package/dist/stores.js +3 -3
- package/dist/trilogy-studio-components.css +1 -1
- package/dist/{useChatWithTools-m3Jn_SV-.js → useChatWithTools-Bkd9MeMh.js} +10 -10
- package/dist/{useDashboard-BOAuvVEw.js → useDashboard-50rwbG0x.js} +889 -889
- package/dist/{useGlobalChatPanel-DuP1q7_f.js → useGlobalChatPanel-JJZaHao8.js} +4 -4
- package/dist/views.js +11 -11
- package/dist/{vocabulary-CKLGILtR.js → vocabulary-DQK5jB7m.js} +3 -1
- 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
|
|
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#
|
|
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 --
|
|
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 =
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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} (${
|
|
17649
|
+
t.push(`${e.name} (${Iv(e)})`);
|
|
17615
17650
|
}), t.length > 0 ? t.join(", ") : "no fields";
|
|
17616
|
-
},
|
|
17617
|
-
let r =
|
|
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 =
|
|
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:
|
|
17677
|
+
suggestedConfig: Pv(e, t)
|
|
17643
17678
|
};
|
|
17644
17679
|
}
|
|
17645
|
-
let o =
|
|
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 =
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
17789
|
+
}, Uv = (e, t, n = {}, r = !0, i = {}) => e ? {
|
|
17756
17790
|
field: e,
|
|
17757
|
-
type:
|
|
17791
|
+
type: Hv(e, t),
|
|
17758
17792
|
title: dv(t.get(e)?.description || e),
|
|
17759
|
-
...
|
|
17793
|
+
...Bv(e, t),
|
|
17760
17794
|
...n,
|
|
17761
|
-
...r ?
|
|
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
|
-
} : {},
|
|
17800
|
+
} : {}, Wv = (e = 0) => e > 0 ? `_l${e}` : "", Gv = (e, t = "") => `${e}${t}`, Kv = (e = "") => ({
|
|
17767
17801
|
fillOpacity: {
|
|
17768
17802
|
condition: {
|
|
17769
|
-
param:
|
|
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:
|
|
17810
|
+
param: Gv("select", e),
|
|
17777
17811
|
empty: !1,
|
|
17778
17812
|
value: 2
|
|
17779
17813
|
}, {
|
|
17780
|
-
param:
|
|
17814
|
+
param: Gv("highlight", e),
|
|
17781
17815
|
empty: !1,
|
|
17782
17816
|
value: 1
|
|
17783
17817
|
}],
|
|
17784
17818
|
value: 0
|
|
17785
17819
|
}
|
|
17786
|
-
}),
|
|
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
|
-
},
|
|
17800
|
-
let c = { tickCount:
|
|
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 =
|
|
17836
|
+
let e = Hv(t, n);
|
|
17803
17837
|
c = {
|
|
17804
17838
|
...c,
|
|
17805
|
-
...
|
|
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,
|
|
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 =
|
|
17844
|
+
let e = Hv(t, n);
|
|
17811
17845
|
c = {
|
|
17812
17846
|
...c,
|
|
17813
|
-
...
|
|
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:
|
|
17867
|
+
param: Gv("highlight", s),
|
|
17834
17868
|
empty: !1,
|
|
17835
17869
|
value: fv
|
|
17836
17870
|
}, {
|
|
17837
|
-
param:
|
|
17871
|
+
param: Gv("select", s),
|
|
17838
17872
|
empty: !1,
|
|
17839
17873
|
value: fv
|
|
17840
17874
|
}] },
|
|
17841
|
-
...
|
|
17875
|
+
...Bv(t, n)
|
|
17842
17876
|
});
|
|
17843
17877
|
}
|
|
17844
17878
|
return p({});
|
|
17845
|
-
},
|
|
17879
|
+
}, Xv = (e, t, n = !1, r = !1) => {
|
|
17846
17880
|
let i = { tickCount: 5 };
|
|
17847
17881
|
if (e) {
|
|
17848
|
-
let r =
|
|
17882
|
+
let r = Hv(e, t);
|
|
17849
17883
|
i = {
|
|
17850
17884
|
...i,
|
|
17851
|
-
...
|
|
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
|
|
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
|
|
17937
|
+
function Qv(e) {
|
|
17904
17938
|
return Object.values(e.connections).map((e) => e.name).join(", ");
|
|
17905
17939
|
}
|
|
17906
|
-
function
|
|
17940
|
+
function $v(e, t) {
|
|
17907
17941
|
return e.connections[t] || e.connectionByName(t) || null;
|
|
17908
17942
|
}
|
|
17909
|
-
async function
|
|
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: ${
|
|
17946
|
+
error: `Connection name is required. Available connections: ${Qv(e) || "None"}`
|
|
17913
17947
|
};
|
|
17914
|
-
let r =
|
|
17948
|
+
let r = $v(e, t);
|
|
17915
17949
|
if (!r) return {
|
|
17916
17950
|
success: !1,
|
|
17917
|
-
error: `Connection "${t}" not found. Available connections: ${
|
|
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
|
|
17937
|
-
let i = /* @__PURE__ */ new Map(), a =
|
|
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
|
|
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
|
|
18006
|
+
function ry(e, t, n) {
|
|
17973
18007
|
if (!e) return [];
|
|
17974
|
-
let r = n ?
|
|
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
|
|
17982
|
-
let a = t.getConnectionName(), o = i && a ?
|
|
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 =
|
|
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
|
|
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
|
|
18007
|
-
let r = e.getConnectionName(), i = n && 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 =
|
|
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
|
|
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: ${
|
|
18063
|
+
error: `Connection name is required. Available connections: ${Qv(t) || "None"}`,
|
|
18030
18064
|
query: i
|
|
18031
18065
|
};
|
|
18032
|
-
let o =
|
|
18066
|
+
let o = $v(t, r);
|
|
18033
18067
|
if (!o) return {
|
|
18034
18068
|
success: !1,
|
|
18035
|
-
error: `Connection "${r}" not found. Available connections: ${
|
|
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 =
|
|
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
|
|
18114
|
+
function sy() {
|
|
18081
18115
|
return {
|
|
18082
18116
|
artifactRegistry: /* @__PURE__ */ new Map(),
|
|
18083
18117
|
lastQueryResults: { value: null }
|
|
18084
18118
|
};
|
|
18085
18119
|
}
|
|
18086
|
-
var
|
|
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 ??
|
|
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 =
|
|
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" +
|
|
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 ?
|
|
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
|
|
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
|
|
18523
|
+
return ty(this.connectionStore, this.editorStore, this.editorContext.connectionName);
|
|
18490
18524
|
}
|
|
18491
18525
|
};
|
|
18492
18526
|
//#endregion
|
|
18493
|
-
export {
|
|
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 };
|