@svgrid/enterprise 3.0.2 → 3.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.
- package/LICENSE +94 -24
- package/README.md +36 -4
- package/dist/SvAdvancedFilter.svelte +1 -1
- package/dist/SvAlertsManager.svelte +2 -1
- package/dist/SvBoard.svelte +2 -1
- package/dist/SvExportMenu.svelte +4 -1
- package/dist/SvExportMenu.svelte.d.ts +0 -13
- package/dist/SvExpressionEditor.svelte +3 -3
- package/dist/SvFormulaBar.svelte +94 -20
- package/dist/SvFormulaBar.svelte.d.ts +7 -0
- package/dist/SvGridAlerts.svelte +49 -9
- package/dist/SvGridBoard.svelte +13 -8
- package/dist/SvGridBulkEditDrawer.svelte +26 -2
- package/dist/SvGridEditPanel.svelte +3 -0
- package/dist/SvGridScheduler.svelte +69 -40
- package/dist/SvGridSelectionBar.svelte +10 -3
- package/dist/SvPivotDesigner.svelte +198 -40
- package/dist/SvPivotDesigner.svelte.d.ts +27 -20
- package/dist/SvSheet.svelte +4043 -243
- package/dist/SvSheet.svelte.d.ts +76 -3
- package/dist/SvSheetChartSetup.svelte +151 -0
- package/dist/SvSheetChartSetup.svelte.d.ts +18 -0
- package/dist/SvSheetComment.svelte +216 -31
- package/dist/SvSheetComment.svelte.d.ts +11 -11
- package/dist/SvSheetConditionalFormat.svelte +28 -31
- package/dist/SvSheetDataValidation.svelte +66 -42
- package/dist/SvSheetEditRanges.svelte +217 -0
- package/dist/SvSheetEditRanges.svelte.d.ts +15 -0
- package/dist/SvSheetErrors.svelte +125 -0
- package/dist/SvSheetErrors.svelte.d.ts +20 -0
- package/dist/SvSheetEvaluate.svelte +133 -0
- package/dist/SvSheetEvaluate.svelte.d.ts +15 -0
- package/dist/SvSheetFilterMenu.svelte +137 -34
- package/dist/SvSheetFilterMenu.svelte.d.ts +5 -0
- package/dist/SvSheetFindReplace.svelte +19 -19
- package/dist/SvSheetFormatCells.svelte +125 -81
- package/dist/SvSheetGoalSeek.svelte +17 -16
- package/dist/SvSheetInsertFunction.svelte +15 -13
- package/dist/SvSheetIteration.svelte +81 -0
- package/dist/SvSheetIteration.svelte.d.ts +10 -0
- package/dist/SvSheetLink.svelte +99 -0
- package/dist/SvSheetLink.svelte.d.ts +17 -0
- package/dist/SvSheetListPicker.svelte +4 -2
- package/dist/SvSheetListPicker.svelte.d.ts +0 -6
- package/dist/SvSheetManageRules.svelte +20 -18
- package/dist/SvSheetNameManager.svelte +17 -15
- package/dist/SvSheetPageSetup.svelte +159 -0
- package/dist/SvSheetPageSetup.svelte.d.ts +13 -0
- package/dist/SvSheetPasteSpecial.svelte +22 -20
- package/dist/SvSheetPivot.svelte +220 -0
- package/dist/SvSheetPivot.svelte.d.ts +21 -0
- package/dist/SvSheetProtectSheet.svelte +81 -0
- package/dist/SvSheetProtectSheet.svelte.d.ts +11 -0
- package/dist/SvSheetRemoveDuplicates.svelte +13 -10
- package/dist/SvSheetRibbon.svelte +104 -51
- package/dist/SvSheetSizeDialog.svelte +4 -2
- package/dist/SvSheetSort.svelte +218 -0
- package/dist/SvSheetSort.svelte.d.ts +28 -0
- package/dist/SvSheetSparklines.svelte +135 -0
- package/dist/SvSheetSparklines.svelte.d.ts +14 -0
- package/dist/SvSheetTable.svelte +160 -0
- package/dist/SvSheetTable.svelte.d.ts +24 -0
- package/dist/SvSheetTabs.svelte +161 -31
- package/dist/SvSheetTabs.svelte.d.ts +22 -8
- package/dist/SvSheetTextToColumns.svelte +20 -20
- package/dist/SvSheetValidationAlert.svelte +8 -6
- package/dist/advanced-filter-enable.js +1 -1
- package/dist/alerts/alert-engine.d.ts +6 -0
- package/dist/alerts/alert-engine.js +21 -4
- package/dist/alerts/alert-formats.js +2 -0
- package/dist/alerts/alert-types.d.ts +1 -1
- package/dist/alerts.d.ts +1 -1
- package/dist/alerts.js +2 -2
- package/dist/board.js +2 -2
- package/dist/bulk-edit.d.ts +3 -1
- package/dist/bulk-edit.js +1 -1
- package/dist/cdn/svgrid-enterprise.svelte-external.js +39827 -16429
- package/dist/designer/assets/GridMenus-8gTNssgV.js +8 -0
- package/dist/designer/assets/{SvChartMenu-D9hEeHar.js → SvChartMenu-BDoeqGz6.js} +1 -1
- package/dist/designer/assets/{SvDateTimePicker-DARvKUL2.js → SvDateTimePicker-DCPg7WGN.js} +1 -1
- package/dist/designer/assets/SvGridCellEditor-DMoWJjEI.js +4 -0
- package/dist/designer/assets/{SvGridChart-B8j1SSy3.js → SvGridChart-Cy1ATrWh.js} +2 -2
- package/dist/designer/assets/SvGridChartBuilder-eQ3mn0L9.js +1 -0
- package/dist/designer/assets/{SvGridChartPanel-pT6ITc2P.js → SvGridChartPanel-CATdVc9X.js} +3 -3
- package/dist/designer/assets/{SvGridChartView-bg281UA1.js → SvGridChartView-CmsZtCED.js} +1 -1
- package/dist/designer/assets/SvGridDropdown-Cjz0H-fI.js +1 -0
- package/dist/designer/assets/{SvTabs-UFCMTHXS.js → SvTabs-2mHC6oaz.js} +1 -1
- package/dist/designer/assets/cell-formatting-BbubJTVz.js +1 -0
- package/dist/designer/assets/{chart-cIPUsbM5.js → chart-Cn7MnNqA.js} +1 -1
- package/dist/designer/assets/{chart-panel-messages-DtTxpBUU.js → chart-panel-messages-Dz7PTIqT.js} +1 -1
- package/dist/designer/assets/{chart-summary-Czu_5epb.js → chart-summary-BD1qYB2R.js} +1 -1
- package/dist/designer/assets/{chart-trend-D5I5VEe8.js → chart-trend-B8xr5LuD.js} +1 -1
- package/dist/designer/assets/{dismissable-BO547MH9.js → dismissable-ChWRPkoa.js} +1 -1
- package/dist/designer/assets/{export-format-D-eP4b2l.js → export-format-DGckmgKr.js} +1 -1
- package/dist/designer/assets/{focus-trap-CAreAhlU.js → focus-trap-V2-6Fy9I.js} +1 -1
- package/dist/designer/assets/{index-DwU7CAMm.js → index-BSjv6Qec.js} +317 -315
- package/dist/designer/assets/index-_JVFxt02.css +1 -0
- package/dist/designer/assets/row-resize-BInFWCpr.js +1 -0
- package/dist/designer/assets/server-block-cache-CXJKkJpy.js +1 -0
- package/dist/designer/assets/src-DUQ4xE3x.js +58 -0
- package/dist/designer/assets/validate-C3B8QKsM.js +1 -0
- package/dist/designer/index.html +17 -16
- package/dist/export-pdf.d.ts +75 -0
- package/dist/export-pdf.js +216 -15
- package/dist/export.js +10 -2
- package/dist/gantt/SvGridGantt.svelte +2890 -0
- package/dist/gantt/SvGridGantt.svelte.d.ts +30 -0
- package/dist/gantt/gantt-config.d.ts +73 -0
- package/dist/gantt/gantt-config.js +1 -0
- package/dist/gantt/gantt-critical-path.d.ts +67 -0
- package/dist/gantt/gantt-critical-path.js +207 -0
- package/dist/gantt/gantt-model.d.ts +257 -0
- package/dist/gantt/gantt-model.js +651 -0
- package/dist/gantt/gantt-resources.d.ts +59 -0
- package/dist/gantt/gantt-resources.js +71 -0
- package/dist/gantt/gantt.d.ts +7 -0
- package/dist/gantt/gantt.js +36 -0
- package/dist/gantt/index.d.ts +15 -0
- package/dist/gantt/index.js +22 -0
- package/dist/gantt/timeline-arrows.d.ts +102 -0
- package/dist/gantt/timeline-arrows.js +181 -0
- package/dist/import.d.ts +2 -2
- package/dist/import.js +1 -1
- package/dist/index.d.ts +34 -7
- package/dist/index.js +44 -7
- package/dist/install.js +8 -1
- package/dist/license-core.d.ts +17 -1
- package/dist/license-core.js +16 -5
- package/dist/license.d.ts +18 -2
- package/dist/license.js +71 -6
- package/dist/node/studio.js +140 -8
- package/dist/pivot-enable.js +2 -2
- package/dist/scheduler-dependencies.d.ts +35 -0
- package/dist/scheduler-dependencies.js +36 -1
- package/dist/scheduler.js +2 -2
- package/dist/selection-bar.js +2 -2
- package/dist/server/SvGroupCell.svelte +213 -0
- package/dist/server/SvGroupCell.svelte.d.ts +62 -0
- package/dist/server/SvRowGroupPanel.svelte +322 -0
- package/dist/server/SvRowGroupPanel.svelte.d.ts +37 -0
- package/dist/server/enable.d.ts +17 -0
- package/dist/server/enable.js +49 -0
- package/dist/server/index.d.ts +22 -0
- package/dist/server/index.js +24 -0
- package/dist/server/messages.d.ts +45 -0
- package/dist/server/messages.js +35 -0
- package/dist/server/server-group-model.d.ts +104 -0
- package/dist/server/server-group-model.js +272 -0
- package/dist/server/server-pivot.d.ts +46 -0
- package/dist/server/server-pivot.js +57 -0
- package/dist/server/server-row-model.d.ts +547 -0
- package/dist/server/server-row-model.js +1551 -0
- package/dist/server/server-selection.d.ts +99 -0
- package/dist/server/server-selection.js +215 -0
- package/dist/server/svgrid-adapter.d.ts +155 -0
- package/dist/server/svgrid-adapter.js +202 -0
- package/dist/sheet/ast.d.ts +45 -1
- package/dist/sheet/ast.js +14 -0
- package/dist/sheet/auto-filter.d.ts +58 -4
- package/dist/sheet/auto-filter.js +159 -11
- package/dist/sheet/autocomplete.d.ts +9 -0
- package/dist/sheet/autocomplete.js +90 -4
- package/dist/sheet/cell-images.d.ts +39 -0
- package/dist/sheet/cell-images.js +90 -0
- package/dist/sheet/cell-types.d.ts +58 -0
- package/dist/sheet/cell-types.js +96 -0
- package/dist/sheet/coerce.d.ts +40 -0
- package/dist/sheet/coerce.js +170 -3
- package/dist/sheet/comments.d.ts +43 -9
- package/dist/sheet/comments.js +85 -27
- package/dist/sheet/conditional-formats.d.ts +29 -6
- package/dist/sheet/conditional-formats.js +37 -12
- package/dist/sheet/csv.d.ts +32 -0
- package/dist/sheet/csv.js +158 -0
- package/dist/sheet/culture.d.ts +87 -0
- package/dist/sheet/culture.js +247 -0
- package/dist/sheet/delta.d.ts +102 -0
- package/dist/sheet/delta.js +203 -0
- package/dist/sheet/deps.d.ts +45 -1
- package/dist/sheet/deps.js +77 -2
- package/dist/sheet/document.d.ts +106 -4
- package/dist/sheet/document.js +204 -3
- package/dist/sheet/engine.d.ts +67 -0
- package/dist/sheet/engine.js +19 -0
- package/dist/sheet/entry.d.ts +8 -4
- package/dist/sheet/entry.js +171 -21
- package/dist/sheet/error-check.d.ts +49 -0
- package/dist/sheet/error-check.js +83 -0
- package/dist/sheet/evaluate-steps.d.ts +48 -0
- package/dist/sheet/evaluate-steps.js +196 -0
- package/dist/sheet/evaluate.d.ts +67 -0
- package/dist/sheet/evaluate.js +725 -13
- package/dist/sheet/filter-files.d.ts +37 -0
- package/dist/sheet/filter-files.js +465 -0
- package/dist/sheet/format-store.d.ts +15 -0
- package/dist/sheet/format-store.js +11 -1
- package/dist/sheet/from-rows.d.ts +14 -0
- package/dist/sheet/from-rows.js +50 -0
- package/dist/sheet/function-catalog.d.ts +1 -1
- package/dist/sheet/function-catalog.js +296 -5
- package/dist/sheet/functions.js +363 -91
- package/dist/sheet/index.d.ts +35 -8
- package/dist/sheet/index.js +35 -8
- package/dist/sheet/links.d.ts +57 -0
- package/dist/sheet/links.js +174 -0
- package/dist/sheet/messages.d.ts +778 -0
- package/dist/sheet/messages.js +806 -0
- package/dist/sheet/number-format.d.ts +52 -5
- package/dist/sheet/number-format.js +465 -68
- package/dist/sheet/objects.d.ts +106 -0
- package/dist/sheet/objects.js +149 -0
- package/dist/sheet/ods-document.d.ts +44 -0
- package/dist/sheet/ods-document.js +1275 -0
- package/dist/sheet/open-file.d.ts +26 -0
- package/dist/sheet/open-file.js +56 -0
- package/dist/sheet/outline.d.ts +82 -0
- package/dist/sheet/outline.js +228 -0
- package/dist/sheet/packs/array.d.ts +16 -0
- package/dist/sheet/packs/array.js +172 -0
- package/dist/sheet/packs/database.d.ts +42 -0
- package/dist/sheet/packs/database.js +142 -0
- package/dist/sheet/packs/engineering.d.ts +2 -0
- package/dist/sheet/packs/engineering.js +296 -0
- package/dist/sheet/packs/financial.d.ts +5 -0
- package/dist/sheet/packs/financial.js +220 -0
- package/dist/sheet/packs/math-stats.d.ts +2 -0
- package/dist/sheet/packs/math-stats.js +372 -0
- package/dist/sheet/packs/special.d.ts +44 -0
- package/dist/sheet/packs/special.js +187 -0
- package/dist/sheet/packs/stats-dist.d.ts +2 -0
- package/dist/sheet/packs/stats-dist.js +785 -0
- package/dist/sheet/packs/text-date.d.ts +2 -0
- package/dist/sheet/packs/text-date.js +332 -0
- package/dist/sheet/packs/trig.d.ts +2 -0
- package/dist/sheet/packs/trig.js +73 -0
- package/dist/sheet/page-setup.d.ts +73 -0
- package/dist/sheet/page-setup.js +73 -0
- package/dist/sheet/parse.js +42 -10
- package/dist/sheet/paste-special.d.ts +22 -0
- package/dist/sheet/paste-special.js +14 -5
- package/dist/sheet/pivot-range.d.ts +160 -0
- package/dist/sheet/pivot-range.js +333 -0
- package/dist/sheet/presence.d.ts +73 -0
- package/dist/sheet/presence.js +79 -0
- package/dist/sheet/print.d.ts +104 -0
- package/dist/sheet/print.js +169 -0
- package/dist/sheet/protection.d.ts +55 -1
- package/dist/sheet/protection.js +55 -2
- package/dist/sheet/rects.d.ts +1 -1
- package/dist/sheet/refs.d.ts +48 -0
- package/dist/sheet/refs.js +155 -8
- package/dist/sheet/ribbon-icons.d.ts +1 -1
- package/dist/sheet/ribbon-icons.js +186 -0
- package/dist/sheet/ribbon.d.ts +1 -1
- package/dist/sheet/ribbon.js +324 -23
- package/dist/sheet/sheet-kinds.d.ts +74 -0
- package/dist/sheet/sheet-kinds.js +91 -0
- package/dist/sheet/shortcuts.d.ts +13 -3
- package/dist/sheet/shortcuts.js +63 -12
- package/dist/sheet/sort.d.ts +51 -0
- package/dist/sheet/sort.js +107 -0
- package/dist/sheet/sparklines.d.ts +111 -0
- package/dist/sheet/sparklines.js +204 -0
- package/dist/sheet/table-styles.d.ts +74 -0
- package/dist/sheet/table-styles.js +152 -0
- package/dist/sheet/tables.d.ts +19 -0
- package/dist/sheet/tables.js +31 -0
- package/dist/sheet/tokenize.d.ts +18 -1
- package/dist/sheet/tokenize.js +60 -9
- package/dist/sheet/validation.d.ts +16 -0
- package/dist/sheet/validation.js +37 -0
- package/dist/sheet/workbook.d.ts +148 -3
- package/dist/sheet/workbook.js +742 -31
- package/dist/sheet/xls-cfb.d.ts +32 -0
- package/dist/sheet/xls-cfb.js +290 -0
- package/dist/sheet/xls-document.d.ts +10 -0
- package/dist/sheet/xls-document.js +1243 -0
- package/dist/sheet/xls-formula.d.ts +40 -0
- package/dist/sheet/xls-formula.js +569 -0
- package/dist/sheet/xlsx-document.d.ts +108 -0
- package/dist/sheet/xlsx-document.js +1849 -0
- package/dist/sheet/xlsx-drawing.d.ts +99 -0
- package/dist/sheet/xlsx-drawing.js +420 -0
- package/dist/sheet-enable.js +6 -7
- package/dist/sheet-text.d.ts +7 -0
- package/dist/sheet-text.js +29 -0
- package/dist/sources/auth-supabase.js +2 -2
- package/dist/sources/realtime-supabase.js +2 -2
- package/dist/sources/rest.d.ts +6 -2
- package/dist/sources/rest.js +42 -7
- package/dist/sources/supabase.d.ts +10 -1
- package/dist/sources/supabase.js +73 -40
- package/dist/studio/emit-project.js +14 -1
- package/dist/studio/index.js +1 -1
- package/dist/studio/project.d.ts +11 -2
- package/dist/studio/project.js +5 -2
- package/dist/studio/ui-components.generated.js +107 -2
- package/dist/sveltekit/in-memory.d.ts +2 -2
- package/dist/sveltekit/in-memory.js +125 -4
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/index.js +1 -1
- package/dist/sveltekit/query-plan.d.ts +50 -4
- package/dist/sveltekit/query-plan.js +25 -3
- package/dist/sveltekit/sql-source.d.ts +6 -0
- package/dist/sveltekit/sql-source.js +34 -6
- package/dist/sveltekit/sql.d.ts +31 -0
- package/dist/sveltekit/sql.js +135 -25
- package/dist/sveltekit/transport.d.ts +10 -2
- package/dist/sveltekit/transport.js +30 -6
- package/dist/upgrade-prompt.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/watermark.js +35 -2
- package/dist/wc/GridMenus-By9vPuq0.js +1088 -0
- package/dist/wc/SvChartMenu-CQNrQGT2.js +308 -0
- package/dist/wc/SvDateRangeInput-DR6m7bSM.js +199 -0
- package/dist/wc/SvDateTimePicker-D-vRnaMo.js +771 -0
- package/dist/wc/SvField-B_kurx_P.js +149 -0
- package/dist/wc/SvGrid.helpers-BdVDtjUm.js +135 -0
- package/dist/wc/SvGridCellEditor-Bnf1Kr5C.js +560 -0
- package/dist/wc/SvGridChart-BqUJh2pG.js +3572 -0
- package/dist/wc/SvGridChartBuilder-CGycC5cn.js +988 -0
- package/dist/wc/SvGridChartPanel-CRnizsHD.js +1746 -0
- package/dist/wc/SvGridChartView-D1pa_xMy.js +47 -0
- package/dist/wc/SvGridDropdown-45WsObLL.js +259 -0
- package/dist/wc/SvModal-DrfOYYyX.js +189 -0
- package/dist/wc/cell-formatting-CBfgahv1.js +86 -0
- package/dist/wc/chart-6OXlybVT.js +4341 -0
- package/dist/wc/chart-export-CeSADGfi.js +135 -0
- package/dist/wc/chart-export-pdf-BGko7MER.js +104 -0
- package/dist/wc/chart-summary-2m3ia1-n.js +137 -0
- package/dist/wc/chart-trend-CQOQtTMl.js +50 -0
- package/dist/wc/chart-validate-CD6ptiwg.js +180 -0
- package/dist/wc/client-DQIzjDBG.js +3064 -0
- package/dist/wc/column-resize-DUPgynvc.js +101 -0
- package/dist/wc/date-format-DS-z_h9v.js +1130 -0
- package/dist/wc/dismissable-CQ6XFXsk.js +45 -0
- package/dist/wc/editor-contract-peBDxir_.js +22 -0
- package/dist/wc/editor-registry-DxGKqcvx.js +24 -0
- package/dist/wc/focus-trap-Dbb2tJxb.js +65 -0
- package/dist/wc/list-option-DrRyivWp.js +82 -0
- package/dist/wc/popover-WsGlvtQv.js +69 -0
- package/dist/wc/positioning-BiKLfu1E.js +112 -0
- package/dist/wc/react.d.ts +41 -0
- package/dist/wc/react.js +90 -0
- package/dist/wc/rolldown-runtime-Dy4uBu1J.js +11 -0
- package/dist/wc/row-drag-touch-CycRR65n.js +93 -0
- package/dist/wc/row-resize-limG_N1z.js +111 -0
- package/dist/wc/sv-sheet-element.d.ts +97 -0
- package/dist/wc/sv-sheet-element.js +44213 -0
- package/dist/wc/svelte-virtualizer.svelte-CXtQy2TQ.js +530 -0
- package/dist/wc/validate-BqDthkNr.js +88 -0
- package/dist/wc/vue.d.ts +31 -0
- package/dist/wc/vue.js +75 -0
- package/package.json +30 -5
- package/dist/designer/assets/GridMenus-BlsvL_aU.js +0 -8
- package/dist/designer/assets/SvGridCellEditor-C7uQXYds.js +0 -4
- package/dist/designer/assets/SvGridChartBuilder-kvyIU_Rl.js +0 -1
- package/dist/designer/assets/SvGridDropdown-Bb3HH2Aj.js +0 -1
- package/dist/designer/assets/cell-formatting-wr5wm7US.js +0 -1
- package/dist/designer/assets/index-CMFgAoVV.css +0 -1
- package/dist/designer/assets/row-resize-DLktcqUK.js +0 -1
- package/dist/designer/assets/src-EdI3Xqds.js +0 -58
- 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
|
-
|
|
144
|
-
|
|
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';
|
package/dist/sveltekit/index.js
CHANGED
|
@@ -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
|
-
/**
|
|
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:
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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);
|
package/dist/sveltekit/sql.d.ts
CHANGED
|
@@ -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;
|
package/dist/sveltekit/sql.js
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
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
|
|
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
|
|
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) {
|