@trilogy-data/trilogy-studio-components 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/dist/AssetAutoImporter-CBzvBaOt.js +405 -0
- package/dist/ChatCreatorModal-CL-jBnc3.js +218 -0
- package/dist/ChatModelSelect-Do-VRUFc.js +40 -0
- package/dist/{CommunityModels-CsDDXyIL.js → CommunityModels-Cmro0hV3.js} +101 -70
- package/dist/{Dashboard-BJfgRfRy.js → Dashboard-BH9ZwMVJ.js} +8 -8
- package/dist/{DashboardCreatorInline-wAlx6oH7.js → DashboardCreatorInline-CDU7kLy3.js} +3 -3
- package/dist/{DashboardImportSelector-D6ufVZIk.js → DashboardImportSelector-DQSvu9T-.js} +1 -1
- package/dist/{DashboardMobile-DoOyBHN7.js → DashboardMobile-BUPtdN2V.js} +4 -4
- package/dist/{Editor-Bs-h7edF.js → Editor-DQsYpkwl.js} +6 -6
- package/dist/{GlobalChatPanel-YsDZGxg8.js → GlobalChatPanel-DhnOaige.js} +155 -119
- package/dist/{IDE-CLF-KH2r.js → IDE-HLa8vybk.js} +9 -9
- package/dist/{JobsView-D1pp2xi5.js → JobsView-B97HBoIE.js} +8 -8
- package/dist/{LLMChat-CwM_brSg.js → LLMChat-CxJcPLEG.js} +1 -1
- package/dist/{LLMView-BiwCZb3Q.js → LLMView-C1MUf0c2.js} +21 -21
- package/dist/{MobileIDE-DrWh_OlB.js → MobileIDE-BpqXoaMR.js} +258 -253
- package/dist/{ModelCard-CsW4UwZI.js → ModelCard-BhN25jaV.js} +10 -10
- package/dist/{ModelCreator-C2douDis.js → ModelCreator-CCdC7EXt.js} +1 -1
- package/dist/{ModelView-D7ENFV2U.js → ModelView-DUDA_PtZ.js} +3 -3
- package/dist/{PageLoading-C5WK5i5Y.js → PageLoading-BzniJM0C.js} +622 -619
- package/dist/{ResultComponent-1MQlbELb.js → ResultComponent-CAn_r3BL.js} +3 -3
- package/dist/{Results-DXIYS4iW.js → Results-BJtlwebY.js} +3 -3
- package/dist/{SymbolsPane-BM6dUN-h.js → SymbolsPane-KYSwN9Sv.js} +1 -1
- package/dist/{TutorialPage-HXbXCznh.js → TutorialPage-NRcee6Dx.js} +8 -8
- package/dist/{VegaLiteChart-CS4h_t-K.js → VegaLiteChart-D1slfOUY.js} +11227 -11195
- package/dist/{WelcomePage-CLKvAvk-.js → WelcomePage-D5mU2x1L.js} +1 -1
- package/dist/components/AssetAutoImporter.vue.d.ts.map +1 -1
- package/dist/components/TipsCTA.vue.d.ts +1 -1
- package/dist/components/VegaLiteChart.vue.d.ts.map +1 -1
- package/dist/components/chartHelpers.d.ts +9 -1
- package/dist/components/chartHelpers.d.ts.map +1 -1
- package/dist/components/chartRenderManager.d.ts +4 -1
- package/dist/components/chartRenderManager.d.ts.map +1 -1
- package/dist/components/community/AddStoreModal.vue.d.ts +1 -1
- package/dist/components/community/AddStoreModal.vue.d.ts.map +1 -1
- package/dist/components/community/CommunityModelCard.vue.d.ts.map +1 -1
- package/dist/components/editor/Editor.vue.d.ts.map +1 -1
- package/dist/components/jobs/JobsAddStoreModal.vue.d.ts.map +1 -1
- package/dist/components/llm/ChatCreatorModal.vue.d.ts +17 -1
- package/dist/components/llm/ChatCreatorModal.vue.d.ts.map +1 -1
- package/dist/components/llm/ChatModelSelect.vue.d.ts +13 -0
- package/dist/components/llm/ChatModelSelect.vue.d.ts.map +1 -0
- package/dist/components/llm/GlobalChatPanel.vue.d.ts.map +1 -1
- package/dist/components/sidebar/CommunityModelList.vue.d.ts +33 -50
- package/dist/components/sidebar/CommunityModelList.vue.d.ts.map +1 -1
- package/dist/components/sidebar/CommunityModelListItem.vue.d.ts.map +1 -1
- package/dist/components/sidebar/EditorList.vue.d.ts +32 -49
- package/dist/components/sidebar/EditorList.vue.d.ts.map +1 -1
- package/dist/components/sidebar/Sidebar.vue.d.ts +65 -99
- package/dist/components/sidebar/Sidebar.vue.d.ts.map +1 -1
- package/dist/dashboard.js +52 -52
- package/dist/{dashboardStore-BzmI4OCV.js → dashboardStore-ppM6nGks.js} +33 -33
- package/dist/dashboards/layerSpec.d.ts +38 -4
- package/dist/dashboards/layerSpec.d.ts.map +1 -1
- package/dist/dashboards/lineAreaSpec.d.ts +8 -80
- package/dist/dashboards/lineAreaSpec.d.ts.map +1 -1
- package/dist/dashboards/mapSpec.d.ts +1 -0
- package/dist/dashboards/mapSpec.d.ts.map +1 -1
- package/dist/dashboards/spec.d.ts.map +1 -1
- package/dist/data/tutorial/documentation.d.ts.map +1 -1
- package/dist/data/tutorial/startupScripts.d.ts +3 -0
- package/dist/data/tutorial/startupScripts.d.ts.map +1 -0
- package/dist/{demoSetup-vmiMeH4_.js → demoSetup-mwa-RDC6.js} +19 -18
- package/dist/{editorRefinementRuntime-hC3u8GMO.js → editorRefinementRuntime-DoP9P5J2.js} +892 -871
- package/dist/{editorRefinementToolExecutor-Bomrpmns.js → editorRefinementToolExecutor-CVY3FA7k.js} +392 -267
- package/dist/{editorStore-Bj3Qnatp.js → editorStore-BhhmXbi-.js} +230 -183
- package/dist/entry.dashboard.d.ts +1 -0
- package/dist/entry.dashboard.d.ts.map +1 -1
- package/dist/{globalChatRuntime-DXI2KyGn.js → globalChatRuntime-DWZxeS57.js} +13 -13
- package/dist/{helpers-CFp2QCvv.js → helpers-BLQAgX3C.js} +17 -16
- package/dist/llm/chatAgentPrompt.d.ts.map +1 -1
- package/dist/llm/data/constants.d.ts +1 -1
- package/dist/llm/data/constants.d.ts.map +1 -1
- package/dist/llm/data/trilogySyntax.generated.d.ts +7 -1
- package/dist/llm/data/trilogySyntax.generated.d.ts.map +1 -1
- package/dist/llm/docsIndex.d.ts.map +1 -1
- package/dist/llm/openai.d.ts +2 -1
- package/dist/llm/openai.d.ts.map +1 -1
- package/dist/{llm-BuEsqH9Q.js → llm-BBOVWb-F.js} +7 -5
- package/dist/llm.js +3 -3
- package/dist/{llmStore-x5lRrKgx.js → llmStore-DJM2JhzY.js} +2 -2
- package/dist/{localStorage-C-ZtI6YA.js → localStorage-D9uyOFEO.js} +14 -14
- package/dist/models/helpers.d.ts.map +1 -1
- package/dist/monaco.js +1 -1
- package/dist/{reconcile-E5LR5p_f.js → reconcile-Dyf4UZmj.js} +1 -1
- package/dist/{remoteStoreSync-MwuJpHHt.js → remoteStoreSync-Z-yjlYtI.js} +1 -1
- package/dist/remotes/displayHelpers.d.ts.map +1 -1
- package/dist/remotes/genericStoreMetadata.d.ts +1 -0
- package/dist/remotes/genericStoreMetadata.d.ts.map +1 -1
- package/dist/remotes/models.d.ts +37 -5
- package/dist/remotes/models.d.ts.map +1 -1
- package/dist/remotes/staticStorePresets.d.ts +22 -0
- package/dist/remotes/staticStorePresets.d.ts.map +1 -0
- package/dist/remotes/storeService.d.ts +25 -4
- package/dist/remotes/storeService.d.ts.map +1 -1
- package/dist/stores/communityApiStore.d.ts +32 -60
- package/dist/stores/communityApiStore.d.ts.map +1 -1
- package/dist/stores/connectionStore.d.ts.map +1 -1
- package/dist/stores/resolver.d.ts +5 -0
- package/dist/stores/resolver.d.ts.map +1 -1
- package/dist/stores/urlStore.d.ts +1 -0
- package/dist/stores/urlStore.d.ts.map +1 -1
- package/dist/{stores-D-gHjj-_.js → stores-BT9hTLny.js} +47 -47
- package/dist/stores.js +7 -7
- package/dist/trilogy-studio-components.css +1 -1
- package/dist/{urlStore-Dgmwx1YI.js → urlStore-52HG6lsX.js} +1 -0
- package/dist/{useChatWithTools-BlCJrJT7.js → useChatWithTools-B0HZ_DVh.js} +1 -1
- package/dist/{useDashboard-CVlbV17V.js → useDashboard-CdSnPe_6.js} +186 -186
- package/dist/{useGlobalChatPanel-CNARTFYu.js → useGlobalChatPanel-D_0luajQ.js} +2 -2
- package/dist/views/MobileIDE.vue.d.ts.map +1 -1
- package/dist/views/TutorialPage.vue.d.ts +32 -49
- package/dist/views/TutorialPage.vue.d.ts.map +1 -1
- package/dist/views.js +16 -16
- package/package.json +1 -1
- package/dist/AssetAutoImporter-BD1P91Hq.js +0 -403
- package/dist/ChatCreatorModal-BTqY06B6.js +0 -177
package/dist/{editorRefinementToolExecutor-Bomrpmns.js → editorRefinementToolExecutor-CVY3FA7k.js}
RENAMED
|
@@ -299,7 +299,7 @@ var c = class e {
|
|
|
299
299
|
size() {
|
|
300
300
|
return this.cache.size;
|
|
301
301
|
}
|
|
302
|
-
}, y = class {
|
|
302
|
+
}, y = class e {
|
|
303
303
|
settingStore;
|
|
304
304
|
validateCache;
|
|
305
305
|
formatCache;
|
|
@@ -338,17 +338,28 @@ var c = class e {
|
|
|
338
338
|
buildResolverUrl(e) {
|
|
339
339
|
return `${this.settingStore.settings.trilogyResolver.replace(/\/+$/, "")}/${e}`;
|
|
340
340
|
}
|
|
341
|
-
|
|
341
|
+
static RETRY_STATUSES = /* @__PURE__ */ new Set([503]);
|
|
342
|
+
static MAX_RETRIES = 3;
|
|
343
|
+
static MAX_RETRY_DELAY_MS = 5e3;
|
|
344
|
+
delay(e) {
|
|
345
|
+
return new Promise((t) => setTimeout(t, e));
|
|
346
|
+
}
|
|
347
|
+
retryDelayMs(t, n) {
|
|
348
|
+
let r = Number(t.headers?.get?.("retry-after")), i = Number.isFinite(r) && r > 0 ? r * 1e3 : 500 * 2 ** n;
|
|
349
|
+
return Math.min(i, e.MAX_RETRY_DELAY_MS) * (.75 + Math.random() * .5);
|
|
350
|
+
}
|
|
351
|
+
async fetchWithErrorHandling(t, n) {
|
|
342
352
|
try {
|
|
343
|
-
let
|
|
344
|
-
|
|
345
|
-
|
|
353
|
+
let r = await fetch(t, n);
|
|
354
|
+
for (let i = 0; !r.ok && e.RETRY_STATUSES.has(r.status) && i < e.MAX_RETRIES; i++) await this.delay(this.retryDelayMs(r, i)), r = await fetch(t, n);
|
|
355
|
+
if (!r.ok) {
|
|
356
|
+
let e = await r.json().catch(() => ({}));
|
|
346
357
|
throw {
|
|
347
|
-
message: `HTTP error ${
|
|
358
|
+
message: `HTTP error ${r.status}`,
|
|
348
359
|
response: { data: e }
|
|
349
360
|
};
|
|
350
361
|
}
|
|
351
|
-
return { data: await
|
|
362
|
+
return { data: await r.json() };
|
|
352
363
|
} catch (e) {
|
|
353
364
|
throw console.log(e), Error(this.getErrorMessage(e));
|
|
354
365
|
}
|
|
@@ -926,9 +937,9 @@ function De(e) {
|
|
|
926
937
|
Object.entries(e).forEach(([e, n]) => {
|
|
927
938
|
t[e] || (t[e] = []), t[e].push(n);
|
|
928
939
|
});
|
|
929
|
-
}), Object.entries(t).map(([e, t]) => t.length > 1 ? `(${t.map((t) =>
|
|
940
|
+
}), Object.entries(t).map(([e, t]) => t.length > 1 ? `(${t.map((t) => C(e, t)).join(" OR ")})` : C(e, t[0])).join(" AND ");
|
|
930
941
|
}
|
|
931
|
-
return Object.keys(e).length === 0 ? "" : Object.entries(e).map(([e, t]) =>
|
|
942
|
+
return Object.keys(e).length === 0 ? "" : Object.entries(e).map(([e, t]) => C(e, t)).join(" AND ");
|
|
932
943
|
}
|
|
933
944
|
function Oe(e) {
|
|
934
945
|
if (e instanceof Date) return `'${e.getFullYear()}-${String(e.getMonth() + 1).padStart(2, "0")}-${String(e.getDate()).padStart(2, "0")}'::date`;
|
|
@@ -937,23 +948,23 @@ function Oe(e) {
|
|
|
937
948
|
if (typeof e == "boolean") return e ? "TRUE" : "FALSE";
|
|
938
949
|
if (typeof e == "number") return e.toString();
|
|
939
950
|
}
|
|
940
|
-
function
|
|
951
|
+
function C(e, t) {
|
|
941
952
|
return t === null ? `${e} IS NULL` : typeof t == "string" ? `${e}='''${t.replace(/'/g, "!`")}'''` : Array.isArray(t) ? `${e} between ${Oe(t[0])} and ${Oe(t[t.length - 1])}` : typeof t == "number" || typeof t == "boolean" ? `${e}=${t}` : t === void 0 ? `${e} IS NULL` : `${e}='${JSON.stringify(t).replace(/'/g, "!`")}'`;
|
|
942
953
|
}
|
|
943
954
|
//#endregion
|
|
944
955
|
//#region dashboards/crossFilters.ts
|
|
945
|
-
function
|
|
956
|
+
function ke(e = []) {
|
|
946
957
|
let t = /* @__PURE__ */ new Set();
|
|
947
958
|
for (let n of e) n?.label && t.add(n.label);
|
|
948
959
|
return Array.from(t);
|
|
949
960
|
}
|
|
950
|
-
function
|
|
961
|
+
function w(e) {
|
|
951
962
|
return { ...e };
|
|
952
963
|
}
|
|
953
|
-
function
|
|
964
|
+
function T(e) {
|
|
954
965
|
return { ...e };
|
|
955
966
|
}
|
|
956
|
-
function
|
|
967
|
+
function Ae(e, t) {
|
|
957
968
|
if (e.op !== t.op) return !1;
|
|
958
969
|
switch (e.op) {
|
|
959
970
|
case "is_null": return !0;
|
|
@@ -968,31 +979,31 @@ function je(e, t) {
|
|
|
968
979
|
}
|
|
969
980
|
}
|
|
970
981
|
}
|
|
971
|
-
function
|
|
982
|
+
function E(e, t) {
|
|
972
983
|
let n = Object.keys(e).sort(), r = Object.keys(t).sort();
|
|
973
|
-
return n.length === r.length && n.every((n, i) => n === r[i] &&
|
|
984
|
+
return n.length === r.length && n.every((n, i) => n === r[i] && Ae(e[n], t[n]));
|
|
974
985
|
}
|
|
975
|
-
function
|
|
986
|
+
function je(e, t) {
|
|
976
987
|
return Array.isArray(e) && Array.isArray(t) ? e.length === t.length && e.every((e, n) => e === t[n]) : e === t;
|
|
977
988
|
}
|
|
978
|
-
function
|
|
989
|
+
function Me(e, t) {
|
|
979
990
|
let n = Object.keys(e).sort(), r = Object.keys(t).sort();
|
|
980
|
-
return n.length === r.length && n.every((n, i) => n === r[i] &&
|
|
991
|
+
return n.length === r.length && n.every((n, i) => n === r[i] && je(e[n], t[n]));
|
|
981
992
|
}
|
|
982
|
-
function
|
|
993
|
+
function Ne(e, t, n, r) {
|
|
983
994
|
let i = e.filter((e) => e.source !== t), a = e.filter((e) => e.source === t);
|
|
984
995
|
if (r === "remove") return i;
|
|
985
996
|
if (r === "add") return [...i, {
|
|
986
997
|
source: t,
|
|
987
|
-
value:
|
|
998
|
+
value: T(n)
|
|
988
999
|
}];
|
|
989
|
-
let o = a.some((e) =>
|
|
1000
|
+
let o = a.some((e) => Me(e.value, n)) ? a.filter((e) => !Me(e.value, n)) : [...a, {
|
|
990
1001
|
source: t,
|
|
991
|
-
value:
|
|
1002
|
+
value: T(n)
|
|
992
1003
|
}];
|
|
993
1004
|
return [...i, ...o];
|
|
994
1005
|
}
|
|
995
|
-
function
|
|
1006
|
+
function Pe(e, t, n) {
|
|
996
1007
|
if (t.size === 0) return n && e.startsWith("local.") ? e.replace(/^local\./, "") : e;
|
|
997
1008
|
if (t.has(e)) return e;
|
|
998
1009
|
if (e.startsWith("local.")) {
|
|
@@ -1001,7 +1012,7 @@ function Fe(e, t, n) {
|
|
|
1001
1012
|
}
|
|
1002
1013
|
return null;
|
|
1003
1014
|
}
|
|
1004
|
-
function
|
|
1015
|
+
function Fe(e, t = [], n = {}) {
|
|
1005
1016
|
let r = new Set(t), i = n.normalizeLocalFields ?? !1;
|
|
1006
1017
|
return Object.entries(e).reduce((e, [t, n]) => {
|
|
1007
1018
|
if (typeof n != "object" || !n || ![
|
|
@@ -1010,11 +1021,11 @@ function Ie(e, t = [], n = {}) {
|
|
|
1010
1021
|
"in",
|
|
1011
1022
|
"is_null"
|
|
1012
1023
|
].includes(n.op)) return e;
|
|
1013
|
-
let a =
|
|
1024
|
+
let a = Pe(t, r, i);
|
|
1014
1025
|
return a && (e[a] = n), e;
|
|
1015
1026
|
}, {});
|
|
1016
1027
|
}
|
|
1017
|
-
function
|
|
1028
|
+
function D(e) {
|
|
1018
1029
|
if (!e.length) return {
|
|
1019
1030
|
filterStrings: [],
|
|
1020
1031
|
parameters: {}
|
|
@@ -1044,8 +1055,8 @@ function E(e) {
|
|
|
1044
1055
|
parameters: r
|
|
1045
1056
|
};
|
|
1046
1057
|
}
|
|
1047
|
-
function
|
|
1048
|
-
let t = e.conceptFilters || [], n = (e.filters || []).filter((e) => e.source !== "cross"), { filterStrings: r, parameters: i } =
|
|
1058
|
+
function O(e) {
|
|
1059
|
+
let t = e.conceptFilters || [], n = (e.filters || []).filter((e) => e.source !== "cross"), { filterStrings: r, parameters: i } = D(t), a = r.join(" AND ");
|
|
1049
1060
|
a && n.push({
|
|
1050
1061
|
source: "cross",
|
|
1051
1062
|
value: a,
|
|
@@ -1054,43 +1065,43 @@ function D(e) {
|
|
|
1054
1065
|
let o = JSON.stringify(n) !== JSON.stringify(e.filters || []);
|
|
1055
1066
|
return e.filters = n, o;
|
|
1056
1067
|
}
|
|
1057
|
-
function
|
|
1068
|
+
function Ie(e, t, n, r) {
|
|
1058
1069
|
let i = e.filter((e) => e.source !== t), a = e.filter((e) => e.source === t);
|
|
1059
1070
|
if (r === "remove") return i;
|
|
1060
1071
|
if (r === "add") return [...i, {
|
|
1061
1072
|
source: t,
|
|
1062
|
-
value:
|
|
1073
|
+
value: w(n)
|
|
1063
1074
|
}];
|
|
1064
|
-
let o = a.some((e) =>
|
|
1075
|
+
let o = a.some((e) => E(e.value, n)) ? a.filter((e) => !E(e.value, n)) : [...a, {
|
|
1065
1076
|
source: t,
|
|
1066
|
-
value:
|
|
1077
|
+
value: w(n)
|
|
1067
1078
|
}];
|
|
1068
1079
|
return [...i, ...o];
|
|
1069
1080
|
}
|
|
1070
|
-
function
|
|
1081
|
+
function Le(e, t, n, r, i) {
|
|
1071
1082
|
let a = [];
|
|
1072
1083
|
return Object.entries(e).forEach(([e, o]) => {
|
|
1073
1084
|
if (e === t) {
|
|
1074
|
-
let e =
|
|
1085
|
+
let e = Ne(o.chartFilters || [], t, r, i);
|
|
1075
1086
|
JSON.stringify(e) !== JSON.stringify(o.chartFilters || []) && (o.chartFilters = e);
|
|
1076
1087
|
return;
|
|
1077
1088
|
}
|
|
1078
1089
|
if (!o.allowCrossFilter) return;
|
|
1079
|
-
let s =
|
|
1090
|
+
let s = Ie(o.conceptFilters || [], t, n, i), c = JSON.stringify(s) !== JSON.stringify(o.conceptFilters || []);
|
|
1080
1091
|
c && (o.conceptFilters = s);
|
|
1081
|
-
let l =
|
|
1092
|
+
let l = O(o);
|
|
1082
1093
|
(c || l) && a.push(e);
|
|
1083
1094
|
}), a;
|
|
1084
1095
|
}
|
|
1085
|
-
function
|
|
1096
|
+
function Re(e, t, n) {
|
|
1086
1097
|
let r = e[t];
|
|
1087
1098
|
if (!r) return !1;
|
|
1088
1099
|
let i = (r.conceptFilters || []).filter((e) => e.source !== n), a = JSON.stringify(i) !== JSON.stringify(r.conceptFilters || []);
|
|
1089
1100
|
a && (r.conceptFilters = i);
|
|
1090
|
-
let o =
|
|
1101
|
+
let o = O(r);
|
|
1091
1102
|
return a || o;
|
|
1092
1103
|
}
|
|
1093
|
-
function
|
|
1104
|
+
function ze(e, t) {
|
|
1094
1105
|
let n = [];
|
|
1095
1106
|
return Object.entries(e).forEach(([e, r]) => {
|
|
1096
1107
|
if (e === t) {
|
|
@@ -1099,27 +1110,27 @@ function Be(e, t) {
|
|
|
1099
1110
|
}
|
|
1100
1111
|
let i = (r.conceptFilters || []).filter((e) => e.source !== t), a = JSON.stringify(i) !== JSON.stringify(r.conceptFilters || []);
|
|
1101
1112
|
a && (r.conceptFilters = i);
|
|
1102
|
-
let o =
|
|
1113
|
+
let o = O(r);
|
|
1103
1114
|
(a || o) && n.push(e);
|
|
1104
1115
|
}), n;
|
|
1105
1116
|
}
|
|
1106
|
-
function
|
|
1117
|
+
function Be(e) {
|
|
1107
1118
|
let t = [];
|
|
1108
1119
|
return Object.entries(e).forEach(([e, n]) => {
|
|
1109
1120
|
let r = !!n.conceptFilters?.length || !!n.chartFilters?.length || !!n.filters?.length;
|
|
1110
1121
|
n.conceptFilters = [], n.chartFilters = [], n.filters = [], r && t.push(e);
|
|
1111
1122
|
}), t;
|
|
1112
1123
|
}
|
|
1113
|
-
function
|
|
1124
|
+
function Ve(e = {}) {
|
|
1114
1125
|
let t = /* @__PURE__ */ new Map(), n = () => {
|
|
1115
1126
|
let t = e.validFields ? o(e.validFields) : [];
|
|
1116
1127
|
return new Set(t);
|
|
1117
1128
|
}, r = (t) => {
|
|
1118
|
-
let r =
|
|
1129
|
+
let r = Fe(t.filters, n(), { normalizeLocalFields: e.normalizeLocalFields ?? !0 });
|
|
1119
1130
|
return {
|
|
1120
1131
|
source: t.source,
|
|
1121
1132
|
filters: r,
|
|
1122
|
-
chart:
|
|
1133
|
+
chart: T(t.chart || {}),
|
|
1123
1134
|
append: t.append
|
|
1124
1135
|
};
|
|
1125
1136
|
};
|
|
@@ -1127,8 +1138,8 @@ function He(e = {}) {
|
|
|
1127
1138
|
getSelections() {
|
|
1128
1139
|
return Array.from(t.values()).flat().map((e) => ({
|
|
1129
1140
|
source: e.source,
|
|
1130
|
-
filters:
|
|
1131
|
-
chart:
|
|
1141
|
+
filters: w(e.filters),
|
|
1142
|
+
chart: T(e.chart || {}),
|
|
1132
1143
|
append: e.append
|
|
1133
1144
|
}));
|
|
1134
1145
|
},
|
|
@@ -1141,7 +1152,7 @@ function He(e = {}) {
|
|
|
1141
1152
|
let o = t.get(e.source) || [];
|
|
1142
1153
|
if (a === "remove") return t.delete(e.source), i.filters;
|
|
1143
1154
|
if (a === "add") return t.set(e.source, [i]), i.filters;
|
|
1144
|
-
let s = o.some((e) =>
|
|
1155
|
+
let s = o.some((e) => E(e.filters, i.filters)) ? o.filter((e) => !E(e.filters, i.filters)) : [...o, i];
|
|
1145
1156
|
return s.length === 0 ? t.delete(e.source) : t.set(e.source, s), i.filters;
|
|
1146
1157
|
},
|
|
1147
1158
|
clearSource(e) {
|
|
@@ -1156,14 +1167,14 @@ function He(e = {}) {
|
|
|
1156
1167
|
getSqlFilterInputsFor(e) {
|
|
1157
1168
|
return Array.from(t.entries()).filter(([t]) => t !== e).flatMap(([e, t]) => t.map((t) => ({
|
|
1158
1169
|
source: e,
|
|
1159
|
-
value:
|
|
1170
|
+
value: w(t.filters)
|
|
1160
1171
|
})));
|
|
1161
1172
|
},
|
|
1162
1173
|
getChartSelectionsFor(e) {
|
|
1163
|
-
return (t.get(e) || []).map((e) =>
|
|
1174
|
+
return (t.get(e) || []).map((e) => T(e.chart || {}));
|
|
1164
1175
|
},
|
|
1165
1176
|
getFilterExpressionFor(e) {
|
|
1166
|
-
let { filterStrings: t } =
|
|
1177
|
+
let { filterStrings: t } = D(this.getSqlFilterInputsFor(e));
|
|
1167
1178
|
return t.join(" AND ");
|
|
1168
1179
|
},
|
|
1169
1180
|
getSqlFiltersFor(e, t = []) {
|
|
@@ -1174,7 +1185,7 @@ function He(e = {}) {
|
|
|
1174
1185
|
let n = t.map((e) => ({
|
|
1175
1186
|
source: "base",
|
|
1176
1187
|
value: e
|
|
1177
|
-
})), { filterStrings: r, parameters: i } =
|
|
1188
|
+
})), { filterStrings: r, parameters: i } = D(this.getSqlFilterInputsFor(e)), a = r.join(" AND ");
|
|
1178
1189
|
return a && n.push({
|
|
1179
1190
|
source: "cross",
|
|
1180
1191
|
value: a,
|
|
@@ -1182,13 +1193,13 @@ function He(e = {}) {
|
|
|
1182
1193
|
}), n;
|
|
1183
1194
|
},
|
|
1184
1195
|
getSqlParametersFor(e) {
|
|
1185
|
-
let { parameters: t } =
|
|
1196
|
+
let { parameters: t } = D(this.getSqlFilterInputsFor(e));
|
|
1186
1197
|
return t;
|
|
1187
1198
|
}
|
|
1188
1199
|
};
|
|
1189
1200
|
}
|
|
1190
|
-
function
|
|
1191
|
-
let t = a(0), n =
|
|
1201
|
+
function He(e = {}) {
|
|
1202
|
+
let t = a(0), n = Ve(o(e)), r = () => {
|
|
1192
1203
|
t.value += 1;
|
|
1193
1204
|
};
|
|
1194
1205
|
return {
|
|
@@ -1234,7 +1245,7 @@ function Ue(e = {}) {
|
|
|
1234
1245
|
}
|
|
1235
1246
|
//#endregion
|
|
1236
1247
|
//#region dashboards/base.ts
|
|
1237
|
-
var
|
|
1248
|
+
var k = {
|
|
1238
1249
|
CHART: "chart",
|
|
1239
1250
|
MARKDOWN: "markdown",
|
|
1240
1251
|
TABLE: "table",
|
|
@@ -1245,24 +1256,24 @@ var O = {
|
|
|
1245
1256
|
APPENDIX_HEADER: "appendix-header",
|
|
1246
1257
|
FREEFORM: "freeform"
|
|
1247
1258
|
};
|
|
1248
|
-
function
|
|
1259
|
+
function Ue(e) {
|
|
1249
1260
|
return !!e && typeof e == "object" && "markdown" in e;
|
|
1250
1261
|
}
|
|
1251
|
-
function
|
|
1262
|
+
function We(e) {
|
|
1252
1263
|
return !!e && typeof e == "object" && "headline" in e && "verdict" in e && "confidence" in e;
|
|
1253
1264
|
}
|
|
1254
|
-
function
|
|
1265
|
+
function Ge(e) {
|
|
1255
1266
|
return !!e && typeof e == "object" && "claim" in e && !("headline" in e);
|
|
1256
1267
|
}
|
|
1257
|
-
function
|
|
1268
|
+
function Ke(e) {
|
|
1258
1269
|
return !!e && typeof e == "object" && typeof e.html == "string";
|
|
1259
1270
|
}
|
|
1260
|
-
function
|
|
1271
|
+
function qe(e) {
|
|
1261
1272
|
let t = e.content;
|
|
1262
|
-
if (typeof t == "string") return e.type ===
|
|
1263
|
-
if (
|
|
1273
|
+
if (typeof t == "string") return e.type === k.CHART || e.type === k.TABLE ? t : void 0;
|
|
1274
|
+
if (Ke(t) || We(t) || Ge(t) || Ue(t)) return t.query || void 0;
|
|
1264
1275
|
}
|
|
1265
|
-
var
|
|
1276
|
+
var Je = class t {
|
|
1266
1277
|
id;
|
|
1267
1278
|
name;
|
|
1268
1279
|
storage;
|
|
@@ -1305,7 +1316,7 @@ var Ye = class t {
|
|
|
1305
1316
|
}
|
|
1306
1317
|
addItem(e, t = 0, n = 0, r = 4, i = null, a = null, o) {
|
|
1307
1318
|
let s = this.nextId.toString(), c = 10, l = r;
|
|
1308
|
-
e ===
|
|
1319
|
+
e === k.MARKDOWN ? c = 3 : e === k.SECTION_HEADER || e === k.APPENDIX_HEADER ? c = 1 : e === k.MEMO ? (c = 8, l = 20) : e === k.CLAIM ? (c = 4, l = 20) : e === k.FREEFORM && (c = 10);
|
|
1309
1320
|
let u = n;
|
|
1310
1321
|
u = this.layout.reduce((e, t) => Math.max(e, t.y + t.h), 0), this.layout.push({
|
|
1311
1322
|
x: t,
|
|
@@ -1316,16 +1327,16 @@ var Ye = class t {
|
|
|
1316
1327
|
static: !1
|
|
1317
1328
|
});
|
|
1318
1329
|
let d = `Note ${s}`;
|
|
1319
|
-
e ===
|
|
1330
|
+
e === k.CHART ? d = `Chart ${s}` : e === k.TABLE ? d = `Table ${s}` : e === k.SECTION_HEADER ? d = "### Section Header" : e === k.MEMO ? d = "Executive Memo" : e === k.CLAIM ? d = `Claim ${s}` : e === k.APPENDIX_HEADER ? d = "### Appendix" : e === k.FREEFORM && (d = `Widget ${s}`);
|
|
1320
1331
|
let f = o || d, p = "# Markdown Cell\nEnter your markdown content here.";
|
|
1321
|
-
e ===
|
|
1332
|
+
e === k.CHART ? p = "SELECT unnest([1,2,3,4]) as value, 'example' as dim" : e === k.TABLE ? p = "SELECT [1,2,3,4] as value, 'example' as dim" : e === k.SECTION_HEADER || e === k.APPENDIX_HEADER ? p = "" : e === k.MEMO ? p = {
|
|
1322
1333
|
headline: "",
|
|
1323
1334
|
verdict: "inconclusive",
|
|
1324
1335
|
magnitude: "",
|
|
1325
1336
|
cause: "",
|
|
1326
1337
|
action: "",
|
|
1327
1338
|
confidence: "medium"
|
|
1328
|
-
} : e ===
|
|
1339
|
+
} : e === k.CLAIM ? p = { claim: "" } : e === k.FREEFORM && (p = {
|
|
1329
1340
|
html: ee,
|
|
1330
1341
|
query: "SELECT unnest(['alpha','beta','gamma']) as dim, random() * 100 as value"
|
|
1331
1342
|
});
|
|
@@ -1336,7 +1347,7 @@ var Ye = class t {
|
|
|
1336
1347
|
name: f,
|
|
1337
1348
|
width: 0,
|
|
1338
1349
|
height: 0,
|
|
1339
|
-
allowCrossFilter: !(e ===
|
|
1350
|
+
allowCrossFilter: !(e === k.SECTION_HEADER || e === k.APPENDIX_HEADER || e === k.MEMO)
|
|
1340
1351
|
}, this.nextId++, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, s;
|
|
1341
1352
|
}
|
|
1342
1353
|
setLayoutType(e) {
|
|
@@ -1427,22 +1438,22 @@ var Ye = class t {
|
|
|
1427
1438
|
}
|
|
1428
1439
|
updateItemFilters(e) {
|
|
1429
1440
|
let t = this.gridItems[e];
|
|
1430
|
-
t &&
|
|
1441
|
+
t && O(t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
|
|
1431
1442
|
}
|
|
1432
1443
|
removeItemCrossFilter(e, t) {
|
|
1433
|
-
|
|
1444
|
+
Re(this.gridItems, e, t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
|
|
1434
1445
|
}
|
|
1435
1446
|
removeItemCrossFilterSource(e) {
|
|
1436
|
-
let t = !!this.gridItems[e]?.chartFilters?.length, n =
|
|
1447
|
+
let t = !!this.gridItems[e]?.chartFilters?.length, n = ze(this.gridItems, e);
|
|
1437
1448
|
return (n.length > 0 || t) && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0), n;
|
|
1438
1449
|
}
|
|
1439
1450
|
removeAllFilters() {
|
|
1440
|
-
let e =
|
|
1451
|
+
let e = Be(this.gridItems);
|
|
1441
1452
|
for (let e in this.gridItems) this.gridItems[e].parameters = {};
|
|
1442
1453
|
return e.length > 0 && (this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0), e;
|
|
1443
1454
|
}
|
|
1444
1455
|
updateItemCrossFilters(e, t, n, r) {
|
|
1445
|
-
let i =
|
|
1456
|
+
let i = Le(this.gridItems, e, t, n, r);
|
|
1446
1457
|
return this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, i;
|
|
1447
1458
|
}
|
|
1448
1459
|
updateItemName(e, t) {
|
|
@@ -1547,7 +1558,7 @@ var Ye = class t {
|
|
|
1547
1558
|
children: n.children || []
|
|
1548
1559
|
});
|
|
1549
1560
|
}
|
|
1550
|
-
},
|
|
1561
|
+
}, Ye = .5, Xe = 1e4, Ze = 2e5, Qe = [
|
|
1551
1562
|
{
|
|
1552
1563
|
value: "demo",
|
|
1553
1564
|
label: "Demo (limited messages)"
|
|
@@ -1572,13 +1583,13 @@ var Ye = class t {
|
|
|
1572
1583
|
value: "deepseek",
|
|
1573
1584
|
label: "DeepSeek"
|
|
1574
1585
|
}
|
|
1575
|
-
],
|
|
1586
|
+
], $e = Object.fromEntries(Qe.map((e) => [e.value, e.label])), et = {
|
|
1576
1587
|
anthropic: "sk-ant-...",
|
|
1577
1588
|
openai: "sk-...",
|
|
1578
1589
|
google: "AIza...",
|
|
1579
1590
|
openrouter: "sk-or-...",
|
|
1580
1591
|
deepseek: "sk-..."
|
|
1581
|
-
}, k = "# 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`", nt = "# 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.", rt = ["aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)"], it = [
|
|
1592
|
+
}, A = "# 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`", tt = "# 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.", nt = ["aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)"], rt = [
|
|
1582
1593
|
"aggregate: count|count_distinct|sum|max|min|avg|stddev|variance|array_agg|bool_or|bool_and|any|grouping(<arg1>) ; grouping_id(<arg1>, <arg2>, ..., <argN>)",
|
|
1583
1594
|
"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>)",
|
|
1584
1595
|
"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()",
|
|
@@ -1593,7 +1604,7 @@ var Ye = class t {
|
|
|
1593
1604
|
"conditional/cast: case|simple_case|coalesce|greatest|least(<arg1>, <arg2>, ..., <argN>) ; cast|nullif(<arg1>, <arg2>) ; isnull(<arg1>)",
|
|
1594
1605
|
"geo: geo_from_text|geo_x|geo_y|geo_centroid(<arg1>) ; geo_point|geo_distance(<arg1>, <arg2>) ; geo_transform(<arg1>, <arg2>, <arg3>)",
|
|
1595
1606
|
"other: is_not_distinct(<arg1>, <arg2>)"
|
|
1596
|
-
],
|
|
1607
|
+
], it = [
|
|
1597
1608
|
"string",
|
|
1598
1609
|
"bytes",
|
|
1599
1610
|
"bool",
|
|
@@ -1615,6 +1626,121 @@ var Ye = class t {
|
|
|
1615
1626
|
"unix_seconds",
|
|
1616
1627
|
"unknown",
|
|
1617
1628
|
"any"
|
|
1629
|
+
], at = [
|
|
1630
|
+
{
|
|
1631
|
+
name: "case-when",
|
|
1632
|
+
title: "Conditional values with case (relabel codes / bucket a measure)",
|
|
1633
|
+
summary: "`case when <cond> then <val> ... else <val> end` to relabel codes or bucket a measure into a derived value, then group by it",
|
|
1634
|
+
body: "# `case when <cond> then <val> ... else <val> end` builds a derived value.\n# Assign it to an `auto` concept, then group/filter by it like any other.\n# Use `else null` when unmatched rows should drop out of downstream grouping.\nimport iris as iris;\n\nauto petal_size <- case\n when iris.petal_length < 2.0 then 'small'\n when iris.petal_length < 5.0 then 'medium'\n else 'large'\nend;\n\nselect\n petal_size,\n count(iris.petal_length) as flower_count,\n avg(iris.petal_width) as avg_petal_width,\norder by flower_count desc;\n"
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
name: "chart",
|
|
1638
|
+
title: "Chart statement - layers, roles, settings and reference lines",
|
|
1639
|
+
summary: "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>`",
|
|
1640
|
+
body: "# `chart` is a top-level statement, like `select` - it wraps a query AND says\n# how to draw it. It renders wherever results render: a trilogy code block in a\n# `trilogy render` report, a studio cell, terminal output, and\n# `copy into png|svg|html|pdf '<path>' from chart ...` for a standalone image.\nimport enrollments as enroll;\n\n# --- 1. Minimal form --------------------------------------------------------\n# `layer <type> ( <role> <- <expr> [as <name>], ... )`. The bindings ARE the\n# select: grouping is automatic by the non-aggregated ones, exactly as in a\n# query. NEVER write a separate `select` for a chart.\nchart layer bar ( x_axis <- enroll.department, y_axis <- sum(enroll.credits) as credits );\n\n# A COMPUTED binding must be aliased: `y_axis <- sum(enroll.credits) as credits`.\n# A bare concept reference needs no alias (`x_axis <- enroll.department`) but may\n# take one as a display label. `y_axis <- sum(...)` with no `as` is a parse error.\n#\n# GRAIN: every binding is a select column, so binding a FINER concept to any\n# role (color, group, annotation, ...) refines the grain and splits the marks -\n# exactly as adding that column to a select would. Aggregate annotations at the\n# mark's own grain (`count(enroll.id) as ...`) instead of binding a detail field.\n\n# --- 2. Every part of the statement -----------------------------------------\n# Components may appear in any order after `chart`; at least one `layer` is\n# required, and multiple `layer`s compose into one overlaid chart.\nchart\n set show_title # title from the value-axis label\n set hide_legend\n set scale_y: log # scale_x | scale_y : linear | log | sqrt\n layer bar (\n x_axis <- enroll.department,\n y_axis <- sum(enroll.credits) as credits,\n color <- enroll.completed, # one legend series per value\n annotation <- count(enroll.id) as enrollments # text label on each mark\n )\n order by credits desc # category order follows ORDER BY\n limit 10 # (ascending when there is no ORDER BY)\n place hline at 5 as target; # reference rule; `place vline at ...`\n # too. `at` takes a literal; `as <label>`\n # is optional\n\n# --- 3. `from <select>` - when the bindings cannot express the query ---------\n# Bindings alone have no `where` / `having` / hidden columns. Add `from` plus a\n# COMPLETE select, which carries its own clauses in the usual order:\nchart layer line (\n x_axis <- enroll.year,\n y_axis <- completions,\n color <- enroll.department\n )\n from\n where enroll.year >= 2016\n select\n enroll.year,\n enroll.department,\n count(enroll.id ? enroll.completed = true) as completions\n order by enroll.year asc;\n# With `from select`, EVERY binding must be a bare reference to one of the\n# select's outputs - put transformations inside the select - and layer-level\n# `order by` / `limit` are rejected: they belong to the select.\n\n# --- 4. Types ---------------------------------------------------------------\n# bar vertical bars: x_axis = category, y_axis = measure\n# barh horizontal bars: x_axis = MEASURE, y_axis = category (roles swap)\n# line x_axis = the ordered axis (date/year), y_axis = measure\n# point scatter; `size <- <measure>` scales the marks\n# area line, filled\n# headline big KPI number - needs only `x_axis <- <measure> as <name>`\n# donut the numeric binding sizes the slices; the other positional one -\n# or `color` - names them. No annotations.\n# heatmap x_axis and y_axis are BOTH categories, `color` is the measure\n# boxplot x_axis = category, y_axis = the distribution being summarized\n# `treemap` parses but no renderer implements it; the error names what does.\nchart layer barh ( x_axis <- sum(enroll.credits) as credits, y_axis <- enroll.department );\nchart layer headline ( x_axis <- count(enroll.student_id) as students );\nchart layer donut ( x_axis <- enroll.department, y_axis <- sum(enroll.credits) as credits );\nchart layer heatmap (\n x_axis <- enroll.year,\n y_axis <- enroll.department,\n color <- sum(enroll.credits) as credits\n);\n# A boxplot summarizes RAW rows, so its select has to carry the fact's row grain\n# - hidden with `--` - or output dedup collapses the distribution to the set of\n# distinct values.\nchart layer boxplot ( x_axis <- enroll.department, y_axis <- enroll.credits )\n from select --enroll.id, enroll.department, enroll.credits;\n\n# --- 5. Roles ---------------------------------------------------------------\n# x_axis, y_axis position\n# color one series per value, WITH a legend\n# size mark size (point)\n# group side-by-side bars, or a per-series split on\n# line/point/area - no legend\n# x_trellis / y_trellis small multiples (columns / rows)\n# annotation per-mark text label\n# geo reserved - parses, then raises\n# Each role may be bound at most once per layer; an unknown role name is a parse\n# error. Trellis roles cannot combine with multiple layers, placements, or\n# annotations (Vega-Lite forbids facets inside layered charts).\nchart layer bar (\n x_axis <- enroll.year,\n y_axis <- sum(enroll.credits) as credits,\n group <- enroll.department # side-by-side bars, no legend\n);\n\n# EXPLICIT SERIES COLORS: select a `string::hex` column (the trait comes from\n# `import std.color;`) alongside a `color` binding and each color value takes the\n# hex code found on its rows (missing -> gray); bind the hex column itself to\n# `color` to use the codes directly.\n"
|
|
1641
|
+
},
|
|
1642
|
+
{
|
|
1643
|
+
name: "correlated-exists-via-grouped-counts",
|
|
1644
|
+
title: "Correlated EXISTS / NOT-EXISTS over rows of the SAME model",
|
|
1645
|
+
summary: "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`",
|
|
1646
|
+
body: "# A self-referential EXISTS / NOT-EXISTS (e.g. \"the ONLY enrollee who completed\n# a course that had more than one enrollee\") becomes TWO filtered counts pinned\n# to the correlation grain, compared in `where`:\n# count(k) by <grain> > 1 # EXISTS another row in the group\n# count(k ? cond) by <grain> = 1 # NOT EXISTS another row matching cond\n# (exactly one matches: the row itself)\n# An aggregate condition in `where` groups by the SELECT grain unless pinned.\n# When the correlation grain DIFFERS from the select grain - as here\n# (correlate by course, select per student) - you MUST pin it with `by <grain>`;\n# a bare `count(...) > 0` would group by the select grain and give wrong rows.\n# SCOPE: the pin fixes only the GROUPS. A `where` aggregate always computes\n# over ALL rows of the model; the other `where` predicates never narrow its\n# input rows - exactly right for whole-table EXISTS checks like this one. If\n# the aggregate should see only already-filtered rows, use `having` or stage\n# it with `then where` (a stage's aggregates see only rows passing earlier\n# stages).\nimport enrollments as enroll;\n\nauto enrollees_per_course <- count(enroll.student_id) by enroll.course;\nauto completers_per_course <- count(enroll.student_id ? enroll.completed = true) by enroll.course;\n\nwhere\n enroll.completed = true\n and enrollees_per_course > 1 # the course had at least one OTHER enrollee\n and completers_per_course = 1 # and no OTHER enrollee completed it\nselect\n enroll.student_id,\n count(enroll.course) as sole_completions\norder by sole_completions desc\nlimit 100;\n"
|
|
1647
|
+
},
|
|
1648
|
+
{
|
|
1649
|
+
name: "dedup-before-aggregate",
|
|
1650
|
+
title: "Deduplicate identical rows before aggregating (UNION DISTINCT)",
|
|
1651
|
+
summary: "a `rowset <- select <cols>` collapses identical tuples to one row (SELECT-grain group by) before a downstream sum - the `distinct`/UNION substitute",
|
|
1652
|
+
body: "# `distinct` and UNION are not available. To collapse identical tuples to one\n# row BEFORE aggregating (so duplicates don't double-count), materialize the\n# tuple in a `rowset <- select <cols>`: a rowset select groups at its own grain,\n# emitting one row per distinct tuple. Then aggregate over the rowset's columns.\n# - ALIAS every rowset column you reference downstream with `as`. Without `as`,\n# the column keeps its FULL source path under the rowset name - `select\n# sales.item.brand_id` is reachable as `rs.sales.item.brand_id`, NOT\n# `rs.brand_id`. `as brand_id` makes it `rs.brand_id`.\nimport enrollments as enroll;\n\n# Collapse repeat (student, course) enrollments to one row each (a student who\n# took a course in multiple years counts once).\nrowset unique_sc <- select\n enroll.student_id as sid,\n enroll.course as course,\n;\n\n# Count over the deduplicated rows - identical (student, course) pairs counted once.\nselect\n unique_sc.course,\n count(unique_sc.sid) as distinct_students,\norder by unique_sc.course asc;\n"
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
name: "except-intersect-setops",
|
|
1656
|
+
title: "Set difference / co-occurrence across sources with except(...) / intersect(...)",
|
|
1657
|
+
summary: "set difference or co-occurrence across sources",
|
|
1658
|
+
body: "# `except(...)` and `intersect(...)` are relational table-valued functions with the\n# SAME shape as `union(...)`: TWO OR MORE inline self-contained select arms, matched\n# by COLUMN POSITION to the trailing `-> (...)` output list. They are SQL SET\n# operators, which differ from the union stack in three ways:\n# - the output is DEDUPLICATED (distinct rows);\n# - whole rows are compared with NULL-SAFE equality (NULL matches NULL - the\n# same identity semantics as `in` / `not in` membership);\n# - for `except(...)` ARM ORDER MATTERS: the FIRST arm is the base population and\n# every later arm is subtracted from it, left to right.\n# Use `except` for \"combinations in A that never appear in B (or C)\" and\n# `intersect` for \"combinations present in every source\".\nimport enrollments as enroll;\n\n# Distinct (course, year) combinations Biology offered that Chemistry did not.\nwith bio_only as except(\n (where enroll.department = 'Biology'\n select enroll.course as course, enroll.year as year),\n (where enroll.department = 'Chemistry'\n select enroll.course as course, enroll.year as year)\n) -> (course, year);\n\nselect\n bio_only.course,\n bio_only.year,\norder by bio_only.course asc nulls first, bio_only.year asc nulls first\nlimit 100;\n\n# Counting the surviving combinations is the idiomatic anti-membership count\n# (e.g. \"customers who bought in store but never online\"):\n# from except((...store combos...), (...web combos...)) -> (last, first, day)\n# select count(day) as combos;\n# ---------------------------------------------------------------------------\n# NOTES:\n# - Same arm rules as `union(...)`: inline arms only, positional binding, equal\n# column count/order/types per arm; reference outputs as `<rowset>.<output>`.\n# - `except((a), (b), (c))` means (a EXCEPT b) EXCEPT c - subtract left to right.\n# `intersect(...)` keeps rows present in EVERY arm (order does not matter).\n# - Outputs are DISTINCT rows: aggregate them directly (`count(x)`) for \"how many\n# unique combinations\" - no extra dedup step needed.\n"
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
name: "existence-anti-join",
|
|
1662
|
+
title: "Existence / anti-join across models",
|
|
1663
|
+
summary: "keeping or excluding rows by whether a key matches another model",
|
|
1664
|
+
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"
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
name: "filtered-aggregate",
|
|
1668
|
+
title: "Filtered aggregate - aggregate only the rows matching a condition",
|
|
1669
|
+
summary: "aggregating only the rows matching a condition, and counting rows correctly",
|
|
1670
|
+
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"
|
|
1671
|
+
},
|
|
1672
|
+
{
|
|
1673
|
+
name: "nested-aggregate-group-average",
|
|
1674
|
+
title: "Compare a per-entity total to the group average of those totals",
|
|
1675
|
+
summary: "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",
|
|
1676
|
+
body: "# Compare each entity's total to the GROUP AVERAGE of those per-entity totals\n# (the \"above 1.2x the group norm\" pattern). Give EACH grain its own `by`: the\n# inner total is per (entity, group); the outer average re-aggregates those\n# totals up to the group. \n# since the aggregates are in the having, both are aggregates *after* the id filter is applied\nimport enrollments as enroll;\n\nauto student_dept_credits <- sum(enroll.credits) by enroll.student_id, enroll.department;\nauto dept_avg_credits <- avg(student_dept_credits) by enroll.department;\n\nwhere enroll.student_id > 103\nselect\n enroll.student_id,\nhaving student_dept_credits > 1.2 * dept_avg_credits;\n\n# BENCHMARK OVER FULL POPULATION + FILTERED OUTPUT: if the department average must span\n# EVERY student but the output should contain only target students (id > 103), put the aggregate\n# comparison in WHERE alongside the output-only filter. Aggregate conditions in WHERE do\n# not filter one another's inputs, so `student_id > 103` does not narrow the department\n# benchmark. Do NOT put the comparison in HAVING after the output filter; that computes a\n# target-students-only department average.\nwhere\n student_dept_credits > 1.2 * dept_avg_credits\n and enroll.student_id > 103\nselect\n enroll.student_id,\n enroll.department,\n student_dept_credits,\n;\n\n# SCOPED INPUT POPULATION: when the totals and their group average must use only\n# rows from a specific period, bind that condition inside the inner aggregate.\n# A separate output-only filter still belongs in the final WHERE and does not\n# narrow the rows feeding either aggregate.\nauto student_dept_credits_2024 <- sum(enroll.credits ? enroll.year = 2024)\n by enroll.student_id, enroll.department;\nauto dept_avg_credits_2024 <- avg(student_dept_credits_2024) by enroll.department;\n\nwhere\n student_dept_credits_2024 > 1.2 * dept_avg_credits_2024\n and enroll.student_id > 103\nselect\n enroll.student_id,\n enroll.department,\n student_dept_credits_2024,\n;\n\n# HIDDEN GROUP KEY: if the requested output is one row per (student, department)\n# but displays only student_id, keep department in the select with the `--`\n# PREFIX. Otherwise identical displayed student rows are deduplicated and the\n# required per-department row grain is lost.\nwhere student_dept_credits_2024 > 1.2 * dept_avg_credits_2024\nselect\n enroll.student_id,\n --enroll.department,\n;\n\n# For more complex boundaries, compute the full-population benchmark in a separate rowset\n# with no output filter, then apply the subset filter only in the final selection.\n"
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
name: "pivot-columns",
|
|
1680
|
+
title: "Pivot dimension values into columns",
|
|
1681
|
+
summary: "turn distinct values of a dimension into columns with a reusable `def` macro + filtered aggregate",
|
|
1682
|
+
body: "# A `def` macro expands inline at each `@call`. Pair it with a filtered\n# aggregate to project one column per category value (a pivot). With no row\n# dimension in the select this yields a single summary row.\nimport iris as iris;\n\ndef species_avg(s) -> avg(iris.petal_length ? iris.species = s);\n\nselect\n @species_avg('setosa') as setosa_avg_petal,\n @species_avg('versicolor') as versicolor_avg_petal,\n @species_avg('virginica') as virginica_avg_petal,\n;\n"
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
name: "python-datasource",
|
|
1686
|
+
title: "Python script datasource - Arrow IPC producer plus Trilogy model",
|
|
1687
|
+
summary: "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",
|
|
1688
|
+
body: "# Python datasources require `enable_python_datasources = true` under\n# `[engine.config]` in trilogy.toml. Create these TWO files side by side.\n#\n# File 1: fibonacci.py\n# ---------------------------------------------------------------------------\n# #!/usr/bin/env -S uv run\n# # /// script\n# # requires-python = \">=3.11\"\n# # dependencies = [\"pyarrow>=16\", \"pytrilogy\"]\n# # ///\n#\n# from trilogy.io import run\n#\n#\n# def fibonacci():\n# values = [0, 1]\n# while len(values) < 20:\n# values.append(values[-1] + values[-2])\n# return [{\"index\": i, \"value\": v} for i, v in enumerate(values)]\n#\n#\n# if __name__ == \"__main__\":\n# raise SystemExit(run(fibonacci))\n#\n# `run` converts whatever you return -- a pyarrow table, a pandas/polars frame,\n# a list of dicts, or an iterator of those -- and gives the script a CLI\n# (`--limit`, `--filter`, `--columns`, `--describe`). Run\n# `trilogy source describe ./fibonacci.py` to print the datasource block below\n# instead of writing it against a guessed schema.\n#\n# IMPORTANT: stdout must contain ONLY the Arrow IPC stream. Send diagnostics\n# to stderr. A PEP 723 block lets `uv run` install script dependencies.\n#\n# File 2: query.preql\n# ---------------------------------------------------------------------------\nkey index int;\nproperty index.value int;\n\n# The left side is the physical Arrow column; the right side is the Trilogy\n# concept declared above. The file suffix identifies this as a Python source.\ndatasource fibonacci_numbers (\n index: index,\n value: value,\n)\ngrain (index)\nfile `./fibonacci.py`;\n\n# A datasource declaration does NOT create a namespace. Use local concepts as\n# `index` and `value`, NOT `fibonacci_numbers.index` / `.value`.\nselect\n index,\n value,\norder by index;\n"
|
|
1689
|
+
},
|
|
1690
|
+
{
|
|
1691
|
+
name: "query-structure",
|
|
1692
|
+
title: "Full query structure - every clause and where it goes (+ rowsets)",
|
|
1693
|
+
summary: "the exact clause order of a query, and how to define a rowset",
|
|
1694
|
+
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"
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
name: "rank-over-rollup",
|
|
1698
|
+
title: "Rank within each rollup level (one window over the rollup)",
|
|
1699
|
+
summary: "rank rollup subtotals/leaves with a SINGLE `rank(a,b) over (partition by level, parent ...)` - not separate ranks per level",
|
|
1700
|
+
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"
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
name: "rollup",
|
|
1704
|
+
title: "Subtotals + grand total in one pass with `by rollup (…)`",
|
|
1705
|
+
summary: "subtotals and a grand total in one pass, and labeling the levels",
|
|
1706
|
+
body: "# A `by rollup (d1, d2)` clause AFTER the select list (before having/order by)\n# computes the whole select at MULTIPLE grain levels in one pass - the grouping\n# sets (d1, d2), (d1), () - i.e. per-(d1,d2) leaf rows, a subtotal per d1, and a\n# grand total. (`by cube (d1, d2)` = every combination; `by rollup ()` over the\n# select's own grain.) It applies to EVERY aggregate in the select, so all\n# measures share one consistent grouping.\nimport enrollments as enroll;\n\n# `grouping(d)` = 1 when d is ROLLED UP on a row (a subtotal/grand-total row), 0 on\n# a leaf - use it to LABEL those rows: the rolled-up dimension is otherwise NULL\n# and would collide with any genuine NULL in the data.\nselect\n case when grouping(enroll.department) = 1 then 'ALL DEPARTMENTS'\n else enroll.department end as department,\n case when grouping(enroll.course) = 1 then 'all courses'\n else enroll.course end as course,\n sum(enroll.credits) as total_credits\nby rollup (enroll.department, enroll.course)\nhaving total_credits > 0\norder by department asc nulls first, course asc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - CLAUSE ORDER matches SQL: the `by rollup (…)` grouping clause comes BEFORE\n# `having` (select list -> by rollup -> having -> order by -> limit). The\n# HAVING filters EVERY grouping level - subtotal and grand-total rows too.\n# - SORT: use EXACTLY the sort the question asks for. Rollup rows are ordinary rows\n# and sort with everything else (their rolled-up dimensions are NULL, hence\n# `nulls first`/`nulls last` decides where they land). Sorting by LEVEL is OPTIONAL\n# and only when you actually want leaves/subtotals/totals grouped together: ORDER BY\n# cannot compute a fresh aggregate, so PROJECT the level (`grouping(a) + grouping(b)`)\n# as a HIDDEN (`--`) column and sort by its alias:\n# --grouping(enroll.department) + grouping(enroll.course) as _level\n# order by _level asc, department asc nulls first\n# Do NOT add `_level` to a sort the question already specified - a leading sort key\n# changes WHICH rows survive a `limit`, so it can silently drop the totals.\n# - COMPOSITE measures just work: `sum(a) - sum(b) as net` rolls up BOTH operands\n# to the same levels because the `by rollup (…)` clause covers the whole select.\n# To zero-fill a measure, wrap it: `coalesce(sum(a), 0) as a`.\n# - FILTER-BEFORE-ROLLUP vs `having`: `by rollup (…) having x > t` filters EVERY level AFTER\n# the rollup, so the subtotal/total rows still AGGREGATE the below-threshold leaves (they\n# just aren't displayed). To keep only leaf groups meeting a threshold and roll up JUST\n# those survivors, filter the leaves in a rowset FIRST (`rowset kept <- select … having x >\n# t`), then `by rollup ()` over `kept`. `having` on the rollup itself is the wrong tool when\n# subtotals must exclude the filtered-out leaves.\n"
|
|
1707
|
+
},
|
|
1708
|
+
{
|
|
1709
|
+
name: "row-level-where-vs-having",
|
|
1710
|
+
title: "Row-level filter (WHERE) vs aggregate filter (HAVING) - don't wrap a per-row threshold in max()/min()",
|
|
1711
|
+
summary: "choosing between WHERE and HAVING for a threshold condition",
|
|
1712
|
+
body: "# A non-aggregate, per-ROW condition belongs in WHERE - it filters the rows that\n# feed every aggregate, BEFORE grouping. HAVING is ONLY for filtering on an\n# aggregate RESULT. These are different operations; do not substitute one for the\n# other.\nimport iris as iris;\n\n# RIGHT - keep only large-sepal FLOWERS, then average their petals.\n# `sepal_length > 6.0` is a per-row test on a column -> WHERE.\nwhere iris.sepal_length > 6.0\nselect\n iris.species,\n avg(iris.petal_length) as avg_petal,\norder by iris.species asc;\n\n# WRONG - `having max(iris.sepal_length) by iris.species > 6.0` does NOT keep\n# large-sepal rows. It keeps whole SPECIES whose single largest flower exceeds\n# 6.0, then averages ALL of that species' flowers (small ones included). Wrapping\n# a per-row threshold in max()/min() in HAVING silently changes the meaning, and\n# AND-ing it with another filter (e.g. a rank cutoff) often yields 0 rows because\n# the two now select disjoint populations.\n#\n# Pick by what the condition is ABOUT:\n# per-row value of a column -> WHERE `where x.amount > 10000`\n# an aggregate's result -> HAVING `having total_amount > 10000`\n# (select it, hide with `--`)\n# aggregate just SOME rows -> inline `?` `sum(x.amount ? x.amount > 10000)`\n#\n# An aggregate condition CAN go in WHERE as a pre-filter (`where count(x) > 0`);\n# it groups by the SELECT grain unless pinned with `by <grain>` (`by *` = a\n# whole-table total). SCOPE: a WHERE aggregate computes over ALL rows of the\n# model - the other WHERE predicates never narrow its input (HAVING, selected,\n# and inline-`?` aggregates DO see the filtered rows). `by` pins the groups,\n# never the input rows; for an aggregate over the filtered rows use `having`\n# or a `then where` stage.\n"
|
|
1713
|
+
},
|
|
1714
|
+
{
|
|
1715
|
+
name: "scoped-join",
|
|
1716
|
+
title: "Blend two models in one query with a scoped subset/union join",
|
|
1717
|
+
summary: "blending a second fact model or rowset into one query",
|
|
1718
|
+
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"
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
name: "set-intersect-difference",
|
|
1722
|
+
title: "Set intersection / difference on a multi-column key (presence per group)",
|
|
1723
|
+
summary: "count tuples present in / absent from other groups via per-group presence flags - the multi-column INTERSECT/EXCEPT idiom",
|
|
1724
|
+
body: "# To intersect or difference rows on a MULTI-COLUMN key (the SQL INTERSECT /\n# EXCEPT of several columns), group by the full key tuple and build a presence\n# flag per set with `sum(case when <set> then 1 else 0 end)`. Grouping keeps\n# NULL key parts as their own group (correct INTERSECT/EXCEPT semantics).\n# (Avoid concatenating nullable fields into a single key: `||` over a NULL\n# yields NULL, and `concat()`/`concat_ws()` skip NULLs, which conflates\n# distinct tuples - tuple grouping handles NULL parts correctly.)\nimport enrollments as enroll;\n\n# Presence of each (student_id, course) tuple among completed vs open enrollments.\nauto in_completed <- sum(case when enroll.completed = true then 1 else 0 end)\n by enroll.student_id, enroll.course;\nauto in_open <- sum(case when enroll.completed = false then 1 else 0 end)\n by enroll.student_id, enroll.course;\n\nselect\n # intersection: tuples appearing in BOTH sets\n sum(case when in_completed > 0 and in_open > 0 then 1 else 0 end) as in_both,\n # difference: tuples completed but never open (completed EXCEPT open)\n sum(case when in_completed > 0 and in_open = 0 then 1 else 0 end) as completed_only,\n;\n"
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
name: "staged-membership",
|
|
1728
|
+
title: "Stage a coarse-grain membership set, then filter a fine-grain query",
|
|
1729
|
+
summary: "filtering a fine-grain query by a set computed at a coarser grain",
|
|
1730
|
+
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"
|
|
1731
|
+
},
|
|
1732
|
+
{
|
|
1733
|
+
name: "union-stack-channels",
|
|
1734
|
+
title: "Stack rows from several sources/channels with the union(...) TVF",
|
|
1735
|
+
summary: "stacking several sources/channels together as rows",
|
|
1736
|
+
body: "# `union(...)` is a relational table-valued function: it STACKS the rows of TWO OR\n# MORE self-contained `select` arms, positionally (like SQL `UNION ALL`), into one\n# named result. Use it to combine CHANNELS / SOURCES / labeled populations - ONE\n# ARM PER SOURCE, to any amount. It\n# is a row STACK, NOT a key-join: arms are matched by COLUMN POSITION, so every arm\n# must project the same number of columns, in the same order and types, as the\n# trailing `-> (...)` output list.\nimport enrollments as enroll;\n\n# IMPORTANT: each arm is an INLINE, self-contained select written directly inside\n# the `union(...)` - you CANNOT pass the name of a previously-defined rowset as an\n# arm; put the full select logic (its own `where`, its own aggregation) in the arm.\n# The arms' i-th column maps to the i-th name in the `-> (...)` output list; the\n# surface names inside each arm need not match - only POSITION does. Each arm may\n# group by its own dims (here `course`), so an arm can emit MANY rows.\nwith by_channel as union(\n (where enroll.department = 'Biology'\n select\n 'Biology' as channel,\n enroll.course as course,\n count(enroll.id) as enrollments,\n ),\n (where enroll.department = 'Chemistry'\n select\n 'Chemistry' as channel,\n enroll.course as course,\n count(enroll.id) as enrollments,\n ),\n (where enroll.department = 'Physics'\n select\n 'Physics' as channel,\n enroll.course as course,\n count(enroll.id) as enrollments,\n )\n) -> (channel, course, enrollments);\n\n# Reference the stacked outputs as `<rowset>.<output>`.\nselect\n by_channel.channel,\n by_channel.course,\n by_channel.enrollments,\norder by by_channel.channel, by_channel.enrollments desc nulls first\nlimit 100;\n\n# ---------------------------------------------------------------------------\n# NOTES:\n# - Pass TWO OR MORE arms - add a fourth/fifth the same way; one arm per source.\n# - The trailing `-> (channel, course, enrollments)` NAMES the positional outputs;\n# column order / count / type must line up across every arm.\n# - Every union output is treated as a KEY (grain component). To RE-AGGREGATE the\n# stack (e.g. a grand total across channels), wrap it in an outer aggregate:\n# select sum(by_channel.enrollments) by rollup by_channel.channel as total;\n# - An arm may carry its OWN query-scoped join (`select ... subset join b = a`\n# after its select list) - localize each source's join to the arm that needs it.\n# - This is NOT the forbidden SQL `UNION` keyword between two selects; it is the\n# `union(...)` function form - the cleanest way to stack rows from several\n# sources.\n# - AGGREGATING across a union/set-op: an arm's `select` dedups to its own output\n# grain BEFORE stacking (all selects are select distinct), \n# so stacking raw per-row values (a measure, a `1` flag)\n# and summing/counting OUTSIDE the union silently undercounts whenever two source\n# rows in one arm project the same tuple (e.g. two same-priced sales in a week).\n# Pre-aggregate INSIDE each arm (`sum(sales) as v`, `sum(1) as cnt` - as this\n# example's `count(enroll.id)` does), or pull the arm's grain key through as an\n# extra output column so every fact row stays distinct (downstream selects can\n# just ignore it). And count a GUARANTEED-non-null key, never a nullable display\n# column: `except`/`intersect` keep all-null rows (NULL-safe), which\n# `count(nullable_col)` then silently skips.\n"
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
name: "window-period-over-period",
|
|
1740
|
+
title: "Period-over-period ratio with a window function",
|
|
1741
|
+
summary: "`lead`/`lag` over (order by ...) to compare a row to an offset period; build a scoped set with `<col> ? <cond>` and filter with `in`",
|
|
1742
|
+
body: "# `<col> ? <cond>` builds a filtered set of values you can match with `in`.\n# lead()/lag() over (order by ...) compares each row to an offset row - here\n# each year's enrollment count divided by the PRIOR year's.\nimport enrollments as enroll;\n\nauto recent_years <- enroll.year ? enroll.year >= 2015;\n\ndef vs_prior_year(amt) -> round(amt / (lag(amt, 1) over (order by enroll.year asc)), 2);\n\nwhere enroll.year in recent_years\nselect\n enroll.year,\n @vs_prior_year(count(enroll.id)) as enrollment_growth,\nhaving\n enrollment_growth is not null\norder by enroll.year asc nulls first;\n"
|
|
1743
|
+
}
|
|
1618
1744
|
];
|
|
1619
1745
|
//#endregion
|
|
1620
1746
|
//#region dashboards/prompts.ts
|
|
@@ -1688,25 +1814,25 @@ function ft(e) {
|
|
|
1688
1814
|
}
|
|
1689
1815
|
var pt = `You are a world-class assistant for generating queries in Trilogy, a SQL inspired language with similar syntax, Use the the syntax description below and field information to answer the user request. Return your answer with a short reasoning and a valid Trilogy query in triple double quotes.
|
|
1690
1816
|
|
|
1691
|
-
${
|
|
1817
|
+
${A}
|
|
1692
1818
|
|
|
1693
1819
|
For any response to the user, use this format -> put your actual response within triple double quotes with thinking and justification before it, in this format (replace placeholders with relevant content): Reasoning: {{reasoning}} """{{ response }}"""
|
|
1694
1820
|
`;
|
|
1695
|
-
function
|
|
1821
|
+
function j(e) {
|
|
1696
1822
|
return ft(e).map((e) => `[name: ${e.name} | type:${e.type}${e.calculation ? " | alias_for:" + e.calculation : ""}${e.description ? " | description:" + e.description : ""}]`).join(", ");
|
|
1697
1823
|
}
|
|
1698
1824
|
function mt(e, t) {
|
|
1699
|
-
return `${pt}. Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: ${
|
|
1825
|
+
return `${pt}. Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: ${j(t)}, create the best valid Trilogy query to answer the following user input: "${e}" Return the query within triple double quotes with your thinking and justification before it, so of this form as a jinja template: Reasoning: {{reasoning_placeholder}} """{{ trilogy }}""". Example: Because the user asked for sales by year, and revenue is the best sales related field available, we can aggregate revenue by year: """SELECT order.year, sum(revenue) as year_revenue order by order.year asc;"""`;
|
|
1700
1826
|
}
|
|
1701
1827
|
function ht(e, t) {
|
|
1702
|
-
return `${pt}. Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: ${
|
|
1828
|
+
return `${pt}. Using these base and aliased calculations, derivations thereof created with valid Trilogy, and any extra context you have: ${j(t)}, create the closest matching syntactically valid WHERE CLAUSE to filter a dashboard from this request """${e}""". Return only the portion of a SQL query representing the where clause, excluding any ordering. Do not include comments. After you generate the code, review it carefully to make sure it is only the partial where clause of a select, starting with WHERE (imagine if you put a "select 1 ..." in front of it.), with no semicolon at the end. Return your generated code within triple double quotes with thinking and justification before it in the form (as a jinja example) Reasoning: {{reasoning}} """{{ trilogy }}""". Example: Reasoning: Because the user asked for rainbow colors, we can filter to where the color is a primary color """where color in ('red', 'blue', 'violet')"""`;
|
|
1703
1829
|
}
|
|
1704
1830
|
function gt(e, t) {
|
|
1705
|
-
return ct(e,
|
|
1831
|
+
return ct(e, j(t));
|
|
1706
1832
|
}
|
|
1707
1833
|
//#endregion
|
|
1708
1834
|
//#region llm/sharedToolSchemas.ts
|
|
1709
|
-
var
|
|
1835
|
+
var M = [
|
|
1710
1836
|
"line",
|
|
1711
1837
|
"bar",
|
|
1712
1838
|
"barh",
|
|
@@ -1720,7 +1846,7 @@ var _t = [
|
|
|
1720
1846
|
"boxplot",
|
|
1721
1847
|
"treemap",
|
|
1722
1848
|
"beeswarm"
|
|
1723
|
-
],
|
|
1849
|
+
], _t = {
|
|
1724
1850
|
chartType: "bar",
|
|
1725
1851
|
xField: "category",
|
|
1726
1852
|
yField: "revenue",
|
|
@@ -1737,13 +1863,13 @@ var _t = [
|
|
|
1737
1863
|
scaleX: "linear",
|
|
1738
1864
|
scaleY: "linear",
|
|
1739
1865
|
linkY2: !1
|
|
1740
|
-
},
|
|
1866
|
+
}, vt = {
|
|
1741
1867
|
type: "object",
|
|
1742
1868
|
description: "One layer of a layered chart, drawn over the same result set.",
|
|
1743
1869
|
properties: {
|
|
1744
1870
|
chartType: {
|
|
1745
1871
|
type: "string",
|
|
1746
|
-
enum:
|
|
1872
|
+
enum: M,
|
|
1747
1873
|
description: "Type of chart to render"
|
|
1748
1874
|
},
|
|
1749
1875
|
xField: {
|
|
@@ -1776,7 +1902,7 @@ var _t = [
|
|
|
1776
1902
|
description: "Which side to draw this layer value axis on (optional)"
|
|
1777
1903
|
}
|
|
1778
1904
|
}
|
|
1779
|
-
},
|
|
1905
|
+
}, yt = {
|
|
1780
1906
|
chartType: "bar",
|
|
1781
1907
|
showTitle: !0,
|
|
1782
1908
|
layers: [{
|
|
@@ -1793,13 +1919,13 @@ var _t = [
|
|
|
1793
1919
|
value: 1e3,
|
|
1794
1920
|
label: "target"
|
|
1795
1921
|
}]
|
|
1796
|
-
},
|
|
1922
|
+
}, N = {
|
|
1797
1923
|
type: "object",
|
|
1798
1924
|
description: "Chart configuration object",
|
|
1799
1925
|
properties: {
|
|
1800
1926
|
chartType: {
|
|
1801
1927
|
type: "string",
|
|
1802
|
-
enum:
|
|
1928
|
+
enum: M,
|
|
1803
1929
|
description: "Type of chart to render"
|
|
1804
1930
|
},
|
|
1805
1931
|
xField: {
|
|
@@ -1874,7 +2000,7 @@ var _t = [
|
|
|
1874
2000
|
},
|
|
1875
2001
|
layers: {
|
|
1876
2002
|
type: "array",
|
|
1877
|
-
items:
|
|
2003
|
+
items: vt,
|
|
1878
2004
|
description: "Draw several marks over one result set, e.g. bars for a total with a line for an average. When set, the chart type and field bindings above are ignored and each layer supplies its own; the settings above (title, legend, scales) still apply to the chart as a whole. Only line, bar, barh, area, point, boxplot, donut and heatmap can be layers. Cannot be combined with trellisField/trellisRowField. Leave unset for an ordinary single-mark chart."
|
|
1879
2005
|
},
|
|
1880
2006
|
placements: {
|
|
@@ -1904,7 +2030,7 @@ var _t = [
|
|
|
1904
2030
|
description: "Force layers to share one y-axis (true) or get independent ones (false). Omit to let the chart decide from the layers value formats (optional)"
|
|
1905
2031
|
}
|
|
1906
2032
|
}
|
|
1907
|
-
},
|
|
2033
|
+
}, bt = M.map((e) => `'${e}'`).join(", "), P = `Chart type is auto-detected based on data shape unless you specify a chartConfig. Only provide chartConfig if the user specifically requests a chart type or configuration; otherwise let auto-detection handle it.
|
|
1908
2034
|
|
|
1909
2035
|
[Important!] Hiding fields used only for filtering/query structure from output - such as ranks - with the "--" trilogy select syntax may be useful to get the right auto-formatting.
|
|
1910
2036
|
|
|
@@ -1913,12 +2039,12 @@ You DO NOT need to specify colorField as the hexField if it is included; it will
|
|
|
1913
2039
|
You can also add a default available Trilogy import "import std.color;" at the top of a query and cast a string case statement of hex values to ::string::hex to create mappings yourself.
|
|
1914
2040
|
Do this if the user asks for specific coloring.
|
|
1915
2041
|
|
|
1916
|
-
Example chartConfig: ${JSON.stringify(
|
|
2042
|
+
Example chartConfig: ${JSON.stringify(_t)}
|
|
1917
2043
|
|
|
1918
2044
|
To draw more than one mark over the same results — bars for a total with a line for an average, say — set 'layers' instead of the top-level chart type and fields. Each layer names its own chartType and fields; title, legend and scale settings stay on the parent. Add 'placements' for constant reference lines.
|
|
1919
|
-
Example layered chartConfig: ${JSON.stringify(
|
|
2045
|
+
Example layered chartConfig: ${JSON.stringify(yt)}
|
|
1920
2046
|
|
|
1921
|
-
Available chartTypes: ${
|
|
2047
|
+
Available chartTypes: ${bt}`, F = {
|
|
1922
2048
|
name: "connect_data_connection",
|
|
1923
2049
|
description: "Connect or reconnect a data connection that is not currently active. Use this when a query fails because the connection is not active, or when you need to establish a connection before running queries.",
|
|
1924
2050
|
input_schema: {
|
|
@@ -1932,16 +2058,16 @@ Available chartTypes: ${xt}`, N = {
|
|
|
1932
2058
|
};
|
|
1933
2059
|
//#endregion
|
|
1934
2060
|
//#region llm/chatAgentPrompt.ts
|
|
1935
|
-
function
|
|
2061
|
+
function xt(e) {
|
|
1936
2062
|
return e({
|
|
1937
|
-
trilogySyntaxReference:
|
|
1938
|
-
rulesInput:
|
|
1939
|
-
aggFunctions:
|
|
1940
|
-
functions:
|
|
1941
|
-
datatypes:
|
|
2063
|
+
trilogySyntaxReference: A,
|
|
2064
|
+
rulesInput: tt,
|
|
2065
|
+
aggFunctions: nt,
|
|
2066
|
+
functions: rt,
|
|
2067
|
+
datatypes: it
|
|
1942
2068
|
});
|
|
1943
2069
|
}
|
|
1944
|
-
var
|
|
2070
|
+
var I = {
|
|
1945
2071
|
name: "return_to_user",
|
|
1946
2072
|
description: "Signal that you are done and return control to the user. You MUST call this tool when you have completed all requested tasks and are ready for the user's next input. Provide a brief summary of what was accomplished. Never end a turn with plain text — always call this tool when finished.",
|
|
1947
2073
|
input_schema: {
|
|
@@ -1952,7 +2078,7 @@ var Ct = {
|
|
|
1952
2078
|
} },
|
|
1953
2079
|
required: ["message"]
|
|
1954
2080
|
}
|
|
1955
|
-
},
|
|
2081
|
+
}, St = [
|
|
1956
2082
|
{
|
|
1957
2083
|
name: "run_trilogy_query",
|
|
1958
2084
|
description: "Execute a Trilogy query and return tabular results. Use this for data exploration, aggregations, and when the user wants to see raw data in a table format.",
|
|
@@ -1973,7 +2099,7 @@ var Ct = {
|
|
|
1973
2099
|
},
|
|
1974
2100
|
{
|
|
1975
2101
|
name: "chart_trilogy_query",
|
|
1976
|
-
description: `Execute a Trilogy query and display results as a chart. ${
|
|
2102
|
+
description: `Execute a Trilogy query and display results as a chart. ${P}`,
|
|
1977
2103
|
input_schema: {
|
|
1978
2104
|
type: "object",
|
|
1979
2105
|
properties: {
|
|
@@ -1986,7 +2112,7 @@ var Ct = {
|
|
|
1986
2112
|
description: "Data connection name to run the query against"
|
|
1987
2113
|
},
|
|
1988
2114
|
chartConfig: {
|
|
1989
|
-
...
|
|
2115
|
+
...N,
|
|
1990
2116
|
description: "Optional chart configuration. Only provide if user specifically requests a chart type or configuration."
|
|
1991
2117
|
}
|
|
1992
2118
|
},
|
|
@@ -2014,7 +2140,7 @@ var Ct = {
|
|
|
2014
2140
|
required: []
|
|
2015
2141
|
}
|
|
2016
2142
|
},
|
|
2017
|
-
|
|
2143
|
+
F,
|
|
2018
2144
|
{
|
|
2019
2145
|
name: "create_markdown",
|
|
2020
2146
|
description: "Create a rich markdown artifact to display formatted text, summaries, reports, or data-driven content. Supports standard markdown syntax (headers, lists, bold, italic, links, tables, code blocks). When a query is provided, data from the query results can be referenced in the markdown template. Use this for narrative reports, executive summaries, annotated insights, or any rich text content.",
|
|
@@ -2107,7 +2233,7 @@ var Ct = {
|
|
|
2107
2233
|
description: "New title for the artifact"
|
|
2108
2234
|
},
|
|
2109
2235
|
chartConfig: {
|
|
2110
|
-
...
|
|
2236
|
+
...N,
|
|
2111
2237
|
description: "New chart configuration (for chart/results artifacts)"
|
|
2112
2238
|
}
|
|
2113
2239
|
},
|
|
@@ -2140,28 +2266,28 @@ var Ct = {
|
|
|
2140
2266
|
required: ["artifact_ids"]
|
|
2141
2267
|
}
|
|
2142
2268
|
},
|
|
2143
|
-
|
|
2269
|
+
I
|
|
2144
2270
|
];
|
|
2145
|
-
function
|
|
2271
|
+
function Ct(e, t) {
|
|
2146
2272
|
if (!t || t.length === 0) return e;
|
|
2147
2273
|
let n = new Set(t);
|
|
2148
2274
|
return e.filter((e) => !n.has(e.name));
|
|
2149
2275
|
}
|
|
2150
|
-
function
|
|
2276
|
+
function wt(e, t) {
|
|
2151
2277
|
if (!t || t.length === 0) return e;
|
|
2152
2278
|
let n = new Set(e.map((e) => e.name)), r = /* @__PURE__ */ new Set(), i = t.map((e) => {
|
|
2153
2279
|
let t = e.definition.name;
|
|
2154
2280
|
if (n.has(t)) throw Error(`Host tool '${t}' shadows a built-in chat tool`);
|
|
2155
2281
|
if (r.has(t)) throw Error(`Host tool '${t}' is declared twice`);
|
|
2156
2282
|
return r.add(t), e.definition;
|
|
2157
|
-
}), a = e.findIndex((e) => e.name ===
|
|
2283
|
+
}), a = e.findIndex((e) => e.name === I.name);
|
|
2158
2284
|
return a === -1 ? [...e, ...i] : [
|
|
2159
2285
|
...e.slice(0, a),
|
|
2160
2286
|
...i,
|
|
2161
2287
|
...e.slice(a)
|
|
2162
2288
|
];
|
|
2163
2289
|
}
|
|
2164
|
-
var
|
|
2290
|
+
var Tt = [
|
|
2165
2291
|
{ text: "- Every query and chart tool call returns an artifact ID. Use these IDs to reference, update, or remove artifacts later." },
|
|
2166
2292
|
{
|
|
2167
2293
|
tool: "create_markdown",
|
|
@@ -2187,13 +2313,13 @@ var Dt = [
|
|
|
2187
2313
|
tool: "create_markdown",
|
|
2188
2314
|
text: "- When the user asks for a summary, report, or narrative, prefer create_markdown over just text responses - it renders in the artifacts panel."
|
|
2189
2315
|
}
|
|
2190
|
-
],
|
|
2316
|
+
], Et = [{
|
|
2191
2317
|
tool: "search_docs",
|
|
2192
2318
|
text: "- When you are unsure how to express something in Trilogy — window functions, date handling, filtering on aggregates, an unfamiliar keyword — call search_docs (kind: language or function) and then read_doc, rather than guessing at syntax. The reference above is a summary; the docs carry the full idioms."
|
|
2193
2319
|
}, {
|
|
2194
2320
|
tool: "read_doc",
|
|
2195
2321
|
text: "- read_doc returns the full article for an id from search_docs; quote the relevant part back to the user when they ask how the language works."
|
|
2196
|
-
}],
|
|
2322
|
+
}], Dt = [
|
|
2197
2323
|
{
|
|
2198
2324
|
tool: "list_artifacts",
|
|
2199
2325
|
text: "Call list_artifacts to see everything currently in the panel."
|
|
@@ -2212,21 +2338,21 @@ var Dt = [
|
|
|
2212
2338
|
},
|
|
2213
2339
|
{ text: "The artifact panel should tell a coherent story that directly answers the user's latest request — not accumulate a growing pile from every prior turn." }
|
|
2214
2340
|
];
|
|
2215
|
-
function
|
|
2341
|
+
function L(e, t) {
|
|
2216
2342
|
return e.filter((e) => !e.tool || !t.includes(e.tool));
|
|
2217
2343
|
}
|
|
2218
|
-
function
|
|
2219
|
-
let { dataConnectionName: t, availableConnections: n, availableConcepts: r, activeImports: i = [], availableImportsForConnection: a = [], isDataConnectionActive: o = !0, disabledTools: s = [] } = e, c =
|
|
2344
|
+
function Ot(e) {
|
|
2345
|
+
let { dataConnectionName: t, availableConnections: n, availableConcepts: r, activeImports: i = [], availableImportsForConnection: a = [], isDataConnectionActive: o = !0, disabledTools: s = [] } = e, c = L(Tt, s).map((e) => e.text).join("\n"), l = L(Dt, s).map((e, t) => `${t + 1}. ${e.text}`).join("\n"), u = L(Et, s), d = u.length > 0 ? `\n\nDOCUMENTATION:\n${u.map((e) => e.text).join("\n")}` : "", f = r && r.length > 0 ? `\n\nAVAILABLE FIELDS FOR QUERIES:\n${j(r)}` : "", p = !s.includes("connect_data_connection"), m = t && !o ? p ? " (NOT CONNECTED - use connect_data_connection tool to connect before running queries)" : " (NOT CONNECTED - queries will fail; tell the user the data connection is not available and call return_to_user)" : "", h = p ? "\n8. If the data connection is not active, use connect_data_connection to establish the connection before running queries" : "", g = t ? `ACTIVE DATA CONNECTION: ${t}${m}` : "No data connection currently selected. Ask the user which connection to use.", _ = i.length > 0 ? `\nACTIVE DATA SOURCE: ${i[0].name}` : "\nNo data source currently selected. Use select_active_import to select a data source, or use list_available_imports to see what is available.", v = a.length > 0 ? `\n\nAVAILABLE DATA SOURCES:\n${a.map((e) => `- ${e.name}${i.some((t) => t.id === e.id) ? " (active)" : ""}`).join("\n")}` : "";
|
|
2220
2346
|
return `You are a data analysis assistant with access to Trilogy query capabilities.
|
|
2221
2347
|
|
|
2222
2348
|
You have access to tools that can select datasources, execute queries and generate visualizations. When a user asks about data, analyze their request, make sure you are looking at the right source, write a valid Trilogy query, and use the appropriate tool.
|
|
2223
2349
|
|
|
2224
|
-
${
|
|
2350
|
+
${g}
|
|
2225
2351
|
AVAILABLE DATA CONNECTIONS: ${n.length > 0 ? n.join(", ") : "None configured - user needs to set up a data connection first"}
|
|
2226
|
-
${
|
|
2352
|
+
${_}${v}
|
|
2227
2353
|
|
|
2228
2354
|
TRILOGY LANGUAGE REFERENCE:
|
|
2229
|
-
${
|
|
2355
|
+
${A}
|
|
2230
2356
|
${f}
|
|
2231
2357
|
|
|
2232
2358
|
IMPORTANT GUIDELINES:
|
|
@@ -2236,8 +2362,7 @@ IMPORTANT GUIDELINES:
|
|
|
2236
2362
|
4. When showing data, prefer tables for detailed exploration and charts for trends/comparisons
|
|
2237
2363
|
5. Use the full field path (e.g., 'order.product.id') - never use FROM clauses
|
|
2238
2364
|
6. Remember: No GROUP BY clause - grouping is implicit by non-aggregated fields in SELECT
|
|
2239
|
-
7. If the user question needs fields that are not in the same source, use select_active_import to switch to a different data source (only one can be active at a time). Always consider this when they change topics
|
|
2240
|
-
8. If the data connection is not active, use connect_data_connection to establish the connection before running queries${d}
|
|
2365
|
+
7. If the user question needs fields that are not in the same source, use select_active_import to switch to a different data source (only one can be active at a time). Always consider this when they change topics.${h}${d}
|
|
2241
2366
|
|
|
2242
2367
|
ARTIFACT MANAGEMENT:
|
|
2243
2368
|
${c}
|
|
@@ -2254,9 +2379,9 @@ COMPLETING YOUR RESPONSE:
|
|
|
2254
2379
|
}
|
|
2255
2380
|
//#endregion
|
|
2256
2381
|
//#region llm/subchatSummarize.ts
|
|
2257
|
-
var
|
|
2258
|
-
async function
|
|
2259
|
-
let r =
|
|
2382
|
+
var kt = "You are summarizing a subchat that has just finished. Read the transcript below (messages and tool calls) and produce a 2–4 sentence summary of what was accomplished, what (if anything) failed or remains, and any concrete artifacts (file names, queries, errors). Be specific. The overseer will use this report to update the user — there is no other source of truth, so do not omit important results.", At = "You are summarizing a subchat's progress for an overseer agent. Read the transcript below (messages and tool calls) and produce a concise 2–4 sentence summary of what has happened so far, what has been accomplished, and what (if anything) remains. Be specific about file names, queries, or errors. Do not editorialize.";
|
|
2383
|
+
async function jt(e, t, n = "completion") {
|
|
2384
|
+
let r = Mt(t.messages), i = n === "completion" ? kt : At, a = e.getFastModel(), o = e.model, s = !!a && a !== o;
|
|
2260
2385
|
try {
|
|
2261
2386
|
return s && (e.model = a), (await e.generateCompletion({
|
|
2262
2387
|
prompt: "",
|
|
@@ -2269,42 +2394,42 @@ async function Nt(e, t, n = "completion") {
|
|
|
2269
2394
|
s && (e.model = o);
|
|
2270
2395
|
}
|
|
2271
2396
|
}
|
|
2272
|
-
function
|
|
2397
|
+
function Mt(e) {
|
|
2273
2398
|
return e.map((e, t) => {
|
|
2274
|
-
let n = [`[${t}] ${e.role}: ${
|
|
2275
|
-
if (e.toolCalls && e.toolCalls.length > 0) for (let t of e.toolCalls) n.push(` → ${t.name}(${
|
|
2276
|
-
if (e.toolResults && e.toolResults.length > 0) for (let t of e.toolResults) n.push(` ← ${t.toolName}: ${
|
|
2399
|
+
let n = [`[${t}] ${e.role}: ${Nt(e.content || "", 800)}`];
|
|
2400
|
+
if (e.toolCalls && e.toolCalls.length > 0) for (let t of e.toolCalls) n.push(` → ${t.name}(${Nt(JSON.stringify(t.input), 200)})`);
|
|
2401
|
+
if (e.toolResults && e.toolResults.length > 0) for (let t of e.toolResults) n.push(` ← ${t.toolName}: ${Nt(t.result, 300)}`);
|
|
2277
2402
|
return n.join("\n");
|
|
2278
2403
|
}).join("\n\n");
|
|
2279
2404
|
}
|
|
2280
|
-
function
|
|
2405
|
+
function Nt(e, t) {
|
|
2281
2406
|
return e.length <= t ? e : e.slice(0, t - 1) + "…";
|
|
2282
2407
|
}
|
|
2283
|
-
var
|
|
2284
|
-
function
|
|
2408
|
+
var Pt = 4, Ft = 8, It = 1600;
|
|
2409
|
+
function R(e) {
|
|
2285
2410
|
return e ? Math.ceil(e.length / 4) : 0;
|
|
2286
2411
|
}
|
|
2287
|
-
function
|
|
2288
|
-
let t =
|
|
2412
|
+
function Lt(e) {
|
|
2413
|
+
let t = Pt + R(e.content);
|
|
2289
2414
|
for (let n of e.toolCalls ?? []) {
|
|
2290
|
-
t +=
|
|
2415
|
+
t += Ft + R(n.name);
|
|
2291
2416
|
try {
|
|
2292
|
-
t +=
|
|
2417
|
+
t += R(JSON.stringify(n.input));
|
|
2293
2418
|
} catch {}
|
|
2294
2419
|
}
|
|
2295
|
-
for (let n of e.toolResults ?? []) t +=
|
|
2420
|
+
for (let n of e.toolResults ?? []) t += Ft + R(n.toolName), t += R(n.result), n.imageData && (t += It);
|
|
2296
2421
|
return t;
|
|
2297
2422
|
}
|
|
2298
|
-
function
|
|
2299
|
-
return e.reduce((e, t) => e +
|
|
2423
|
+
function Rt(e) {
|
|
2424
|
+
return e.reduce((e, t) => e + Lt(t), 0);
|
|
2300
2425
|
}
|
|
2301
2426
|
//#endregion
|
|
2302
2427
|
//#region llm/toolLoopCore.ts
|
|
2303
|
-
var
|
|
2304
|
-
function
|
|
2428
|
+
var zt = "<user_input>", Bt = "</user_input>", z = "<system_input>", B = "</system_input>";
|
|
2429
|
+
function Vt(e) {
|
|
2305
2430
|
return e.replace(/<\/?(?:user_input|system_input)>/gi, "");
|
|
2306
2431
|
}
|
|
2307
|
-
function
|
|
2432
|
+
function Ht(e) {
|
|
2308
2433
|
let t = e?.data;
|
|
2309
2434
|
return !Array.isArray(t) || t.length <= 100 ? {
|
|
2310
2435
|
head: t ?? [],
|
|
@@ -2318,11 +2443,11 @@ function Ut(e) {
|
|
|
2318
2443
|
cutCount: t.length - 100
|
|
2319
2444
|
};
|
|
2320
2445
|
}
|
|
2321
|
-
function
|
|
2446
|
+
function Ut(e) {
|
|
2322
2447
|
if (e.success) if (e.artifact) {
|
|
2323
2448
|
let t = e.artifact.config, n = e.artifact.data, r = e.artifact.id, i = "";
|
|
2324
2449
|
if (n) {
|
|
2325
|
-
let e = typeof n.toJSON == "function" ? n.toJSON() : n, a = t?.resultSize ?? (Array.isArray(e?.data) ? e.data.length : 0), { head: o, tail: s, cutCount: c } =
|
|
2450
|
+
let e = typeof n.toJSON == "function" ? n.toJSON() : n, a = t?.resultSize ?? (Array.isArray(e?.data) ? e.data.length : 0), { head: o, tail: s, cutCount: c } = Ht(e);
|
|
2326
2451
|
if (c > 0) {
|
|
2327
2452
|
let t = JSON.stringify({
|
|
2328
2453
|
...e,
|
|
@@ -2340,7 +2465,7 @@ function Wt(e) {
|
|
|
2340
2465
|
else return "Success.";
|
|
2341
2466
|
else return `Error: ${e.error}`;
|
|
2342
2467
|
}
|
|
2343
|
-
function
|
|
2468
|
+
function Wt(e) {
|
|
2344
2469
|
if (e instanceof Error) return e.message;
|
|
2345
2470
|
if (typeof e == "string") return e;
|
|
2346
2471
|
try {
|
|
@@ -2349,18 +2474,18 @@ function Gt(e) {
|
|
|
2349
2474
|
return String(e);
|
|
2350
2475
|
}
|
|
2351
2476
|
}
|
|
2352
|
-
async function
|
|
2477
|
+
async function Gt(e, t, n, r, i, a, o) {
|
|
2353
2478
|
let s = o.maxIterations ?? 20, c = r.getMessages().slice(0, -1).map((e) => e.role === "user" && !e.hidden && e.content ? {
|
|
2354
2479
|
...e,
|
|
2355
|
-
content: `${
|
|
2356
|
-
} : e), l = `${
|
|
2480
|
+
content: `${zt}${Vt(e.content)}${Bt}`
|
|
2481
|
+
} : e), l = `${zt}${Vt(e)}${Bt}`, u = l, d = !1, f = 0;
|
|
2357
2482
|
for (let e = 0; e < s; e++) {
|
|
2358
2483
|
if (a.checkAborted()) return e === 0 && r.addMessage({
|
|
2359
2484
|
role: "assistant",
|
|
2360
2485
|
content: "(Response in progress when stopped)"
|
|
2361
2486
|
}), r.addMessage({
|
|
2362
2487
|
role: "user",
|
|
2363
|
-
content: `${
|
|
2488
|
+
content: `${z}[User requested pause - conversation can be continued]${B}`,
|
|
2364
2489
|
hidden: !0
|
|
2365
2490
|
}), {
|
|
2366
2491
|
terminated: !1,
|
|
@@ -2373,7 +2498,7 @@ async function Kt(e, t, n, r, i, a, o) {
|
|
|
2373
2498
|
content: "(Response in progress when stopped)"
|
|
2374
2499
|
}), r.addMessage({
|
|
2375
2500
|
role: "user",
|
|
2376
|
-
content: `${
|
|
2501
|
+
content: `${z}[User requested pause - conversation can be continued]${B}`,
|
|
2377
2502
|
hidden: !0
|
|
2378
2503
|
}), {
|
|
2379
2504
|
terminated: !1,
|
|
@@ -2390,7 +2515,7 @@ async function Kt(e, t, n, r, i, a, o) {
|
|
|
2390
2515
|
content: g
|
|
2391
2516
|
}), r.addMessage({
|
|
2392
2517
|
role: "user",
|
|
2393
|
-
content: `${
|
|
2518
|
+
content: `${z}[User requested pause - conversation can be continued]${B}`,
|
|
2394
2519
|
hidden: !0
|
|
2395
2520
|
}), {
|
|
2396
2521
|
terminated: !1,
|
|
@@ -2411,7 +2536,7 @@ async function Kt(e, t, n, r, i, a, o) {
|
|
|
2411
2536
|
};
|
|
2412
2537
|
let e = o.noToolCallReminder ?? "You must call a tool to proceed. If you are finished, call the appropriate completion tool to return control to the user. Do not respond with text only.";
|
|
2413
2538
|
console.log("[toolLoopCore] No tool call — re-prompting agent to use a tool");
|
|
2414
|
-
let t = `${
|
|
2539
|
+
let t = `${z}${e}${B}`;
|
|
2415
2540
|
r.addMessage({
|
|
2416
2541
|
role: "user",
|
|
2417
2542
|
content: t,
|
|
@@ -2449,7 +2574,7 @@ async function Kt(e, t, n, r, i, a, o) {
|
|
|
2449
2574
|
hidden: !0
|
|
2450
2575
|
}), r.addMessage({
|
|
2451
2576
|
role: "user",
|
|
2452
|
-
content: `${
|
|
2577
|
+
content: `${z}[User requested pause - conversation can be continued]${B}`,
|
|
2453
2578
|
hidden: !0
|
|
2454
2579
|
}), {
|
|
2455
2580
|
terminated: !1,
|
|
@@ -2463,7 +2588,7 @@ async function Kt(e, t, n, r, i, a, o) {
|
|
|
2463
2588
|
} catch (n) {
|
|
2464
2589
|
t = {
|
|
2465
2590
|
success: !1,
|
|
2466
|
-
error: `Tool "${e.name}" failed: ${
|
|
2591
|
+
error: `Tool "${e.name}" failed: ${Wt(n)}`
|
|
2467
2592
|
};
|
|
2468
2593
|
}
|
|
2469
2594
|
let n = {
|
|
@@ -2520,7 +2645,7 @@ async function Kt(e, t, n, r, i, a, o) {
|
|
|
2520
2645
|
finalMessage: t.message
|
|
2521
2646
|
};
|
|
2522
2647
|
}
|
|
2523
|
-
let i =
|
|
2648
|
+
let i = Ut(t);
|
|
2524
2649
|
v.push({
|
|
2525
2650
|
toolCallId: e.id,
|
|
2526
2651
|
toolName: e.name,
|
|
@@ -2567,26 +2692,26 @@ async function Kt(e, t, n, r, i, a, o) {
|
|
|
2567
2692
|
}
|
|
2568
2693
|
//#endregion
|
|
2569
2694
|
//#region llm/compaction.ts
|
|
2570
|
-
var
|
|
2571
|
-
function
|
|
2695
|
+
var Kt = 2e4, qt = 6, Jt = 20, Yt = 6, Xt = "You are a context summarization assistant. You are given the earlier portion of an assistant conversation that is about to fall out of the context window. Produce a structured summary that the assistant will rely on INSTEAD of those messages.\n\nUse exactly these sections:\n## Goal — what the user is ultimately trying to accomplish.\n## Progress — what was built, changed, or discovered, with exact names and ids (editors, dashboards, jobs, connections, queries, fields).\n## Key decisions — choices made and why, including approaches tried and rejected.\n## Current state — where the work stands right now.\n## Open items — unresolved questions, known failures, and obvious next steps.\n\nBe concrete: prefer exact identifiers, query text and error messages over paraphrase. Aim for 300-600 words. Omit nothing the assistant would need to continue seamlessly. Do not editorialize and do not add recommendations of your own.";
|
|
2696
|
+
function Zt(e) {
|
|
2572
2697
|
return e.role === "user" && !(e.toolResults && e.toolResults.length > 0);
|
|
2573
2698
|
}
|
|
2574
|
-
function
|
|
2575
|
-
let n = t.keepRecentTokens ?? 2e4, r = t.minKeepRecentCount ??
|
|
2576
|
-
for (let t = e.length - 1; t >= 0 && !(o >= i || o >= r && s >= n); t--) s +=
|
|
2699
|
+
function Qt(e, t) {
|
|
2700
|
+
let n = t.keepRecentTokens ?? 2e4, r = t.minKeepRecentCount ?? qt, i = t.maxKeepRecentCount ?? Jt, a = t.minArchiveCount ?? Yt, o = 0, s = 0;
|
|
2701
|
+
for (let t = e.length - 1; t >= 0 && !(o >= i || o >= r && s >= n); t--) s += Lt(e[t]), o++;
|
|
2577
2702
|
let c = e.length - o;
|
|
2578
|
-
for (; c > 0 && !
|
|
2703
|
+
for (; c > 0 && !Zt(e[c]);) c--;
|
|
2579
2704
|
return c < a ? -1 : c;
|
|
2580
2705
|
}
|
|
2581
|
-
function
|
|
2582
|
-
return !t || t <= 0 ? !1 : (n ??
|
|
2706
|
+
function $t(e, t, n) {
|
|
2707
|
+
return !t || t <= 0 ? !1 : (n ?? Rt(e.getLLMMessages())) > t;
|
|
2583
2708
|
}
|
|
2584
|
-
async function
|
|
2585
|
-
let r =
|
|
2709
|
+
async function en(e, t, n) {
|
|
2710
|
+
let r = Mt(t), i = e.getFastModel(), a = e.model, o = !!i && i !== a;
|
|
2586
2711
|
try {
|
|
2587
2712
|
return o && (e.model = i), (await e.generateCompletion({
|
|
2588
2713
|
prompt: "",
|
|
2589
|
-
systemPrompt: n ? `${
|
|
2714
|
+
systemPrompt: n ? `${Xt}\n\nAdditional emphasis requested: ${n}` : Xt
|
|
2590
2715
|
}, [{
|
|
2591
2716
|
role: "user",
|
|
2592
2717
|
content: `--- TRANSCRIPT ---\n${r}`
|
|
@@ -2595,22 +2720,22 @@ async function tn(e, t, n) {
|
|
|
2595
2720
|
o && (e.model = a);
|
|
2596
2721
|
}
|
|
2597
2722
|
}
|
|
2598
|
-
function
|
|
2723
|
+
function tn(e) {
|
|
2599
2724
|
return {
|
|
2600
2725
|
role: "user",
|
|
2601
2726
|
hidden: !0,
|
|
2602
2727
|
content: e
|
|
2603
2728
|
};
|
|
2604
2729
|
}
|
|
2605
|
-
async function
|
|
2606
|
-
let r = t.getLLMMessages(), i =
|
|
2730
|
+
async function nn(e, t, n = {}) {
|
|
2731
|
+
let r = t.getLLMMessages(), i = Qt(r, n);
|
|
2607
2732
|
if (i < 0) return null;
|
|
2608
|
-
let a = r.slice(0, i), o = await
|
|
2733
|
+
let a = r.slice(0, i), o = await en(e, a, n.focus), s = t.messages.indexOf(r[i]);
|
|
2609
2734
|
if (s < 0) return null;
|
|
2610
2735
|
a.forEach((e) => {
|
|
2611
2736
|
e.archived = !0;
|
|
2612
2737
|
});
|
|
2613
|
-
let c = n.createSummaryMessage ??
|
|
2738
|
+
let c = n.createSummaryMessage ?? tn, l = `${z}[Conversation compacted. Summary of the earlier conversation:]\n\n${o}${n.appendix ?? ""}${B}`;
|
|
2614
2739
|
t.messages.splice(s, 0, c(l));
|
|
2615
2740
|
let u = {
|
|
2616
2741
|
archivedCount: a.length,
|
|
@@ -2618,17 +2743,17 @@ async function rn(e, t, n = {}) {
|
|
|
2618
2743
|
};
|
|
2619
2744
|
return n.onCompacted?.(u), u;
|
|
2620
2745
|
}
|
|
2621
|
-
function
|
|
2746
|
+
function rn(e) {
|
|
2622
2747
|
let t = e.artifacts.filter((e) => !e.hidden);
|
|
2623
2748
|
return t.length === 0 ? "" : `\n\nArtifacts still available via get_artifact/get_artifact_rows:\n${t.map((e) => {
|
|
2624
2749
|
let t = e.config?.title || e.config?.query?.slice(0, 60) || "", n = e.config?.resultSize == null ? "" : `, ${e.config.resultSize} rows`;
|
|
2625
2750
|
return `- [${e.id}] ${e.type}${t ? ` "${t}"` : ""}${n}`;
|
|
2626
2751
|
}).join("\n")}`;
|
|
2627
2752
|
}
|
|
2628
|
-
async function
|
|
2629
|
-
return
|
|
2753
|
+
async function an(e, t, n = {}) {
|
|
2754
|
+
return nn(e, t, {
|
|
2630
2755
|
...n,
|
|
2631
|
-
appendix: n.appendix ??
|
|
2756
|
+
appendix: n.appendix ?? rn(t),
|
|
2632
2757
|
createSummaryMessage: n.createSummaryMessage ?? ((e) => ({
|
|
2633
2758
|
role: "user",
|
|
2634
2759
|
hidden: !0,
|
|
@@ -2640,10 +2765,10 @@ async function on(e, t, n = {}) {
|
|
|
2640
2765
|
}
|
|
2641
2766
|
});
|
|
2642
2767
|
}
|
|
2643
|
-
async function
|
|
2644
|
-
if (
|
|
2768
|
+
async function on(e, t, n = {}) {
|
|
2769
|
+
if (!$t(t, e.getCompactionThresholdTokens(), t.lastContextTokens)) return null;
|
|
2645
2770
|
try {
|
|
2646
|
-
let r = await
|
|
2771
|
+
let r = await an(e, t, n);
|
|
2647
2772
|
return r && (t.lastContextTokens = void 0), r;
|
|
2648
2773
|
} catch (e) {
|
|
2649
2774
|
return console.error("Automatic conversation compaction failed:", e), null;
|
|
@@ -2651,7 +2776,7 @@ async function sn(e, t, n = {}) {
|
|
|
2651
2776
|
}
|
|
2652
2777
|
//#endregion
|
|
2653
2778
|
//#region llm/editorRefinementTools.ts
|
|
2654
|
-
function
|
|
2779
|
+
function sn(e) {
|
|
2655
2780
|
return e.filter((e) => e.trilogyType === "concept").map((e) => ({
|
|
2656
2781
|
name: e.label,
|
|
2657
2782
|
type: e.datatype || e.type,
|
|
@@ -2661,7 +2786,7 @@ function z(e) {
|
|
|
2661
2786
|
}));
|
|
2662
2787
|
}
|
|
2663
2788
|
function cn(e) {
|
|
2664
|
-
return
|
|
2789
|
+
return j(sn(e));
|
|
2665
2790
|
}
|
|
2666
2791
|
var ln = [
|
|
2667
2792
|
{
|
|
@@ -2704,10 +2829,10 @@ var ln = [
|
|
|
2704
2829
|
name: "edit_chart_config",
|
|
2705
2830
|
description: `Update the chart configuration for visualizing query results. Use this when the user wants to change how results are displayed.
|
|
2706
2831
|
|
|
2707
|
-
${
|
|
2832
|
+
${P}`,
|
|
2708
2833
|
input_schema: {
|
|
2709
2834
|
type: "object",
|
|
2710
|
-
properties: { chartConfig:
|
|
2835
|
+
properties: { chartConfig: N },
|
|
2711
2836
|
required: ["chartConfig"]
|
|
2712
2837
|
}
|
|
2713
2838
|
},
|
|
@@ -2785,7 +2910,7 @@ ${M}`,
|
|
|
2785
2910
|
required: []
|
|
2786
2911
|
}
|
|
2787
2912
|
},
|
|
2788
|
-
|
|
2913
|
+
F
|
|
2789
2914
|
], un = [
|
|
2790
2915
|
{
|
|
2791
2916
|
name: "browse_database_tree",
|
|
@@ -2899,7 +3024,7 @@ ${M}`,
|
|
|
2899
3024
|
required: []
|
|
2900
3025
|
}
|
|
2901
3026
|
},
|
|
2902
|
-
|
|
3027
|
+
F
|
|
2903
3028
|
], dn = ln;
|
|
2904
3029
|
function fn(e) {
|
|
2905
3030
|
return e === "sql" ? un : ln;
|
|
@@ -2913,7 +3038,7 @@ ${i}
|
|
|
2913
3038
|
` : "", c = a ? `
|
|
2914
3039
|
CURRENT CHART CONFIG:
|
|
2915
3040
|
${JSON.stringify(a, null, 2)}
|
|
2916
|
-
` : "", l =
|
|
3041
|
+
` : "", l = sn(o), u = l.length > 0 ? `
|
|
2917
3042
|
AVAILABLE FIELDS FOR QUERIES (${l.length} concepts):
|
|
2918
3043
|
${cn(o)}
|
|
2919
3044
|
` : "";
|
|
@@ -2969,7 +3094,7 @@ AVAILABLE TOOLS:
|
|
|
2969
3094
|
- close_session: Immediately close (only after user confirms or says "done")
|
|
2970
3095
|
${u}
|
|
2971
3096
|
TRILOGY LANGUAGE REFERENCE:
|
|
2972
|
-
${
|
|
3097
|
+
${A}
|
|
2973
3098
|
|
|
2974
3099
|
WORKFLOW:
|
|
2975
3100
|
1. Understand what the user wants to change
|
|
@@ -2991,13 +3116,13 @@ IMPORTANT:
|
|
|
2991
3116
|
}
|
|
2992
3117
|
//#endregion
|
|
2993
3118
|
//#region chats/chat.ts
|
|
2994
|
-
function
|
|
3119
|
+
function V() {
|
|
2995
3120
|
return `art-${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 6)}`;
|
|
2996
3121
|
}
|
|
2997
|
-
function
|
|
3122
|
+
function mn() {
|
|
2998
3123
|
return `Chat ${(/* @__PURE__ */ new Date()).toLocaleTimeString()}`;
|
|
2999
3124
|
}
|
|
3000
|
-
var
|
|
3125
|
+
var hn = class e {
|
|
3001
3126
|
id;
|
|
3002
3127
|
name;
|
|
3003
3128
|
dataConnectionName;
|
|
@@ -3021,7 +3146,7 @@ var mn = class e {
|
|
|
3021
3146
|
changed;
|
|
3022
3147
|
deleted;
|
|
3023
3148
|
constructor(e = {}) {
|
|
3024
|
-
this.id = e.id || `chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, this.name = e.name ||
|
|
3149
|
+
this.id = e.id || `chat-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, this.name = e.name || mn(), this.dataConnectionName = e.dataConnectionName || "", this.dataConnectionId = e.dataConnectionId || "", this.llmConnectionName = e.llmConnectionName || "", this.messages = e.messages || [], this.artifacts = e.artifacts || [], this.activeArtifactIndex = e.activeArtifactIndex ?? -1, this.imports = e.imports || [], this.createdAt = e.createdAt || /* @__PURE__ */ new Date(), this.updatedAt = e.updatedAt || /* @__PURE__ */ new Date(), this.storage = e.storage || "local", this.source = e.source || "user", this.sourceRefId = e.sourceRefId ?? null, this.kind = e.kind || "user", this.parentChatId = e.parentChatId ?? null, this.parentProjectId = e.parentProjectId ?? null, this.pendingInjections = e.pendingInjections || [], this.pendingContextNote = e.pendingContextNote ?? null, this.lastContextTokens = e.lastContextTokens ?? void 0, this.changed = e.changed ?? !0, this.deleted = e.deleted ?? !1;
|
|
3025
3150
|
}
|
|
3026
3151
|
setName(e) {
|
|
3027
3152
|
this.name !== e && (this.name = e, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0);
|
|
@@ -3036,7 +3161,7 @@ var mn = class e {
|
|
|
3036
3161
|
this.messages.push(e), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
|
|
3037
3162
|
}
|
|
3038
3163
|
addArtifact(e) {
|
|
3039
|
-
return e.id ||=
|
|
3164
|
+
return e.id ||= V(), this.artifacts.push(e), this.activeArtifactIndex = this.artifacts.length - 1, this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, this.activeArtifactIndex;
|
|
3040
3165
|
}
|
|
3041
3166
|
getArtifactById(e) {
|
|
3042
3167
|
return this.artifacts.find((t) => t.id === e) || null;
|
|
@@ -3070,7 +3195,7 @@ var mn = class e {
|
|
|
3070
3195
|
return this.activeArtifactIndex >= 0 && this.activeArtifactIndex < this.artifacts.length ? this.artifacts[this.activeArtifactIndex] : null;
|
|
3071
3196
|
}
|
|
3072
3197
|
clearMessages() {
|
|
3073
|
-
this.messages = [], this.artifacts = [], this.activeArtifactIndex = -1, this.lastContextTokens = void 0, this.name =
|
|
3198
|
+
this.messages = [], this.artifacts = [], this.activeArtifactIndex = -1, this.lastContextTokens = void 0, this.name = mn(), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0;
|
|
3074
3199
|
}
|
|
3075
3200
|
addImport(e) {
|
|
3076
3201
|
return this.imports.some((t) => t.id === e.id) ? !1 : (this.imports.push(e), this.updatedAt = /* @__PURE__ */ new Date(), this.changed = !0, !0);
|
|
@@ -3123,19 +3248,19 @@ var mn = class e {
|
|
|
3123
3248
|
changed: !1
|
|
3124
3249
|
});
|
|
3125
3250
|
}
|
|
3126
|
-
},
|
|
3251
|
+
}, gn = [
|
|
3127
3252
|
"line",
|
|
3128
3253
|
"area",
|
|
3129
3254
|
"bar",
|
|
3130
3255
|
"barh",
|
|
3131
3256
|
"point",
|
|
3132
3257
|
"heatmap"
|
|
3133
|
-
],
|
|
3258
|
+
], _n = [
|
|
3134
3259
|
"geo-map",
|
|
3135
3260
|
"tree",
|
|
3136
3261
|
"donut",
|
|
3137
3262
|
"headline"
|
|
3138
|
-
],
|
|
3263
|
+
], vn = [
|
|
3139
3264
|
"bar",
|
|
3140
3265
|
"barh",
|
|
3141
3266
|
"line",
|
|
@@ -3144,7 +3269,7 @@ var mn = class e {
|
|
|
3144
3269
|
"boxplot",
|
|
3145
3270
|
"donut",
|
|
3146
3271
|
"heatmap"
|
|
3147
|
-
],
|
|
3272
|
+
], yn = [
|
|
3148
3273
|
{
|
|
3149
3274
|
id: "group-by",
|
|
3150
3275
|
label: "Group By",
|
|
@@ -3279,7 +3404,7 @@ var mn = class e {
|
|
|
3279
3404
|
field: "trellisField",
|
|
3280
3405
|
columnFilter: "categorical",
|
|
3281
3406
|
allowEmpty: !0,
|
|
3282
|
-
visibleFor:
|
|
3407
|
+
visibleFor: gn,
|
|
3283
3408
|
filterGroup: "appearance"
|
|
3284
3409
|
},
|
|
3285
3410
|
{
|
|
@@ -3288,7 +3413,7 @@ var mn = class e {
|
|
|
3288
3413
|
field: "trellisRowField",
|
|
3289
3414
|
columnFilter: "categorical",
|
|
3290
3415
|
allowEmpty: !0,
|
|
3291
|
-
visibleFor:
|
|
3416
|
+
visibleFor: gn,
|
|
3292
3417
|
filterGroup: "appearance"
|
|
3293
3418
|
},
|
|
3294
3419
|
{
|
|
@@ -3419,7 +3544,7 @@ var mn = class e {
|
|
|
3419
3544
|
],
|
|
3420
3545
|
filterGroup: "appearance"
|
|
3421
3546
|
}
|
|
3422
|
-
],
|
|
3547
|
+
], bn = [
|
|
3423
3548
|
{
|
|
3424
3549
|
value: "bar",
|
|
3425
3550
|
label: "Bar Chart",
|
|
@@ -3480,7 +3605,7 @@ var mn = class e {
|
|
|
3480
3605
|
label: "Beeswarm",
|
|
3481
3606
|
icon: "mdi mdi-chart-bubble"
|
|
3482
3607
|
}
|
|
3483
|
-
],
|
|
3608
|
+
], xn = "#1f77b4", Sn = "#9467bd", Cn = "#FF7F7F", H = (e, t) => e[t], wn = [
|
|
3484
3609
|
"year",
|
|
3485
3610
|
"week",
|
|
3486
3611
|
"day",
|
|
@@ -3488,7 +3613,7 @@ var mn = class e {
|
|
|
3488
3613
|
"minute",
|
|
3489
3614
|
"second",
|
|
3490
3615
|
"quarter"
|
|
3491
|
-
],
|
|
3616
|
+
], Tn = [
|
|
3492
3617
|
"decade",
|
|
3493
3618
|
"year",
|
|
3494
3619
|
"month",
|
|
@@ -3498,50 +3623,50 @@ var mn = class e {
|
|
|
3498
3623
|
"minute",
|
|
3499
3624
|
"second",
|
|
3500
3625
|
"day_of_week"
|
|
3501
|
-
], U = (e) => e.traits ? e.traits.some((e) =>
|
|
3626
|
+
], U = (e) => e.traits ? e.traits.some((e) => Tn.some((t) => e === t || e.endsWith("_" + t))) : !1, En = (e, t) => {
|
|
3502
3627
|
let n = t.get(e);
|
|
3503
3628
|
return n ? U(n) : !1;
|
|
3504
|
-
},
|
|
3629
|
+
}, Dn = [
|
|
3505
3630
|
"rank",
|
|
3506
3631
|
"index",
|
|
3507
3632
|
"grade",
|
|
3508
3633
|
"flag",
|
|
3509
3634
|
"letter_grade"
|
|
3510
|
-
],
|
|
3511
|
-
...
|
|
3512
|
-
...
|
|
3635
|
+
], On = [
|
|
3636
|
+
...wn,
|
|
3637
|
+
...Dn,
|
|
3513
3638
|
"identifier",
|
|
3514
3639
|
"decade"
|
|
3515
3640
|
];
|
|
3516
|
-
function
|
|
3641
|
+
function kn(e) {
|
|
3517
3642
|
return new Date(e);
|
|
3518
3643
|
}
|
|
3519
3644
|
var W = (e, t, n) => {
|
|
3520
3645
|
if (!e || !t.get(e)) return !1;
|
|
3521
3646
|
let r = t.get(e);
|
|
3522
|
-
return r ?
|
|
3523
|
-
},
|
|
3647
|
+
return r ? An(r, n) : !1;
|
|
3648
|
+
}, An = (e, t) => !!(e.traits && e.traits.some((e) => e.endsWith(t))), G = (e) => e.purpose === "key" || U(e) ? !1 : [
|
|
3524
3649
|
n.NUMBER,
|
|
3525
3650
|
n.INTEGER,
|
|
3526
3651
|
n.FLOAT
|
|
3527
|
-
].includes(e.type) && !e.traits?.some((e) => e.endsWith("latitude") || e.endsWith("longitude")),
|
|
3652
|
+
].includes(e.type) && !e.traits?.some((e) => e.endsWith("latitude") || e.endsWith("longitude")), jn = (e) => !!(e.traits && Dn.some((t) => e.traits?.includes(t))), Mn = (e) => !!([n.STRING].includes(e.type) && e.traits?.some((e) => e.endsWith("url_image"))), K = (e) => !!([
|
|
3528
3653
|
n.DATE,
|
|
3529
3654
|
n.DATETIME,
|
|
3530
3655
|
n.TIME,
|
|
3531
3656
|
n.TIMESTAMP
|
|
3532
|
-
].includes(e.type) || e.traits &&
|
|
3657
|
+
].includes(e.type) || e.traits && wn.some((t) => e.traits?.includes(t))), Nn = (e) => !!([n.STRING, n.BOOLEAN].includes(e.type) || e.purpose === "key" || e.traits && On.some((t) => e.traits?.includes(t))), Pn = (e) => e.traits?.includes("hex") ?? !1, q = (e, t) => e.traits ? e.traits.some((e) => e.endsWith(t)) : !1, J = (e, t) => {
|
|
3533
3658
|
let n = [];
|
|
3534
3659
|
return t.forEach((t, r) => {
|
|
3535
|
-
(e === "all" || e === "numeric" && G(t) && !K(t) || e === "categorical" &&
|
|
3660
|
+
(e === "all" || e === "numeric" && G(t) && !K(t) || e === "categorical" && Nn(t) || e === "temporal" && K(t) || e === "latitude" && q(t, "latitude") || e === "longitude" && q(t, "longitude") || e === "geographic" && t.traits?.some((e) => e.endsWith("state") || e.endsWith("state_short") || e.endsWith("country"))) && n.push(t);
|
|
3536
3661
|
}), n;
|
|
3537
|
-
},
|
|
3662
|
+
}, Fn = (e, t) => {
|
|
3538
3663
|
let n = t.filter((t) => t.name !== e.xField && t.name !== e.yField);
|
|
3539
3664
|
return n.length > 0 ? n[0] : t[0];
|
|
3540
|
-
},
|
|
3665
|
+
}, In = (e, t, n) => {
|
|
3541
3666
|
let r = {
|
|
3542
3667
|
showTitle: !1,
|
|
3543
3668
|
hideLegend: !1
|
|
3544
|
-
}, i = J("numeric", t), a = J("categorical", t).filter((e) => !
|
|
3669
|
+
}, i = J("numeric", t), a = J("categorical", t).filter((e) => !Pn(e)), o = J("temporal", t), s = J("latitude", t), c = J("longitude", t), l = J("geographic", t);
|
|
3545
3670
|
if (t.size === 0) return {};
|
|
3546
3671
|
if (!n && i.length === 0 && l.length === 0 && c.length === 0 && s.length === 0) return r.chartType = "headline", r;
|
|
3547
3672
|
let u = a.filter((e) => U(e));
|
|
@@ -3556,7 +3681,7 @@ var W = (e, t, n) => {
|
|
|
3556
3681
|
n.size > 7 ? r.chartType = "barh" : r.chartType = "bar";
|
|
3557
3682
|
} else i.length >= 2 ? e.length === 1 ? r.chartType = "headline" : r.chartType = "point" : a.length >= 2 && i.length > 0 ? r.chartType = "heatmap" : i.length > 0 && a.length == 0 && (r.chartType = "headline");
|
|
3558
3683
|
if (r.chartType === "barh") {
|
|
3559
|
-
r.yField = H(a, 0)?.name, i.length > 0 && (r.xField =
|
|
3684
|
+
r.yField = H(a, 0)?.name, i.length > 0 && (r.xField = Fn(r, i).name);
|
|
3560
3685
|
let e = a.filter((e) => e.name !== r.yField && e.name !== r.xField);
|
|
3561
3686
|
e.length > 0 && (r.colorField = e[0].name), i.length > 1 && !r.colorField && (r.colorField = i[1].name);
|
|
3562
3687
|
let t = a.filter((e) => e.name !== r.yField && e.name !== r.xField && e.name !== r.colorField);
|
|
@@ -3604,7 +3729,7 @@ var W = (e, t, n) => {
|
|
|
3604
3729
|
n.length > 0 && (r.trellisRowField = n[0].name), n.length > 1 && (r.trellisField = n[1].name);
|
|
3605
3730
|
}
|
|
3606
3731
|
return r;
|
|
3607
|
-
},
|
|
3732
|
+
}, Ln = (e, t) => {
|
|
3608
3733
|
let n = J("numeric", t), r = J("categorical", t), i = J("temporal", t), a = J("latitude", t), o = J("longitude", t), s = J("geographic", t), c = [];
|
|
3609
3734
|
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;
|
|
3610
3735
|
let l = r.filter((e) => U(e));
|
|
@@ -3613,19 +3738,19 @@ var W = (e, t, n) => {
|
|
|
3613
3738
|
for (let r = 0; r < Math.min(e.length, 50); r++) e[r] && e[r][t] !== void 0 && n.add(e[r][t]);
|
|
3614
3739
|
c.push("bar"), c.push("barh"), c.push("donut");
|
|
3615
3740
|
}
|
|
3616
|
-
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) =>
|
|
3617
|
-
},
|
|
3741
|
+
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) => bn.map((e) => e.value).includes(e));
|
|
3742
|
+
}, Rn = (e) => q(e, "latitude") ? "latitude" : q(e, "longitude") ? "longitude" : e.traits?.some((e) => e.endsWith("state") || e.endsWith("state_short") || e.endsWith("country")) ? "geographic" : K(e) ? "temporal" : U(e) ? "categorical" : G(e) ? "numeric" : Nn(e) ? "categorical" : "other", zn = (e) => {
|
|
3618
3743
|
let t = [];
|
|
3619
3744
|
return e.forEach((e) => {
|
|
3620
|
-
t.push(`${e.name} (${
|
|
3745
|
+
t.push(`${e.name} (${Rn(e)})`);
|
|
3621
3746
|
}), t.length > 0 ? t.join(", ") : "no fields";
|
|
3622
|
-
},
|
|
3623
|
-
let i =
|
|
3747
|
+
}, Bn = (e, n, r) => {
|
|
3748
|
+
let i = Ln(e, n), a = [];
|
|
3624
3749
|
if (r.layers && r.layers.length > 0) {
|
|
3625
3750
|
let e = [], a = Array.from(n.keys());
|
|
3626
3751
|
return r.layers.forEach((r, i) => {
|
|
3627
3752
|
let o = `Layer ${i + 1}`;
|
|
3628
|
-
|
|
3753
|
+
vn.includes(r.chartType) || e.push(`${o}: chart type "${r.chartType}" cannot be used as a layer. Layerable types: ${vn.join(", ")}.`);
|
|
3629
3754
|
for (let i of t) {
|
|
3630
3755
|
let t = r[i];
|
|
3631
3756
|
typeof t == "string" && t !== "" && !n.has(t) && e.push(`${o}: field "${t}" (${i}) does not exist in the query results. Available columns: ${a.join(", ")}.`);
|
|
@@ -3639,16 +3764,16 @@ var W = (e, t, n) => {
|
|
|
3639
3764
|
};
|
|
3640
3765
|
}
|
|
3641
3766
|
if (!i.includes(r.chartType)) {
|
|
3642
|
-
let t =
|
|
3767
|
+
let t = zn(n);
|
|
3643
3768
|
return {
|
|
3644
3769
|
valid: !1,
|
|
3645
3770
|
chartTypeError: `Chart type "${r.chartType}" is not compatible with the available fields (${t}). Eligible chart types: ${i.join(", ")}.`,
|
|
3646
3771
|
fieldErrors: [],
|
|
3647
3772
|
eligibleChartTypes: i,
|
|
3648
|
-
suggestedConfig:
|
|
3773
|
+
suggestedConfig: In(e, n)
|
|
3649
3774
|
};
|
|
3650
3775
|
}
|
|
3651
|
-
let o =
|
|
3776
|
+
let o = In(e, n, [
|
|
3652
3777
|
"line",
|
|
3653
3778
|
"bar",
|
|
3654
3779
|
"barh",
|
|
@@ -3675,7 +3800,7 @@ var W = (e, t, n) => {
|
|
|
3675
3800
|
let t = r[e];
|
|
3676
3801
|
typeof t == "string" && t !== "" && !n.has(t) && a.push(`Field "${t}" (${e}) does not exist in the query results. Available columns: ${s.join(", ")}.`);
|
|
3677
3802
|
}
|
|
3678
|
-
let c =
|
|
3803
|
+
let c = yn.filter((e) => !e.allowEmpty && e.visibleFor.includes(r.chartType));
|
|
3679
3804
|
for (let e of c) {
|
|
3680
3805
|
let t = r[e.field];
|
|
3681
3806
|
(typeof t != "string" || t === "") && a.push(`Chart type "${r.chartType}" requires ${e.label} (${String(e.field)}) to be specified, but it is missing.`);
|
|
@@ -3686,7 +3811,7 @@ var W = (e, t, n) => {
|
|
|
3686
3811
|
}
|
|
3687
3812
|
if ((r.chartType === "line" || r.chartType === "area") && r.xField && n.has(r.xField)) {
|
|
3688
3813
|
let e = n.get(r.xField);
|
|
3689
|
-
!K(e) && !
|
|
3814
|
+
!K(e) && !Nn(e) && !G(e) && a.push(`For ${r.chartType} charts, xField should typically be a temporal or categorical column.`);
|
|
3690
3815
|
}
|
|
3691
3816
|
let l = { ...o };
|
|
3692
3817
|
for (let [e, t] of Object.entries(r)) t != null && (typeof t == "string" && t === "" || (l[e] = t));
|
|
@@ -3696,7 +3821,7 @@ var W = (e, t, n) => {
|
|
|
3696
3821
|
eligibleChartTypes: i,
|
|
3697
3822
|
suggestedConfig: l
|
|
3698
3823
|
};
|
|
3699
|
-
},
|
|
3824
|
+
}, Vn = (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.`, Y = (e, t) => {
|
|
3700
3825
|
if (!e || !t.get(e)) return {};
|
|
3701
3826
|
let r = t.get(e);
|
|
3702
3827
|
if (!r) return {};
|
|
@@ -3721,7 +3846,7 @@ var W = (e, t, n) => {
|
|
|
3721
3846
|
case n.INTEGER: return {};
|
|
3722
3847
|
default: return {};
|
|
3723
3848
|
}
|
|
3724
|
-
},
|
|
3849
|
+
}, Hn = (e, t, n = null) => {
|
|
3725
3850
|
if (!e || !t.get(e)) return {};
|
|
3726
3851
|
let r = t.get(e);
|
|
3727
3852
|
return r ? W(e, t, "day_of_week_name") ? { sort: [
|
|
@@ -3732,7 +3857,7 @@ var W = (e, t, n) => {
|
|
|
3732
3857
|
"Thursday",
|
|
3733
3858
|
"Friday",
|
|
3734
3859
|
"Saturday"
|
|
3735
|
-
] } :
|
|
3860
|
+
] } : jn(r) || U(r) || K(r) ? { sort: {
|
|
3736
3861
|
field: e,
|
|
3737
3862
|
order: "ascending"
|
|
3738
3863
|
} } : G(r) ? n ? { sort: {
|
|
@@ -3757,19 +3882,19 @@ var W = (e, t, n) => {
|
|
|
3757
3882
|
n.DATETIME,
|
|
3758
3883
|
n.TIMESTAMP,
|
|
3759
3884
|
n.INTEGER
|
|
3760
|
-
].includes(r.type) || [n.DATE].includes(r.type) ? "temporal" : "ordinal" :
|
|
3761
|
-
},
|
|
3885
|
+
].includes(r.type) || [n.DATE].includes(r.type) ? "temporal" : "ordinal" : jn(r) || U(r) ? "ordinal" : G(r) ? "quantitative" : "nominal" : "nominal";
|
|
3886
|
+
}, Un = (e, t, n = {}, i = !0, a = {}) => e ? {
|
|
3762
3887
|
field: e,
|
|
3763
3888
|
type: X(e, t),
|
|
3764
3889
|
title: r(t.get(e)?.description || e),
|
|
3765
3890
|
...Y(e, t),
|
|
3766
3891
|
...n,
|
|
3767
|
-
...i ?
|
|
3892
|
+
...i ? Hn(e, t) : {},
|
|
3768
3893
|
...a.scale !== void 0 || a.zero !== void 0 ? { scale: {
|
|
3769
3894
|
...a.scale !== void 0 && { type: a.scale },
|
|
3770
3895
|
...a.zero !== void 0 && { zero: a.zero }
|
|
3771
3896
|
} } : {}
|
|
3772
|
-
} : {},
|
|
3897
|
+
} : {}, Wn = (e = 0) => e > 0 ? `_l${e}` : "", Z = (e, t = "") => `${e}${t}`, Gn = (e = "") => ({
|
|
3773
3898
|
fillOpacity: {
|
|
3774
3899
|
condition: {
|
|
3775
3900
|
param: Z("select", e),
|
|
@@ -3789,7 +3914,7 @@ var W = (e, t, n) => {
|
|
|
3789
3914
|
}],
|
|
3790
3915
|
value: 0
|
|
3791
3916
|
}
|
|
3792
|
-
}),
|
|
3917
|
+
}), Kn = (e, t, n, r = "color") => {
|
|
3793
3918
|
let i = !1;
|
|
3794
3919
|
return n === "quantitative" && r === "color" && (i = !0), e && e.length > 10 ? {
|
|
3795
3920
|
orient: t ? "bottom" : "right",
|
|
@@ -3802,14 +3927,14 @@ var W = (e, t, n) => {
|
|
|
3802
3927
|
direction: t ? "horizontal" : "vertical",
|
|
3803
3928
|
titleFontSize: t ? 10 : 12
|
|
3804
3929
|
};
|
|
3805
|
-
},
|
|
3806
|
-
let l = { tickCount:
|
|
3930
|
+
}, qn = 15, Jn = (e, t, n, i = !1, a = "light", o = !1, s = [], c = "") => {
|
|
3931
|
+
let l = { tickCount: qn }, u = [], d = [], f = s || [], p = Array.from(n.entries()).filter(([e, t]) => t.traits?.includes("hex")).map(([e, t]) => e);
|
|
3807
3932
|
if (t) {
|
|
3808
3933
|
let e = X(t, n);
|
|
3809
3934
|
l = {
|
|
3810
3935
|
...l,
|
|
3811
|
-
...
|
|
3812
|
-
}, (e === "nominal" || e === "ordinal") && (d = Array.from(new Set(f.map((e) => String(e[t] ?? "")))).sort((e, t) => e.localeCompare(t))), u = d.slice(0,
|
|
3936
|
+
...Kn(t, i, e)
|
|
3937
|
+
}, (e === "nominal" || e === "ordinal") && (d = Array.from(new Set(f.map((e) => String(e[t] ?? "")))).sort((e, t) => e.localeCompare(t))), u = d.slice(0, qn);
|
|
3813
3938
|
}
|
|
3814
3939
|
let m = (e) => (!o && Object.keys(l).length > 0 ? e.legend = l : o && (e.legend = null), e);
|
|
3815
3940
|
if (t && n.get(t)) {
|
|
@@ -3838,23 +3963,23 @@ var W = (e, t, n) => {
|
|
|
3838
3963
|
...c === null ? {} : { condition: [{
|
|
3839
3964
|
param: Z("highlight", c),
|
|
3840
3965
|
empty: !1,
|
|
3841
|
-
value:
|
|
3966
|
+
value: Cn
|
|
3842
3967
|
}, {
|
|
3843
3968
|
param: Z("select", c),
|
|
3844
3969
|
empty: !1,
|
|
3845
|
-
value:
|
|
3970
|
+
value: Cn
|
|
3846
3971
|
}] },
|
|
3847
3972
|
...Y(t, n)
|
|
3848
3973
|
});
|
|
3849
3974
|
}
|
|
3850
3975
|
return m({});
|
|
3851
|
-
},
|
|
3976
|
+
}, Yn = (e, t, n = !1, i = !1) => {
|
|
3852
3977
|
let a = { tickCount: 5 };
|
|
3853
3978
|
if (e) {
|
|
3854
3979
|
let r = X(e, t);
|
|
3855
3980
|
a = {
|
|
3856
3981
|
...a,
|
|
3857
|
-
...
|
|
3982
|
+
...Kn(e, n, r, "size")
|
|
3858
3983
|
};
|
|
3859
3984
|
}
|
|
3860
3985
|
return e && t.get(e) ? ((e) => (!i && Object.keys(a).length > 0 ? e.legend = a : i && (e.legend = null), e))({
|
|
@@ -3869,7 +3994,7 @@ var W = (e, t, n) => {
|
|
|
3869
3994
|
};
|
|
3870
3995
|
//#endregion
|
|
3871
3996
|
//#region llm/importConcepts.ts
|
|
3872
|
-
async function
|
|
3997
|
+
async function Xn(e, t, n) {
|
|
3873
3998
|
let { connectionStore: r, editorStore: i, queryExecutionService: a } = e;
|
|
3874
3999
|
try {
|
|
3875
4000
|
if (!i?.editors[t.id]?.contents) return "No fields found in this data source.";
|
|
@@ -3912,7 +4037,7 @@ function Q(e) {
|
|
|
3912
4037
|
function $(e, t) {
|
|
3913
4038
|
return e.connections[t] || e.connectionByName(t) || null;
|
|
3914
4039
|
}
|
|
3915
|
-
async function
|
|
4040
|
+
async function Zn(e, t, n) {
|
|
3916
4041
|
if (!t || typeof t != "string") return {
|
|
3917
4042
|
success: !1,
|
|
3918
4043
|
error: `Connection name is required. Available connections: ${Q(e) || "None"}`
|
|
@@ -3939,7 +4064,7 @@ async function Xn(e, t, n) {
|
|
|
3939
4064
|
};
|
|
3940
4065
|
}
|
|
3941
4066
|
}
|
|
3942
|
-
function
|
|
4067
|
+
function Qn(e, t, n, r) {
|
|
3943
4068
|
let i = /* @__PURE__ */ new Map(), a = $(e, n), o = a?.id || n;
|
|
3944
4069
|
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) => {
|
|
3945
4070
|
i.set(e.name, e.contents);
|
|
@@ -3951,7 +4076,7 @@ function Zn(e, t, n, r) {
|
|
|
3951
4076
|
contents: t
|
|
3952
4077
|
}));
|
|
3953
4078
|
}
|
|
3954
|
-
function
|
|
4079
|
+
function $n(e, t, n, r) {
|
|
3955
4080
|
let i = typeof t?.toJSON == "function" ? t.toJSON() : t, a = i?.data;
|
|
3956
4081
|
if (!Array.isArray(a)) return {
|
|
3957
4082
|
success: !1,
|
|
@@ -3975,7 +4100,7 @@ function Qn(e, t, n, r) {
|
|
|
3975
4100
|
}, null, 2)
|
|
3976
4101
|
};
|
|
3977
4102
|
}
|
|
3978
|
-
function
|
|
4103
|
+
function er(e, t, n) {
|
|
3979
4104
|
if (!e) return [];
|
|
3980
4105
|
let r = n ? $(n, t) : null;
|
|
3981
4106
|
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) => ({
|
|
@@ -3984,7 +4109,7 @@ function $n(e, t, n) {
|
|
|
3984
4109
|
alias: ""
|
|
3985
4110
|
}));
|
|
3986
4111
|
}
|
|
3987
|
-
async function
|
|
4112
|
+
async function tr(e, t, n, r, i) {
|
|
3988
4113
|
let a = t.getConnectionName(), o = i && a ? $(i, a) : null;
|
|
3989
4114
|
if (!e || e.trim() === "") return t.setActiveImports([]), {
|
|
3990
4115
|
success: !0,
|
|
@@ -3995,27 +4120,27 @@ async function er(e, t, n, r, i) {
|
|
|
3995
4120
|
success: !1,
|
|
3996
4121
|
error: "No data connection selected. Please select a data connection first."
|
|
3997
4122
|
};
|
|
3998
|
-
let s =
|
|
4123
|
+
let s = er(r, o?.id || a, i), c = s.find((t) => t.name === e || t.name.endsWith(`.${e}`));
|
|
3999
4124
|
if (!c) return {
|
|
4000
4125
|
success: !1,
|
|
4001
4126
|
error: `Import "${e}" not found. Available imports: ${s.map((e) => e.name).join(", ") || "none"}`
|
|
4002
4127
|
};
|
|
4003
4128
|
let l = t.getActiveImports(), u = l.length === 1 && l[0].id === c.id;
|
|
4004
4129
|
u || t.setActiveImports([c]);
|
|
4005
|
-
let d = await
|
|
4130
|
+
let d = await Xn(n, c, o?.id || a);
|
|
4006
4131
|
return {
|
|
4007
4132
|
success: !0,
|
|
4008
4133
|
message: `${u ? `"${c.name}" is already the active data source.` : `Successfully selected "${c.name}" as the active data source.`}\n\n${d}`,
|
|
4009
4134
|
triggersSymbolRefresh: !u
|
|
4010
4135
|
};
|
|
4011
4136
|
}
|
|
4012
|
-
function
|
|
4137
|
+
function nr(e, t, n) {
|
|
4013
4138
|
let r = e.getConnectionName(), i = n && r ? $(n, r) : null, a = i?.name || r;
|
|
4014
4139
|
if (!r) return {
|
|
4015
4140
|
success: !1,
|
|
4016
4141
|
error: "No data connection selected. Please select a data connection first."
|
|
4017
4142
|
};
|
|
4018
|
-
let o =
|
|
4143
|
+
let o = er(t, i?.id || r, n), s = e.getActiveImports(), c = s.length > 0 ? s[0] : null;
|
|
4019
4144
|
return o.length === 0 ? {
|
|
4020
4145
|
success: !0,
|
|
4021
4146
|
message: `No data sources available on connection "${a}".`
|
|
@@ -4024,7 +4149,7 @@ function tr(e, t, n) {
|
|
|
4024
4149
|
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.`
|
|
4025
4150
|
};
|
|
4026
4151
|
}
|
|
4027
|
-
async function
|
|
4152
|
+
async function rr(e, t, n, r, i, a) {
|
|
4028
4153
|
if (!i || typeof i != "string" || i.trim() === "") return {
|
|
4029
4154
|
success: !1,
|
|
4030
4155
|
error: "Query is required and must be a non-empty string",
|
|
@@ -4046,7 +4171,7 @@ async function nr(e, t, n, r, i, a) {
|
|
|
4046
4171
|
error: `Connection "${o.name}" is not connected. Use connect_data_connection first.`,
|
|
4047
4172
|
query: i
|
|
4048
4173
|
};
|
|
4049
|
-
let s =
|
|
4174
|
+
let s = Qn(t, n, o.id, a), c = (a || []).map((e) => ({
|
|
4050
4175
|
name: e.name,
|
|
4051
4176
|
alias: e.alias || ""
|
|
4052
4177
|
})), l = {
|
|
@@ -4083,13 +4208,13 @@ async function nr(e, t, n, r, i, a) {
|
|
|
4083
4208
|
}
|
|
4084
4209
|
//#endregion
|
|
4085
4210
|
//#region llm/editorRefinementToolExecutor.ts
|
|
4086
|
-
function
|
|
4211
|
+
function ir() {
|
|
4087
4212
|
return {
|
|
4088
4213
|
artifactRegistry: /* @__PURE__ */ new Map(),
|
|
4089
4214
|
lastQueryResults: { value: null }
|
|
4090
4215
|
};
|
|
4091
4216
|
}
|
|
4092
|
-
var
|
|
4217
|
+
var ar = class {
|
|
4093
4218
|
queryExecutionService;
|
|
4094
4219
|
connectionStore;
|
|
4095
4220
|
editorStore;
|
|
@@ -4098,7 +4223,7 @@ var ir = class {
|
|
|
4098
4223
|
lastQueryResults;
|
|
4099
4224
|
constructor(e, t, n, r, i) {
|
|
4100
4225
|
this.queryExecutionService = e, this.connectionStore = t, this.editorContext = n, this.editorStore = r || null;
|
|
4101
|
-
let a = i ??
|
|
4226
|
+
let a = i ?? ir();
|
|
4102
4227
|
this.artifactRegistry = a.artifactRegistry, this.lastQueryResults = a.lastQueryResults;
|
|
4103
4228
|
}
|
|
4104
4229
|
async executeToolCall(e, t) {
|
|
@@ -4251,7 +4376,7 @@ var ir = class {
|
|
|
4251
4376
|
generatedSql: i.generatedSql
|
|
4252
4377
|
};
|
|
4253
4378
|
let a = {
|
|
4254
|
-
id:
|
|
4379
|
+
id: V(),
|
|
4255
4380
|
type: "results",
|
|
4256
4381
|
data: i.results,
|
|
4257
4382
|
config: {
|
|
@@ -4317,7 +4442,7 @@ var ir = class {
|
|
|
4317
4442
|
};
|
|
4318
4443
|
let t = this.editorContext.getCurrentResults?.() ?? this.lastQueryResults.value ?? null;
|
|
4319
4444
|
if (t && t.headers) {
|
|
4320
|
-
let n =
|
|
4445
|
+
let n = Bn(t.data, t.headers, e);
|
|
4321
4446
|
if (!n.valid) {
|
|
4322
4447
|
let e = n.suggestedConfig;
|
|
4323
4448
|
try {
|
|
@@ -4325,7 +4450,7 @@ var ir = class {
|
|
|
4325
4450
|
} catch {}
|
|
4326
4451
|
return {
|
|
4327
4452
|
success: !1,
|
|
4328
|
-
error: "Auto-corrected to default configuration.\n" +
|
|
4453
|
+
error: "Auto-corrected to default configuration.\n" + Vn(n)
|
|
4329
4454
|
};
|
|
4330
4455
|
}
|
|
4331
4456
|
}
|
|
@@ -4394,7 +4519,7 @@ var ir = class {
|
|
|
4394
4519
|
generatedSql: t.generatedSql
|
|
4395
4520
|
};
|
|
4396
4521
|
let n = {
|
|
4397
|
-
id:
|
|
4522
|
+
id: V(),
|
|
4398
4523
|
type: "results",
|
|
4399
4524
|
data: t.results,
|
|
4400
4525
|
config: {
|
|
@@ -4424,7 +4549,7 @@ var ir = class {
|
|
|
4424
4549
|
}
|
|
4425
4550
|
getArtifactRows(e, t, n) {
|
|
4426
4551
|
let r = this.artifactRegistry.get(e);
|
|
4427
|
-
return r ?
|
|
4552
|
+
return r ? $n(e, r.data, t, n) : {
|
|
4428
4553
|
success: !1,
|
|
4429
4554
|
error: `Artifact "${e}" not found. Only artifacts created by run_query or run_active_editor_query in this session are available.`
|
|
4430
4555
|
};
|
|
@@ -4451,7 +4576,7 @@ var ir = class {
|
|
|
4451
4576
|
}
|
|
4452
4577
|
}
|
|
4453
4578
|
async connectDataConnection(e) {
|
|
4454
|
-
return
|
|
4579
|
+
return Zn(this.connectionStore, e);
|
|
4455
4580
|
}
|
|
4456
4581
|
serializeDatabaseTree(e, t, n) {
|
|
4457
4582
|
let r = this.connectionStore.connectionByName(this.editorContext.connectionName), i = r?.databases || [], a = (e ? i.filter((t) => t.name === e) : i).map((e) => {
|
|
@@ -4492,8 +4617,8 @@ var ir = class {
|
|
|
4492
4617
|
};
|
|
4493
4618
|
}
|
|
4494
4619
|
buildEditorExtraContent() {
|
|
4495
|
-
return
|
|
4620
|
+
return Qn(this.connectionStore, this.editorStore, this.editorContext.connectionName);
|
|
4496
4621
|
}
|
|
4497
4622
|
};
|
|
4498
4623
|
//#endregion
|
|
4499
|
-
export {
|
|
4624
|
+
export { Vt as $, ie as $t, bn as A, qe as At, dn as B, Re as Bt, X as C, Xe as Ct, Wn as D, $e as Dt, Mn as E, Qe as Et, Sn as F, D as Ft, an as G, ae as Gt, sn as H, O as Ht, xn as I, Be as It, $t as J, ne as Jt, nn as K, te as Kt, hn as L, Ve as Lt, vn as M, Ue as Mt, _n as N, We as Nt, Z as O, k as Ot, gn as P, Le as Pt, Gt as Q, le as Qt, mn as R, ke as Rt, Hn as S, Ze as St, Pn as T, et as Tt, fn as U, He as Ut, pn as V, ze as Vt, Kt as W, De as Wt, z as X, re as Xt, B as Y, se as Yt, Ut as Z, b as Zt, Ln as _, mt as _t, rr as a, m as an, St as at, W as b, at as bt, nr as c, d as cn, xt as ct, kn as d, P as dt, Ce as en, Ht as et, Jn as f, N as ft, In as g, ht as gt, Yn as h, gt as ht, Zn as i, y as in, jt as it, yn as j, Ge as jt, Bn as k, Je as kt, tr as l, u as ln, Ct as lt, Gn as m, j as mt, ir as n, ye as nn, Lt as nt, $n as o, f as on, I as ot, Un as p, F as pt, on as q, oe as qt, Qn as r, Se as rn, R as rt, er as s, c as sn, Ot as st, ar as t, xe as tn, Rt as tt, Cn as u, wt as ut, J as v, ut as vt, En as w, Ye as wt, Y as x, A as xt, Vn as y, ot as yt, V as z, Fe as zt };
|