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