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: {

Reply via email to