diff --git a/package-lock.json b/package-lock.json index 685fee2..9f66e86 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "jsonschema": "^1.4.1", "jspdf": "^2.5.1", "lexical": "^0.12.5", - "node-sql-parser": "^4.17.0", + "node-sql-parser": "^5.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.4.1", @@ -1715,6 +1715,11 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/pegjs": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/@types/pegjs/-/pegjs-0.10.6.tgz", + "integrity": "sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw==" + }, "node_modules/@types/prop-types": { "version": "15.7.11", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", @@ -4368,10 +4373,11 @@ "dev": true }, "node_modules/node-sql-parser": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.17.0.tgz", - "integrity": "sha512-3IhovpmUBpcETnoKK/KBdkz2mz53kVG5E1dnqz1QuYvtzdxYZW5xaGGEvW9u6Yyy2ivwR3eUZrn9inmEVef02w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-5.0.0.tgz", + "integrity": "sha512-hkNU1gIT8BNe8vmcsU7uYie0gzow/6AIj5KnGRBJQSZlgEu1NNuLVS11it5gAEdpmvJHelc34BwR439Iela+zQ==", "dependencies": { + "@types/pegjs": "^0.10.0", "big-integer": "^1.6.48" }, "engines": { diff --git a/package.json b/package.json index 875cdbc..fa19917 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "jsonschema": "^1.4.1", "jspdf": "^2.5.1", "lexical": "^0.12.5", - "node-sql-parser": "^4.17.0", + "node-sql-parser": "^5.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.4.1", diff --git a/src/components/EditorHeader/Modal/ImportSource.jsx b/src/components/EditorHeader/Modal/ImportSource.jsx index 1b86454..f5283ab 100644 --- a/src/components/EditorHeader/Modal/ImportSource.jsx +++ b/src/components/EditorHeader/Modal/ImportSource.jsx @@ -1,7 +1,12 @@ -import { Upload, Checkbox } from "@douyinfe/semi-ui"; +import { Upload, Checkbox, Banner } from "@douyinfe/semi-ui"; import { STATUS } from "../../../data/constants"; -export default function ImportSource({ importData, setImportData, setError }) { +export default function ImportSource({ + importData, + setImportData, + error, + setError, +}) { return (
Overwrite existing diagram +
+ {error.type === STATUS.ERROR ? ( + {error.message}
} + /> + ) : error.type === STATUS.OK ? ( + {error.message}
} + /> + ) : ( + error.type === STATUS.WARNING && ( + {error.message}} + /> + ) + )} + ); diff --git a/src/components/EditorHeader/Modal/Modal.jsx b/src/components/EditorHeader/Modal/Modal.jsx index 462503b..f2f870b 100644 --- a/src/components/EditorHeader/Modal/Modal.jsx +++ b/src/components/EditorHeader/Modal/Modal.jsx @@ -114,9 +114,17 @@ export default function Modal({ try { ast = parser.astify(importSource.src, { database: "MySQL" }); } catch (err) { - Toast.error( - "Could not parse the sql file. Make sure there are no syntax errors.", - ); + setError({ + type: STATUS.ERROR, + message: + err.name + + " [Ln " + + err.location.start.line + + ", Col " + + err.location.start.column + + "]: " + + err.message, + }); return; } @@ -124,6 +132,7 @@ export default function Modal({ if (importSource.overwrite) { setTables(d.tables); setRelationships(d.relationships); + setTransform((prev) => ({ ...prev, pan: { x: 0, y: 0 } })); setNotes([]); setAreas([]); setTypes([]); @@ -133,6 +142,7 @@ export default function Modal({ setTables((prev) => [...prev, ...d.tables]); setRelationships((prev) => [...prev, ...d.relationships]); } + setModal(MODAL.NONE); }; const createNewDiagram = (id) => { @@ -167,7 +177,6 @@ export default function Modal({ return; case MODAL.IMPORT_SRC: parseSQLAndLoadDiagram(); - setModal(MODAL.NONE); return; case MODAL.OPEN: if (selectedDiagramId === 0) return; @@ -207,6 +216,7 @@ export default function Modal({ ); diff --git a/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx b/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx index 30b7e33..ae574ba 100644 --- a/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx +++ b/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx @@ -80,7 +80,7 @@ export default function FieldDetails({ data, tid, index }) { undo: editField, redo: { values: data.values }, message: `Edit table field values to "${JSON.stringify( - data.values + data.values, )}"`, }, ]); @@ -123,9 +123,11 @@ export default function FieldDetails({ data, tid, index }) {
Precision
updateField(tid, index, { size: value })} diff --git a/src/utils/astToDiagram.js b/src/utils/astToDiagram.js index 2742219..92991ad 100644 --- a/src/utils/astToDiagram.js +++ b/src/utils/astToDiagram.js @@ -1,9 +1,49 @@ -import { Cardinality } from "../data/constants"; +import { + Cardinality, + tableColorStripHeight, + tableFieldHeight, + tableHeaderHeight, +} from "../data/constants"; + +function buildSQLFromAST(ast) { + if (ast.type === "binary_expr") { + const leftSQL = buildSQLFromAST(ast.left); + const rightSQL = buildSQLFromAST(ast.right); + return `${leftSQL} ${ast.operator} ${rightSQL}`; + } + + if (ast.type === "function") { + let expr = ""; + expr = ast.name; + if (ast.args) { + expr += + "(" + + ast.args.value + .map((v) => { + if (v.type === "column_ref") return "`" + v.column + "`"; + if ( + v.type === "single_quote_string" || + v.type === "double_quote_string" + ) + return "'" + v.value + "'"; + return v.value; + }) + .join(", ") + + ")"; + } + return expr; + } else if (ast.type === "column_ref") { + return "`" + ast.column + "`"; + } else if (ast.type === "expr_list") { + return ast.value.map((v) => v.value).join(" AND "); + } else { + return typeof ast.value === "string" ? "'" + ast.value + "'" : ast.value; + } +} export function astToDiagram(ast) { const tables = []; const relationships = []; - const inlineForeignKeys = []; ast.forEach((e) => { if (e.type === "create") { @@ -14,13 +54,15 @@ export function astToDiagram(ast) { table.color = "#175e7a"; table.fields = []; table.indices = []; - table.x = 0; - table.y = 0; + table.id = tables.length; e.create_definitions.forEach((d) => { if (d.resource === "column") { const field = {}; field.name = d.column.column; field.type = d.definition.dataType; + if (d.definition.expr && d.definition.expr.type === "expr_list") { + field.values = d.definition.expr.value.map((v) => v.value); + } field.comment = ""; field.unique = false; if (d.unique) field.unique = true; @@ -31,39 +73,42 @@ export function astToDiagram(ast) { field.primary = false; if (d.primary_key) field.primary = true; field.default = ""; - if (d.default_val) field.default = d.default_val.value.value.toString(); - if (d.definition["length"]) field.size = d.definition["length"]; + if (d.default_val) { + let defaultValue = ""; + if (d.default_val.value.type === "function") { + defaultValue = d.default_val.value.name; + if (d.default_val.value.args) { + defaultValue += + "(" + + d.default_val.value.args.value + .map((v) => { + if ( + v.type === "single_quote_string" || + v.type === "double_quote_string" + ) + return "'" + v.value + "'"; + return v.value; + }) + .join(", ") + + ")"; + } + } else if (d.default_val.value.type === "null") { + defaultValue = "NULL"; + } else { + defaultValue = d.default_val.value.value.toString(); + } + field.default = defaultValue; + } + if (d.definition["length"]) { + if (d.definition.scale) { + field.size = d.definition["length"] + "," + d.definition.scale; + } else { + field.size = d.definition["length"]; + } + } field.check = ""; if (d.check) { - let check = ""; - if (d.check.definition[0].left.column) { - let value = d.check.definition[0].right.value; - if ( - d.check.definition[0].right.type === "double_quote_string" || - d.check.definition[0].right.type === "single_quote_string" - ) - value = "'" + value + "'"; - check = - d.check.definition[0].left.column + - " " + - d.check.definition[0].operator + - " " + - value; - } else { - let value = d.check.definition[0].right.value; - if ( - d.check.definition[0].left.type === "double_quote_string" || - d.check.definition[0].left.type === "single_quote_string" - ) - value = "'" + value + "'"; - check = - value + - " " + - d.check.definition[0].operator + - " " + - d.check.definition[0].right.column; - } - field.check = check; + field.check = buildSQLFromAST(d.check.definition[0]); } table.fields.push(field); @@ -77,17 +122,58 @@ export function astToDiagram(ast) { }); }); } else if (d.constraint_type === "FOREIGN KEY") { - inlineForeignKeys.push({ ...d, startTable: e.table[0].table }); + const relationship = {}; + const startTableId = table.id; + const startTable = e.table[0].table; + const startField = d.definition[0].column; + const endTable = d.reference_definition.table[0].table; + const endField = d.reference_definition.definition[0].column; + + const endTableId = tables.findIndex((t) => t.name === endTable); + if (endTableId === -1) return; + + const endFieldId = tables[endTableId].fields.findIndex( + (f) => f.name === endField, + ); + if (endField === -1) return; + + const startFieldId = table.fields.findIndex( + (f) => f.name === startField, + ); + if (startFieldId === -1) return; + + relationship.name = startTable + "_" + startField + "_fk"; + relationship.startTableId = startTableId; + relationship.endTableId = endTableId; + relationship.endFieldId = endFieldId; + relationship.startFieldId = startFieldId; + let updateConstraint = "No action"; + let deleteConstraint = "No action"; + d.reference_definition.on_action.forEach((c) => { + if (c.type === "on update") { + updateConstraint = c.value.value; + updateConstraint = + updateConstraint[0].toUpperCase() + + updateConstraint.substring(1); + } else if (c.type === "on delete") { + deleteConstraint = c.value.value; + deleteConstraint = + deleteConstraint[0].toUpperCase() + + deleteConstraint.substring(1); + } + }); + + relationship.updateConstraint = updateConstraint; + relationship.deleteConstraint = deleteConstraint; + relationship.cardinality = Cardinality.ONE_TO_ONE; + relationships.push(relationship); } } }); - tables.push(table); - tables.forEach((e, i) => { - e.id = i; - e.fields.forEach((f, j) => { - f.id = j; - }); + table.fields.forEach((f, j) => { + f.id = j; }); + tables.push(table); } else if (e.keyword === "index") { const index = {}; index.name = e.index; @@ -108,145 +194,89 @@ export function astToDiagram(ast) { if (found !== -1) tables[found].indices.forEach((i, j) => (i.id = j)); } } else if (e.type === "alter") { - if ( - e.expr[0].action === "add" && - e.expr[0].create_definitions.constraint_type === "FOREIGN KEY" - ) { - const relationship = {}; - const startTable = e.table[0].table; - const startField = e.expr[0].create_definitions.definition[0].column; - const endTable = - e.expr[0].create_definitions.reference_definition.table[0].table; - const endField = - e.expr[0].create_definitions.reference_definition.definition[0] - .column; - let updateConstraint = "No action"; - let deleteConstraint = "No action"; - e.expr[0].create_definitions.reference_definition.on_action.forEach( - (c) => { - if (c.type === "on update") { - updateConstraint = c.value.value; - updateConstraint = - updateConstraint[0].toUpperCase() + - updateConstraint.substring(1); - } else if (c.type === "on delete") { - deleteConstraint = c.value.value; - deleteConstraint = - deleteConstraint[0].toUpperCase() + - deleteConstraint.substring(1); - } - } - ); + e.expr.forEach((expr) => { + if ( + expr.action === "add" && + expr.create_definitions.constraint_type === "FOREIGN KEY" + ) { + const relationship = {}; + const startTable = e.table[0].table; + const startField = expr.create_definitions.definition[0].column; + const endTable = + expr.create_definitions.reference_definition.table[0].table; + const endField = + expr.create_definitions.reference_definition.definition[0].column; + let updateConstraint = "No action"; + let deleteConstraint = "No action"; + expr.create_definitions.reference_definition.on_action.forEach( + (c) => { + if (c.type === "on update") { + updateConstraint = c.value.value; + updateConstraint = + updateConstraint[0].toUpperCase() + + updateConstraint.substring(1); + } else if (c.type === "on delete") { + deleteConstraint = c.value.value; + deleteConstraint = + deleteConstraint[0].toUpperCase() + + deleteConstraint.substring(1); + } + }, + ); - let startTableId = -1; - let startFieldId = -1; - let endTableId = -1; - let endFieldId = -1; + const startTableId = tables.findIndex((t) => t.name === startTable); + if (startTable === -1) return; - tables.forEach((t) => { - if (t.name === startTable) { - startTableId = t.id; - return; - } + const endTableId = tables.findIndex((t) => t.name === endTable); + if (endTableId === -1) return; - if (t.name === endTable) { - endTableId = t.id; - } - }); + const endFieldId = tables[endTableId].fields.findIndex( + (f) => f.name === endField, + ); + if (endField === -1) return; - if (startTableId === -1 || endTableId === -1) return; + const startFieldId = tables[startTableId].fields.findIndex( + (f) => f.name === startField, + ); + if (startFieldId === -1) return; - tables[startTableId].fields.forEach((f) => { - if (f.name === startField) { - startFieldId = f.id; - return; - } + relationship.name = startTable + "_" + startField + "_fk"; + relationship.startTableId = startTableId; + relationship.startFieldId = startFieldId; + relationship.endTableId = endTableId; + relationship.endFieldId = endFieldId; + relationship.updateConstraint = updateConstraint; + relationship.deleteConstraint = deleteConstraint; + relationship.cardinality = Cardinality.ONE_TO_ONE; + relationships.push(relationship); - if (f.name === endField) { - endFieldId = f.id; - } - }); - - if (startFieldId === -1 || endFieldId === -1) return; - - relationship.name = startTable + "_" + startField + "_fk"; - relationship.startTableId = startTableId; - relationship.startFieldId = startFieldId; - relationship.endTableId = endTableId; - relationship.endFieldId = endFieldId; - relationship.updateConstraint = updateConstraint; - relationship.deleteConstraint = deleteConstraint; - relationship.cardinality = Cardinality.ONE_TO_ONE; - relationships.push(relationship); - - relationships.forEach((r, i) => (r.id = i)); - } + relationships.forEach((r, i) => (r.id = i)); + } + }); } }); - inlineForeignKeys.forEach((fk) => { - const relationship = {}; - const startTable = fk.startTable; - const startField = fk.definition[0].column; - const endTable = fk.reference_definition.table[0].table; - const endField = fk.reference_definition.definition[0].column; - let updateConstraint = "No action"; - let deleteConstraint = "No action"; - fk.reference_definition.on_action.forEach((c) => { - if (c.type === "on update") { - updateConstraint = c.value.value; - updateConstraint = - updateConstraint[0].toUpperCase() + updateConstraint.substring(1); - } else if (c.type === "on delete") { - deleteConstraint = c.value.value; - deleteConstraint = - deleteConstraint[0].toUpperCase() + deleteConstraint.substring(1); - } - }); - - let startTableId = -1; - let startFieldId = -1; - let endTableId = -1; - let endFieldId = -1; - - tables.forEach((t) => { - if (t.name === startTable) { - startTableId = t.id; - return; - } - - if (t.name === endTable) { - endTableId = t.id; - } - }); - - if (startTableId === -1 || endTableId === -1) return; - - tables[startTableId].fields.forEach((f) => { - if (f.name === startField) { - startFieldId = f.id; - return; - } - - if (f.name === endField) { - endFieldId = f.id; - } - }); - - if (startFieldId === -1 || endFieldId === -1) return; - - relationship.name = startTable + "_" + startField + "_fk"; - relationship.startTableId = startTableId; - relationship.startFieldId = startFieldId; - relationship.endTableId = endTableId; - relationship.endFieldId = endFieldId; - relationship.updateConstraint = updateConstraint; - relationship.deleteConstraint = deleteConstraint; - relationship.cardinality = Cardinality.ONE_TO_ONE; - relationships.push(relationship); - }); - relationships.forEach((r, i) => (r.id = i)); + let maxHeight = -1; + const tableWidth = 200; + const gapX = 54; + const gapY = 40; + tables.forEach((table, i) => { + if (i < tables.length / 2) { + table.x = i * tableWidth + (i + 1) * gapX; + table.y = gapY; + const height = + table.fields.length * tableFieldHeight + + tableHeaderHeight + + tableColorStripHeight; + maxHeight = Math.max(height, maxHeight); + } else { + const index = tables.length - i - 1; + table.x = index * tableWidth + (index + 1) * gapX; + table.y = maxHeight + 2 * gapY; + } + }); + return { tables, relationships }; } diff --git a/src/utils/issues.js b/src/utils/issues.js index b95fa74..90e1259 100644 --- a/src/utils/issues.js +++ b/src/utils/issues.js @@ -1,4 +1,4 @@ -import { strHasQuotes } from "./utils"; +import { isFunction, strHasQuotes } from "./utils"; function validateDateStr(str) { return /^(?!0000)(?!00)(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-9]|3[01])|(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31))$/.test( @@ -9,13 +9,23 @@ function validateDateStr(str) { function checkDefault(field) { if (field.default === "") return true; + if (isFunction(field.default)) return true; + + if (!field.notNull && field.default.toLowerCase() === "null") return true; + switch (field.type) { case "INT": case "BIGINT": case "SMALLINT": return /^-?\d*$/.test(field.default); + case "SET": { + const defaultValues = field.default.split(","); + for (let i = 0; i < defaultValues.length; i++) { + if (!field.values.includes(defaultValues[i].trim())) return false; + } + return true; + } case "ENUM": - case "SET": return field.values.includes(field.default); case "CHAR": case "VARCHAR": @@ -30,7 +40,8 @@ function checkDefault(field) { ); case "BOOLEAN": return ( - field.default.trim() === "false" || field.default.trim() === "true" + field.default.trim().toLowerCase() === "false" || + field.default.trim().toLowerCase() === "true" ); case "FLOAT": case "DECIMAL": @@ -55,6 +66,9 @@ function checkDefault(field) { return parseInt(date[0]) >= 1970 && parseInt(date[0]) <= 2038; } case "DATETIME": { + if (field.default.toUpperCase() === "CURRENT_TIMESTAMP") { + return true; + } if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(field.default)) { return false; } @@ -116,12 +130,6 @@ export function getIssues(diagram) { ); } - if (field.type === "DOUBLE" && field.size !== "") { - issues.push( - `Specifying number of digits for floating point data types is deprecated.`, - ); - } - if (duplicateFieldNames[field.name]) { issues.push(`Duplicate table fields in table "${table.name}"`); } else { @@ -182,12 +190,6 @@ export function getIssues(diagram) { } } - if (field.type === "DOUBLE" && field.size !== "") { - issues.push( - `Specifying number of digits for floating point data types is deprecated.`, - ); - } - if (duplicateFieldNames[field.name]) { issues.push(`Duplicate type fields in "${type.name}"`); } else { diff --git a/src/utils/toSQL.js b/src/utils/toSQL.js index 4720a6e..40577ce 100644 --- a/src/utils/toSQL.js +++ b/src/utils/toSQL.js @@ -1,5 +1,5 @@ import { sqlDataTypes } from "../data/constants"; -import { strHasQuotes } from "./utils"; +import { isFunction, isKeyword, strHasQuotes } from "./utils"; export function getJsonType(f) { if (!sqlDataTypes.includes(f.type)) { @@ -44,10 +44,7 @@ export function getTypeString(field, dbms = "mysql", baseType = false) { if (field.type === "UUID") { return `VARCHAR(36)`; } - if (isSized(field.type)) { - return `${field.type}(${field.size})`; - } - if (hasPrecision(field.type)) { + if (hasPrecision(field.type) || isSized(field.type)) { return `${field.type}${field.size ? `(${field.size})` : ""}`; } if (field.type === "SET" || field.type === "ENUM") { @@ -147,13 +144,16 @@ export function hasQuotes(type) { } export function parseDefault(field) { - if (strHasQuotes(field.default)) { + if ( + strHasQuotes(field.default) || + isFunction(field.default) || + isKeyword(field.default) || + !hasQuotes(field.type) + ) { return field.default; } - return hasQuotes(field.type) && field.default.toLowerCase() !== "null" - ? `'${field.default}'` - : `${field.default}`; + return `'${field.default}'`; } export function jsonToMySQL(obj) { diff --git a/src/utils/utils.js b/src/utils/utils.js index 53226a6..a51919f 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -24,3 +24,13 @@ export function strHasQuotes(str) { (str[0] === str[str.length - 1] && str[0] === "`") ); } + +const keywords = ["CURRENT_TIMESTAMP", "NULL"]; + +export function isKeyword(str) { + return keywords.includes(str.toUpperCase()); +} + +export function isFunction(str) { + return /\w+\([^)]*\)$/.test(str); +}