Skip to content

Commit

Permalink
[MODFQMMGR-389] Migration service implementation (#336)
Browse files Browse the repository at this point in the history
* [MODFQMMGR-389] Migration service interface but string

* Bump query processor to snapshot

* Architecture

* Migration helper

* First two entity types

* docblock

* staticize

* docblocks

* adjust template to be a bit less heavy on creation

* qol improvements

* Remove translations from removed entity types

* More qol

* completed mappings

* oops, those are important

* Fix tests

* optimize

* Basic migration strategy tester

* AbstractSimpleMigrationStrategy tests

* add tests for all but poc migration logic

* Full migration service test

* poc migration tests

* Initial warnings infrastructure

* typo

* lombok constructors

* Add warnings class implementations tests

* abstract strategy coverage goes wheee

* dedupe

* Test entity type warnings

* test new migration warning logic

* re-abstract
  • Loading branch information
ncovercash authored Aug 2, 2024
1 parent 3a74b22 commit 351b93c
Show file tree
Hide file tree
Showing 32 changed files with 2,405 additions and 415 deletions.
3 changes: 3 additions & 0 deletions PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ If you are adding entity type(s), have you:
- [ ] Added views to liquibase, as applicable?
- [ ] Added required interfaces to the module descriptor?
- [ ] Checked that querying fields works correctly and all SQL is valid?

If you are changing/removing entity type(s), have you:
- [ ] Added migration code for any changes?
Binary file modified entity-type-creator/bun.lockb
Binary file not shown.
279 changes: 279 additions & 0 deletions entity-type-creator/components/MigrationHelper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
import { EntityType } from '@/types';
import { java } from '@codemirror/lang-java';
import { json } from '@codemirror/lang-json';
import { Button, Container, FormControlLabel, Grid, Switch } from '@mui/material';
import CodeMirror, { EditorView } from '@uiw/react-codemirror';
import { constantCase } from 'change-case';
import json5 from 'json5';
import { useEffect, useMemo, useRef, useState } from 'react';

export default function MigrationHelper() {
const [oldEntityType, setOldEntityType] = useState<EntityType | null>(null);
const [newEntityType, setNewEntityType] = useState<EntityType | null>(null);
const [columnMapping, setColumnMapping] = useState<Record<string, string>>({});

const canvasRef = useRef<HTMLCanvasElement>();

const [showMatched, setShowMatched] = useState(true);
const [showOnlyMatchingDataTypes, setShowOnlyMatchingDataTypes] = useState(false);
const [clicked, setClicked] = useState<string | null>(null);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });

const result = useMemo(() => {
let result = '';

const oldAndNewAreSame = oldEntityType?.id === newEntityType?.id;

const oldNameUnqualified = constantCase(oldEntityType?.name ?? 'FillMeIn');
const oldName = oldAndNewAreSame ? oldNameUnqualified : `OLD_${oldNameUnqualified}`;
const newNameUnqualified = constantCase(newEntityType?.name ?? 'FillMeIn');
const newName = oldAndNewAreSame ? newNameUnqualified : `NEW_${newNameUnqualified}`;

if (oldAndNewAreSame) {
result += `
private static final UUID ${oldName} = UUID.fromString("${oldEntityType?.id}");
`;
} else {
result += `
private static final UUID ${oldName} = UUID.fromString("${oldEntityType?.id}");
private static final UUID ${newName} = UUID.fromString("${newEntityType?.id}");
`;
}

const columnMapName = oldNameUnqualified + '_COLUMN_MAPPING';
result += `
private static final Map<String, String> ${columnMapName} = Map.ofEntries(
${Object.entries(columnMapping)
.toSorted(([a], [b]) => a.localeCompare(b))
.filter(([a, b]) => a !== b)
.map(([oldName, newName]) => `Map.entry("${oldName}", "${newName}")`)
.join(',\n ')}
);
`.trimStart();

if (oldAndNewAreSame) {
result += `
@Override
protected Map<UUID, UUID> getEntityTypeChanges() {
return Map.ofEntries();
}
`;
} else {
result += `
@Override
protected Map<UUID, UUID> getEntityTypeChanges() {
return Map.ofEntries(
Map.entry(${oldName}, ${newName})
);
}
`;
}

result += `
@Override
protected Map<UUID, Map<String, String>> getFieldChanges() {
return Map.ofEntries(
Map.entry(${oldName}, ${columnMapName})
);
}
`;

return result.trimStart();
}, [oldEntityType, newEntityType, columnMapping]);

useEffect(() => {
const canvas = canvasRef.current;
const context = canvasRef.current?.getContext('2d');
if (!canvas || !context) {
return;
}

let oldColumns = oldEntityType?.columns || [];
let newColumns = newEntityType?.columns || [];

if (!showMatched) {
oldColumns = oldColumns.filter((c) => !columnMapping[c.name]);
newColumns = newColumns.filter((c) => !Object.values(columnMapping).includes(c.name));
}

oldColumns = oldColumns.toSorted((a, b) => a.name.localeCompare(b.name));
newColumns = newColumns.toSorted((a, b) => a.name.localeCompare(b.name));

const clickedColumn = oldColumns.find((c) => c.name === clicked);
if (showOnlyMatchingDataTypes && clickedColumn) {
newColumns = newColumns.filter((column) => column.dataType.dataType === clickedColumn?.dataType.dataType);
}

const canvasWidth = canvas.getBoundingClientRect().width;
context.canvas.width = canvasWidth;

const canvasHeight = Math.max(oldColumns.length, newColumns.length) * 24 + 24 * 2;
context.canvas.height = canvasHeight;
context.canvas.style.height = canvasHeight + 'px';

context.reset();
context.font = '16px monospace';

const columnWidth = canvasWidth * 0.4;

context.textAlign = 'right';
context.font = 'bold 16px monospace';
context.fillText(oldEntityType?.name ?? 'Old Entity Type', columnWidth, 16, columnWidth);

context.font = '16px monospace';
for (let i = 0; i < oldColumns.length; i++) {
const column = oldColumns[i];

if (columnMapping[column.name]) {
context.fillStyle = 'grey';
} else {
context.fillStyle = 'black';
}

context.fillText(column.name, columnWidth, (i + 2) * 24, columnWidth);
context.strokeRect(1, (i + 2) * 24 - 16, columnWidth + 4, 24);
}

context.textAlign = 'left';
context.font = 'bold 16px monospace';
context.fillText(newEntityType?.name ?? 'New Entity Type', canvasWidth - columnWidth, 16, columnWidth);

for (let i = 0; i < newColumns.length; i++) {
const column = newColumns[i];

context.font = '16px monospace';
if (Object.values(columnMapping).includes(column.name)) {
context.fillStyle = 'grey';
} else if (column.dataType.dataType === clickedColumn?.dataType.dataType) {
context.font = 'bold 16px monospace';
context.fillStyle = 'green';
} else {
context.fillStyle = 'black';
}
context.fillText(column.name, canvasWidth - columnWidth, (i + 2) * 24, columnWidth);
context.strokeRect(canvasWidth - columnWidth - 4, (i + 2) * 24 - 16, columnWidth + 4, 24);
}

for (const [oldName, newName] of Object.entries(columnMapping)) {
const oldIndex = oldColumns.findIndex((c) => c.name === oldName);
const newIndex = newColumns.findIndex((c) => c.name === newName);

if (oldIndex >= 0 && newIndex >= 0) {
context.beginPath();
context.strokeStyle = 'black';
context.lineWidth = 2;
context.moveTo(columnWidth + 4, 24 * oldIndex - 4 + 48);
context.lineTo(canvasWidth - columnWidth - 4, 24 * newIndex - 4 + 48);
context.stroke();
}
}

if (clicked) {
context.beginPath();
context.strokeStyle = 'red';
context.lineWidth = 4;
context.moveTo(columnWidth + 4, 24 * oldColumns.findIndex((c) => c.name === clicked) - 4 + 48);
context.lineTo(mousePosition.x, mousePosition.y);
context.stroke();
}

context.canvas.onmousedown = (e) => {
// if old column, set clicked = that
const x = e.clientX - canvas.getBoundingClientRect().left;
const y = e.clientY - canvas.getBoundingClientRect().top;
const index = Math.floor((y - 28) / 24);

if (x < columnWidth && clicked !== null) {
delete columnMapping[clicked];
setColumnMapping({ ...columnMapping });
setClicked(null);
} else if (x < columnWidth) {
if (index >= 0 && index < oldColumns.length) {
setClicked(oldColumns[index].name);
}
} else if (clicked && x > canvasWidth - columnWidth) {
if (index >= 0 && index < newColumns.length) {
setColumnMapping((prev) => ({ ...prev, [clicked]: newColumns[index].name }));
setClicked(null);
}
}
};
}, [oldEntityType, newEntityType, columnMapping, mousePosition, clicked, showMatched, showOnlyMatchingDataTypes]);

return (
<Grid container spacing={2}>
<Grid item xs={6} sx={{ height: 300, overflow: 'auto' }}>
<label style={oldEntityType === null ? { color: 'red' } : {}}>Old entity type:</label>
<CodeMirror
onChange={(s) => {
try {
setOldEntityType(json5.parse(s));
} catch (e) {
setOldEntityType(null);
}
}}
extensions={[json(), EditorView.lineWrapping]}
/>
</Grid>
<Grid item xs={6} sx={{ height: 300, overflow: 'auto' }}>
<label style={newEntityType === null ? { color: 'red' } : {}}>New entity type, fully resolved:</label>
<CodeMirror
onChange={(s) => {
try {
setNewEntityType(json5.parse(s));
} catch (e) {
setNewEntityType(null);
}
}}
extensions={[json(), EditorView.lineWrapping]}
/>
</Grid>
<Grid item xs={12}>
<Container>
<Grid container>
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Button
color="error"
variant="outlined"
onClick={() => {
setColumnMapping({});
}}
>
reset
</Button>
<FormControlLabel
control={<Switch checked={showMatched} onChange={(e) => setShowMatched(e.target.checked)} />}
label="Show matched columns"
/>
<FormControlLabel
control={
<Switch
checked={showOnlyMatchingDataTypes}
onChange={(e) => setShowOnlyMatchingDataTypes(e.target.checked)}
/>
}
label="Show only matching data types when matching"
/>
</Grid>
<Grid item xs={12}>
<canvas
ref={canvasRef}
style={{ width: '100%', cursor: 'pointer' }}
onMouseMove={(e) => {
const rect = (e.target as HTMLCanvasElement).getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

setMousePosition({ x, y });
}}
/>
</Grid>
<Grid item xs={12}>
<CodeMirror value={result} readOnly extensions={[java(), EditorView.lineWrapping]} />
</Grid>
</Grid>
</Container>
</Grid>
</Grid>
);
}
1 change: 1 addition & 0 deletions entity-type-creator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@codemirror/lang-java": "^6.0.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-sql": "^6.6.2",
"@emotion/cache": "^11.11.0",
Expand Down
7 changes: 6 additions & 1 deletion entity-type-creator/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import CheckValidity from '@/components/CheckValidity';
import DBInspector from '@/components/DBInspector';
import EntityTypeManager from '@/components/EntityTypeManager';
import FqmConnector from '@/components/FqmConnector';
import MigrationHelper from '@/components/MigrationHelper';
import ModuleInstaller from '@/components/ModuleInstaller';
import PostgresConnector from '@/components/PostgresConnector';
import QueryTool from '@/components/QueryTool';
Expand Down Expand Up @@ -75,7 +76,7 @@ export default function EntryPoint() {
<Tabs
value={selectedTab}
onChange={(_e, n) => {
if (n === 5) {
if (n === 6) {
setExpandedBottom((e) => !e);
} else {
setSelectedTab(n);
Expand All @@ -88,6 +89,7 @@ export default function EntryPoint() {
<Tab label="Check Validity" />
<Tab label="Query Tool" />
<Tab label="DB Inspector" />
<Tab label="Migration Helper" />
<Tab label={expandedBottom ? 'Collapse' : 'Expand'} />
</Tabs>

Expand All @@ -104,6 +106,9 @@ export default function EntryPoint() {
<Box sx={{ display: selectedTab === 4 ? 'block' : 'none', p: 2 }}>
<DBInspector socket={socket} schema={schema} entityType={currentEntityType} />
</Box>
<Box sx={{ display: selectedTab === 5 ? 'block' : 'none', p: 2 }}>
<MigrationHelper />
</Box>
</Box>
</Box>
</Drawer>
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<folio-spring-base.version>8.1.0</folio-spring-base.version>
<folio-query-tool-metadata.version>2.1.0-SNAPSHOT</folio-query-tool-metadata.version>
<mapstruct.version>1.5.2.Final</mapstruct.version>
<lib-fqm-query-processor.version>2.0.0</lib-fqm-query-processor.version>
<lib-fqm-query-processor.version>2.1.0-SNAPSHOT</lib-fqm-query-processor.version>
<coffee-boots.version>4.0.0</coffee-boots.version>
<snakeyaml.version>2.0</snakeyaml.version>
<org.postgresql.version>42.5.4</org.postgresql.version>
Expand Down
Loading

0 comments on commit 351b93c

Please sign in to comment.