I recently had to model a data warehouse star schema. The central table of a data warehouse is called a fact table. This table has two kinds of columns: dimensions and facts. If I were modelling it from scratch, I could have used two separate child links for the different columns. In pseudocode:
concept FactTable
children:
- dimensions: DimensionColumn[0..n]
- facts: FactColumn[0..n]
However, in my case I had to make sure the FactTable
is a KernelF
record so it had to implement the IRecordDeclaration
interface concept:
interface concept IRecordDeclaration extends ...
children:
- members : IRecordMember[0..n]
Basically, I had to store two kinds of members in the members
link. Not a problem by itself, I can have both
DimensionColumn
and FactColumn
implement the IRecordMember
interface. However, besides being record members they
are actually quite different: a dimension column would reference a dimension from a catalog of all dimensions and
inherit that dimension’s name and type, whereas a fact column would specify its own name and type. The editors of
DimensionColumn
and FactColumn
would look quite different, and when creating an editor for the FactTable
concept I
would like to split the collection into two and have dimensions listed separately from facts.
Implementation
Let’s have a look at what functionality MPS provides to implement this split. In fact, you see an example of it every
time you work with MPS: a concept definition lets you define children and references separately but both of them are
actually stored in AbstractConceptDeclaration.linkDeclaration
containment link.
To do this we include the members
list twice in the editor:
Each instance specifies a filter, element factory, and element action map:
- We tell MPS that we only want to display a subset of all children.
- This piece of code is invoked when we create a new element by pressing
Enter
orShift+Enter
. We create an element of the specific subclass, so that it is displayed in the right place. - We specify an action map that handles the deletion of each element properly.
Element Deletion
The default behavior of MPS after an element is deleted is to select its nearest neighbor in the same containment link. However, in our case the nearest neighbor may be of a different kind so the cursor may jump into a different section. We need to write code to explicitly select the neighbor of the same kind or the appropriate placeholder cell if we deleted the last element of a particular kind.
The action map we use is pretty simple and calls out to a Java class. Moving the logic to a separate class lets us define helper methods.
The helper class looks as follows:
public class FactTableColumnDeletionHelper {
private static boolean canDelete(EditorContext editorContext, node<FactTableColumn> node) {
EditorCell nodeCell = editorContext.getEditorComponent().findNodeCell(node);
return nodeCell != null
&& !ReadOnlyUtil.isCellOrSelectionReadOnlyInEditor(editorContext.getEditorComponent(), nodeCell)
&& node.parent.isInstanceOf(FactTable);
}
private static [node<>, node<>] getPrevNext(node<> node, sequence<node<>> nodes) {
node<> prevNode = null;
node<> nextNode = null;
boolean nodeVisited = false;
foreach n in nodes {
if (n == node) {
nodeVisited = true;
continue;
}
if (!nodeVisited) {
prevNode = n;
} else {
nextNode = n;
break;
}
}
return [prevNode, nextNode];
}
public static void delete(EditorContext editorContext, node<FactTableColumn> node, boolean isBackspace) {
if (!canDelete(editorContext, node)) { return; }
if (node.approveDelete [in: editorContext]) { return; }
boolean isDimension = node.isInstanceOf(DimensionColumn);
node<FactTable> containing = node.parent:FactTable;
sequence<node<FactTableColumn>> similarMembers = containing.members.where({~it => isDimension ? it.isInstanceOf(DimensionColumn) : it.isInstanceOf(FactColumn); });
node<> prev;
node<> next;
[prev, next] = getPrevNext(node, similarMembers);
node.detach;
if (isBackspace) {
if (prev != null) {
prev.select[in: editorContext, cell: LAST, selectionStart: -1];
return;
}
if (next != null) {
next.select[in: editorContext, cell: FIRST, selectionStart: 0];
return;
}
} else {
if (next != null) {
next.select[in: editorContext, cell: FIRST, selectionStart: 0];
return;
}
if (prev != null) {
prev.select[in: editorContext, cell: LAST, selectionStart: -1];
return;
}
}
if (isDimension) {
containing.select[in: editorContext, cell: emptyDimensionsPlaceholder, selectionStart: 0];
} else {
containing.select[in: editorContext, cell: emptyValuesPlaceholder, selectionStart: 0];
}
}
}
The code is a simplified version of the code in LinkDeclarationsDeleteActions
action map (MPS
link). The overall algorithm is as follows:
- Check that the node and the cell is not read-only, otherwise don’t delete anything.
- Support two-step deletion: don’t do anything if the deletion wasn’t approved yet.
- Figure out our neighboring nodes so that we know which node to select after the current node will be gone.
- Remove (detach) the current node.
- Set the selection according to point 3.
To be able to select placeholder cells programmatically we need to create custom empty cells and give them explicit IDs (cell id property).
Now dimensions and facts can be edited as two separate lists even though there is only one underlying child link,
IRecordDeclaration.members
: