Skip to content

Commit 12f27b4

Browse files
The JavaCC and ANTLR grammars in updated to handle the ticks part of the parsing, instead of in the java code
1 parent 823b35d commit 12f27b4

File tree

5 files changed

+104
-81
lines changed

5 files changed

+104
-81
lines changed

jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4/ClickHouseLexer.g4

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,18 @@ JSON_TRUE : 'true';
386386

387387
// Tokens
388388

389+
// Order matters: quoted identifiers must come before unquoted IDENTIFIER
390+
BACKTICK_ID:
391+
BACKQUOTE ( ~([\\`]) | (BACKSLASH .) | (BACKQUOTE BACKQUOTE))* BACKQUOTE
392+
;
393+
394+
QUOTED_IDENTIFIER:
395+
QUOTE_DOUBLE (~([\\"]) | (BACKSLASH .) | (QUOTE_DOUBLE QUOTE_DOUBLE))* QUOTE_DOUBLE
396+
;
397+
389398
IDENTIFIER:
390399
(LETTER | UNDERSCORE) (LETTER | UNDERSCORE | DEC_DIGIT)*
391400
| DEC_DIGIT+ (LETTER | UNDERSCORE) (LETTER | UNDERSCORE | DEC_DIGIT)*
392-
| BACKQUOTE ( ~([\\`]) | (BACKSLASH .) | (BACKQUOTE BACKQUOTE))* BACKQUOTE
393-
| QUOTE_DOUBLE (~([\\"]) | (BACKSLASH .) | (QUOTE_DOUBLE QUOTE_DOUBLE))* QUOTE_DOUBLE
394401
;
395402
FLOATING_LITERAL:
396403
HEXADECIMAL_LITERAL DOT HEX_DIGIT* (P | E) (PLUS | DASH)? DEC_DIGIT+

jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4/ClickHouseParser.g4

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ nameCollectionKey
196196
;
197197

198198
userIdentifier
199-
: (IDENTIFIER | STRING_LITERAL)
199+
: (BACKTICK_ID | QUOTED_IDENTIFIER | IDENTIFIER | STRING_LITERAL)
200200
;
201201

202202
userIdentifiedClause
@@ -1152,7 +1152,8 @@ tableFunctionExpr
11521152
;
11531153

11541154
tableIdentifier
1155-
: (databaseIdentifier DOT)? identifier
1155+
: databaseIdentifier DOT identifier
1156+
| identifier
11561157
;
11571158

11581159
viewIdentifier
@@ -1450,7 +1451,9 @@ alias
14501451
; // |interval| can't be an alias, otherwise 'INTERVAL 1 SOMETHING' becomes ambiguous.
14511452

14521453
identifier
1453-
: IDENTIFIER
1454+
: BACKTICK_ID
1455+
| QUOTED_IDENTIFIER
1456+
| IDENTIFIER
14541457
| interval
14551458
| keyword
14561459
;

jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedPreparedStatement.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
public final class ParsedPreparedStatement {
1010

1111
private String table;
12+
13+
private String database;
1214

1315
private String useDatabase;
1416

@@ -80,6 +82,14 @@ public void setTable(String table) {
8082
this.table = table;
8183
}
8284

85+
public String getDatabase() {
86+
return database;
87+
}
88+
89+
public void setDatabase(String database) {
90+
this.database = database;
91+
}
92+
8393
public int[] getParamPositions() {
8494
return paramPositions;
8595
}

jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParserFacade.java

Lines changed: 48 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.ArrayList;
2626
import java.util.Collections;
2727
import java.util.List;
28+
import java.util.stream.Collectors;
2829

2930
public abstract class SqlParserFacade {
3031

@@ -137,6 +138,14 @@ public ParsedStatement parsedStatement(String sql) {
137138
public ParsedPreparedStatement parsePreparedStatement(String sql) {
138139
ParsedPreparedStatement stmt = new ParsedPreparedStatement();
139140
parseSQL(sql, new ParsedPreparedStatementListener(stmt));
141+
142+
// Combine database and table like JavaCC does
143+
String tableName = stmt.getTable();
144+
if (stmt.getDatabase() != null && stmt.getTable() != null) {
145+
tableName = String.format("%s.%s", stmt.getDatabase(), stmt.getTable());
146+
}
147+
stmt.setTable(tableName);
148+
140149
parseParameters(sql, stmt);
141150
return stmt;
142151
}
@@ -249,56 +258,6 @@ public void enterColumnExprPrecedence3(ClickHouseParser.ColumnExprPrecedence3Con
249258
super.enterColumnExprPrecedence3(ctx);
250259
}
251260

252-
private String unquoteTableIdentifier(String rawTableId) {
253-
if (rawTableId == null || rawTableId.isEmpty()) {
254-
return rawTableId;
255-
}
256-
257-
// Parse respecting quoted identifiers - don't split dots inside quotes
258-
StringBuilder result = new StringBuilder();
259-
boolean inQuote = false;
260-
char quoteChar = 0;
261-
StringBuilder currentPart = new StringBuilder();
262-
263-
for (int i = 0; i < rawTableId.length(); i++) {
264-
char ch = rawTableId.charAt(i);
265-
266-
if (!inQuote && (ch == '`' || ch == '"' || ch == '\'')) {
267-
inQuote = true;
268-
quoteChar = ch;
269-
currentPart.append(ch);
270-
} else if (inQuote && ch == quoteChar) {
271-
// Check for escaped quote (doubled quote)
272-
if (i + 1 < rawTableId.length() && rawTableId.charAt(i + 1) == quoteChar) {
273-
currentPart.append(ch).append(ch);
274-
i++; // Skip the next quote
275-
} else {
276-
inQuote = false;
277-
currentPart.append(ch);
278-
}
279-
} else if (!inQuote && ch == '.') {
280-
// Dot outside quotes - split here
281-
if (result.length() > 0) {
282-
result.append('.');
283-
}
284-
result.append(ClickHouseSqlUtils.unescape(currentPart.toString()));
285-
currentPart.setLength(0);
286-
} else {
287-
currentPart.append(ch);
288-
}
289-
}
290-
291-
// Append the last part
292-
if (currentPart.length() > 0) {
293-
if (result.length() > 0) {
294-
result.append('.');
295-
}
296-
result.append(ClickHouseSqlUtils.unescape(currentPart.toString()));
297-
}
298-
299-
return result.toString();
300-
}
301-
302261
@Override
303262
public void visitErrorNode(ErrorNode node) {
304263
parsedStatement.setHasErrors(true);
@@ -315,19 +274,18 @@ public void enterAssignmentValuesList(ClickHouseParser.AssignmentValuesListConte
315274
parsedStatement.setAssignValuesListStopPosition(ctx.getStop().getStopIndex());
316275
}
317276

318-
319277
@Override
320278
public void enterTableExprIdentifier(ClickHouseParser.TableExprIdentifierContext ctx) {
321279
if (ctx.tableIdentifier() != null) {
322-
parsedStatement.setTable(unquoteTableIdentifier(ctx.tableIdentifier().getText()));
280+
extractAndSetDatabaseAndTable(ctx.tableIdentifier());
323281
}
324282
}
325283

326284
@Override
327285
public void enterInsertStmt(ClickHouseParser.InsertStmtContext ctx) {
328286
ClickHouseParser.TableIdentifierContext tableId = ctx.tableIdentifier();
329287
if (tableId != null) {
330-
parsedStatement.setTable(unquoteTableIdentifier(tableId.getText()));
288+
extractAndSetDatabaseAndTable(tableId);
331289
}
332290

333291
ClickHouseParser.ColumnsClauseContext columns = ctx.columnsClause();
@@ -343,6 +301,35 @@ public void enterInsertStmt(ClickHouseParser.InsertStmtContext ctx) {
343301
parsedStatement.setInsert(true);
344302
}
345303

304+
/**
305+
* Extracts database and table from parse tree using grammar structure.
306+
* Grammar: tableIdentifier = (databaseIdentifier DOT)? identifier
307+
* The grammar itself defines what's database vs table!
308+
*
309+
* Examples:
310+
* table -> databaseIdentifier=null, identifier="table"
311+
* db.table -> databaseIdentifier="db", identifier="table"
312+
* a.b.c -> databaseIdentifier="a.b", identifier="c"
313+
*/
314+
private void extractAndSetDatabaseAndTable(ClickHouseParser.TableIdentifierContext tableId) {
315+
if (tableId == null) {
316+
return;
317+
}
318+
319+
// Table is always the standalone identifier (last part)
320+
if (tableId.identifier() != null) {
321+
parsedStatement.setTable(ClickHouseSqlUtils.unescape(tableId.identifier().getText()));
322+
}
323+
324+
// Database is the databaseIdentifier part (if present)
325+
if (tableId.databaseIdentifier() != null) {
326+
String database = tableId.databaseIdentifier().identifier().stream()
327+
.map(id -> ClickHouseSqlUtils.unescape(id.getText()))
328+
.collect(Collectors.joining("."));
329+
parsedStatement.setDatabase(database);
330+
}
331+
}
332+
346333
@Override
347334
public void enterDataClauseSelect(ClickHouseParser.DataClauseSelectContext ctx) {
348335
parsedStatement.setInsertWithSelect(true);
@@ -366,6 +353,14 @@ private static class ANTLR4AndParamsParser extends ANTLR4Parser {
366353
public ParsedPreparedStatement parsePreparedStatement(String sql) {
367354
ParsedPreparedStatement stmt = new ParsedPreparedStatement();
368355
parseSQL(sql, new ParseStatementAndParamsListener(stmt));
356+
357+
// Combine database and table like JavaCC does
358+
String tableName = stmt.getTable();
359+
if (stmt.getDatabase() != null && stmt.getTable() != null) {
360+
tableName = String.format("%s.%s", stmt.getDatabase(), stmt.getTable());
361+
}
362+
stmt.setTable(tableName);
363+
369364
return stmt;
370365
}
371366

jdbc-v2/src/main/javacc/ClickHouseSqlParser.jj

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -903,36 +903,44 @@ void nestedIdentifier(): {} {
903903
(<ASTERISK> | anyIdentifier()) (LOOKAHEAD(2) <DOT> (<ASTERISK> | anyIdentifier()))*
904904
}
905905

906-
void tableIdentifier(boolean record): { Token t; java.util.List<String> parts = new java.util.ArrayList<>(); } {
906+
void tableIdentifier(boolean record): { } {
907907
(
908-
t = anyIdentifier() { parts.add(ClickHouseSqlUtils.unescape(t.image)); }
909-
(
910-
LOOKAHEAD(2) <DOT> t = anyIdentifier() { parts.add(ClickHouseSqlUtils.unescape(t.image)); }
911-
)*
912-
(LOOKAHEAD(2)
913-
<LPAREN> { token_source.addCustomKeywordPosition(ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_START, token); }
914-
anyExprList()
915-
<RPAREN> { token_source.addCustomKeywordPosition(ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_END, token); }
916-
)?
917-
)
908+
// Match all identifiers followed by DOT as database parts
909+
LOOKAHEAD(anyIdentifier() <DOT>) databasePart(record) <DOT>
910+
)*
911+
// Last identifier is the table
912+
tablePart(record)
913+
// Optional column list
914+
(LOOKAHEAD(2)
915+
<LPAREN> { token_source.addCustomKeywordPosition(ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_START, token); }
916+
anyExprList()
917+
<RPAREN> { token_source.addCustomKeywordPosition(ClickHouseSqlStatement.KEYWORD_TABLE_COLUMNS_END, token); }
918+
)?
919+
}
920+
921+
void databasePart(boolean record): { Token t; } {
922+
t = anyIdentifier()
918923
{
919-
if (record && token_source.table == null && parts.size() > 0) {
920-
// Last part is always the table name
921-
token_source.table = parts.get(parts.size() - 1);
922-
923-
// All parts before the last are the database (if any)
924-
if (parts.size() > 1) {
925-
StringBuilder db = new StringBuilder();
926-
for (int i = 0; i < parts.size() - 1; i++) {
927-
if (i > 0) db.append('.');
928-
db.append(parts.get(i));
929-
}
930-
token_source.database = db.toString();
924+
if (record && token_source.table == null) {
925+
String part = ClickHouseSqlUtils.unescape(t.image);
926+
if (token_source.database == null) {
927+
token_source.database = part;
928+
} else {
929+
token_source.database = token_source.database + "." + part;
931930
}
932931
}
933932
}
934933
}
935934

935+
void tablePart(boolean record): { Token t; } {
936+
t = anyIdentifier()
937+
{
938+
if (record && token_source.table == null) {
939+
token_source.table = ClickHouseSqlUtils.unescape(t.image);
940+
}
941+
}
942+
}
943+
936944
void databaseIdentifier(boolean record): { Token t; } {
937945
t = anyIdentifier() { if (record) token_source.database = ClickHouseSqlUtils.unescape(t.image); }
938946
}

0 commit comments

Comments
 (0)