feat:Added support for exporting SQL as OracleDB SQL (#192)

* feat:Added support for exporting SQL as OracleDB SQL

* Add parser

* Import from sql

* Add beta tag

* Export from generic

* Fix export

* Add import affinity

---------

Co-authored-by: 1ilit <1ilit@proton.me>
This commit is contained in:
Aditya 2025-04-02 04:41:08 +05:30 committed by GitHub
parent fcc16bbe85
commit 16ea2ee23c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 739 additions and 60 deletions

7
package-lock.json generated
View File

@ -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",

View File

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/assets/oraclesql.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -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 (
<Dropdown
style={{ width: "120px" }}
style={{ width: "150px" }}
key={item}
position="rightTop"
render={
@ -1712,9 +1762,18 @@ export default function ControlPanel({
(e, i) => (
<Dropdown.Item
key={i}
onClick={Object.values(e)[0]}
onClick={e.function}
className="flex justify-between"
>
{t(Object.keys(e)[0])}
<span>{e.name}</span>
{e.label && (
<Tag
size="small"
color="light-blue"
>
{e.label}
</Tag>
)}
</Dropdown.Item>
),
)}

View File

@ -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}`

View File

@ -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() {
<div
key={x.name}
onClick={() => 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"}`}
>
<div className="font-semibold">{x.name}</div>
<div className="flex items-center justify-between">
<div className="font-semibold">{x.name}</div>
{x.beta && (
<Tag size="small" color="light-blue">
Beta
</Tag>
)}
</div>
{x.image && (
<img
src={x.image}
className="h-10"
className="h-8"
style={{
filter:
"opacity(0.4) drop-shadow(0 0 0 white) drop-shadow(0 0 0 white)",

View File

@ -113,6 +113,7 @@ export const DB = {
MSSQL: "transactsql",
SQLITE: "sqlite",
MARIADB: "mariadb",
ORACLESQL: "oraclesql",
GENERIC: "generic",
};

View File

@ -3,6 +3,7 @@ import postgresImage from "../assets/postgres-icon.png";
import sqliteImage from "../assets/sqlite-icon.png";
import mariadbImage from "../assets/mariadb-icon.png";
import mssqlImage from "../assets/mssql-icon.png";
import oraclesqlImage from "../assets/oraclesql-icon.png";
import i18n from "../i18n/i18n";
import { DB } from "./constants";
@ -42,6 +43,15 @@ export const databases = new Proxy(
image: mssqlImage,
hasTypes: false,
},
[DB.ORACLESQL]: {
name: "Oracle SQL",
label: DB.ORACLESQL,
image: oraclesqlImage,
hasTypes: false,
hasEnums: false,
hasArrays: false,
beta: true,
},
[DB.GENERIC]: {
name: i18n.t("generic"),
label: DB.GENERIC,

View File

@ -55,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) => {
@ -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, {

View File

@ -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() {
<div className="text-lg font-medium text-center mt-12 mb-6">
Design for your database
</div>
<div className="flex justify-center items-center gap-8 md:block">
<div className="grid grid-cols-3 place-items-center sm:grid-cols-1 sm:gap-10">
{dbs.map((s, i) => (
<img
key={"icon-" + i}
@ -300,6 +301,7 @@ const dbs = [
{ icon: sqlite_icon, height: 64 },
{ icon: mariadb_icon, height: 64 },
{ icon: sql_server_icon, height: 64 },
{ icon: oraclesql_icon, height: 172 },
];
const features = [

View File

@ -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;
}
}
@ -213,13 +255,15 @@ 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")}`)
.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")}`;
}

View File

@ -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 "";
}

View File

@ -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")}`;
}

View File

@ -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;

View File

@ -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 };
}