@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
@@ -19,16 +19,59 @@ import {
19
19
  DataGrid,
20
20
  type DataGridCellRendererParams,
21
21
  type DataGridColumnDefinition,
22
+ type DataGridDefaultMenuItem,
23
+ type DataGridGetContextMenuItemsParams,
24
+ type DataGridMenuItemDef,
22
25
  } from '@finos/legend-lego/data-grid';
23
- import { memo, useMemo } from 'react';
26
+ import { memo, useCallback, useMemo } from 'react';
24
27
  import { NULL_CELL_TEXT } from '../../__lib__/LegendCubeLabels.js';
25
28
  import { LEGEND_CUBE_TEST_ID } from '../../__lib__/LegendCubeTesting.js';
26
29
  import type { CubeResultValue } from '../../graph-manager/CubeEngine.js';
30
+ import type { CubeEditorState } from '../../stores/CubeEditorState.js';
27
31
  import type { CubeExecutionResult } from '../../stores/CubeExecutionState.js';
32
+ import { getCubeGridQuickActions } from '../../stores/CubeGridQuickActions.js';
28
33
  import { compareCellValues, isNumericFamily } from './CubeGridValues.js';
29
34
 
30
35
  type CubeRow = readonly CubeResultValue[];
31
36
 
37
+ /** A column's id in the grid: its position, as `c<position>` */
38
+ const COLUMN_ID_PREFIX = 'c';
39
+
40
+ /**
41
+ * The grid's context menu (spec §12.4): on a cell, its quick actions, then
42
+ * ag-grid's own items, which M7 curates; elsewhere ag-grid's only. Read when
43
+ * the menu opens, so it reflects the cube and the run as they are then.
44
+ */
45
+ export const getCubeGridContextMenuItems = (
46
+ editorState: CubeEditorState,
47
+ params: DataGridGetContextMenuItemsParams<CubeRow>,
48
+ ): (DataGridDefaultMenuItem | DataGridMenuItemDef<CubeRow>)[] => {
49
+ const defaultItems = params.defaultItems ?? [];
50
+ const colId = params.column?.getColId();
51
+ if (!colId?.startsWith(COLUMN_ID_PREFIX)) {
52
+ return defaultItems;
53
+ }
54
+ const position = Number(colId.slice(COLUMN_ID_PREFIX.length));
55
+ const cell = params.node?.data?.[position] ?? null;
56
+ const actions = getCubeGridQuickActions(editorState, position, cell);
57
+ if (!actions.length) {
58
+ return defaultItems;
59
+ }
60
+ return [
61
+ ...actions.map((quickAction): DataGridMenuItemDef<CubeRow> => {
62
+ const tooltip = quickAction.disabledReason ?? quickAction.hint;
63
+ return {
64
+ name: quickAction.label,
65
+ disabled: quickAction.disabledReason !== undefined,
66
+ ...(tooltip === undefined ? {} : { tooltip }),
67
+ action: () => quickAction.apply(),
68
+ };
69
+ }),
70
+ 'separator',
71
+ ...defaultItems,
72
+ ];
73
+ };
74
+
32
75
  const CubeCell: React.FC<
33
76
  DataGridCellRendererParams<CubeRow, CubeResultValue>
34
77
  > = (props) =>
@@ -41,17 +84,27 @@ const CubeCell: React.FC<
41
84
  /**
42
85
  * The rows of a run (PLAN §9): columns from Cube's own schema of the capture
43
86
  * node, by position, so any column name works. Numbers are right-aligned and
44
- * shown and sorted exactly as the engine wrote them.
87
+ * shown and sorted exactly as the engine wrote them. A cell's context menu
88
+ * offers the quick actions (`getCubeGridContextMenuItems`).
45
89
  */
46
90
  export const CubeResultGrid = memo(
47
- (props: { result: CubeExecutionResult; darkMode: boolean }) => {
48
- const { result, darkMode } = props;
91
+ (props: {
92
+ editorState: CubeEditorState;
93
+ result: CubeExecutionResult;
94
+ darkMode: boolean;
95
+ }) => {
96
+ const { editorState, result, darkMode } = props;
97
+ const getContextMenuItems = useCallback(
98
+ (params: DataGridGetContextMenuItemsParams<CubeRow>) =>
99
+ getCubeGridContextMenuItems(editorState, params),
100
+ [editorState],
101
+ );
49
102
  const columnDefs = useMemo(
50
103
  (): DataGridColumnDefinition<CubeRow, CubeResultValue>[] =>
51
104
  result.schema.columns.map((column, position) => {
52
105
  const numeric = isNumericFamily(column.type.family);
53
106
  return {
54
- colId: `c${position}`,
107
+ colId: `${COLUMN_ID_PREFIX}${position}`,
55
108
  headerName: column.name,
56
109
  headerTooltip: `${column.type.displayName}${column.nullable ? '?' : ''} (${column.type.fullName})`,
57
110
  valueGetter: (params) => params.data?.[position] ?? null,
@@ -76,6 +129,7 @@ export const CubeResultGrid = memo(
76
129
  columnDefs={columnDefs}
77
130
  suppressFieldDotNotation={true}
78
131
  tooltipShowDelay={500}
132
+ getContextMenuItems={getContextMenuItems}
79
133
  />
80
134
  </div>
81
135
  );
@@ -64,10 +64,24 @@ export interface CubeOutlineDatabase {
64
64
  readonly schemas: readonly CubeOutlineSchema[];
65
65
  }
66
66
 
67
+ /** A relational connection of a runtime, for the operations some databases take another way (PLAN §11.4) */
68
+ export interface CubeOutlineConnection {
69
+ /** The store it is keyed by */
70
+ readonly storePath: string;
71
+ /** The engine's name for its database type, e.g. `H2` or `SqlServer` */
72
+ readonly databaseType: string;
73
+ }
74
+
67
75
  export interface CubeOutlineRuntime {
68
76
  readonly path: string;
69
77
  /** The stores its connections are keyed by, exactly; includes are not followed (PLAN §6.2.5) */
70
78
  readonly storePaths: readonly string[];
79
+ /**
80
+ * Its relational connections that name a database type, a list rather
81
+ * than a record keyed by a store path from the model; optional, as an
82
+ * outline from before M2 has none
83
+ */
84
+ readonly connections?: readonly CubeOutlineConnection[];
71
85
  }
72
86
 
73
87
  /** What a model offers the source picker: its databases and runtimes, as plain data */
@@ -30,3 +30,29 @@ export const getRuntimesForDatabase = (
30
30
  outline.runtimes.filter((runtime) =>
31
31
  runtime.storePaths.includes(databasePath),
32
32
  );
33
+
34
+ /**
35
+ * The database type a query on these databases runs on with the runtime, for
36
+ * the operations some databases take another way (PLAN §11.4): the type of
37
+ * the runtime's connections to them. None when the runtime isn't in the
38
+ * outline, a database has no typed connection, or the databases' types
39
+ * differ: the native forms are written then.
40
+ */
41
+ export const getDatabaseType = (
42
+ outline: CubeModelOutline,
43
+ runtimePath: string,
44
+ databasePaths: readonly string[],
45
+ ): string | undefined => {
46
+ const runtime = outline.runtimes.find(({ path }) => path === runtimePath);
47
+ const types = new Set<string>();
48
+ for (const databasePath of new Set(databasePaths)) {
49
+ const databaseTypes = (runtime?.connections ?? [])
50
+ .filter(({ storePath }) => storePath === databasePath)
51
+ .map(({ databaseType }) => databaseType);
52
+ if (!databaseTypes.length) {
53
+ return undefined;
54
+ }
55
+ databaseTypes.forEach((databaseType) => types.add(databaseType));
56
+ }
57
+ return types.size === 1 ? [...types][0] : undefined;
58
+ };
@@ -19,6 +19,7 @@ import type { PlainObject } from '@finos/legend-shared';
19
19
  import {
20
20
  CubeTableFlag,
21
21
  type CubeModelOutline,
22
+ type CubeOutlineConnection,
22
23
  type CubeOutlineDatabase,
23
24
  type CubeOutlineRuntime,
24
25
  type CubeOutlineSchema,
@@ -32,6 +33,9 @@ import {
32
33
 
33
34
  const DATABASE_TYPE = 'relational';
34
35
  const RUNTIME_TYPE = 'runtime';
36
+ const CONNECTION_TYPE = 'connection';
37
+ const CONNECTION_POINTER_TYPE = 'connectionPointer';
38
+ const RELATIONAL_CONNECTION_TYPE = 'RelationalDatabaseConnection';
35
39
  /** The only runtime value whose store keys Cube can read; others are hidden */
36
40
  const ENGINE_RUNTIME_TYPE = 'engineRuntime';
37
41
 
@@ -117,8 +121,34 @@ const buildDatabase = (element: PlainObject): CubeOutlineDatabase => ({
117
121
  schemas: asList(element.schemas).map(buildSchema),
118
122
  });
119
123
 
124
+ /** The database type of a relational connection value: its `type`, else its `databaseType` */
125
+ const getRelationalDatabaseType = (value: PlainObject): string | undefined =>
126
+ value._type === RELATIONAL_CONNECTION_TYPE
127
+ ? (asString(value.type) ?? asString(value.databaseType))
128
+ : undefined;
129
+
130
+ /**
131
+ * The database type of a runtime's connection, embedded or a pointer to a
132
+ * connection element (`connectionTypes`, by path); none for one that isn't
133
+ * relational
134
+ */
135
+ const getConnectionDatabaseType = (
136
+ json: unknown,
137
+ connectionTypes: ReadonlyMap<string, string>,
138
+ ): string | undefined => {
139
+ const connection = asObject(json);
140
+ if (connection._type === CONNECTION_POINTER_TYPE) {
141
+ const path = asString(connection.connection);
142
+ return path === undefined ? undefined : connectionTypes.get(path);
143
+ }
144
+ return getRelationalDatabaseType(connection);
145
+ };
146
+
120
147
  /** The stores a runtime's connections are keyed by, in both syntaxes; none when it can't be read */
121
- const buildRuntime = (element: PlainObject): CubeOutlineRuntime | undefined => {
148
+ const buildRuntime = (
149
+ element: PlainObject,
150
+ connectionTypes: ReadonlyMap<string, string>,
151
+ ): CubeOutlineRuntime | undefined => {
122
152
  const value = asObject(element.runtimeValue);
123
153
  if (value._type !== ENGINE_RUNTIME_TYPE) {
124
154
  return undefined;
@@ -135,7 +165,51 @@ const buildRuntime = (element: PlainObject): CubeOutlineRuntime | undefined => {
135
165
  ),
136
166
  ),
137
167
  ].filter((path): path is string => path !== undefined);
138
- return { path: elementPath(element), storePaths: [...new Set(storePaths)] };
168
+ const connections: CubeOutlineConnection[] = [];
169
+ const addConnection = (
170
+ storePath: string | undefined,
171
+ databaseType: string | undefined,
172
+ ): void => {
173
+ if (
174
+ storePath !== undefined &&
175
+ databaseType !== undefined &&
176
+ !connections.some(
177
+ (connection) =>
178
+ connection.storePath === storePath &&
179
+ connection.databaseType === databaseType,
180
+ )
181
+ ) {
182
+ connections.push({ storePath, databaseType });
183
+ }
184
+ };
185
+ asList(value.connections).forEach((json) => {
186
+ const storeConnections = asObject(json);
187
+ const storePath = asString(asObject(storeConnections.store).path);
188
+ asList(storeConnections.storeConnections).forEach((identified) =>
189
+ addConnection(
190
+ storePath,
191
+ getConnectionDatabaseType(
192
+ asObject(identified).connection,
193
+ connectionTypes,
194
+ ),
195
+ ),
196
+ );
197
+ });
198
+ asList(value.connectionStores).forEach((json) => {
199
+ const connectionStore = asObject(json);
200
+ const databaseType = getConnectionDatabaseType(
201
+ connectionStore.connectionPointer,
202
+ connectionTypes,
203
+ );
204
+ asList(connectionStore.storePointers).forEach((pointer) =>
205
+ addConnection(asString(asObject(pointer).path), databaseType),
206
+ );
207
+ });
208
+ return {
209
+ path: elementPath(element),
210
+ storePaths: [...new Set(storePaths)],
211
+ connections,
212
+ };
139
213
  };
140
214
 
141
215
  /** The outline of a parsed model: its Database elements and its readable runtimes, in model order */
@@ -143,13 +217,25 @@ export const V1_buildCubeModelOutline = (
143
217
  modelData: PlainObject,
144
218
  ): CubeModelOutline => {
145
219
  const elements = asList(modelData.elements).map(asObject);
220
+ // the database type of each relational connection element, by path
221
+ const connectionTypes = new Map<string, string>();
222
+ elements
223
+ .filter((element) => element._type === CONNECTION_TYPE)
224
+ .forEach((element) => {
225
+ const databaseType = getRelationalDatabaseType(
226
+ asObject(element.connectionValue),
227
+ );
228
+ if (databaseType !== undefined) {
229
+ connectionTypes.set(elementPath(element), databaseType);
230
+ }
231
+ });
146
232
  return {
147
233
  databases: elements
148
234
  .filter((element) => element._type === DATABASE_TYPE)
149
235
  .map(buildDatabase),
150
236
  runtimes: elements
151
237
  .filter((element) => element._type === RUNTIME_TYPE)
152
- .map(buildRuntime)
238
+ .map((element) => buildRuntime(element, connectionTypes))
153
239
  .filter(
154
240
  (runtime): runtime is CubeOutlineRuntime => runtime !== undefined,
155
241
  ),
@@ -15,7 +15,10 @@
15
15
  */
16
16
 
17
17
  import { jest } from '@jest/globals';
18
- import { ENGINE_TEST_SUPPORT__grammarToJSON_model } from '@finos/legend-graph/test';
18
+ import {
19
+ ENGINE_TEST_SUPPORT__grammarToJSON_model,
20
+ ENGINE_TEST_SUPPORT_API_URL,
21
+ } from '@finos/legend-graph/test';
19
22
  import {
20
23
  NetworkClientError,
21
24
  type PlainObject,
@@ -40,7 +43,7 @@ const clientError = (status: number, payload: unknown): NetworkClientError =>
40
43
  {
41
44
  status,
42
45
  statusText: String(status),
43
- url: 'http://localhost:6300/api',
46
+ url: ENGINE_TEST_SUPPORT_API_URL,
44
47
  } as Response,
45
48
  payload as NetworkClientError['payload'],
46
49
  );
@@ -74,7 +77,7 @@ export const V1_createEngineBackedCubeEngine = (): {
74
77
  >;
75
78
  } => {
76
79
  const engine = new V1_LegendCubeEngine(
77
- { baseUrl: 'http://localhost:6300/api' },
80
+ { baseUrl: ENGINE_TEST_SUPPORT_API_URL },
78
81
  new TracerService(),
79
82
  );
80
83
  const { client } = engine;
@@ -19,6 +19,10 @@ import {
19
19
  createNodeRegistry,
20
20
  CubeDocument,
21
21
  diffSchemas,
22
+ findLostSortOrders,
23
+ MESSAGE_SORT_COLUMNS_CUT,
24
+ MESSAGE_SORT_COLUMNS_DROPPED,
25
+ MESSAGE_SORT_ORDER_LOST,
22
26
  type ModelContext,
23
27
  type NodeRegistry,
24
28
  type Query,
@@ -53,6 +57,7 @@ import {
53
57
  CubeEngineErrorKind,
54
58
  type CubeModelOutline,
55
59
  } from '../graph-manager/CubeEngine.js';
60
+ import { getDatabaseType } from '../graph-manager/CubeModelOutlineHelper.js';
56
61
  import { CubeExecutionState } from './CubeExecutionState.js';
57
62
  import type { CubeHost } from './CubeHost.js';
58
63
  import { CubeNodeEditorState } from './CubeNodeEditorState.js';
@@ -138,6 +143,7 @@ export class CubeEditorState implements CommandRegistrar {
138
143
  history: observable.ref,
139
144
  hostIssues: observable.ref,
140
145
  warnings: observable.ref,
146
+ derivedWarnings: computed,
141
147
  isResolvingSources: computed,
142
148
  rowLimit: observable,
143
149
  isPaletteCollapsed: observable,
@@ -155,6 +161,7 @@ export class CubeEditorState implements CommandRegistrar {
155
161
  select: action,
156
162
  connect: action,
157
163
  addNode: action,
164
+ addConfiguredNode: action,
158
165
  dropNode: action,
159
166
  removeNode: action,
160
167
  swapInputs: action,
@@ -208,6 +215,52 @@ export class CubeEditorState implements CommandRegistrar {
208
215
  return this.pendingSources.has(node);
209
216
  }
210
217
 
218
+ /**
219
+ * Warnings worked out from the query, by node id: a Sort whose order is
220
+ * lost before it is used (PLAN §11.4). Never stored and never errors, so
221
+ * Execute stays enabled; they go as soon as the query no longer loses it.
222
+ * A partial loss names each node that removes some of the Sort's columns,
223
+ * then the columns that came after a removed one. A loss waits until the
224
+ * Sort and every node it names have no errors, e.g. a Restrict just added,
225
+ * with no column yet: their own errors come first.
226
+ */
227
+ get derivedWarnings(): ReadonlyMap<string, readonly string[]> {
228
+ const { validity, schemas } = this.analysis;
229
+ const isValid = (nodeId: string): boolean =>
230
+ validity.get(nodeId)?.length === 0;
231
+ const { query } = this.document;
232
+ return new Map(
233
+ Array.from(findLostSortOrders(query, undefined, schemas))
234
+ .filter(
235
+ ([sortId, loss]) =>
236
+ isValid(sortId) &&
237
+ isValid(loss.nodeId) &&
238
+ (loss.removals ?? []).every(({ nodeId }) => isValid(nodeId)),
239
+ )
240
+ .map(([sortId, loss]) => [
241
+ sortId,
242
+ loss.removals
243
+ ? [
244
+ ...loss.removals.map(({ nodeId, columns }) =>
245
+ MESSAGE_SORT_COLUMNS_DROPPED(columns, nodeId),
246
+ ),
247
+ ...(loss.cutColumns?.length
248
+ ? [MESSAGE_SORT_COLUMNS_CUT(loss.cutColumns)]
249
+ : []),
250
+ ]
251
+ : [MESSAGE_SORT_ORDER_LOST(loss.nodeId)],
252
+ ]),
253
+ );
254
+ }
255
+
256
+ /** A node's warnings: the stored ones (by key), then the derived ones (by id) */
257
+ getNodeWarnings(node: QueryNode): readonly string[] {
258
+ return [
259
+ ...(this.warnings.get(node.key) ?? []),
260
+ ...(this.derivedWarnings.get(node.id) ?? []),
261
+ ];
262
+ }
263
+
211
264
  /**
212
265
  * The node's errors, each once: its own and those of the query rules, then
213
266
  * the first line of the engine's error on it
@@ -225,15 +278,53 @@ export class CubeEditorState implements CommandRegistrar {
225
278
  /** The outline of the cube's model, once an editor has loaded it */
226
279
  get modelOutline(): CubeModelOutline | undefined {
227
280
  const model = this.document.context?.model;
228
- return model === undefined ? undefined : this.modelOutlines.get(model);
281
+ return model === undefined ? undefined : this.getModelOutline(model);
282
+ }
283
+
284
+ /** The outline of a model, once loaded */
285
+ getModelOutline(model: ModelContext): CubeModelOutline | undefined {
286
+ return this.modelOutlines.get(model);
229
287
  }
230
288
 
231
289
  /**
232
- * Loads the outline of the cube's model, once per model. It only adds
233
- * warnings, so a model that fails to load shows none.
290
+ * The database type a run of the query up to the node needs, with the
291
+ * model's runtime (PLAN §11.4), from the model's outline once it is loaded
292
+ * (`loadModelOutline`): the type of the runtime's connections to the
293
+ * databases the node reads. None without an outline, which writes every
294
+ * operation the native way.
234
295
  */
235
- *loadModelOutline(): GeneratorFn<void> {
236
- const model = this.document.context?.model;
296
+ getRunDatabaseType(
297
+ query: Query,
298
+ nodeId: string,
299
+ model: ModelContext,
300
+ runtime: string,
301
+ ): string | undefined {
302
+ const outline = this.getModelOutline(model);
303
+ if (!outline) {
304
+ return undefined;
305
+ }
306
+ const databases = new Set<string>();
307
+ const visit = (id: string): void => {
308
+ const node = query.getNode(id);
309
+ if (node instanceof RelationalTableSource) {
310
+ databases.add(node.database);
311
+ }
312
+ query
313
+ .getInputIds(id)
314
+ .forEach((inputId) => inputId !== undefined && visit(inputId));
315
+ };
316
+ visit(nodeId);
317
+ return getDatabaseType(outline, runtime, [...databases]);
318
+ }
319
+
320
+ /**
321
+ * Loads the outline of a model, the cube's by default, once per model. It
322
+ * only adds warnings and database types, so a model that fails to load
323
+ * shows none and runs every operation the native way.
324
+ */
325
+ *loadModelOutline(
326
+ model: ModelContext | undefined = this.document.context?.model,
327
+ ): GeneratorFn<void> {
237
328
  if (model === undefined || this.modelOutlines.has(model)) {
238
329
  return;
239
330
  }
@@ -585,6 +676,20 @@ export class CubeEditorState implements CommandRegistrar {
585
676
  );
586
677
  }
587
678
 
679
+ /**
680
+ * Adds a node made elsewhere, with its settings, as the grid's quick
681
+ * actions do (spec §12.4): spliced in after `afterId`, so it becomes the
682
+ * node that runs when that one did, as one undo step. Does nothing in a
683
+ * read-only cube, or when the query can't take it.
684
+ */
685
+ addConfiguredNode(node: QueryNode, afterId?: string): void {
686
+ const { query } = this.document;
687
+ if (this.readOnly || !query.canAdd(node, afterId)) {
688
+ return;
689
+ }
690
+ this.applyQuery(query.add(node, afterId));
691
+ }
692
+
588
693
  /** Whether dropping a node on another does anything: connect it, or else move it after it */
589
694
  canDropNode(nodeId: string, targetId: string): boolean {
590
695
  const { query } = this.document;
@@ -14,9 +14,21 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { isSchemasError, type Query, type Schema } from '@finos/legend-cube';
17
+ import {
18
+ isSchemasError,
19
+ needsDatabaseType,
20
+ type Query,
21
+ type Schema,
22
+ } from '@finos/legend-cube';
18
23
  import { assertErrorThrown, type GeneratorFn } from '@finos/legend-shared';
19
- import { action, computed, flow, makeObservable, observable } from 'mobx';
24
+ import {
25
+ action,
26
+ computed,
27
+ flow,
28
+ flowResult,
29
+ makeObservable,
30
+ observable,
31
+ } from 'mobx';
20
32
  import {
21
33
  CubeEngineError,
22
34
  CubeEngineErrorKind,
@@ -162,12 +174,33 @@ export class CubeExecutionState {
162
174
  if (!model || !runtime || !schema) {
163
175
  return;
164
176
  }
165
- const lambda = emitter.emitExecutionLambda({ rowLimit, runtime });
177
+ // the run starts before any wait, so Stop works and a second F9 is ignored
166
178
  const controller = new AbortController();
167
179
  this.runController = controller;
168
180
  this.error = undefined;
169
181
  this.editorState.clearHostIssues();
170
182
  try {
183
+ // a Drop, Slice, Limit or Distinct is written for the database it runs on
184
+ // (PLAN §11.4), which the model's outline gives: loaded once, waited
185
+ // for only then; the query, model and runtime are the ones captured
186
+ let databaseType: string | undefined;
187
+ if (needsDatabaseType(query, captureId)) {
188
+ yield flowResult(this.editorState.loadModelOutline(model));
189
+ if (this.runController !== controller || controller.signal.aborted) {
190
+ return;
191
+ }
192
+ databaseType = this.editorState.getRunDatabaseType(
193
+ query,
194
+ captureId,
195
+ model,
196
+ runtime,
197
+ );
198
+ }
199
+ const lambda = emitter.emitExecutionLambda({
200
+ rowLimit,
201
+ runtime,
202
+ databaseType,
203
+ });
171
204
  const response = (yield host.engine.execute(model, lambda, {
172
205
  abortController: controller,
173
206
  })) as CubeResult;
@@ -0,0 +1,129 @@
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 {
18
+ buildQuickFilterRule,
19
+ FilterOperator,
20
+ Filter,
21
+ isOperatorAvailable,
22
+ isSortableType,
23
+ Sort,
24
+ TypeFamily,
25
+ } from '@finos/legend-cube';
26
+ import {
27
+ CUBE_QUICK_ACTION_DISABLED_REASON,
28
+ FILTER_FLOAT_COMPARISON_HINT,
29
+ READ_ONLY_CUBE_TITLE,
30
+ } from '../__lib__/LegendCubeLabels.js';
31
+ import type { CubeResultValue } from '../graph-manager/CubeEngine.js';
32
+ import type { CubeEditorState } from './CubeEditorState.js';
33
+
34
+ /** One of the grid's quick actions on a cell (spec §12.4) */
35
+ export interface CubeGridQuickAction {
36
+ /** e.g. `Sort by "SHIP_COUNTRY"` */
37
+ readonly label: string;
38
+ /** Why it can't be used, its tooltip then; `undefined` when it can */
39
+ readonly disabledReason: string | undefined;
40
+ /** A note shown as its tooltip when it can be used, e.g. on comparing floats */
41
+ readonly hint: string | undefined;
42
+ /** Adds its node after the node that runs, as one undo step; nothing runs */
43
+ readonly apply: () => void;
44
+ }
45
+
46
+ /**
47
+ * The quick actions on a cell of the shown rows (spec §12.4, PLAN §11.4):
48
+ * "Sort by" adds an ascending Sort on the cell's column, and "Filter by" an
49
+ * Equal on its value (Is Empty on a null), after the node that runs, which
50
+ * they replace as the node that runs. Both need the rows to be from the
51
+ * current query, no run in flight, and a cube that can change; "Sort by" a
52
+ * type that sorts, and "Filter by" a value its column's type can compare.
53
+ * Read when the menu opens; `[]` when the position is not a column of the
54
+ * shown rows.
55
+ */
56
+ export const getCubeGridQuickActions = (
57
+ editorState: CubeEditorState,
58
+ position: number,
59
+ cell: CubeResultValue,
60
+ ): CubeGridQuickAction[] => {
61
+ const { execution, document } = editorState;
62
+ const { result } = execution;
63
+ const column = result?.schema.columns[position];
64
+ if (!result || !column) {
65
+ return [];
66
+ }
67
+ const { query } = document;
68
+ const afterId = query.selected;
69
+ const typeName = column.type.displayName;
70
+ const rule = buildQuickFilterRule(column.name, column.type, cell);
71
+ // what keeps both from being used, in order
72
+ const blocked =
73
+ result.query !== query
74
+ ? CUBE_QUICK_ACTION_DISABLED_REASON.STALE_ROWS
75
+ : execution.isRunning
76
+ ? CUBE_QUICK_ACTION_DISABLED_REASON.RUNNING
77
+ : editorState.readOnly
78
+ ? READ_ONLY_CUBE_TITLE
79
+ : undefined;
80
+ const sortReason =
81
+ blocked ??
82
+ (isSortableType(column.type)
83
+ ? undefined
84
+ : CUBE_QUICK_ACTION_DISABLED_REASON.notSortable(typeName));
85
+ const filterReason =
86
+ blocked ??
87
+ (rule
88
+ ? undefined
89
+ : isOperatorAvailable(FilterOperator.EQUAL, column.type)
90
+ ? CUBE_QUICK_ACTION_DISABLED_REASON.UNREADABLE_VALUE
91
+ : CUBE_QUICK_ACTION_DISABLED_REASON.notComparable(typeName));
92
+ return [
93
+ {
94
+ label: `Sort by "${column.name}"`,
95
+ disabledReason: sortReason,
96
+ hint: undefined,
97
+ apply: () => {
98
+ // as the menu found it: nothing changed since it opened
99
+ if (sortReason === undefined && editorState.document.query === query) {
100
+ editorState.addConfiguredNode(
101
+ Sort.byColumn(query.generateId(Sort.TYPE), column.name),
102
+ afterId,
103
+ );
104
+ }
105
+ },
106
+ },
107
+ {
108
+ label: `Filter by "${column.name}"`,
109
+ disabledReason: filterReason,
110
+ hint:
111
+ rule?.operator === FilterOperator.EQUAL &&
112
+ column.type.family === TypeFamily.FLOAT
113
+ ? FILTER_FLOAT_COMPARISON_HINT
114
+ : undefined,
115
+ apply: () => {
116
+ if (
117
+ filterReason === undefined &&
118
+ rule &&
119
+ editorState.document.query === query
120
+ ) {
121
+ editorState.addConfiguredNode(
122
+ new Filter(query.generateId(Filter.TYPE), rule),
123
+ afterId,
124
+ );
125
+ }
126
+ },
127
+ },
128
+ ];
129
+ };