@duckcodeailabs/dql-compiler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/LICENSE +123 -0
  2. package/dist/charts/area.d.ts +5 -0
  3. package/dist/charts/area.d.ts.map +1 -0
  4. package/dist/charts/area.js +30 -0
  5. package/dist/charts/area.js.map +1 -0
  6. package/dist/charts/bar.d.ts +5 -0
  7. package/dist/charts/bar.d.ts.map +1 -0
  8. package/dist/charts/bar.js +37 -0
  9. package/dist/charts/bar.js.map +1 -0
  10. package/dist/charts/boxplot.d.ts +5 -0
  11. package/dist/charts/boxplot.d.ts.map +1 -0
  12. package/dist/charts/boxplot.js +36 -0
  13. package/dist/charts/boxplot.js.map +1 -0
  14. package/dist/charts/chart-registry.d.ts +24 -0
  15. package/dist/charts/chart-registry.d.ts.map +1 -0
  16. package/dist/charts/chart-registry.js +14 -0
  17. package/dist/charts/chart-registry.js.map +1 -0
  18. package/dist/charts/combo.d.ts +5 -0
  19. package/dist/charts/combo.d.ts.map +1 -0
  20. package/dist/charts/combo.js +54 -0
  21. package/dist/charts/combo.js.map +1 -0
  22. package/dist/charts/funnel.d.ts +5 -0
  23. package/dist/charts/funnel.d.ts.map +1 -0
  24. package/dist/charts/funnel.js +36 -0
  25. package/dist/charts/funnel.js.map +1 -0
  26. package/dist/charts/gauge.d.ts +5 -0
  27. package/dist/charts/gauge.d.ts.map +1 -0
  28. package/dist/charts/gauge.js +53 -0
  29. package/dist/charts/gauge.js.map +1 -0
  30. package/dist/charts/geo.d.ts +5 -0
  31. package/dist/charts/geo.d.ts.map +1 -0
  32. package/dist/charts/geo.js +48 -0
  33. package/dist/charts/geo.js.map +1 -0
  34. package/dist/charts/grouped-bar.d.ts +5 -0
  35. package/dist/charts/grouped-bar.d.ts.map +1 -0
  36. package/dist/charts/grouped-bar.js +37 -0
  37. package/dist/charts/grouped-bar.js.map +1 -0
  38. package/dist/charts/heatmap.d.ts +5 -0
  39. package/dist/charts/heatmap.d.ts.map +1 -0
  40. package/dist/charts/heatmap.js +31 -0
  41. package/dist/charts/heatmap.js.map +1 -0
  42. package/dist/charts/histogram.d.ts +5 -0
  43. package/dist/charts/histogram.d.ts.map +1 -0
  44. package/dist/charts/histogram.js +32 -0
  45. package/dist/charts/histogram.js.map +1 -0
  46. package/dist/charts/index.d.ts +25 -0
  47. package/dist/charts/index.d.ts.map +1 -0
  48. package/dist/charts/index.js +66 -0
  49. package/dist/charts/index.js.map +1 -0
  50. package/dist/charts/kpi.d.ts +14 -0
  51. package/dist/charts/kpi.d.ts.map +1 -0
  52. package/dist/charts/kpi.js +71 -0
  53. package/dist/charts/kpi.js.map +1 -0
  54. package/dist/charts/line.d.ts +5 -0
  55. package/dist/charts/line.d.ts.map +1 -0
  56. package/dist/charts/line.js +38 -0
  57. package/dist/charts/line.js.map +1 -0
  58. package/dist/charts/pie.d.ts +5 -0
  59. package/dist/charts/pie.d.ts.map +1 -0
  60. package/dist/charts/pie.js +25 -0
  61. package/dist/charts/pie.js.map +1 -0
  62. package/dist/charts/sankey.d.ts +10 -0
  63. package/dist/charts/sankey.d.ts.map +1 -0
  64. package/dist/charts/sankey.js +120 -0
  65. package/dist/charts/sankey.js.map +1 -0
  66. package/dist/charts/scatter.d.ts +5 -0
  67. package/dist/charts/scatter.d.ts.map +1 -0
  68. package/dist/charts/scatter.js +34 -0
  69. package/dist/charts/scatter.js.map +1 -0
  70. package/dist/charts/shared.d.ts +11 -0
  71. package/dist/charts/shared.d.ts.map +1 -0
  72. package/dist/charts/shared.js +52 -0
  73. package/dist/charts/shared.js.map +1 -0
  74. package/dist/charts/small-multiples.d.ts +5 -0
  75. package/dist/charts/small-multiples.d.ts.map +1 -0
  76. package/dist/charts/small-multiples.js +59 -0
  77. package/dist/charts/small-multiples.js.map +1 -0
  78. package/dist/charts/sparkline.d.ts +5 -0
  79. package/dist/charts/sparkline.d.ts.map +1 -0
  80. package/dist/charts/sparkline.js +41 -0
  81. package/dist/charts/sparkline.js.map +1 -0
  82. package/dist/charts/stacked-bar.d.ts +5 -0
  83. package/dist/charts/stacked-bar.d.ts.map +1 -0
  84. package/dist/charts/stacked-bar.js +34 -0
  85. package/dist/charts/stacked-bar.js.map +1 -0
  86. package/dist/charts/table.d.ts +14 -0
  87. package/dist/charts/table.d.ts.map +1 -0
  88. package/dist/charts/table.js +58 -0
  89. package/dist/charts/table.js.map +1 -0
  90. package/dist/charts/treemap.d.ts +9 -0
  91. package/dist/charts/treemap.d.ts.map +1 -0
  92. package/dist/charts/treemap.js +60 -0
  93. package/dist/charts/treemap.js.map +1 -0
  94. package/dist/charts/waterfall.d.ts +5 -0
  95. package/dist/charts/waterfall.d.ts.map +1 -0
  96. package/dist/charts/waterfall.js +36 -0
  97. package/dist/charts/waterfall.js.map +1 -0
  98. package/dist/codegen/bundle.d.ts +34 -0
  99. package/dist/codegen/bundle.d.ts.map +1 -0
  100. package/dist/codegen/bundle.js +20 -0
  101. package/dist/codegen/bundle.js.map +1 -0
  102. package/dist/codegen/html-emitter.d.ts +10 -0
  103. package/dist/codegen/html-emitter.d.ts.map +1 -0
  104. package/dist/codegen/html-emitter.js +1082 -0
  105. package/dist/codegen/html-emitter.js.map +1 -0
  106. package/dist/codegen/index.d.ts +6 -0
  107. package/dist/codegen/index.d.ts.map +1 -0
  108. package/dist/codegen/index.js +6 -0
  109. package/dist/codegen/index.js.map +1 -0
  110. package/dist/codegen/react-emitter.d.ts +42 -0
  111. package/dist/codegen/react-emitter.d.ts.map +1 -0
  112. package/dist/codegen/react-emitter.js +135 -0
  113. package/dist/codegen/react-emitter.js.map +1 -0
  114. package/dist/codegen/runtime-emitter.d.ts +2 -0
  115. package/dist/codegen/runtime-emitter.d.ts.map +1 -0
  116. package/dist/codegen/runtime-emitter.js +1807 -0
  117. package/dist/codegen/runtime-emitter.js.map +1 -0
  118. package/dist/codegen/vega-lite-emitter.d.ts +23 -0
  119. package/dist/codegen/vega-lite-emitter.d.ts.map +1 -0
  120. package/dist/codegen/vega-lite-emitter.js +92 -0
  121. package/dist/codegen/vega-lite-emitter.js.map +1 -0
  122. package/dist/compiler.d.ts +17 -0
  123. package/dist/compiler.d.ts.map +1 -0
  124. package/dist/compiler.js +106 -0
  125. package/dist/compiler.js.map +1 -0
  126. package/dist/index.d.ts +7 -0
  127. package/dist/index.d.ts.map +1 -0
  128. package/dist/index.js +9 -0
  129. package/dist/index.js.map +1 -0
  130. package/dist/ir/index.d.ts +3 -0
  131. package/dist/ir/index.d.ts.map +1 -0
  132. package/dist/ir/index.js +2 -0
  133. package/dist/ir/index.js.map +1 -0
  134. package/dist/ir/ir-nodes.d.ts +169 -0
  135. package/dist/ir/ir-nodes.d.ts.map +1 -0
  136. package/dist/ir/ir-nodes.js +2 -0
  137. package/dist/ir/ir-nodes.js.map +1 -0
  138. package/dist/ir/lowering.d.ts +9 -0
  139. package/dist/ir/lowering.d.ts.map +1 -0
  140. package/dist/ir/lowering.js +980 -0
  141. package/dist/ir/lowering.js.map +1 -0
  142. package/dist/modules/index.d.ts +4 -0
  143. package/dist/modules/index.d.ts.map +1 -0
  144. package/dist/modules/index.js +3 -0
  145. package/dist/modules/index.js.map +1 -0
  146. package/dist/modules/resolver.d.ts +21 -0
  147. package/dist/modules/resolver.d.ts.map +1 -0
  148. package/dist/modules/resolver.js +112 -0
  149. package/dist/modules/resolver.js.map +1 -0
  150. package/dist/modules/use-expander.d.ts +11 -0
  151. package/dist/modules/use-expander.d.ts.map +1 -0
  152. package/dist/modules/use-expander.js +42 -0
  153. package/dist/modules/use-expander.js.map +1 -0
  154. package/dist/themes/colorful.d.ts +3 -0
  155. package/dist/themes/colorful.d.ts.map +1 -0
  156. package/dist/themes/colorful.js +14 -0
  157. package/dist/themes/colorful.js.map +1 -0
  158. package/dist/themes/corporate.d.ts +3 -0
  159. package/dist/themes/corporate.d.ts.map +1 -0
  160. package/dist/themes/corporate.js +14 -0
  161. package/dist/themes/corporate.js.map +1 -0
  162. package/dist/themes/dark.d.ts +3 -0
  163. package/dist/themes/dark.d.ts.map +1 -0
  164. package/dist/themes/dark.js +14 -0
  165. package/dist/themes/dark.js.map +1 -0
  166. package/dist/themes/index.d.ts +11 -0
  167. package/dist/themes/index.d.ts.map +1 -0
  168. package/dist/themes/index.js +29 -0
  169. package/dist/themes/index.js.map +1 -0
  170. package/dist/themes/light.d.ts +3 -0
  171. package/dist/themes/light.d.ts.map +1 -0
  172. package/dist/themes/light.js +14 -0
  173. package/dist/themes/light.js.map +1 -0
  174. package/dist/themes/minimal.d.ts +3 -0
  175. package/dist/themes/minimal.d.ts.map +1 -0
  176. package/dist/themes/minimal.js +14 -0
  177. package/dist/themes/minimal.js.map +1 -0
  178. package/dist/themes/theme-types.d.ts +14 -0
  179. package/dist/themes/theme-types.d.ts.map +1 -0
  180. package/dist/themes/theme-types.js +2 -0
  181. package/dist/themes/theme-types.js.map +1 -0
  182. package/package.json +43 -0
@@ -0,0 +1,980 @@
1
+ import { NodeKind, } from '@duckcodeailabs/dql-core';
2
+ export function lowerProgram(program, options = {}) {
3
+ const dashboards = [];
4
+ for (const stmt of program.statements) {
5
+ switch (stmt.kind) {
6
+ case NodeKind.Dashboard:
7
+ dashboards.push(lowerDashboard(stmt, options));
8
+ break;
9
+ case NodeKind.ChartCall:
10
+ // Standalone chart -> wrap in a single-chart dashboard
11
+ dashboards.push({
12
+ title: 'Untitled',
13
+ charts: [lowerChart(stmt, 0, options)],
14
+ filters: [],
15
+ params: [],
16
+ notifications: extractNotifications(stmt.decorators),
17
+ alerts: extractAlerts(stmt.decorators),
18
+ schedule: extractSchedule(stmt.decorators),
19
+ layout: buildLayout(1),
20
+ variables: {},
21
+ });
22
+ break;
23
+ case NodeKind.Workbook:
24
+ // Flatten workbook pages into dashboards for backward compat
25
+ for (const page of lowerWorkbook(stmt, options).pages) {
26
+ dashboards.push({
27
+ title: `${stmt.title} - ${page.title}`,
28
+ charts: page.charts,
29
+ filters: page.filters,
30
+ params: page.params,
31
+ schedule: extractSchedule(stmt.decorators),
32
+ notifications: extractNotifications(stmt.decorators),
33
+ alerts: extractAlerts(stmt.decorators),
34
+ layout: page.layout,
35
+ variables: page.variables,
36
+ layoutDiagnostics: page.layoutDiagnostics,
37
+ });
38
+ }
39
+ break;
40
+ case NodeKind.BlockDecl: {
41
+ const lowered = lowerBlockDecl(stmt, options);
42
+ if (lowered)
43
+ dashboards.push(lowered);
44
+ break;
45
+ }
46
+ case NodeKind.ImportDecl:
47
+ // Import declarations are resolved at compile time, not at IR level
48
+ break;
49
+ }
50
+ }
51
+ return dashboards;
52
+ }
53
+ export function lowerWorkbookProgram(program, options = {}) {
54
+ for (const stmt of program.statements) {
55
+ if (stmt.kind === NodeKind.Workbook) {
56
+ return lowerWorkbook(stmt, options);
57
+ }
58
+ }
59
+ return null;
60
+ }
61
+ function lowerWorkbook(node, options) {
62
+ return {
63
+ title: node.title,
64
+ pages: node.pages.map((page) => lowerPage(page, options)),
65
+ schedule: extractSchedule(node.decorators),
66
+ notifications: extractNotifications(node.decorators),
67
+ alerts: extractAlerts(node.decorators),
68
+ };
69
+ }
70
+ function lowerPage(node, options) {
71
+ const { charts, filters, params, variables, layout, layoutDiagnostics } = lowerBodyItems(node.body, options);
72
+ return {
73
+ title: node.title,
74
+ charts,
75
+ filters,
76
+ params,
77
+ layout,
78
+ variables,
79
+ layoutDiagnostics: layoutDiagnostics.length > 0 ? layoutDiagnostics : undefined,
80
+ };
81
+ }
82
+ function lowerBodyItems(body, options) {
83
+ const charts = [];
84
+ const filters = [];
85
+ const params = [];
86
+ const variables = {};
87
+ const layoutDiagnostics = [];
88
+ let chartIndex = 0;
89
+ let filterIndex = 0;
90
+ let explicitLayout = null;
91
+ for (const item of body) {
92
+ switch (item.kind) {
93
+ case NodeKind.VariableDecl:
94
+ variables[item.name] = evaluateExpression(item.initializer);
95
+ break;
96
+ case NodeKind.ParamDecl:
97
+ params.push(lowerParamDecl(item));
98
+ // Also register as variable with default value
99
+ if (item.defaultValue) {
100
+ variables[item.name] = evaluateExpression(item.defaultValue);
101
+ }
102
+ break;
103
+ case NodeKind.ChartCall:
104
+ charts.push(lowerChart(item, chartIndex++, options));
105
+ break;
106
+ case NodeKind.FilterCall:
107
+ filters.push(lowerFilter(item, filterIndex++));
108
+ break;
109
+ case NodeKind.UseDecl:
110
+ // Use declarations are resolved at compile time
111
+ break;
112
+ case NodeKind.LayoutBlock:
113
+ {
114
+ const loweredLayout = lowerLayoutBlock(item, chartIndex);
115
+ explicitLayout = loweredLayout.layout;
116
+ layoutDiagnostics.push(...loweredLayout.diagnostics);
117
+ }
118
+ // Process charts inside layout rows
119
+ for (const row of item.rows) {
120
+ for (const rowItem of row.items) {
121
+ if (rowItem.node.kind === NodeKind.ChartCall) {
122
+ charts.push(lowerChart(rowItem.node, chartIndex++, options));
123
+ }
124
+ else if (rowItem.node.kind === NodeKind.FilterCall) {
125
+ filters.push(lowerFilter(rowItem.node, filterIndex++));
126
+ }
127
+ }
128
+ }
129
+ break;
130
+ }
131
+ }
132
+ const baseLayout = explicitLayout ?? buildLayout(charts.length);
133
+ const validated = validateAndNormalizeLayout(baseLayout, charts);
134
+ layoutDiagnostics.push(...validated.diagnostics);
135
+ return {
136
+ charts,
137
+ filters,
138
+ params,
139
+ variables,
140
+ layout: validated.layout,
141
+ layoutDiagnostics,
142
+ };
143
+ }
144
+ function lowerDashboard(node, options) {
145
+ const { charts, filters, params, variables, layout, layoutDiagnostics } = lowerBodyItems(node.body, options);
146
+ // Extract @refresh(seconds) decorator for auto-refresh interval
147
+ const refreshDecorator = node.decorators.find((d) => d.name === 'refresh');
148
+ let refreshInterval;
149
+ if (refreshDecorator && refreshDecorator.arguments.length > 0) {
150
+ const val = evaluateExpression(refreshDecorator.arguments[0]);
151
+ refreshInterval = typeof val === 'number' ? val : Number(val);
152
+ if (isNaN(refreshInterval) || refreshInterval <= 0)
153
+ refreshInterval = undefined;
154
+ }
155
+ return {
156
+ title: node.title,
157
+ charts,
158
+ filters,
159
+ params,
160
+ schedule: extractSchedule(node.decorators),
161
+ notifications: extractNotifications(node.decorators),
162
+ alerts: extractAlerts(node.decorators),
163
+ layout,
164
+ variables,
165
+ refreshInterval,
166
+ layoutDiagnostics: layoutDiagnostics.length > 0 ? layoutDiagnostics : undefined,
167
+ };
168
+ }
169
+ function lowerParamDecl(node) {
170
+ return {
171
+ name: node.name,
172
+ paramType: node.paramType,
173
+ defaultValue: node.defaultValue ? evaluateExpression(node.defaultValue) : undefined,
174
+ };
175
+ }
176
+ function lowerLayoutBlock(node, startChartIndex) {
177
+ const items = [];
178
+ const diagnostics = [];
179
+ const columns = node.columns;
180
+ let chartIdx = startChartIndex;
181
+ let currentRow = 1;
182
+ for (const row of node.rows) {
183
+ let currentCol = 1;
184
+ for (const rowItem of row.items) {
185
+ const derivedSpan = rowItem.span ?? Math.floor(columns / row.items.length);
186
+ const colSpan = derivedSpan > 0 ? derivedSpan : 1;
187
+ if (derivedSpan <= 0) {
188
+ diagnostics.push({
189
+ level: 'warning',
190
+ row: currentRow,
191
+ message: `Invalid span ${derivedSpan} in layout row ${currentRow}; defaulted to 1.`,
192
+ });
193
+ }
194
+ if ((currentCol + colSpan - 1) > columns) {
195
+ diagnostics.push({
196
+ level: 'warning',
197
+ row: currentRow,
198
+ message: `Layout row ${currentRow} exceeds ${columns} columns.`,
199
+ });
200
+ }
201
+ if (rowItem.node.kind === NodeKind.ChartCall) {
202
+ items.push({
203
+ chartId: `chart-${chartIdx}`,
204
+ gridColumn: `${currentCol} / span ${colSpan}`,
205
+ gridRow: `${currentRow}`,
206
+ });
207
+ chartIdx++;
208
+ }
209
+ currentCol += colSpan;
210
+ }
211
+ currentRow++;
212
+ }
213
+ return { layout: { type: 'grid', columns, items }, diagnostics };
214
+ }
215
+ function lowerChart(node, index, options) {
216
+ const normalizedChartType = normalizeChartTypeAlias(node.chartType);
217
+ const config = extractChartConfig(node.args);
218
+ const interaction = extractInteractionConfig(node.args);
219
+ let { sql, params } = processSQL(node.query.rawSQL, node.query.interpolations);
220
+ // Extract @if condition from decorators
221
+ const ifDecorator = node.decorators.find((d) => d.name === 'if');
222
+ let condition;
223
+ if (ifDecorator && ifDecorator.arguments.length > 0) {
224
+ condition = evaluateExpression(ifDecorator.arguments[0]);
225
+ }
226
+ // Extract @rls decorator — inject WHERE clause for row-level security
227
+ const rlsApplied = applyRLSDecorators(node.decorators, sql, params);
228
+ sql = rlsApplied.sql;
229
+ params = rlsApplied.params;
230
+ // Extract @cache(ttl) decorator for query caching
231
+ const cacheDecorator = node.decorators.find((d) => d.name === 'cache');
232
+ let cacheTTL;
233
+ if (cacheDecorator && cacheDecorator.arguments.length > 0) {
234
+ const ttlVal = evaluateExpression(cacheDecorator.arguments[0]);
235
+ cacheTTL = typeof ttlVal === 'number' ? ttlVal : Number(ttlVal);
236
+ if (isNaN(cacheTTL))
237
+ cacheTTL = undefined;
238
+ }
239
+ // Extract @annotate(x_value, label [, color]) decorators for chart annotations
240
+ const annotateDecorators = node.decorators.filter((d) => d.name === 'annotate');
241
+ const annotations = annotateDecorators.length > 0
242
+ ? annotateDecorators.map((d) => {
243
+ const args = d.arguments.map(evaluateExpression);
244
+ return {
245
+ x: args[0] != null ? String(args[0]) : undefined,
246
+ label: args[1] != null ? String(args[1]) : '',
247
+ color: args[2] != null ? String(args[2]) : undefined,
248
+ };
249
+ })
250
+ : undefined;
251
+ // Extract @materialize(refresh = "hourly") decorator
252
+ const materializeDec = node.decorators.find((d) => d.name === 'materialize');
253
+ const materializeRefresh = materializeDec && materializeDec.arguments.length > 0
254
+ ? String(evaluateExpression(materializeDec.arguments[0]))
255
+ : undefined;
256
+ const drillConfig = buildDrillConfig(node.args, options);
257
+ return {
258
+ id: `chart-${index}`,
259
+ chartType: normalizedChartType,
260
+ sql,
261
+ sqlParams: params,
262
+ config,
263
+ interaction: hasInteraction(interaction) ? interaction : undefined,
264
+ title: getStringArg(node.args, 'title'),
265
+ theme: getStringArg(node.args, 'theme'),
266
+ condition,
267
+ cacheTTL,
268
+ connection: getStringArg(node.args, 'connection'),
269
+ annotations,
270
+ materializeRefresh,
271
+ drillConfig,
272
+ };
273
+ }
274
+ const SAFE_RLS_COLUMN = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
275
+ const RLS_TEMPLATE_VARIABLE = /^\{([A-Za-z_][A-Za-z0-9_.]*)\}$/;
276
+ function applyRLSDecorators(decorators, sql, params) {
277
+ const rlsDecorators = decorators.filter((d) => d.name === 'rls');
278
+ if (rlsDecorators.length === 0)
279
+ return { sql, params };
280
+ const rlsClauses = [];
281
+ const nextParams = [...params];
282
+ let nextPosition = nextParams.reduce((max, p) => Math.max(max, p.position), 0);
283
+ let literalParamCounter = 0;
284
+ for (const rls of rlsDecorators) {
285
+ if (rls.arguments.length < 2)
286
+ continue;
287
+ const column = normalizeRLSColumn(rls.arguments[0]);
288
+ if (!column)
289
+ continue;
290
+ const valueBinding = resolveRLSValueBinding(rls.arguments[1]);
291
+ if (!valueBinding)
292
+ continue;
293
+ nextPosition += 1;
294
+ if (valueBinding.type === 'variable') {
295
+ nextParams.push({ name: valueBinding.variableName, position: nextPosition });
296
+ }
297
+ else {
298
+ literalParamCounter += 1;
299
+ nextParams.push({
300
+ name: `__rls_literal_${literalParamCounter}`,
301
+ position: nextPosition,
302
+ literalValue: valueBinding.literalValue,
303
+ });
304
+ }
305
+ rlsClauses.push(`${column} = $${nextPosition}`);
306
+ }
307
+ if (rlsClauses.length === 0)
308
+ return { sql, params: nextParams };
309
+ // Wrap the original SQL in a subquery and apply RLS WHERE clause
310
+ const rlsWhere = rlsClauses.join(' AND ');
311
+ return {
312
+ sql: `SELECT * FROM (${sql}) _dql_rls WHERE ${rlsWhere}`,
313
+ params: nextParams,
314
+ };
315
+ }
316
+ function normalizeRLSColumn(arg) {
317
+ let raw = null;
318
+ if (arg.kind === NodeKind.StringLiteral) {
319
+ raw = arg.value;
320
+ }
321
+ else if (arg.kind === NodeKind.Identifier) {
322
+ raw = arg.name;
323
+ }
324
+ else {
325
+ const evaluated = evaluateExpression(arg);
326
+ if (typeof evaluated === 'string')
327
+ raw = evaluated;
328
+ }
329
+ if (!raw)
330
+ return null;
331
+ const trimmed = raw.trim();
332
+ if (!SAFE_RLS_COLUMN.test(trimmed))
333
+ return null;
334
+ return trimmed;
335
+ }
336
+ function resolveRLSValueBinding(arg) {
337
+ if (arg.kind === NodeKind.StringLiteral) {
338
+ const raw = String(arg.value ?? '');
339
+ const match = raw.trim().match(RLS_TEMPLATE_VARIABLE);
340
+ if (match) {
341
+ return { type: 'variable', variableName: match[1] };
342
+ }
343
+ return { type: 'literal', literalValue: raw };
344
+ }
345
+ if (arg.kind === NodeKind.Identifier) {
346
+ return { type: 'variable', variableName: arg.name };
347
+ }
348
+ if (arg.kind === NodeKind.NumberLiteral || arg.kind === NodeKind.BooleanLiteral) {
349
+ return { type: 'literal', literalValue: arg.value };
350
+ }
351
+ const evaluated = evaluateExpression(arg);
352
+ if (typeof evaluated === 'string') {
353
+ const match = evaluated.trim().match(RLS_TEMPLATE_VARIABLE);
354
+ if (match) {
355
+ return { type: 'variable', variableName: match[1] };
356
+ }
357
+ return { type: 'literal', literalValue: evaluated };
358
+ }
359
+ if (typeof evaluated === 'number' || typeof evaluated === 'boolean') {
360
+ return { type: 'literal', literalValue: evaluated };
361
+ }
362
+ return null;
363
+ }
364
+ function lowerFilter(node, index) {
365
+ const label = getStringArg(node.args, 'label') ?? `Filter ${index + 1}`;
366
+ const param = getStringArg(node.args, 'param') ?? `filter_${index}`;
367
+ let sql;
368
+ let sqlParams = [];
369
+ if (node.query) {
370
+ const processed = processSQL(node.query.rawSQL, node.query.interpolations);
371
+ sql = processed.sql;
372
+ sqlParams = processed.params;
373
+ }
374
+ const defaultValue = getStringArg(node.args, 'default_value');
375
+ const placeholder = getStringArg(node.args, 'placeholder');
376
+ const options = {};
377
+ const format = getStringArg(node.args, 'format');
378
+ const debounce = getNumberArg(node.args, 'debounce');
379
+ const min = getNumberArg(node.args, 'min');
380
+ const max = getNumberArg(node.args, 'max');
381
+ const step = getNumberArg(node.args, 'step');
382
+ const width = getNumberArg(node.args, 'width');
383
+ if (format != null)
384
+ options.format = format;
385
+ if (debounce != null)
386
+ options.debounce = debounce;
387
+ if (min != null)
388
+ options.min = min;
389
+ if (max != null)
390
+ options.max = max;
391
+ if (step != null)
392
+ options.step = step;
393
+ if (width != null)
394
+ options.width = width;
395
+ return {
396
+ id: `filter-${index}`,
397
+ filterType: node.filterType,
398
+ sql,
399
+ sqlParams,
400
+ label,
401
+ param,
402
+ defaultValue: defaultValue ?? undefined,
403
+ placeholder,
404
+ options: Object.keys(options).length > 0 ? options : undefined,
405
+ };
406
+ }
407
+ function extractInteractionConfig(args) {
408
+ const config = {};
409
+ for (const arg of args) {
410
+ const value = evaluateExpression(arg.value);
411
+ switch (arg.name) {
412
+ case 'on_click':
413
+ config.onClick = String(value);
414
+ break;
415
+ case 'drill_down':
416
+ config.drillDown = String(value);
417
+ break;
418
+ case 'link_to':
419
+ config.linkTo = String(value);
420
+ break;
421
+ case 'filter_by':
422
+ config.filterBy = Array.isArray(value) ? value.map(String) : String(value);
423
+ break;
424
+ }
425
+ }
426
+ return config;
427
+ }
428
+ function hasInteraction(config) {
429
+ return !!(config.onClick || config.drillDown || config.linkTo || config.filterBy);
430
+ }
431
+ function extractChartConfig(args) {
432
+ const config = {};
433
+ for (const arg of args) {
434
+ const value = evaluateExpression(arg.value);
435
+ switch (arg.name) {
436
+ case 'x':
437
+ config.x = String(value);
438
+ break;
439
+ case 'y':
440
+ config.y = String(value);
441
+ break;
442
+ case 'color':
443
+ config.color = String(value);
444
+ break;
445
+ case 'size':
446
+ config.size = String(value);
447
+ break;
448
+ case 'facet':
449
+ config.facet = String(value);
450
+ break;
451
+ case 'color_field':
452
+ config.colorField = String(value);
453
+ break;
454
+ case 'line_width':
455
+ config.lineWidth = Number(value);
456
+ break;
457
+ case 'fill_opacity':
458
+ config.fillOpacity = Number(value);
459
+ break;
460
+ case 'bar_width':
461
+ config.barWidth = Number(value);
462
+ break;
463
+ case 'inner_radius':
464
+ config.innerRadius = Number(value);
465
+ break;
466
+ case 'orientation':
467
+ config.orientation = String(value);
468
+ break;
469
+ case 'x_axis_label':
470
+ config.xAxisLabel = String(value);
471
+ break;
472
+ case 'y_axis_label':
473
+ config.yAxisLabel = String(value);
474
+ break;
475
+ case 'title_font_size':
476
+ config.titleFontSize = Number(value);
477
+ break;
478
+ case 'show_grid':
479
+ config.showGrid = Boolean(value);
480
+ break;
481
+ case 'show_legend':
482
+ config.showLegend = Boolean(value);
483
+ break;
484
+ case 'width':
485
+ config.width = Number(value);
486
+ break;
487
+ case 'height':
488
+ config.height = Number(value);
489
+ break;
490
+ case 'stroke_dash':
491
+ config.strokeDash = String(value);
492
+ break;
493
+ case 'metrics':
494
+ config.metrics = value;
495
+ break;
496
+ case 'compare_to_previous':
497
+ config.compareToPrevious = Boolean(value);
498
+ break;
499
+ case 'formatting':
500
+ config.formatting = String(value);
501
+ break;
502
+ case 'columns':
503
+ config.columns = value;
504
+ break;
505
+ case 'sortable':
506
+ config.sortable = Boolean(value);
507
+ break;
508
+ case 'page_size':
509
+ config.pageSize = Number(value);
510
+ break;
511
+ case 'y2':
512
+ config.y2 = String(value);
513
+ break;
514
+ case 'tooltip':
515
+ config.tooltip = Array.isArray(value) ? value.map(String) : [String(value)];
516
+ break;
517
+ case 'format_x':
518
+ config.formatX = String(value);
519
+ break;
520
+ case 'format_y':
521
+ config.formatY = String(value);
522
+ break;
523
+ case 'color_rule':
524
+ config.colorRule = String(value);
525
+ break;
526
+ case 'pin_columns':
527
+ config.pinColumns = Array.isArray(value) ? value.map(String) : [String(value)];
528
+ break;
529
+ case 'row_color':
530
+ config.rowColor = String(value);
531
+ break;
532
+ case 'drill_hierarchy':
533
+ config.drillHierarchy = String(value);
534
+ break;
535
+ case 'drill_path':
536
+ config.drillPath = String(value);
537
+ break;
538
+ case 'drill_mode':
539
+ config.drillMode = String(value);
540
+ break;
541
+ case 'topology_url':
542
+ config.topologyUrl = String(value);
543
+ break;
544
+ }
545
+ }
546
+ return config;
547
+ }
548
+ function buildDrillConfig(args, options) {
549
+ const hierarchy = getStringArg(args, 'drill_hierarchy');
550
+ const path = getStringArg(args, 'drill_path');
551
+ const modeRaw = getStringArg(args, 'drill_mode');
552
+ if (!hierarchy) {
553
+ if ((path || modeRaw) && options.diagnostics) {
554
+ options.diagnostics.push('drill_path/drill_mode provided without drill_hierarchy; falling back to legacy drill_down behavior.');
555
+ }
556
+ return undefined;
557
+ }
558
+ const mode = modeRaw && (modeRaw === 'modal' || modeRaw === 'replace' || modeRaw === 'expand')
559
+ ? modeRaw
560
+ : 'modal';
561
+ if (modeRaw && modeRaw !== mode && options.diagnostics) {
562
+ options.diagnostics.push(`Invalid drill_mode '${modeRaw}' for hierarchy '${hierarchy}'. Falling back to 'modal'.`);
563
+ }
564
+ const drillConfig = {
565
+ hierarchy,
566
+ path: path ?? undefined,
567
+ mode,
568
+ };
569
+ const semanticLayer = options.semanticLayer;
570
+ if (!semanticLayer)
571
+ return drillConfig;
572
+ const hierarchyDef = semanticLayer.getHierarchy(hierarchy);
573
+ if (!hierarchyDef) {
574
+ options.diagnostics?.push(`Unknown drill hierarchy '${hierarchy}'.`);
575
+ return drillConfig;
576
+ }
577
+ const resolvedLevels = semanticLayer.resolveDrillPath(hierarchy, path ?? undefined);
578
+ if (path && resolvedLevels.length === 0) {
579
+ options.diagnostics?.push(`Unknown drill path '${path}' for hierarchy '${hierarchy}'.`);
580
+ return {
581
+ ...drillConfig,
582
+ rollup: hierarchyDef.defaultRollup,
583
+ };
584
+ }
585
+ return {
586
+ ...drillConfig,
587
+ rollup: hierarchyDef.defaultRollup,
588
+ levels: resolvedLevels.map((level) => ({
589
+ name: level.name,
590
+ dimension: level.dimension,
591
+ })),
592
+ };
593
+ }
594
+ function processSQL(rawSQL, interpolations) {
595
+ if (interpolations.length === 0) {
596
+ return { sql: rawSQL, params: [] };
597
+ }
598
+ let sql = rawSQL;
599
+ const params = [];
600
+ // Replace ${varName}/{varName} with $N placeholders (process right-to-left to preserve offsets)
601
+ const sorted = [...interpolations].sort((a, b) => b.offsetInSQL - a.offsetInSQL);
602
+ let paramIndex = interpolations.length;
603
+ for (const interp of sorted) {
604
+ const placeholder = `$${paramIndex}`;
605
+ const from = interp.offsetInSQL;
606
+ const rest = sql.slice(from);
607
+ const paramPattern = new RegExp(`^\\$\\{${interp.variableName}\\}|^\\{${interp.variableName}\\}`);
608
+ const match = rest.match(paramPattern);
609
+ if (!match) {
610
+ paramIndex--;
611
+ continue;
612
+ }
613
+ sql = sql.slice(0, from) + placeholder + sql.slice(from + match[0].length);
614
+ params.unshift({ name: interp.variableName, position: paramIndex });
615
+ paramIndex--;
616
+ }
617
+ return { sql, params };
618
+ }
619
+ function lowerBlockDecl(node, _options) {
620
+ if (!node.query)
621
+ return null;
622
+ const vizChartType = getBlockVisualizationChartType(node);
623
+ const chartType = vizChartType ? vizChartType : 'bar';
624
+ const normalizedChartType = normalizeChartTypeAlias(chartType);
625
+ const config = lowerBlockVisualizationConfig(node);
626
+ const { sql, params } = processSQL(node.query.rawSQL, node.query.interpolations);
627
+ const paramIRs = (node.params?.params ?? []).map((p) => ({
628
+ name: p.name,
629
+ paramType: inferParamType(p.initializer),
630
+ defaultValue: evaluateExpression(p.initializer),
631
+ }));
632
+ const variables = {};
633
+ for (const p of node.params?.params ?? []) {
634
+ variables[p.name] = evaluateExpression(p.initializer);
635
+ }
636
+ return {
637
+ title: node.name,
638
+ charts: [{
639
+ id: 'chart-0',
640
+ chartType: normalizedChartType,
641
+ sql,
642
+ sqlParams: params,
643
+ config,
644
+ title: node.description || node.name,
645
+ blockType: node.blockType,
646
+ }],
647
+ filters: [],
648
+ params: paramIRs,
649
+ schedule: extractSchedule(node.decorators),
650
+ notifications: extractNotifications(node.decorators),
651
+ alerts: extractAlerts(node.decorators),
652
+ layout: buildLayout(1),
653
+ variables,
654
+ layoutDiagnostics: undefined,
655
+ };
656
+ }
657
+ function getBlockVisualizationChartType(node) {
658
+ const chartProp = node.visualization?.properties.find((p) => p.name === 'chart');
659
+ if (!chartProp)
660
+ return null;
661
+ const value = evaluateExpression(chartProp.value);
662
+ if (typeof value !== 'string')
663
+ return null;
664
+ return value;
665
+ }
666
+ function lowerBlockVisualizationConfig(node) {
667
+ const config = {};
668
+ for (const prop of node.visualization?.properties ?? []) {
669
+ const value = evaluateExpression(prop.value);
670
+ switch (prop.name) {
671
+ case 'x':
672
+ config.x = String(value);
673
+ break;
674
+ case 'y':
675
+ config.y = String(value);
676
+ break;
677
+ case 'y2':
678
+ config.y2 = String(value);
679
+ break;
680
+ case 'color':
681
+ config.color = String(value);
682
+ break;
683
+ case 'size':
684
+ config.size = String(value);
685
+ break;
686
+ case 'facet':
687
+ config.facet = String(value);
688
+ break;
689
+ case 'metrics':
690
+ config.metrics = Array.isArray(value) ? value.map(String) : [String(value)];
691
+ break;
692
+ case 'columns':
693
+ config.columns = Array.isArray(value) ? value.map(String) : [String(value)];
694
+ break;
695
+ case 'tooltip':
696
+ config.tooltip = Array.isArray(value) ? value.map(String) : [String(value)];
697
+ break;
698
+ case 'width':
699
+ config.width = Number(value);
700
+ break;
701
+ case 'height':
702
+ config.height = Number(value);
703
+ break;
704
+ case 'theme':
705
+ // theme handled at dashboard compile options today.
706
+ break;
707
+ }
708
+ }
709
+ return config;
710
+ }
711
+ function inferParamType(expr) {
712
+ switch (expr.kind) {
713
+ case NodeKind.NumberLiteral:
714
+ return 'number';
715
+ case NodeKind.BooleanLiteral:
716
+ return 'boolean';
717
+ default:
718
+ return 'string';
719
+ }
720
+ }
721
+ function normalizeChartTypeAlias(chartType) {
722
+ switch (chartType) {
723
+ case 'grouped-bar':
724
+ return 'grouped_bar';
725
+ case 'stacked-bar':
726
+ return 'stacked_bar';
727
+ case 'stacked-area':
728
+ case 'stacked_area':
729
+ return 'area';
730
+ case 'donut':
731
+ return 'pie';
732
+ case 'forecast':
733
+ return 'line';
734
+ case 'tree-map':
735
+ return 'treemap';
736
+ case 'flow':
737
+ return 'sankey';
738
+ case 'spark-line':
739
+ case 'spark':
740
+ return 'sparkline';
741
+ case 'small-multiples':
742
+ case 'small_multiple':
743
+ case 'small-multiple':
744
+ return 'small_multiples';
745
+ default:
746
+ return chartType;
747
+ }
748
+ }
749
+ function extractSchedule(decorators) {
750
+ const scheduleDec = decorators.find((d) => d.name === 'schedule');
751
+ if (!scheduleDec)
752
+ return undefined;
753
+ const args = scheduleDec.arguments.map(evaluateExpression);
754
+ return normalizeSchedule(args);
755
+ }
756
+ function normalizeSchedule(args) {
757
+ if (args.length === 0) {
758
+ return { cron: '0 0 * * *' }; // default: daily midnight
759
+ }
760
+ const first = String(args[0]);
761
+ switch (first) {
762
+ case 'daily': {
763
+ const time = args[1] ? parseTime(String(args[1])) : { hour: 0, minute: 0 };
764
+ return { cron: `${time.minute} ${time.hour} * * *` };
765
+ }
766
+ case 'hourly':
767
+ return { cron: '0 * * * *' };
768
+ case 'weekly': {
769
+ const day = args[1] ? parseDayOfWeek(String(args[1])) : 1;
770
+ return { cron: `0 0 * * ${day}` };
771
+ }
772
+ case 'cron':
773
+ return { cron: String(args[1] ?? '0 0 * * *') };
774
+ default:
775
+ // Try as raw cron expression
776
+ return { cron: first };
777
+ }
778
+ }
779
+ function parseTime(time) {
780
+ // Parse "9:00 AM", "14:30", etc.
781
+ const match = time.match(/^(\d{1,2}):(\d{2})\s*(AM|PM)?$/i);
782
+ if (!match)
783
+ return { hour: 0, minute: 0 };
784
+ let hour = parseInt(match[1], 10);
785
+ const minute = parseInt(match[2], 10);
786
+ const ampm = match[3]?.toUpperCase();
787
+ if (ampm === 'PM' && hour < 12)
788
+ hour += 12;
789
+ if (ampm === 'AM' && hour === 12)
790
+ hour = 0;
791
+ return { hour, minute };
792
+ }
793
+ function parseDayOfWeek(day) {
794
+ const days = {
795
+ sunday: 0,
796
+ monday: 1,
797
+ tuesday: 2,
798
+ wednesday: 3,
799
+ thursday: 4,
800
+ friday: 5,
801
+ saturday: 6,
802
+ };
803
+ return days[day.toLowerCase()] ?? 1;
804
+ }
805
+ function extractNotifications(decorators) {
806
+ const notifications = [];
807
+ for (const dec of decorators) {
808
+ if (dec.name === 'email_to') {
809
+ notifications.push({
810
+ type: 'email',
811
+ recipients: dec.arguments.map((a) => String(evaluateExpression(a))),
812
+ });
813
+ }
814
+ else if (dec.name === 'slack_channel') {
815
+ notifications.push({
816
+ type: 'slack',
817
+ recipients: dec.arguments.map((a) => String(evaluateExpression(a))),
818
+ });
819
+ }
820
+ }
821
+ return notifications;
822
+ }
823
+ function extractAlerts(decorators) {
824
+ const alerts = [];
825
+ for (const dec of decorators) {
826
+ if (dec.name !== 'alert')
827
+ continue;
828
+ const args = dec.arguments.map((a) => evaluateExpression(a));
829
+ if (args.length < 1)
830
+ continue;
831
+ const conditionSQL = String(args[0] ?? '').trim();
832
+ if (!conditionSQL)
833
+ continue;
834
+ let operator = '>';
835
+ let threshold = 0;
836
+ let message = undefined;
837
+ if (args.length >= 2) {
838
+ const second = String(args[1]);
839
+ const match = second.match(/^([><=!]+)\s*(\d+(?:\.\d+)?)$/);
840
+ if (match) {
841
+ operator = match[1];
842
+ threshold = parseFloat(match[2]);
843
+ }
844
+ else {
845
+ const n = Number(args[1]);
846
+ threshold = Number.isFinite(n) ? n : 0;
847
+ }
848
+ }
849
+ if (args.length >= 3) {
850
+ message = String(args[2]);
851
+ }
852
+ alerts.push({ conditionSQL, operator, threshold, message });
853
+ }
854
+ return alerts;
855
+ }
856
+ function buildLayout(chartCount) {
857
+ const items = [];
858
+ const columns = 12;
859
+ // Auto-layout: KPIs get full width row, charts get 6-col (2 per row)
860
+ let currentRow = 1;
861
+ let currentCol = 1;
862
+ for (let i = 0; i < chartCount; i++) {
863
+ const colSpan = chartCount === 1 ? 12 : 6;
864
+ items.push({
865
+ chartId: `chart-${i}`,
866
+ gridColumn: `${currentCol} / span ${colSpan}`,
867
+ gridRow: `${currentRow}`,
868
+ });
869
+ currentCol += colSpan;
870
+ if (currentCol > columns) {
871
+ currentCol = 1;
872
+ currentRow++;
873
+ }
874
+ }
875
+ return { type: 'grid', columns, items };
876
+ }
877
+ function validateAndNormalizeLayout(layout, charts) {
878
+ const diagnostics = [];
879
+ const chartIds = new Set(charts.map((chart) => chart.id));
880
+ const seen = new Set();
881
+ const normalizedItems = [];
882
+ for (const item of layout.items) {
883
+ if (!chartIds.has(item.chartId)) {
884
+ diagnostics.push({
885
+ level: 'error',
886
+ chartId: item.chartId,
887
+ message: `Layout references unknown chart "${item.chartId}".`,
888
+ });
889
+ continue;
890
+ }
891
+ if (seen.has(item.chartId)) {
892
+ diagnostics.push({
893
+ level: 'warning',
894
+ chartId: item.chartId,
895
+ message: `Layout contains duplicate entry for "${item.chartId}". Keeping first occurrence.`,
896
+ });
897
+ continue;
898
+ }
899
+ const span = parseGridSpan(item.gridColumn);
900
+ if (span > layout.columns) {
901
+ diagnostics.push({
902
+ level: 'warning',
903
+ chartId: item.chartId,
904
+ message: `Layout span ${span} exceeds grid columns ${layout.columns} for "${item.chartId}".`,
905
+ });
906
+ }
907
+ seen.add(item.chartId);
908
+ normalizedItems.push(item);
909
+ }
910
+ const missingCharts = charts.filter((chart) => !seen.has(chart.id));
911
+ if (missingCharts.length > 0) {
912
+ const fallbackLayout = buildLayout(missingCharts.length);
913
+ const existingRows = normalizedItems
914
+ .map((item) => Number.parseInt(item.gridRow, 10))
915
+ .filter((row) => Number.isFinite(row));
916
+ const rowOffset = existingRows.length > 0 ? Math.max(...existingRows) : 0;
917
+ missingCharts.forEach((chart, index) => {
918
+ const fallback = fallbackLayout.items[index];
919
+ const fallbackRow = Number.parseInt(fallback.gridRow, 10);
920
+ normalizedItems.push({
921
+ chartId: chart.id,
922
+ gridColumn: fallback.gridColumn,
923
+ gridRow: Number.isFinite(fallbackRow) ? String(rowOffset + fallbackRow) : String(rowOffset + 1),
924
+ });
925
+ diagnostics.push({
926
+ level: 'warning',
927
+ chartId: chart.id,
928
+ message: `Chart "${chart.id}" was missing from explicit layout and was auto-placed.`,
929
+ });
930
+ });
931
+ }
932
+ return {
933
+ layout: { ...layout, items: normalizedItems },
934
+ diagnostics,
935
+ };
936
+ }
937
+ function parseGridSpan(gridColumn) {
938
+ const match = gridColumn.match(/span\s+(\d+)/i);
939
+ if (!match)
940
+ return 1;
941
+ const span = Number.parseInt(match[1], 10);
942
+ return Number.isFinite(span) && span > 0 ? span : 1;
943
+ }
944
+ function evaluateExpression(node) {
945
+ switch (node.kind) {
946
+ case NodeKind.StringLiteral:
947
+ return node.value;
948
+ case NodeKind.NumberLiteral:
949
+ return node.value;
950
+ case NodeKind.BooleanLiteral:
951
+ return node.value;
952
+ case NodeKind.Identifier:
953
+ return node.name;
954
+ case NodeKind.ArrayLiteral:
955
+ return node.elements.map(evaluateExpression);
956
+ case NodeKind.IntervalExpr:
957
+ return node.value;
958
+ case NodeKind.BinaryExpr:
959
+ // Simplified: return string representation
960
+ return `${evaluateExpression(node.left)} ${node.operator} ${evaluateExpression(node.right)}`;
961
+ case NodeKind.FunctionCall:
962
+ return node.callee;
963
+ case NodeKind.TemplateString:
964
+ return node.parts.map((p) => (typeof p === 'string' ? p : String(evaluateExpression(p)))).join('');
965
+ }
966
+ }
967
+ function getStringArg(args, name) {
968
+ const arg = args.find((a) => a.name === name);
969
+ if (!arg)
970
+ return undefined;
971
+ return String(evaluateExpression(arg.value));
972
+ }
973
+ function getNumberArg(args, name) {
974
+ const arg = args.find((a) => a.name === name);
975
+ if (!arg)
976
+ return undefined;
977
+ const val = evaluateExpression(arg.value);
978
+ return typeof val === 'number' ? val : Number(val);
979
+ }
980
+ //# sourceMappingURL=lowering.js.map