diff --git a/package-lock.json b/package-lock.json index 9e4af97..2fa0011 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "lexical": "^0.12.5", "node-sql-parser": "^5.3.8", "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", @@ -5373,6 +5374,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 fdd3389..5cbb190 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "lexical": "^0.12.5", "node-sql-parser": "^5.3.8", "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 0000000..ef0f66b 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 0000000..cb7686e 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 993acd8..824640e 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,6 +31,7 @@ import { jsonToSQLite, jsonToMariaDB, jsonToSQLServer, + jsonToOracleSQL, } from "../../utils/exportSQL/generic"; import { ObjectType, @@ -793,13 +795,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", }, ], }, @@ -807,34 +811,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", + }, + { + function: () => { + setModal(MODAL.IMPORT_SRC); + setImportDb(DB.ORACLESQL); + }, + name: "Oracle", + label: "Beta", }, ], }), @@ -848,7 +865,8 @@ export default function ControlPanel({ ...(database === DB.GENERIC && { children: [ { - MySQL: () => { + name: "MySQL", + function: () => { setModal(MODAL.CODE); const src = jsonToMySQL({ tables: tables, @@ -864,7 +882,8 @@ export default function ControlPanel({ }, }, { - PostgreSQL: () => { + name: "PostgreSQL", + function: () => { setModal(MODAL.CODE); const src = jsonToPostgreSQL({ tables: tables, @@ -880,7 +899,8 @@ export default function ControlPanel({ }, }, { - SQLite: () => { + name: "SQLite", + function: () => { setModal(MODAL.CODE); const src = jsonToSQLite({ tables: tables, @@ -896,7 +916,8 @@ export default function ControlPanel({ }, }, { - MariaDB: () => { + name: "MariaDB", + function: () => { setModal(MODAL.CODE); const src = jsonToMariaDB({ tables: tables, @@ -912,7 +933,8 @@ export default function ControlPanel({ }, }, { - MSSQL: () => { + name: "MSSQL", + function: () => { setModal(MODAL.CODE); const src = jsonToSQLServer({ tables: tables, @@ -927,6 +949,24 @@ export default function ControlPanel({ })); }, }, + { + label: "Beta", + name: "Oracle", + function: () => { + setModal(MODAL.CODE); + const src = jsonToOracleSQL({ + tables: tables, + references: relationships, + types: types, + database: database, + }); + setExportData((prev) => ({ + ...prev, + data: src, + extension: "sql", + })); + }, + }, ], }), function: () => { @@ -949,7 +989,8 @@ export default function ControlPanel({ export_as: { children: [ { - PNG: () => { + name: "PNG", + function: () => { toPng(document.getElementById("canvas")).then(function (dataUrl) { setExportData((prev) => ({ ...prev, @@ -961,7 +1002,8 @@ export default function ControlPanel({ }, }, { - JPEG: () => { + name: "JPEG", + function: () => { toJpeg(document.getElementById("canvas"), { quality: 0.95 }).then( function (dataUrl) { setExportData((prev) => ({ @@ -975,7 +1017,8 @@ export default function ControlPanel({ }, }, { - SVG: () => { + name: "SVG", + function: () => { const filter = (node) => node.tagName !== "i"; toSvg(document.getElementById("canvas"), { filter: filter }).then( function (dataUrl) { @@ -990,7 +1033,8 @@ export default function ControlPanel({ }, }, { - JSON: () => { + name: "JSON", + function: () => { setModal(MODAL.CODE); const result = JSON.stringify( { @@ -1014,7 +1058,8 @@ export default function ControlPanel({ }, }, { - DBML: () => { + name: "DBML", + function: () => { setModal(MODAL.CODE); const result = toDBML({ tables, @@ -1029,7 +1074,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", [ @@ -1049,7 +1095,8 @@ export default function ControlPanel({ }, }, { - MERMAID: () => { + name: "Mermaid", + function: () => { setModal(MODAL.CODE); const result = jsonToMermaid({ tables: tables, @@ -1067,7 +1114,8 @@ export default function ControlPanel({ }, }, { - readme: () => { + name: "Markdown", + function: () => { setModal(MODAL.CODE); const result = jsonToDocumentation({ tables: tables, @@ -1271,7 +1319,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"); @@ -1281,7 +1330,8 @@ export default function ControlPanel({ }, }, { - dark: () => { + name: t("dark"), + function: () => { const body = document.body; if (body.hasAttribute("theme-mode")) { body.setAttribute("theme-mode", "dark"); @@ -1602,9 +1652,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(); } } }} @@ -1703,7 +1753,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 270172e..34cb712 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, @@ -131,12 +132,21 @@ export default function Modal({ }; const parseSQLAndLoadDiagram = () => { - const parser = new Parser(); + const targetDatabase = database === DB.GENERIC ? importDb : database; + let ast = null; try { - ast = parser.astify(importSource.src, { - database: database === DB.GENERIC ? importDb : database, - }); + if (targetDatabase === DB.ORACLESQL) { + const oracleParser = new OracleParser(); + + ast = oracleParser.parse(importSource.src); + } 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}` diff --git a/src/components/Workspace.jsx b/src/components/Workspace.jsx index 1797851..faab921 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"; @@ -161,7 +161,7 @@ export default function WorkSpace() { enums, gistId, loadedFromGistId, - saveState + saveState, ]); const load = useCallback(async () => { @@ -467,17 +467,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 /^-?\d+(\.\d+)?$/.test(field.default); + }, + hasCheck: true, + isSized: false, + hasPrecision: true, + canIncrement: false, + }, FLOAT: { type: "FLOAT", checkDefault: (field) => { @@ -110,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, @@ -140,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, @@ -223,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, @@ -405,7 +447,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, @@ -913,8 +957,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, @@ -939,7 +984,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()) ); }, @@ -1118,7 +1164,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; } @@ -1128,7 +1178,7 @@ const postgresTypesBase = { hasPrecision: false, hasQuotes: true, }, - HALFVEC:{ + HALFVEC: { type: "HALFVEC", checkDefault: (field) => { let elements; @@ -1138,7 +1188,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; } @@ -1155,9 +1209,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, @@ -1320,7 +1374,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, @@ -1581,7 +1637,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, @@ -1747,6 +1805,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, @@ -1754,6 +2024,7 @@ const dbToTypesBase = { [DB.SQLITE]: sqliteTypes, [DB.MSSQL]: mssqlTypes, [DB.MARIADB]: mysqlTypes, + [DB.ORACLESQL]: oraclesqlTypes, }; export const dbToTypes = new Proxy(dbToTypesBase, { diff --git a/src/pages/LandingPage.jsx b/src/pages/LandingPage.jsx index 5bda06e..700ff36 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"; @@ -150,7 +151,7 @@ export default function LandingPage() {
Design for your database
-
+
{dbs.map((s, i) => ( `\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")}`) + .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` @@ -247,7 +291,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)}` : "" @@ -377,7 +421,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 !== "" @@ -436,7 +480,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 @@ -449,7 +493,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" : "" @@ -493,3 +537,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 ad877da..987fb26 100644 --- a/src/utils/exportSQL/index.js +++ b/src/utils/exportSQL/index.js @@ -2,6 +2,7 @@ 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"; @@ -17,6 +18,8 @@ export function exportSQL(diagram) { return toMariaDB(diagram); case DB.MSSQL: return toMSSQL(diagram); + case DB.ORACLESQL: + return toOracleSQL(diagram); default: return ""; } diff --git a/src/utils/exportSQL/oraclesql.js b/src/utils/exportSQL/oraclesql.js new file mode 100644 index 0000000..81d8081 --- /dev/null +++ b/src/utils/exportSQL/oraclesql.js @@ -0,0 +1,56 @@ +import { dbToTypes } from "../../data/datatypes"; +import { parseDefault } from "./shared"; + +export function toOracleSQL(diagram) { + return `${diagram.tables + .map( + (table) => + `${ + table.comment === "" ? "" : `/* ${table.comment} */\n` + }CREATE TABLE "${table.name}" (\n${table.fields + .map( + (field) => + `${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t"${ + field.name + }" ${field.type}${field.size && Boolean(field.size.trim()) ? "(" + field.size + ")" : ""}${ + field.notNull ? " NOT NULL" : "" + }${ + field.increment ? " GENERATED ALWAYS AS IDENTITY" : "" + }${field.unique ? " UNIQUE" : ""}${ + field.default !== "" + ? ` DEFAULT ${parseDefault(field, diagram.database)}` + : "" + }${ + field.check === "" || + !dbToTypes[diagram.database][field.type].hasCheck + ? "" + : ` CHECK(${field.check})` + }${field.comment ? ` -- ${field.comment}` : ""}`, + ) + .join(",\n")}${ + table.fields.filter((f) => f.primary).length > 0 + ? `,\n\tPRIMARY KEY(${table.fields + .filter((f) => f.primary) + .map((f) => `"${f.name}"`) + .join(", ")})` + : "" + }\n)${table.comment ? ` -- ${table.comment}` : ""};\n${`\n${table.indices + .map( + (i) => + `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name}"\nON "${table.name}" (${i.fields + .map((f) => `"${f}"`) + .join(", ")});`, + ) + .join("")}`}`, + ) + .join("\n")}\n${diagram.references + .map( + (r) => + `ALTER TABLE "${diagram.tables[r.startTableId].name}"\nADD CONSTRAINT "${r.name}" FOREIGN KEY ("${ + diagram.tables[r.startTableId].fields[r.startFieldId].name + }") REFERENCES "${diagram.tables[r.endTableId].name}" ("${ + diagram.tables[r.endTableId].fields[r.endFieldId].name + }")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};`, + ) + .join("\n")}`; +} diff --git a/src/utils/importSQL/index.js b/src/utils/importSQL/index.js index fb849e4..e92ea72 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/oraclesql.js b/src/utils/importSQL/oraclesql.js new file mode 100644 index 0000000..790f0e2 --- /dev/null +++ b/src/utils/importSQL/oraclesql.js @@ -0,0 +1,137 @@ +import { Cardinality, 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) => { + console.log(e); + if (e.operation === "create") { + if (e.object === "table") { + const table = {}; + table.name = e.name.name; + table.comment = ""; + table.color = "#175e7a"; + table.fields = []; + table.indices = []; + table.id = tables.length; + e.table.relational_properties.forEach((d) => { + if (d.resource === "column") { + const field = {}; + field.name = d.name; + + let type = d.type.type.toUpperCase(); + if (!dbToTypes[diagramDb][type]) { + type = affinity[diagramDb][type]; + } + field.type = type; + + if (d.type.scale && d.type.precision) { + field.size = d.type.precision + "," + d.type.scale; + } else if (d.type.size || d.type.precision) { + field.size = d.type.size || d.type.precision; + } + + field.comment = ""; + field.check = ""; + field.default = ""; + field.unique = false; + field.increment = false; + field.notNull = false; + field.primary = false; + + for (const c of d.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") { + const relationship = {}; + const startTableId = table.id; + 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; + + if (table.fields[startFieldId].unique) { + relationship.cardinality = Cardinality.ONE_TO_ONE; + } else { + relationship.cardinality = Cardinality.MANY_TO_ONE; + } + + relationships.push(relationship); + } + }); + table.fields.forEach((f, j) => { + f.id = j; + }); + tables.push(table); + } + } + }; + + ast.forEach((e) => parseSingleStatement(e)); + + relationships.forEach((r, i) => (r.id = i)); + + return { tables, relationships, enums }; +}