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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "node --test",
"preview": "vite preview"
},
"dependencies": {
Expand Down
8 changes: 7 additions & 1 deletion src/components/EditorHeader/Modal/Modal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { isRtl } from "../../../i18n/utils/rtl";
import { useExtensions } from "../../../context/ExtensionsContext";
import { importSQL } from "../../../utils/importSQL";
import { removeUnsupportedPostgresViews } from "../../../utils/importSQL/postgresViews";
import {
allowedTypesFor,
normalizeAiDiagram,
Expand Down Expand Up @@ -131,7 +132,12 @@ export default function Modal({
} else {
const parser = new Parser();

ast = parser.astify(importSource.src, {
const source =
targetDatabase === DB.POSTGRES
? removeUnsupportedPostgresViews(importSource.src)
: importSource.src;

ast = parser.astify(source, {
database: targetDatabase,
});
}
Expand Down
160 changes: 160 additions & 0 deletions src/utils/importSQL/postgresViews.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
function splitPostgresStatements(sql) {
const statements = [];
let start = 0;
let quote = null; // { marker, escapeBackslash }
let dollarTag = null;
let blockCommentDepth = 0;
let index = 0;

while (index < sql.length) {
const char = sql[index];

if (blockCommentDepth > 0) {
if (sql.startsWith("/*", index)) {
blockCommentDepth += 1;
index += 2;
} else if (sql.startsWith("*/", index)) {
blockCommentDepth -= 1;
index += 2;
} else {
index += 1;
}
continue;
}

if (dollarTag) {
if (sql.startsWith(dollarTag, index)) {
index += dollarTag.length;
dollarTag = null;
} else {
index += 1;
}
continue;
}

if (quote) {
if (char === "\\" && quote.escapeBackslash) {
index += 2;
} else if (char === quote.marker) {
if (sql[index + 1] === quote.marker) {
index += 2;
} else {
quote = null;
index += 1;
}
} else {
index += 1;
}
continue;
}

if ((char === "e" || char === "E") && sql[index + 1] === "'") {
quote = { marker: "'", escapeBackslash: true };
index += 2;
} else if (char === "'" || char === '"' || char === "`") {
quote = { marker: char, escapeBackslash: false };
index += 1;
} else if (char === "-" && sql[index + 1] === "-") {
index += 2;
while (index < sql.length && sql[index] !== "\n" && sql[index] !== "\r") {
index += 1;
}
} else if (char === "/" && sql[index + 1] === "*") {
blockCommentDepth = 1;
index += 2;
} else if (char === "$") {
const closingTag = sql.indexOf("$", index + 1);
if (closingTag !== -1) {
const candidate = sql.slice(index, closingTag + 1);
if (/^\$[A-Za-z0-9_]*\$/.test(candidate)) {
dollarTag = candidate;
index += dollarTag.length;
} else {
index += 1;
}
} else {
index += 1;
}
} else if (char === ";") {
statements.push(sql.slice(start, index + 1));
start = index + 1;
index += 1;
} else {
index += 1;
}
}

if (start < sql.length) {
statements.push(sql.slice(start));
}
return statements;
}

function statementKeywords(statement) {
const keywords = [];
let index = 0;

while (index < statement.length && keywords.length < 6) {
while (index < statement.length && /\s/.test(statement[index])) {
index += 1;
}

if (statement.startsWith("--", index)) {
const newlineIndex = statement.slice(index).search(/\r?\n/);
if (newlineIndex === -1) break;
index += newlineIndex + 1;
continue;
}

if (statement.startsWith("/*", index)) {
let depth = 1;
index += 2;
while (index < statement.length && depth > 0) {
if (statement.startsWith("/*", index)) {
depth += 1;
index += 2;
} else if (statement.startsWith("*/", index)) {
depth -= 1;
index += 2;
} else {
index += 1;
}
}
if (depth > 0) break;
continue;
}

const match = /^[A-Za-z_][A-Za-z0-9_]*/.exec(statement.slice(index));
if (!match) break;
keywords.push(match[0].toUpperCase());
index += match[0].length;
}

return keywords;
}

function isUnsupportedViewStatement(statement) {
const keywords = statementKeywords(statement);
if (keywords[0] !== "CREATE") return false;

let index = 1;
if (keywords[index] === "OR" && keywords[index + 1] === "REPLACE") {
index += 2;
}
if (keywords[index] === "GLOBAL" || keywords[index] === "LOCAL") {
index += 1;
}
if (keywords[index] === "TEMP" || keywords[index] === "TEMPORARY") {
index += 1;
}
if (keywords[index] === "MATERIALIZED") {
index += 1;
}
return keywords[index] === "VIEW";
}

export function removeUnsupportedPostgresViews(sql) {
return splitPostgresStatements(sql)
.filter((statement) => !isUnsupportedViewStatement(statement))
.join("\n");
}
58 changes: 58 additions & 0 deletions src/utils/importSQL/postgresViews.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import nodeSQLParser from "node-sql-parser";

import { removeUnsupportedPostgresViews } from "./postgresViews.js";

const { Parser } = nodeSQLParser;

test("multi-select PostgreSQL views are removed before SQL parsing", () => {
const sql = `CREATE TABLE public.people (id integer);
CREATE OR REPLACE VIEW public.person_ids AS
SELECT id FROM public.people
UNION
SELECT id FROM public.employees;
CREATE TABLE public.employees (id integer);`;

const result = removeUnsupportedPostgresViews(sql);
assert.ok(result.includes("CREATE TABLE public.people"));
assert.ok(result.includes("CREATE TABLE public.employees"));
assert.ok(!result.includes("person_ids"));

const ast = new Parser().astify(result, { database: "postgresql" });
assert.equal(ast.length, 2);
});

test("temporary and materialized view forms are recognized", () => {
const result =
removeUnsupportedPostgresViews(`CREATE TEMPORARY VIEW a AS SELECT 1;
CREATE MATERIALIZED VIEW b AS SELECT 2;
CREATE TABLE c (id integer);`);

assert.ok(!result.includes(" VIEW "));
assert.ok(result.includes("CREATE TABLE c"));
});

test("semicolons inside strings and dollar-quoted bodies do not split statements", () => {
const sql = `COMMENT ON TABLE public.people IS 'keep this; semicolon';
DO $procedure$
BEGIN
RAISE NOTICE 'nested; statement';
END
$procedure$;
CREATE TABLE public.people (id integer);`;

const result = removeUnsupportedPostgresViews(sql);
assert.ok(result.includes("keep this; semicolon"));
assert.ok(result.includes("nested; statement"));
assert.ok(result.includes("CREATE TABLE public.people"));
});

test("comments before a view declaration do not hide it", () => {
const result = removeUnsupportedPostgresViews(`-- diagram source
/* strip this /* with a nested comment */ */ CREATE VIEW public.v AS SELECT 1;
CREATE TABLE public.t (id integer);`);

assert.ok(!result.includes("public.v"));
assert.ok(result.includes("CREATE TABLE public.t"));
});
Loading