@toclocoinc/lattice-grid 1.16.0 → 1.17.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.
@@ -437,7 +437,7 @@
437
437
  <div class="shell">
438
438
  <aside class="rail">
439
439
  <p class="rail__brand">Lattice Grid</p>
440
- <p class="rail__sub">Developer guide · v1.16.0</p>
440
+ <p class="rail__sub">Developer guide · v1.17.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
@@ -2554,9 +2554,9 @@ grid.destroy();
2554
2554
  </p>
2555
2555
 
2556
2556
  <div class="example">
2557
- <p class="example__label">On by default; turn it off or change the cap</p>
2558
- <pre><code>createGrid(element, { stickyGroupHeaders: <span class="kw">false</span> }); <span class="cmt">// off</span>
2559
- createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack up to three</span></code></pre>
2557
+ <p class="example__label">Off by default; opt in or set the cap</p>
2558
+ <pre><code>createGrid(element, { stickyGroupHeaders: <span class="kw">true</span> }); <span class="cmt">// on, up to two</span>
2559
+ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// on, up to three</span></code></pre>
2560
2560
  </div>
2561
2561
 
2562
2562
  <div class="why">
@@ -2608,7 +2608,8 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
2608
2608
  <table>
2609
2609
  <thead><tr><th>Option</th><th>Type</th><th>Description</th></tr></thead>
2610
2610
  <tbody>
2611
- <tr><td class="name">retainSource</td><td class="type">boolean</td><td class="desc">Default <code>true</code>. Set <code>false</code> to keep only the packed columns and reconstruct a plain row object from them on demand. <code>rows.data()</code> then returns freshly reconstructed objects — a new object each call — so <code>row === sourceObject</code> and a custom renderer reading <code>row.sourceObject</code> no longer hold, and equality becomes value-based. Cell values are identical either way, so <code>get()</code>, <code>byKey()</code>, <code>value()</code> and <code>values()</code> are unaffected.</td></tr>
2611
+ <tr><td class="name">retainSource</td><td class="type">boolean</td><td class="desc">Default <code>true</code>. Set <code>false</code> to keep only the packed columns and reconstruct a plain row object from them on demand. <code>rows.data()</code> then returns freshly reconstructed objects — a new object each call — so <code>row === sourceObject</code> and a custom renderer reading <code>row.sourceObject</code> no longer hold, and equality becomes value-based. Cell values are identical either way, so <code>get()</code>, <code>byKey()</code>, <code>value()</code> and <code>values()</code> are unaffected. On its own this drops only the store's reference array, not the objects: the source layer still holds them.</td></tr>
2612
+ <tr><td class="name">dropSourceRows</td><td class="type">boolean</td><td class="desc">Default <code>false</code>. Set <code>true</code> to release the caller's row objects from the <em>source layer</em> and the grid config once the store is built, so the packed columns are the only resident copy. This is where the large reduction lives — roughly an order of magnitude at a million rows — because the caller's objects are the dominant term. Implies <code>retainSource: false</code> (the store must reconstruct), so it carries the same identity tradeoff. An impure computed column — a shadow or a rank/positional column — is never stored, so it cannot be served from the columns: it warns once and drops out rather than answering wrongly, and <code>rows.move()</code> is refused. Do not enable it on a grid that sorts, filters, groups or totals on such a column, or that reorders rows in place.</td></tr>
2612
2613
  </tbody>
2613
2614
  </table>
2614
2615
  </div>
@@ -2618,14 +2619,17 @@ createGrid(element, { stickyGroupHeaders: 3 }); <span class="cmt">// stack
2618
2619
  columns,
2619
2620
  rowKey: 'id',
2620
2621
  rows,
2621
- ingest: { retainSource: <span class="kw">false</span> },
2622
+ <span class="cmt">// Release the caller's objects entirely; the packed columns are the sole copy.</span>
2623
+ ingest: { dropSourceRows: <span class="kw">true</span> },
2622
2624
  });</code></pre>
2623
2625
  </div>
2624
2626
  <div class="why">
2625
- <p>The saving is the store's own copy of the object references, not the objects
2626
- themselves: whoever handed the grid its rows still owns them. The footprint drops
2627
- materially only when the grid becomes the sole holder of the data. Reach for this when
2628
- you can let go of the source array and do not depend on caller identity through
2627
+ <p>With <code>retainSource: false</code> alone the saving is only the store's own copy of the
2628
+ object references, not the objects themselves: the source layer and the grid config still
2629
+ hold the array, so whoever handed the grid its rows keeps them alive. <code>dropSourceRows</code>
2630
+ releases those references too, so once the caller lets go the objects can be collected and the
2631
+ grid becomes the sole holder of the data. Reach for it on a large, read-mostly grid where you
2632
+ can let go of the source array and do not depend on caller identity through
2629
2633
  <code>rows.data()</code>.</p>
2630
2634
  </div>
2631
2635
 
@@ -2746,7 +2750,8 @@ createGrid(el, { columns, rowKey: 'id', source });
2746
2750
  plan.pushed; <span class="cmt">// the query the adapter was given</span>
2747
2751
  plan.residual; <span class="cmt">// { filters, sort, quick } the grid applied after</span>
2748
2752
  plan.unpushed; <span class="cmt">// ['filter'], the parts that stayed behind</span>
2749
- plan.needsAll; <span class="cmt">// true when the whole result had to be fetched</span></code></pre>
2753
+ plan.needsAll; <span class="cmt">// true when the whole result had to be fetched</span>
2754
+ plan.full; <span class="cmt">// true when fullDataset forced it, not just residual work</span></code></pre>
2750
2755
  </div>
2751
2756
  <p>
2752
2757
  The grid also warns once, naming the predicate that could not be pushed, because the fix is
@@ -2788,6 +2793,78 @@ createGrid(el, {
2788
2793
  with no server involved.</p>
2789
2794
  </div>
2790
2795
 
2796
+ <h3 id="fulldataset">Whole-dataset statistics over a remote source</h3>
2797
+ <p class="lead-in">
2798
+ A windowed source reduces a total or statistic over the rows it has loaded, not the whole
2799
+ matching set — a footer median of the 200 rows on screen, which is wrong and looks right.
2800
+ <code>fullDataset</code> makes the source hold the entire matching set client-side so those
2801
+ figures are computed over everything.
2802
+ </p>
2803
+ <div class="example">
2804
+ <p class="example__label">Correct footer figures over a REST or DuckDB source</p>
2805
+ <pre><code><span class="kw">const</span> source = createPushdownSource({
2806
+ adapter: restAdapter({ url: '/api/trades' }),
2807
+ fullDataset: {
2808
+ enabled: <span class="kw">true</span>, <span class="cmt">// hold the whole matching set, once per query</span>
2809
+ maxRows: 1_000_000, <span class="cmt">// refuse (visible error) past this</span>
2810
+ maxBytesEstimate: 512 * 1024 * 1024,
2811
+ },
2812
+ });</code></pre>
2813
+ </div>
2814
+ <div class="why">
2815
+ <p>It is <strong>off by default</strong> and reuses the same whole-result path that residual
2816
+ work already takes: the flag ORs into <code>needsAll</code>, so once the set is held it is
2817
+ ordinary in-memory data and the grid's existing total and statistics kernels reduce over all of
2818
+ it, with no per-stat change. When it is active and within the limits, the whole matching set is
2819
+ covered, so the windowed-statistic warning (BACKLOG-0000731) stays silent — the figure is now
2820
+ honestly whole-dataset.</p>
2821
+ <p><strong>It is refused loudly, never truncated.</strong> A matching set past
2822
+ <code>maxRows</code> or <code>maxBytesEstimate</code> is thrown and surfaced as a
2823
+ <code>source:error</code> with no rows shown, rather than held as a fraction and presented as
2824
+ the whole. A fraction shown as the whole is exactly the silent wrong answer this feature
2825
+ exists to remove, so it is never how the feature fails. For a <code>restAdapter</code>, which
2826
+ cannot compute, this is the only route to a correct whole-dataset statistic.</p>
2827
+ </div>
2828
+
2829
+ <h3 id="aggregates">Pushing statistics down to the engine</h3>
2830
+ <p class="lead-in">
2831
+ A DuckDB-class engine computes a median or a standard deviation over the whole matching set far
2832
+ faster than pulling every row here to do it. The <code>aggregates</code> config decides, at
2833
+ grid setup, which statistics the engine computes and which the grid does — a design-time
2834
+ developer choice, fixed for the life of the grid, never a runtime toggle and never shown to an
2835
+ end user.
2836
+ </p>
2837
+ <div class="example">
2838
+ <p class="example__label">Push the verified-identical stats, keep the rest exact</p>
2839
+ <pre><code><span class="kw">const</span> source = createPushdownSource({
2840
+ adapter: duckdbAdapter({ connection: conn, from: <span class="str">'trades'</span> }),
2841
+ aggregates: {
2842
+ <span class="cmt">// 'engine' pushes everything expressible; 'engine-if-identical' pushes only</span>
2843
+ <span class="cmt">// the stats whose engine result is verified identical to the grid kernel;</span>
2844
+ <span class="cmt">// 'client' (the default when absent) computes everything here.</span>
2845
+ <span class="kw">default</span>: <span class="str">'engine-if-identical'</span>,
2846
+ overrides: { mode: <span class="str">'client'</span> }, <span class="cmt">// I want the grid's null-when-distinct mode</span>
2847
+ },
2848
+ });</code></pre>
2849
+ </div>
2850
+ <div class="why">
2851
+ <p>Every statistic is classified <strong>IDENTICAL</strong> (the engine's result equals the
2852
+ grid's own kernel, verified against it on the same data) or <strong>MAY-DIFFER</strong> (the
2853
+ engine computes it by a method that can differ from the grid's definition — <code>mode</code>
2854
+ returns a value where the grid returns null). The classification drives the docs and
2855
+ build-time provenance, <em>not</em> whether a stat is pushed: that is your choice.
2856
+ <code>weightedQuantile</code> is the one genuine fallback, always client-side, because the
2857
+ engine cannot express the grid's midpoint convention. The published table of every stat, its
2858
+ class and its SQL is generated from one map (<code>STAT_PUSHDOWN</code>) so it cannot drift.</p>
2859
+ <p><strong>No mixed provenance.</strong> An engine figure and a client figure never appear in
2860
+ one result set. Aggregates are pushed only when the filter is <em>fully</em> pushed; a residual
2861
+ filter the engine could not apply forces every aggregate client-side, because an engine number
2862
+ computed over a superset beside a client number over the real set would be wrong-but-plausible.
2863
+ <code>source.lastPlan().aggregates</code> reports, per stat, whether the engine or the grid
2864
+ computed it and the class it was assigned — inspection during the build, not a per-figure
2865
+ runtime marker.</p>
2866
+ </div>
2867
+
2791
2868
  <h2 id="derived">Grids built from other grids</h2>
2792
2869
  <p class="lead-in">
2793
2870
  A derived grid takes its rows from another grid rather than from a load: grouped and
@@ -2982,6 +3059,27 @@ columns: [
2982
3059
  scrolling past a rounded corner is cut by it rather than squaring it off.
2983
3060
  </p>
2984
3061
 
3062
+ <div class="example">
3063
+ <p class="example__label">Zebra striping (opt-in)</p>
3064
+ <pre><code>createGrid(element, {
3065
+ columns, rows,
3066
+ stripedRows: <span class="kw">true</span>, <span class="cmt">// shade alternate data rows; off by default</span>
3067
+ });</code></pre>
3068
+ </div>
3069
+
3070
+ <p class="lead-in">
3071
+ <code>stripedRows</code> shades every other data row. It is strictly opt-in and off by default,
3072
+ so a grid that never mentions it looks exactly as it did on upgrade. Parity is decided by each
3073
+ row's <em>logical</em> index rather than its position in the DOM: rows are virtualised and
3074
+ recycled, so a <code>:nth-child</code> rule would repaint the stripe onto whichever row landed
3075
+ in an odd slot after a scroll, and a logical-index stripe keeps a row shaded consistently across
3076
+ a scroll and across the left-pinned, centre and right-pinned segments of the same row. Group
3077
+ headings, group footers and the grand total are structure rather than data, so they are never
3078
+ striped. The stripe uses the theme's <code>--lattice-surface-alt</code> token, which every
3079
+ palette defines, so dark, high-contrast and terminal are correct without any extra rule, and
3080
+ both selection and hover still win over it.
3081
+ </p>
3082
+
2985
3083
  <h2 id="cards">Cards, lists and feeds</h2>
2986
3084
  <p class="lead-in">
2987
3085
  <code>rowTemplate</code> draws each row with a layout of your own instead of dividing it into
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.16.0, type declarations
2
+ * Lattice Grid 1.17.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -779,6 +779,41 @@ export interface IngestConfig {
779
779
  */
780
780
  retainSource?: boolean;
781
781
 
782
+ /**
783
+ * Release the caller's row objects from the *source layer* once the column
784
+ * store has been built, so the columns become the sole resident copy of the
785
+ * data. Default `false`, which keeps today's behaviour.
786
+ *
787
+ * `retainSource:false` stops the {@link https://en.wikipedia.org/wiki/Column-oriented_DBMS column store}
788
+ * from holding the caller's objects, but the memory source and the grid config
789
+ * still retain the supplied array by reference — so the objects stay alive and
790
+ * the resident footprint does not actually fall. This flag closes that gap: it
791
+ * clears `MemorySource`'s retained array and drops the array from the grid
792
+ * config, leaving nothing on the heap but the packed columns. That is where
793
+ * the large reduction comes from (roughly an order of magnitude at a million
794
+ * rows), not from `retainSource` on its own.
795
+ *
796
+ * Implies `retainSource:false`: dropping the caller's objects while the store
797
+ * still expects to read through them would leave the source with no data at
798
+ * all, so setting this on forces the store to reconstruct rows from columns.
799
+ * Every read is therefore served from the columns — `at()`, `byKey()`,
800
+ * `get()`, `value()`, `values()`, filtering, sorting, grouping, totals and
801
+ * export are all unaffected in their values. What changes is the same three
802
+ * identity behaviours `retainSource:false` documents: `rows.data()` returns
803
+ * freshly reconstructed objects (so `row === sourceObject` no longer holds), a
804
+ * custom renderer reaching for `row.sourceObject` gets a reconstruction, and
805
+ * equality against a row becomes value-based.
806
+ *
807
+ * One consumer cannot be served from the columns: an *impure computed column*
808
+ * (a shadow, or a rank/positional column) is deliberately never materialised
809
+ * into the store, so its handle is built by reading the source objects. Under
810
+ * `dropSourceRows` those objects are gone, so such a column reduces over
811
+ * nothing and warns once rather than returning a silently wrong figure. Do not
812
+ * enable `dropSourceRows` on a grid that sorts, filters, groups or totals on a
813
+ * shadow or a positional column.
814
+ */
815
+ dropSourceRows?: boolean;
816
+
782
817
  /**
783
818
  * Columnize `stream`-source ingest on a Worker so a large load does not block
784
819
  * the main thread. Default `false`. When on, an arriving chunk that clears
@@ -1158,6 +1193,21 @@ export interface GridConfig {
1158
1193
  */
1159
1194
  cornerRadius?: boolean | number | string;
1160
1195
 
1196
+ /**
1197
+ * Shade alternate data rows (zebra striping).
1198
+ *
1199
+ * Off by default, and strictly opt-in: an existing grid must look exactly the
1200
+ * same on upgrade. When `true`, every other data row takes the theme's
1201
+ * `--lattice-surface-alt` background, which every palette already defines, so
1202
+ * dark, high-contrast and terminal stripe correctly without extra work.
1203
+ *
1204
+ * Parity follows the row's *logical* index, not its position in the DOM, so a
1205
+ * row keeps its stripe across a scroll even though the rows are recycled.
1206
+ * Structural rows — group headings, group footers and the grand total — are
1207
+ * never striped, and both selection and hover still win over the stripe.
1208
+ */
1209
+ stripedRows?: boolean;
1210
+
1161
1211
  /**
1162
1212
  * Show a bar above the column headings for filtering columns by tag.
1163
1213
  *
@@ -1312,7 +1362,12 @@ export interface GridConfig {
1312
1362
  * only the sort, filter and menu controls inside them.
1313
1363
  */
1314
1364
  showHeader?: boolean;
1315
- /** Header height in pixels. */
1365
+ /**
1366
+ * Header height in pixels. Omitted, the header takes its height from the
1367
+ * density-scaled `--lattice-header-height` token, so `density` sizes the
1368
+ * header as it sizes the rows. A number names one explicitly and outranks the
1369
+ * token.
1370
+ */
1316
1371
  headerHeight?: number;
1317
1372
  /** How many rows to render beyond the viewport. More costs memory and
1318
1373
  * smooths fast scrolling; fewer is lighter and can show a gap. */
@@ -1562,9 +1617,11 @@ export interface GridConfig {
1562
1617
  * Keep the enclosing group headings pinned above the viewport while
1563
1618
  * scrolling inside a group.
1564
1619
  *
1565
- * On by default, stacking at most two. `false` turns it off; a number, or
1566
- * `{ depth }`, sets how many may stack: each costs a row of viewport, so a
1567
- * deep grouping would otherwise spend the screen describing itself.
1620
+ * Off by default a deliberate product default; sticky group headers are
1621
+ * opt-in. `true` turns it on, stacking at most two; a number, or `{ depth }`,
1622
+ * sets how many may stack: each costs a row of viewport, so a deep grouping
1623
+ * would otherwise spend the screen describing itself. `false` is off, the
1624
+ * same as leaving it unset.
1568
1625
  */
1569
1626
  stickyGroupHeaders?: boolean | number | { depth?: number };
1570
1627
  /**
@@ -1892,6 +1949,103 @@ export interface PushdownPlan {
1892
1949
  needsAll: boolean;
1893
1950
  /** Which parts could not be pushed: `filter`, `sort`, `quick`. */
1894
1951
  unpushed: string[];
1952
+ /**
1953
+ * Whether the whole result was fetched because `fullDataset` is on, rather
1954
+ * than only because residual work forced it. When true, totals and statistics
1955
+ * reduce over the whole matching set and the windowed-stat warning is silent.
1956
+ */
1957
+ full: boolean;
1958
+ /**
1959
+ * Per-aggregate provenance, present only when the last request computed
1960
+ * aggregates (BACKLOG-0000730 Part B): which statistics the engine computed
1961
+ * and which the client did, with the class the pushdown map assigned each.
1962
+ * Under grouping it also carries the `groupBy` the subtotals were computed
1963
+ * over. Build-time inspection, not a runtime per-figure marker.
1964
+ */
1965
+ aggregates?: {
1966
+ engine: AggregateProvenance[];
1967
+ client: AggregateProvenance[];
1968
+ groupBy?: string[];
1969
+ };
1970
+ }
1971
+
1972
+ /**
1973
+ * Opt-in, sticky full-dataset pull for a pushdown/remote source
1974
+ * (BACKLOG-0000730). Off by default. When enabled, the source materialises the
1975
+ * entire matching set client-side once per query signature and serves every
1976
+ * window, total and statistic from it, so those figures are computed over the
1977
+ * whole set rather than the loaded window. A set past either limit is refused
1978
+ * with a visible `source:error` — never silently truncated.
1979
+ */
1980
+ export interface PushdownFullDatasetConfig {
1981
+ /** Sticky: hold the whole matching set client-side. Default `false`. */
1982
+ enabled?: boolean;
1983
+ /** Refuse (visible error) past this many rows. Default `1_000_000`. */
1984
+ maxRows?: number;
1985
+ /** Refuse past this estimated heap cost, in bytes. Default `512 * 1024 * 1024`. */
1986
+ maxBytesEstimate?: number;
1987
+ }
1988
+
1989
+ /** How one requested aggregate should be computed. */
1990
+ export type AggregateMode = 'engine' | 'client' | 'engine-if-identical';
1991
+
1992
+ /**
1993
+ * Design-time aggregate-pushdown policy for a pushdown source
1994
+ * (BACKLOG-0000730 Part B, ungrouped). The developer chooses, at grid setup
1995
+ * before render, whether each statistic is computed by the engine (fast, over
1996
+ * the matching set) or client-side (the grid's exact definition, needs a
1997
+ * full-dataset pull). It is fixed for the life of the grid, never a runtime
1998
+ * toggle, and never surfaced to an end user.
1999
+ *
2000
+ * Absent, every aggregate is computed client-side — today's behaviour, so no
2001
+ * existing caller regresses. `engine-if-identical` is the recommended setting
2002
+ * for a windowed DuckDB source: it pushes only the statistics whose engine
2003
+ * result is verified identical to the grid kernel, keeping the documented
2004
+ * MAY-DIFFER stats (e.g. `mode`) client-side. The engine is used only when the
2005
+ * filter is fully pushed; a residual filter forces every aggregate client-side,
2006
+ * so an engine figure and a client figure never mix in one result set.
2007
+ */
2008
+ export interface PushdownAggregatesConfig {
2009
+ /**
2010
+ * The default policy for stats the engine can express. `'engine'` pushes
2011
+ * everything expressible (using the engine's method for MAY-DIFFER stats);
2012
+ * `'engine-if-identical'` pushes only the verified-identical ones; `'client'`
2013
+ * computes everything client-side. Default `'client'`.
2014
+ */
2015
+ default?: AggregateMode;
2016
+ /** Per-stat overrides, winning over `default`. A stat the engine cannot
2017
+ * express (`weightedQuantile`) is always client-side regardless. */
2018
+ overrides?: Record<string, 'engine' | 'client'>;
2019
+ }
2020
+
2021
+ /**
2022
+ * One aggregate the grid asks the source to compute over the matching set.
2023
+ * `params` carries e.g. `{ share: 0.1 }` so an adapter emits the matching SQL;
2024
+ * `weight` names the second column for a two-column stat like `correlation`.
2025
+ */
2026
+ export interface AggregateRequest {
2027
+ /** Keys the result back to the request. */
2028
+ id: string;
2029
+ /** The column to reduce. */
2030
+ col: string;
2031
+ /** The statistic name, as used in `total: '<name>'`. */
2032
+ fn: string;
2033
+ /** The second column, for a two-column statistic. */
2034
+ weight?: string;
2035
+ /** Parameters the statistic takes, e.g. a trim share. */
2036
+ params?: Record<string, unknown>;
2037
+ }
2038
+
2039
+ /** How one aggregate was routed, for `lastPlan()` provenance. */
2040
+ export interface AggregateProvenance {
2041
+ id: string;
2042
+ col: string;
2043
+ fn: string;
2044
+ /** How the engine result relates to the grid kernel. */
2045
+ class: 'identical' | 'may-differ' | 'fallback';
2046
+ /** Why it is client-side, when it is (config, fallback, or the guard). */
2047
+ reason?: string;
2048
+ weight?: string;
1895
2049
  }
1896
2050
 
1897
2051
  export interface PushdownSourceConfig {
@@ -1899,6 +2053,16 @@ export interface PushdownSourceConfig {
1899
2053
  /** The compute barrel, for applying whatever the engine could not. */
1900
2054
  compute?: object;
1901
2055
  pageSize?: number;
2056
+ /**
2057
+ * Opt-in full-dataset pull. Off unless `fullDataset.enabled` is set. See
2058
+ * {@link PushdownFullDatasetConfig}.
2059
+ */
2060
+ fullDataset?: PushdownFullDatasetConfig;
2061
+ /**
2062
+ * Design-time aggregate-pushdown policy. Absent = client-side (today's
2063
+ * behaviour). See {@link PushdownAggregatesConfig}.
2064
+ */
2065
+ aggregates?: PushdownAggregatesConfig;
1902
2066
  }
1903
2067
 
1904
2068
  export interface StatisticsApi {
@@ -3414,7 +3578,51 @@ export function toneOf(direction: string, goodWhen: string): 'good' | 'bad' | 'f
3414
3578
  */
3415
3579
  export function createPushdownSource(
3416
3580
  config: PushdownSourceConfig,
3417
- ): SourceConfig & { lastPlan(): PushdownPlan | null };
3581
+ ): SourceConfig & {
3582
+ lastPlan(): PushdownPlan | null;
3583
+ /**
3584
+ * Compute a set of aggregates over the matching set, splitting them between
3585
+ * the engine and the client by the design-time `aggregates` config
3586
+ * (BACKLOG-0000730 Part B). Ungrouped, returns the engine-computed `values`
3587
+ * keyed by id. When the request carries a `groupBy`, returns `groups` instead:
3588
+ * one entry per subtotal level and the grand total (`level: 0`, produced by a
3589
+ * single `GROUP BY ROLLUP`), each with its key values and its aggregate values
3590
+ * keyed by id. The client list is what the caller computes itself over the
3591
+ * full set. Aggregates are pushed only when the filter is fully pushed and —
3592
+ * under grouping — every grouping key is a plain column the engine can group
3593
+ * by; a residual filter or an unpushable group key forces every aggregate
3594
+ * client-side (no mixed provenance).
3595
+ */
3596
+ aggregate(
3597
+ request: RemoteRequest,
3598
+ requested: AggregateRequest[],
3599
+ ): Promise<{
3600
+ values: Record<string, unknown>;
3601
+ groups?: Array<{
3602
+ keys: unknown[];
3603
+ grouping?: number[];
3604
+ level: number;
3605
+ values: Record<string, unknown>;
3606
+ }>;
3607
+ engine: AggregateProvenance[];
3608
+ client: AggregateProvenance[];
3609
+ }>;
3610
+ };
3611
+
3612
+ /**
3613
+ * The pushdown map (BACKLOG-0000730 Part B): one published record per statistic
3614
+ * giving whether the engine can express it, the DuckDB aggregate SQL it emits,
3615
+ * and whether that result is IDENTICAL to the grid's own kernel or MAY-DIFFER.
3616
+ * The single source of truth the push router, the docs and `lastPlan()` all read.
3617
+ */
3618
+ export const STAT_PUSHDOWN: Readonly<Record<string, {
3619
+ pushable: boolean;
3620
+ class: 'identical' | 'may-differ' | 'fallback';
3621
+ sql?: string;
3622
+ note?: string;
3623
+ twoColumn?: boolean;
3624
+ blankAware?: boolean;
3625
+ }>>;
3418
3626
 
3419
3627
  /**
3420
3628
  * The capability set an adapter that declares nothing is treated as having: