This is an automated email from the ASF dual-hosted git repository.
zihaoxiang pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git
The following commit(s) were added to refs/heads/dev by this push:
new 1caa65d6a1 [Fix][SQL][#17040] Preserve PostgreSQL PL/pgSQL DO $$
blocks during SQL splitting (#17145)
1caa65d6a1 is described below
commit 1caa65d6a168c1fc926800e23a1a3171e4823703
Author: Eshwaran <[email protected]>
AuthorDate: Wed Apr 23 05:35:24 2025 +0100
[Fix][SQL][#17040] Preserve PostgreSQL PL/pgSQL DO $$ blocks during SQL
splitting (#17145)
---
.../param/PostgreSQLDataSourceProcessor.java | 92 ++++++++++++++++-
.../PostgreSQLDataSourceProcessorTest.java | 114 +++++++++++++++++++++
2 files changed, 205 insertions(+), 1 deletion(-)
diff --git
a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/param/PostgreSQLDataSourceProcessor.java
b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/param/PostgreSQLDataSourceProcessor.java
index 260e5cb4af..b19ce008f0 100644
---
a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/param/PostgreSQLDataSourceProcessor.java
+++
b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/main/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/param/PostgreSQLDataSourceProcessor.java
@@ -132,7 +132,97 @@ public class PostgreSQLDataSourceProcessor extends
AbstractDataSourceProcessor {
@Override
public List<String> splitAndRemoveComment(String sql) {
String cleanSQL = SQLParserUtils.removeComment(sql,
com.alibaba.druid.DbType.postgresql);
- return SQLParserUtils.split(cleanSQL,
com.alibaba.druid.DbType.postgresql);
+ return splitSqlRespectingDollarQuotes(cleanSQL);
+ }
+
+ private int findNextDollar(String sql, int from) {
+ for (int i = from; i < sql.length(); i++) {
+ if (sql.charAt(i) == '$') {
+ return i;
+ }
+ if (!Character.isLetterOrDigit(sql.charAt(i))) {
+ break; // Not a valid dollar tag
+ }
+ }
+ return -1;
+ }
+
+ private int findClosingTag(String sql, int startIndex, String tag) {
+ boolean inString = false;
+
+ for (int i = startIndex; i <= sql.length() - tag.length(); i++) {
+ char ch = sql.charAt(i);
+
+ if (ch == '\'') {
+ // Handle escaped quote: ''
+ if (i + 1 < sql.length() && sql.charAt(i + 1) == '\'') {
+ i++; // skip escaped quote
+ } else {
+ inString = !inString;
+ }
+ }
+
+ if (!inString && sql.startsWith(tag, i)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ private List<String> splitSqlRespectingDollarQuotes(String sql) {
+ List<String> result = new ArrayList<>();
+ StringBuilder current = new StringBuilder();
+
+ boolean insideDollarBlock = false;
+ String dollarTag = null;
+ int i = 0;
+
+ while (i < sql.length()) {
+ char ch = sql.charAt(i);
+
+ // Detect start of dollar-quote block (e.g. $DO$, $func$)
+ if (!insideDollarBlock && ch == '$') {
+ int tagEnd = findNextDollar(sql, i + 1);
+ if (tagEnd > i) {
+ String potentialTag = sql.substring(i, tagEnd + 1);
+ int closingIndex = findClosingTag(sql, tagEnd + 1,
potentialTag);
+ if (closingIndex != -1) {
+ // We're starting a dollar block
+ insideDollarBlock = true;
+ dollarTag = potentialTag;
+ current.append(dollarTag);
+ i = tagEnd + 1;
+ continue;
+ }
+ }
+ }
+
+ // Detect end of dollar-quote block
+ if (insideDollarBlock && dollarTag != null &&
sql.startsWith(dollarTag, i)) {
+ insideDollarBlock = false;
+ current.append(dollarTag);
+ i += dollarTag.length();
+ continue;
+ }
+
+ // Split only outside of a dollar block
+ if (!insideDollarBlock && ch == ';') {
+ result.add(current.toString().trim());
+ current.setLength(0);
+ i++;
+ continue;
+ }
+
+ current.append(ch);
+ i++;
+ }
+
+ if (!current.toString().trim().isEmpty()) {
+ result.add(current.toString().trim());
+ }
+
+ return result;
}
private String transformOther(Map<String, String> otherMap) {
diff --git
a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/test/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/PostgreSQLDataSourceProcessorTest.java
b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/test/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/PostgreSQLDataSourceProcessorTest.java
new file mode 100644
index 0000000000..5be8d3d843
--- /dev/null
+++
b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-postgresql/src/test/java/org/apache/dolphinscheduler/plugin/datasource/postgresql/PostgreSQLDataSourceProcessorTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.
+ */
+
+package org.apache.dolphinscheduler.plugin.datasource.postgresql;
+
+import
org.apache.dolphinscheduler.plugin.datasource.postgresql.param.PostgreSQLDataSourceProcessor;
+
+import java.util.List;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class PostgreSQLDataSourceProcessorTest {
+
+ private final PostgreSQLDataSourceProcessor processor = new
PostgreSQLDataSourceProcessor();
+
+ @Test
+ public void testSingleDoBlock() {
+ String sql = "DO $$ BEGIN RAISE NOTICE 'Hello'; END $$;";
+ List<String> parts = processor.splitAndRemoveComment(sql);
+ Assertions.assertEquals(1, parts.size());
+ Assertions.assertEquals("DO $$ BEGIN RAISE NOTICE 'Hello'; END $$",
parts.get(0));
+ }
+
+ @Test
+ public void testDoBlockAndInsert() {
+ String sql = "DO $$ BEGIN RAISE NOTICE 'Start'; END $$;\nINSERT INTO
test_table(id) VALUES (1);";
+ List<String> parts = processor.splitAndRemoveComment(sql);
+ Assertions.assertEquals(2, parts.size());
+ }
+
+ @Test
+ public void testStandardSql() {
+ String sql = "INSERT INTO test_table(id) VALUES (1); UPDATE test_table
SET id = 2 WHERE id = 1;";
+ List<String> parts = processor.splitAndRemoveComment(sql);
+ Assertions.assertEquals(2, parts.size());
+ Assertions.assertEquals("INSERT INTO test_table(id) VALUES (1)",
parts.get(0));
+ Assertions.assertEquals("UPDATE test_table SET id = 2 WHERE id = 1",
parts.get(1));
+ }
+
+ @Test
+ public void testCustomDollarTag() {
+ String sql = "DO $func$ BEGIN RAISE NOTICE 'Tag test'; END $func$;";
+ List<String> parts = processor.splitAndRemoveComment(sql);
+ Assertions.assertEquals(1, parts.size());
+ Assertions.assertTrue(parts.get(0).contains("$func$"));
+ Assertions.assertEquals("DO $func$ BEGIN RAISE NOTICE 'Tag test'; END
$func$", parts.get(0));
+ }
+
+ @Test
+ public void testCommentsPreserved() {
+ String sql =
+ "-- comment here\nDO $$ BEGIN NULL; END $$;\n-- trailing
comment\nINSERT INTO test_table VALUES (5);";
+ List<String> parts = processor.splitAndRemoveComment(sql);
+ Assertions.assertEquals(2, parts.size());
+ Assertions.assertEquals("DO $$ BEGIN NULL; END $$", parts.get(0));
+ Assertions.assertEquals("INSERT INTO test_table VALUES (5)",
parts.get(1));
+ }
+
+ @Test
+ public void testDoBlockWithSemicolonInsideString() {
+ String sql = "DO $$ BEGIN RAISE NOTICE 'hello; world'; END $$;";
+ List<String> parts = processor.splitAndRemoveComment(sql);
+ Assertions.assertEquals(1, parts.size());
+ Assertions.assertEquals("DO $$ BEGIN RAISE NOTICE 'hello; world'; END
$$", parts.get(0));
+ }
+
+ @Test
+ public void testDoBlockWithDollarTagInsideString() {
+ String sql = "DO $$ BEGIN RAISE NOTICE 'this has $DO$ inside'; END
$$;";
+ List<String> parts = processor.splitAndRemoveComment(sql);
+ Assertions.assertEquals(1, parts.size());
+ Assertions.assertEquals("DO $$ BEGIN RAISE NOTICE 'this has $DO$
inside'; END $$", parts.get(0));
+ }
+
+ @Test
+ public void testMultipleStatementsWithDoBlock() {
+ String sql = "DO $$ BEGIN RAISE NOTICE 'msg'; END $$; INSERT INTO
test_table VALUES (1);";
+ List<String> parts = processor.splitAndRemoveComment(sql);
+ Assertions.assertEquals(2, parts.size());
+ Assertions.assertEquals("DO $$ BEGIN RAISE NOTICE 'msg'; END $$",
parts.get(0));
+ Assertions.assertEquals("INSERT INTO test_table VALUES (1)",
parts.get(1));
+ }
+
+ @Test
+ public void testCustomDollarTagBlock() {
+ String sql = "DO $func$ BEGIN RAISE NOTICE 'custom tag'; END $func$;";
+ List<String> parts = processor.splitAndRemoveComment(sql);
+ Assertions.assertEquals(1, parts.size());
+ Assertions.assertEquals("DO $func$ BEGIN RAISE NOTICE 'custom tag';
END $func$", parts.get(0));
+ }
+
+ @Test
+ public void testDoBlockWithEscapedSingleQuote() {
+ String sql = "DO $$ BEGIN RAISE NOTICE 'don''t split here'; END $$;";
+ List<String> parts = processor.splitAndRemoveComment(sql);
+ Assertions.assertEquals(1, parts.size());
+ Assertions.assertEquals("DO $$ BEGIN RAISE NOTICE 'don''t split here';
END $$", parts.get(0));
+ }
+}