This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new c5206f50b4a Replace node-sql-parser with sqlparser-ts (#61111)
c5206f50b4a is described below
commit c5206f50b4ab441e9a7174a971c0c6589b581499
Author: Guan-Ming (Wesley) Chiu <[email protected]>
AuthorDate: Tue Feb 17 01:04:07 2026 +0800
Replace node-sql-parser with sqlparser-ts (#61111)
---
airflow-core/src/airflow/ui/package.json | 2 +-
airflow-core/src/airflow/ui/pnpm-lock.yaml | 32 ++---
.../ui/src/components/SqlParserProvider.tsx | 42 ++++++
.../src/pages/TaskInstance/RenderedTemplates.tsx | 9 +-
.../airflow/ui/src/utils/detectLanguage.test.ts | 147 +++++++++++++++++++++
.../src/airflow/ui/src/utils/detectLanguage.ts | 20 +--
airflow-core/src/airflow/ui/vite.config.ts | 3 +
7 files changed, 214 insertions(+), 41 deletions(-)
diff --git a/airflow-core/src/airflow/ui/package.json
b/airflow-core/src/airflow/ui/package.json
index 949a03b2e50..cab0c91f197 100644
--- a/airflow-core/src/airflow/ui/package.json
+++ b/airflow-core/src/airflow/ui/package.json
@@ -29,6 +29,7 @@
"@chakra-ui/react": "^3.20.0",
"@codemirror/lang-json": "^6.0.2",
"@emotion/react": "^11.14.0",
+ "@guanmingchiu/sqlparser-ts": "^0.61.1",
"@monaco-editor/react": "^4.7.0",
"@tanstack/react-query": "^5.90.11",
"@tanstack/react-table": "^8.21.3",
@@ -51,7 +52,6 @@
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"next-themes": "^0.4.6",
- "node-sql-parser": "^5.3.10",
"react": "^19.2.1",
"react-chartjs-2": "^5.3.0",
"react-dom": "^19.2.1",
diff --git a/airflow-core/src/airflow/ui/pnpm-lock.yaml
b/airflow-core/src/airflow/ui/pnpm-lock.yaml
index c9afae689fd..59041aff203 100644
--- a/airflow-core/src/airflow/ui/pnpm-lock.yaml
+++ b/airflow-core/src/airflow/ui/pnpm-lock.yaml
@@ -20,6 +20,9 @@ importers:
'@emotion/react':
specifier: ^11.14.0
version: 11.14.0(@types/[email protected])([email protected])
+ '@guanmingchiu/sqlparser-ts':
+ specifier: ^0.61.1
+ version: 0.61.1
'@monaco-editor/react':
specifier: ^4.7.0
version:
4.7.0([email protected])([email protected]([email protected]))([email protected])
@@ -86,9 +89,6 @@ importers:
next-themes:
specifier: ^0.4.6
version: 0.4.6([email protected]([email protected]))([email protected])
- node-sql-parser:
- specifier: ^5.3.10
- version: 5.3.10
react:
specifier: ^19.2.1
version: 19.2.1
@@ -787,6 +787,10 @@ packages:
'@floating-ui/[email protected]':
resolution: {integrity:
sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
+ '@guanmingchiu/[email protected]':
+ resolution: {integrity:
sha512-5RA05UHDkcm4cyhBNz2oJqU+8+fhCZvseSGtAvuVcwM1EEh2FfRaU1qdPygpriGqsQRT3Cn1PK5YShJffQjmXg==}
+ engines: {node: '>=16.0.0'}
+
'@hey-api/[email protected]':
resolution: {integrity:
sha512-DA3Zf5ONxMK1PUkK88lAuYbXMgn5BvU5sjJdTAO2YOn6Eu/9ovilBztMzvu8pyY44PmL3n4ex4+f+XIwvgfhvw==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -1356,9 +1360,6 @@ packages:
'@types/[email protected]':
resolution: {integrity:
sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
- '@types/[email protected]':
- resolution: {integrity:
sha512-eLYXDbZWXh2uxf+w8sXS8d6KSoXTswfps6fvCUuVAGN8eRpfe7h9eSRydxiSJvo9Bf+GzifsDOr9TMQlmJdmkw==}
-
'@types/[email protected]':
resolution: {integrity:
sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
@@ -2090,10 +2091,6 @@ packages:
[email protected]:
resolution: {integrity:
sha512-pNdYkNPiJUnEhnfXV56+sQy8+AaPcG3POZAUnwr4EeqCUZFz4u2PePbo3e5Gj4ziYPCWGUZT9RHisvJKnwFuBQ==}
- [email protected]:
- resolution: {integrity:
sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
- engines: {node: '>=0.6'}
-
[email protected]:
resolution: {integrity:
sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -3609,10 +3606,6 @@ packages:
[email protected]:
resolution: {integrity:
sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
- [email protected]:
- resolution: {integrity:
sha512-cf+iXXJ9Foz4hBIu+eNNeg207ac6XruA9I9DXEs+jCxeS9t/k9T0GZK8NZngPwkv+P26i3zNFj9jxJU2v3pJnw==}
- engines: {node: '>=8'}
-
[email protected]:
resolution: {integrity:
sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
@@ -5346,6 +5339,8 @@ snapshots:
'@floating-ui/[email protected]': {}
+ '@guanmingchiu/[email protected]': {}
+
'@hey-api/[email protected]([email protected])([email protected])':
dependencies:
'@apidevtools/json-schema-ref-parser': 11.6.4
@@ -5870,8 +5865,6 @@ snapshots:
'@types/[email protected]': {}
- '@types/[email protected]': {}
-
'@types/[email protected]': {}
'@types/[email protected](@types/[email protected])':
@@ -7270,8 +7263,6 @@ snapshots:
[email protected]: {}
- [email protected]: {}
-
[email protected]: {}
[email protected]:
@@ -9154,11 +9145,6 @@ snapshots:
[email protected]: {}
- [email protected]:
- dependencies:
- '@types/pegjs': 0.10.6
- big-integer: 1.6.52
-
[email protected]:
dependencies:
hosted-git-info: 2.8.9
diff --git a/airflow-core/src/airflow/ui/src/components/SqlParserProvider.tsx
b/airflow-core/src/airflow/ui/src/components/SqlParserProvider.tsx
new file mode 100644
index 00000000000..2b26a138fb2
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/SqlParserProvider.tsx
@@ -0,0 +1,42 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { init } from "@guanmingchiu/sqlparser-ts";
+import { type ReactNode, useEffect, useState } from "react";
+
+/**
+ * Waits for the sqlparser WASM module to load before rendering children.
+ * This ensures detectLanguage() can detect SQL on the first render.
+ */
+export const SqlParserProvider = ({ children }: { readonly children: ReactNode
}) => {
+ const [isReady, setIsReady] = useState(false);
+
+ useEffect(() => {
+ init()
+ .catch(() => {
+ /* empty */
+ })
+ .finally(() => setIsReady(true));
+ }, []);
+
+ if (!isReady) {
+ return undefined;
+ }
+
+ return children;
+};
diff --git
a/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.tsx
b/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.tsx
index c04bce6fa02..6f6a7da599c 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/RenderedTemplates.tsx
@@ -20,12 +20,13 @@ import { Box, Table } from "@chakra-ui/react";
import { useParams } from "react-router-dom";
import { useTaskInstanceServiceGetMappedTaskInstance } from "openapi/queries";
+import { SqlParserProvider } from "src/components/SqlParserProvider";
import { ClipboardRoot, ClipboardIconButton } from "src/components/ui";
import { useColorMode } from "src/context/colorMode";
import { detectLanguage } from "src/utils/detectLanguage";
import { oneDark, oneLight, SyntaxHighlighter } from
"src/utils/syntaxHighlighter";
-export const RenderedTemplates = () => {
+const RenderedTemplatesContent = () => {
const { dagId = "", mapIndex = "-1", runId = "", taskId = "" } = useParams();
const { colorMode } = useColorMode();
@@ -94,3 +95,9 @@ export const RenderedTemplates = () => {
</Box>
);
};
+
+export const RenderedTemplates = () => (
+ <SqlParserProvider>
+ <RenderedTemplatesContent />
+ </SqlParserProvider>
+);
diff --git a/airflow-core/src/airflow/ui/src/utils/detectLanguage.test.ts
b/airflow-core/src/airflow/ui/src/utils/detectLanguage.test.ts
new file mode 100644
index 00000000000..784dcc9c8c9
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/utils/detectLanguage.test.ts
@@ -0,0 +1,147 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * @vitest-environment node
+ */
+import { init } from "@guanmingchiu/sqlparser-ts";
+import { beforeAll, describe, expect, it } from "vitest";
+
+import { detectLanguage } from "./detectLanguage";
+
+beforeAll(async () => {
+ await init();
+});
+
+describe("detectLanguage", () => {
+ describe("JSON detection", () => {
+ it("detects valid JSON object", () => {
+ expect(detectLanguage('{"key": "value"}')).toBe("json");
+ });
+
+ it("detects valid JSON array", () => {
+ expect(detectLanguage("[1, 2, 3]")).toBe("json");
+ });
+
+ it("detects nested JSON", () => {
+ expect(detectLanguage('{"nested": {"key": "value"}}')).toBe("json");
+ });
+ });
+
+ describe("SQL detection", () => {
+ it("detects SELECT statement", () => {
+ expect(detectLanguage("SELECT * FROM users")).toBe("sql");
+ });
+
+ it("detects SELECT with WHERE clause", () => {
+ expect(detectLanguage("SELECT id, name FROM users WHERE id =
1")).toBe("sql");
+ });
+
+ it("detects INSERT statement", () => {
+ expect(detectLanguage("INSERT INTO users (name) VALUES
('test')")).toBe("sql");
+ });
+
+ it("detects UPDATE statement", () => {
+ expect(detectLanguage("UPDATE users SET name = 'test' WHERE id =
1")).toBe("sql");
+ });
+
+ it("detects DELETE statement", () => {
+ expect(detectLanguage("DELETE FROM users WHERE id = 1")).toBe("sql");
+ });
+
+ it("detects CREATE TABLE statement", () => {
+ expect(detectLanguage("CREATE TABLE users (id INT, name
VARCHAR(255))")).toBe("sql");
+ });
+
+ it("detects WITH (CTE) statement", () => {
+ expect(detectLanguage("WITH cte AS (SELECT 1) SELECT * FROM
cte")).toBe("sql");
+ });
+
+ it("detects multiline SQL", () => {
+ const sql = `
+ SELECT *
+ FROM users
+ WHERE id = 1
+ `;
+
+ expect(detectLanguage(sql)).toBe("sql");
+ });
+ });
+
+ describe("Bash detection", () => {
+ it("detects shebang", () => {
+ expect(detectLanguage("#!/bin/bash\necho hello")).toBe("bash");
+ });
+
+ it("detects common bash commands", () => {
+ expect(detectLanguage("echo 'Hello World'")).toBe("bash");
+ expect(detectLanguage("ls -la")).toBe("bash");
+ expect(detectLanguage("cd /tmp")).toBe("bash");
+ });
+
+ it("detects pipe operator", () => {
+ expect(detectLanguage("cat file.txt | grep pattern")).toBe("bash");
+ });
+
+ it("detects command substitution", () => {
+ expect(detectLanguage("echo $(date)")).toBe("bash");
+ });
+
+ it("detects logical operators", () => {
+ expect(detectLanguage("command1 && command2")).toBe("bash");
+ expect(detectLanguage("command1 || command2")).toBe("bash");
+ });
+ });
+
+ describe("YAML detection", () => {
+ it("detects simple YAML", () => {
+ expect(detectLanguage("key: value")).toBe("yaml");
+ });
+
+ it("detects nested YAML", () => {
+ const yaml = `
+parent:
+ child: value
+`;
+
+ expect(detectLanguage(yaml)).toBe("yaml");
+ });
+
+ it("detects YAML list", () => {
+ const yaml = `
+items:
+ - item1
+ - item2
+`;
+
+ expect(detectLanguage(yaml)).toBe("yaml");
+ });
+ });
+
+ describe("edge cases", () => {
+ it("handles string with leading/trailing whitespace", () => {
+ expect(detectLanguage(" SELECT * FROM users ")).toBe("sql");
+ });
+
+ it("prioritizes JSON over YAML for valid JSON", () => {
+ // Valid JSON is also valid YAML, but JSON should be detected first
+ expect(detectLanguage('{"key": "value"}')).toBe("json");
+ });
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/utils/detectLanguage.ts
b/airflow-core/src/airflow/ui/src/utils/detectLanguage.ts
index 009b4febf66..1734c483d64 100644
--- a/airflow-core/src/airflow/ui/src/utils/detectLanguage.ts
+++ b/airflow-core/src/airflow/ui/src/utils/detectLanguage.ts
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Parser } from "node-sql-parser";
+import { validate } from "@guanmingchiu/sqlparser-ts";
import { parse as parseYaml } from "yaml";
export const detectLanguage = (value: string): string => {
@@ -31,25 +31,13 @@ export const detectLanguage = (value: string): string => {
// Not valid JSON, continue
}
- // Try to detect SQL by parsing with node-sql-parser
+ // Try to detect SQL using sqlparser-rs
try {
- const parser = new Parser();
-
- // Support multiple SQL dialects
- parser.astify(trimmed, { database: "postgresql" });
+ validate(trimmed);
return "sql";
} catch {
- // Try with other dialects if PostgreSQL fails
- try {
- const parser = new Parser();
-
- parser.astify(trimmed, { database: "mysql" });
-
- return "sql";
- } catch {
- // Not valid SQL, continue to other checks
- }
+ // Not valid SQL, continue to other checks
}
// Try to detect Bash (basic heuristics)
diff --git a/airflow-core/src/airflow/ui/vite.config.ts
b/airflow-core/src/airflow/ui/vite.config.ts
index 57b7f9f8c4c..b622f4eddf2 100644
--- a/airflow-core/src/airflow/ui/vite.config.ts
+++ b/airflow-core/src/airflow/ui/vite.config.ts
@@ -24,6 +24,9 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
base: "./",
build: { chunkSizeWarningLimit: 1600, manifest: true },
+ optimizeDeps: {
+ exclude: ["@guanmingchiu/sqlparser-ts"], // WASM package needs to be
excluded from pre-bundling
+ },
plugins: [
react({
babel: {