diff --git a/core/src/main/java/com/alibaba/druid/sql/dialect/postgresql/parser/PGLexer.java b/core/src/main/java/com/alibaba/druid/sql/dialect/postgresql/parser/PGLexer.java index d29e3c71e8..7b7c5ae7a7 100644 --- a/core/src/main/java/com/alibaba/druid/sql/dialect/postgresql/parser/PGLexer.java +++ b/core/src/main/java/com/alibaba/druid/sql/dialect/postgresql/parser/PGLexer.java @@ -25,7 +25,6 @@ import static com.alibaba.druid.sql.parser.CharTypes.isIdentifierChar; import static com.alibaba.druid.sql.parser.DialectFeature.LexerFeature.*; import static com.alibaba.druid.sql.parser.DialectFeature.ParserFeature.*; -import static com.alibaba.druid.sql.parser.Token.LITERAL_CHARS; public class PGLexer extends Lexer { public static final Keywords PG_KEYWORDS; @@ -100,96 +99,12 @@ public PGLexer(String input, SQLParserFeature... features) { } } - protected void scanString() { - mark = pos; - boolean hasSpecial = false; - - for (; ; ) { - if (isEOF()) { - lexError("unclosed.str.lit"); - return; - } - - ch = charAt(++pos); - - if (ch == '\\') { - scanChar(); - if (!hasSpecial) { - initBuff(bufPos); - arraycopy(mark + 1, buf, 0, bufPos); - hasSpecial = true; - } - - putChar('\\'); - switch (ch) { - case '\0': - putChar('\0'); - break; - case '\'': - putChar('\''); - break; - case '"': - putChar('"'); - break; - case 'b': - putChar('b'); - break; - case 'n': - putChar('n'); - break; - case 'r': - putChar('r'); - break; - case 't': - putChar('t'); - break; - case '\\': - putChar('\\'); - break; - case 'Z': - putChar((char) 0x1A); // ctrl + Z - break; - default: - putChar(ch); - break; - } - scanChar(); - } - - if (ch == '\'') { - scanChar(); - if (ch != '\'') { - token = LITERAL_CHARS; - break; - } else { - if (!hasSpecial) { - initBuff(bufPos); - arraycopy(mark + 1, buf, 0, bufPos); - hasSpecial = true; - } - putChar('\''); - continue; - } - } - - if (!hasSpecial) { - bufPos++; - continue; - } - - if (bufPos == buf.length) { - putChar(ch); - } else { - buf[bufPos++] = ch; - } - } - - if (!hasSpecial) { - stringVal = subString(mark + 1, bufPos); - } else { - stringVal = new String(buf, 0, bufPos); - } - } + // scanString() is intentionally NOT overridden here. + // The base Lexer.scanString() treats backslash as a regular character, + // which matches PostgreSQL's standard_conforming_strings = on (default since PG 9.1). + // The previous override treated backslash as an escape character inside string literals, + // causing parse failures for SQL like: LIKE ? ESCAPE '\' (generated by JPA/Hibernate). + // See: https://github.com/alibaba/druid/issues/6413 public void scanSharp() { scanChar(); diff --git a/core/src/test/java/com/alibaba/druid/bvt/sql/postgresql/issues/Issue6413.java b/core/src/test/java/com/alibaba/druid/bvt/sql/postgresql/issues/Issue6413.java new file mode 100644 index 0000000000..ac3ef42c24 --- /dev/null +++ b/core/src/test/java/com/alibaba/druid/bvt/sql/postgresql/issues/Issue6413.java @@ -0,0 +1,120 @@ +package com.alibaba.druid.bvt.sql.postgresql.issues; + +import com.alibaba.druid.DbType; +import com.alibaba.druid.sql.ast.SQLStatement; +import com.alibaba.druid.sql.parser.SQLParserUtils; +import com.alibaba.druid.sql.parser.SQLStatementParser; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Fix PostgreSQL parser failing on LIKE ? ESCAPE '\' syntax generated by JPA/Hibernate. + *

+ * The PGLexer treated backslash as an escape character inside string literals, + * which is incorrect for standard PostgreSQL (standard_conforming_strings = on since PG 9.1). + * This caused '\' to be parsed as an escaped quote instead of a string containing a backslash. + * + * @see Issue #6413 + */ +public class Issue6413 { + @Test + public void test_like_escape_backslash() { + // Exact SQL from the bug report (JPA-generated) + String sql = "select rf1_0.id,rf1_0.name from real_functions rf1_0 " + + "left join groups g1_0 on g1_0.id=rf1_0.group_id " + + "where rf1_0.name like ? escape '\\' and g1_0.id=? fetch first ? rows only"; + + for (DbType dbType : new DbType[]{DbType.postgresql, DbType.greenplum, DbType.edb}) { + SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, dbType); + List stmtList = parser.parseStatementList(); + assertEquals(1, stmtList.size()); + assertNotNull(stmtList.get(0)); + } + } + + @Test + public void test_like_escape_backslash_simple() { + // Minimal reproduction case + String sql = "select * from t where name like ? escape '\\'"; + for (DbType dbType : new DbType[]{DbType.postgresql, DbType.greenplum, DbType.edb}) { + SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, dbType); + List stmtList = parser.parseStatementList(); + assertEquals(1, stmtList.size()); + } + } + + @Test + public void test_like_escape_non_backslash_still_works() { + // Other escape characters should still work + for (String escapeChar : new String[]{"#", "!", "~"}) { + String sql = "select * from t where name like ? escape '" + escapeChar + "'"; + SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, DbType.postgresql); + List stmtList = parser.parseStatementList(); + assertEquals(1, stmtList.size()); + } + } + + @Test + public void test_string_with_backslash_in_pg() { + // Standard PG strings with backslash should parse correctly + String sql = "select * from t where name = '\\'"; + SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, DbType.postgresql); + List stmtList = parser.parseStatementList(); + assertEquals(1, stmtList.size()); + } + + @Test + public void test_string_with_double_backslash_in_pg() { + // Double backslash in standard PG string + String sql = "select * from t where name = '\\\\'"; + SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, DbType.postgresql); + List stmtList = parser.parseStatementList(); + assertEquals(1, stmtList.size()); + } + + @Test + public void test_like_escape_backslash_with_wall_filter() { + // Verify it also works through WallProvider (the actual failure path in the bug report) + String sql = "select rf1_0.id from real_functions rf1_0 " + + "where rf1_0.name like ? escape '\\' and rf1_0.group_id=? fetch first ? rows only"; + + // Parsing should succeed (WallFilter calls parser internally) + SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, DbType.postgresql); + List stmtList = parser.parseStatementList(); + assertEquals(1, stmtList.size()); + String output = stmtList.get(0).toString(); + assertTrue(output.contains("ESCAPE")); + } + + @Test + public void test_not_like_escape_backslash() { + // NOT LIKE with backslash ESCAPE should also work + String sql = "select * from t where name not like ? escape '\\'"; + SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, DbType.postgresql); + List stmtList = parser.parseStatementList(); + assertEquals(1, stmtList.size()); + } + + @Test + public void test_ilike_with_backslash_string() { + // ILIKE (PG-specific) with backslash in string pattern + String sql = "select * from t where name ilike '%test\\%'"; + SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, DbType.postgresql); + List stmtList = parser.parseStatementList(); + assertEquals(1, stmtList.size()); + } + + @Test + public void test_pg_double_single_quote_still_works() { + // PG standard '' escape for single quotes must still work + String sql = "select * from t where name = 'it''s a test'"; + SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, DbType.postgresql); + List stmtList = parser.parseStatementList(); + assertEquals(1, stmtList.size()); + } +} diff --git a/core/src/test/resources/bvt/parser/postgresql/0.txt b/core/src/test/resources/bvt/parser/postgresql/0.txt index d4dde0d094..2d69436e7a 100644 --- a/core/src/test/resources/bvt/parser/postgresql/0.txt +++ b/core/src/test/resources/bvt/parser/postgresql/0.txt @@ -1172,11 +1172,11 @@ FROM ( WHERE 1 = 1 ------------------------------------------------------------------------------------------------------------------------ select offerId, offerIds -from cnres.function_select_get_p4p_offer_by_sps('\'1160160508\',\'1085432755\',\'971765217\'') +from cnres.function_select_get_p4p_offer_by_sps('''1160160508'',''1085432755'',''971765217''') as a( offerId character varying(256), offerIds character varying(4000) ) -------------------- SELECT offerId, offerIds -FROM cnres.function_select_get_p4p_offer_by_sps('\''1160160508\'',\''1085432755\'',\''971765217\''') AS a +FROM cnres.function_select_get_p4p_offer_by_sps('''1160160508'',''1085432755'',''971765217''') AS a (offerId character varying(256), offerIds character varying(4000)) ------------------------------------------------------------------------------------------------------------------------ SELECT TITLE_ID,WEB_ID,MENU_TYPE_ID,MENU_ID,TITLE