Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
399 changes: 354 additions & 45 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run"
},
"dependencies": {
"@dbml/core": "^3.13.9",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@douyinfe/semi-ui": "^2.77.1",
"@guanmingchiu/sqlparser-ts": "^0.61.1",
"@lexical/react": "^0.12.5",
"@monaco-editor/react": "^4.7.0",
"@vercel/analytics": "^1.2.2",
Expand All @@ -33,8 +35,6 @@
"lodash": "^4.17.23",
"luxon": "^3.7.1",
"nanoid": "^5.1.5",
"node-sql-parser": "^5.4.0",
"oracle-sql-parser": "^0.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hotkeys-hook": "^4.4.1",
Expand All @@ -56,6 +56,7 @@
"postcss": "^8.4.32",
"prettier": "3.2.5",
"tailwindcss": "^4.0.14",
"vite": "^6.4.1"
"vite": "^6.4.1",
"vitest": "^4.0.18"
}
}
38 changes: 22 additions & 16 deletions src/components/EditorHeader/Modal/Modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import {
Toast,
} from "@douyinfe/semi-ui";
import { saveAs } from "file-saver";
import { Parser } from "node-sql-parser";
import { Parser as OracleParser } from "oracle-sql-parser";
import { useContext, useState } from "react";
import { init as initSqlParser, parse as parseSql } from "@guanmingchiu/sqlparser-ts";
import { useContext, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { DB, MODAL, STATUS, State } from "../../../data/constants";
import { databases } from "../../../data/databases";
Expand Down Expand Up @@ -160,25 +159,32 @@ export default function Modal({
});
};

const parseSQLAndLoadDiagram = () => {
const dbToDialect = {
[DB.MYSQL]: "mysql",
[DB.POSTGRES]: "postgresql",
[DB.SQLITE]: "sqlite",
[DB.MARIADB]: "mysql",
[DB.MSSQL]: "mssql",
[DB.ORACLESQL]: "oracle",
[DB.GENERIC]: "generic",
};

const parserInitialized = useRef(false);

const parseSQLAndLoadDiagram = async () => {
const targetDatabase = database === DB.GENERIC ? importDb : database;
const dialect = dbToDialect[targetDatabase] || "generic";

let ast = null;
try {
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,
});
if (!parserInitialized.current) {
await initSqlParser();
parserInitialized.current = true;
}
ast = parseSql(importSource.src, dialect);
} catch (error) {
const message = error.location
? `${error.name} [Ln ${error.location.start.line}, Col ${error.location.start.column}]: ${error.message}`
? `${error.name} [Ln ${error.location.line}, Col ${error.location.column}]: ${error.message}`
: error.message;

setError({ type: STATUS.ERROR, message });
Expand Down Expand Up @@ -257,7 +263,7 @@ export default function Modal({
}
return;
case MODAL.IMPORT_SRC:
parseSQLAndLoadDiagram();
await parseSQLAndLoadDiagram();
return;
case MODAL.OPEN:
if (selectedDiagramId === 0) return;
Expand Down
70 changes: 70 additions & 0 deletions src/utils/importSQL/__tests__/mariadb.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, it, expect } from "vitest";
import { parse } from "@guanmingchiu/sqlparser-ts";
import { fromMariaDB } from "../mariadb";
import { DB } from "../../../data/constants";

// MariaDB uses MySQL dialect in sqlparser-ts
function parseMariaDB(sql) {
return parse(sql, "mysql");
}

describe("fromMariaDB", () => {
it("parses a basic CREATE TABLE", () => {
const ast = parseMariaDB(
"CREATE TABLE users (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) NOT NULL);",
);
const result = fromMariaDB(ast, DB.MARIADB);

expect(result.tables).toHaveLength(1);
expect(result.tables[0].fields[0].primary).toBe(true);
expect(result.tables[0].fields[0].increment).toBe(true);
expect(result.tables[0].fields[1].notNull).toBe(true);
});

it("parses FOREIGN KEY constraints", () => {
const ast = parseMariaDB(`
CREATE TABLE departments (id INT PRIMARY KEY);
CREATE TABLE users (
id INT PRIMARY KEY,
dept_id INT,
FOREIGN KEY (dept_id) REFERENCES departments(id) ON DELETE CASCADE ON UPDATE NO ACTION
);
`);
const result = fromMariaDB(ast, DB.MARIADB);

expect(result.relationships).toHaveLength(1);
expect(result.relationships[0].deleteConstraint).toBe("Cascade");
expect(result.relationships[0].updateConstraint).toBe("No action");
});

it("parses table comments", () => {
const ast = parseMariaDB(
"CREATE TABLE users (id INT) COMMENT = 'User table';",
);
const result = fromMariaDB(ast, DB.MARIADB);

expect(result.tables[0].comment).toBe("User table");
});

it("parses ALTER TABLE ADD FOREIGN KEY", () => {
const ast = parseMariaDB(`
CREATE TABLE a (id INT PRIMARY KEY);
CREATE TABLE b (id INT PRIMARY KEY, a_id INT);
ALTER TABLE b ADD CONSTRAINT fk_a FOREIGN KEY (a_id) REFERENCES a(id);
`);
const result = fromMariaDB(ast, DB.MARIADB);

expect(result.relationships).toHaveLength(1);
});

it("parses CREATE INDEX", () => {
const ast = parseMariaDB(`
CREATE TABLE users (id INT PRIMARY KEY, email VARCHAR(255));
CREATE INDEX idx_email ON users (email);
`);
const result = fromMariaDB(ast, DB.MARIADB);

expect(result.tables[0].indices).toHaveLength(1);
expect(result.tables[0].indices[0].unique).toBe(false);
});
});
78 changes: 78 additions & 0 deletions src/utils/importSQL/__tests__/mssql.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect } from "vitest";
import { parse } from "@guanmingchiu/sqlparser-ts";
import { fromMSSQL } from "../mssql";
import { DB } from "../../../data/constants";

function parseMSSQL(sql) {
return parse(sql, "mssql");
}

describe("fromMSSQL", () => {
it("parses a basic CREATE TABLE", () => {
const ast = parseMSSQL(
"CREATE TABLE users (id INT PRIMARY KEY, name NVARCHAR(100));",
);
const result = fromMSSQL(ast, DB.MSSQL);

expect(result.tables).toHaveLength(1);
expect(result.tables[0].name).toBe("users");
expect(result.tables[0].fields).toHaveLength(2);
expect(result.tables[0].fields[0].primary).toBe(true);
});

it("parses IDENTITY columns", () => {
const ast = parseMSSQL(
"CREATE TABLE users (id INT IDENTITY(1,1) PRIMARY KEY);",
);
const result = fromMSSQL(ast, DB.MSSQL);

expect(result.tables[0].fields[0].increment).toBe(true);
expect(result.tables[0].fields[0].primary).toBe(true);
});

it("parses FOREIGN KEY constraints", () => {
const ast = parseMSSQL(`
CREATE TABLE departments (id INT PRIMARY KEY);
CREATE TABLE users (
id INT PRIMARY KEY,
dept_id INT,
CONSTRAINT fk_dept FOREIGN KEY (dept_id) REFERENCES departments(id) ON DELETE CASCADE
);
`);
const result = fromMSSQL(ast, DB.MSSQL);

expect(result.relationships).toHaveLength(1);
expect(result.relationships[0].deleteConstraint).toBe("Cascade");
});

it("parses ALTER TABLE ADD FOREIGN KEY", () => {
const ast = parseMSSQL(`
CREATE TABLE a (id INT PRIMARY KEY);
CREATE TABLE b (id INT PRIMARY KEY, a_id INT);
ALTER TABLE b ADD CONSTRAINT fk_a FOREIGN KEY (a_id) REFERENCES a(id);
`);
const result = fromMSSQL(ast, DB.MSSQL);

expect(result.relationships).toHaveLength(1);
});

it("parses CREATE INDEX", () => {
const ast = parseMSSQL(`
CREATE TABLE users (id INT PRIMARY KEY, email NVARCHAR(255));
CREATE UNIQUE INDEX idx_email ON users (email);
`);
const result = fromMSSQL(ast, DB.MSSQL);

expect(result.tables[0].indices).toHaveLength(1);
expect(result.tables[0].indices[0].unique).toBe(true);
});

it("maps types to GENERIC affinity", () => {
const ast = parseMSSQL(
"CREATE TABLE t (a BIT, b NCHAR(10));",
);
const result = fromMSSQL(ast, DB.GENERIC);

expect(result.tables[0].fields[0].type).toBe("BOOLEAN");
});
});
Loading