@finos/legend-cube-builder 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +60 -1
  2. package/docs/adding-an-operation.md +92 -0
  3. package/docs/hosting.md +79 -0
  4. package/docs/testing.md +83 -0
  5. package/lib/__lib__/LegendCubeHelpText.d.ts.map +1 -1
  6. package/lib/__lib__/LegendCubeHelpText.js +11 -3
  7. package/lib/__lib__/LegendCubeHelpText.js.map +1 -1
  8. package/lib/__lib__/LegendCubeLabels.d.ts +19 -0
  9. package/lib/__lib__/LegendCubeLabels.d.ts.map +1 -1
  10. package/lib/__lib__/LegendCubeLabels.js +19 -0
  11. package/lib/__lib__/LegendCubeLabels.js.map +1 -1
  12. package/lib/__test-utils__/CubeEngineTestSupport.d.ts +6 -0
  13. package/lib/__test-utils__/CubeEngineTestSupport.d.ts.map +1 -1
  14. package/lib/__test-utils__/CubeEngineTestSupport.js +30 -0
  15. package/lib/__test-utils__/CubeEngineTestSupport.js.map +1 -1
  16. package/lib/__test-utils__/CubeOperationsTestUtils.d.ts +42 -0
  17. package/lib/__test-utils__/CubeOperationsTestUtils.d.ts.map +1 -0
  18. package/lib/__test-utils__/CubeOperationsTestUtils.js +92 -0
  19. package/lib/__test-utils__/CubeOperationsTestUtils.js.map +1 -0
  20. package/lib/__test-utils__/FakeCubeEngine.d.ts.map +1 -1
  21. package/lib/__test-utils__/FakeCubeEngine.js +7 -1
  22. package/lib/__test-utils__/FakeCubeEngine.js.map +1 -1
  23. package/lib/components/CubeNodeIcon.d.ts.map +1 -1
  24. package/lib/components/CubeNodeIcon.js +12 -2
  25. package/lib/components/CubeNodeIcon.js.map +1 -1
  26. package/lib/components/canvas/CubeCanvas.d.ts.map +1 -1
  27. package/lib/components/canvas/CubeCanvas.js +5 -3
  28. package/lib/components/canvas/CubeCanvas.js.map +1 -1
  29. package/lib/components/canvas/CubeCanvasElements.d.ts +2 -0
  30. package/lib/components/canvas/CubeCanvasElements.d.ts.map +1 -1
  31. package/lib/components/canvas/CubeCanvasElements.js +2 -1
  32. package/lib/components/canvas/CubeCanvasElements.js.map +1 -1
  33. package/lib/components/canvas/CubeCanvasNode.d.ts.map +1 -1
  34. package/lib/components/canvas/CubeCanvasNode.js +5 -2
  35. package/lib/components/canvas/CubeCanvasNode.js.map +1 -1
  36. package/lib/components/editors/CubeColumnPicker.d.ts +9 -2
  37. package/lib/components/editors/CubeColumnPicker.d.ts.map +1 -1
  38. package/lib/components/editors/CubeColumnPicker.js +9 -4
  39. package/lib/components/editors/CubeColumnPicker.js.map +1 -1
  40. package/lib/components/editors/CubeDistinctEditor.d.ts +23 -0
  41. package/lib/components/editors/CubeDistinctEditor.d.ts.map +1 -0
  42. package/lib/components/editors/CubeDistinctEditor.js +24 -0
  43. package/lib/components/editors/CubeDistinctEditor.js.map +1 -0
  44. package/lib/components/editors/CubeIntegerField.d.ts +29 -0
  45. package/lib/components/editors/CubeIntegerField.d.ts.map +1 -0
  46. package/lib/components/editors/CubeIntegerField.js +30 -0
  47. package/lib/components/editors/CubeIntegerField.js.map +1 -0
  48. package/lib/components/editors/CubeJoinEditor.d.ts.map +1 -1
  49. package/lib/components/editors/CubeJoinEditor.js +11 -3
  50. package/lib/components/editors/CubeJoinEditor.js.map +1 -1
  51. package/lib/components/editors/CubeNodeEditorPanel.d.ts.map +1 -1
  52. package/lib/components/editors/CubeNodeEditorPanel.js +3 -2
  53. package/lib/components/editors/CubeNodeEditorPanel.js.map +1 -1
  54. package/lib/components/editors/CubeNodeEditorRegistry.d.ts.map +1 -1
  55. package/lib/components/editors/CubeNodeEditorRegistry.js +14 -1
  56. package/lib/components/editors/CubeNodeEditorRegistry.js.map +1 -1
  57. package/lib/components/editors/CubeRenameEditor.d.ts +26 -0
  58. package/lib/components/editors/CubeRenameEditor.d.ts.map +1 -0
  59. package/lib/components/editors/CubeRenameEditor.js +58 -0
  60. package/lib/components/editors/CubeRenameEditor.js.map +1 -0
  61. package/lib/components/editors/CubeRestrictEditor.d.ts +26 -0
  62. package/lib/components/editors/CubeRestrictEditor.d.ts.map +1 -0
  63. package/lib/components/editors/CubeRestrictEditor.js +23 -0
  64. package/lib/components/editors/CubeRestrictEditor.js.map +1 -0
  65. package/lib/components/editors/CubeRowCountEditor.d.ts +25 -0
  66. package/lib/components/editors/CubeRowCountEditor.d.ts.map +1 -0
  67. package/lib/components/editors/CubeRowCountEditor.js +38 -0
  68. package/lib/components/editors/CubeRowCountEditor.js.map +1 -0
  69. package/lib/components/editors/CubeSliceEditor.d.ts +25 -0
  70. package/lib/components/editors/CubeSliceEditor.d.ts.map +1 -0
  71. package/lib/components/editors/CubeSliceEditor.js +40 -0
  72. package/lib/components/editors/CubeSliceEditor.js.map +1 -0
  73. package/lib/components/editors/CubeSortEditor.d.ts +27 -0
  74. package/lib/components/editors/CubeSortEditor.d.ts.map +1 -0
  75. package/lib/components/editors/CubeSortEditor.js +72 -0
  76. package/lib/components/editors/CubeSortEditor.js.map +1 -0
  77. package/lib/components/editors/CubeSourceEditor.d.ts.map +1 -1
  78. package/lib/components/editors/CubeSourceEditor.js +1 -2
  79. package/lib/components/editors/CubeSourceEditor.js.map +1 -1
  80. package/lib/components/editors/CubeValueEditor.d.ts.map +1 -1
  81. package/lib/components/editors/CubeValueEditor.js +2 -1
  82. package/lib/components/editors/CubeValueEditor.js.map +1 -1
  83. package/lib/components/grid/CubeGridRegion.d.ts.map +1 -1
  84. package/lib/components/grid/CubeGridRegion.js +1 -1
  85. package/lib/components/grid/CubeGridRegion.js.map +1 -1
  86. package/lib/components/grid/CubeResultGrid.d.ts +14 -1
  87. package/lib/components/grid/CubeResultGrid.d.ts.map +1 -1
  88. package/lib/components/grid/CubeResultGrid.js +41 -5
  89. package/lib/components/grid/CubeResultGrid.js.map +1 -1
  90. package/lib/graph-manager/CubeEngine.d.ts +13 -0
  91. package/lib/graph-manager/CubeEngine.d.ts.map +1 -1
  92. package/lib/graph-manager/CubeEngine.js.map +1 -1
  93. package/lib/graph-manager/CubeModelOutlineHelper.d.ts +8 -0
  94. package/lib/graph-manager/CubeModelOutlineHelper.d.ts.map +1 -1
  95. package/lib/graph-manager/CubeModelOutlineHelper.js +21 -0
  96. package/lib/graph-manager/CubeModelOutlineHelper.js.map +1 -1
  97. package/lib/graph-manager/protocol/pure/v1/V1_CubeModelOutlineBuilder.d.ts.map +1 -1
  98. package/lib/graph-manager/protocol/pure/v1/V1_CubeModelOutlineBuilder.js +56 -3
  99. package/lib/graph-manager/protocol/pure/v1/V1_CubeModelOutlineBuilder.js.map +1 -1
  100. package/lib/graph-manager/protocol/pure/v1/__test-utils__/V1_CubeEngineTestUtils.d.ts.map +1 -1
  101. package/lib/graph-manager/protocol/pure/v1/__test-utils__/V1_CubeEngineTestUtils.js +3 -3
  102. package/lib/graph-manager/protocol/pure/v1/__test-utils__/V1_CubeEngineTestUtils.js.map +1 -1
  103. package/lib/index.css +1 -1
  104. package/lib/stores/CubeEditorState.d.ts +34 -4
  105. package/lib/stores/CubeEditorState.d.ts.map +1 -1
  106. package/lib/stores/CubeEditorState.js +87 -6
  107. package/lib/stores/CubeEditorState.js.map +1 -1
  108. package/lib/stores/CubeExecutionState.d.ts.map +1 -1
  109. package/lib/stores/CubeExecutionState.js +19 -3
  110. package/lib/stores/CubeExecutionState.js.map +1 -1
  111. package/lib/stores/CubeGridQuickActions.d.ts +40 -0
  112. package/lib/stores/CubeGridQuickActions.d.ts.map +1 -0
  113. package/lib/stores/CubeGridQuickActions.js +86 -0
  114. package/lib/stores/CubeGridQuickActions.js.map +1 -0
  115. package/lib/stores/CubeNodeEditorState.d.ts +19 -1
  116. package/lib/stores/CubeNodeEditorState.d.ts.map +1 -1
  117. package/lib/stores/CubeNodeEditorState.js +55 -0
  118. package/lib/stores/CubeNodeEditorState.js.map +1 -1
  119. package/lib/stores/CubeShowPureState.d.ts +4 -1
  120. package/lib/stores/CubeShowPureState.d.ts.map +1 -1
  121. package/lib/stores/CubeShowPureState.js +22 -5
  122. package/lib/stores/CubeShowPureState.js.map +1 -1
  123. package/lib/stores/editors/CubeIntegerText.d.ts +24 -0
  124. package/lib/stores/editors/CubeIntegerText.d.ts.map +1 -0
  125. package/lib/stores/editors/CubeIntegerText.js +33 -0
  126. package/lib/stores/editors/CubeIntegerText.js.map +1 -0
  127. package/lib/stores/editors/CubeJoinDraft.d.ts +13 -5
  128. package/lib/stores/editors/CubeJoinDraft.d.ts.map +1 -1
  129. package/lib/stores/editors/CubeJoinDraft.js +21 -12
  130. package/lib/stores/editors/CubeJoinDraft.js.map +1 -1
  131. package/lib/stores/editors/CubeNodeDraftRegistry.d.ts +6 -0
  132. package/lib/stores/editors/CubeNodeDraftRegistry.d.ts.map +1 -1
  133. package/lib/stores/editors/CubeNodeDraftRegistry.js +23 -1
  134. package/lib/stores/editors/CubeNodeDraftRegistry.js.map +1 -1
  135. package/lib/stores/editors/CubeRenameDraft.d.ts +44 -0
  136. package/lib/stores/editors/CubeRenameDraft.d.ts.map +1 -0
  137. package/lib/stores/editors/CubeRenameDraft.js +79 -0
  138. package/lib/stores/editors/CubeRenameDraft.js.map +1 -0
  139. package/lib/stores/editors/CubeRestrictDraft.d.ts +37 -0
  140. package/lib/stores/editors/CubeRestrictDraft.d.ts.map +1 -0
  141. package/lib/stores/editors/CubeRestrictDraft.js +73 -0
  142. package/lib/stores/editors/CubeRestrictDraft.js.map +1 -0
  143. package/lib/stores/editors/CubeRowCountDraft.d.ts +42 -0
  144. package/lib/stores/editors/CubeRowCountDraft.d.ts.map +1 -0
  145. package/lib/stores/editors/CubeRowCountDraft.js +55 -0
  146. package/lib/stores/editors/CubeRowCountDraft.js.map +1 -0
  147. package/lib/stores/editors/CubeSliceDraft.d.ts +42 -0
  148. package/lib/stores/editors/CubeSliceDraft.d.ts.map +1 -0
  149. package/lib/stores/editors/CubeSliceDraft.js +72 -0
  150. package/lib/stores/editors/CubeSliceDraft.js.map +1 -0
  151. package/lib/stores/editors/CubeSortDraft.d.ts +49 -0
  152. package/lib/stores/editors/CubeSortDraft.d.ts.map +1 -0
  153. package/lib/stores/editors/CubeSortDraft.js +99 -0
  154. package/lib/stores/editors/CubeSortDraft.js.map +1 -0
  155. package/package.json +8 -8
  156. package/src/__lib__/LegendCubeHelpText.ts +22 -2
  157. package/src/__lib__/LegendCubeLabels.ts +30 -0
  158. package/src/__test-utils__/CubeEngineTestSupport.ts +35 -0
  159. package/src/__test-utils__/CubeOperationsTestUtils.ts +177 -0
  160. package/src/__test-utils__/FakeCubeEngine.ts +7 -1
  161. package/src/components/CubeNodeIcon.tsx +18 -1
  162. package/src/components/canvas/CubeCanvas.tsx +5 -3
  163. package/src/components/canvas/CubeCanvasElements.ts +4 -1
  164. package/src/components/canvas/CubeCanvasNode.tsx +9 -1
  165. package/src/components/editors/CubeColumnPicker.tsx +36 -12
  166. package/src/components/editors/CubeDistinctEditor.tsx +28 -0
  167. package/src/components/editors/CubeIntegerField.tsx +51 -0
  168. package/src/components/editors/CubeJoinEditor.tsx +26 -3
  169. package/src/components/editors/CubeNodeEditorPanel.tsx +12 -1
  170. package/src/components/editors/CubeNodeEditorRegistry.ts +20 -0
  171. package/src/components/editors/CubeRenameEditor.tsx +143 -0
  172. package/src/components/editors/CubeRestrictEditor.tsx +103 -0
  173. package/src/components/editors/CubeRowCountEditor.tsx +53 -0
  174. package/src/components/editors/CubeSliceEditor.tsx +69 -0
  175. package/src/components/editors/CubeSortEditor.tsx +206 -0
  176. package/src/components/editors/CubeSourceEditor.tsx +0 -10
  177. package/src/components/editors/CubeValueEditor.tsx +2 -1
  178. package/src/components/grid/CubeGridRegion.tsx +1 -0
  179. package/src/components/grid/CubeResultGrid.tsx +59 -5
  180. package/src/graph-manager/CubeEngine.ts +14 -0
  181. package/src/graph-manager/CubeModelOutlineHelper.ts +26 -0
  182. package/src/graph-manager/protocol/pure/v1/V1_CubeModelOutlineBuilder.ts +89 -3
  183. package/src/graph-manager/protocol/pure/v1/__test-utils__/V1_CubeEngineTestUtils.ts +6 -3
  184. package/src/stores/CubeEditorState.ts +110 -5
  185. package/src/stores/CubeExecutionState.ts +36 -3
  186. package/src/stores/CubeGridQuickActions.ts +129 -0
  187. package/src/stores/CubeNodeEditorState.ts +70 -1
  188. package/src/stores/CubeShowPureState.ts +27 -5
  189. package/src/stores/editors/CubeIntegerText.ts +34 -0
  190. package/src/stores/editors/CubeJoinDraft.ts +43 -16
  191. package/src/stores/editors/CubeNodeDraftRegistry.ts +35 -1
  192. package/src/stores/editors/CubeRenameDraft.ts +110 -0
  193. package/src/stores/editors/CubeRestrictDraft.ts +83 -0
  194. package/src/stores/editors/CubeRowCountDraft.ts +69 -0
  195. package/src/stores/editors/CubeSliceDraft.ts +87 -0
  196. package/src/stores/editors/CubeSortDraft.ts +135 -0
  197. package/tsconfig.json +15 -0
@@ -14,7 +14,14 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import type { NodeRegistry, QueryNode } from '@finos/legend-cube';
17
+ import {
18
+ canFixJoinDuplicates,
19
+ fixJoinDuplicates,
20
+ type NodeRegistry,
21
+ type Query,
22
+ type QueryNode,
23
+ type Schema,
24
+ } from '@finos/legend-cube';
18
25
  import {
19
26
  action,
20
27
  computed,
@@ -87,8 +94,10 @@ export class CubeNodeEditorState {
87
94
  hasChanges: computed,
88
95
  open: action,
89
96
  canSwapInputs: computed,
97
+ canRenameDuplicateColumns: computed,
90
98
  apply: action,
91
99
  swapInputs: action,
100
+ renameDuplicateColumns: action,
92
101
  close: action,
93
102
  cancel: action,
94
103
  discard: action,
@@ -190,6 +199,66 @@ export class CubeNodeEditorState {
190
199
  this.bind(query.getNode(node.id) ?? node);
191
200
  }
192
201
 
202
+ /**
203
+ * The cube's query with the draft in place of its node, when it has
204
+ * changes, and the schemas of the node's inputs, which the draft doesn't
205
+ * change
206
+ */
207
+ private get queryWithEdits(): {
208
+ query: Query;
209
+ inputSchemas: (Schema | undefined)[];
210
+ } {
211
+ const { node, edited } = this;
212
+ let { query } = this.editorState.document;
213
+ if (edited && this.hasChanges && query.canReplace(edited)) {
214
+ query = query.replace(edited);
215
+ }
216
+ return {
217
+ query,
218
+ inputSchemas: node
219
+ ? query
220
+ .getInputIds(node.id)
221
+ .map((inputId) =>
222
+ inputId === undefined
223
+ ? undefined
224
+ : this.editorState.analysis.schemas.get(inputId),
225
+ )
226
+ : [],
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Whether the join being edited, edits included, can have the columns its
232
+ * inputs share renamed (spec §7.11's autofix, PLAN §11.4): its only problem
233
+ * is the duplicate rule, and the cube isn't read-only
234
+ */
235
+ get canRenameDuplicateColumns(): boolean {
236
+ const { node } = this;
237
+ if (!node || node.key !== this.nodeKey || this.editorState.readOnly) {
238
+ return false;
239
+ }
240
+ const { query, inputSchemas } = this.queryWithEdits;
241
+ const [leftSchema, rightSchema] = inputSchemas;
242
+ return canFixJoinDuplicates(query, node.id, leftSchema, rightSchema);
243
+ }
244
+
245
+ /**
246
+ * Renames the columns the join's inputs share with a Rename before each
247
+ * input, applying the draft first, as one undo step, then goes on editing
248
+ * the join, whose keys follow the renames
249
+ */
250
+ renameDuplicateColumns(): void {
251
+ const { node } = this;
252
+ if (!node || !this.canRenameDuplicateColumns) {
253
+ return;
254
+ }
255
+ const { query, inputSchemas } = this.queryWithEdits;
256
+ const [leftSchema, rightSchema] = inputSchemas;
257
+ const fixed = fixJoinDuplicates(query, node.id, leftSchema, rightSchema);
258
+ this.editorState.applyQuery(fixed);
259
+ this.bind(fixed.getNode(node.id) ?? node);
260
+ }
261
+
193
262
  /** Applies the draft, then closes the panel (spec §17.5: edits commit on close) */
194
263
  close(): void {
195
264
  this.apply();
@@ -14,8 +14,9 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
+ import { needsDatabaseType } from '@finos/legend-cube';
17
18
  import type { GeneratorFn } from '@finos/legend-shared';
18
- import { action, flow, makeObservable, observable } from 'mobx';
19
+ import { action, flow, flowResult, makeObservable, observable } from 'mobx';
19
20
  import {
20
21
  CubeEngineError,
21
22
  CubeEngineErrorKind,
@@ -50,11 +51,18 @@ export class CubeShowPureState {
50
51
  this.editorState = editorState;
51
52
  }
52
53
 
53
- /** Opens the dialog and renders the query; does nothing when Execute can't run */
54
+ /**
55
+ * Opens the dialog and renders the query, written for the database it runs
56
+ * on, as Execute writes it; does nothing when Execute can't run
57
+ */
54
58
  *open(): GeneratorFn<void> {
55
59
  const { execution, document, emitter, rowLimit, host } = this.editorState;
56
- const runtime = document.context?.runtime;
57
- if (!execution.canExecute || runtime === undefined) {
60
+ const { query, context } = document;
61
+ const runtime = context?.runtime;
62
+ const model = context?.model;
63
+ // checked by canExecute
64
+ const captureId = query.selected as string;
65
+ if (!execution.canExecute || runtime === undefined || model === undefined) {
58
66
  return;
59
67
  }
60
68
  const request = ++this.request;
@@ -63,8 +71,22 @@ export class CubeShowPureState {
63
71
  this.error = undefined;
64
72
  this.isRendering = true;
65
73
  try {
74
+ let databaseType: string | undefined;
75
+ if (needsDatabaseType(query, captureId)) {
76
+ yield flowResult(this.editorState.loadModelOutline(model));
77
+ // closed, or opened again, while the outline loaded
78
+ if (request !== this.request) {
79
+ return;
80
+ }
81
+ databaseType = this.editorState.getRunDatabaseType(
82
+ query,
83
+ captureId,
84
+ model,
85
+ runtime,
86
+ );
87
+ }
66
88
  const text = (yield host.engine.renderPure(
67
- emitter.emitExecutionLambda({ rowLimit, runtime }),
89
+ emitter.emitExecutionLambda({ rowLimit, runtime, databaseType }),
68
90
  )) as string;
69
91
  if (request === this.request) {
70
92
  this.text = text;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Copyright (c) 2026-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ /** Whole-number text: an optional sign, then digits */
18
+ const WHOLE_NUMBER_TEXT = /^[+-]?\d+$/u;
19
+
20
+ /**
21
+ * The number a size or row index field holds. Text that isn't a whole number,
22
+ * empty text included, gives `undefined`, which the node then reports: never
23
+ * a default. It doesn't use `Number()` on the raw text, which reads `''` as 0,
24
+ * `0x10` as 16 and `1e3` as 1000. Spaces around the digits are allowed; a
25
+ * number too long for a double gives `undefined`.
26
+ */
27
+ export const parseWholeNumberText = (text: string): number | undefined => {
28
+ const trimmed = text.trim();
29
+ if (!WHOLE_NUMBER_TEXT.test(trimmed)) {
30
+ return undefined;
31
+ }
32
+ const value = Number(trimmed);
33
+ return Number.isFinite(value) ? value : undefined;
34
+ };
@@ -20,6 +20,7 @@ import {
20
20
  JoinType,
21
21
  type Query,
22
22
  RelationalTableSource,
23
+ Rename,
23
24
  type SchemaInferenceResult,
24
25
  } from '@finos/legend-cube';
25
26
  import { action, makeObservable, observable } from 'mobx';
@@ -138,28 +139,42 @@ export class CubeJoinDraft extends CubeNodeDraft<Join> {
138
139
  }
139
140
  }
140
141
 
142
+ /** Where a column of a node's output comes from: a table, and the column's name in it */
143
+ export interface CubeColumnOrigin {
144
+ readonly source: RelationalTableSource;
145
+ readonly column: string;
146
+ }
147
+
141
148
  /**
142
- * The table sources a column of a node's output comes from: followed back
143
- * through the inputs whose output has the column. A join key of the same name
144
- * on both sides comes from the side the join keeps (the left for INNER and
145
- * LEFT OUTER, the right for RIGHT OUTER), and from both for FULL OUTER, which
146
- * merges them.
149
+ * The table columns a column of a node's output comes from: followed back
150
+ * through the inputs whose output has it, and through a Rename to its old
151
+ * name. A join key of the same name on both sides comes from the side the
152
+ * join keeps (the left for INNER and LEFT OUTER, the right for RIGHT OUTER),
153
+ * and from both for FULL OUTER, which merges them. A column the node's output
154
+ * doesn't have, e.g. one a Restrict dropped, comes from nowhere.
147
155
  */
148
- export const findColumnSources = (
156
+ export const findColumnOrigins = (
149
157
  query: Query,
150
158
  analysis: SchemaInferenceResult,
151
159
  nodeId: string,
152
160
  columnName: string,
153
- ): RelationalTableSource[] => {
161
+ ): CubeColumnOrigin[] => {
154
162
  const node = query.getNode(nodeId);
155
163
  if (node instanceof RelationalTableSource) {
156
164
  return node.resolution.kind === 'resolved' &&
157
165
  node.resolution.schema.lookup(columnName)
158
- ? [node]
166
+ ? [{ source: node, column: columnName }]
159
167
  : [];
160
168
  }
169
+ if (!node || !analysis.schemas.get(nodeId)?.lookup(columnName)) {
170
+ return [];
171
+ }
161
172
  const inputIds = query.getInputIds(nodeId);
162
173
  const [leftId, rightId] = inputIds;
174
+ const inputName =
175
+ node instanceof Rename
176
+ ? (node.mappings.find(({ to }) => to === columnName)?.from ?? columnName)
177
+ : columnName;
163
178
  const followed =
164
179
  node instanceof Join &&
165
180
  node.joinType !== JoinType.FULL_OUTER &&
@@ -169,12 +184,23 @@ export const findColumnSources = (
169
184
  ? [node.joinType === JoinType.RIGHT_OUTER ? rightId : leftId]
170
185
  : inputIds;
171
186
  return followed.flatMap((inputId) =>
172
- inputId !== undefined && analysis.schemas.get(inputId)?.lookup(columnName)
173
- ? findColumnSources(query, analysis, inputId, columnName)
187
+ inputId !== undefined && analysis.schemas.get(inputId)?.lookup(inputName)
188
+ ? findColumnOrigins(query, analysis, inputId, inputName)
174
189
  : [],
175
190
  );
176
191
  };
177
192
 
193
+ /** The table sources a column of a node's output comes from (`findColumnOrigins`) */
194
+ export const findColumnSources = (
195
+ query: Query,
196
+ analysis: SchemaInferenceResult,
197
+ nodeId: string,
198
+ columnName: string,
199
+ ): RelationalTableSource[] =>
200
+ findColumnOrigins(query, analysis, nodeId, columnName).map(
201
+ ({ source }) => source,
202
+ );
203
+
178
204
  /**
179
205
  * Whether the column of the node's output comes from a table column whose
180
206
  * type Cube can't read (OTHER or ARRAY), so it was typed as a bare String
@@ -188,10 +214,11 @@ export const isUntypedColumn = (
188
214
  columnName: string,
189
215
  ): boolean =>
190
216
  outline !== undefined &&
191
- findColumnSources(query, analysis, nodeId, columnName).some((source) =>
192
- outline.databases
193
- .find((database) => database.path === source.database)
194
- ?.schemas.find((schema) => schema.name === source.schema)
195
- ?.tables.find((table) => table.name === source.table)
196
- ?.untypedColumns.includes(columnName),
217
+ findColumnOrigins(query, analysis, nodeId, columnName).some(
218
+ ({ source, column }) =>
219
+ outline.databases
220
+ .find((database) => database.path === source.database)
221
+ ?.schemas.find((schema) => schema.name === source.schema)
222
+ ?.tables.find((table) => table.name === source.table)
223
+ ?.untypedColumns.includes(column),
197
224
  );
@@ -14,12 +14,28 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { Filter, Join, type QueryNode } from '@finos/legend-cube';
17
+ import {
18
+ Distinct,
19
+ Drop,
20
+ Filter,
21
+ Join,
22
+ Limit,
23
+ type QueryNode,
24
+ Rename,
25
+ Restrict,
26
+ Slice,
27
+ Sort,
28
+ } from '@finos/legend-cube';
18
29
  import { guaranteeType } from '@finos/legend-shared';
19
30
  import type { CubeEditorState } from '../CubeEditorState.js';
20
31
  import { CubeFilterDraft } from './CubeFilterDraft.js';
21
32
  import { CubeJoinDraft } from './CubeJoinDraft.js';
22
33
  import { type CubeNodeDraft, CubeReadOnlyNodeDraft } from './CubeNodeDraft.js';
34
+ import { CubeRenameDraft } from './CubeRenameDraft.js';
35
+ import { CubeRestrictDraft } from './CubeRestrictDraft.js';
36
+ import { CubeRowCountDraft } from './CubeRowCountDraft.js';
37
+ import { CubeSliceDraft } from './CubeSliceDraft.js';
38
+ import { CubeSortDraft } from './CubeSortDraft.js';
23
39
 
24
40
  /** Makes the draft of a node of one type */
25
41
  export type CubeNodeDraftFactory = (
@@ -36,10 +52,28 @@ export const CUBE_NODE_DRAFT_FACTORIES: ReadonlyMap<
36
52
  string,
37
53
  CubeNodeDraftFactory
38
54
  > = new Map<string, CubeNodeDraftFactory>([
55
+ [Sort.TYPE, (node) => new CubeSortDraft(guaranteeType(node, Sort))],
39
56
  [Filter.TYPE, (node) => new CubeFilterDraft(guaranteeType(node, Filter))],
57
+ [
58
+ Restrict.TYPE,
59
+ (node) => new CubeRestrictDraft(guaranteeType(node, Restrict)),
60
+ ],
61
+ [Rename.TYPE, (node) => new CubeRenameDraft(guaranteeType(node, Rename))],
40
62
  [Join.TYPE, (node) => new CubeJoinDraft(guaranteeType(node, Join))],
63
+ [Drop.TYPE, (node) => new CubeRowCountDraft(guaranteeType(node, Drop))],
64
+ [Limit.TYPE, (node) => new CubeRowCountDraft(guaranteeType(node, Limit))],
65
+ [Slice.TYPE, (node) => new CubeSliceDraft(guaranteeType(node, Slice))],
41
66
  ]);
42
67
 
68
+ /**
69
+ * The transforms with nothing to set (spec §17.6: Distinct is "description
70
+ * only"): an editor in `CUBE_NODE_EDITORS` and no draft factory, so the panel
71
+ * shows no Apply or Cancel (PLAN §7.4 item 2)
72
+ */
73
+ export const CUBE_NODE_TYPES_WITHOUT_SETTINGS: readonly string[] = [
74
+ Distinct.TYPE,
75
+ ];
76
+
43
77
  /** The node's draft; a node with nothing to edit gets a read-only one */
44
78
  export const createCubeNodeDraft = (
45
79
  node: QueryNode,
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Copyright (c) 2026-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import type { Rename, RenameMapping } from '@finos/legend-cube';
18
+ import { action, makeObservable, observable } from 'mobx';
19
+ import { CubeNodeDraft } from './CubeNodeDraft.js';
20
+
21
+ /** One row of the Rename editor: an old column and its new name, `''` until picked or typed */
22
+ export interface CubeRenameRow {
23
+ /** Identifies the row in the editor */
24
+ readonly key: number;
25
+ readonly from: string;
26
+ readonly to: string;
27
+ }
28
+
29
+ let nextRowKey = 1;
30
+
31
+ const blankRow = (): CubeRenameRow => ({ key: nextRowKey++, from: '', to: '' });
32
+
33
+ const isSameMappings = (
34
+ a: readonly RenameMapping[],
35
+ b: readonly RenameMapping[],
36
+ ): boolean =>
37
+ a.length === b.length &&
38
+ a.every(
39
+ (mapping, index) =>
40
+ mapping.from === b[index]?.from && mapping.to === b[index]?.to,
41
+ );
42
+
43
+ /**
44
+ * The Rename editor's draft (spec §17.6: a list of old column and new name
45
+ * rows). A new name is kept exactly as typed, never trimmed. A row with
46
+ * neither name is left out when built, as a Filter's blank rows are; a row
47
+ * with only one is kept, so validation reports it.
48
+ */
49
+ export class CubeRenameDraft extends CubeNodeDraft<Rename> {
50
+ rows: readonly CubeRenameRow[];
51
+ /** Anything was changed; until then, `build()` gives the original back */
52
+ private touched = false;
53
+
54
+ constructor(original: Rename) {
55
+ super(original);
56
+ makeObservable<CubeRenameDraft, 'touched'>(this, {
57
+ rows: observable.ref,
58
+ touched: observable,
59
+ addRow: action,
60
+ removeRow: action,
61
+ setFrom: action,
62
+ setTo: action,
63
+ });
64
+ this.rows = original.mappings.length
65
+ ? original.mappings.map(({ from, to }) => ({
66
+ key: nextRowKey++,
67
+ from,
68
+ to,
69
+ }))
70
+ : [blankRow()];
71
+ }
72
+
73
+ /** The mappings the rows build, in order: a row with neither name is left out */
74
+ get mappings(): RenameMapping[] {
75
+ return this.rows
76
+ .filter((row) => row.from || row.to)
77
+ .map(({ from, to }) => ({ from, to }));
78
+ }
79
+
80
+ addRow(): void {
81
+ this.rows = [...this.rows, blankRow()];
82
+ this.touched = true;
83
+ }
84
+
85
+ removeRow(key: number): void {
86
+ this.rows = this.rows.filter((row) => row.key !== key);
87
+ this.touched = true;
88
+ }
89
+
90
+ setFrom(key: number, from: string): void {
91
+ this.rows = this.rows.map((row) =>
92
+ row.key === key ? { ...row, from } : row,
93
+ );
94
+ this.touched = true;
95
+ }
96
+
97
+ setTo(key: number, to: string): void {
98
+ this.rows = this.rows.map((row) =>
99
+ row.key === key ? { ...row, to } : row,
100
+ );
101
+ this.touched = true;
102
+ }
103
+
104
+ build(): Rename {
105
+ const { original, mappings } = this;
106
+ return !this.touched || isSameMappings(mappings, original.mappings)
107
+ ? original
108
+ : original.withMappings(mappings);
109
+ }
110
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Copyright (c) 2026-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import type { Restrict, Schema } from '@finos/legend-cube';
18
+ import { action, makeObservable, observable } from 'mobx';
19
+ import { CubeNodeDraft } from './CubeNodeDraft.js';
20
+
21
+ /**
22
+ * The columns in the input's order, then those the input doesn't have, in
23
+ * the order they were picked: a saved column the input lost stays listed
24
+ * until it is unticked
25
+ */
26
+ const inInputOrder = (names: readonly string[], schema: Schema): string[] => [
27
+ ...schema.names().filter((name) => names.includes(name)),
28
+ ...names.filter((name) => schema.lookup(name) === undefined),
29
+ ];
30
+
31
+ const isSameSet = (a: readonly string[], b: readonly string[]): boolean =>
32
+ a.length === b.length && a.every((name) => b.includes(name));
33
+
34
+ /**
35
+ * The Restrict editor's draft (spec §17.6: a multi-select of the columns to
36
+ * keep). It keeps the picked columns in the input's order, as the node's
37
+ * output has them.
38
+ */
39
+ export class CubeRestrictDraft extends CubeNodeDraft<Restrict> {
40
+ columns: readonly string[];
41
+ /** Anything was changed; until then, `build()` gives the original back */
42
+ private touched = false;
43
+
44
+ constructor(original: Restrict) {
45
+ super(original);
46
+ makeObservable<CubeRestrictDraft, 'touched'>(this, {
47
+ columns: observable.ref,
48
+ touched: observable,
49
+ toggleColumn: action,
50
+ selectAll: action,
51
+ clear: action,
52
+ });
53
+ this.columns = original.columns;
54
+ }
55
+
56
+ /** Ticks or unticks the column */
57
+ toggleColumn(name: string, schema: Schema): void {
58
+ this.columns = this.columns.includes(name)
59
+ ? this.columns.filter((picked) => picked !== name)
60
+ : inInputOrder([...this.columns, name], schema);
61
+ this.touched = true;
62
+ }
63
+
64
+ /** Ticks every column of the input */
65
+ selectAll(schema: Schema): void {
66
+ this.columns = inInputOrder([...schema.names(), ...this.columns], schema);
67
+ this.touched = true;
68
+ }
69
+
70
+ /** Unticks every column */
71
+ clear(): void {
72
+ this.columns = [];
73
+ this.touched = true;
74
+ }
75
+
76
+ /** The original while the same columns are picked, whatever their order */
77
+ build(): Restrict {
78
+ const { original, columns } = this;
79
+ return !this.touched || isSameSet(columns, original.columns)
80
+ ? original
81
+ : original.withColumns(columns);
82
+ }
83
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Copyright (c) 2026-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import type { QueryNode } from '@finos/legend-cube';
18
+ import { action, makeObservable, observable } from 'mobx';
19
+ import { parseWholeNumberText } from './CubeIntegerText.js';
20
+ import { CubeNodeDraft } from './CubeNodeDraft.js';
21
+
22
+ /** A node whose one setting is a number of rows: a Limit or a Drop */
23
+ export interface CubeRowCountNode<N extends QueryNode> extends QueryNode {
24
+ readonly size: number | undefined;
25
+ withSize(size: number | undefined): N;
26
+ }
27
+
28
+ /**
29
+ * The draft of a node whose one setting is a number of rows (spec §17.6: "one
30
+ * integer field"). It keeps the text as typed. Text that isn't a whole
31
+ * number, empty text included, builds a cleared size, which the node reports:
32
+ * never the default (spec §7.7). The text it opened with gives the original
33
+ * back, so a saved size the field can't read (`1.5` from an imported spec) is
34
+ * kept unless the user types something else.
35
+ */
36
+ export class CubeRowCountDraft<
37
+ N extends CubeRowCountNode<N>,
38
+ > extends CubeNodeDraft<N> {
39
+ sizeText: string;
40
+ /** The text the field opened with, which builds the original */
41
+ private readonly initialText: string;
42
+
43
+ constructor(original: N) {
44
+ super(original);
45
+ makeObservable(this, {
46
+ sizeText: observable,
47
+ setSizeText: action,
48
+ });
49
+ this.initialText = original.size === undefined ? '' : String(original.size);
50
+ this.sizeText = this.initialText;
51
+ }
52
+
53
+ /** The size the text gives: `undefined` unless it is a whole number */
54
+ get size(): number | undefined {
55
+ return parseWholeNumberText(this.sizeText);
56
+ }
57
+
58
+ setSizeText(text: string): void {
59
+ this.sizeText = text;
60
+ }
61
+
62
+ /** The original while the field holds its opening text or the same size */
63
+ build(): N {
64
+ const { original, size } = this;
65
+ return this.sizeText.trim() === this.initialText || size === original.size
66
+ ? original
67
+ : original.withSize(size);
68
+ }
69
+ }