@svgrid/enterprise 3.0.2 → 3.0.3

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 (364) hide show
  1. package/LICENSE +94 -24
  2. package/README.md +36 -4
  3. package/dist/SvAdvancedFilter.svelte +1 -1
  4. package/dist/SvAlertsManager.svelte +2 -1
  5. package/dist/SvBoard.svelte +2 -1
  6. package/dist/SvExportMenu.svelte +4 -1
  7. package/dist/SvExportMenu.svelte.d.ts +0 -13
  8. package/dist/SvExpressionEditor.svelte +3 -3
  9. package/dist/SvFormulaBar.svelte +94 -20
  10. package/dist/SvFormulaBar.svelte.d.ts +7 -0
  11. package/dist/SvGridAlerts.svelte +49 -9
  12. package/dist/SvGridBoard.svelte +13 -8
  13. package/dist/SvGridBulkEditDrawer.svelte +26 -2
  14. package/dist/SvGridEditPanel.svelte +3 -0
  15. package/dist/SvGridScheduler.svelte +69 -40
  16. package/dist/SvGridSelectionBar.svelte +10 -3
  17. package/dist/SvPivotDesigner.svelte +198 -40
  18. package/dist/SvPivotDesigner.svelte.d.ts +27 -20
  19. package/dist/SvSheet.svelte +4043 -243
  20. package/dist/SvSheet.svelte.d.ts +76 -3
  21. package/dist/SvSheetChartSetup.svelte +151 -0
  22. package/dist/SvSheetChartSetup.svelte.d.ts +18 -0
  23. package/dist/SvSheetComment.svelte +216 -31
  24. package/dist/SvSheetComment.svelte.d.ts +11 -11
  25. package/dist/SvSheetConditionalFormat.svelte +28 -31
  26. package/dist/SvSheetDataValidation.svelte +66 -42
  27. package/dist/SvSheetEditRanges.svelte +217 -0
  28. package/dist/SvSheetEditRanges.svelte.d.ts +15 -0
  29. package/dist/SvSheetErrors.svelte +125 -0
  30. package/dist/SvSheetErrors.svelte.d.ts +20 -0
  31. package/dist/SvSheetEvaluate.svelte +133 -0
  32. package/dist/SvSheetEvaluate.svelte.d.ts +15 -0
  33. package/dist/SvSheetFilterMenu.svelte +137 -34
  34. package/dist/SvSheetFilterMenu.svelte.d.ts +5 -0
  35. package/dist/SvSheetFindReplace.svelte +19 -19
  36. package/dist/SvSheetFormatCells.svelte +125 -81
  37. package/dist/SvSheetGoalSeek.svelte +17 -16
  38. package/dist/SvSheetInsertFunction.svelte +15 -13
  39. package/dist/SvSheetIteration.svelte +81 -0
  40. package/dist/SvSheetIteration.svelte.d.ts +10 -0
  41. package/dist/SvSheetLink.svelte +99 -0
  42. package/dist/SvSheetLink.svelte.d.ts +17 -0
  43. package/dist/SvSheetListPicker.svelte +4 -2
  44. package/dist/SvSheetListPicker.svelte.d.ts +0 -6
  45. package/dist/SvSheetManageRules.svelte +20 -18
  46. package/dist/SvSheetNameManager.svelte +17 -15
  47. package/dist/SvSheetPageSetup.svelte +159 -0
  48. package/dist/SvSheetPageSetup.svelte.d.ts +13 -0
  49. package/dist/SvSheetPasteSpecial.svelte +22 -20
  50. package/dist/SvSheetPivot.svelte +220 -0
  51. package/dist/SvSheetPivot.svelte.d.ts +21 -0
  52. package/dist/SvSheetProtectSheet.svelte +81 -0
  53. package/dist/SvSheetProtectSheet.svelte.d.ts +11 -0
  54. package/dist/SvSheetRemoveDuplicates.svelte +13 -10
  55. package/dist/SvSheetRibbon.svelte +104 -51
  56. package/dist/SvSheetSizeDialog.svelte +4 -2
  57. package/dist/SvSheetSort.svelte +218 -0
  58. package/dist/SvSheetSort.svelte.d.ts +28 -0
  59. package/dist/SvSheetSparklines.svelte +135 -0
  60. package/dist/SvSheetSparklines.svelte.d.ts +14 -0
  61. package/dist/SvSheetTable.svelte +160 -0
  62. package/dist/SvSheetTable.svelte.d.ts +24 -0
  63. package/dist/SvSheetTabs.svelte +161 -31
  64. package/dist/SvSheetTabs.svelte.d.ts +22 -8
  65. package/dist/SvSheetTextToColumns.svelte +20 -20
  66. package/dist/SvSheetValidationAlert.svelte +8 -6
  67. package/dist/advanced-filter-enable.js +1 -1
  68. package/dist/alerts/alert-engine.d.ts +6 -0
  69. package/dist/alerts/alert-engine.js +21 -4
  70. package/dist/alerts/alert-formats.js +2 -0
  71. package/dist/alerts/alert-types.d.ts +1 -1
  72. package/dist/alerts.d.ts +1 -1
  73. package/dist/alerts.js +2 -2
  74. package/dist/board.js +2 -2
  75. package/dist/bulk-edit.d.ts +3 -1
  76. package/dist/bulk-edit.js +1 -1
  77. package/dist/cdn/svgrid-enterprise.svelte-external.js +39827 -16429
  78. package/dist/designer/assets/GridMenus-8gTNssgV.js +8 -0
  79. package/dist/designer/assets/{SvChartMenu-D9hEeHar.js → SvChartMenu-BDoeqGz6.js} +1 -1
  80. package/dist/designer/assets/{SvDateTimePicker-DARvKUL2.js → SvDateTimePicker-DCPg7WGN.js} +1 -1
  81. package/dist/designer/assets/SvGridCellEditor-DMoWJjEI.js +4 -0
  82. package/dist/designer/assets/{SvGridChart-B8j1SSy3.js → SvGridChart-Cy1ATrWh.js} +2 -2
  83. package/dist/designer/assets/SvGridChartBuilder-eQ3mn0L9.js +1 -0
  84. package/dist/designer/assets/{SvGridChartPanel-pT6ITc2P.js → SvGridChartPanel-CATdVc9X.js} +3 -3
  85. package/dist/designer/assets/{SvGridChartView-bg281UA1.js → SvGridChartView-CmsZtCED.js} +1 -1
  86. package/dist/designer/assets/SvGridDropdown-Cjz0H-fI.js +1 -0
  87. package/dist/designer/assets/{SvTabs-UFCMTHXS.js → SvTabs-2mHC6oaz.js} +1 -1
  88. package/dist/designer/assets/cell-formatting-BbubJTVz.js +1 -0
  89. package/dist/designer/assets/{chart-cIPUsbM5.js → chart-Cn7MnNqA.js} +1 -1
  90. package/dist/designer/assets/{chart-panel-messages-DtTxpBUU.js → chart-panel-messages-Dz7PTIqT.js} +1 -1
  91. package/dist/designer/assets/{chart-summary-Czu_5epb.js → chart-summary-BD1qYB2R.js} +1 -1
  92. package/dist/designer/assets/{chart-trend-D5I5VEe8.js → chart-trend-B8xr5LuD.js} +1 -1
  93. package/dist/designer/assets/{dismissable-BO547MH9.js → dismissable-ChWRPkoa.js} +1 -1
  94. package/dist/designer/assets/{export-format-D-eP4b2l.js → export-format-DGckmgKr.js} +1 -1
  95. package/dist/designer/assets/{focus-trap-CAreAhlU.js → focus-trap-V2-6Fy9I.js} +1 -1
  96. package/dist/designer/assets/{index-DwU7CAMm.js → index-D5bVbuzI.js} +317 -315
  97. package/dist/designer/assets/index-_JVFxt02.css +1 -0
  98. package/dist/designer/assets/row-resize-BInFWCpr.js +1 -0
  99. package/dist/designer/assets/server-block-cache-CXJKkJpy.js +1 -0
  100. package/dist/designer/assets/src-DUQ4xE3x.js +58 -0
  101. package/dist/designer/assets/validate-C3B8QKsM.js +1 -0
  102. package/dist/designer/index.html +17 -16
  103. package/dist/export-pdf.d.ts +75 -0
  104. package/dist/export-pdf.js +216 -15
  105. package/dist/export.js +10 -2
  106. package/dist/gantt/SvGridGantt.svelte +2890 -0
  107. package/dist/gantt/SvGridGantt.svelte.d.ts +30 -0
  108. package/dist/gantt/gantt-config.d.ts +73 -0
  109. package/dist/gantt/gantt-config.js +1 -0
  110. package/dist/gantt/gantt-critical-path.d.ts +67 -0
  111. package/dist/gantt/gantt-critical-path.js +207 -0
  112. package/dist/gantt/gantt-model.d.ts +257 -0
  113. package/dist/gantt/gantt-model.js +651 -0
  114. package/dist/gantt/gantt-resources.d.ts +59 -0
  115. package/dist/gantt/gantt-resources.js +71 -0
  116. package/dist/gantt/gantt.d.ts +7 -0
  117. package/dist/gantt/gantt.js +36 -0
  118. package/dist/gantt/index.d.ts +15 -0
  119. package/dist/gantt/index.js +22 -0
  120. package/dist/gantt/timeline-arrows.d.ts +102 -0
  121. package/dist/gantt/timeline-arrows.js +181 -0
  122. package/dist/import.d.ts +2 -2
  123. package/dist/import.js +1 -1
  124. package/dist/index.d.ts +34 -7
  125. package/dist/index.js +44 -7
  126. package/dist/install.js +8 -1
  127. package/dist/license-core.d.ts +17 -1
  128. package/dist/license-core.js +16 -5
  129. package/dist/license.d.ts +18 -2
  130. package/dist/license.js +71 -6
  131. package/dist/node/studio.js +140 -8
  132. package/dist/pivot-enable.js +2 -2
  133. package/dist/scheduler-dependencies.d.ts +35 -0
  134. package/dist/scheduler-dependencies.js +36 -1
  135. package/dist/scheduler.js +2 -2
  136. package/dist/selection-bar.js +2 -2
  137. package/dist/server/SvGroupCell.svelte +213 -0
  138. package/dist/server/SvGroupCell.svelte.d.ts +62 -0
  139. package/dist/server/SvRowGroupPanel.svelte +322 -0
  140. package/dist/server/SvRowGroupPanel.svelte.d.ts +37 -0
  141. package/dist/server/enable.d.ts +17 -0
  142. package/dist/server/enable.js +49 -0
  143. package/dist/server/index.d.ts +22 -0
  144. package/dist/server/index.js +24 -0
  145. package/dist/server/messages.d.ts +45 -0
  146. package/dist/server/messages.js +35 -0
  147. package/dist/server/server-group-model.d.ts +104 -0
  148. package/dist/server/server-group-model.js +272 -0
  149. package/dist/server/server-pivot.d.ts +46 -0
  150. package/dist/server/server-pivot.js +57 -0
  151. package/dist/server/server-row-model.d.ts +547 -0
  152. package/dist/server/server-row-model.js +1551 -0
  153. package/dist/server/server-selection.d.ts +99 -0
  154. package/dist/server/server-selection.js +215 -0
  155. package/dist/server/svgrid-adapter.d.ts +155 -0
  156. package/dist/server/svgrid-adapter.js +202 -0
  157. package/dist/sheet/ast.d.ts +45 -1
  158. package/dist/sheet/ast.js +14 -0
  159. package/dist/sheet/auto-filter.d.ts +58 -4
  160. package/dist/sheet/auto-filter.js +159 -11
  161. package/dist/sheet/autocomplete.d.ts +9 -0
  162. package/dist/sheet/autocomplete.js +90 -4
  163. package/dist/sheet/cell-images.d.ts +39 -0
  164. package/dist/sheet/cell-images.js +90 -0
  165. package/dist/sheet/cell-types.d.ts +58 -0
  166. package/dist/sheet/cell-types.js +96 -0
  167. package/dist/sheet/coerce.d.ts +40 -0
  168. package/dist/sheet/coerce.js +170 -3
  169. package/dist/sheet/comments.d.ts +43 -9
  170. package/dist/sheet/comments.js +85 -27
  171. package/dist/sheet/conditional-formats.d.ts +29 -6
  172. package/dist/sheet/conditional-formats.js +37 -12
  173. package/dist/sheet/csv.d.ts +32 -0
  174. package/dist/sheet/csv.js +158 -0
  175. package/dist/sheet/culture.d.ts +87 -0
  176. package/dist/sheet/culture.js +247 -0
  177. package/dist/sheet/delta.d.ts +102 -0
  178. package/dist/sheet/delta.js +203 -0
  179. package/dist/sheet/deps.d.ts +45 -1
  180. package/dist/sheet/deps.js +77 -2
  181. package/dist/sheet/document.d.ts +106 -4
  182. package/dist/sheet/document.js +204 -3
  183. package/dist/sheet/engine.d.ts +67 -0
  184. package/dist/sheet/engine.js +19 -0
  185. package/dist/sheet/entry.d.ts +8 -4
  186. package/dist/sheet/entry.js +171 -21
  187. package/dist/sheet/error-check.d.ts +49 -0
  188. package/dist/sheet/error-check.js +83 -0
  189. package/dist/sheet/evaluate-steps.d.ts +48 -0
  190. package/dist/sheet/evaluate-steps.js +196 -0
  191. package/dist/sheet/evaluate.d.ts +67 -0
  192. package/dist/sheet/evaluate.js +725 -13
  193. package/dist/sheet/filter-files.d.ts +37 -0
  194. package/dist/sheet/filter-files.js +465 -0
  195. package/dist/sheet/format-store.d.ts +15 -0
  196. package/dist/sheet/format-store.js +11 -1
  197. package/dist/sheet/from-rows.d.ts +14 -0
  198. package/dist/sheet/from-rows.js +50 -0
  199. package/dist/sheet/function-catalog.d.ts +1 -1
  200. package/dist/sheet/function-catalog.js +296 -5
  201. package/dist/sheet/functions.js +363 -91
  202. package/dist/sheet/index.d.ts +35 -8
  203. package/dist/sheet/index.js +35 -8
  204. package/dist/sheet/links.d.ts +57 -0
  205. package/dist/sheet/links.js +174 -0
  206. package/dist/sheet/messages.d.ts +778 -0
  207. package/dist/sheet/messages.js +806 -0
  208. package/dist/sheet/number-format.d.ts +52 -5
  209. package/dist/sheet/number-format.js +465 -68
  210. package/dist/sheet/objects.d.ts +106 -0
  211. package/dist/sheet/objects.js +149 -0
  212. package/dist/sheet/ods-document.d.ts +44 -0
  213. package/dist/sheet/ods-document.js +1275 -0
  214. package/dist/sheet/open-file.d.ts +26 -0
  215. package/dist/sheet/open-file.js +56 -0
  216. package/dist/sheet/outline.d.ts +82 -0
  217. package/dist/sheet/outline.js +228 -0
  218. package/dist/sheet/packs/array.d.ts +16 -0
  219. package/dist/sheet/packs/array.js +172 -0
  220. package/dist/sheet/packs/database.d.ts +42 -0
  221. package/dist/sheet/packs/database.js +142 -0
  222. package/dist/sheet/packs/engineering.d.ts +2 -0
  223. package/dist/sheet/packs/engineering.js +296 -0
  224. package/dist/sheet/packs/financial.d.ts +5 -0
  225. package/dist/sheet/packs/financial.js +220 -0
  226. package/dist/sheet/packs/math-stats.d.ts +2 -0
  227. package/dist/sheet/packs/math-stats.js +372 -0
  228. package/dist/sheet/packs/special.d.ts +44 -0
  229. package/dist/sheet/packs/special.js +187 -0
  230. package/dist/sheet/packs/stats-dist.d.ts +2 -0
  231. package/dist/sheet/packs/stats-dist.js +785 -0
  232. package/dist/sheet/packs/text-date.d.ts +2 -0
  233. package/dist/sheet/packs/text-date.js +332 -0
  234. package/dist/sheet/packs/trig.d.ts +2 -0
  235. package/dist/sheet/packs/trig.js +73 -0
  236. package/dist/sheet/page-setup.d.ts +73 -0
  237. package/dist/sheet/page-setup.js +73 -0
  238. package/dist/sheet/parse.js +42 -10
  239. package/dist/sheet/paste-special.d.ts +22 -0
  240. package/dist/sheet/paste-special.js +14 -5
  241. package/dist/sheet/pivot-range.d.ts +160 -0
  242. package/dist/sheet/pivot-range.js +333 -0
  243. package/dist/sheet/presence.d.ts +73 -0
  244. package/dist/sheet/presence.js +79 -0
  245. package/dist/sheet/print.d.ts +104 -0
  246. package/dist/sheet/print.js +169 -0
  247. package/dist/sheet/protection.d.ts +55 -1
  248. package/dist/sheet/protection.js +55 -2
  249. package/dist/sheet/rects.d.ts +1 -1
  250. package/dist/sheet/refs.d.ts +48 -0
  251. package/dist/sheet/refs.js +155 -8
  252. package/dist/sheet/ribbon-icons.d.ts +1 -1
  253. package/dist/sheet/ribbon-icons.js +186 -0
  254. package/dist/sheet/ribbon.d.ts +1 -1
  255. package/dist/sheet/ribbon.js +324 -23
  256. package/dist/sheet/sheet-kinds.d.ts +74 -0
  257. package/dist/sheet/sheet-kinds.js +91 -0
  258. package/dist/sheet/shortcuts.d.ts +13 -3
  259. package/dist/sheet/shortcuts.js +63 -12
  260. package/dist/sheet/sort.d.ts +51 -0
  261. package/dist/sheet/sort.js +107 -0
  262. package/dist/sheet/sparklines.d.ts +111 -0
  263. package/dist/sheet/sparklines.js +204 -0
  264. package/dist/sheet/table-styles.d.ts +74 -0
  265. package/dist/sheet/table-styles.js +152 -0
  266. package/dist/sheet/tables.d.ts +19 -0
  267. package/dist/sheet/tables.js +31 -0
  268. package/dist/sheet/tokenize.d.ts +18 -1
  269. package/dist/sheet/tokenize.js +60 -9
  270. package/dist/sheet/validation.d.ts +16 -0
  271. package/dist/sheet/validation.js +37 -0
  272. package/dist/sheet/workbook.d.ts +148 -3
  273. package/dist/sheet/workbook.js +742 -31
  274. package/dist/sheet/xls-cfb.d.ts +32 -0
  275. package/dist/sheet/xls-cfb.js +290 -0
  276. package/dist/sheet/xls-document.d.ts +10 -0
  277. package/dist/sheet/xls-document.js +1243 -0
  278. package/dist/sheet/xls-formula.d.ts +40 -0
  279. package/dist/sheet/xls-formula.js +569 -0
  280. package/dist/sheet/xlsx-document.d.ts +108 -0
  281. package/dist/sheet/xlsx-document.js +1849 -0
  282. package/dist/sheet/xlsx-drawing.d.ts +99 -0
  283. package/dist/sheet/xlsx-drawing.js +420 -0
  284. package/dist/sheet-enable.js +6 -7
  285. package/dist/sheet-text.d.ts +7 -0
  286. package/dist/sheet-text.js +29 -0
  287. package/dist/sources/auth-supabase.js +2 -2
  288. package/dist/sources/realtime-supabase.js +2 -2
  289. package/dist/sources/rest.d.ts +6 -2
  290. package/dist/sources/rest.js +42 -7
  291. package/dist/sources/supabase.d.ts +10 -1
  292. package/dist/sources/supabase.js +73 -40
  293. package/dist/studio/emit-project.js +14 -1
  294. package/dist/studio/index.js +1 -1
  295. package/dist/studio/project.d.ts +11 -2
  296. package/dist/studio/project.js +5 -2
  297. package/dist/studio/ui-components.generated.js +107 -2
  298. package/dist/sveltekit/in-memory.d.ts +2 -2
  299. package/dist/sveltekit/in-memory.js +125 -4
  300. package/dist/sveltekit/index.d.ts +1 -1
  301. package/dist/sveltekit/index.js +1 -1
  302. package/dist/sveltekit/query-plan.d.ts +50 -4
  303. package/dist/sveltekit/query-plan.js +25 -3
  304. package/dist/sveltekit/sql-source.d.ts +6 -0
  305. package/dist/sveltekit/sql-source.js +34 -6
  306. package/dist/sveltekit/sql.d.ts +31 -0
  307. package/dist/sveltekit/sql.js +135 -25
  308. package/dist/sveltekit/transport.d.ts +10 -2
  309. package/dist/sveltekit/transport.js +30 -6
  310. package/dist/upgrade-prompt.d.ts +1 -1
  311. package/dist/version.d.ts +1 -1
  312. package/dist/version.js +1 -1
  313. package/dist/watermark.js +35 -2
  314. package/dist/wc/GridMenus-By9vPuq0.js +1088 -0
  315. package/dist/wc/SvChartMenu-CQNrQGT2.js +308 -0
  316. package/dist/wc/SvDateRangeInput-DR6m7bSM.js +199 -0
  317. package/dist/wc/SvDateTimePicker-D-vRnaMo.js +771 -0
  318. package/dist/wc/SvField-B_kurx_P.js +149 -0
  319. package/dist/wc/SvGrid.helpers-BdVDtjUm.js +135 -0
  320. package/dist/wc/SvGridCellEditor-Bnf1Kr5C.js +560 -0
  321. package/dist/wc/SvGridChart-BqUJh2pG.js +3572 -0
  322. package/dist/wc/SvGridChartBuilder-CGycC5cn.js +988 -0
  323. package/dist/wc/SvGridChartPanel-CRnizsHD.js +1746 -0
  324. package/dist/wc/SvGridChartView-D1pa_xMy.js +47 -0
  325. package/dist/wc/SvGridDropdown-45WsObLL.js +259 -0
  326. package/dist/wc/SvModal-DrfOYYyX.js +189 -0
  327. package/dist/wc/cell-formatting-CBfgahv1.js +86 -0
  328. package/dist/wc/chart-6OXlybVT.js +4341 -0
  329. package/dist/wc/chart-export-CeSADGfi.js +135 -0
  330. package/dist/wc/chart-export-pdf-BGko7MER.js +104 -0
  331. package/dist/wc/chart-summary-2m3ia1-n.js +137 -0
  332. package/dist/wc/chart-trend-CQOQtTMl.js +50 -0
  333. package/dist/wc/chart-validate-CD6ptiwg.js +180 -0
  334. package/dist/wc/client-DQIzjDBG.js +3064 -0
  335. package/dist/wc/column-resize-DUPgynvc.js +101 -0
  336. package/dist/wc/date-format-DS-z_h9v.js +1130 -0
  337. package/dist/wc/dismissable-CQ6XFXsk.js +45 -0
  338. package/dist/wc/editor-contract-peBDxir_.js +22 -0
  339. package/dist/wc/editor-registry-DxGKqcvx.js +24 -0
  340. package/dist/wc/focus-trap-Dbb2tJxb.js +65 -0
  341. package/dist/wc/list-option-DrRyivWp.js +82 -0
  342. package/dist/wc/popover-WsGlvtQv.js +69 -0
  343. package/dist/wc/positioning-BiKLfu1E.js +112 -0
  344. package/dist/wc/react.d.ts +41 -0
  345. package/dist/wc/react.js +90 -0
  346. package/dist/wc/rolldown-runtime-Dy4uBu1J.js +11 -0
  347. package/dist/wc/row-drag-touch-CycRR65n.js +93 -0
  348. package/dist/wc/row-resize-limG_N1z.js +111 -0
  349. package/dist/wc/sv-sheet-element.d.ts +97 -0
  350. package/dist/wc/sv-sheet-element.js +44213 -0
  351. package/dist/wc/svelte-virtualizer.svelte-CXtQy2TQ.js +530 -0
  352. package/dist/wc/validate-BqDthkNr.js +88 -0
  353. package/dist/wc/vue.d.ts +31 -0
  354. package/dist/wc/vue.js +75 -0
  355. package/package.json +30 -5
  356. package/dist/designer/assets/GridMenus-BlsvL_aU.js +0 -8
  357. package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +0 -4
  358. package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +0 -1
  359. package/dist/designer/assets/SvGridDropdown-Bb3HH2Aj.js +0 -1
  360. package/dist/designer/assets/cell-formatting-wr5wm7US.js +0 -1
  361. package/dist/designer/assets/index-CMFgAoVV.css +0 -1
  362. package/dist/designer/assets/row-resize-DLktcqUK.js +0 -1
  363. package/dist/designer/assets/src-EdI3Xqds.js +0 -58
  364. package/dist/designer/assets/validate-h-aslRV9.js +0 -1
@@ -53,6 +53,53 @@ function matches(row, plan) {
53
53
  * - the rest ignore non-numeric / null values, and yield null for an empty
54
54
  * numeric set (SQL returns NULL for SUM/AVG/MIN/MAX over no rows).
55
55
  */
56
+ /**
57
+ * The grand-total row: every requested aggregate over the whole filtered
58
+ * set. Shaped like a group row (values under their column ids) so the
59
+ * grid renders it with the same columns.
60
+ */
61
+ /**
62
+ * The aggregates of `rows` into `out`: one value per aggregation, or under
63
+ * pivot one per (pivot key path x aggregation) named `<path>_<col>`, with
64
+ * every pivot key path seen collected into `paths`.
65
+ */
66
+ function aggregateInto(out, rows, plan, paths) {
67
+ if (plan.pivotBy?.length) {
68
+ const cells = new Map();
69
+ for (const r of rows) {
70
+ const path = plan.pivotBy.map((p) => asString(r[p])).join('_');
71
+ const cell = cells.get(path);
72
+ if (cell)
73
+ cell.push(r);
74
+ else
75
+ cells.set(path, [r]);
76
+ }
77
+ for (const [path, cellRows] of cells) {
78
+ paths?.add(path);
79
+ for (const agg of plan.aggregations ?? []) {
80
+ out[`${path}_${agg.field}`] = aggregate(cellRows, agg.field, agg.fn);
81
+ }
82
+ }
83
+ }
84
+ // The plain aggregate, on its own or beside the per-key fields of a pivot,
85
+ // where it is the row total.
86
+ for (const agg of plan.aggregations ?? [])
87
+ out[agg.field] = aggregate(rows, agg.field, agg.fn);
88
+ }
89
+ function grandTotalOf(all, plan, expression) {
90
+ // The group path is the trailing `pathPredicates` entries of `where`; it
91
+ // scopes this request, not the total, so it comes off.
92
+ const filtersOnly = {
93
+ ...plan,
94
+ where: plan.where.slice(0, plan.where.length - (plan.pathPredicates ?? 0)),
95
+ };
96
+ let rows = all.filter((r) => matches(r, filtersOnly));
97
+ if (expression)
98
+ rows = rows.filter(expression);
99
+ const out = {};
100
+ aggregateInto(out, rows, plan);
101
+ return out;
102
+ }
56
103
  function aggregate(rows, field, fn) {
57
104
  if (fn === 'count')
58
105
  return rows.length;
@@ -112,6 +159,7 @@ export function createInMemoryDataSource(initial, schema) {
112
159
  // grid then tells the user the filter did not run rather than showing a
113
160
  // superset that looks filtered.
114
161
  let appliedExpression = false;
162
+ let expressionPredicate = null;
115
163
  if (plan.expression) {
116
164
  const predicate = compilePredicate(plan.expression, {
117
165
  getValue: (row, columnId) => row[columnId],
@@ -119,9 +167,16 @@ export function createInMemoryDataSource(initial, schema) {
119
167
  });
120
168
  if (predicate) {
121
169
  filtered = filtered.filter(predicate);
170
+ expressionPredicate = predicate;
122
171
  appliedExpression = true;
123
172
  }
124
173
  }
174
+ // The grand total spans everything the FILTERS admit - the whole
175
+ // store minus the filters and the expression, but NOT minus the group
176
+ // path, which only scopes this request.
177
+ const grandTotal = plan.grandTotal
178
+ ? { grandTotal: grandTotalOf(store, plan, expressionPredicate) }
179
+ : {};
125
180
  // Grouped request: return one row per distinct key at this level, each
126
181
  // carrying the requested aggregates. The chosen path is already in
127
182
  // `plan.where`, so `filtered` is exactly this group's slice of the data.
@@ -138,18 +193,38 @@ export function createInMemoryDataSource(initial, schema) {
138
193
  else
139
194
  buckets.set(key, [row]);
140
195
  }
196
+ // Pivoting: every aggregate is computed once per distinct pivot key
197
+ // path inside the group, under a field named from the path, and the
198
+ // field list goes back so the grid can build the columns.
199
+ const pivotPaths = new Set();
141
200
  const groups = [...buckets.entries()].map(([key, rows]) => {
142
201
  const out = { [groupField]: key };
143
- for (const agg of plan.aggregations ?? []) {
144
- out[agg.field] = aggregate(rows, agg.field, agg.fn);
145
- }
202
+ aggregateInto(out, rows, plan, pivotPaths);
203
+ // What opening this group would show: distinct keys of the next
204
+ // level, or the rows themselves at the innermost group level.
205
+ out.childCount = plan.childGroupBy
206
+ ? new Set(rows.map((r) => asString(r[plan.childGroupBy]))).size
207
+ : rows.length;
146
208
  return out;
147
209
  });
210
+ // The pivoted fields in a fixed order - key paths in plain string
211
+ // order, aggregations as requested under each - so the grid builds
212
+ // the same columns whatever order the groups came in. Every group
213
+ // row carries every field; a cell with no rows under it is null, the
214
+ // way a conditional aggregate answers in SQL.
215
+ const pivotFields = [...pivotPaths]
216
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
217
+ .flatMap((path) => (plan.aggregations ?? []).map((agg) => `${path}_${agg.field}`));
218
+ for (const g of groups) {
219
+ for (const f of pivotFields)
220
+ if (!(f in g))
221
+ g[f] = null;
222
+ }
148
223
  // Sort by the group key unless the request sorted on a column we
149
224
  // actually produced (the key or an aggregate).
150
225
  const produced = new Set([
151
226
  groupField,
152
- ...(plan.aggregations ?? []).map((a) => a.field),
227
+ ...(plan.pivotBy?.length ? [] : (plan.aggregations ?? []).map((a) => a.field)),
153
228
  ]);
154
229
  const groupOrder = plan.orderBy.filter((o) => produced.has(o.field));
155
230
  const sorted = groupOrder.length
@@ -161,6 +236,8 @@ export function createInMemoryDataSource(initial, schema) {
161
236
  // its scrollbar and paging from this.
162
237
  rowCount: sorted.length,
163
238
  appliedExpression,
239
+ ...grandTotal,
240
+ ...(plan.pivotBy?.length ? { pivotResultFields: pivotFields } : {}),
164
241
  };
165
242
  }
166
243
  const sortedLeaves = sortRows(filtered, plan.orderBy);
@@ -168,6 +245,7 @@ export function createInMemoryDataSource(initial, schema) {
168
245
  rows: sortedLeaves.slice(plan.offset, plan.offset + plan.limit),
169
246
  rowCount: sortedLeaves.length,
170
247
  appliedExpression,
248
+ ...grandTotal,
171
249
  };
172
250
  },
173
251
  async createRow(input) {
@@ -191,6 +269,49 @@ export function createInMemoryDataSource(initial, schema) {
191
269
  async deleteRow(id) {
192
270
  store = store.filter((r) => asString(r[idField]) !== id);
193
271
  },
272
+ /**
273
+ * The reference for a bulk edit by RULE: every row the filters admit
274
+ * that the selection rule says is selected, patched in place. The rule
275
+ * is resolved by walking group values the way the grid builds routes,
276
+ * so the nested shape works without the grid having sent any rows.
277
+ */
278
+ async updateWhere(filterModel, patch, selection) {
279
+ const plan = planQuery(schema, {
280
+ startRow: 0, endRow: 0, pageIndex: 0, pageSize: 0, sortModel: [], filterModel,
281
+ });
282
+ // Which columns the nested rule is keyed by, level by level, comes
283
+ // from the request that built it; a flat rule needs none. Callers
284
+ // pass the groupBy through `filterModel.expression`-free means: the
285
+ // rule itself carries `groupBy` when it is nested.
286
+ const groupBy = selection.groupBy ?? [];
287
+ const selected = (row) => {
288
+ if ('selectAll' in selection) {
289
+ const id = asString(row[idField]);
290
+ return selection.selectAll !== selection.toggled.includes(id);
291
+ }
292
+ // Walk the tree: each level's key is the row's value in that group
293
+ // column; the leaf is the row id.
294
+ let node = selection;
295
+ let state = selection.selectAllChildren;
296
+ const keys = [...groupBy.map((g) => asString(row[g])), asString(row[idField])];
297
+ for (const key of keys) {
298
+ const next = node.toggled[key];
299
+ if (!next)
300
+ return state;
301
+ node = next;
302
+ state = next.selectAllChildren;
303
+ }
304
+ return state;
305
+ };
306
+ let changed = 0;
307
+ store = store.map((row) => {
308
+ if (!matches(row, plan) || !selected(row))
309
+ return row;
310
+ changed += 1;
311
+ return { ...row, ...patch };
312
+ });
313
+ return changed;
314
+ },
194
315
  async getAggregate(request) {
195
316
  // Apply the (optional) filter using the same plan machinery as getRows,
196
317
  // over the whole store (no paging), then reduce in memory.
@@ -1,4 +1,4 @@
1
- export { planQuery, coerce, type QueryPlan, type PlanPredicate, type PlanOp, } from './query-plan.js';
1
+ export { planQuery, coerce, BUILTIN_AGGREGATORS, type QueryPlan, type PlanPredicate, type PlanOp, type PlanAggregate, type PlanOptions, } from './query-plan.js';
2
2
  export { createInMemoryDataSource } from './in-memory.js';
3
3
  export { createKitDataSource, createKitHandlers, type KitDataSourceOptions, type KitHandlerOptions, type KitScope, type KitHandlers, type KitMessage, } from './transport.js';
4
4
  export { planToSql, type SqlDialect, type SqlPlan } from './sql.js';
@@ -1,4 +1,4 @@
1
- export { planQuery, coerce, } from './query-plan.js';
1
+ export { planQuery, coerce, BUILTIN_AGGREGATORS, } from './query-plan.js';
2
2
  export { createInMemoryDataSource } from './in-memory.js';
3
3
  export { createKitDataSource, createKitHandlers, } from './transport.js';
4
4
  export { planToSql } from './sql.js';
@@ -24,10 +24,25 @@ export type PlanPredicate = {
24
24
  /** Operands for `in` (set / facet filter). */
25
25
  values?: unknown[];
26
26
  };
27
- /** An aggregate to compute per group. */
27
+ /**
28
+ * An aggregate to compute per group. `fn` is one of the built-ins or a
29
+ * name the planner was told to allow (`PlanOptions.aggregators`); it is
30
+ * never a string the client chose on its own, because it ends up in SQL.
31
+ */
28
32
  export type PlanAggregate = {
29
33
  field: string;
30
- fn: 'sum' | 'avg' | 'min' | 'max' | 'count';
34
+ fn: string;
35
+ };
36
+ /** What the planner emits without being asked. */
37
+ export declare const BUILTIN_AGGREGATORS: ReadonlyArray<string>;
38
+ export type PlanOptions = {
39
+ /**
40
+ * Aggregate function names to allow besides the built-ins, exactly as
41
+ * the database spells them (`median`, `percentile_cont`, `count_distinct`).
42
+ * Anything a request asks for that is not built in or listed here is
43
+ * dropped from the plan.
44
+ */
45
+ aggregators?: ReadonlyArray<string>;
31
46
  };
32
47
  export type QueryPlan = {
33
48
  /** AND-combined column predicates. */
@@ -56,8 +71,39 @@ export type QueryPlan = {
56
71
  * grouping existed) stays valid; `planQuery` always sets it.
57
72
  */
58
73
  groupBy?: string | null;
59
- /** Aggregates to compute alongside `groupBy`. Empty/absent for a leaf query. */
74
+ /**
75
+ * Aggregates to compute alongside `groupBy`. Empty for a leaf query,
76
+ * unless `grandTotal` is set - then they are what the total is made of.
77
+ */
60
78
  aggregations?: PlanAggregate[];
79
+ /**
80
+ * On a grouped query, also count what the NEXT level holds per group -
81
+ * the distinct keys of `childGroupBy`, or the raw rows when this is the
82
+ * innermost group level. Lands on each group row as `childCount`, which
83
+ * the grid shows beside the key and uses to size the group before it
84
+ * opens. Always set on a grouped plan from `planQuery`.
85
+ */
86
+ childGroupBy?: string | null;
87
+ /**
88
+ * How many entries at the END of `where` are the group path (`groupKeys`
89
+ * turned into equality predicates) rather than filters the user set.
90
+ * The grand total aggregates over the filters alone, so it drops these.
91
+ */
92
+ pathPredicates?: number;
93
+ /**
94
+ * Server-side pivot, on a grouped plan: the columns whose distinct values
95
+ * become fields. Each group row then carries, for every (pivot key path x
96
+ * aggregation), a field named `<key>[_<key>]_<col>` - `2024_amount` - and
97
+ * the response lists them in `pivotResultFields`. Absent when not
98
+ * pivoting. The separator is `_`, matching the row model's default.
99
+ */
100
+ pivotBy?: string[];
101
+ /**
102
+ * The grid wants a grand-total row and does not have one cached. Compute
103
+ * the aggregates over the whole filtered set (the request's filters,
104
+ * without the group path) and answer with `ServerResult.grandTotal`.
105
+ */
106
+ grandTotal?: boolean;
61
107
  /**
62
108
  * The advanced-filter expression, present ONLY when every column it
63
109
  * references is on the schema.
@@ -72,4 +118,4 @@ export type QueryPlan = {
72
118
  };
73
119
  /** Coerce a raw string request value to the field's runtime type for correct comparisons. */
74
120
  export declare function coerce(type: EntityFieldType, raw: unknown): unknown;
75
- export declare function planQuery<TData extends RowData>(schema: EntitySchema<TData>, request: ServerRequest): QueryPlan;
121
+ export declare function planQuery<TData extends RowData>(schema: EntitySchema<TData>, request: ServerRequest, options?: PlanOptions): QueryPlan;
@@ -1,3 +1,7 @@
1
+ /** What the planner emits without being asked. */
2
+ export const BUILTIN_AGGREGATORS = ['sum', 'avg', 'min', 'max', 'count'];
3
+ /** Function names are identifiers, nothing else, before they reach SQL. */
4
+ const AGG_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
1
5
  /** Grid filter operator -> normalized plan operator. Unmapped operators are ignored. */
2
6
  const OP_MAP = {
3
7
  equals: 'eq',
@@ -26,8 +30,12 @@ export function coerce(type, raw) {
26
30
  return typeof raw === 'string' ? raw : String(raw);
27
31
  }
28
32
  }
29
- export function planQuery(schema, request) {
33
+ export function planQuery(schema, request, options = {}) {
30
34
  const fields = new Map(schema.fields.map((f) => [f.field, f]));
35
+ const allowedFn = new Set([
36
+ ...BUILTIN_AGGREGATORS,
37
+ ...(options.aggregators ?? []).filter((name) => AGG_NAME.test(name)),
38
+ ]);
31
39
  const where = [];
32
40
  const fm = request.filterModel ?? {};
33
41
  for (const [field, spec] of Object.entries(fm.columns ?? {})) {
@@ -86,9 +94,13 @@ export function planQuery(schema, request) {
86
94
  }
87
95
  // Above the innermost level the grid wants GROUP rows; at or below it, leaves.
88
96
  const groupBy = groupKeys.length < groupCols.length ? (groupCols[groupKeys.length] ?? null) : null;
89
- const aggregations = groupBy
97
+ // Aggregates ride along on a grouped plan, and on a leaf plan ONLY when a
98
+ // grand total was asked for - the leaf query itself never uses them, but
99
+ // the total statement does.
100
+ const aggregations = groupBy || request.needsGrandTotal
90
101
  ? (request.aggregations ?? [])
91
- .filter((a) => fields.has(a.col))
102
+ // Column AND function are whitelisted: both end up in SQL.
103
+ .filter((a) => fields.has(a.col) && allowedFn.has(a.fn))
92
104
  .map((a) => ({ field: a.col, fn: a.fn }))
93
105
  : [];
94
106
  // ---- Advanced filter ---------------------------------------------------
@@ -96,6 +108,10 @@ export function planQuery(schema, request) {
96
108
  // schema the whole expression is dropped. Keeping the understood parts would
97
109
  // silently widen the result set.
98
110
  const expression = admitExpression(request.filterModel?.expression, fields);
111
+ // What the level below this one groups by, so a group row can say how
112
+ // many children it has. Null at the innermost group level (children are
113
+ // leaves) and on a leaf query.
114
+ const childGroupBy = groupBy ? (groupCols[groupKeys.length + 1] ?? null) : null;
99
115
  return {
100
116
  where,
101
117
  search,
@@ -104,6 +120,12 @@ export function planQuery(schema, request) {
104
120
  offset: request.startRow,
105
121
  groupBy,
106
122
  aggregations,
123
+ ...(groupBy ? { childGroupBy } : {}),
124
+ ...(groupBy && request.pivotMode && request.pivotBy?.length
125
+ ? { pivotBy: request.pivotBy.filter((f) => fields.has(f)) }
126
+ : {}),
127
+ pathPredicates: Math.min(groupKeys.length, groupCols.length),
128
+ ...(request.needsGrandTotal ? { grandTotal: true } : {}),
107
129
  ...(expression ? { expression } : {}),
108
130
  };
109
131
  }
@@ -43,5 +43,11 @@ export type SqlDataSourceConfig<TData extends RowData> = {
43
43
  * Default true.
44
44
  */
45
45
  returning?: boolean;
46
+ /**
47
+ * Aggregate function names to accept from a request besides `sum`,
48
+ * `avg`, `min`, `max` and `count`, spelt as the database spells them.
49
+ * Names outside this list never reach SQL.
50
+ */
51
+ aggregators?: ReadonlyArray<string>;
46
52
  };
47
53
  export declare function createSqlDataSource<TData extends RowData>(config: SqlDataSourceConfig<TData>): WritableDataSource<TData> & AggregateSource;
@@ -1,5 +1,5 @@
1
1
  import { resolveIdField } from '../schema.js';
2
- import { nudgeEnterprise } from '../license.js';
2
+ import { nudgeEnterpriseFor } from '../license.js';
3
3
  import { planQuery } from './query-plan.js';
4
4
  import { planToSql } from './sql.js';
5
5
  const SQL_AGG = { sum: 'SUM', avg: 'AVG', min: 'MIN', max: 'MAX' };
@@ -19,10 +19,11 @@ function binder(dialect, params) {
19
19
  }
20
20
  const squish = (sql) => sql.replace(/\s+/g, ' ').trim();
21
21
  export function createSqlDataSource(config) {
22
- nudgeEnterprise('Studio'); // soft-gate; never blocks, safe on the server
22
+ nudgeEnterpriseFor('studio', 'Studio'); // evaluation nudge; never blocks, safe on the server
23
23
  const { schema, execute } = config;
24
24
  const dialect = config.dialect ?? {};
25
25
  const returning = config.returning ?? true;
26
+ const planOptions = { aggregators: config.aggregators ?? [] };
26
27
  const id = quoter(dialect);
27
28
  const idField = resolveIdField(schema);
28
29
  // Optionally schema-qualify the table: "schema"."table" (each part quoted separately).
@@ -41,15 +42,42 @@ export function createSqlDataSource(config) {
41
42
  const RET = returning ? ` RETURNING ${projection}` : '';
42
43
  return {
43
44
  async getRows(request) {
44
- const plan = planQuery(schema, request);
45
+ const plan = planQuery(schema, request, planOptions);
45
46
  const sql = planToSql(plan, planDialect);
46
47
  const t = qtable;
47
- const rowsSql = squish(`SELECT ${projection} FROM ${t} ${sql.whereText} ${sql.orderByText} LIMIT ${sql.limit} OFFSET ${sql.offset}`);
48
- const countSql = squish(`SELECT COUNT(*) AS count FROM ${t} ${sql.whereText}`);
48
+ // A grouped level selects the group column + aggregates and groups by
49
+ // it; a leaf level selects the projection. `countText` already knows
50
+ // that a grouped total is the number of DISTINCT keys, not of rows.
51
+ // Pivot: the distinct key paths first, then one conditional aggregate
52
+ // per (path x aggregation) in the grouped SELECT.
53
+ let select = sql.select;
54
+ let grandTotalSelect = sql.grandTotalSelect;
55
+ let pivotResultFields;
56
+ if (plan.groupBy && plan.pivotBy?.length) {
57
+ const keyRows = await execute(squish(`SELECT ${sql.pivotKeysSelect} FROM ${t} ${sql.whereText}`), sql.params);
58
+ const pivot = sql.pivotSelect(keyRows);
59
+ select = pivot.select;
60
+ if (plan.grandTotal)
61
+ grandTotalSelect = pivot.grandTotalSelect;
62
+ pivotResultFields = pivot.fields;
63
+ }
64
+ const rowsSql = plan.groupBy
65
+ ? squish(`SELECT ${select} FROM ${t} ${sql.whereText} ${sql.groupByText} ${sql.orderByText} LIMIT ${sql.limit} OFFSET ${sql.offset}`)
66
+ : squish(`SELECT ${projection} FROM ${t} ${sql.whereText} ${sql.orderByText} LIMIT ${sql.limit} OFFSET ${sql.offset}`);
67
+ const countSql = squish(`SELECT ${sql.countText} AS count FROM ${t} ${sql.whereText}`);
49
68
  const rows = await execute(rowsSql, sql.params);
50
69
  const countRows = await execute(countSql, sql.params);
51
70
  const rowCount = Number(countRows[0]?.count ?? rows.length);
52
- return { rows: rows, rowCount };
71
+ const result = { rows: rows, rowCount };
72
+ if (pivotResultFields)
73
+ result.pivotResultFields = pivotResultFields;
74
+ // The grand total is its own statement, over the filters without the
75
+ // group path - planToSql hands back exactly those pieces.
76
+ if (plan.grandTotal && grandTotalSelect) {
77
+ const totalRows = await execute(squish(`SELECT ${grandTotalSelect} FROM ${t} ${sql.grandTotalWhereText}`), sql.grandTotalParams);
78
+ result.grandTotal = (totalRows[0] ?? null);
79
+ }
80
+ return result;
53
81
  },
54
82
  async createRow(input) {
55
83
  const cols = Object.keys(input);
@@ -64,5 +64,36 @@ export type SqlPlan = {
64
64
  * grid's scrollbar and paging will be sized from the wrong number.
65
65
  */
66
66
  countText: string;
67
+ /**
68
+ * The grand-total statement's pieces, present only when the request asked
69
+ * for one (`plan.grandTotal`): a SELECT list of every aggregate over the
70
+ * whole filtered set (aliased like `select`), and a WHERE + params of the
71
+ * filters WITHOUT the group path. Run it as its own query:
72
+ *
73
+ * SELECT ${sql.grandTotalSelect} FROM sales ${sql.grandTotalWhereText}
74
+ *
75
+ * All three are empty when no total was asked for.
76
+ */
77
+ grandTotalSelect: string;
78
+ grandTotalWhereText: string;
79
+ grandTotalParams: unknown[];
80
+ /**
81
+ * Server-side pivot needs two statements, because SQL cannot make
82
+ * columns out of values it has not seen yet. First fetch the distinct
83
+ * key paths: `SELECT ${sql.pivotKeysSelect} FROM t ${sql.whereText}`
84
+ * (one row per path, columns named after the pivot columns). Then build
85
+ * the grouped SELECT with `pivotSelect(keyRows)`: one conditional
86
+ * aggregate per (key path x aggregation), aliased `<key>_<col>` - the
87
+ * fields the grid expects in `pivotResultFields` (returned alongside).
88
+ * Key values are inlined as quoted literals; they came from the database
89
+ * itself. Both are empty when not pivoting.
90
+ */
91
+ pivotKeysSelect: string;
92
+ pivotSelect: (keyRows: ReadonlyArray<Record<string, unknown>>) => {
93
+ select: string;
94
+ fields: string[];
95
+ /** The same aggregates for the grand-total statement, no group column. */
96
+ grandTotalSelect: string;
97
+ };
67
98
  };
68
99
  export declare function planToSql(plan: QueryPlan, dialect?: SqlDialect): SqlPlan;
@@ -2,37 +2,66 @@ export function planToSql(plan, dialect = {}) {
2
2
  const q = dialect.quote ?? '"';
3
3
  const useIlike = dialect.ilike ?? false;
4
4
  const params = [];
5
- const ph = () => {
6
- if (dialect.placeholders === '$')
7
- return `$${params.length}`;
8
- if (dialect.placeholders === '@')
9
- return `@p${params.length}`;
10
- return '?';
11
- };
12
- const bind = (v) => {
13
- params.push(v);
14
- return ph();
15
- };
16
5
  const columnFor = dialect.column ?? ((f) => f);
17
6
  const id = (field) => {
18
7
  const c = columnFor(field);
19
8
  return `${q}${c.replace(new RegExp(q, 'g'), q + q)}${q}`;
20
9
  };
21
- const like = (field, pattern) => {
22
- const p = bind(pattern);
23
- return useIlike ? `${id(field)} ILIKE ${p}` : `LOWER(${id(field)}) LIKE LOWER(${p})`;
10
+ // The planner whitelists function names; this is the second lock on the
11
+ // same door, since an aggregate name is pasted into the statement raw.
12
+ const aggName = (fn) => {
13
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(fn))
14
+ throw new Error(`planToSql: "${fn}" is not an aggregate function name`);
15
+ return fn.toUpperCase();
24
16
  };
25
- const clauses = [];
26
- for (const pred of plan.where) {
27
- clauses.push(predicateSql(pred, { id, bind, like }));
28
- }
29
- if (plan.search && plan.search.fields.length > 0) {
30
- const term = `%${plan.search.term}%`;
31
- const ors = plan.search.fields.map((f) => like(f, term));
32
- clauses.push(`(${ors.join(' OR ')})`);
17
+ /**
18
+ * Render predicates + search into one WHERE body, binding into `into`.
19
+ * Placeholder numbering follows that array, so a second call with a fresh
20
+ * array (the grand total) starts from $1 again.
21
+ */
22
+ const buildWhere = (preds, into) => {
23
+ const ph = () => {
24
+ if (dialect.placeholders === '$')
25
+ return `$${into.length}`;
26
+ if (dialect.placeholders === '@')
27
+ return `@p${into.length}`;
28
+ return '?';
29
+ };
30
+ const bind = (v) => {
31
+ into.push(v);
32
+ return ph();
33
+ };
34
+ const like = (field, pattern) => {
35
+ const p = bind(pattern);
36
+ return useIlike ? `${id(field)} ILIKE ${p}` : `LOWER(${id(field)}) LIKE LOWER(${p})`;
37
+ };
38
+ const clauses = [];
39
+ for (const pred of preds)
40
+ clauses.push(predicateSql(pred, { id, bind, like }));
41
+ if (plan.search && plan.search.fields.length > 0) {
42
+ const term = `%${plan.search.term}%`;
43
+ const ors = plan.search.fields.map((f) => like(f, term));
44
+ clauses.push(`(${ors.join(' OR ')})`);
45
+ }
46
+ return clauses.join(' AND ');
47
+ };
48
+ const where = buildWhere(plan.where, params);
49
+ // A grouped level can only order by what it produces - the key and the
50
+ // aggregates (under pivot only the key, the aggregates being split per
51
+ // pivot key); a leaf column there is a SQL error. With nothing left the
52
+ // key orders the groups, so paging over them is deterministic. Same rule
53
+ // as the in-memory reference.
54
+ let order = plan.orderBy;
55
+ if (plan.groupBy) {
56
+ const produced = new Set([
57
+ plan.groupBy,
58
+ ...(plan.pivotBy?.length ? [] : (plan.aggregations ?? []).map((a) => a.field)),
59
+ ]);
60
+ order = order.filter((o) => produced.has(o.field));
61
+ if (order.length === 0)
62
+ order = [{ field: plan.groupBy, desc: false }];
33
63
  }
34
- const where = clauses.join(' AND ');
35
- const orderBy = plan.orderBy.map((o) => `${id(o.field)} ${o.desc ? 'DESC' : 'ASC'}`).join(', ');
64
+ const orderBy = order.map((o) => `${id(o.field)} ${o.desc ? 'DESC' : 'ASC'}`).join(', ');
36
65
  // ---- Grouping ----------------------------------------------------------
37
66
  // `plan.groupBy` is a single column: the grid asks one level at a time and
38
67
  // sends the chosen path as ordinary predicates, so there is never a
@@ -44,13 +73,89 @@ export function planToSql(plan, dialect = {}) {
44
73
  for (const agg of plan.aggregations ?? []) {
45
74
  // COUNT(*) rather than COUNT(col) so the tally counts rows in the group
46
75
  // rather than non-null values of one column.
47
- const expr = agg.fn === 'count' ? 'COUNT(*)' : `${agg.fn.toUpperCase()}(${id(agg.field)})`;
76
+ const expr = agg.fn === 'count' ? 'COUNT(*)' : `${aggName(agg.fn)}(${id(agg.field)})`;
48
77
  // Aliased back to the source column name: that is the key the grid reads
49
78
  // the aggregate from on the group row.
50
79
  parts.push(`${expr} AS ${id(agg.field)}`);
51
80
  }
81
+ // What opening the group would show, for the badge beside its key: the
82
+ // distinct keys of the next level, or the rows when this is the innermost
83
+ // group level. Always emitted; a caller that does not want the extra
84
+ // aggregate can drop the alias from the list.
85
+ parts.push(plan.childGroupBy
86
+ ? `COUNT(DISTINCT ${id(plan.childGroupBy)}) AS ${id('childCount')}`
87
+ : `COUNT(*) AS ${id('childCount')}`);
52
88
  select = parts.join(', ');
53
89
  }
90
+ // The grand total aggregates over everything the FILTERS admit, so the
91
+ // group path (the trailing `pathPredicates` entries of `where`, per
92
+ // planQuery) is left out.
93
+ let grandTotalSelect = '';
94
+ let grandTotalWhereText = '';
95
+ const grandTotalParams = [];
96
+ if (plan.grandTotal) {
97
+ grandTotalSelect = (plan.aggregations ?? [])
98
+ .map((agg) => {
99
+ const expr = agg.fn === 'count' ? 'COUNT(*)' : `${aggName(agg.fn)}(${id(agg.field)})`;
100
+ return `${expr} AS ${id(agg.field)}`;
101
+ })
102
+ .join(', ');
103
+ const filtersOnly = plan.where.slice(0, plan.where.length - (plan.pathPredicates ?? 0));
104
+ const body = buildWhere(filtersOnly, grandTotalParams);
105
+ grandTotalWhereText = body ? `WHERE ${body}` : '';
106
+ }
107
+ // ---- Pivot ------------------------------------------------------------
108
+ let pivotKeysSelect = '';
109
+ let pivotSelect = () => ({ select: '', fields: [], grandTotalSelect: '' });
110
+ if (plan.groupBy && plan.pivotBy?.length) {
111
+ const pivotCols = plan.pivotBy;
112
+ const keyList = pivotCols
113
+ .map((c) => (columnFor(c) === c ? id(c) : `${id(c)} AS ${q}${c}${q}`))
114
+ .join(', ');
115
+ pivotKeysSelect = `DISTINCT ${keyList}`;
116
+ pivotSelect = (keyRows) => {
117
+ // Key paths in plain string order, so the field list (and the
118
+ // columns the grid builds from it) is the same whatever order the
119
+ // distinct-keys query returned - and the same as the reference.
120
+ const pathOf = (row) => pivotCols.map((c) => String(row[c] ?? '')).join('_');
121
+ keyRows = [...keyRows].sort((a, b) => {
122
+ const x = pathOf(a);
123
+ const y = pathOf(b);
124
+ return x < y ? -1 : x > y ? 1 : 0;
125
+ });
126
+ const groupCol = id(plan.groupBy);
127
+ const parts = [groupCol];
128
+ const totals = [];
129
+ const fields = [];
130
+ for (const keyRow of keyRows) {
131
+ const path = pivotCols.map((c) => String(keyRow[c] ?? ''));
132
+ // The key path as an inline condition. Values are quoted as string
133
+ // literals with doubled quotes; a caller with typed columns can
134
+ // rewrite through `pivotSelect` itself.
135
+ const cond = pivotCols
136
+ .map((c, i) => `${id(c)} = '${path[i].replace(/'/g, "''")}'`)
137
+ .join(' AND ');
138
+ for (const agg of plan.aggregations ?? []) {
139
+ const field = `${path.join('_')}_${agg.field}`;
140
+ const inner = agg.fn === 'count' ? '1' : id(agg.field);
141
+ const fn = agg.fn === 'count' ? 'COUNT' : aggName(agg.fn);
142
+ const expr = `${fn}(CASE WHEN ${cond} THEN ${inner} END) AS ${id(field)}`;
143
+ parts.push(expr);
144
+ totals.push(expr);
145
+ fields.push(field);
146
+ }
147
+ }
148
+ // The plain aggregates too: the row total beside the per-key cells,
149
+ // which is what a Total column group reads.
150
+ for (const agg of plan.aggregations ?? []) {
151
+ const expr = agg.fn === 'count' ? 'COUNT(*)' : `${aggName(agg.fn)}(${id(agg.field)})`;
152
+ parts.push(`${expr} AS ${id(agg.field)}`);
153
+ totals.push(`${expr} AS ${id(agg.field)}`);
154
+ }
155
+ parts.push(`COUNT(*) AS ${id('childCount')}`);
156
+ return { select: parts.join(', '), fields, grandTotalSelect: totals.join(', ') };
157
+ };
158
+ }
54
159
  // A grouped total is the number of distinct keys, not of underlying rows.
55
160
  const countText = plan.groupBy
56
161
  ? `COUNT(DISTINCT ${id(plan.groupBy)})`
@@ -67,6 +172,11 @@ export function planToSql(plan, dialect = {}) {
67
172
  groupBy,
68
173
  groupByText: groupBy ? `GROUP BY ${groupBy}` : '',
69
174
  countText,
175
+ grandTotalSelect,
176
+ grandTotalWhereText,
177
+ grandTotalParams,
178
+ pivotKeysSelect,
179
+ pivotSelect,
70
180
  };
71
181
  }
72
182
  function predicateSql(pred, ctx) {