Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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 <a href="https://github.com/alibaba/druid/issues/6413">Issue #6413</a>
*/
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<SQLStatement> 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<SQLStatement> 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<SQLStatement> 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<SQLStatement> 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<SQLStatement> 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<SQLStatement> 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<SQLStatement> 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<SQLStatement> 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<SQLStatement> stmtList = parser.parseStatementList();
assertEquals(1, stmtList.size());
}
}
4 changes: 2 additions & 2 deletions core/src/test/resources/bvt/parser/postgresql/0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down