@platforma-sdk/model 1.63.12 → 1.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/columns/column_collection_builder.cjs +105 -92
  2. package/dist/columns/column_collection_builder.cjs.map +1 -1
  3. package/dist/columns/column_collection_builder.d.ts +13 -12
  4. package/dist/columns/column_collection_builder.d.ts.map +1 -1
  5. package/dist/columns/column_collection_builder.js +107 -94
  6. package/dist/columns/column_collection_builder.js.map +1 -1
  7. package/dist/columns/column_selector.cjs +8 -80
  8. package/dist/columns/column_selector.cjs.map +1 -1
  9. package/dist/columns/column_selector.d.ts +6 -14
  10. package/dist/columns/column_selector.d.ts.map +1 -1
  11. package/dist/columns/column_selector.js +6 -77
  12. package/dist/columns/column_selector.js.map +1 -1
  13. package/dist/columns/column_snapshot.cjs +3 -3
  14. package/dist/columns/column_snapshot.cjs.map +1 -1
  15. package/dist/columns/column_snapshot.d.ts +3 -3
  16. package/dist/columns/column_snapshot.d.ts.map +1 -1
  17. package/dist/columns/column_snapshot.js +3 -3
  18. package/dist/columns/column_snapshot.js.map +1 -1
  19. package/dist/columns/column_snapshot_provider.cjs +1 -1
  20. package/dist/columns/column_snapshot_provider.cjs.map +1 -1
  21. package/dist/columns/column_snapshot_provider.d.ts +8 -8
  22. package/dist/columns/column_snapshot_provider.d.ts.map +1 -1
  23. package/dist/columns/column_snapshot_provider.js +1 -1
  24. package/dist/columns/column_snapshot_provider.js.map +1 -1
  25. package/dist/columns/ctx_column_sources.cjs.map +1 -1
  26. package/dist/columns/ctx_column_sources.d.ts +2 -1
  27. package/dist/columns/ctx_column_sources.d.ts.map +1 -1
  28. package/dist/columns/ctx_column_sources.js.map +1 -1
  29. package/dist/columns/expand_by_partition.cjs +106 -0
  30. package/dist/columns/expand_by_partition.cjs.map +1 -0
  31. package/dist/columns/expand_by_partition.d.ts +33 -0
  32. package/dist/columns/expand_by_partition.d.ts.map +1 -0
  33. package/dist/columns/expand_by_partition.js +105 -0
  34. package/dist/columns/expand_by_partition.js.map +1 -0
  35. package/dist/columns/index.cjs +1 -0
  36. package/dist/columns/index.d.ts +4 -3
  37. package/dist/columns/index.js +1 -0
  38. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs +26 -0
  39. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs.map +1 -0
  40. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js +25 -0
  41. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js.map +1 -0
  42. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs +68 -0
  43. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.cjs.map +1 -0
  44. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js +67 -0
  45. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV3.js.map +1 -0
  46. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs +27 -17
  47. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs.map +1 -1
  48. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.d.ts +4 -0
  49. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.d.ts.map +1 -1
  50. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js +28 -18
  51. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js.map +1 -1
  52. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +258 -175
  53. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
  54. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts +37 -21
  55. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.d.ts.map +1 -1
  56. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +261 -175
  57. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
  58. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +64 -0
  59. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -0
  60. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts +17 -0
  61. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.d.ts.map +1 -0
  62. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +63 -0
  63. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -0
  64. package/dist/components/PlDataTable/createPlDataTable/index.cjs +2 -1
  65. package/dist/components/PlDataTable/createPlDataTable/index.cjs.map +1 -1
  66. package/dist/components/PlDataTable/createPlDataTable/index.d.ts +2 -1
  67. package/dist/components/PlDataTable/createPlDataTable/index.d.ts.map +1 -1
  68. package/dist/components/PlDataTable/createPlDataTable/index.js +2 -1
  69. package/dist/components/PlDataTable/createPlDataTable/index.js.map +1 -1
  70. package/dist/components/PlDataTable/createPlDataTable/utils.cjs +109 -0
  71. package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -0
  72. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts +19 -0
  73. package/dist/components/PlDataTable/createPlDataTable/utils.d.ts.map +1 -0
  74. package/dist/components/PlDataTable/createPlDataTable/utils.js +102 -0
  75. package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -0
  76. package/dist/components/PlDataTable/index.cjs +3 -1
  77. package/dist/components/PlDataTable/index.d.ts +5 -3
  78. package/dist/components/PlDataTable/index.js +3 -1
  79. package/dist/components/PlDataTable/labels.cjs +25 -11
  80. package/dist/components/PlDataTable/labels.cjs.map +1 -1
  81. package/dist/components/PlDataTable/labels.js +25 -11
  82. package/dist/components/PlDataTable/labels.js.map +1 -1
  83. package/dist/components/PlDataTable/state-migration.cjs +8 -2
  84. package/dist/components/PlDataTable/state-migration.cjs.map +1 -1
  85. package/dist/components/PlDataTable/state-migration.d.ts.map +1 -1
  86. package/dist/components/PlDataTable/state-migration.js +8 -2
  87. package/dist/components/PlDataTable/state-migration.js.map +1 -1
  88. package/dist/components/PlDataTable/typesV5.d.ts +23 -15
  89. package/dist/components/PlDataTable/typesV5.d.ts.map +1 -1
  90. package/dist/components/index.cjs +3 -1
  91. package/dist/components/index.d.ts +4 -2
  92. package/dist/components/index.js +3 -1
  93. package/dist/index.cjs +13 -9
  94. package/dist/index.d.ts +9 -7
  95. package/dist/index.js +6 -4
  96. package/dist/labels/derive_distinct_labels.cjs +39 -27
  97. package/dist/labels/derive_distinct_labels.cjs.map +1 -1
  98. package/dist/labels/derive_distinct_labels.d.ts +15 -15
  99. package/dist/labels/derive_distinct_labels.d.ts.map +1 -1
  100. package/dist/labels/derive_distinct_labels.js +39 -27
  101. package/dist/labels/derive_distinct_labels.js.map +1 -1
  102. package/dist/labels/index.cjs +0 -1
  103. package/dist/labels/index.d.ts +1 -2
  104. package/dist/labels/index.js +0 -1
  105. package/dist/package.cjs +1 -1
  106. package/dist/package.js +1 -1
  107. package/dist/render/api.cjs +10 -3
  108. package/dist/render/api.cjs.map +1 -1
  109. package/dist/render/api.d.ts +2 -2
  110. package/dist/render/api.d.ts.map +1 -1
  111. package/dist/render/api.js +10 -3
  112. package/dist/render/api.js.map +1 -1
  113. package/dist/render/util/column_collection.cjs +3 -3
  114. package/dist/render/util/column_collection.cjs.map +1 -1
  115. package/dist/render/util/column_collection.d.ts.map +1 -1
  116. package/dist/render/util/column_collection.js +3 -3
  117. package/dist/render/util/column_collection.js.map +1 -1
  118. package/dist/render/util/label.cjs +2 -2
  119. package/dist/render/util/label.cjs.map +1 -1
  120. package/dist/render/util/label.js +2 -2
  121. package/dist/render/util/label.js.map +1 -1
  122. package/dist/render/util/pcolumn_data.cjs.map +1 -1
  123. package/dist/render/util/pcolumn_data.d.ts +2 -2
  124. package/dist/render/util/pcolumn_data.d.ts.map +1 -1
  125. package/dist/render/util/pcolumn_data.js.map +1 -1
  126. package/package.json +7 -7
  127. package/src/columns/column_collection_builder.test.ts +40 -27
  128. package/src/columns/column_collection_builder.ts +176 -131
  129. package/src/columns/column_selector.test.ts +17 -399
  130. package/src/columns/column_selector.ts +14 -127
  131. package/src/columns/column_snapshot.ts +5 -5
  132. package/src/columns/column_snapshot_provider.ts +11 -10
  133. package/src/columns/ctx_column_sources.ts +2 -2
  134. package/src/columns/expand_by_partition.test.ts +4 -4
  135. package/src/columns/expand_by_partition.ts +4 -3
  136. package/src/columns/index.ts +1 -0
  137. package/src/components/PlDataTable/createPlDataTable/createPTableDefV2.ts +42 -0
  138. package/src/components/PlDataTable/createPlDataTable/createPTableDefV3.ts +89 -0
  139. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV2.ts +51 -19
  140. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +500 -313
  141. package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +122 -0
  142. package/src/components/PlDataTable/createPlDataTable/index.ts +4 -2
  143. package/src/components/PlDataTable/createPlDataTable/utils.test.ts +257 -0
  144. package/src/components/PlDataTable/createPlDataTable/utils.ts +160 -0
  145. package/src/components/PlDataTable/index.ts +15 -2
  146. package/src/components/PlDataTable/labels.ts +29 -18
  147. package/src/components/PlDataTable/state-migration.ts +6 -1
  148. package/src/components/PlDataTable/typesV5.ts +25 -12
  149. package/src/labels/derive_distinct_labels.test.ts +143 -45
  150. package/src/labels/derive_distinct_labels.ts +102 -49
  151. package/src/labels/index.ts +0 -1
  152. package/src/render/api.ts +15 -5
  153. package/src/render/util/column_collection.ts +4 -3
  154. package/src/render/util/label.ts +2 -2
  155. package/src/render/util/pcolumn_data.ts +5 -3
  156. package/dist/labels/write_labels_to_specs.cjs +0 -14
  157. package/dist/labels/write_labels_to_specs.cjs.map +0 -1
  158. package/dist/labels/write_labels_to_specs.d.ts +0 -7
  159. package/dist/labels/write_labels_to_specs.d.ts.map +0 -1
  160. package/dist/labels/write_labels_to_specs.js +0 -13
  161. package/dist/labels/write_labels_to_specs.js.map +0 -1
  162. package/src/labels/write_labels_to_specs.ts +0 -12
@@ -1,380 +1,567 @@
1
1
  import type {
2
2
  AxisId,
3
3
  CanonicalizedJson,
4
- DataInfo,
4
+ FilterSpecNode,
5
5
  PColumn,
6
- PColumnIdAndSpec,
7
- PColumnValues,
8
6
  PObjectId,
9
7
  PTableColumnId,
10
8
  PTableColumnIdAxis,
11
9
  PTableColumnIdColumn,
12
- PTableDefV2,
13
10
  PTableSorting,
14
- SpecQuery,
15
- SingleAxisSelector,
16
- SpecQueryExpression,
17
- SpecQueryJoinEntry,
18
11
  PColumnSpec,
19
- PlRef,
20
12
  MultiColumnSelector,
13
+ SUniversalPColumnId,
21
14
  } from "@milaboratories/pl-model-common";
22
15
  import {
23
- Annotation,
24
16
  canonicalizeJson,
25
17
  getAxisId,
26
18
  getColumnIdAndSpec,
27
- isLinkerColumn,
28
- readAnnotation,
29
- isBooleanExpression,
30
19
  parseJson,
31
20
  uniqueBy,
32
21
  } from "@milaboratories/pl-model-common";
33
- import { filterSpecToSpecQueryExpr } from "../../../filters";
34
- import { collectFilterSpecColumns } from "../../../filters/traverse";
35
- import type { RenderCtxBase, TreeNodeAccessor, PColumnDataUniversal } from "../../../render";
36
- import { allPColumnsReady } from "../../../render";
37
- import { isFunction, isNil } from "es-toolkit";
22
+ import { collectFilterSpecColumns, traverseFilterSpec } from "../../../filters/traverse";
23
+ import type { RenderCtxBase, PColumnDataUniversal } from "../../../render";
38
24
  import { isEmpty } from "es-toolkit/compat";
39
- import { distillFilterSpec } from "../../../filters/distill";
40
- import type { PlDataTableFilters, PlDataTableModel } from "../typesV5";
25
+ import type { PlDataTableFilters, PlDataTableFilterSpecLeaf, PlDataTableModel } from "../typesV5";
41
26
  import { upgradePlDataTableStateV2 } from "../state-migration";
42
27
  import type { PlDataTableStateV2 } from "../state-migration";
43
- import type { ColumnSource, MatchingMode } from "../../../columns";
28
+ import type { ColumnMatch, ColumnSnapshot, MatchingMode } from "../../../columns";
44
29
  import { Services, type RequireServices } from "@milaboratories/pl-model-common";
45
- import { ColumnCollectionBuilder } from "../../../columns";
46
- import { isColumnSnapshotProvider } from "../../../columns/column_snapshot_provider";
47
- import { collectCtxColumnSnapshotProviders } from "../../../columns/ctx_column_sources";
48
30
  import { getAllLabelColumns, getMatchingLabelColumns } from "../labels";
49
-
50
- /** Convert a PTableColumnId to a SpecQueryExpression reference. */
51
- export function columnIdToExpr(col: PTableColumnId): SpecQueryExpression {
52
- if (col.type === "axis") {
53
- return { type: "axisRef", value: col.id as SingleAxisSelector };
54
- }
55
- return { type: "columnRef", value: col.id };
56
- }
57
-
58
- /** Wrap a SpecQuery as a SpecQueryJoinEntry with empty qualifications. */
59
- export function joinEntry<C>(input: SpecQuery<C>): SpecQueryJoinEntry<C> {
60
- return { entry: input, qualifications: [] };
61
- }
62
-
63
- export function createPTableDef(params: {
64
- columns: PColumn<PColumnDataUniversal>[];
65
- labelColumns: PColumn<PColumnDataUniversal>[];
66
- coreJoinType: "inner" | "full";
67
- filters: null | PlDataTableFilters;
68
- sorting: PTableSorting[];
69
- coreColumnPredicate?: (spec: PColumnIdAndSpec) => boolean;
70
- }): PTableDefV2<PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>> {
71
- let coreColumns = params.columns;
72
- const secondaryColumns: typeof params.columns = [];
73
-
74
- if (isFunction(params.coreColumnPredicate)) {
75
- coreColumns = [];
76
- for (const c of params.columns)
77
- if (params.coreColumnPredicate(getColumnIdAndSpec(c))) coreColumns.push(c);
78
- else secondaryColumns.push(c);
79
- }
80
-
81
- secondaryColumns.push(...params.labelColumns);
82
-
83
- // Build SpecQuery directly from columns
84
- const coreJoinQuery: SpecQuery<
85
- PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>
86
- > = {
87
- type: params.coreJoinType === "inner" ? "innerJoin" : "fullJoin",
88
- entries: coreColumns.map((c) => joinEntry({ type: "column", column: c })),
89
- };
90
-
91
- let query: SpecQuery<PColumn<TreeNodeAccessor | PColumnValues | DataInfo<TreeNodeAccessor>>> = {
92
- type: "outerJoin",
93
- primary: joinEntry(coreJoinQuery),
94
- secondary: secondaryColumns.map((c) => joinEntry({ type: "column", column: c })),
95
- };
96
-
97
- // Apply filters
98
- if (params.filters !== null) {
99
- const nonEmpty = distillFilterSpec(params.filters);
100
-
101
- if (!isNil(nonEmpty)) {
102
- const pridicate = filterSpecToSpecQueryExpr(nonEmpty);
103
- if (!isBooleanExpression(pridicate)) {
104
- throw new Error(
105
- `Filter conversion produced a non-boolean expression (got type "${pridicate.type}"), expected a boolean predicate for query filtering`,
106
- );
107
- }
108
- query = {
109
- type: "filter",
110
- input: query,
111
- predicate: pridicate,
112
- };
31
+ import type { DeriveLabelsOptions } from "../../../labels/derive_distinct_labels";
32
+ import {
33
+ deriveAllLabels,
34
+ isColumnHidden,
35
+ isColumnOptional,
36
+ withLabelAnnotations,
37
+ withTableVisualAnnotations,
38
+ } from "./utils";
39
+ import { createPTableDefV3 } from "./createPTableDefV3";
40
+ import { discoverTableColumnSnaphots, type DiscoveredTableColumnOptions } from "./discoverColumns";
41
+ import { isNil, RequiredBy, throwError, type Nil } from "@milaboratories/helpers";
42
+
43
+ export type createPlDataTableOptionsV3 = (
44
+ | {
45
+ discoverColumnOptions: DiscoveredTableColumnOptions;
113
46
  }
114
- }
115
-
116
- // Apply sorting
117
- if (params.sorting.length > 0) {
118
- query = {
119
- type: "sort",
120
- input: query,
121
- sortBy: params.sorting.map((s) => ({
122
- expression: columnIdToExpr(s.column),
123
- ascending: s.ascending,
124
- nullsFirst: !s.naAndAbsentAreLeastValues,
125
- })),
126
- };
127
- }
128
-
129
- return { query };
130
- }
131
-
132
- /** Check if column should be omitted from the table */
133
- export function isColumnHidden(spec: { annotations?: Annotation }): boolean {
134
- return readAnnotation(spec, Annotation.Table.Visibility) === "hidden";
135
- }
47
+ | {
48
+ columns: Nil | TableColumnSnapshot<SUniversalPColumnId>[];
49
+ }
50
+ ) & {
51
+ filters?: PlDataTableFilters;
52
+ sorting?: PTableSorting[];
53
+ primaryJoinType?: "inner" | "full";
136
54
 
137
- /** Check if column is hidden by default */
138
- export function isColumnOptional(spec: { annotations?: Annotation }): boolean {
139
- return readAnnotation(spec, Annotation.Table.Visibility) === "optional";
140
- }
55
+ tableState?: PlDataTableStateV2;
56
+ labelsOptions?: DeriveLabelsOptions;
57
+ columnsDisplayOptions?: ColumnsDisplayOptions;
58
+ };
141
59
 
142
60
  /** Structured source config — selectors/anchors instead of raw ColumnSource. */
143
- type ColumnsSelectorConfig = {
61
+ export type ColumnsSelectorConfig = {
144
62
  include?: MultiColumnSelector | MultiColumnSelector[];
145
63
  exclude?: MultiColumnSelector | MultiColumnSelector[];
146
- anchors?: Record<string, PlRef | PObjectId | PColumnSpec>;
147
64
  mode?: MatchingMode;
148
65
  maxHops?: number;
149
66
  };
150
67
 
151
- export type createPlDataTableOptionsV3 = {
152
- source?: ColumnSource | ColumnSource[];
153
- columns: ColumnsSelectorConfig;
68
+ export type ColumnsDisplayOptions = {
69
+ /** Column ordering rules. Higher priority = further left. First matching rule wins. */
70
+ ordering?: ColumnOrderRule[];
71
+ /** Column visibility rules. First matching rule wins. Unmatched columns use default visibility. */
72
+ visibility?: ColumnVisibilityRule[];
73
+ };
154
74
 
155
- // Existing from V2
156
- filters?: PlDataTableFilters;
157
- sorting?: PTableSorting[];
158
- coreJoinType?: "inner" | "full";
159
- coreColumnPredicate?: (spec: PColumnIdAndSpec) => boolean;
75
+ export type ColumnOrderRule = {
76
+ match: ColumnMatcher;
77
+ /** Higher number = further left in table */
78
+ priority: number;
79
+ };
160
80
 
161
- state?: PlDataTableStateV2;
81
+ export type ColumnVisibilityRule = {
82
+ match: ColumnMatcher;
83
+ visibility: "default" | "optional" | "hidden";
162
84
  };
163
85
 
164
- // interface ColumnDisplayConfig {
165
- // /** Column ordering rules. Higher priority = further left. First matching rule wins. */
166
- // ordering?: ColumnOrderRule[];
167
- // /** Column visibility rules. First matching rule wins. Unmatched columns use default visibility. */
168
- // visibility?: ColumnVisibilityRule[];
169
- // }
170
-
171
- // interface ColumnOrderRule {
172
- // match: ColumnMatcher;
173
- // /** Higher number = further left in table */
174
- // priority: number;
175
- // }
176
-
177
- // interface ColumnVisibilityRule {
178
- // match: ColumnMatcher;
179
- // visibility: "default" | "optional" | "hidden";
180
- // }
181
-
182
- // type ColumnMatcher =
183
- // | ((spec: PColumnSpec) => boolean)
184
- // | { name: string | string[] }
185
- // | { annotation: Record<string, string> }
186
- // | { ids: Set<string> };
86
+ export type ColumnMatcher = (spec: PColumnSpec) => boolean;
87
+
88
+ // Main Function
187
89
 
188
90
  export function createPlDataTableV3<A, U, S extends RequireServices<typeof Services.PFrameSpec>>(
189
91
  ctx: RenderCtxBase<A, U, S>,
190
92
  options: createPlDataTableOptionsV3,
191
93
  ): PlDataTableModel | undefined {
192
- const providers = options.source
193
- ? normalizeSourceList(options.source).filter(isColumnSnapshotProvider)
194
- : collectCtxColumnSnapshotProviders(ctx);
195
-
196
- if (providers.length === 0) return undefined;
197
-
198
- // Step 1: Build collection from sources
199
- const builder = new ColumnCollectionBuilder(ctx.services.pframeSpec).addSources(providers);
200
- const anchors = options.columns.anchors;
201
- const collection = isNil(anchors) ? builder.build() : builder.build({ anchors });
202
-
203
- if (!collection) return undefined;
204
- try {
205
- // Step 2: Get data columns, excluding annotation-hidden ones
206
- const findOptions = options.columns
207
- ? {
208
- include: options.columns.include,
209
- exclude: options.columns.exclude,
210
- mode: options.columns.mode,
211
- maxHops: options.columns.maxHops,
212
- }
213
- : undefined;
214
- const findResult = collection.findColumns(findOptions);
215
- const snapshots = findResult.map((v) => ("column" in v ? v.column : v));
216
- const dataSnapshots = snapshots.filter((s) => !isColumnHidden(s.spec));
217
- if (dataSnapshots.length === 0) return undefined;
218
-
219
- // Convert snapshots to PColumn<PColumnDataUniversal>[]
220
- const columns: PColumn<PColumnDataUniversal>[] = [];
221
- for (const snap of dataSnapshots) {
222
- if (!snap.data) return undefined;
223
- const data = snap.data.get();
224
- if (data === undefined) return undefined;
225
- columns.push({ id: snap.id, spec: snap.spec, data });
226
- }
94
+ const state = upgradePlDataTableStateV2(options.tableState);
95
+ const primaryJoinType = options.primaryJoinType ?? "full";
96
+
97
+ const discovered =
98
+ "discoverColumnOptions" in options
99
+ ? discoverTableColumnSnaphots(ctx, options.discoverColumnOptions)
100
+ : options.columns;
101
+ if (isNil(discovered) || discovered.length === 0) return undefined;
102
+
103
+ const splited = splitDiscoveredColumns(discovered);
104
+ const resolved = resolveDiscoveredColumns(splited, discovered);
105
+
106
+ const labelColumns = getMatchingLabelColumns(resolved.all, getAllLabelColumns(ctx));
107
+
108
+ const derivedLabels = deriveAllLabels({
109
+ columns: discovered.map((dc) => ({
110
+ id: dc.id,
111
+ spec: dc.spec,
112
+ linkerPath: dc.linkerPath?.map((lp) => ({ spec: lp.linker.spec })),
113
+ })),
114
+ labelColumns,
115
+ deriveLabelsOptions: {
116
+ includeNativeLabel: true,
117
+ ...options.labelsOptions,
118
+ },
119
+ });
120
+
121
+ const annotated = annotateColumnGroups(
122
+ resolved,
123
+ labelColumns,
124
+ derivedLabels,
125
+ options.columnsDisplayOptions,
126
+ );
127
+
128
+ const primaryColumnIds = new Set<PObjectId>(
129
+ discovered.filter((dc) => dc.isPrimary).map((dc) => dc.id),
130
+ );
131
+ const primaryColumns = annotated.direct.filter((c) => primaryColumnIds.has(c.id));
132
+ const secondaryColumns = annotated.direct.filter((c) => !primaryColumnIds.has(c.id));
133
+
134
+ if (primaryColumns.length === 0) return undefined;
135
+
136
+ const columnIsAvailable = createColumnValidationById([
137
+ ...annotated.direct,
138
+ ...annotated.linked.flatMap((lc) => [...(annotated.linkers.get(lc.id) ?? []), lc]),
139
+ ...annotated.labels,
140
+ ]);
141
+
142
+ const remapedDefaultFilters = remapFilterColumnIds(options.filters, discovered);
143
+ const filters = concatFilters(
144
+ state.pTableParams.filters,
145
+ state.pTableParams.defaultFilters ?? remapedDefaultFilters,
146
+ );
147
+ validateFilters(filters, columnIsAvailable);
148
+
149
+ const sorting = resolveSorting(
150
+ state.pTableParams.sorting,
151
+ remapSortingColumnIds(options.sorting, discovered),
152
+ );
153
+ validateSorting(sorting, columnIsAvailable);
154
+
155
+ const fullDef = createPTableDefV3({
156
+ primaryJoinType,
157
+ primaryColumns,
158
+ secondaryGroups: [
159
+ ...secondaryColumns.map((c) => [c]),
160
+ ...annotated.linked.map((lc) => [...(annotated.linkers.get(lc.id) ?? []), lc]),
161
+ ...annotated.labels.map((c) => [c]),
162
+ ],
163
+ filters,
164
+ sorting,
165
+ });
166
+
167
+ const fullHandle = ctx.createPTableV2(fullDef);
168
+ const pframeHandle = ctx.createPFrame([
169
+ ...annotated.direct,
170
+ ...annotated.linked,
171
+ ...annotated.labels,
172
+ ...uniqueBy([...annotated.linkers.values()].flat(), (c) => c.id),
173
+ ]);
174
+
175
+ const hiddenSpecs = state.pTableParams.hiddenColIds;
176
+ const hiddenColumnIds = computeHiddenColumns(
177
+ [...annotated.direct, ...annotated.linked],
178
+ sorting,
179
+ filters,
180
+ hiddenSpecs,
181
+ );
182
+
183
+ const visible = buildVisibleColumns(annotated, hiddenColumnIds, labelColumns);
184
+ const visibleNonCoreDirect = secondaryColumns.filter((c) => !hiddenColumnIds.has(c.id));
185
+ const visibleLinkedGroups = buildVisibleLinkedGroups(
186
+ visible.direct,
187
+ visible.linked,
188
+ annotated.linkers,
189
+ hiddenSpecs,
190
+ );
191
+
192
+ const visibleDef = createPTableDefV3({
193
+ primaryJoinType,
194
+ primaryColumns,
195
+ secondaryGroups: [
196
+ ...visibleNonCoreDirect.map((c) => [c]),
197
+ ...visibleLinkedGroups,
198
+ ...visible.labels.map((c) => [c]),
199
+ ],
200
+ filters,
201
+ sorting,
202
+ });
203
+ const visibleHandle = ctx.createPTableV2(visibleDef);
204
+
205
+ return {
206
+ sourceId: state.pTableParams.sourceId,
207
+ fullTableHandle: fullHandle,
208
+ fullPframeHandle: pframeHandle,
209
+ visibleTableHandle: visibleHandle,
210
+ defaultFilters: remapedDefaultFilters,
211
+ } satisfies PlDataTableModel;
212
+ }
227
213
 
228
- // Step 3: Normalize table state
229
- const tableStateNormalized = upgradePlDataTableStateV2(options.state);
214
+ /** A single column discovered from sources — normalized from raw ColumnSnapshot/ColumnMatch. */
215
+ export type TableColumnSnapshot<Id extends PObjectId | SUniversalPColumnId> = ColumnSnapshot<Id> & {
216
+ readonly isPrimary?: boolean;
217
+ readonly originalId?: PObjectId;
218
+ readonly linkerPath?: ColumnMatch["path"];
219
+ };
230
220
 
231
- // Step 4: Get label columns from result pool and match to data columns
232
- const allLabelColumns = getAllLabelColumns(ctx.resultPool);
233
- if (!allLabelColumns) return undefined;
221
+ type TableColumn = PColumn<undefined | PColumnDataUniversal>;
234
222
 
235
- const fullLabelColumns = getMatchingLabelColumns(
236
- columns.map(getColumnIdAndSpec),
237
- allLabelColumns,
238
- );
223
+ type SplitDiscoveredColumns = {
224
+ readonly direct: TableColumnSnapshot<SUniversalPColumnId>[];
225
+ readonly linked: TableColumnSnapshot<SUniversalPColumnId>[];
226
+ };
239
227
 
240
- const fullColumns = [...columns, ...fullLabelColumns];
228
+ type ResolvedColumns = {
229
+ readonly direct: TableColumn[];
230
+ readonly linked: TableColumn[];
231
+ readonly linkers: Map<PObjectId, TableColumn[]>;
232
+ readonly all: TableColumn[];
233
+ };
241
234
 
242
- // Step 5: Build column ID set for filter/sorting validation
243
- const fullColumnsAxes = uniqueBy(
244
- fullColumns.flatMap((c) => c.spec.axesSpec.map((a) => getAxisId(a))),
245
- (a) => canonicalizeJson<AxisId>(a),
246
- );
247
- const fullColumnsIds: PTableColumnId[] = [
248
- ...fullColumnsAxes.map((a) => ({ type: "axis", id: a }) satisfies PTableColumnIdAxis),
249
- ...fullColumns.map((c) => ({ type: "column", id: c.id }) satisfies PTableColumnIdColumn),
250
- ];
251
- const fullColumnsIdsSet = new Set(
252
- fullColumnsIds.map((c) => canonicalizeJson<PTableColumnId>(c)),
253
- );
254
- const isValidColumnId = (id: string): boolean =>
255
- fullColumnsIdsSet.has(id as CanonicalizedJson<PTableColumnId>);
256
-
257
- // Step 6: Filtering validation
258
- const stateFilters = tableStateNormalized.pTableParams.filters;
259
- const opsFilters = options?.filters ?? null;
260
- const filters: null | PlDataTableFilters =
261
- stateFilters != null && opsFilters != null
262
- ? { type: "and", filters: [stateFilters, opsFilters] }
263
- : (stateFilters ?? opsFilters);
264
- const filterColumns = filters ? collectFilterSpecColumns(filters) : [];
265
- const firstInvalidFilterColumn = filterColumns.find((col) => !isValidColumnId(col));
266
- if (firstInvalidFilterColumn)
267
- throw new Error(
268
- `Invalid filter column ${firstInvalidFilterColumn}: column reference does not match the table columns`,
269
- );
270
-
271
- // Step 7: Sorting validation
272
- const userSorting = tableStateNormalized.pTableParams.sorting;
273
- const sorting = (isEmpty(userSorting) ? options?.sorting : userSorting) ?? [];
274
- const firstInvalidSortingColumn = sorting.find(
275
- (s) => !isValidColumnId(canonicalizeJson<PTableColumnId>(s.column)),
276
- );
277
- if (firstInvalidSortingColumn)
278
- throw new Error(
279
- `Invalid sorting column ${JSON.stringify(firstInvalidSortingColumn.column)}: column reference does not match the table columns`,
280
- );
281
-
282
- // Step 8: Build full table definition and handles
283
- const coreJoinType = options?.coreJoinType ?? "full";
284
- const fullDef = createPTableDef({
285
- columns,
286
- labelColumns: fullLabelColumns,
287
- coreJoinType,
288
- filters,
289
- sorting,
290
- coreColumnPredicate: options?.coreColumnPredicate,
291
- });
235
+ type AnnotatedColumnGroups = {
236
+ readonly direct: TableColumn[];
237
+ readonly linked: TableColumn[];
238
+ readonly linkers: Map<PObjectId, TableColumn[]>;
239
+ readonly labels: TableColumn[];
240
+ };
292
241
 
293
- const fullHandle = ctx.createPTableV2(fullDef);
294
- const pframeHandle = ctx.createPFrame(fullColumns);
295
- if (!fullHandle || !pframeHandle) return undefined;
242
+ type VisibleColumns = {
243
+ readonly direct: TableColumn[];
244
+ readonly linked: TableColumn[];
245
+ readonly labels: PColumn<PColumnDataUniversal>[];
246
+ };
296
247
 
297
- // Step 9: Determine hidden columns
298
- const hiddenColumns = new Set<PObjectId>(
299
- ((): PObjectId[] => {
300
- // Inner join works as a filter — all columns must be present
301
- if (coreJoinType === "inner") return [];
248
+ /** Split discovered columns into direct (no linker path) and linked (with linker path). */
249
+ function splitDiscoveredColumns(
250
+ columns: TableColumnSnapshot<SUniversalPColumnId>[],
251
+ ): SplitDiscoveredColumns {
252
+ return {
253
+ direct: columns.filter((dc) => isNil(dc.linkerPath) || dc.linkerPath.length === 0),
254
+ linked: columns.filter((dc) => !isNil(dc.linkerPath) && dc.linkerPath.length > 0),
255
+ };
256
+ }
257
+
258
+ /** Resolve DiscoveredColumn snapshots into PColumn objects with lazily-evaluated data. */
259
+ function resolveDiscoveredColumns(
260
+ split: SplitDiscoveredColumns,
261
+ allDiscovered: TableColumnSnapshot<SUniversalPColumnId>[],
262
+ ): ResolvedColumns {
263
+ const linked = split.linked.map(resolveSnapshot);
264
+ const linkers = new Map<PObjectId, TableColumn[]>(
265
+ split.linked
266
+ .filter(
267
+ (dc): dc is RequiredBy<TableColumnSnapshot<SUniversalPColumnId>, "linkerPath"> =>
268
+ !isNil(dc.linkerPath),
269
+ )
270
+ .map((dc, i) => [linked[i].id, dc.linkerPath.map((s) => resolveSnapshot(s.linker))]),
271
+ );
272
+
273
+ return {
274
+ all: allDiscovered.map(resolveSnapshot),
275
+ direct: split.direct.map(resolveSnapshot),
276
+ linked,
277
+ linkers,
278
+ };
279
+ }
280
+
281
+ /** Annotate all column groups with derived labels and display options. */
282
+ function annotateColumnGroups(
283
+ resolved: ResolvedColumns,
284
+ labelColumns: PColumn<PColumnDataUniversal>[],
285
+ derivedLabels: Record<string, string>,
286
+ displayOptions: ColumnsDisplayOptions | undefined,
287
+ ): AnnotatedColumnGroups {
288
+ const direct = withTableVisualAnnotations(
289
+ displayOptions,
290
+ withLabelAnnotations(derivedLabels, resolved.direct),
291
+ );
292
+ const linked = withTableVisualAnnotations(
293
+ displayOptions,
294
+ withLabelAnnotations(derivedLabels, resolved.linked),
295
+ );
296
+ const linkers = new Map<PObjectId, TableColumn[]>(
297
+ [...resolved.linkers].map(([id, cols]) => [id, withLabelAnnotations(derivedLabels, cols)]),
298
+ );
299
+ const labels = withLabelAnnotations(derivedLabels, labelColumns);
300
+
301
+ return { direct, linked, linkers, labels };
302
+ }
302
303
 
303
- const hiddenColIds = tableStateNormalized.pTableParams.hiddenColIds;
304
- if (hiddenColIds) return hiddenColIds;
304
+ /** Build an index of all valid column IDs (axes + columns) for filter/sorting validation. */
305
+ function createColumnValidationById(fullColumns: TableColumn[]) {
306
+ const axisIds = uniqueBy(
307
+ fullColumns.flatMap((c) => c.spec.axesSpec.map(getAxisId)),
308
+ (a) => canonicalizeJson<AxisId>(a),
309
+ );
305
310
 
306
- return columns.filter((c) => isColumnOptional(c.spec)).map((c) => c.id);
307
- })(),
311
+ const allIds: PTableColumnId[] = [
312
+ ...axisIds.map((a) => ({ type: "axis", id: a }) satisfies PTableColumnIdAxis),
313
+ ...fullColumns.map((c) => ({ type: "column", id: c.id }) satisfies PTableColumnIdColumn),
314
+ ];
315
+
316
+ const validIdSet = new Set(allIds.map((c) => canonicalizeJson<PTableColumnId>(c)));
317
+
318
+ return (id: string): boolean => {
319
+ return validIdSet.has(id as CanonicalizedJson<PTableColumnId>);
320
+ };
321
+ }
322
+
323
+ /** Validate that all column references in filters exist in the table. */
324
+ function validateFilters(
325
+ filters: Nil | PlDataTableFilters,
326
+ isValidColumnId: (id: string) => boolean,
327
+ ): void {
328
+ if (filters == null) return;
329
+ const filterColumns = collectFilterSpecColumns(filters);
330
+ const firstInvalid = filterColumns.find((col) => !isValidColumnId(col));
331
+ if (firstInvalid !== undefined) {
332
+ throw new Error(
333
+ `Invalid filter column ${firstInvalid}: column reference does not match the table columns`,
308
334
  );
335
+ }
336
+ }
337
+
338
+ /** Merge two filter trees into one AND-combined tree. Returns the non-nil one if the other is nil. */
339
+ function concatFilters(
340
+ a: Nil | PlDataTableFilters,
341
+ b: Nil | PlDataTableFilters,
342
+ ): Nil | PlDataTableFilters {
343
+ if (isNil(a)) return b;
344
+ if (isNil(b)) return a;
345
+ return { ...a, filters: [...a.filters, ...b.filters] };
346
+ }
347
+
348
+ /** Pick user sorting from state if non-empty, otherwise fall back to options default. */
349
+ function resolveSorting(
350
+ userSorting: PTableSorting[],
351
+ defaultSorting: Nil | PTableSorting[],
352
+ ): PTableSorting[] {
353
+ return (isEmpty(userSorting) ? defaultSorting : userSorting) ?? [];
354
+ }
355
+
356
+ /** Validate that all column references in sorting exist in the table. */
357
+ function validateSorting(sorting: PTableSorting[], isValidColumnId: (id: string) => boolean): void {
358
+ const firstInvalid = sorting.find(
359
+ (s) => !isValidColumnId(canonicalizeJson<PTableColumnId>(s.column)),
360
+ );
361
+ if (firstInvalid !== undefined) {
362
+ throw new Error(
363
+ `Invalid sorting column ${JSON.stringify(firstInvalid.column)}: column reference does not match the table columns`,
364
+ );
365
+ }
366
+ }
367
+
368
+ /** Determine which columns should be hidden based on state or optional-column defaults. */
369
+ function computeHiddenColumns(
370
+ columns: TableColumn[],
371
+ sorting: Nil | PTableSorting[],
372
+ filters: Nil | PlDataTableFilters,
373
+ hiddenSpecs: Nil | PTableColumnId[],
374
+ ): Set<PObjectId> {
375
+ const alwaysHidden = columns.filter((c) => isColumnHidden(c.spec)).map((c) => c.id);
376
+ const optionalHidden = !isNil(hiddenSpecs)
377
+ ? hiddenSpecs.filter((s): s is PTableColumnIdColumn => s.type === "column").map((s) => s.id)
378
+ : columns.filter((c) => isColumnOptional(c.spec)).map((c) => c.id);
379
+ const initial = [...alwaysHidden, ...optionalHidden];
380
+ const preserved = collectPreservedColumnIds(sorting, filters);
381
+
382
+ return new Set(initial.filter((id) => !preserved.has(id)));
383
+ }
309
384
 
310
- // Preserve linker columns
311
- columns.filter((c) => isLinkerColumn(c.spec)).forEach((c) => hiddenColumns.delete(c.id));
385
+ /**
386
+ * Build visible linked column groups. Non-hidden groups are included fully;
387
+ * hidden groups are trimmed to only the prefix that brings axes not yet
388
+ * covered by earlier groups (visible or previously trimmed).
389
+ */
390
+ function buildVisibleLinkedGroups(
391
+ direct: TableColumn[],
392
+ linked: TableColumn[],
393
+ linkers: Map<PObjectId, TableColumn[]>,
394
+ hiddenSpecs: PTableColumnId[] | null,
395
+ ): TableColumn[][] {
396
+ const result: TableColumn[][] = [];
397
+ const coveredAxisIds = new Set<CanonicalizedJson<AxisId>>();
398
+
399
+ const collectAxes = (group: TableColumn[]) => {
400
+ for (const col of group) {
401
+ for (const as of col.spec.axesSpec) {
402
+ coveredAxisIds.add(canonicalizeJson(getAxisId(as)));
403
+ }
404
+ }
405
+ };
406
+
407
+ collectAxes(direct);
408
+
409
+ for (const lc of linked) {
410
+ const group = [...(linkers.get(lc.id) ?? []), lc];
411
+ result.push(group);
412
+ collectAxes(group);
413
+ }
312
414
 
313
- // Preserve core columns as they change the shape of join
314
- const coreColumnPredicate = options?.coreColumnPredicate;
315
- if (coreColumnPredicate) {
316
- const coreColumns = columns.flatMap((c) =>
317
- coreColumnPredicate(getColumnIdAndSpec(c)) ? [c.id] : [],
318
- );
319
- coreColumns.forEach((c) => hiddenColumns.delete(c));
415
+ for (const group of linkers.values()) {
416
+ const trimmed = trimGroupByVisibleAxes(group, hiddenSpecs, coveredAxisIds);
417
+ if (trimmed.length > 0) {
418
+ result.push(trimmed);
419
+ collectAxes(trimmed);
320
420
  }
421
+ }
422
+
423
+ return result;
424
+ }
321
425
 
322
- // Preserve sorted columns from being hidden
323
- sorting
324
- .map((s) => s.column)
325
- .filter((c): c is PTableColumnIdColumn => c.type === "column")
326
- .forEach((c) => hiddenColumns.delete(c.id));
327
-
328
- // Preserve filter columns from being hidden
329
- if (filters) {
330
- collectFilterSpecColumns(filters)
331
- .flatMap((c) => {
332
- const obj = parseJson(c);
333
- return obj.type === "column" ? [obj.id] : [];
334
- })
335
- .forEach((c) => hiddenColumns.delete(c));
426
+ /**
427
+ * For a linked column group [linker1, ..., linkerN, column], find the rightmost
428
+ * element that has at least one non-hidden and not-yet-covered axis.
429
+ * Return the prefix up to and including that element.
430
+ * If no element has such an axis, return empty array.
431
+ */
432
+ function trimGroupByVisibleAxes(
433
+ group: TableColumn[],
434
+ hiddenSpecs: PTableColumnId[] | null,
435
+ coveredAxisIds: Set<CanonicalizedJson<AxisId>>,
436
+ ): TableColumn[] {
437
+ if (hiddenSpecs === null) return group;
438
+
439
+ const hiddenAxisIds = new Set(
440
+ hiddenSpecs
441
+ .filter((s): s is PTableColumnIdAxis => s.type === "axis")
442
+ .map((s) => canonicalizeJson(s.id)),
443
+ );
444
+
445
+ const uncoveredAxisIds = new Set(
446
+ group
447
+ .flatMap((c) => c.spec.axesSpec.map((as) => canonicalizeJson(getAxisId(as))))
448
+ .filter((id) => !hiddenAxisIds.has(id) && !coveredAxisIds.has(id)),
449
+ );
450
+
451
+ let lastNeeded = -1;
452
+ for (let i = 0; i < group.length; i++) {
453
+ const newAxes = group[i].spec.axesSpec
454
+ .map((as) => canonicalizeJson(getAxisId(as)))
455
+ .filter((id) => uncoveredAxisIds.has(id));
456
+ if (newAxes.length > 0) {
457
+ for (const id of newAxes) uncoveredAxisIds.delete(id);
458
+ lastNeeded = i;
336
459
  }
460
+ }
461
+ return lastNeeded === -1 ? [] : group.slice(0, lastNeeded + 1);
462
+ }
337
463
 
338
- // Step 10: Build visible table definition
339
- const visibleColumns = columns.filter((c) => !hiddenColumns.has(c.id));
340
- const visibleLabelColumns = getMatchingLabelColumns(
341
- visibleColumns.map(getColumnIdAndSpec),
342
- allLabelColumns,
343
- );
464
+ /** Collect IDs of columns that must remain visible (sorted, filtered). */
465
+ function collectPreservedColumnIds(
466
+ sorting: Nil | PTableSorting[],
467
+ filters: Nil | PlDataTableFilters,
468
+ ): Set<PObjectId> {
469
+ const sortedIds = (sorting ?? [])
470
+ .map((s) => s.column)
471
+ .filter((c): c is PTableColumnIdColumn => c.type === "column")
472
+ .map((c) => c.id);
473
+
474
+ const filterIds = !isNil(filters)
475
+ ? collectFilterSpecColumns(filters).flatMap((c) => {
476
+ const obj = parseJson(c);
477
+ return obj.type === "column" ? [obj.id] : [];
478
+ })
479
+ : [];
480
+
481
+ return new Set<PObjectId>([...sortedIds, ...filterIds]);
482
+ }
344
483
 
345
- if (!allPColumnsReady([...visibleColumns, ...visibleLabelColumns])) return undefined;
484
+ /** Filter annotated columns to only visible ones, re-matching label columns for the visible subset. */
485
+ function buildVisibleColumns(
486
+ annotated: AnnotatedColumnGroups,
487
+ hiddenColumns: Set<PObjectId>,
488
+ originalLabelColumns: PColumn<PColumnDataUniversal>[],
489
+ ): VisibleColumns {
490
+ const direct = annotated.direct.filter((c) => !hiddenColumns.has(c.id));
491
+ const linked = annotated.linked.filter((c) => !hiddenColumns.has(c.id));
492
+ const labels = getMatchingLabelColumns(
493
+ [...direct, ...linked].map(getColumnIdAndSpec),
494
+ originalLabelColumns,
495
+ );
496
+ return { direct, linked, labels };
497
+ }
346
498
 
347
- const visibleDef = createPTableDef({
348
- columns: visibleColumns,
349
- labelColumns: visibleLabelColumns,
350
- coreJoinType,
351
- filters,
352
- sorting,
353
- coreColumnPredicate,
354
- });
355
- const visibleHandle = ctx.createPTableV2(visibleDef);
499
+ /** Resolve a ColumnSnapshot to a PColumn with lazily-evaluated data. */
500
+ function resolveSnapshot(
501
+ snap: ColumnSnapshot<PObjectId>,
502
+ ): PColumn<undefined | PColumnDataUniversal> {
503
+ return { id: snap.id, spec: snap.spec, data: snap.data?.get() };
504
+ }
505
+
506
+ /** Remap column references in sorting entries. */
507
+ function remapSortingColumnIds(
508
+ sorting: Nil | PTableSorting[],
509
+ columns: TableColumnSnapshot<PObjectId | SUniversalPColumnId>[],
510
+ ): Nil | PTableSorting[] {
511
+ return sorting?.map((s) => {
512
+ if (s.column.type === "axis") return s; // Axis references are unaffected by column ID remapping
356
513
 
357
- if (!visibleHandle) return undefined;
514
+ const id = s.column.id;
515
+ const column =
516
+ columns.find((c) => (c.originalId ?? c.id) === id) ??
517
+ throwError(`Column ID "${id}" in sorting does not match any discovered column`);
358
518
 
359
519
  return {
360
- sourceId: tableStateNormalized.pTableParams.sourceId,
361
- fullTableHandle: fullHandle,
362
- fullPframeHandle: pframeHandle,
363
- visibleTableHandle: visibleHandle,
364
- } satisfies PlDataTableModel;
365
- } finally {
366
- collection.dispose();
367
- }
520
+ ...s,
521
+ column: {
522
+ type: "column",
523
+ id: column.id,
524
+ },
525
+ };
526
+ });
368
527
  }
369
528
 
370
- /** Normalize raw ColumnSource | ColumnSource[] into a flat list of sources. */
371
- function normalizeSourceList(source: ColumnSource | ColumnSource[]): ColumnSource[] {
372
- if (
373
- Array.isArray(source) &&
374
- source.length > 0 &&
375
- (Array.isArray(source[0]) || isColumnSnapshotProvider(source[0]))
376
- ) {
377
- return source as ColumnSource[];
378
- }
379
- return [source as ColumnSource];
529
+ type PlDataTableFilterNode = FilterSpecNode<PlDataTableFilterSpecLeaf>;
530
+
531
+ /** Remap column references in a filter tree. */
532
+ function remapFilterColumnIds(
533
+ filters: Nil | PlDataTableFilters,
534
+ columns: TableColumnSnapshot<PObjectId | SUniversalPColumnId>[],
535
+ ): Nil | PlDataTableFilters {
536
+ if (isNil(filters)) return filters;
537
+
538
+ const map = (
539
+ tableColumnId: CanonicalizedJson<PTableColumnId>,
540
+ ): CanonicalizedJson<PTableColumnId> => {
541
+ const parsed = parseJson<PTableColumnId>(tableColumnId);
542
+ if (parsed.type === "axis") return tableColumnId; // Axis references are unaffected by column ID remapping
543
+
544
+ const originalId = parsed.id;
545
+ const column =
546
+ columns.find((c) => (c.originalId ?? c.id) === originalId) ??
547
+ throwError(`Column ID "${parsed.id}" in filters does not match any discovered column`);
548
+
549
+ return canonicalizeJson<PTableColumnId>({
550
+ type: "column",
551
+ id: column.id,
552
+ });
553
+ };
554
+
555
+ return traverseFilterSpec(filters, {
556
+ leaf: (leaf): PlDataTableFilterNode => {
557
+ if (leaf.type === undefined) return leaf;
558
+ const result = { ...leaf };
559
+ if ("column" in result) result.column = map(result.column);
560
+ if ("rhs" in result) result.rhs = map(result.rhs);
561
+ return result;
562
+ },
563
+ and: (results): PlDataTableFilterNode => ({ type: "and", filters: results }),
564
+ or: (results): PlDataTableFilterNode => ({ type: "or", filters: results }),
565
+ not: (result): PlDataTableFilterNode => ({ type: "not", filter: result }),
566
+ }) as PlDataTableFilters;
380
567
  }