@platforma-sdk/model 1.80.17 → 1.81.1

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 (123) hide show
  1. package/dist/columns/column.cjs +9 -6
  2. package/dist/columns/column.cjs.map +1 -1
  3. package/dist/columns/column.d.ts +5 -5
  4. package/dist/columns/column.d.ts.map +1 -1
  5. package/dist/columns/column.js +10 -7
  6. package/dist/columns/column.js.map +1 -1
  7. package/dist/columns/column_providers/index.cjs.map +1 -1
  8. package/dist/columns/column_providers/index.d.ts.map +1 -1
  9. package/dist/columns/column_providers/index.js.map +1 -1
  10. package/dist/columns/column_providers/providers.cjs +5 -5
  11. package/dist/columns/column_providers/providers.cjs.map +1 -1
  12. package/dist/columns/column_providers/providers.d.ts +8 -8
  13. package/dist/columns/column_providers/providers.d.ts.map +1 -1
  14. package/dist/columns/column_providers/providers.js +5 -5
  15. package/dist/columns/column_providers/providers.js.map +1 -1
  16. package/dist/columns/column_providers/types.d.ts +3 -3
  17. package/dist/columns/column_providers/types.d.ts.map +1 -1
  18. package/dist/columns/column_recipes/column_discovered_recipe.cjs +1 -1
  19. package/dist/columns/column_recipes/column_discovered_recipe.cjs.map +1 -1
  20. package/dist/columns/column_recipes/column_discovered_recipe.d.ts +1 -1
  21. package/dist/columns/column_recipes/column_discovered_recipe.js +1 -1
  22. package/dist/columns/column_recipes/column_discovered_recipe.js.map +1 -1
  23. package/dist/columns/column_recipes/column_overrided_recipe.cjs +47 -2
  24. package/dist/columns/column_recipes/column_overrided_recipe.cjs.map +1 -1
  25. package/dist/columns/column_recipes/column_overrided_recipe.d.ts +30 -3
  26. package/dist/columns/column_recipes/column_overrided_recipe.d.ts.map +1 -1
  27. package/dist/columns/column_recipes/column_overrided_recipe.js +47 -3
  28. package/dist/columns/column_recipes/column_overrided_recipe.js.map +1 -1
  29. package/dist/columns/column_recipes/index.cjs +4 -14
  30. package/dist/columns/column_recipes/index.cjs.map +1 -1
  31. package/dist/columns/column_recipes/index.d.ts +3 -10
  32. package/dist/columns/column_recipes/index.d.ts.map +1 -1
  33. package/dist/columns/column_recipes/index.js +5 -14
  34. package/dist/columns/column_recipes/index.js.map +1 -1
  35. package/dist/columns/{column_lazy.cjs → data_column.cjs} +57 -54
  36. package/dist/columns/data_column.cjs.map +1 -0
  37. package/dist/columns/data_column.d.ts +152 -0
  38. package/dist/columns/data_column.d.ts.map +1 -0
  39. package/dist/columns/{column_lazy.js → data_column.js} +55 -52
  40. package/dist/columns/data_column.js.map +1 -0
  41. package/dist/columns/derive_axis_values_labels.cjs +4 -4
  42. package/dist/columns/derive_axis_values_labels.cjs.map +1 -1
  43. package/dist/columns/derive_axis_values_labels.d.ts +2 -2
  44. package/dist/columns/derive_axis_values_labels.js +4 -4
  45. package/dist/columns/derive_axis_values_labels.js.map +1 -1
  46. package/dist/columns/index.cjs +9 -9
  47. package/dist/columns/index.d.ts +5 -5
  48. package/dist/columns/index.js +5 -5
  49. package/dist/columns/{expand_by_partition.cjs → split_by_axes.cjs} +13 -8
  50. package/dist/columns/split_by_axes.cjs.map +1 -0
  51. package/dist/columns/{expand_by_partition.d.ts → split_by_axes.d.ts} +18 -8
  52. package/dist/columns/split_by_axes.d.ts.map +1 -0
  53. package/dist/columns/{expand_by_partition.js → split_by_axes.js} +14 -9
  54. package/dist/columns/split_by_axes.js.map +1 -0
  55. package/dist/columns/utils.cjs +50 -43
  56. package/dist/columns/utils.cjs.map +1 -1
  57. package/dist/columns/utils.d.ts +44 -19
  58. package/dist/columns/utils.d.ts.map +1 -1
  59. package/dist/columns/utils.js +50 -44
  60. package/dist/columns/utils.js.map +1 -1
  61. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs +3 -3
  62. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.cjs.map +1 -1
  63. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js +3 -3
  64. package/dist/components/PlDataTable/createPlDataTable/createPTableDefV2.js.map +1 -1
  65. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs +2 -2
  66. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.cjs.map +1 -1
  67. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js +2 -2
  68. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV2.js.map +1 -1
  69. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs +1 -1
  70. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.cjs.map +1 -1
  71. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js +2 -2
  72. package/dist/components/PlDataTable/createPlDataTable/createPlDataTableV3.js.map +1 -1
  73. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs +1 -1
  74. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.cjs.map +1 -1
  75. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js +2 -2
  76. package/dist/components/PlDataTable/createPlDataTable/discoverColumns.js.map +1 -1
  77. package/dist/components/PlDataTable/createPlDataTable/utils.cjs +1 -1
  78. package/dist/components/PlDataTable/createPlDataTable/utils.cjs.map +1 -1
  79. package/dist/components/PlDataTable/createPlDataTable/utils.js +1 -1
  80. package/dist/components/PlDataTable/createPlDataTable/utils.js.map +1 -1
  81. package/dist/components/PlDatasetSelector/enrichment_discovery.cjs +1 -1
  82. package/dist/components/PlDatasetSelector/enrichment_discovery.cjs.map +1 -1
  83. package/dist/components/PlDatasetSelector/enrichment_discovery.js +2 -2
  84. package/dist/components/PlDatasetSelector/enrichment_discovery.js.map +1 -1
  85. package/dist/index.cjs +9 -9
  86. package/dist/index.d.ts +5 -5
  87. package/dist/index.js +5 -5
  88. package/dist/package.cjs +1 -1
  89. package/dist/package.js +1 -1
  90. package/package.json +8 -8
  91. package/src/columns/__test_helpers__/collection_driver.ts +6 -6
  92. package/src/columns/__test_helpers__/stub_registry.ts +4 -6
  93. package/src/columns/column.ts +20 -9
  94. package/src/columns/column_providers/index.ts +9 -3
  95. package/src/columns/column_providers/providers.ts +16 -15
  96. package/src/columns/column_providers/types.ts +3 -3
  97. package/src/columns/column_recipes/column_discovered_recipe.ts +1 -1
  98. package/src/columns/column_recipes/column_overrided_recipe.ts +58 -4
  99. package/src/columns/column_recipes/column_recipes.test.ts +2 -2
  100. package/src/columns/column_recipes/index.ts +5 -20
  101. package/src/columns/{column_lazy.test.ts → data_column.test.ts} +19 -19
  102. package/src/columns/{column_lazy.ts → data_column.ts} +105 -67
  103. package/src/columns/derive_axis_values_labels.ts +4 -4
  104. package/src/columns/index.ts +2 -2
  105. package/src/columns/predicates.test.ts +124 -0
  106. package/src/columns/{expand_by_partition.test.ts → split_by_axes.test.ts} +29 -21
  107. package/src/columns/{expand_by_partition.ts → split_by_axes.ts} +22 -9
  108. package/src/columns/utils.ts +53 -48
  109. package/src/components/PlDataTable/columnResolver.test.ts +3 -3
  110. package/src/components/PlDataTable/createPlDataTable/createPTableDefV2.ts +3 -3
  111. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV2.ts +2 -2
  112. package/src/components/PlDataTable/createPlDataTable/createPlDataTableV3.ts +2 -2
  113. package/src/components/PlDataTable/createPlDataTable/discoverColumns.ts +2 -2
  114. package/src/components/PlDataTable/createPlDataTable/utils.test.ts +34 -3
  115. package/src/components/PlDataTable/createPlDataTable/utils.ts +4 -1
  116. package/src/components/PlDatasetSelector/enrichment_discovery.ts +2 -2
  117. package/dist/columns/column_lazy.cjs.map +0 -1
  118. package/dist/columns/column_lazy.d.ts +0 -137
  119. package/dist/columns/column_lazy.d.ts.map +0 -1
  120. package/dist/columns/column_lazy.js.map +0 -1
  121. package/dist/columns/expand_by_partition.cjs.map +0 -1
  122. package/dist/columns/expand_by_partition.d.ts.map +0 -1
  123. package/dist/columns/expand_by_partition.js.map +0 -1
@@ -0,0 +1,124 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import type {
3
+ AxisSpec,
4
+ PColumnSpec,
5
+ PObjectId,
6
+ SpecOverrides,
7
+ } from "@milaboratories/pl-model-common";
8
+ import type { PColumnDataUniversal } from "../render/internal";
9
+ import { DataColumnImpl, type DataColumnRecipe } from "./data_column";
10
+ import { ColumnFilteredRecipe } from "./column_recipes/column_filtered_recipe";
11
+ import { ColumnOverriddenRecipe } from "./column_recipes/column_overrided_recipe";
12
+ import { hasReachableData, hasSingleDataColumn } from "./utils";
13
+
14
+ // --- Helpers ---
15
+
16
+ const axis = (name: string): AxisSpec => ({ name, type: "String" }) as AxisSpec;
17
+
18
+ const twoAxisSpec: PColumnSpec = {
19
+ kind: "PColumn",
20
+ name: "count",
21
+ valueType: "Int",
22
+ axesSpec: [axis("sampleId"), axis("clonotypeKey")],
23
+ annotations: {},
24
+ } as PColumnSpec;
25
+
26
+ const overrides = (patch: Partial<SpecOverrides>): SpecOverrides => ({
27
+ annotations: patch.annotations ?? {},
28
+ domain: patch.domain ?? {},
29
+ contextDomain: patch.contextDomain ?? {},
30
+ axesSpec: patch.axesSpec ?? {},
31
+ });
32
+
33
+ const DATA = { marker: "leaf-data" } as unknown as PColumnDataUniversal;
34
+
35
+ const leaf = () =>
36
+ DataColumnImpl.fromColumn({
37
+ id: "leafId" as PObjectId,
38
+ spec: twoAxisSpec,
39
+ data: DATA,
40
+ });
41
+
42
+ // ════════════════════════════════════════════════════════════════════════════
43
+ // hasReachableData — "can I read this column's data, consistent with its spec?"
44
+ // ════════════════════════════════════════════════════════════════════════════
45
+
46
+ describe("hasReachableData", () => {
47
+ test("bare leaf: true, and getData returns the leaf payload", () => {
48
+ const col = leaf();
49
+ expect(hasReachableData(col)).toBe(true);
50
+ if (!hasReachableData(col)) throw new Error("unreachable");
51
+ expect(col.getData()).toBe(DATA);
52
+ });
53
+
54
+ test("override over a leaf: true — a spec patch never reshapes data", () => {
55
+ const col = ColumnOverriddenRecipe.wrap(leaf(), overrides({ annotations: { tag: "1" } }));
56
+ expect(hasReachableData(col)).toBe(true);
57
+ if (!hasReachableData(col)) throw new Error("unreachable");
58
+ expect(col.getData()).toBe(DATA);
59
+ // The patch landed on the spec while the data stayed the leaf's.
60
+ expect(col.getSpec().annotations?.tag).toBe("1");
61
+ expect(col.getSpec().axesSpec).toHaveLength(2);
62
+ });
63
+
64
+ test("axis-filtered leaf: false — spec drops an axis the data still carries", () => {
65
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
66
+ // The mismatch this guards against: one axis in the spec, two in the data.
67
+ expect(filtered.getSpec().axesSpec).toHaveLength(1);
68
+ expect(hasReachableData(filtered)).toBe(false);
69
+ });
70
+
71
+ test("override over an axis-filtered leaf: false", () => {
72
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
73
+ const col = ColumnOverriddenRecipe.wrap(filtered, overrides({ annotations: { tag: "1" } }));
74
+ expect(hasReachableData(col)).toBe(false);
75
+ });
76
+
77
+ test("an unreadable override has no getData at all — nothing to call, nothing to throw", () => {
78
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
79
+ const col = ColumnOverriddenRecipe.wrap(filtered, overrides({ annotations: { tag: "1" } }));
80
+ expect((col as Partial<DataColumnRecipe>).getData).toBeUndefined();
81
+ });
82
+
83
+ test("merging overrides keeps the readable variant", () => {
84
+ const once = leaf().withSpecs(overrides({ annotations: { a: "1" } }));
85
+ const twice = once.withSpecs(overrides({ annotations: { b: "2" } }));
86
+ expect(hasReachableData(twice)).toBe(true);
87
+ if (!hasReachableData(twice)) throw new Error("unreachable");
88
+ expect(twice.getData()).toBe(DATA);
89
+ expect(twice.getSpec().annotations).toMatchObject({ a: "1", b: "2" });
90
+ });
91
+
92
+ test("merging overrides does not resurrect readability over a filtered leaf", () => {
93
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
94
+ const twice = filtered
95
+ .withSpecs(overrides({ annotations: { a: "1" } }))
96
+ .withSpecs(overrides({ annotations: { b: "2" } }));
97
+ expect(hasReachableData(twice)).toBe(false);
98
+ });
99
+ });
100
+
101
+ // ════════════════════════════════════════════════════════════════════════════
102
+ // hasSingleDataColumn — "does this column read one data column, or several?"
103
+ // ════════════════════════════════════════════════════════════════════════════
104
+
105
+ describe("hasSingleDataColumn", () => {
106
+ test("bare leaf, override and axis filter all read a single data column", () => {
107
+ const bare = leaf();
108
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
109
+ const overridden = ColumnOverriddenRecipe.wrap(filtered, overrides({ domain: { d: "1" } }));
110
+
111
+ expect(hasSingleDataColumn(bare)).toBe(true);
112
+ expect(hasSingleDataColumn(filtered)).toBe(true);
113
+ expect(hasSingleDataColumn(overridden)).toBe(true);
114
+ });
115
+
116
+ test("is a different question from hasReachableData", () => {
117
+ const filtered = ColumnFilteredRecipe.wrap(leaf(), [[0, "s1"]]);
118
+ // Reads a single data column, yet its data is not reachable here (the
119
+ // slice happens engine-side). Conflating the two is the bug this pair of
120
+ // predicates replaced.
121
+ expect(hasSingleDataColumn(filtered)).toBe(true);
122
+ expect(hasReachableData(filtered)).toBe(false);
123
+ });
124
+ });
@@ -13,9 +13,10 @@ import {
13
13
  } from "@milaboratories/pl-model-common";
14
14
  import { describe, expect, test } from "vitest";
15
15
  import type { PColumnDataUniversal } from "../render/internal";
16
- import type { ColumnLazy } from "./column_lazy";
17
- import { ColumnLazyImpl } from "./column_lazy";
18
- import { expandByPartition } from "./expand_by_partition";
16
+ import type { DataColumnRecipe } from "./data_column";
17
+ import { DataColumnImpl } from "./data_column";
18
+ import type { SplitByAxesOpts } from "./split_by_axes";
19
+ import { splitByAxes } from "./split_by_axes";
19
20
 
20
21
  // --- Helpers ---
21
22
 
@@ -33,13 +34,13 @@ function createSpec(name: string, axes: AxisSpec[]): PColumnSpec {
33
34
  } as PColumnSpec;
34
35
  }
35
36
 
36
- /** Build a synthetic ColumnLazy whose data is a JsonPartitioned DataInfoEntries. */
37
+ /** Build a synthetic DataColumnRecipe whose data is a JsonPartitioned DataInfoEntries. */
37
38
  function createReadyLazy(
38
39
  id: string,
39
40
  spec: PColumnSpec,
40
41
  partitionKeyLength: number,
41
42
  parts: { key: (string | number)[]; value: unknown }[],
42
- ): ColumnLazy {
43
+ ): DataColumnRecipe {
43
44
  const dataEntries: JsonPartitionedDataInfoEntries<unknown> = {
44
45
  type: "JsonPartitioned",
45
46
  partitionKeyLength,
@@ -48,15 +49,15 @@ function createReadyLazy(
48
49
  // `data` is typed `PColumnDataUniversal` but `getUniquePartitionKeys`
49
50
  // duck-types DataInfoEntries via `isDataInfoEntries`, so the JsonPartitioned
50
51
  // payload works at runtime.
51
- return ColumnLazyImpl.fromColumn({
52
+ return DataColumnImpl.fromColumn({
52
53
  id: id as PObjectId,
53
54
  spec,
54
55
  data: dataEntries as unknown as PColumnDataUniversal,
55
56
  });
56
57
  }
57
58
 
58
- function createComputingLazy(id: string, spec: PColumnSpec): ColumnLazy {
59
- return ColumnLazyImpl.fromColumn({
59
+ function createComputingLazy(id: string, spec: PColumnSpec): DataColumnRecipe {
60
+ return DataColumnImpl.fromColumn({
60
61
  id: id as PObjectId,
61
62
  spec,
62
63
  data: undefined,
@@ -74,14 +75,21 @@ function extractTrace(recipe: { getSpec(): PColumnSpec }): Trace[] {
74
75
  return raw ? (JSON.parse(raw) as Trace[]) : [];
75
76
  }
76
77
 
78
+ /**
79
+ * `axisValuesLabels` defaults to `deriveAxisValuesLabels()`, which needs the
80
+ * ambient render ctx these unit tests deliberately run without. Every case
81
+ * below is about the split itself, so opt out of label resolution explicitly.
82
+ */
83
+ const NO_LABELS: SplitByAxesOpts = { axisValuesLabels: () => undefined };
84
+
77
85
  // --- Tests ---
78
86
 
79
- describe("expandByPartition", () => {
87
+ describe("splitByAxes", () => {
80
88
  test("no split axes returns inputs unchanged", () => {
81
89
  const s = createReadyLazy("col1", createSpec("c", [createAxis("a")]), 1, [
82
90
  { key: ["x"], value: {} },
83
91
  ]);
84
- const result = expandByPartition([s], []);
92
+ const result = splitByAxes([s], []);
85
93
  expect(result).toBeDefined();
86
94
  expect(result).toHaveLength(1);
87
95
  expect(result![0]).toBe(s); // same reference
@@ -99,7 +107,7 @@ describe("expandByPartition", () => {
99
107
  ],
100
108
  );
101
109
 
102
- const result = expandByPartition([s], [{ idx: 0 }]);
110
+ const result = splitByAxes([s], [{ idx: 0 }], NO_LABELS);
103
111
  expect(result).toBeDefined();
104
112
  // unique values on axis 0: s1, s2 → 2 recipes
105
113
  expect(result).toHaveLength(2);
@@ -124,7 +132,7 @@ describe("expandByPartition", () => {
124
132
  ],
125
133
  );
126
134
 
127
- const result = expandByPartition([s], [{ idx: 0 }, { idx: 1 }]);
135
+ const result = splitByAxes([s], [{ idx: 0 }, { idx: 1 }], NO_LABELS);
128
136
  expect(result).toBeDefined();
129
137
  // a: a1, a2 (2) × b: b1, b2 (2) = 4
130
138
  expect(result).toHaveLength(4);
@@ -148,7 +156,7 @@ describe("expandByPartition", () => {
148
156
  ],
149
157
  );
150
158
 
151
- const result = expandByPartition([s], [{ idx: 0 }]);
159
+ const result = splitByAxes([s], [{ idx: 0 }], NO_LABELS);
152
160
  expect(result).toBeDefined();
153
161
  expect(result).toHaveLength(2);
154
162
 
@@ -183,7 +191,7 @@ describe("expandByPartition", () => {
183
191
  };
184
192
  const s = createReadyLazy("col1", spec, 1, [{ key: ["s1"], value: {} }]);
185
193
 
186
- const result = expandByPartition([s], [{ idx: 0 }]);
194
+ const result = splitByAxes([s], [{ idx: 0 }], NO_LABELS);
187
195
  expect(result).toBeDefined();
188
196
  expect(result).toHaveLength(1);
189
197
 
@@ -205,7 +213,7 @@ describe("expandByPartition", () => {
205
213
  s2: "Sample Two",
206
214
  };
207
215
 
208
- const result = expandByPartition([s], [{ idx: 0 }], {
216
+ const result = splitByAxes([s], [{ idx: 0 }], {
209
217
  axisValuesLabels: () => labels,
210
218
  });
211
219
 
@@ -216,7 +224,7 @@ describe("expandByPartition", () => {
216
224
 
217
225
  test("computing input (data undefined) returns undefined", () => {
218
226
  const s = createComputingLazy("col1", createSpec("c", [createAxis("a")]));
219
- const result = expandByPartition([s], [{ idx: 0 }]);
227
+ const result = splitByAxes([s], [{ idx: 0 }], NO_LABELS);
220
228
  expect(result).toBeUndefined();
221
229
  });
222
230
 
@@ -228,7 +236,7 @@ describe("expandByPartition", () => {
228
236
  [], // no parts → no unique keys
229
237
  );
230
238
 
231
- const result = expandByPartition([s], [{ idx: 0 }]);
239
+ const result = splitByAxes([s], [{ idx: 0 }], NO_LABELS);
232
240
  expect(result).toBeDefined();
233
241
  expect(result).toHaveLength(0);
234
242
  });
@@ -244,7 +252,7 @@ describe("expandByPartition", () => {
244
252
  ],
245
253
  );
246
254
 
247
- const result = expandByPartition([s], [{ idx: 0 }]);
255
+ const result = splitByAxes([s], [{ idx: 0 }], NO_LABELS);
248
256
  expect(result).toBeDefined();
249
257
  expect(result).toHaveLength(2);
250
258
 
@@ -274,7 +282,7 @@ describe("expandByPartition", () => {
274
282
  ],
275
283
  );
276
284
 
277
- const result = expandByPartition([s], [{ idx: 0 }]);
285
+ const result = splitByAxes([s], [{ idx: 0 }], NO_LABELS);
278
286
  expect(result).toBeDefined();
279
287
  expect(result).toHaveLength(2);
280
288
 
@@ -306,7 +314,7 @@ describe("expandByPartition", () => {
306
314
  ],
307
315
  );
308
316
 
309
- const result = expandByPartition([s1, s2], [{ idx: 0 }]);
317
+ const result = splitByAxes([s1, s2], [{ idx: 0 }], NO_LABELS);
310
318
  expect(result).toBeDefined();
311
319
  // s1: 2 unique + s2: 3 unique = 5 total
312
320
  expect(result).toHaveLength(5);
@@ -320,6 +328,6 @@ describe("expandByPartition", () => {
320
328
  [{ key: ["x"], value: {} }],
321
329
  );
322
330
 
323
- expect(() => expandByPartition([s], [{ idx: 1 }])).toThrow(/Not enough partition keys/);
331
+ expect(() => splitByAxes([s], [{ idx: 1 }], NO_LABELS)).toThrow(/Not enough partition keys/);
324
332
  });
325
333
  });
@@ -13,11 +13,12 @@ import {
13
13
  } from "@milaboratories/pl-model-common";
14
14
  import { TreeNodeAccessor } from "../render/accessor";
15
15
  import { getUniquePartitionKeys } from "../render/util/pcolumn_data";
16
+ import { deriveAxisValuesLabels } from "./derive_axis_values_labels";
16
17
  import type { ColumnRecipe } from "./column_recipes";
17
18
  import { ColumnFilteredRecipe } from "./column_recipes/column_filtered_recipe";
18
19
  import { ColumnOverriddenRecipe } from "./column_recipes/column_overrided_recipe";
19
20
  import { Column } from "./column";
20
- import { getLeafColumnData } from "./utils";
21
+ import { hasReachableData } from "./utils";
21
22
 
22
23
  // --- Types ---
23
24
 
@@ -26,8 +27,15 @@ export interface SplitAxis {
26
27
  readonly idx: number;
27
28
  }
28
29
 
29
- export interface ExpandByPartitionOpts {
30
- /** Resolve axis values to human-readable labels. */
30
+ export interface SplitByAxesOpts {
31
+ /**
32
+ * Resolve axis values to human-readable labels.
33
+ *
34
+ * Defaults to {@link deriveAxisValuesLabels} over the ambient render ctx
35
+ * (current block outputs + result pool), resolved lazily on the first
36
+ * lookup. Pass an explicit callback to narrow the label source, or
37
+ * `() => undefined` to keep raw axis values.
38
+ */
31
39
  axisValuesLabels?: (axisId: AxisId) => undefined | Record<string | number, string>;
32
40
  }
33
41
 
@@ -36,8 +44,11 @@ export interface ExpandByPartitionOpts {
36
44
  const MAX_KEY_COMBINATIONS = 10_000;
37
45
 
38
46
  /**
39
- * Expand each input column along the requested partition axes into one
40
- * {@link ColumnRecipe} per Cartesian combination of unique partition values.
47
+ * Split each input column along the requested axes into one
48
+ * {@link ColumnRecipe} per Cartesian combination of the axes' distinct values.
49
+ *
50
+ * The split axes must be partition-key axes of the input's data — the distinct
51
+ * values are read off the partition keys.
41
52
  *
42
53
  * Each split is produced as `ColumnOverriddenRecipe.wrap(ColumnFilteredRecipe.wrap(inner, axisFilters), { domain, annotations })`:
43
54
  * - `ColumnFilteredRecipe` pins all split axes at once (one wrap call with
@@ -55,22 +66,24 @@ const MAX_KEY_COMBINATIONS = 10_000;
55
66
  * Returns `undefined` when any input's `getData()` is not a
56
67
  * {@link TreeNodeAccessor} — partition inspection is not yet possible.
57
68
  */
58
- export function expandByPartition(
69
+ export function splitByAxes(
59
70
  inputs: Column[],
60
71
  splitAxes: SplitAxis[],
61
- opts?: ExpandByPartitionOpts,
72
+ opts?: SplitByAxesOpts,
62
73
  ): ColumnRecipe[] | undefined {
63
74
  if (splitAxes.length === 0) {
64
75
  return [...inputs];
65
76
  }
66
77
 
78
+ const axisValuesLabels = opts?.axisValuesLabels ?? deriveAxisValuesLabels();
79
+
67
80
  const splitAxisIdxs = splitAxes.map((a) => a.idx).sort((a, b) => a - b);
68
81
  const maxSplitIdx = splitAxisIdxs[splitAxisIdxs.length - 1];
69
82
 
70
83
  const result: ColumnRecipe[] = [];
71
84
 
72
85
  for (const inner of inputs) {
73
- const data = getLeafColumnData(inner);
86
+ const data = hasReachableData(inner) ? inner.getData() : undefined;
74
87
  // Partition inspection requires either a live tree-accessor or already
75
88
  // parsed DataInfoEntries. Anything else means the input is not yet ready.
76
89
  if (!(data instanceof TreeNodeAccessor) && !isDataInfoEntries(data)) {
@@ -90,7 +103,7 @@ export function expandByPartition(
90
103
  const axesSpec = spec.axesSpec;
91
104
  const splitAxisSpecs = splitAxisIdxs.map((idx) => axesSpec[idx]);
92
105
  const splitAxisIds = splitAxisSpecs.map((axisSpec) => getAxisId(axisSpec));
93
- const axesLabels = splitAxisIds.map((axisId) => opts?.axisValuesLabels?.(axisId));
106
+ const axesLabels = splitAxisIds.map((axisId) => axisValuesLabels(axisId));
94
107
 
95
108
  const existingTraceRaw = readAnnotation(spec, Annotation.Trace);
96
109
  const baseTrace: TraceEntry[] = existingTraceRaw
@@ -11,10 +11,13 @@ import { throwError } from "@milaboratories/helpers";
11
11
  import type { GlobalCfgRenderCtx } from "../render/internal";
12
12
  import { TreeNodeAccessor } from "../render";
13
13
  import { deriveDistinctLabels, type DeriveLabelsOptions } from "../labels/derive_distinct_labels";
14
- import { ColumnLazy, ColumnLazyImpl, type ColumnLazyData } from "./column_lazy";
14
+ import { DataColumnRecipe, DataColumnImpl, type ColumnData } from "./data_column";
15
15
  import { ColumnDiscoveredRecipe } from "./column_recipes/column_discovered_recipe";
16
16
  import { ColumnFilteredRecipe } from "./column_recipes/column_filtered_recipe";
17
- import { ColumnOverriddenRecipe } from "./column_recipes/column_overrided_recipe";
17
+ import {
18
+ ColumnOverriddenRecipe,
19
+ DataColumnOverriddenRecipe,
20
+ } from "./column_recipes/column_overrided_recipe";
18
21
  import type { ColumnRecipe } from "./column_recipes/types";
19
22
  import { ColumnsCollection, isColumnsCollection } from "./columns_collection";
20
23
  import type { ColumnsSource } from "./column_providers/types";
@@ -26,7 +29,7 @@ import type { ColumnsSource } from "./column_providers/types";
26
29
  * other engine-consumed column.
27
30
  *
28
31
  * Pure query walk — no registry access. Use {@link collectLinkerColumns} for
29
- * the resolved {@link ColumnLazy} variant.
32
+ * the resolved {@link DataColumnRecipe} variant.
30
33
  */
31
34
  export function collectLinkerIds(recipe: ColumnRecipe): PObjectId[] {
32
35
  const hit = extractPObjectId(recipe.id);
@@ -47,16 +50,16 @@ export function collectLinkerIds(recipe: ColumnRecipe): PObjectId[] {
47
50
 
48
51
  /**
49
52
  * {@link collectLinkerIds} resolved against the ambient context as
50
- * {@link ColumnLazy} instances. Throws if any id fails to resolve —
53
+ * {@link DataColumnRecipe} leaves. Throws if any id fails to resolve —
51
54
  * matches the contract of the legacy `resolveLinkers` it replaces.
52
55
  */
53
56
  export function collectLinkerColumns(
54
57
  recipe: ColumnRecipe,
55
58
  opts: { ctx?: GlobalCfgRenderCtx } = {},
56
- ): ColumnLazy[] {
59
+ ): DataColumnRecipe<PObjectId>[] {
57
60
  return collectLinkerIds(recipe).map(
58
61
  (id) =>
59
- ColumnLazyImpl.fromId(id, opts) ??
62
+ DataColumnImpl.fromId(id, opts) ??
60
63
  throwError(`materializeLinkers: linker ${id} not resolvable`),
61
64
  );
62
65
  }
@@ -95,7 +98,7 @@ export function queriesQualifications(
95
98
  *
96
99
  * Walks every physical leaf the recipe depends on via
97
100
  * {@link ColumnRecipe.getReferencedIds} (resolves each id back to a
98
- * {@link ColumnLazy} through the ambient ctx) and ANDs `hasData()` across
101
+ * {@link DataColumnRecipe} through the ambient ctx) and ANDs `hasData()` across
99
102
  * all of them. Inline {@link PColumnValues} payloads count as present.
100
103
  *
101
104
  * Returns `false` if any leaf cannot be re-resolved in `opts.ctx` (treat as
@@ -107,15 +110,15 @@ export function hasColumnData(
107
110
  opts: { ctx?: GlobalCfgRenderCtx } = {},
108
111
  ): boolean {
109
112
  for (const id of recipe.getReferencedIds()) {
110
- const lazy = ColumnLazyImpl.fromId(id, opts);
111
- if (lazy === undefined) return false;
112
- if (lazy.getDataStatus() !== "present") return false;
113
- if (!isLazyDataPresent(lazy.getData())) return false;
113
+ const leaf = DataColumnImpl.fromId(id, opts);
114
+ if (leaf === undefined) return false;
115
+ if (leaf.getDataStatus() !== "present") return false;
116
+ if (!isLeafDataPresent(leaf.getData())) return false;
114
117
  }
115
118
  return true;
116
119
  }
117
120
 
118
- function isLazyDataPresent(data: ColumnLazyData): boolean {
121
+ function isLeafDataPresent(data: ColumnData): boolean {
119
122
  if (data === undefined) return false;
120
123
  if (Array.isArray(data)) return true;
121
124
  if (data instanceof TreeNodeAccessor) return data.hasData();
@@ -151,51 +154,53 @@ function findDiscovered(recipe: ColumnRecipe): undefined | ColumnDiscoveredRecip
151
154
  }
152
155
 
153
156
  /**
154
- * A recipe is a "leaf" (directly co-indexed on the anchor, reached without a
155
- * linker chain) iff its wrapper chain contains no {@link ColumnDiscoveredRecipe}.
157
+ * Whether the recipe reads exactly one data column — its own — rather than
158
+ * reaching through others.
159
+ *
160
+ * True for a bare leaf and for projections over one (spec overrides, axis
161
+ * slicing): those add layers but no new columns. False when the recipe gets at
162
+ * its data through further columns, which happens when discovery reached it via
163
+ * a linker chain.
156
164
  *
157
- * `Filtered` / `Overridden` over a bare leaf stay leaves; anything wrapping a
158
- * `Discovered` (e.g. `Overridden(Discovered(...))`) is linked. Use this — not an
159
- * `instanceof ColumnLazyImpl` check — to split direct vs. linker-joined columns,
160
- * since projections over a plain leaf are still direct.
165
+ * **Why callers care.** A recipe over a single data column is co-indexed by its
166
+ * own axes, so its spec is the whole truth about them. One that needs other
167
+ * columns only lines up after they are joined in, and therefore drags axes into
168
+ * the join that its own spec never mentions — which is why it cannot serve as a
169
+ * primary column, and why `createPlDataTableV3` splits on this.
170
+ *
171
+ * The count comes from {@link ColumnRecipe.getReferencedIds}, which resolves
172
+ * through the whole wrapper chain, so no class walk is involved and foreign
173
+ * `ColumnRecipe` implementations are handled too.
161
174
  */
162
- export function isLeafColumn(recipe: ColumnRecipe): boolean {
163
- return findDiscovered(recipe) === undefined;
175
+ export function hasSingleDataColumn(recipe: ColumnRecipe): boolean {
176
+ return recipe.getReferencedIds().length === 1;
164
177
  }
165
178
 
166
179
  /**
167
- * Data of the bare leaf a "leaf" recipe bottoms out at — reads the leaf-only
168
- * {@link ColumnLazy.getData} after walking the wrapper chain down via
169
- * {@link extractLeafColumn}. Returns `undefined` when the recipe is not a leaf (its
170
- * chain reaches a {@link ColumnDiscoveredRecipe}); the symmetric counterpart
171
- * of {@link isLeafColumn} returning `false`.
180
+ * @deprecated Renamed to {@link hasSingleDataColumn} — "leaf" read as "you can
181
+ * get data out of it", which is a different question (see
182
+ * {@link hasReachableData}). Same semantics.
172
183
  */
173
- export function getLeafColumnData(recipe: ColumnRecipe): ColumnLazyData {
174
- if (!isLeafColumn(recipe)) {
175
- throw new Error(`getLeafColumnData: recipe ${recipe.id} is not a leaf column`);
176
- }
177
- return extractLeafColumn(recipe)?.getData();
178
- }
184
+ export const isLeafColumn = hasSingleDataColumn;
179
185
 
180
186
  /**
181
- * Walk wrapper layers (Overridden, Filtered) down to the bare {@link ColumnLazy}
182
- * leaf the chain bottoms out at. Returns `undefined` if the chain reaches a
183
- * {@link ColumnDiscoveredRecipe} instead — i.e. the recipe is not a leaf
184
- * ({@link isLeafColumn} would return `false`). Mirror of {@link findDiscovered}.
187
+ * Whether the column's data can be read here and now, consistent with its
188
+ * spec — narrows to {@link DataColumnRecipe}, which exposes `getData()`.
189
+ *
190
+ * True for a bare leaf and for a spec-override over a bare leaf. False for an
191
+ * axis-filtered recipe (its spec has dropped axes the underlying data still
192
+ * carries, so the two no longer line up) and for anything reached through
193
+ * other columns (nothing to read until the engine joins them). In both of
194
+ * those cases the data exists only engine-side: pass `recipe.id` to
195
+ * `createPFrame` / `createPTable` instead of trying to read it.
196
+ *
197
+ * The distinction is settled when the recipe is built, not here — recipes that
198
+ * carry data are instances of a class that declares `getData`. So a `true`
199
+ * result narrows to a value that really has the method, and there is no
200
+ * throwing path behind the guard.
185
201
  */
186
- function extractLeafColumn(recipe: ColumnRecipe): undefined | ColumnLazy {
187
- let current: ColumnRecipe = recipe;
188
- while (true) {
189
- if (current instanceof ColumnLazyImpl) return current;
190
- if (current instanceof ColumnOverriddenRecipe || current instanceof ColumnFilteredRecipe) {
191
- current = current.getInner();
192
- continue;
193
- }
194
- if (current instanceof ColumnDiscoveredRecipe) {
195
- throw new Error(`extractLeafColumn: recipe ${recipe.id} is not a leaf column`);
196
- }
197
- throw new Error(`extractLeafColumn: unrecognized recipe layer for ${recipe.id}`);
198
- }
202
+ export function hasReachableData(recipe: ColumnRecipe): recipe is DataColumnRecipe {
203
+ return recipe instanceof DataColumnImpl || recipe instanceof DataColumnOverriddenRecipe;
199
204
  }
200
205
 
201
206
  /** Drop-down option built over a {@link ColumnsCollection} — universal-id valued. */
@@ -9,7 +9,7 @@ import {
9
9
  type PTableColumnId,
10
10
  type SUniversalPColumnId,
11
11
  } from "@milaboratories/pl-model-common";
12
- import { ColumnLazyImpl } from "../../columns";
12
+ import { DataColumnImpl } from "../../columns";
13
13
  import type { ColumnRecipe } from "../../columns";
14
14
  import { createColumnResolver } from "./columnResolver";
15
15
 
@@ -20,7 +20,7 @@ function wrap(id: PObjectId | SUniversalPColumnId, tag: string): SUniversalPColu
20
20
  });
21
21
  }
22
22
 
23
- // ColumnLazy constructor pulls `getCfgRenderCtx()` from globalThis on some
23
+ // DataColumnRecipe constructor pulls `getCfgRenderCtx()` from globalThis on some
24
24
  // paths — keep a minimal mock the same way p_column_lazy.test.ts does.
25
25
  const mockCtx = {
26
26
  getAccessorHandleByName: () => undefined,
@@ -36,7 +36,7 @@ afterEach(() => {
36
36
 
37
37
  function lazyOf(id: PObjectId, axes: AxisSpec[] = []) {
38
38
  const spec = { kind: "PColumn", name: "stub", axesSpec: axes } as unknown as PColumnSpec;
39
- return ColumnLazyImpl.fromColumn({
39
+ return DataColumnImpl.fromColumn({
40
40
  id,
41
41
  spec,
42
42
  data: undefined,
@@ -10,7 +10,7 @@ import type { PColumnDataUniversal } from "../../../render";
10
10
  import { isFunction } from "es-toolkit";
11
11
  import type { PlDataTableFilters } from "../typesV8";
12
12
  import { createPTableDefV3 } from "./createPTableDefV3";
13
- import { ColumnLazyImpl } from "../../../columns";
13
+ import { DataColumn } from "../../../columns";
14
14
 
15
15
  export function createPTableDefV2(params: {
16
16
  columns: PColumn<undefined | PColumnDataUniversal>[];
@@ -33,8 +33,8 @@ export function createPTableDefV2(params: {
33
33
  secondaryColumns.push(...params.labelColumns);
34
34
 
35
35
  return createPTableDefV3({
36
- primary: coreColumns.map((column) => ColumnLazyImpl.fromColumn(column)),
37
- secondary: secondaryColumns.map((column) => ColumnLazyImpl.fromColumn(column)),
36
+ primary: coreColumns.map((column) => DataColumn.fromColumn(column)),
37
+ secondary: secondaryColumns.map((column) => DataColumn.fromColumn(column)),
38
38
  primaryJoinType: params.coreJoinType,
39
39
  filters: params.filters,
40
40
  sorting: params.sorting,
@@ -31,7 +31,7 @@ import { getMatchingLabelColumns } from "../labels";
31
31
  import { collectFilterSpecColumns, traverseFilterSpec } from "../../../filters/traverse";
32
32
  import { createPTableDefV2 } from "./createPTableDefV2";
33
33
  import { isColumnOptional } from "./utils";
34
- import { ColumnLazyImpl } from "../../../columns";
34
+ import { DataColumn } from "../../../columns";
35
35
  import { createColumnResolver, type ColumnResolver } from "../columnResolver";
36
36
  import { isNil, type Nil } from "@milaboratories/helpers";
37
37
 
@@ -83,7 +83,7 @@ export function createPlDataTableV2<A, U>(
83
83
  const fullColumns = [...columns, ...fullLabelColumns];
84
84
 
85
85
  const resolver = createColumnResolver(
86
- fullColumns.map((c) => ColumnLazyImpl.fromColumn(c)),
86
+ fullColumns.map((c) => DataColumn.fromColumn(c)),
87
87
  { warn: ctx.logWarn.bind(ctx) },
88
88
  );
89
89
 
@@ -27,7 +27,7 @@ import { upgradePlDataTableStateV2 } from "../state-migration";
27
27
  import type { PlDataTableStateV2 } from "../state-migration";
28
28
  import type { MatchingMode } from "@milaboratories/pl-model-common";
29
29
  import {
30
- isLeafColumn,
30
+ hasSingleDataColumn,
31
31
  hitQualifications,
32
32
  collectLinkerColumns,
33
33
  queriesQualifications,
@@ -261,7 +261,7 @@ function splitByTopology(columns: ColumnRecipe[]): {
261
261
  const direct: ColumnRecipe[] = [];
262
262
  const linked: ColumnRecipe[] = [];
263
263
  for (const c of columns) {
264
- if (isLeafColumn(c)) direct.push(c);
264
+ if (hasSingleDataColumn(c)) direct.push(c);
265
265
  else linked.push(c);
266
266
  }
267
267
  return { direct, linked };
@@ -7,7 +7,7 @@ import type {
7
7
  import { PColumnName } from "@milaboratories/pl-model-common";
8
8
  import type { RenderCtxBase } from "../../../render";
9
9
  import type { ColumnRecipe, ColumnsSource } from "../../../columns";
10
- import { ColumnsCollection, isLeafColumn } from "../../../columns";
10
+ import { ColumnsCollection, hasSingleDataColumn } from "../../../columns";
11
11
  import type { ColumnsSelectorConfig } from "./createPlDataTableV3";
12
12
 
13
13
  export type DiscoverTableColumnOptions = {
@@ -38,7 +38,7 @@ export function discoverTableColumns(
38
38
  const primary: ColumnRecipe[] = [];
39
39
  const secondary: ColumnRecipe[] = [];
40
40
  for (const col of discoveredColumns) {
41
- if (isLeafColumn(col)) primary.push(col);
41
+ if (hasSingleDataColumn(col)) primary.push(col);
42
42
  else secondary.push(col);
43
43
  }
44
44
  return { primary, secondary };