diff --git a/package-lock.json b/package-lock.json index 37e0f4f8d..9314a27ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "lexical": "^0.12.5", "node-sql-parser": "^5.3.6", "octokit": "^4.0.2", + "oracle-sql-parser": "^0.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.4.1", @@ -8424,6 +8425,12 @@ "node": ">= 0.8.0" } }, + "node_modules/oracle-sql-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/oracle-sql-parser/-/oracle-sql-parser-0.1.0.tgz", + "integrity": "sha512-8MLYOJIKaOY1cWvnMFuYPxWcDH5GfmJMh/f1Tyow0bydC31heO+eSoexZW+NJBSdK87lNJl8nsQ/SY//ZGOwcQ==", + "license": "MIT" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", diff --git a/package.json b/package.json index a874a5343..cf3f73022 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "lexical": "^0.12.5", "node-sql-parser": "^5.3.6", "octokit": "^4.0.2", + "oracle-sql-parser": "^0.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.4.1", diff --git a/src/assets/oraclesql-icon.png b/src/assets/oraclesql-icon.png new file mode 100644 index 000000000..ef0f66bb7 Binary files /dev/null and b/src/assets/oraclesql-icon.png differ diff --git a/src/assets/oraclesql.png b/src/assets/oraclesql.png new file mode 100644 index 000000000..cb7686e43 Binary files /dev/null and b/src/assets/oraclesql.png differ diff --git a/src/components/EditorHeader/ControlPanel.jsx b/src/components/EditorHeader/ControlPanel.jsx index 02e050dfd..51fed65f0 100644 --- a/src/components/EditorHeader/ControlPanel.jsx +++ b/src/components/EditorHeader/ControlPanel.jsx @@ -20,6 +20,7 @@ import { InputNumber, Tooltip, Spin, + Tag, Toast, Popconfirm, } from "@douyinfe/semi-ui"; @@ -30,7 +31,7 @@ import { jsonToSQLite, jsonToMariaDB, jsonToSQLServer, - jsonToOracle, + jsonToOracleSQL, } from "../../utils/exportSQL/generic"; import { ObjectType, @@ -1010,13 +1011,15 @@ export default function ControlPanel({ import_from: { children: [ { - JSON: fileImport, + function: fileImport, + name: "JSON", }, { - DBML: () => { + function: () => { setModal(MODAL.IMPORT); setImportFrom(IMPORT_FROM.DBML); }, + name: "DBML", }, ], }, @@ -1024,40 +1027,47 @@ export default function ControlPanel({ ...(database === DB.GENERIC && { children: [ { - MySQL: () => { + function: () => { setModal(MODAL.IMPORT_SRC); setImportDb(DB.MYSQL); }, + name: "MySQL", }, { - PostgreSQL: () => { + function: () => { setModal(MODAL.IMPORT_SRC); setImportDb(DB.POSTGRES); }, + name: "PostgreSQL", }, { - SQLite: () => { + function: () => { setModal(MODAL.IMPORT_SRC); setImportDb(DB.SQLITE); }, + name: "SQLite", }, { - MariaDB: () => { + function: () => { setModal(MODAL.IMPORT_SRC); setImportDb(DB.MARIADB); }, + name: "MariaDB", }, { - MSSQL: () => { + function: () => { setModal(MODAL.IMPORT_SRC); setImportDb(DB.MSSQL); }, + name: "MSSQL", }, { - Oracle: () => { + function: () => { setModal(MODAL.IMPORT_SRC); - setImportDb(DB.ORACLE); + setImportDb(DB.ORACLESQL); }, + name: "Oracle", + label: "Beta", }, ], }), @@ -1071,7 +1081,8 @@ export default function ControlPanel({ ...(database === DB.GENERIC && { children: [ { - MySQL: () => { + name: "MySQL", + function: () => { setModal(MODAL.CODE); const src = jsonToMySQL({ tables: tables, @@ -1087,7 +1098,8 @@ export default function ControlPanel({ }, }, { - PostgreSQL: () => { + name: "PostgreSQL", + function: () => { setModal(MODAL.CODE); const src = jsonToPostgreSQL({ tables: tables, @@ -1103,7 +1115,8 @@ export default function ControlPanel({ }, }, { - SQLite: () => { + name: "SQLite", + function: () => { setModal(MODAL.CODE); const src = jsonToSQLite({ tables: tables, @@ -1119,7 +1132,8 @@ export default function ControlPanel({ }, }, { - MariaDB: () => { + name: "MariaDB", + function: () => { setModal(MODAL.CODE); const src = jsonToMariaDB({ tables: tables, @@ -1135,7 +1149,8 @@ export default function ControlPanel({ }, }, { - MSSQL: () => { + name: "MSSQL", + function: () => { setModal(MODAL.CODE); const src = jsonToSQLServer({ tables: tables, @@ -1151,9 +1166,11 @@ export default function ControlPanel({ }, }, { - Oracle: () => { + label: "Beta", + name: "Oracle", + function: () => { setModal(MODAL.CODE); - const src = jsonToOracle({ + const src = jsonToOracleSQL({ tables: tables, references: relationships, types: types, @@ -1164,7 +1181,7 @@ export default function ControlPanel({ data: src, extension: "sql", })); - } + }, }, ], }), @@ -1188,7 +1205,8 @@ export default function ControlPanel({ export_as: { children: [ { - PNG: () => { + name: "PNG", + function: () => { toPng(document.getElementById("canvas")).then(function (dataUrl) { setExportData((prev) => ({ ...prev, @@ -1200,7 +1218,8 @@ export default function ControlPanel({ }, }, { - JPEG: () => { + name: "JPEG", + function: () => { toJpeg(document.getElementById("canvas"), { quality: 0.95 }).then( function (dataUrl) { setExportData((prev) => ({ @@ -1214,7 +1233,8 @@ export default function ControlPanel({ }, }, { - SVG: () => { + name: "SVG", + function: () => { const filter = (node) => node.tagName !== "i"; toSvg(document.getElementById("canvas"), { filter: filter }).then( function (dataUrl) { @@ -1229,7 +1249,8 @@ export default function ControlPanel({ }, }, { - JSON: () => { + name: "JSON", + function: () => { setModal(MODAL.CODE); const result = JSON.stringify( { @@ -1253,7 +1274,8 @@ export default function ControlPanel({ }, }, { - DBML: () => { + name: "DBML", + function: () => { setModal(MODAL.CODE); const result = toDBML({ tables, @@ -1268,7 +1290,8 @@ export default function ControlPanel({ }, }, { - PDF: () => { + name: "PDF", + function: () => { const canvas = document.getElementById("canvas"); toJpeg(canvas).then(function (dataUrl) { const doc = new jsPDF("l", "px", [ @@ -1288,7 +1311,8 @@ export default function ControlPanel({ }, }, { - MERMAID: () => { + name: "Mermaid", + function: () => { setModal(MODAL.CODE); const result = jsonToMermaid({ tables: tables, @@ -1306,7 +1330,8 @@ export default function ControlPanel({ }, }, { - readme: () => { + name: "Markdown", + function: () => { setModal(MODAL.CODE); const result = jsonToDocumentation({ tables: tables, @@ -1518,7 +1543,8 @@ export default function ControlPanel({ theme: { children: [ { - light: () => { + name: t("light"), + function: () => { const body = document.body; if (body.hasAttribute("theme-mode")) { body.setAttribute("theme-mode", "light"); @@ -1528,7 +1554,8 @@ export default function ControlPanel({ }, }, { - dark: () => { + name: t("dark"), + function: () => { const body = document.body; if (body.hasAttribute("theme-mode")) { body.setAttribute("theme-mode", "dark"); @@ -1852,9 +1879,9 @@ export default function ControlPanel({ const body = document.body; if (body.hasAttribute("theme-mode")) { if (body.getAttribute("theme-mode") === "light") { - menu["view"]["theme"].children[1]["dark"](); + menu["view"]["theme"].children[1].function(); } else { - menu["view"]["theme"].children[0]["light"](); + menu["view"]["theme"].children[0].function(); } } }} @@ -1953,7 +1980,7 @@ export default function ControlPanel({ if (menu[category][item].children) { return ( ( - {t(Object.keys(e)[0])} + {e.name} + {e.label && ( + + {e.label} + + )} ), )} diff --git a/src/components/EditorHeader/Modal/Modal.jsx b/src/components/EditorHeader/Modal/Modal.jsx index 0eb7b5dcf..d8bb3d4f7 100644 --- a/src/components/EditorHeader/Modal/Modal.jsx +++ b/src/components/EditorHeader/Modal/Modal.jsx @@ -20,6 +20,7 @@ import { } from "../../../hooks"; import { saveAs } from "file-saver"; import { Parser } from "node-sql-parser"; +import { Parser as OracleParser } from "oracle-sql-parser"; import { getModalTitle, getModalWidth, @@ -129,18 +130,68 @@ export default function Modal({ } }) .catch((error) => { - console.log(error); + console.error(error); Toast.error(t("didnt_find_diagram")); }); }; const parseSQLAndLoadDiagram = () => { - const parser = new Parser(); + const targetDatabase = database === DB.GENERIC ? importDb : database; + + // Preprocess SQL for Oracle parser + const preprocessOracleSQL = (sql) => { + return sql + // Remove comments that start with --@ which can cause parsing issues + .replace(/--@[^\r\n]*/g, '') + // Remove standalone comment lines that just contain -- + .replace(/^\s*--\s*$/gm, '') + // Remove table comment blocks like -- TABLE: TABLENAME -- + .replace(/--\s*TABLE:\s*[^-]*--/g, '') + // Clean up multiple empty lines + .replace(/\n\s*\n\s*\n/g, '\n\n') + // Ensure statements end with semicolons + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0 && !line.match(/^--\s*$/)) + .join('\n'); + }; + let ast = null; try { - ast = parser.astify(importSource.src, { - database: database === DB.GENERIC ? importDb : database, - }); + if (targetDatabase === DB.ORACLESQL) { + // Try Oracle parser first + try { + const oracleParser = new OracleParser(); + const preprocessedSQL = preprocessOracleSQL(importSource.src); + ast = oracleParser.parse(preprocessedSQL); + } catch (oracleError) { + console.warn("Oracle parser failed, trying standard parser:", oracleError); + // Fallback to standard parser with modified SQL + try { + const parser = new Parser(); + // Try to convert Oracle-specific syntax to MySQL-compatible + const mysqlCompatibleSQL = importSource.src + .replace(/NUMBER\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)/gi, 'DECIMAL($1,$2)') + .replace(/NUMBER\s*\(\s*(\d+)\s*\)/gi, 'INT($1)') + .replace(/NUMBER(?!\s*\()/gi, 'INT') + .replace(/VARCHAR2/gi, 'VARCHAR') + .replace(/--@[^\r\n]*/g, ''); + + ast = parser.astify(mysqlCompatibleSQL, { + database: "mysql", + }); + } catch (standardError) { + console.error("Both parsers failed:", standardError); + throw new Error(`Failed to parse SQL. Oracle parser error: ${oracleError.message}. Standard parser error: ${standardError.message}`); + } + } + } else { + const parser = new Parser(); + + ast = parser.astify(importSource.src, { + database: targetDatabase, + }); + } } catch (error) { const message = error.location ? `${error.name} [Ln ${error.location.start.line}, Col ${error.location.start.column}]: ${error.message}` @@ -151,6 +202,10 @@ export default function Modal({ } try { + if (!ast) { + throw new Error("AST is null or undefined"); + } + const diagramData = importSQL( ast, database === DB.GENERIC ? importDb : database, @@ -180,10 +235,11 @@ export default function Modal({ } setModal(MODAL.NONE); - } catch { + } catch (importError) { + console.error("Import error:", importError); setError({ type: STATUS.ERROR, - message: `Please check for syntax errors or let us know about the error.`, + message: `Import failed: ${importError.message || "Please check for syntax errors or let us know about the error."}`, }); } }; diff --git a/src/components/Workspace.jsx b/src/components/Workspace.jsx index 9d59bb2a9..7bf55b84e 100644 --- a/src/components/Workspace.jsx +++ b/src/components/Workspace.jsx @@ -19,7 +19,7 @@ import { useEnums, } from "../hooks"; import FloatingControls from "./FloatingControls"; -import { Modal } from "@douyinfe/semi-ui"; +import { Modal, Tag } from "@douyinfe/semi-ui"; import { useTranslation } from "react-i18next"; import { databases } from "../data/databases"; import { isRtl } from "../i18n/utils/rtl"; @@ -165,7 +165,7 @@ export default function WorkSpace() { enums, gistId, loadedFromGistId, - saveState + saveState, ]); const load = useCallback(async () => { @@ -471,17 +471,24 @@ export default function WorkSpace() {
setSelectedDb(x.label)} - className={`space-y-3 py-3 px-4 rounded-md border-2 select-none ${ + className={`space-y-3 p-3 rounded-md border-2 select-none ${ settings.mode === "dark" ? "bg-zinc-700 hover:bg-zinc-600" : "bg-zinc-100 hover:bg-zinc-200" } ${selectedDb === x.label ? "border-zinc-400" : "border-transparent"}`} > -
{x.name}
+
+
{x.name}
+ {x.beta && ( + + Beta + + )} +
{x.image && ( { - return intRegex.test(field.default); - }, - hasCheck: true, - isSized: false, - hasPrecision: true, - canIncrement: true, - }, INT: { type: "INT", checkDefault: (field) => { @@ -65,6 +55,16 @@ const defaultTypesBase = { isSized: false, hasPrecision: true, }, + NUMBER: { + type: "NUMBER", + checkDefault: (field) => { + return /^-?\d+(\.\d+)?$/.test(field.default); + }, + hasCheck: true, + isSized: false, + hasPrecision: true, + canIncrement: false, + }, FLOAT: { type: "FLOAT", checkDefault: (field) => { @@ -120,6 +120,20 @@ const defaultTypesBase = { defaultSize: 255, hasQuotes: true, }, + VARCHAR2: { + type: "VARCHAR2", + checkDefault: (field) => { + if (strHasQuotes(field.default)) { + return field.default.length - 2 <= field.size; + } + return field.default.length <= field.size; + }, + hasCheck: true, + isSized: true, + hasPrecision: false, + defaultSize: 225, + hasQuotes: true, + }, TEXT: { type: "TEXT", checkDefault: (field) => true, @@ -150,7 +164,9 @@ const defaultTypesBase = { } const content = field.default.split(" "); const date = content[0].split("-"); - return Number.parseInt(date[0]) >= 1970 && Number.parseInt(date[0]) <= 2038; + return ( + Number.parseInt(date[0]) >= 1970 && Number.parseInt(date[0]) <= 2038 + ); }, hasCheck: false, isSized: false, @@ -233,6 +249,22 @@ const defaultTypesBase = { hasPrecision: false, noDefault: true, }, + CLOB: { + type: "CLOB", + checkDefault: (field) => true, + isSized: false, + hasCheck: false, + hasPrecision: false, + noDefault: true, + }, + NCLOB: { + type: "NCLOB", + checkDefault: (field) => true, + isSized: false, + hasCheck: false, + hasPrecision: false, + noDefault: true, + }, JSON: { type: "JSON", checkDefault: (field) => true, @@ -273,19 +305,6 @@ const defaultTypesBase = { hasPrecision: false, noDefault: true, }, - VARCHAR2: { - type: "VARCHAR2", - checkDefault: (field) => { - if (strHasQuotes(field.default)) { - return field.default.length - 2 <= field.size; - } - return field.default.length <= field.size; - }, - hasCheck: true, - isSized: true, - hasPrecision: false, - defaultSize: 10, - }, }; export const defaultTypes = new Proxy(defaultTypesBase, { @@ -552,7 +571,9 @@ const mysqlTypesBase = { } const content = field.default.split(" "); const date = content[0].split("-"); - return Number.parseInt(date[0]) >= 1970 && Number.parseInt(date[0]) <= 2038; + return ( + Number.parseInt(date[0]) >= 1970 && Number.parseInt(date[0]) <= 2038 + ); }, hasCheck: false, isSized: false, @@ -1060,8 +1081,9 @@ const postgresTypesBase = { checkDefault: (field) => { const specialValues = ["now", "allballs"]; return ( - /^(?:[01]?\d|2[0-3]):[0-5]?\d:[0-5]?\d([+-]\d{2}:\d{2})?$/.test(field.default) || - specialValues.includes(field.default.toLowerCase()) + /^(?:[01]?\d|2[0-3]):[0-5]?\d:[0-5]?\d([+-]\d{2}:\d{2})?$/.test( + field.default, + ) || specialValues.includes(field.default.toLowerCase()) ); }, hasCheck: false, @@ -1086,7 +1108,8 @@ const postgresTypesBase = { ]; return ( /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(field.default) || - (Number.parseInt(date[0]) >= 1970 && Number.parseInt(date[0]) <= 2038) || + (Number.parseInt(date[0]) >= 1970 && + Number.parseInt(date[0]) <= 2038) || specialValues.includes(field.default.toLowerCase()) ); }, @@ -1265,7 +1288,11 @@ const postgresTypesBase = { elementsStr = field.default.slice(1, -1); } elements = JSON.parse(elementsStr); - return Array.isArray(elements) && elements.length === field.size && elements.every(Number.isFinite); + return ( + Array.isArray(elements) && + elements.length === field.size && + elements.every(Number.isFinite) + ); } catch (e) { return false; } @@ -1275,7 +1302,7 @@ const postgresTypesBase = { hasPrecision: false, hasQuotes: true, }, - HALFVEC:{ + HALFVEC: { type: "HALFVEC", checkDefault: (field) => { let elements; @@ -1285,7 +1312,11 @@ const postgresTypesBase = { elementsStr = field.default.slice(1, -1); } elements = JSON.parse(elementsStr); - return Array.isArray(elements) && elements.length === field.size && elements.every(Number.isFinite); + return ( + Array.isArray(elements) && + elements.length === field.size && + elements.every(Number.isFinite) + ); } catch (e) { return false; } @@ -1302,9 +1333,9 @@ const postgresTypesBase = { if (strHasQuotes(field.default)) { elementsStr = field.default.slice(1, -1); } - const lengthStr = elementsStr.split('/')[1] - const length = Number.parseInt(lengthStr) - return length === field.size + const lengthStr = elementsStr.split("/")[1]; + const length = Number.parseInt(lengthStr); + return length === field.size; }, hasCheck: true, isSized: true, @@ -1467,7 +1498,9 @@ const sqliteTypesBase = { } const content = field.default.split(" "); const date = content[0].split("-"); - return Number.parseInt(date[0]) >= 1970 && Number.parseInt(date[0]) <= 2038; + return ( + Number.parseInt(date[0]) >= 1970 && Number.parseInt(date[0]) <= 2038 + ); }, hasCheck: false, isSized: false, @@ -1728,7 +1761,9 @@ const mssqlTypesBase = { } const content = field.default.split(" "); const date = content[0].split("-"); - return Number.parseInt(date[0]) >= 1970 && Number.parseInt(date[0]) <= 2038; + return ( + Number.parseInt(date[0]) >= 1970 && Number.parseInt(date[0]) <= 2038 + ); }, hasCheck: false, isSized: false, @@ -1894,6 +1929,218 @@ export const mssqlTypes = new Proxy(mssqlTypesBase, { get: (target, prop) => (prop in target ? target[prop] : false), }); +const oraclesqlTypesBase = { + INTEGER: { + type: "INTEGER", + checkDefault: (field) => { + return intRegex.test(field.default); + }, + hasCheck: true, + isSized: false, + hasPrecision: false, + canIncrement: true, + }, + NUMBER: { + type: "NUMBER", + checkDefault: (field) => { + return /^-?\d+(\.\d+)?$/.test(field.default); + }, + hasCheck: true, + isSized: false, + hasPrecision: true, + canIncrement: false, + }, + FLOAT: { + type: "FLOAT", + checkDefault: (field) => { + return /^-?\d+(\.\d+)?$/.test(field.default); + }, + hasCheck: true, + isSized: false, + hasPrecision: true, + }, + LONG: { + type: "LONG", + checkDefault: (field) => { + return intRegex.test(field.default); + }, + hasCheck: true, + isSized: false, + hasPrecision: false, + canIncrement: true, + }, + VARCHAR2: { + type: "VARCHAR2", + checkDefault: (field) => { + if (strHasQuotes(field.default)) { + return field.default.length - 2 <= field.size; + } + return field.default.length <= field.size; + }, + hasCheck: true, + isSized: true, + hasPrecision: false, + defaultSize: 4000, + hasQuotes: true, + }, + NVARCHAR2: { + type: "VARCHAR2", + checkDefault: (field) => { + if (strHasQuotes(field.default)) { + return field.default.length - 2 <= field.size; + } + return field.default.length <= field.size; + }, + hasCheck: true, + isSized: true, + hasPrecision: false, + defaultSize: 4000, + hasQuotes: true, + }, + CHAR: { + type: "CHAR", + checkDefault: (field) => { + if (strHasQuotes(field.default)) { + return field.default.length - 2 <= field.size; + } + return field.default.length <= field.size; + }, + hasCheck: true, + isSized: true, + hasPrecision: false, + defaultSize: 1, + hasQuotes: true, + }, + NCHAR: { + type: "NCHAR", + checkDefault: (field) => { + if (strHasQuotes(field.default)) { + return field.default.length - 2 <= field.size; + } + return field.default.length <= field.size; + }, + hasCheck: true, + isSized: true, + hasPrecision: false, + defaultSize: 1, + hasQuotes: true, + }, + CLOB: { + type: "CLOB", + checkDefault: (field) => true, + isSized: false, + hasCheck: false, + hasPrecision: false, + noDefault: true, + }, + NCLOB: { + type: "NCLOB", + checkDefault: (field) => true, + isSized: false, + hasCheck: false, + hasPrecision: false, + noDefault: true, + }, + BLOB: { + type: "BLOB", + checkDefault: (field) => true, + isSized: false, + hasCheck: false, + hasPrecision: false, + noDefault: true, + }, + BFILE: { + type: "BFILE", + checkDefault: (field) => true, + isSized: false, + hasCheck: false, + hasPrecision: false, + noDefault: true, + }, + JSON: { + type: "JSON", + checkDefault: (field) => true, + isSized: false, + hasCheck: false, + hasPrecision: false, + noDefault: true, + }, + VECTOR: { + type: "VECTOR", + checkDefault: (field) => true, + isSized: false, + hasCheck: false, + hasPrecision: false, + noDefault: true, + }, + DATE: { + type: "DATE", + checkDefault: (field) => { + return /^\d{4}-\d{2}-\d{2}$/.test(field.default); + }, + hasCheck: false, + isSized: false, + hasPrecision: false, + hasQuotes: true, + }, + TIMESTAMP: { + type: "TIMESTAMP", + checkDefault: (field) => { + if (field.default.toUpperCase() === "CURRENT_TIMESTAMP") { + return true; + } + return /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test( + field.default, + ); + }, + hasCheck: false, + isSized: false, + hasPrecision: true, + hasQuotes: true, + }, + INTERVAL: { + type: "INTERVAL", + checkDefault: (field) => { + return /^INTERVAL\s'\d+'(\s+DAY|HOUR|MINUTE|SECOND)?$/.test( + field.default, + ); + }, + hasCheck: false, + isSized: false, + hasPrecision: false, + hasQuotes: true, + }, + BOOLEAN: { + type: "BOOLEAN", + checkDefault: (field) => { + return ( + field.default === "0" || + field.default === "1" || + field.default.toUpperCase() === "TRUE" || + field.default.toUpperCase() === "FALSE" + ); + }, + hasCheck: false, + isSized: false, + hasPrecision: false, + }, + RAW: { + type: "RAW", + checkDefault: (field) => { + return /^[0-9A-Fa-f]+$/.test(field.default); + }, + hasCheck: false, + isSized: true, + hasPrecision: false, + defaultSize: 2000, + hasQuotes: false, + }, +}; + +export const oraclesqlTypes = new Proxy(oraclesqlTypesBase, { + get: (target, prop) => (prop in target ? target[prop] : false), +}); + const dbToTypesBase = { [DB.GENERIC]: defaultTypes, [DB.MYSQL]: mysqlTypes, @@ -1901,7 +2148,7 @@ const dbToTypesBase = { [DB.SQLITE]: sqliteTypes, [DB.MSSQL]: mssqlTypes, [DB.MARIADB]: mysqlTypes, - [DB.ORACLE]: oracleTypes, + [DB.ORACLESQL]: oraclesqlTypes, }; export const dbToTypes = new Proxy(dbToTypesBase, { diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 0398cb889..8c846ef00 100644 --- a/src/pages/LandingPage.jsx +++ b/src/pages/LandingPage.jsx @@ -8,6 +8,7 @@ import mysql_icon from "../assets/mysql.png"; import postgres_icon from "../assets/postgres.png"; import sqlite_icon from "../assets/sqlite.png"; import mariadb_icon from "../assets/mariadb.png"; +import oraclesql_icon from "../assets/oraclesql.png"; import sql_server_icon from "../assets/sql-server.png"; import discord from "../assets/discord.png"; import github from "../assets/github.png"; @@ -15,7 +16,6 @@ import screenshot from "../assets/screenshot.png"; import FadeIn from "../animations/FadeIn"; import axios from "axios"; import { languages } from "../i18n/i18n"; -import { Tweet } from "react-tweet"; import { socials } from "../data/socials"; function shortenNumber(number) { @@ -202,19 +202,36 @@ export default function LandingPage() {
- {/* Tweets */} + {/* Supported Databases */}
- What the internet says about us + Supported Databases
-
- - - - +
+ + + + + +
diff --git a/src/utils/exportSQL/generic.js b/src/utils/exportSQL/generic.js index 84e0f9f47..972fcb52e 100644 --- a/src/utils/exportSQL/generic.js +++ b/src/utils/exportSQL/generic.js @@ -1,3 +1,4 @@ +import { DB } from "../../data/constants"; import { dbToTypes, defaultTypes } from "../../data/datatypes"; import { getInlineFK, parseDefault } from "./shared"; @@ -42,10 +43,10 @@ export function generateSchema(type) { export function getTypeString( field, currentDb, - dbms = "mysql", + dbms = DB.MYSQL, baseType = false, ) { - if (dbms === "mysql") { + if (dbms === DB.MYSQL) { if (field.type === "UUID") { return `VARCHAR(36)`; } @@ -62,7 +63,7 @@ export function getTypeString( return "JSON"; } return field.type; - } else if (dbms === "postgres") { + } else if (dbms === DB.POSTGRES) { if (field.type === "SMALLINT" && field.increment) { return "smallserial"; } @@ -97,7 +98,7 @@ export function getTypeString( return `${field.type.toLowerCase()}${field.size ? `(${field.size})` : ""}`; } return field.type.toLowerCase(); - } else if (dbms === "mssql") { + } else if (dbms === DB.MSSQL) { let type = field.type; switch (field.type) { case "ENUM": @@ -134,6 +135,47 @@ export function getTypeString( } return type; + } else if (dbms === DB.ORACLESQL) { + let oracleType; + switch (field.type) { + case "BIGINT": + oracleType = "NUMBER"; + break; + case "VARCHAR": + oracleType = "VARCHAR2"; + break; + case "TEXT": + oracleType = "CLOB"; + break; + case "TIME": + case "DATETIME": + oracleType = "TIMESTAMP"; + break; + case "BINARY": + case "VARBINARY": + oracleType = "RAW"; + break; + case "UUID": + oracleType = "RAW(16)"; + break; + case "SET": + case "ENUM": + oracleType = field.name + "_t"; + break; + default: + oracleType = field.type; + break; + } + const typeInfo = dbToTypes[currentDb][oracleType]; + if (typeInfo.isSized || typeInfo.hasPrecision) { + if (oracleType === "NUMBER") { + return `${oracleType}${field.size ? `(${field.size})` : "(38,0)"}`; + } else { + return `${oracleType}${field.size ? `(${field.size})` : ""}`; + } + } + + return oracleType; } } @@ -250,16 +292,20 @@ export function jsonToPostgreSQL(obj) { type.comment === "" ? "" : `/**\n${type.comment}\n*/\n` }CREATE TYPE ${type.name} AS (\n${type.fields .map( - (f) => `\t${f.name} ${getTypeString(f, obj.database, "postgres")}`, + (f) => `\t${f.name} ${getTypeString(f, obj.database, DB.POSTGRES)}`, ) .join("\n")}\n);` ); } else { return `CREATE TYPE ${type.name} AS (\n${type.fields - .map((f) => `\t${f.name} ${getTypeString(f, obj.database, "postgres")}`) - .join( - "\n", - )}\n);\n${type.comment != "" ? `\nCOMMENT ON TYPE ${type.name} IS '${type.comment}';\n` : ""}`; + .map( + (f) => `\t${f.name} ${getTypeString(f, obj.database, DB.POSTGRES)}`, + ) + .join(",\n")}\n);\n${ + type.comment && type.comment.trim() != "" + ? `\nCOMMENT ON TYPE ${type.name} IS '${type.comment}';\n` + : "" + }`; } })}\n${obj.tables .map( @@ -282,7 +328,7 @@ export function jsonToPostgreSQL(obj) { (field) => `${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t"${ field.name - }" ${getTypeString(field, obj.database, "postgres")}${ + }" ${getTypeString(field, obj.database, DB.POSTGRES)}${ field.notNull ? " NOT NULL" : "" }${field.unique ? " UNIQUE" : ""}${ field.default !== "" ? ` DEFAULT ${parseDefault(field)}` : "" @@ -412,7 +458,7 @@ export function jsonToMariaDB(obj) { (field) => `\t\`${ field.name - }\` ${getTypeString(field, obj.database)}${field.notNull ? " NOT NULL" : ""}${ + }\` ${getTypeString(field, obj.database, DB.MYSQL)}${field.notNull ? " NOT NULL" : ""}${ field.increment ? " AUTO_INCREMENT" : "" }${field.unique ? " UNIQUE" : ""}${ field.default !== "" @@ -471,7 +517,7 @@ export function jsonToSQLServer(obj) { }CREATE TYPE [${type.name}] FROM ${ type.fields.length < 0 ? "" - : `${getTypeString(type.fields[0], obj.database, "mssql", true)}` + : `${getTypeString(type.fields[0], obj.database, DB.MSSQL, true)}` };\nGO\n`; }) .join("\n")}\n${obj.tables @@ -484,7 +530,7 @@ export function jsonToSQLServer(obj) { (field) => `${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t[${ field.name - }] ${getTypeString(field, obj.database, "mssql")}${ + }] ${getTypeString(field, obj.database, DB.MSSQL)}${ field.notNull ? " NOT NULL" : "" }${field.increment ? " IDENTITY" : ""}${ field.unique ? " UNIQUE" : "" @@ -528,3 +574,70 @@ export function jsonToSQLServer(obj) { ) .join("\n")}`; } + +export function jsonToOracleSQL(obj) { + return `${obj.tables + .map( + (table) => + `${ + table.fields.filter((f) => f.type === "ENUM" || f.type === "SET") + .length > 0 + ? `${table.fields + .filter((f) => f.type === "ENUM" || f.type === "SET") + .map( + (f) => + `CREATE DOMAIN "${f.name}_t" AS ENUM (${f.values + .map((v) => `'${v}'`) + .join(", ")});\n`, + ) + .join("\n")}\n` + : "" + }${ + table.comment === "" ? "" : `/* ${table.comment} */\n` + }CREATE TABLE "${table.name}" (\n${table.fields + .map( + (field) => + `${field.comment === "" ? "" : ` -- ${field.comment}\n`} "${ + field.name + }" ${getTypeString(field, obj.database, DB.ORACLESQL)}${ + field.notNull ? " NOT NULL" : "" + }${field.increment ? " GENERATED ALWAYS AS IDENTITY" : ""}${ + field.unique ? " UNIQUE" : "" + }${ + field.default !== "" + ? ` DEFAULT ${parseDefault(field, obj.database)}` + : "" + }${ + field.check === "" || + !dbToTypes[obj.database][field.type].hasCheck + ? "" + : ` CHECK (${field.check})` + }`, + ) + .join(",\n")}${ + table.fields.filter((f) => f.primary).length > 0 + ? `,\n PRIMARY KEY (${table.fields + .filter((f) => f.primary) + .map((f) => `"${f.name}"`) + .join(", ")})` + : "" + }\n);\n${table.indices + .map( + (i) => + `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}"\n ON "${ + table.name + }" (${i.fields.map((f) => `"${f}"`).join(", ")});`, + ) + .join("\n")}`, + ) + .join("\n\n")}\n${obj.references + .map( + (r) => + `ALTER TABLE "${obj.tables[r.startTableId].name}"\nADD CONSTRAINT "${r.name}" FOREIGN KEY ("${ + obj.tables[r.startTableId].fields[r.startFieldId].name + }") REFERENCES "${obj.tables[r.endTableId].name}"("${ + obj.tables[r.endTableId].fields[r.endFieldId].name + }");`, + ) + .join("\n")}`; +} diff --git a/src/utils/exportSQL/index.js b/src/utils/exportSQL/index.js index 2eabbdb9a..987fb26a5 100644 --- a/src/utils/exportSQL/index.js +++ b/src/utils/exportSQL/index.js @@ -2,9 +2,9 @@ import { DB } from "../../data/constants"; import { toMariaDB } from "./mariadb"; import { toMSSQL } from "./mssql"; import { toMySQL } from "./mysql"; +import { toOracleSQL } from "./oraclesql"; import { toPostgres } from "./postgres"; import { toSqlite } from "./sqlite"; -import { toOracle } from "./oracle"; export function exportSQL(diagram) { switch (diagram.database) { @@ -18,8 +18,8 @@ export function exportSQL(diagram) { return toMariaDB(diagram); case DB.MSSQL: return toMSSQL(diagram); - case DB.ORACLE: - return toOracle(diagram); + case DB.ORACLESQL: + return toOracleSQL(diagram); default: return ""; } diff --git a/src/utils/exportSQL/oracle.js b/src/utils/exportSQL/oraclesql.js similarity index 65% rename from src/utils/exportSQL/oracle.js rename to src/utils/exportSQL/oraclesql.js index 4cda4de17..6234273d1 100644 --- a/src/utils/exportSQL/oracle.js +++ b/src/utils/exportSQL/oraclesql.js @@ -1,47 +1,49 @@ import { parseDefault } from "./shared"; -export function toOracle(diagram) { +export function toOracleSQL(diagram) { return `${diagram.tables .map( (table) => `CREATE TABLE ${table.name} (\n${table.fields .map( (field) => - `\t"${field.name}" ${field.type}${field.size ? `(${field.size})` : ""}${ + `\t${field.name} ${field.type}${field.size ? `(${field.size})` : ""}${ field.default !== "" ? ` DEFAULT ${parseDefault(field, diagram.database)}` : "" - }${field.notNull ? " NOT NULL" : ""}` + }${field.notNull ? " NOT NULL" : ""}${ + field.increment ? " GENERATED ALWAYS AS IDENTITY" : "" + }` ) .join(",\n")}${ table.fields.filter((f) => f.unique && !f.primary).length > 0 ? `,\n\t${table.fields .filter((f) => f.unique && !f.primary) - .map((f) => `CONSTRAINT ${table.name}_${f.name}_uk UNIQUE("${f.name}")`) + .map((f) => `CONSTRAINT ${table.name}_${f.name}_uk UNIQUE(${f.name})`) .join(",\n\t")}` : "" }${ table.fields.filter((f) => f.check).length > 0 ? `,\n\t${table.fields .filter((f) => f.check) - .map((f) => `CONSTRAINT ${table.name}_${f.name}_chk CHECK("${f.name}" ${f.check})`) + .map((f) => `CONSTRAINT ${table.name}_${f.name}_chk CHECK(${f.name} ${f.check})`) .join(",\n\t")}` : "" }${ table.fields.filter((f) => f.primary).length > 0 ? `,\n\tCONSTRAINT ${table.name}_pk PRIMARY KEY(${table.fields .filter((f) => f.primary) - .map((f) => `"${f.name}"`) + .map((f) => `${f.name}`) .join(", ")})` : "" }\n);\n${table.comment ? `COMMENT ON TABLE ${table.name} IS '${table.comment}';` : ""}${table.fields .map( (field) => - field.comment ? `\nCOMMENT ON COLUMN "${table.name}"."${field.name}" IS '${field.comment}';` : "" + field.comment ? `\nCOMMENT ON COLUMN ${table.name}.${field.name} IS '${field.comment}';` : "" ) .join("")}\n${table.indices .map( (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}" ON ${table.name} (${i.fields - .map((f) => `"${f}"`) + `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX ${i.name} ON ${table.name} (${i.fields + .map((f) => `${f}`) .join(", ")});` ) .join("")}` @@ -49,10 +51,10 @@ export function toOracle(diagram) { .join("\n")}\n${diagram.references .map( (r) => { - const deleteConstraint = r.deleteConstraint && ['CASCADE', 'SET NULL'].includes(r.deleteConstraint.toUpperCase()) - ? ` ON DELETE ${r.deleteConstraint.toUpperCase()}` + const deleteConstraint = r.deleteConstraint && ['CASCADE', 'SET NULL'].includes(r.deleteConstraint.toUpperCase()) + ? ` ON DELETE ${r.deleteConstraint.toUpperCase()}` : ''; - return `ALTER TABLE ${diagram.tables[r.startTableId].name} ADD CONSTRAINT ${diagram.tables[r.startTableId].name}_${diagram.tables[r.startTableId].fields[r.startFieldId].name}_fk\nFOREIGN KEY("${diagram.tables[r.startTableId].fields[r.startFieldId].name}") REFERENCES ${diagram.tables[r.endTableId].name}("${diagram.tables[r.endTableId].fields[r.endFieldId].name}")${deleteConstraint};` + return `ALTER TABLE ${diagram.tables[r.startTableId].name} ADD CONSTRAINT ${diagram.tables[r.startTableId].name}_${diagram.tables[r.startTableId].fields[r.startFieldId].name}_fk\nFOREIGN KEY(${diagram.tables[r.startTableId].fields[r.startFieldId].name}) REFERENCES ${diagram.tables[r.endTableId].name}(${diagram.tables[r.endTableId].fields[r.endFieldId].name})${deleteConstraint};` } ) .join("\n")}`; diff --git a/src/utils/importSQL/index.js b/src/utils/importSQL/index.js index fb849e430..e92ea7240 100644 --- a/src/utils/importSQL/index.js +++ b/src/utils/importSQL/index.js @@ -3,6 +3,7 @@ import { arrangeTables } from "../arrangeTables"; import { fromMariaDB } from "./mariadb"; import { fromMSSQL } from "./mssql"; import { fromMySQL } from "./mysql"; +import { fromOracleSQL } from "./oraclesql"; import { fromPostgres } from "./postgres"; import { fromSQLite } from "./sqlite"; @@ -24,6 +25,9 @@ export function importSQL(ast, toDb = DB.MYSQL, diagramDb = DB.GENERIC) { case DB.MSSQL: diagram = fromMSSQL(ast, diagramDb); break; + case DB.ORACLESQL: + diagram = fromOracleSQL(ast, diagramDb); + break; default: diagram = { tables: [], relationships: [] }; break; diff --git a/src/utils/importSQL/oracle.js b/src/utils/importSQL/oracle.js deleted file mode 100644 index 29df9e70a..000000000 --- a/src/utils/importSQL/oracle.js +++ /dev/null @@ -1,270 +0,0 @@ -import { RelationshipType, RelationshipCardinalities, DB } from "../../data/constants"; -import { dbToTypes } from "../../data/datatypes"; -import { buildSQLFromAST } from "./shared"; - -const affinity = { - [DB.MARIADB]: new Proxy( - { INT: "INTEGER" }, - { get: (target, prop) => (prop in target ? target[prop] : "BLOB") }, - ), - [DB.GENERIC]: new Proxy( - { - INT: "INTEGER", - TINYINT: "SMALLINT", - MEDIUMINT: "INTEGER", - BIT: "BOOLEAN", - YEAR: "INTEGER", - }, - { get: (target, prop) => (prop in target ? target[prop] : "BLOB") }, - ), -}; - -export function fromOracle(ast, diagramDb = DB.GENERIC) { - const tables = []; - const relationships = []; - - const parseSingleStatement = (e) => { - if (e.type === "create") { - if (e.keyword === "table") { - const table = {}; - table.name = e.table[0].table; - table.comment = ""; - table.color = "#175e7a"; - table.fields = []; - table.indices = []; - table.id = tables.length; - e.create_definitions.forEach((d) => { - if (d.resource === "column") { - const field = {}; - field.name = d.column.column; - - let type = d.definition.dataType; - if (!dbToTypes[diagramDb][type]) { - type = affinity[diagramDb][type]; - } - field.type = type; - - if (d.definition.expr && d.definition.expr.type === "expr_list") { - field.values = d.definition.expr.value.map((v) => v.value); - } - field.comment = d.comment ? d.comment.value.value : ""; - field.unique = false; - if (d.unique) field.unique = true; - field.increment = false; - if (d.auto_increment) field.increment = true; - field.notNull = false; - if (d.nullable) field.notNull = true; - field.primary = false; - if (d.primary_key) field.primary = true; - field.default = ""; - if (d.default_val) { - let defaultValue = ""; - if (d.default_val.value.type === "function") { - defaultValue = d.default_val.value.name.name[0].value; - 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) { - field.check = buildSQLFromAST(d.check.definition[0], DB.MARIADB); - } - - table.fields.push(field); - } else if (d.resource === "constraint") { - if (d.constraint_type === "primary key") { - d.definition.forEach((c) => { - table.fields.forEach((f) => { - if (f.name === c.column && !f.primary) { - f.primary = true; - } - }); - }); - } else if (d.constraint_type.toLowerCase() === "foreign key") { - 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 (endFieldId === -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; - - if (table.fields[startFieldId].unique) { - relationship.relationshipType = RelationshipType.ONE_TO_ONE; - relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label; - } else { - relationship.relationshipType = RelationshipType.ONE_TO_MANY; - relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label; - } - - relationships.push(relationship); - } - } - }); - table.fields.forEach((f, j) => { - f.id = j; - }); - tables.push(table); - } else if (e.keyword === "index") { - const index = {}; - index.name = e.index; - index.unique = false; - if (e.index_type === "unique") index.unique = true; - index.fields = []; - e.index_columns.forEach((f) => index.fields.push(f.column)); - - let found = -1; - tables.forEach((t, i) => { - if (found !== -1) return; - if (t.name === e.table.table) { - t.indices.push(index); - found = i; - } - }); - - if (found !== -1) tables[found].indices.forEach((i, j) => (i.id = j)); - } - } else if (e.type === "alter") { - e.expr.forEach((expr) => { - if ( - expr.action === "add" && - expr.create_definitions.constraint_type.toLowerCase() === "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); - } - }, - ); - - const startTableId = tables.findIndex((t) => t.name === startTable); - if (startTable === -1) return; - - const endTableId = tables.findIndex((t) => t.name === endTable); - if (endTableId === -1) return; - - const endFieldId = tables[endTableId].fields.findIndex( - (f) => f.name === endField, - ); - if (endFieldId === -1) return; - - const startFieldId = tables[startTableId].fields.findIndex( - (f) => f.name === startField, - ); - if (startFieldId === -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; - - if (tables[startTableId].fields[startFieldId].unique) { - relationship.relationshipType = RelationshipType.ONE_TO_ONE; - relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_ONE][0].label; - } else { - relationship.relationshipType = RelationshipType.ONE_TO_MANY; - relationship.cardinality = RelationshipCardinalities[RelationshipType.ONE_TO_MANY][0].label; - } - - relationships.push(relationship); - - relationships.forEach((r, i) => (r.id = i)); - } - }); - } - }; - - if (Array.isArray(ast)) { - ast.forEach((e) => parseSingleStatement(e)); - } else { - parseSingleStatement(ast); - } - - relationships.forEach((r, i) => (r.id = i)); - - return { tables, relationships }; -} - - diff --git a/src/utils/importSQL/oraclesql.js b/src/utils/importSQL/oraclesql.js new file mode 100644 index 000000000..deada3de8 --- /dev/null +++ b/src/utils/importSQL/oraclesql.js @@ -0,0 +1,306 @@ +import { Constraint, DB } from "../../data/constants"; +import { dbToTypes } from "../../data/datatypes"; + +const affinity = { + [DB.ORACLESQL]: new Proxy( + { INT: "INTEGER" }, + { NUMERIC: "NUMBER" }, + { DECIMAL: "NUMBER" }, + { CHARACTER: "CHAR" }, + { get: (target, prop) => (prop in target ? target[prop] : "BLOB") }, + ), + [DB.GENERIC]: new Proxy( + { + INTEGER: "INT", + MEDIUMINT: "INTEGER", + }, + { get: (target, prop) => (prop in target ? target[prop] : "BLOB") }, + ), +}; + +export function fromOracleSQL(ast, diagramDb = DB.GENERIC) { + const tables = []; + const relationships = []; + const enums = []; + + const parseSingleStatement = (e) => { + // Handle Oracle parser format + if (e.operation === "create" && e.object === "table") { + const table = {}; + table.name = e.name?.name || e.name; + table.comment = ""; + table.color = "#175e7a"; + table.fields = []; + table.indices = []; + table.id = tables.length; + // Check if table structure exists + if (!e.table) { + console.error("Table structure missing in AST:", e); + return; + } + // Check for different possible structures + const columns = e.table.relational_properties || e.table.columns || e.columns; + if (!columns) { + console.error("No columns found in table structure:", e.table); + return; + } + parseTableColumns(table, columns); + tables.push(table); + } + // Handle standard parser format (fallback) + else if (e.type === "create" && e.keyword === "table") { + const table = {}; + table.name = e.table[0].table; + table.comment = ""; + table.color = "#175e7a"; + table.fields = []; + table.indices = []; + table.id = tables.length; + if (e.create_definitions) { + parseStandardTableDefinitions(table, e.create_definitions); + } + tables.push(table); + } + }; + + const parseTableColumns = (table, columns) => { + columns.forEach((d) => { + if (d.resource === "column" || d.type) { + const field = {}; + field.name = d.name; + + // Handle different type structures + let typeInfo = d.type || d; + let type = typeInfo.type?.toUpperCase() || typeInfo.dataType?.toUpperCase() || "VARCHAR2"; + if (!dbToTypes[diagramDb][type]) { + type = affinity[diagramDb][type] || type; + } + field.type = type; + + // Handle precision and scale + if (typeInfo.scale && typeInfo.precision) { + field.size = typeInfo.precision + "," + typeInfo.scale; + } else if (typeInfo.size || typeInfo.precision) { + field.size = typeInfo.size || typeInfo.precision; + } + + field.comment = ""; + field.check = ""; + field.default = ""; + field.unique = false; + field.increment = false; + field.notNull = false; + field.primary = false; + + // Handle constraints - check if they exist + const constraints = d.constraints || []; + for (const c of constraints) { + if (c.constraint.primary_key === "primary key") + field.primary = true; + if (c.constraint.not_null === "not null") field.notNull = true; + if (c.constraint.unique === "unique") field.unique = true; + } + + if (d.identity) { + field.increment = true; + } + + // TODO: reconstruct default when implemented in parser + if (d.default) { + field.default = JSON.stringify(d.default.expr); + } + + table.fields.push(field); + } else if (d.resource === "constraint") { + parseConstraint(table, d); + } + }); + }; + + const parseStandardTableDefinitions = (table, definitions) => { + definitions.forEach((d) => { + if (d.resource === "column") { + const field = {}; + field.name = d.column.column; + + let type = d.definition.dataType; + if (!dbToTypes[diagramDb][type]) { + type = affinity[diagramDb][type] || type; + } + field.type = type; + + field.comment = d.comment ? d.comment.value.value : ""; + field.check = ""; + field.default = d.default_val ? d.default_val.value.value : ""; + field.unique = false; + field.increment = false; + field.notNull = false; + field.primary = false; + + // Handle size/precision + if (d.definition.length) { + if (Array.isArray(d.definition.length)) { + // Handle precision and scale: [precision, scale] + field.size = d.definition.length.join(","); + } else { + field.size = d.definition.length; + } + } + + // Handle NOT NULL constraint + if (d.nullable && d.nullable.value === "not null") { + field.notNull = true; + } + + table.fields.push(field); + } else if (d.resource === "constraint") { + // Handle table-level constraints + if (d.constraint_type === "primary key") { + if (d.definition && Array.isArray(d.definition)) { + d.definition.forEach(colRef => { + const columnName = colRef.column; + const field = table.fields.find(f => f.name === columnName); + if (field) { + field.primary = true; + } else { + console.warn("Could not find field for primary key:", columnName); + } + }); + } + } + if (d.constraint_type === "FOREIGN KEY" || d.constraint_type === "foreign key") { + // Handle foreign key constraints + // Extract foreign key information + const sourceColumn = d.definition[0]?.column; + const targetTable = d.reference_definition?.table[0]?.table; + const targetColumn = d.reference_definition?.definition[0]?.column; + // Store foreign key for later processing + if (!table.foreignKeys) table.foreignKeys = []; + table.foreignKeys.push({ + sourceColumn, + targetTable, + targetColumn, + constraintName: d.constraint + }); + } + } + }); + // Set field IDs + table.fields.forEach((f, j) => { + f.id = j; + }); + }; + + const parseConstraint = (table, d) => { + // Handle PRIMARY KEY constraints + if (d.constraint.primary_key) { + if (d.constraint.columns && Array.isArray(d.constraint.columns)) { + d.constraint.columns.forEach(columnName => { + const field = table.fields.find(f => f.name === columnName); + if (field) { + field.primary = true; + } else { + console.warn("Could not find field for primary key:", columnName); + } + }); + } + } + // Check if this is a foreign key constraint + if (d.constraint.reference) { + const relationship = {}; + const startTableId = table.id; + // Validate structure before accessing + if (!d.constraint.columns || !d.constraint.reference.columns) { + console.warn("Invalid constraint structure - missing columns:", d.constraint); + return; + } + const startField = d.constraint.columns[0]; + const endField = d.constraint.reference.columns[0]; + const endTable = d.constraint.reference.object.name; + + const endTableId = tables.findIndex((t) => t.name === endTable); + if (endTableId === -1) return; + + const endFieldId = tables[endTableId].fields.findIndex( + (f) => f.name === endField, + ); + if (endFieldId === -1) return; + + const startFieldId = table.fields.findIndex( + (f) => f.name === startField, + ); + if (startFieldId === -1) return; + + relationship.startTableId = startTableId; + relationship.startFieldId = startFieldId; + relationship.endTableId = endTableId; + relationship.endFieldId = endFieldId; + relationship.updateConstraint = Constraint.NONE; + relationship.name = + d.name && Boolean(d.name.trim()) + ? d.name + : "fk_" + table.name + "_" + startField + "_" + endTable; + relationship.deleteConstraint = + d.constraint.reference.on_delete && + Boolean(d.constraint.reference.on_delete.trim()) + ? d.constraint.reference.on_delete[0].toUpperCase() + + d.constraint.reference.on_delete.substring(1) + : Constraint.NONE; + + relationships.push(relationship); + } + }; + + ast.forEach((e) => parseSingleStatement(e)); + + // Process stored foreign keys after all tables are created + tables.forEach((table) => { + if (table.foreignKeys && table.foreignKeys.length > 0) { + table.foreignKeys.forEach((fk) => { + const relationship = {}; + const foreignKeyField = fk.sourceColumn; + const primaryKeyTable = fk.targetTable; + const primaryKeyField = fk.targetColumn; + // The table that contains the Primary Key is the START + const startTableId = tables.findIndex((t) => t.name === primaryKeyTable); + if (startTableId === -1) { + console.warn("Could not find referenced table:", primaryKeyTable); + return; + } + + const startFieldId = tables[startTableId].fields.findIndex( + (f) => f.name === primaryKeyField, + ); + if (startFieldId === -1) { + console.warn("Could not find referenced field:", primaryKeyField); + return; + } + + // La tabla que contiene la Foreign Key es el END + const endFieldId = table.fields.findIndex( + (f) => f.name === foreignKeyField, + ); + if (endFieldId === -1) { + console.warn("Could not find source field:", foreignKeyField); + return; + } + + relationship.id = relationships.length; + relationship.startTableId = startTableId; // Table with PK + relationship.startFieldId = startFieldId; // PK field + relationship.endTableId = table.id; // Table with FK + relationship.endFieldId = endFieldId; // FK field + relationship.updateConstraint = Constraint.NONE; + relationship.name = fk.constraintName || + "fk_" + primaryKeyTable + "_" + primaryKeyField + "_" + table.name; + relationship.deleteConstraint = Constraint.NONE; + + relationships.push(relationship); + }); + } + }); + + relationships.forEach((r, i) => (r.id = i)); + + return { tables, relationships, enums }; +} diff --git a/test/oracle__u_idx.test.js b/test/oracle__u_idx.test.js index e7c3895dd..6ded4dd1f 100644 --- a/test/oracle__u_idx.test.js +++ b/test/oracle__u_idx.test.js @@ -1,8 +1,8 @@ /* eslint-env jest */ -import { toOracle } from "../src/utils/exportSQL/oracle.js"; +import { toOracleSQL } from "../src/utils/exportSQL/oraclesql.js"; import { DB } from "../src/data/constants.js"; -describe("toOracle", () => { +describe("toOracleSQL", () => { test("test for unique index creation", () => { const diagram = { database: DB.ORACLE, @@ -39,16 +39,16 @@ describe("toOracle", () => { }; const expectedSQL = `CREATE TABLE mochila ( -\t"mochila_id" NUMBER(10,0) NOT NULL, -\t"capacidad_kg" NUMBER(2,0) NOT NULL, -\tCONSTRAINT mochila_pk PRIMARY KEY("mochila_id") +\tmochila_id NUMBER(10,0) NOT NULL, +\tcapacidad_kg NUMBER(2,0) NOT NULL, +\tCONSTRAINT mochila_pk PRIMARY KEY(mochila_id) ); -CREATE UNIQUE INDEX "mochila_index_0" ON mochila ("mochila_id"); -`; +CREATE UNIQUE INDEX mochila_index_0 ON mochila (mochila_id); +`; - const result = toOracle(diagram); + const result = toOracleSQL(diagram); expect(result.trim()).toBe(expectedSQL.trim()); }); }); diff --git a/test/oracle_c_idx.test.js b/test/oracle_c_idx.test.js index 5815fe1c8..3d6118780 100644 --- a/test/oracle_c_idx.test.js +++ b/test/oracle_c_idx.test.js @@ -1,8 +1,8 @@ /* eslint-env jest */ -import { toOracle } from "../src/utils/exportSQL/oracle.js"; +import { toOracleSQL } from "../src/utils/exportSQL/oraclesql.js"; import { DB } from "../src/data/constants.js"; -describe("toOracle", () => { +describe("toOracleSQL", () => { test("test for unique index creation", () => { const diagram = { database: DB.ORACLE, @@ -46,17 +46,17 @@ describe("toOracle", () => { }; const expectedSQL = `CREATE TABLE mochila ( -\t"mochila_id" NUMBER(10,0) NOT NULL, -\t"capacidad_kg" NUMBER(2,0) NOT NULL, -\t"compartimentos" NUMBER(2,0) NOT NULL, -\tCONSTRAINT mochila_pk PRIMARY KEY("mochila_id") +\tmochila_id NUMBER(10,0) NOT NULL, +\tcapacidad_kg NUMBER(2,0) NOT NULL, +\tcompartimentos NUMBER(2,0) NOT NULL, +\tCONSTRAINT mochila_pk PRIMARY KEY(mochila_id) ); -CREATE UNIQUE INDEX "mochila_index_0" ON mochila ("capacidad_kg", "compartimentos"); -`; +CREATE UNIQUE INDEX mochila_index_0 ON mochila (capacidad_kg, compartimentos); +`; - const result = toOracle(diagram); + const result = toOracleSQL(diagram); expect(result.trim()).toBe(expectedSQL.trim()); }); }); diff --git a/test/oracle_chk.test.js b/test/oracle_chk.test.js index c9e05a99c..6cfcbc2ff 100644 --- a/test/oracle_chk.test.js +++ b/test/oracle_chk.test.js @@ -1,8 +1,8 @@ /* eslint-env jest */ -import { toOracle } from "../src/utils/exportSQL/oracle.js"; +import { toOracleSQL } from "../src/utils/exportSQL/oraclesql.js"; import { DB } from "../src/data/constants.js"; -describe("toOracle", () => { +describe("toOracleSQL", () => { test("test for check constraint in any field of a table", () => { const diagram = { database: DB.ORACLE, @@ -34,13 +34,13 @@ describe("toOracle", () => { }; const expectedSQL = `CREATE TABLE salon ( -\t"salon_id" NUMBER(10,0) NOT NULL, -\t"capacidad" NUMBER(10,0) NOT NULL, -\tCONSTRAINT salon_capacidad_chk CHECK("capacidad" > 0), -\tCONSTRAINT salon_pk PRIMARY KEY("salon_id") -);`; +\tsalon_id NUMBER(10,0) NOT NULL, +\tcapacidad NUMBER(10,0) NOT NULL, +\tCONSTRAINT salon_capacidad_chk CHECK(capacidad > 0), +\tCONSTRAINT salon_pk PRIMARY KEY(salon_id) +);`; - const result = toOracle(diagram); + const result = toOracleSQL(diagram); expect(result.trim()).toBe(expectedSQL.trim()); }); }); diff --git a/test/oracle_export.test.js b/test/oracle_export.test.js index e631a8d20..5ec65e3cd 100644 --- a/test/oracle_export.test.js +++ b/test/oracle_export.test.js @@ -1,8 +1,8 @@ /* eslint-env jest */ -import { toOracle } from "../src/utils/exportSQL/oracle.js"; +import { toOracleSQL } from "../src/utils/exportSQL/oraclesql.js"; import { DB } from "../src/data/constants.js"; -describe("toOracle", () => { +describe("toOracleSQL", () => { test("should generate correct Oracle SQL for tables with relationships", () => { const diagram = { database: DB.ORACLE, @@ -54,22 +54,22 @@ describe("toOracle", () => { }; const expectedSQL = `CREATE TABLE casa ( -\t"id" NUMBER(10,0) NOT NULL, -\t"xd" VARCHAR(255) NOT NULL, -\tCONSTRAINT casa_pk PRIMARY KEY("id") +\tid NUMBER(10,0) NOT NULL, +\txd VARCHAR(255) NOT NULL, +\tCONSTRAINT casa_pk PRIMARY KEY(id) ); CREATE TABLE cuarto ( -\t"id" NUMBER, -\tCONSTRAINT cuarto_pk PRIMARY KEY("id") +\tid NUMBER, +\tCONSTRAINT cuarto_pk PRIMARY KEY(id) ); ALTER TABLE casa ADD CONSTRAINT casa_id_fk -FOREIGN KEY("id") REFERENCES cuarto("id");`; +FOREIGN KEY(id) REFERENCES cuarto(id);`; - const result = toOracle(diagram); + const result = toOracleSQL(diagram); expect(result.trim()).toBe(expectedSQL.trim()); }); -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/test/oracle_pk_fk.test.js b/test/oracle_pk_fk.test.js index e631a8d20..5ec65e3cd 100644 --- a/test/oracle_pk_fk.test.js +++ b/test/oracle_pk_fk.test.js @@ -1,8 +1,8 @@ /* eslint-env jest */ -import { toOracle } from "../src/utils/exportSQL/oracle.js"; +import { toOracleSQL } from "../src/utils/exportSQL/oraclesql.js"; import { DB } from "../src/data/constants.js"; -describe("toOracle", () => { +describe("toOracleSQL", () => { test("should generate correct Oracle SQL for tables with relationships", () => { const diagram = { database: DB.ORACLE, @@ -54,22 +54,22 @@ describe("toOracle", () => { }; const expectedSQL = `CREATE TABLE casa ( -\t"id" NUMBER(10,0) NOT NULL, -\t"xd" VARCHAR(255) NOT NULL, -\tCONSTRAINT casa_pk PRIMARY KEY("id") +\tid NUMBER(10,0) NOT NULL, +\txd VARCHAR(255) NOT NULL, +\tCONSTRAINT casa_pk PRIMARY KEY(id) ); CREATE TABLE cuarto ( -\t"id" NUMBER, -\tCONSTRAINT cuarto_pk PRIMARY KEY("id") +\tid NUMBER, +\tCONSTRAINT cuarto_pk PRIMARY KEY(id) ); ALTER TABLE casa ADD CONSTRAINT casa_id_fk -FOREIGN KEY("id") REFERENCES cuarto("id");`; +FOREIGN KEY(id) REFERENCES cuarto(id);`; - const result = toOracle(diagram); + const result = toOracleSQL(diagram); expect(result.trim()).toBe(expectedSQL.trim()); }); -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/test/oracle_unique.test.js b/test/oracle_unique.test.js index 177c9c043..9d375f32d 100644 --- a/test/oracle_unique.test.js +++ b/test/oracle_unique.test.js @@ -1,8 +1,8 @@ /* eslint-env jest */ -import { toOracle } from "../src/utils/exportSQL/oracle.js"; +import { toOracleSQL } from "../src/utils/exportSQL/oraclesql.js"; import { DB } from "../src/data/constants.js"; -describe("toOracle", () => { +describe("toOracleSQL", () => { test("test for unique constraints", () => { const diagram = { database: DB.ORACLE, @@ -34,13 +34,13 @@ describe("toOracle", () => { }; const expectedSQL = `CREATE TABLE computador ( -\t"computador_id" NUMBER(10,0) NOT NULL, -\t"num_serie" VARCHAR2(40) NOT NULL, -\tCONSTRAINT computador_num_serie_uk UNIQUE("num_serie"), -\tCONSTRAINT computador_pk PRIMARY KEY("computador_id") +\tcomputador_id NUMBER(10,0) NOT NULL, +\tnum_serie VARCHAR2(40) NOT NULL, +\tCONSTRAINT computador_num_serie_uk UNIQUE(num_serie), +\tCONSTRAINT computador_pk PRIMARY KEY(computador_id) );`; - const result = toOracle(diagram); + const result = toOracleSQL(diagram); expect(result.trim()).toBe(expectedSQL.trim()); }); }); \ No newline at end of file