This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/dev/pr-12996-22a34f323d825b44fc8219b1eb343a8ee7fe3535 in repository https://gitbox.apache.org/repos/asf/seatunnel.git
commit 886a3ac6e59aad5e46755761cf9fb8ac2a6d3df9 Author: Sepuri Sai Krishna <[email protected]> AuthorDate: Wed Oct 28 17:29:44 2026 +0000 [Fix][Connector-V2][JDBC] Make the SQL structure guards locale independent (#12996) --- .../jdbc/source/SqlWhereConditionHelper.java | 15 +-- .../jdbc/source/SqlWhereConditionHelperTest.java | 104 +++++++++++++++++++++ 2 files changed, 112 insertions(+), 7 deletions(-) diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/SqlWhereConditionHelper.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/SqlWhereConditionHelper.java index 577bd6b32e..10f7c567e1 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/SqlWhereConditionHelper.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/SqlWhereConditionHelper.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; @@ -171,7 +172,7 @@ public class SqlWhereConditionHelper { * top level would make auto-adding columns unsafe. */ public static boolean hasTopLevelSetOperator(String sql) { - String upper = sql.toUpperCase(); + String upper = sql.toUpperCase(Locale.ROOT); int length = upper.length(); int parenCount = 0; boolean inString = false; @@ -237,7 +238,7 @@ public class SqlWhereConditionHelper { String cleaned = sql.replaceAll("'[^']*'|\"[^\"]*\"", ""); // Find the column list between SELECT and FROM - String upperCleaned = cleaned.toUpperCase(); + String upperCleaned = cleaned.toUpperCase(Locale.ROOT); int selectIdx = -1; // Find SELECT keyword (not inside parentheses) @@ -275,9 +276,9 @@ public class SqlWhereConditionHelper { String columnList = cleaned.substring(selectEndIdx, fromIdx).trim(); // Skip DISTINCT/ALL keywords - if (columnList.toUpperCase().startsWith("DISTINCT ")) { + if (columnList.toUpperCase(Locale.ROOT).startsWith("DISTINCT ")) { columnList = columnList.substring(9).trim(); - } else if (columnList.toUpperCase().startsWith("ALL ")) { + } else if (columnList.toUpperCase(Locale.ROOT).startsWith("ALL ")) { columnList = columnList.substring(4).trim(); } @@ -335,7 +336,7 @@ public class SqlWhereConditionHelper { * and CTE (WITH ... AS). */ public static boolean hasComplexClause(String sql) { - String upper = sql.toUpperCase(); + String upper = sql.toUpperCase(Locale.ROOT); int length = upper.length(); int parenCount = 0; boolean inString = false; @@ -502,7 +503,7 @@ public class SqlWhereConditionHelper { /** Find the index of the main FROM clause in the SQL query. */ private static int findMainFromClauseIndex(String sql) { - String upperSql = sql.toUpperCase(); + String upperSql = sql.toUpperCase(Locale.ROOT); int length = sql.length(); int parenthesisCount = 0; boolean inQuote = false; @@ -563,7 +564,7 @@ public class SqlWhereConditionHelper { /** Check if a string is a SQL keyword that should be ignored. */ private static boolean isKeyword(String word) { - String upper = word.toUpperCase(); + String upper = word.toUpperCase(Locale.ROOT); return upper.equals("AND") || upper.equals("OR") || upper.equals("NOT") diff --git a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/SqlWhereConditionHelperTest.java b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/SqlWhereConditionHelperTest.java index ebcc02cf4d..9034b998ee 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/SqlWhereConditionHelperTest.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/source/SqlWhereConditionHelperTest.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; /** Unit tests for SqlWhereConditionHelper */ @@ -1455,4 +1456,107 @@ public class SqlWhereConditionHelperTest { "Outer simple query with GROUP BY only in subquery should add fields. Result: " + result); } + + /** + * Locales to drive the guards under. Turkish and Azerbaijani both uppercase "i" to the dotted + * capital "\u0130", so every guard keyword containing an "i" stops matching once the SQL is + * uppercased with the default locale. + */ + private static final List<Locale> DOTTED_I_LOCALES = + Arrays.asList(Locale.forLanguageTag("tr-TR"), Locale.forLanguageTag("az-AZ")); + + private static Locale[] snapshotDefaultLocales() { + return new Locale[] { + Locale.getDefault(), + Locale.getDefault(Locale.Category.FORMAT), + Locale.getDefault(Locale.Category.DISPLAY) + }; + } + + /** Restores the base locale first, because setting it overwrites both category defaults. */ + private static void restoreDefaultLocales(Locale[] saved) { + Locale.setDefault(saved[0]); + Locale.setDefault(Locale.Category.FORMAT, saved[1]); + Locale.setDefault(Locale.Category.DISPLAY, saved[2]); + } + + @Test + public void testSqlStructureGuardsAreIndependentOfTheDefaultLocale() { + Locale[] saved = snapshotDefaultLocales(); + try { + for (Locale locale : DOTTED_I_LOCALES) { + Locale.setDefault(locale); + + // Set operators: adding a column to only the first branch changes its arity, so + // these have to keep being detected whatever the default locale is. + for (String sql : + Arrays.asList( + "select id from a union select id from b", + "select id from a union all select id from b", + "select id from a intersect select id from b", + "select id from a minus select id from b", + "select id from a except select id from b")) { + Assertions.assertTrue( + SqlWhereConditionHelper.hasTopLevelSetOperator(sql), + "set operator must be detected under " + locale + ": " + sql); + } + + // Complex clauses: auto-adding a column changes the result of these queries. + for (String sql : + Arrays.asList( + "select distinct id from t", + "select id from t group by id", + "select id from t having count(*) > 1", + "select id from t limit 10", + "select id from t offset 5", + "select id, row_number() over (order by id) from t", + "with x as (select id from t) select id from x")) { + Assertions.assertTrue( + SqlWhereConditionHelper.hasComplexClause(sql), + "complex clause must be detected under " + locale + ": " + sql); + } + } + } finally { + restoreDefaultLocales(saved); + } + } + + @Test + public void testApplyWhereConditionWithWrapIsIndependentOfTheDefaultLocale() { + List<String> queries = + Arrays.asList( + "select id from a union select id from b", + "select distinct id from t", + "select id from t limit 10", + "select id from t having count(*) > 1"); + String whereCondition = "name = 'x'"; + + Locale[] saved = snapshotDefaultLocales(); + try { + Locale.setDefault(Locale.ROOT); + for (String sql : queries) { + String expected = + SqlWhereConditionHelper.applyWhereConditionWithWrap( + sql, whereCondition, true); + // The guard holds, so the inner query is wrapped without being rewritten. + Assertions.assertTrue( + expected.contains(sql), "guarded sql must not be rewritten: " + expected); + + for (Locale locale : DOTTED_I_LOCALES) { + Locale.setDefault(locale); + Assertions.assertEquals( + expected, + SqlWhereConditionHelper.applyWhereConditionWithWrap( + sql, whereCondition, true), + "emitted sql must not depend on the default locale (" + + locale + + "): " + + sql); + } + Locale.setDefault(Locale.ROOT); + } + } finally { + restoreDefaultLocales(saved); + } + } }
