From 60ddadd52eb9f753a71197c275c558d930d6a36a Mon Sep 17 00:00:00 2001 From: yzxcj797 <1784931579@qq.com> Date: Tue, 18 Aug 2026 22:19:25 +0800 Subject: [PATCH] Skip unsupported PostgreSQL views during import Split PostgreSQL source with quote, comment, and dollar-quote awareness before parsing, then omit view declarations that the diagram model does not represent. This keeps multi-SELECT views from aborting import of the surrounding tables. --- package.json | 1 + src/components/EditorHeader/Modal/Modal.jsx | 8 +- src/utils/importSQL/postgresViews.js | 160 ++++++++++++++++++++ src/utils/importSQL/postgresViews.test.js | 58 +++++++ 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 src/utils/importSQL/postgresViews.js create mode 100644 src/utils/importSQL/postgresViews.test.js diff --git a/package.json b/package.json index 848104764..4ddf6e2b6 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/components/EditorHeader/Modal/Modal.jsx b/src/components/EditorHeader/Modal/Modal.jsx index 149549d3a..a2e36135d 100644 --- a/src/components/EditorHeader/Modal/Modal.jsx +++ b/src/components/EditorHeader/Modal/Modal.jsx @@ -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, @@ -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, }); } diff --git a/src/utils/importSQL/postgresViews.js b/src/utils/importSQL/postgresViews.js new file mode 100644 index 000000000..d3391125a --- /dev/null +++ b/src/utils/importSQL/postgresViews.js @@ -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"); +} diff --git a/src/utils/importSQL/postgresViews.test.js b/src/utils/importSQL/postgresViews.test.js new file mode 100644 index 000000000..a9ce5b56c --- /dev/null +++ b/src/utils/importSQL/postgresViews.test.js @@ -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")); +});