diff --git a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4_light/ClickHouseLexer.g4 b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4_light/ClickHouseLexer.g4 new file mode 100644 index 000000000..64b99c264 --- /dev/null +++ b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4_light/ClickHouseLexer.g4 @@ -0,0 +1,489 @@ + +// $antlr-format alignColons trailing, alignLabels true, alignLexerCommands true, alignSemicolons ownLine, alignTrailers true +// $antlr-format alignTrailingComments true, allowShortBlocksOnASingleLine true, allowShortRulesOnASingleLine true, columnLimit 150 +// $antlr-format maxEmptyLinesToKeep 1, minEmptyLines 0, reflowComments false, singleLineOverrulesHangingColon true, useTab false + +lexer grammar ClickHouseLexer; + +// NOTE: don't forget to add new keywords to the parser rule "keyword"! + +// Keywords + + + +ACCESS : A C C E S S; +ADD : A D D; +ADMIN : A D M I N; +AFTER : A F T E R; +ALIAS : A L I A S; +ALL : A L L; +ALLOW : A L L O W; +ALTER : A L T E R; +AND : A N D; +ANTI : A N T I; +ANY : A N Y; +ARBITRARY : A R B I T R A R Y ; +ARRAY : A R R A Y; +AS : A S; +ASCENDING : A S C | A S C E N D I N G; +ASOF : A S O F; +AST : A S T; +ASYNC : A S Y N C; +ASYNCHRONOUS : A S Y N C H R O N O U S ; +ATTACH : A T T A C H; +AZURE : A Z U R E; +BACKUP : B A C K U P; +BCRYPT_HASH : B C R Y P T '_' H A S H; +BCRYPT_PASSWORD : B C R Y P T '_' P A S S W O R D; +BETWEEN : B E T W E E N; +BLOCKING : B L O C K I N G ; +BOTH : B O T H; +BY : B Y; +CACHE : C A C H E ; +CACHES : C A C H E S ; +CANCEL : C A N C E L; +CASE : C A S E; +CAST : C A S T; +CHANGEABLE_IN_READONLY : C H A N G E A B L E UNDERSCORE I N UNDERSCORE R E A D O N L Y; +CHANGED : C H A N G E D; +CHECK : C H E C K; +CLEANUP : C L E A N U P; +CLEAR : C L E A R; +CLIENT : C L I E N T ; +CLUSTER : C L U S T E R; +CLUSTERS : C L U S T E R S; +CN : C N; +CODEC : C O D E C; +COLLATE : C O L L A T E; +COLLECTION : C O L L E C T I O N ; +COLLECTIONS : C O L L E C T I O N S ; +COLUMN : C O L U M N; +COLUMNS : C O L U M N S ; +COMMENT : C O M M E N T; +COMPILED : C O M P I L E D ; +CONDITION : C O N D I T I O N; +CONFIG : C O N F I G ; +CONNECTIONS : C O N N E C T I O N S ; +CONST : C O N S T; +CONSTRAINT : C O N S T R A I N T; +CREATE : C R E A T E; +CROSS : C R O S S; +CUBE : C U B E; +CURRENT : C U R R E N T; +CURRENT_USER : C U R R E N T '_' U S E R; +CUSTOM : C U S T O M; +DATABASE : D A T A B A S E; +DATABASES : D A T A B A S E S; +DATE : D A T E; +DAY : D A Y; +DEDUPLICATE : D E D U P L I C A T E; +DEFAULT : D E F A U L T; +DEFINER : D E F I N E R; +DELAY : D E L A Y; +DELETE : D E L E T E; +DESC : D E S C; +DESCENDING : D E S C E N D I N G; +DESCRIBE : D E S C R I B E; +DETACH : D E T A C H; +DICTIONARIES : D I C T I O N A R I E S; +DICTIONARY : D I C T I O N A R Y; +DISK : D I S K; +DISTINCT : D I S T I N C T; +DISTRIBUTED : D I S T R I B U T E D; +DNS : D N S ; +DOUBLE_SHA1_HASH : D O U B L E '_' S H A '1' '_' H A S H; +DOUBLE_SHA1_PASSWORD : D O U B L E '_' S H A '1' '_' P A S S W O R D; +DROP : D R O P; +ELSE : E L S E; +EMBEDDED : E M B E D D E D ; +ENABLED : E N A B L E D; +END : E N D; +ENGINE : E N G I N E; +ENGINES : E N G I N E S; +ESTIMATE : E S T I M A T E; +EVENTS : E V E N T S; +EXCEPT : E X C E P T; +EXCHANGE : E X C H A N G E; +EXISTS : E X I S T S; +EXPLAIN : E X P L A I N; +EXPRESSION : E X P R E S S I O N; +EXTENDED : E X T E N D E D; +EXTRACT : E X T R A C T; +FAILPOINT : F A I L P O I N T ; +FETCHES : F E T C H E S; +FETCH : F E T C H ; +FILE : F I L E; +FILESYSTEM : F I L E S Y S T E M ; +FILTER : F I L T E R; +FINAL : F I N A L; +FIRST : F I R S T; +FLUSH : F L U S H; +FOLLOWING : F O L L O W I N G; +FOR : F O R; +FORMAT : F O R M A T; +FREEZE : F R E E Z E; +FROM : F R O M; +FULL : F U L L; +FUNCTION : F U N C T I O N; +FUNCTIONS : F U N C T I O N S; +FUZZER : F U Z Z E R ; +GLOBAL : G L O B A L; +GRANTEES : G R A N T E E S; +GRANT : G R A N T; +GRANTS : G R A N T S; +GRANULARITY : G R A N U L A R I T Y; +GROUP : G R O U P; +GRPC : G R P C; +HAVING : H A V I N G; +HDFS : H D F S; +HIERARCHICAL : H I E R A R C H I C A L; +HIVE : H I V E; +HOST : H O S T; +HOUR : H O U R; +HTTP : H T T P; +HTTPS : H T T P S; +IDENTIFIED : I D E N T I F I E D; +ID : I D; +IF : I F; +ILIKE : I L I K E; +IMPLICIT : I M P L I C I T; +INDEXES : I N D E X E S; +INDEX : I N D E X; +INDICES : I N D I C E S; +INF : I N F | I N F I N I T Y; +INHERIT : I N H E R I T; +IN : I N; +INJECTIVE : I N J E C T I V E; +INNER : I N N E R; +INSERT : I N S E R T; +INTERVAL : I N T E R V A L; +INTO : I N T O; +INTROSPECTION : I N T R O S P E C T I O N; +IP : I P; +IS : I S; +IS_OBJECT_ID : I S UNDERSCORE O B J E C T UNDERSCORE I D; +JDBC : J D B C; +JEMALLOC : J E M A L L O C ; +JOIN : J O I N; +KAFKA : K A F K A; +KERBEROS : K E R B E R O S; +KEYED : K E Y E D; +KEY : K E Y; +KEYS : K E Y S; +KILL : K I L L; +LAST : L A S T; +LAYOUT : L A Y O U T; +LDAP : L D A P; +LEADING : L E A D I N G; +LEFT : L E F T; +LIFETIME : L I F E T I M E; +LIGHTWEIGHT : L I G H T W E I G H T; +LIKE : L I K E; +LIMIT : L I M I T; +LIMITS : L I M I T S; +LISTEN : L I S T E N ; +LIVE : L I V E; +LOADING : L O A D I N G; +LOAD : L O A D ; +LOCAL : L O C A L; +LOG : L O G ; +LOGS : L O G S; +MANAGEMENT : M A N A G E M E N T; +MARK : M A R K ; +MATERIALIZED : M A T E R I A L I Z E D; +MATERIALIZE : M A T E R I A L I Z E; +MAX : M A X; +MERGES : M E R G E S; +METRICS : M E T R I C S ; +MIN : M I N; +MINUTE : M I N U T E; +MMAP : M M A P ; +MODEL : M O D E L ; +MODIFY : M O D I F Y; +MONGO : M O N G O; +MONTH : M O N T H; +MOVE : M O V E; +MOVES : M O V E S ; +MUTATION : M U T A T I O N; +MYSQL : M Y S Q L; +NAMED : N A M E D ; +NAME : N A M E; +NAN_SQL : N A N; // conflicts with macro NAN +NATS : N A T S; +NONE : N O N E; +NO : N O; +NO_PASSWORD : N O '_' P A S S W O R D; +NOT : N O T; +NULLS : N U L L S; +NULL_SQL : N U L L; // conflicts with macro NULL +ODBC : O D B C; +OFFSET : O F F S E T; +ONLY : O N L Y; +ON : O N; +OPTIMIZE : O P T I M I Z E; +OPTION : O P T I O N; +ORDER : O R D E R; +OR : O R; +OUTER : O U T E R; +OUTFILE : O U T F I L E; +OVER : O V E R; +OVERRIDE : O V E R R I D E; +PAGE : P A G E ; +PARTITION : P A R T I T I O N; +PART : P A R T; +PARTS : P A R T S; +PERMISSIVE : P E R M I S S I V E; +PIPELINE : P I P E L I N E; +PLAINTEXT_PASSWORD : P L A I N T E X T '_' P A S S W O R D; +PLAN : P L A N; +POLICIES : P O L I C I E S ; +POLICY : P O L I C Y; +POPULATE : P O P U L A T E; +POSTGRES : P O S T G R E S; +POSTGRESQL : P O S T G R E S Q L; +PRECEDING : P R E C E D I N G; +PREWHERE : P R E W H E R E; +PRIMARY : P R I M A R Y; +PROCESSLIST : P R O C E S S L I S T; +PROFILE : P R O F I L E; +PROFILES : P R O F I L E S; +PROJECTION : P R O J E C T I O N; +PROMETHEUS : P R O M E T H E U S; +PROXY : P R O X Y; +PULLING : P U L L I N G ; +PULL : P U L L; +QUARTER : Q U A R T E R; +QUERIES : Q U E R I E S; +QUERY : Q U E R Y; +QUEUE : Q U E U E ; +QUEUES : Q U E U E S ; +QUOTA : Q U O T A; +QUOTAS : Q U O T A S ; +RABBITMQ : R A B B I T M Q; +RANDOMIZED : R A N D O M I Z E D; +RANGE : R A N G E; +READINESS : R E A D I N E S S; +READONLY : R E A D O N L Y; +REALM : R E A L M; +REDIS : R E D I S; +REDUCE : R E D U C E ; +REFRESH : R E F R E S H ; +REGEXP : R E G E X P; +RELOAD : R E L O A D; +REMOTE : R E M O T E; +REMOVE : R E M O V E; +RENAME : R E N A M E; +REPLACE : R E P L A C E; +REPLICA : R E P L I C A; +REPLICAS : R E P L I C A S; +REPLICATED : R E P L I C A T E D; +REPLICATION : R E P L I C A T I O N ; +RESOURCE : R E S O U R C E ; +RESTART : R E S T A R T; +RESTORE : R E S T O R E ; +RESTRICTIVE : R E S T R I C T I V E; +REVOKE : R E V O K E; +RIGHT : R I G H T; +ROLE : R O L E; +ROLES : R O L E S ; +ROLLUP : R O L L U P; +ROW : R O W; +ROWS : R O W S; +S3 : S '3'; +SAMPLE : S A M P L E; +SCHEMA : S C H E M A; +SCRAM_SHA256_HASH : S C R A M '_' S H A '2' '5' '6' '_' H A S H; +SCRAM_SHA256_PASSWORD : S C R A M '_' S H A '2' '5' '6' '_' P A S S W O R D; +SECOND : S E C O N D; +SECRETS : S E C R E T S ; +SECURE : S E C U R E; +SECURITY : S E C U R I T Y; +SELECT : S E L E C T; +SEMI : S E M I; +SENDS : S E N D S; +SERVER : S E R V E R; +SET : S E T; +SETTING : S E T T I N G; +SETTINGS : S E T T I N G S; +SHA256_HASH : S H A '2' '5' '6' '_' H A S H; +SHA256_PASSWORD : S H A '2' '5' '6' '_' P A S S W O R D; +SHARD : S H A R D; +SHARDS : S H A R D S; +SHOW : S H O W; +SHUTDOWN : S H U T D O W N ; +SOURCE : S O U R C E; +SOURCES : S O U R C E S; +SQLITE : S Q L I T E; +SQL : S Q L; +SSH_KEY : S S H '_' K E Y; +SSL_CERTIFICATE : S S L '_' C E R T I F I C A T E; +START : S T A R T; +STATISTICS : S T A T I S T I C S ; +STOP : S T O P; +STRICT : S T R I C T; +SUBSTRING : S U B S T R I N G; +SYNC : S Y N C; +SYNTAX : S Y N T A X; +SYSTEM : S Y S T E M; +TABLES : T A B L E S; +TABLE : T A B L E; +TAG : T A G; +TCP : T C P; +TEMPORARY : T E M P O R A R Y; +TEST : T E S T; +THEN : T H E N; +THREAD : T H R E A D ; +TIES : T I E S; +TIMEOUT : T I M E O U T; +TIMESTAMP : T I M E S T A M P; +TOP : T O P; +TOTALS : T O T A L S; +TO : T O; +TRACKING : T R A C K I N G; +TRAILING : T R A I L I N G; +TRANSACTION : T R A N S A C T I O N; +TREE : T R E E; +TRIM : T R I M; +TRUNCATE : T R U N C A T E; +TTL : T T L; +TYPE : T Y P E; +UNBOUNDED : U N B O U N D E D; +UNCOMPRESSED : U N C O M P R E S S E D ; +UNDROP : U N D R O P; +UNFREEZE : U N F R E E Z E ; +UNION : U N I O N; +UNLOAD : U N L O A D ; +UNTIL : U N T I L; +UPDATE : U P D A T E; +URL : U R L; +USERS : U S E R S ; +USER : U S E R; +USE : U S E; +USING : U S I N G; +UUID : U U I D; +VALID : V A L I D; +VALUES : V A L U E S; +VIEWS : V I E W S; +VIEW : V I E W; +VIRTUAL : V I R T U A L; +VOLUME : V O L U M E; +WAIT : W A I T; +WATCH : W A T C H; +WEEK : W E E K; +WHEN : W H E N; +WHERE : W H E R E; +WINDOW : W I N D O W; +WITH : W I T H; +WORKLOAD : W O R K L O A D; +WRITABLE : W R I T A B L E; +YEAR : Y E A R | Y Y Y Y; +ZKPATH : Z K P A T H; +SUM : S U M; +AVG : A V G; + +JSON_FALSE : 'false'; +JSON_TRUE : 'true'; + +// Tokens + +// Order matters: quoted identifiers must come before unquoted IDENTIFIER +BACKTICK_ID: + BACKQUOTE ( ~([\\`]) | (BACKSLASH .) | (BACKQUOTE BACKQUOTE))* BACKQUOTE +; + +QUOTED_IDENTIFIER: + QUOTE_DOUBLE (~([\\"]) | (BACKSLASH .) | (QUOTE_DOUBLE QUOTE_DOUBLE))* QUOTE_DOUBLE +; + +IDENTIFIER: + (LETTER | UNDERSCORE) (LETTER | UNDERSCORE | DEC_DIGIT)* + | DEC_DIGIT+ (LETTER | UNDERSCORE) (LETTER | UNDERSCORE | DEC_DIGIT)* +; +FLOATING_LITERAL: + HEXADECIMAL_LITERAL DOT HEX_DIGIT* (P | E) (PLUS | DASH)? DEC_DIGIT+ + | HEXADECIMAL_LITERAL (P | E) (PLUS | DASH)? DEC_DIGIT+ + | DECIMAL_LITERAL DOT DEC_DIGIT* E (PLUS | DASH)? DEC_DIGIT+ + | DOT DECIMAL_LITERAL E (PLUS | DASH)? DEC_DIGIT+ + | DECIMAL_LITERAL E (PLUS | DASH)? DEC_DIGIT+ +; +OCTAL_LITERAL : '0' OCT_DIGIT+; +DECIMAL_LITERAL : DEC_DIGIT+; +HEXADECIMAL_LITERAL : '0' X HEX_DIGIT+; + +CAST_OP : '::'; + +// It's important that quote-symbol is a single character. +STRING_LITERAL: + QUOTE_SINGLE (~([\\']) | (BACKSLASH .) | (QUOTE_SINGLE QUOTE_SINGLE))* QUOTE_SINGLE +; + +// Alphabet and allowed symbols + +fragment A : [aA]; +fragment B : [bB]; +fragment C : [cC]; +fragment D : [dD]; +fragment E : [eE]; +fragment F : [fF]; +fragment G : [gG]; +fragment H : [hH]; +fragment I : [iI]; +fragment J : [jJ]; +fragment K : [kK]; +fragment L : [lL]; +fragment M : [mM]; +fragment N : [nN]; +fragment O : [oO]; +fragment P : [pP]; +fragment Q : [qQ]; +fragment R : [rR]; +fragment S : [sS]; +fragment T : [tT]; +fragment U : [uU]; +fragment V : [vV]; +fragment W : [wW]; +fragment X : [xX]; +fragment Y : [yY]; +fragment Z : [zZ]; + +fragment LETTER : [a-zA-Z]; +fragment OCT_DIGIT : [0-7]; +fragment DEC_DIGIT : [0-9]; +fragment HEX_DIGIT : [0-9a-fA-F]; + +ARROW : '->'; +ASTERISK : '*'; +BACKQUOTE : '`'; +BACKSLASH : '\\'; +COLON : ':'; +COMMA : ','; +CONCAT : '||'; +DASH : '-'; +DOT : '.'; +EQ_DOUBLE : '=='; +EQ_SINGLE : '='; +GE : '>='; +GT : '>'; +LBRACE : '{'; +LBRACKET : '['; +LE : '<='; +LPAREN : '('; +LT : '<'; +NOT_EQ : '!=' | '<>'; +PERCENT : '%'; +PLUS : '+'; +JDBC_PARAM_PLACEHOLDER : '?'; +QUOTE_DOUBLE : '"'; +QUOTE_SINGLE : '\''; +RBRACE : '}'; +RBRACKET : ']'; +RPAREN : ')'; +SEMICOLON : ';'; +SLASH : '/'; +UNDERSCORE : '_'; + +// Comments and whitespace + +MULTI_LINE_COMMENT : '/*' .*? '*/' -> skip; +SINGLE_LINE_COMMENT : ('--' | '#!' | '#') ~('\n' | '\r')* ('\n' | '\r' | EOF) -> skip; +WHITESPACE : [ \u000B\u000C\t\r\n] -> skip; // '\n' can be part of multiline single query \ No newline at end of file diff --git a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4_light/ClickHouseLightParser.g4 b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4_light/ClickHouseLightParser.g4 new file mode 100644 index 000000000..9a5a3f7a8 --- /dev/null +++ b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/parser/antlr4_light/ClickHouseLightParser.g4 @@ -0,0 +1,190 @@ + +// Simplified ClickHouse SQL parser. +// +// Design goals: +// - Filter comments (handled by the lexer: /* */, --, #!, #) +// - Detect statement verb (SELECT, SHOW, INSERT, SET, EXPLAIN, etc.) +// - For INSERT: parse table name and optional column list +// - For SET: parse completely (key = value pairs) +// - For everything else: accept any tokens without detailed parsing + +parser grammar ClickHouseLightParser; + +options { + tokenVocab = ClickHouseLexer; +} + +// Top-level entry point + +queryStmt + : insertStmt SEMICOLON? EOF # InsertQueryStmt + | setStmt SEMICOLON? EOF # SetQueryStmt + | useStmt SEMICOLON? EOF # UseQueryStmt + | otherStmt SEMICOLON? EOF # OtherQueryStmt + ; + +// INSERT - parse table identifier and optional column list, accept rest + +insertStmt + : INSERT INTO TABLE? tableIdentifier columnsClause? dataClause restOfQuery # InsertTableStmt + | INSERT INTO FUNCTION identifier LPAREN functionArgs RPAREN columnsClause? dataClause restOfQuery # InsertFunctionStmt + ; + +columnsClause + : LPAREN nestedIdentifier (COMMA nestedIdentifier)* RPAREN + ; + +functionArgs + : (LPAREN functionArgs RPAREN | ~(LPAREN | RPAREN))* + ; + +dataClause + : FORMAT identifier # DataClauseFormat + | VALUES assignmentValues (COMMA assignmentValues)* # DataClauseValues + | (WITH | SELECT) restOfQuery # DataClauseSelect + ; + +assignmentValues + : LPAREN assignmentValue? (COMMA assignmentValue)* RPAREN + ; + +assignmentValue + : literal # InsertRawValue + | JDBC_PARAM_PLACEHOLDER # InsertParameter + | identifier functionArgs # InsertParameterFuncExpr + | LPAREN? .* RPAREN? # InserParameterExpr + ; + +// SET - fully parsed + +setStmt + : SET settingExprList + ; + +// USE - fully parsed + +useStmt + : USE identifier + ; + +settingExprList + : settingExpr (COMMA settingExpr)* + ; + +settingExpr + : identifier EQ_SINGLE (literal | JDBC_PARAM_PLACEHOLDER) + ; + +// Any other statement - capture first verb if present, accept rest + +otherStmt + : statementVerb restOfQuery # OtherWithVerbStmt + | restOfQuery # OtherNoVerbStmt + ; + +statementVerb + : keyword + | IDENTIFIER + ; + +// Consume all remaining non-semicolon tokens + +restOfQuery + : ~SEMICOLON* + ; + +// Table and column identifiers + +tableIdentifier + : identifier (DOT identifier)* + ; + +nestedIdentifier + : identifier (DOT identifier)? + ; + +// Literals + +literal + : numberLiteral + | STRING_LITERAL + | NULL_SQL + | JSON_TRUE + | JSON_FALSE + ; + +numberLiteral + : (PLUS | DASH)? ( + FLOATING_LITERAL + | OCTAL_LITERAL + | DECIMAL_LITERAL + | HEXADECIMAL_LITERAL + | INF + | NAN_SQL + ) + ; + +// Identifiers - all keywords can be used as identifiers + +identifier + : BACKTICK_ID + | QUOTED_IDENTIFIER + | IDENTIFIER + | keyword + ; + +// All keywords (so they can appear as table names, column names, setting names, etc.) + +keyword + : ACCESS | ADD | ADMIN | AFTER | ALIAS | ALL | ALLOW | ALTER | AND | ANTI | ANY + | ARBITRARY | ARRAY | AS | ASCENDING | ASOF | AST | ASYNC | ASYNCHRONOUS | ATTACH + | AZURE + | BACKUP | BCRYPT_HASH | BCRYPT_PASSWORD | BETWEEN | BLOCKING | BOTH | BY + | CACHE | CACHES | CANCEL | CASE | CAST | CHANGEABLE_IN_READONLY | CHANGED | CHECK + | CLEANUP | CLEAR | CLIENT | CLUSTER | CLUSTERS | CN | CODEC | COLLATE | COLLECTION + | COLLECTIONS | COLUMN | COLUMNS | COMMENT | COMPILED | CONDITION | CONFIG | CONNECTIONS + | CONST | CONSTRAINT | CREATE | CROSS | CUBE | CURRENT | CURRENT_USER | CUSTOM + | DATABASE | DATABASES | DATE | DAY | DEDUPLICATE | DEFAULT | DEFINER | DELAY | DELETE + | DESC | DESCENDING | DESCRIBE | DETACH | DICTIONARIES | DICTIONARY | DISK | DISTINCT + | DISTRIBUTED | DNS | DOUBLE_SHA1_HASH | DOUBLE_SHA1_PASSWORD | DROP + | ELSE | EMBEDDED | ENABLED | END | ENGINE | ENGINES | ESTIMATE | EVENTS | EXCEPT + | EXCHANGE | EXISTS | EXPLAIN | EXPRESSION | EXTENDED | EXTRACT + | FAILPOINT | FETCH | FETCHES | FILE | FILESYSTEM | FILTER | FINAL | FIRST | FLUSH + | FOLLOWING | FOR | FORMAT | FREEZE | FROM | FULL | FUNCTION | FUNCTIONS | FUZZER + | GLOBAL | GRANT | GRANTEES | GRANTS | GRANULARITY | GROUP | GRPC + | HAVING | HDFS | HIERARCHICAL | HIVE | HOST | HOUR | HTTP | HTTPS + | ID | IDENTIFIED | IF | ILIKE | IMPLICIT | IN | INDEX | INDEXES | INDICES + | INHERIT | INJECTIVE | INNER | INSERT | INTERVAL | INTO | INTROSPECTION | IP | IS + | IS_OBJECT_ID + | JDBC | JEMALLOC | JOIN | JSON_FALSE | JSON_TRUE + | KAFKA | KERBEROS | KEY | KEYED | KEYS | KILL + | LAST | LAYOUT | LDAP | LEADING | LEFT | LIFETIME | LIGHTWEIGHT | LIKE | LIMIT | LIMITS + | LISTEN | LIVE | LOAD | LOADING | LOCAL | LOG | LOGS + | MANAGEMENT | MARK | MATERIALIZE | MATERIALIZED | MAX | MERGES | METRICS | MIN | MINUTE + | MMAP | MODEL | MODIFY | MONGO | MONTH | MOVE | MOVES | MUTATION | MYSQL + | NAME | NAMED | NATS | NO | NONE | NOT | NO_PASSWORD | NULLS | NULL_SQL + | ODBC | OFFSET | ON | ONLY | OPTIMIZE | OPTION | OR | ORDER | OUTER | OUTFILE | OVER + | OVERRIDE + | PAGE | PART | PARTITION | PARTS | PERMISSIVE | PIPELINE | PLAINTEXT_PASSWORD | PLAN + | POLICIES | POLICY | POPULATE | POSTGRES | POSTGRESQL | PRECEDING | PREWHERE | PRIMARY + | PROCESSLIST | PROFILE | PROFILES | PROJECTION | PROMETHEUS | PROXY | PULL | PULLING + | QUARTER | QUERIES | QUERY | QUEUE | QUEUES | QUOTA | QUOTAS + | RABBITMQ | RANDOMIZED | RANGE | READINESS | READONLY | REALM | REDIS | REDUCE | REFRESH + | REGEXP | RELOAD | REMOTE | REMOVE | RENAME | REPLACE | REPLICA | REPLICAS | REPLICATED + | REPLICATION | RESOURCE | RESTART | RESTORE | RESTRICTIVE | REVOKE | RIGHT | ROLE | ROLES + | ROLLUP | ROW | ROWS + | S3 | SAMPLE | SCHEMA | SCRAM_SHA256_HASH | SCRAM_SHA256_PASSWORD | SECOND | SECRETS + | SECURE | SECURITY | SELECT | SEMI | SENDS | SERVER | SET | SETTING | SETTINGS + | SHA256_HASH | SHA256_PASSWORD | SHARD | SHARDS | SHOW | SHUTDOWN | SOURCE | SOURCES + | SQLITE | SQL | SSH_KEY | SSL_CERTIFICATE | START | STATISTICS | STOP | STRICT + | SUBSTRING | SYNC | SYNTAX | SYSTEM + | TABLE | TABLES | TAG | TCP | TEMPORARY | TEST | THEN | THREAD | TIES | TIMEOUT + | TIMESTAMP | TO | TOP | TOTALS | TRACKING | TRAILING | TRANSACTION | TREE | TRIM + | TRUNCATE | TTL | TYPE + | UNBOUNDED | UNCOMPRESSED | UNDROP | UNFREEZE | UNION | UNLOAD | UNTIL | UPDATE | URL + | USE | USER | USERS | USING | UUID + | VALID | VALUES | VIEW | VIEWS | VIRTUAL | VOLUME + | WAIT | WATCH | WEEK | WHEN | WHERE | WINDOW | WITH | WORKLOAD | WRITABLE + | YEAR | ZKPATH + | SUM | AVG + ; diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedPreparedStatement.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedPreparedStatement.java index 4f02dd5ab..d49f700a5 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedPreparedStatement.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedPreparedStatement.java @@ -38,6 +38,8 @@ public final class ParsedPreparedStatement { private int assignValuesGroups = 0; + private String statementVerb; + public void setHasResultSet(boolean hasResultSet) { this.hasResultSet = hasResultSet; } @@ -150,6 +152,14 @@ public void setHasErrors(boolean hasErrors) { this.hasErrors = hasErrors; } + public String getStatementVerb() { + return statementVerb; + } + + public void setStatementVerb(String statementVerb) { + this.statementVerb = statementVerb; + } + void appendParameter(int startIndex) { argCount++; if (argCount > paramPositions.length) { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java index f9eb5cde8..5b20f2cfc 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java @@ -17,6 +17,8 @@ public final class ParsedStatement { private boolean hasErrors; + private String statementVerb; + public void setUseDatabase(String useDatabase) { this.useDatabase = useDatabase; } @@ -56,4 +58,12 @@ public boolean isHasErrors() { public void setHasErrors(boolean hasErrors) { this.hasErrors = hasErrors; } + + public String getStatementVerb() { + return statementVerb; + } + + public void setStatementVerb(String statementVerb) { + this.statementVerb = statementVerb; + } } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParserFacade.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParserFacade.java index f819fea49..6553374b0 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParserFacade.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParserFacade.java @@ -1,11 +1,15 @@ package com.clickhouse.jdbc.internal; import com.clickhouse.client.api.sql.SQLUtils; +import com.google.common.collect.ImmutableSet; import com.clickhouse.data.ClickHouseUtils; import com.clickhouse.jdbc.DriverProperties; import com.clickhouse.jdbc.internal.parser.antlr4.ClickHouseLexer; import com.clickhouse.jdbc.internal.parser.antlr4.ClickHouseParser; import com.clickhouse.jdbc.internal.parser.antlr4.ClickHouseParserBaseListener; +import com.clickhouse.jdbc.internal.parser.antlr4_light.ClickHouseLightParser; +import com.clickhouse.jdbc.internal.parser.antlr4_light.ClickHouseLightParserBaseListener; +import com.clickhouse.jdbc.internal.parser.antlr4_light.ClickHouseLightParserListener; import com.clickhouse.jdbc.internal.parser.javacc.ClickHouseSqlParser; import com.clickhouse.jdbc.internal.parser.javacc.ClickHouseSqlStatement; import com.clickhouse.jdbc.internal.parser.javacc.ClickHouseSqlUtils; @@ -27,6 +31,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Locale; import java.util.stream.Collectors; public abstract class SqlParserFacade { @@ -444,6 +449,236 @@ public void enterViewParam(ClickHouseParser.ViewParamContext ctx) { } } + private static class ANTLR4LightParser extends SqlParserFacade { + private static final ImmutableSet VERBS_WITHOUT_RESULT_SET = ImmutableSet.builder() + .add("ALTER") + .add("ATTACH") + .add("BACKUP") + .add("CREATE") + .add("DELETE") + .add("DETACH") + .add("DROP") + .add("EXCHANGE") + .add("GRANT") + .add("INSERT") + .add("KILL") + .add("MOVE") + .add("OPTIMIZE") + .add("RENAME") + .add("REPLACE") + .add("RESTORE") + .add("REVOKE") + .add("SET") + .add("TRUNCATE") + .add("UNDROP") + .add("UPDATE") + .add("USE") + .build(); + + private static boolean isStmtWithResultSetVerb(String verb) { + return !VERBS_WITHOUT_RESULT_SET.contains(verb); + } + + private static String normalizeVerb(String rawVerb) { + return rawVerb == null ? null : rawVerb.toUpperCase(Locale.ROOT); + } + + @Override + public ParsedStatement parsedStatement(String sql) { + ParsedStatement stmt = new ParsedStatement(); + parseSQL(sql, new ANTLR4LightParser.ParsedStatementListener(stmt)); + return stmt; + } + + @Override + public ParsedPreparedStatement parsePreparedStatement(String sql) { + ParsedPreparedStatement stmt = new ParsedPreparedStatement(); + parseSQL(sql, new ANTLR4LightParser.ParsedPreparedStatementListener(stmt)); + + // Combine database and table like JavaCC does + String tableName = stmt.getTable(); + if (stmt.getDatabase() != null && stmt.getTable() != null) { + tableName = String.format("%s.%s", stmt.getDatabase(), stmt.getTable()); + } + stmt.setTable(tableName); + + parseParameters(sql, stmt); + return stmt; + } + + protected ClickHouseLightParser parseSQL(String sql, ClickHouseLightParserListener listener) { + CharStream charStream = CharStreams.fromString(sql); + ClickHouseLexer lexer = new ClickHouseLexer(charStream); + ClickHouseLightParser parser = new ClickHouseLightParser(new CommonTokenStream(lexer)); + parser.removeErrorListeners(); + parser.addErrorListener(new ANTLR4Parser.ParserErrorListener()); + + ClickHouseLightParser.QueryStmtContext parseTree = parser.queryStmt(); + IterativeParseTreeWalker.DEFAULT.walk(listener, parseTree); + + return parser; + } + + private static class ParsedStatementListener extends ClickHouseLightParserBaseListener { + private final ParsedStatement stmt; + + public ParsedStatementListener(ParsedStatement stmt) { + this.stmt = stmt; + this.stmt.setHasResultSet(true); + this.stmt.setInsert(false); + } + + @Override + public void visitErrorNode(ErrorNode node) { + stmt.setHasErrors(true); + } + + @Override + public void enterInsertQueryStmt(ClickHouseLightParser.InsertQueryStmtContext ctx) { + stmt.setStatementVerb("INSERT"); + stmt.setInsert(true); + stmt.setHasResultSet(false); + } + + @Override + public void enterSetQueryStmt(ClickHouseLightParser.SetQueryStmtContext ctx) { + stmt.setStatementVerb("SET"); + stmt.setHasResultSet(false); + } + + @Override + public void enterUseQueryStmt(ClickHouseLightParser.UseQueryStmtContext ctx) { + stmt.setStatementVerb("USE"); + stmt.setHasResultSet(false); + } + + @Override + public void enterUseStmt(ClickHouseLightParser.UseStmtContext ctx) { + if (ctx.identifier() != null) { + stmt.setUseDatabase(SQLUtils.unquoteIdentifier(ctx.identifier().getText())); + } + } + + @Override + public void enterStatementVerb(ClickHouseLightParser.StatementVerbContext ctx) { + String verb = normalizeVerb(ctx.getText()); + stmt.setStatementVerb(verb); + stmt.setHasResultSet(isStmtWithResultSetVerb(verb)); + } + } + + private static class ParsedPreparedStatementListener extends ClickHouseLightParserBaseListener { + + private final ParsedPreparedStatement stmt; + + public ParsedPreparedStatementListener(ParsedPreparedStatement stmt) { + this.stmt = stmt; + this.stmt.setHasResultSet(true); + this.stmt.setInsert(false); + } + + @Override + public void visitErrorNode(ErrorNode node) { + stmt.setHasErrors(true); + } + + @Override + public void enterInsertQueryStmt(ClickHouseLightParser.InsertQueryStmtContext ctx) { + stmt.setStatementVerb("INSERT"); + stmt.setInsert(true); + stmt.setHasResultSet(false); + } + + @Override + public void enterSetQueryStmt(ClickHouseLightParser.SetQueryStmtContext ctx) { + stmt.setStatementVerb("SET"); + stmt.setHasResultSet(false); + } + + @Override + public void enterUseQueryStmt(ClickHouseLightParser.UseQueryStmtContext ctx) { + stmt.setStatementVerb("USE"); + stmt.setHasResultSet(false); + } + + @Override + public void enterUseStmt(ClickHouseLightParser.UseStmtContext ctx) { + if (ctx.identifier() != null) { + stmt.setUseDatabase(SQLUtils.unquoteIdentifier(ctx.identifier().getText())); + } + } + + @Override + public void enterStatementVerb(ClickHouseLightParser.StatementVerbContext ctx) { + String verb = normalizeVerb(ctx.getText()); + stmt.setStatementVerb(verb); + stmt.setHasResultSet(isStmtWithResultSetVerb(verb)); + } + + @Override + public void enterInsertFunctionStmt(ClickHouseLightParser.InsertFunctionStmtContext ctx) { + super.enterInsertFunctionStmt(ctx); + } + + @Override + public void enterInsertTableStmt(ClickHouseLightParser.InsertTableStmtContext ctx) { + ClickHouseLightParser.TableIdentifierContext tableIdentifier = ctx.tableIdentifier(); + if (tableIdentifier == null) { + return; + } + + List identifiers = tableIdentifier.identifier(); + if (identifiers.isEmpty()) { + return; + } + + if (identifiers.size() == 1) { + stmt.setTable(ClickHouseSqlUtils.unescape(identifiers.get(0).getText())); + } else { + stmt.setDatabase(identifiers.subList(0, identifiers.size() - 1).stream() + .map(id -> ClickHouseSqlUtils.unescape(id.getText())) + .collect(Collectors.joining("."))); + stmt.setTable(ClickHouseSqlUtils.unescape(identifiers.get(identifiers.size() - 1).getText())); + } + + ClickHouseLightParser.ColumnsClauseContext columnsClause = ctx.columnsClause(); + if (columnsClause != null) { + List columns = columnsClause.nestedIdentifier(); + String[] insertColumns = new String[columns.size()]; + for (int i = 0; i < columns.size(); i++) { + insertColumns[i] = columns.get(i).getText(); + } + stmt.setInsertColumns(insertColumns); + } + } + + @Override + public void enterDataClauseSelect(ClickHouseLightParser.DataClauseSelectContext ctx) { + stmt.setInsertWithSelect(true); + } + + @Override + public void enterDataClauseValues(ClickHouseLightParser.DataClauseValuesContext ctx) { + stmt.setAssignValuesGroups(ctx.assignmentValues().size()); + } + + @Override + public void enterAssignmentValues(ClickHouseLightParser.AssignmentValuesContext ctx) { + int currentStart = stmt.getAssignValuesListStartPosition(); + int currentStop = stmt.getAssignValuesListStopPosition(); + int start = ctx.getStart().getStartIndex(); + int stop = ctx.getStop().getStopIndex(); + + if (currentStart < 0 || start < currentStart) { + stmt.setAssignValuesListStartPosition(start); + } + if (currentStop < 0 || stop > currentStop) { + stmt.setAssignValuesListStopPosition(stop); + } + } + } + } + private static void parseParameters(String originalQuery, ParsedPreparedStatement stmt) { int len = originalQuery.length(); for (int i = 0; i < len; i++) { @@ -488,7 +723,14 @@ public enum SQLParser { * ANTLR4 used to determine sql type (SELECT, INSERT, etc.), extract some information. * Separate procedure parses sql for `?` parameter placeholders. */ - ANTLR4 + ANTLR4, + + /** + * Lightweight parser that extracts only required information from statement. + * There passes invalid statements to server. It is done to not block unknown statements from execution. + * + */ + ANTLR4_LIGHT, } public static SqlParserFacade getParser(String name, JdbcConfiguration jdbcConfiguration) throws SQLException { @@ -503,6 +745,8 @@ public static SqlParserFacade getParser(String name, JdbcConfiguration jdbcConfi return new ANTLR4AndParamsParser(saveRoles); case ANTLR4: return new ANTLR4Parser(saveRoles); + case ANTLR4_LIGHT: + return new ANTLR4LightParser(); } throw new SQLException("Unsupported parser: " + parserSelection); } catch (IllegalArgumentException e) { diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java index 0aabc98fd..080881efb 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java @@ -860,66 +860,6 @@ void testClearParameters() throws Exception { } } - @DataProvider - Object[][] testBatchInsertWithRowBinary_dp() { - return new Object[][]{ - {"INSERT INTO \n `%s` \nVALUES (?, ?, abs(?), ?)", PreparedStatementImpl.class}, // only string possible (because of abs(?)) - {"INSERT INTO\n `%s` \nVALUES (?, ?, ?, ?)", WriterStatementImpl.class}, // row binary writer - {" INSERT INTO %s (ts, v1, v2, v3) VALUES (?, ?, ?, ?)", WriterStatementImpl.class}, // only string supported now - {"INSERT INTO %s SELECT ?, ?, ?, ?", PreparedStatementImpl.class}, // only string possible (because of SELECT) - }; - } - - @Test(dataProvider = "testBatchInsertWithRowBinary_dp") - void testBatchInsertWithRowBinary(String sql, Class implClass) throws Exception { - String table = "test_batch"; - long seed = System.currentTimeMillis(); - Random rnd = new Random(seed); - System.out.println("testBatchInsert seed" + seed); - Properties properties = new Properties(); - properties.put(DriverProperties.BETA_ROW_BINARY_WRITER.getKey(), "true"); - try (Connection conn = getJdbcConnection(properties)) { - - try (Statement stmt = conn.createStatement()) { - stmt.execute("CREATE TABLE IF NOT EXISTS " + table + - " ( ts DateTime, v1 Int32, v2 Float32, v3 Int32) Engine MergeTree ORDER BY ()"); - } - - final int nBatches = 10; - try (PreparedStatement stmt = conn.prepareStatement(String.format(sql, table))) { - Assert.assertEquals(stmt.getClass(), implClass); - for (int bI = 0; bI < nBatches; bI++) { - stmt.setTimestamp(1, Timestamp.valueOf(LocalDateTime.now())); - stmt.setInt(2, rnd.nextInt()); - stmt.setFloat(3, rnd.nextFloat()); - stmt.setInt(4, rnd.nextInt()); - stmt.addBatch(); - } - - int[] result = stmt.executeBatch(); - for (int r : result) { - Assert.assertTrue(r == 1 || r == PreparedStatement.SUCCESS_NO_INFO); - } - } - - try (Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM " + table);) { - - int count = 0; - while (rs.next()) { - Timestamp ts = rs.getTimestamp(1); - assertNotNull(ts); - assertTrue(rs.getInt(2) != 0); - assertTrue(rs.getFloat(3) != 0.0f); - assertTrue(rs.getInt(4) != 0); - count++; - } - assertEquals(count, nBatches); - - stmt.execute("TRUNCATE " + table); - } - } - } @DataProvider Object[][] testBatchInsertTextStatement_dp() { diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/WriterStatementImplTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/WriterStatementImplTest.java index 2762c76c6..65e417c4d 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/WriterStatementImplTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/WriterStatementImplTest.java @@ -1,13 +1,26 @@ package com.clickhouse.jdbc; +import com.clickhouse.jdbc.internal.SqlParserFacade; import org.testng.Assert; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import java.sql.Connection; import java.sql.JDBCType; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.EnumSet; import java.util.Properties; +import java.util.Random; +import java.util.Set; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; @Test(groups = {"integration"}) public class WriterStatementImplTest extends JdbcIntegrationTest { @@ -28,4 +41,81 @@ public void testTargetTypeMethodThrowException() throws SQLException { Assert.expectThrows(SQLException.class, () -> stmt.setObject(1, "", JDBCType.DECIMAL, 3)); } } + + @DataProvider + Object[][] testBatchInsertWithRowBinary_dp() { + + Object[][] template = new Object[][]{ + {"INSERT INTO \n `%s` \nVALUES (?, ?, abs(?), ?)", PreparedStatementImpl.class, null}, // only string possible (because of abs(?)) + {"INSERT INTO\n `%s` \nVALUES (?, ?, ?, ?)", WriterStatementImpl.class, null}, // row binary writer + {" INSERT INTO %s (ts, v1, v2, v3) VALUES (?, ?, ?, ?)", WriterStatementImpl.class, null}, // only string supported now + {"INSERT INTO %s SELECT ?, ?, ?, ?", PreparedStatementImpl.class, null}, // only string possible (because of SELECT) + }; + + Set parsers = EnumSet.of(SqlParserFacade.SQLParser.ANTLR4_LIGHT, SqlParserFacade.SQLParser.JAVACC); + Object[][] dataset = new Object[template.length * parsers.size()][]; + + int i = 0; + for (SqlParserFacade.SQLParser p : parsers) { + for (Object[] t : template) { + Object[] test = new Object[t.length]; + System.arraycopy(t, 0, test, 0, t.length); + test[t.length - 1] = p; + dataset[i++] = test; + } + } + + return dataset; + } + + @Test(dataProvider = "testBatchInsertWithRowBinary_dp") + void testBatchInsertWithRowBinary(String sql, Class implClass, SqlParserFacade.SQLParser parser) throws Exception { + String table = "test_batch"; + long seed = System.currentTimeMillis(); + Random rnd = new Random(seed); + System.out.println("testBatchInsert seed" + seed); + Properties properties = new Properties(); + properties.put(DriverProperties.BETA_ROW_BINARY_WRITER.getKey(), "true"); + try (Connection conn = getJdbcConnection(properties)) { + + try (Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE IF NOT EXISTS " + table + + " ( ts DateTime, v1 Int32, v2 Float32, v3 Int32) Engine MergeTree ORDER BY ()"); + } + + final int nBatches = 10; + try (PreparedStatement stmt = conn.prepareStatement(String.format(sql, table))) { + Assert.assertEquals(stmt.getClass(), implClass); + for (int bI = 0; bI < nBatches; bI++) { + stmt.setTimestamp(1, Timestamp.valueOf(LocalDateTime.now())); + stmt.setInt(2, rnd.nextInt()); + stmt.setFloat(3, rnd.nextFloat()); + stmt.setInt(4, rnd.nextInt()); + stmt.addBatch(); + } + + int[] result = stmt.executeBatch(); + for (int r : result) { + Assert.assertTrue(r == 1 || r == PreparedStatement.SUCCESS_NO_INFO); + } + } + + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM " + table);) { + + int count = 0; + while (rs.next()) { + Timestamp ts = rs.getTimestamp(1); + assertNotNull(ts); + assertTrue(rs.getInt(2) != 0); + assertTrue(rs.getFloat(3) != 0.0f); + assertTrue(rs.getInt(4) != 0); + count++; + } + assertEquals(count, nBatches); + + stmt.execute("TRUNCATE " + table); + } + } + } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/Antlr4LightParserTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/Antlr4LightParserTest.java new file mode 100644 index 000000000..f9c7af499 --- /dev/null +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/Antlr4LightParserTest.java @@ -0,0 +1,336 @@ +package com.clickhouse.jdbc.internal; + +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.Properties; + +public class Antlr4LightParserTest extends BaseSqlParserFacadeTest { + public Antlr4LightParserTest() throws Exception { + super(SqlParserFacade.SQLParser.ANTLR4_LIGHT.name()); + } + + @Test + public void testOtherStatementSavesFirstVerb() throws Exception { + SqlParserFacade parser = SqlParserFacade.getParser(SqlParserFacade.SQLParser.ANTLR4_LIGHT.name(), + new JdbcConfiguration("jdbc:ch://localhost:8123/", new Properties())); + ParsedPreparedStatement stmt = parser.parsePreparedStatement("WITH cte AS (SELECT 1) SELECT * FROM cte"); + Assert.assertEquals(stmt.getStatementVerb(), "WITH"); + Assert.assertTrue(stmt.isHasResultSet()); + + stmt = parser.parsePreparedStatement("SHOW TABLES"); + Assert.assertEquals(stmt.getStatementVerb(), "SHOW"); + Assert.assertTrue(stmt.isHasResultSet()); + } + + @Test + public void testUseAndInsertAreParsed() throws Exception { + SqlParserFacade parser = SqlParserFacade.getParser(SqlParserFacade.SQLParser.ANTLR4_LIGHT.name(), + new JdbcConfiguration("jdbc:ch://localhost:8123/", new Properties())); + + ParsedStatement useStmt = parser.parsedStatement("USE analytics"); + Assert.assertEquals(useStmt.getStatementVerb(), "USE"); + Assert.assertEquals(useStmt.getUseDatabase(), "analytics"); + + ParsedPreparedStatement insertStmt = parser.parsePreparedStatement("INSERT INTO db1.t1 (id, name) VALUES (?, ?)"); + Assert.assertEquals(insertStmt.getStatementVerb(), "INSERT"); + Assert.assertTrue(insertStmt.isInsert()); + Assert.assertEquals(insertStmt.getTable(), "db1.t1"); + Assert.assertEquals(insertStmt.getInsertColumns().length, 2); + } + + @Test + public void testMultiDotNotation() { + SqlParserFacade parser = lightParser(); + + // Test INSERT with multi-dot notation + String sql3 = "INSERT INTO a.b.c (col1, col2) VALUES (?, ?)"; + ParsedPreparedStatement stmt3 = parser.parsePreparedStatement(sql3); + Assert.assertEquals(stmt3.getArgCount(), 2); + Assert.assertFalse(stmt3.isHasErrors()); + Assert.assertTrue(stmt3.isInsert()); + Assert.assertEquals(stmt3.getTable(), "a.b.c"); + } + + @Test + public void testQuotedIdentifiersWithDots() { + SqlParserFacade parser = lightParser(); + /* + * Comprehensive test for quoted identifiers containing dots. + * These cases are all valid in ClickHouse with MySQL-style backtick quoting. + */ + + // Case 1: Unquoted database + unquoted table + testParsedTableName(parser, "INSERT INTO db.table (id) VALUES (?)", "db.table"); + + // Case 2: Quoted database + quoted table + testParsedTableName(parser, "INSERT INTO `db`.`table` (id) VALUES (?)", "db.table"); + + // Case 3: Dots inside quoted table name + testParsedTableName(parser, "INSERT INTO db.`table.name` (id) VALUES (?)", "db.table.name"); + + // Case 4: Dots inside quoted database name + testParsedTableName(parser, "INSERT INTO `db.part1`.`table` (id) VALUES (?)", "db.part1.table"); + + // Case 5: Mixed quoted/unquoted identifiers + testParsedTableName(parser, "INSERT INTO db.`table.name` (id) VALUES (?)", "db.table.name"); + + // Case 6: Mixed quoted/unquoted (reverse) + testParsedTableName(parser, "INSERT INTO `db.part1`.table (id) VALUES (?)", "db.part1.table"); + + // Case 7: Escaped backticks inside quoted identifier + testParsedTableName(parser, "INSERT INTO db.`tab``le` (id) VALUES (?)", "db.tab`le"); + + // Case 8: Weird characters inside quoted identifiers (spaces, symbols) + testParsedTableName(parser, "INSERT INTO `my db`.`table name!@#` (id) VALUES (?)", "my db.table name!@#"); + + // Case 9: Quoted database and table with dots + testParsedTableName(parser, "INSERT INTO `db.part1`.`table.name` (id) VALUES (?)", "db.part1.table.name"); + + // Case 10: Quoted table name containing multiple dots + testParsedTableName(parser, "INSERT INTO db.`a.b.c.d` (id) VALUES (?)", "db.a.b.c.d"); + + // Case 11: Quoted database name containing multiple dots + testParsedTableName(parser, "INSERT INTO `db.part1.part2`.`table` (id) VALUES (?)", "db.part1.part2.table"); + + // Case 12: Multi-part unquoted chain (3-part identifier) + testParsedTableName(parser, "INSERT INTO db.part1.table2 (id) VALUES (?)", "db.part1.table2"); + + // Case 13: Multi-part quoted chain + testParsedTableName(parser, "INSERT INTO `db.part1`.`part2`.`table` (id) VALUES (?)", "db.part1.part2.table"); + + // Case 14: Mixed multi-part unquoted + quoted + testParsedTableName(parser, "INSERT INTO db.part1.`table.name` (id) VALUES (?)", "db.part1.table.name"); + + // Case 15: Mixed multi-part quoted + unquoted + testParsedTableName(parser, "INSERT INTO `db.part1`.part2.table3 (id) VALUES (?)", "db.part1.part2.table3"); + } + + private void testParsedTableName(SqlParserFacade parser, String sql, String expectedTableName) { + ParsedPreparedStatement stmt = parser.parsePreparedStatement(sql); + Assert.assertFalse(stmt.isHasErrors(), "Query should parse without errors: " + sql); + Assert.assertEquals(stmt.getTable(), expectedTableName, "Table name mismatch for: " + sql); + } + + private SqlParserFacade lightParser() { + try { + return SqlParserFacade.getParser(SqlParserFacade.SQLParser.ANTLR4_LIGHT.name(), + new JdbcConfiguration("jdbc:ch://localhost:8123/", new Properties())); + } catch (Exception e) { + throw new RuntimeException("Unable to create ANTLR4 light parser", e); + } + } + + + @DataProvider + public static Object[][] testStatementWithoutResultSetDP() { + return new Object[][]{ + /* has result set */ + {"SELECT * FROM test_table", 0, true}, + {"SELECT 1 table WHERE 1 = ?", 1, true}, + {"SHOW CREATE TABLE `db`.`test_table`", 0, true}, + {"SHOW CREATE TEMPORARY TABLE `db1`.`tmp_table`", 0, true}, + {"SHOW CREATE DICTIONARY dict1", 0, true}, + {"SHOW CREATE VIEW view1", 0, true}, + {"SHOW CREATE DATABASE db1", 0, true}, + {"SHOW CREATE TABLE table1 INTO OUTFILE 'table1.sql'", 0, true}, + {"SHOW TABLES ", 0, true}, + {"SHOW TABLES FROM system LIKE '%user%'", 0, true}, + {"SHOW COLUMNS FROM `orders` LIKE 'delivery_%'", 0, true}, + {"SHOW DICTIONARIES FROM db LIKE '%reg%' LIMIT 2", 0, true}, + {"SHOW INDEX FROM `tbl`", 0, true}, + {"SHOW PROCESSLIST", 0, true}, + {"SHOW GRANTS FOR `user01`", 0, true}, + {"SHOW GRANTS FOR `user01` FINAL", 0, true}, + {"SHOW GRANTS FOR `user01` WITH IMPLICIT FINAL", 0, true}, + {"SHOW CREATE USER `user01`", 0, true}, + {"SHOW CREATE USER CURRENT_USER", 0, true}, + {"SHOW CREATE ROLE `role_01`", 0, true}, + {"SHOW CREATE POLICY policy_1 ON `tableA`, `db1`.`tableB`", 0, true}, + {"SHOW CREATE ROW POLICY policy_1 ON `tableA`, `db1`.`tableB`", 0, true}, + {"SHOW CREATE QUOTA CURRENT", 0, true}, + {"SHOW CREATE QUOTA `q1`", 0, true}, + {"SHOW CREATE PROFILE `p1`", 0, true}, + {"SHOW CREATE SETTINGS PROFILE `p3`", 0, true}, + {"SHOW USERS", 0, true}, + {"SHOW CURRENT ROLES", 0, true}, + {"SHOW ENABLED ROLES", 0, true}, + {"SHOW SETTINGS PROFILES", 0, true}, + {"SHOW PROFILES", 0, true}, + {"SHOW POLICIES ON `db`.`table`", 0, true}, + {"SHOW ROW POLICIES ON table1", 0, true}, + {"SHOW QUOTAS", 0, true}, + {"SHOW CURRENT QUOTA", 0, true}, + {"SHOW QUOTA", 0, true}, + {"SHOW ACCESS", 0, true}, + {"SHOW CLUSTER `default`", 0, true}, + {"SHOW CLUSTERS LIKE 'test%' LIMIT 1", 0, true}, + {"SHOW SETTINGS LIKE 'send_timeout'", 0, true}, + {"SHOW SETTINGS ILIKE '%CONNECT_timeout%'", 0, true}, + {"SHOW CHANGED SETTINGS ILIKE '%MEMORY%'", 0, true}, + {"SHOW SETTING `min_insert_block_size_rows`", 0, true}, + {"SHOW FILESYSTEM CACHES", 0, true}, + {"SHOW ENGINES", 0, true}, + {"SHOW FUNCTIONS", 0, true}, + {"SHOW FUNCTIONS LIKE '%max%'", 0, true}, + {"SHOW MERGES", 0, true}, + {"SHOW MERGES LIKE 'your_t%' LIMIT 1", 0, true}, + {"EXPLAIN SELECT sum(number) FROM numbers(10) GROUP BY number", 0, true}, + {"EXPLAIN SELECT 1", 0, true}, + {"EXPLAIN SELECT sum(number) FROM numbers(10) UNION ALL SELECT sum(number) FROM numbers(10) ORDER BY sum(number) ASC FORMAT TSV", 0, true}, + {"DESCRIBE TABLE table", 0, true}, + {"DESC TABLE table1", 0, true}, + {"EXISTS TABLE `db`.`table01`", 0, true}, + {"CHECK GRANT SELECT(col2) ON table_2", 0, true}, + {"CHECK TABLE test_table", 0, true}, + {"CHECK TABLE t0 PARTITION ID '201003' FORMAT PrettyCompactMonoBlock SETTINGS check_query_single_value_result = 0", 0, true}, + + + /* parser fallback because SYSTEM keyword can be used with result set queries */ + {"SYSTEM START FETCHES", 0, true}, + {"SYSTEM RELOAD DICTIONARIES", 0, true}, + {"SYSTEM RELOAD DICTIONARIES ON CLUSTER `default`", 0, true}, + + /* no result set */ + {"INSERT INTO test_table VALUES (1, ?)", 1, false}, + {"CREATE DATABASE `test_db`", 0, false}, + {"CREATE DATABASE `test_db` COMMENT 'for tests'", 0, false}, + {"CREATE DATABASE IF NOT EXISTS `test_db`", 0, false}, + {"CREATE DATABASE IF NOT EXISTS `test_db` ON CLUSTER `cluster`", 0, false}, + {"CREATE DATABASE IF NOT EXISTS `test_db` ON CLUSTER `cluster` ENGINE = Replicated('clickhouse1:9000', 'test_db')", 0, false}, + {"CREATE TABLE `test_table` (id UInt64)", 0, false}, + {"CREATE TABLE IF NOT EXISTS `test_table` (id UInt64)", 0, false}, + {"CREATE TABLE `test_table` (id UInt64) ENGINE = MergeTree ORDER BY (id)", 0, false}, + {"CREATE TABLE `test_table` (id UInt64) ENGINE = Memory", 0, false}, + {"CREATE TABLE `test_table` (id UInt64 NOT NULL ) ENGINE = MergeTree ORDER BY id", 0, false}, + {"CREATE TABLE `test_table` (id UInt64 NULL ) ENGINE = MergeTree() ORDER BY id", 0, false}, + {"CREATE TABLE `test_table` (id UInt64) ENGINE = MergeTree() ORDER BY id ON CLUSTER `cluster`", 0, false}, + {"CREATE TABLE `test_table` (id UInt64) ENGINE = MergeTree() ORDER BY id ON CLUSTER `cluster` ENGINE = Replicated('clickhouse1:9000', 'test_db')", 0, false}, + {"CREATE TABLE `test_table` (id UInt64) ENGINE = MergeTree() ORDER BY id ON CLUSTER `cluster` ENGINE = Replicated('clickhouse1:9000', 'test_db') COMMENT 'for tests'", 0, false}, + {"CREATE TABLE myusers ( id UInt64, ip String, url String, tenant String) ENGINE = MergeTree() PRIMARY KEY (id)", 0, false}, + {"CREATE VIEW `test_db`.`source_table` source AS ( SELECT * FROM source_a UNION SELECT * FROM source_b)", 0, false}, + {"CREATE OR REPLACE VIEW `test_db`.`source_table` source AS ( SELECT * FROM source_a UNION SELECT * FROM source_b)", 0, false}, + {"CREATE OR REPLACE VIEW `test_db`.`source_table` source ON CLUSTER `cluster` AS ( SELECT * FROM source_a UNION SELECT * FROM source_b)", 0, false}, + {"CREATE VIEW `test_db`.`source_table` source AS ( SELECT * FROM source_a UNION SELECT * FROM source_b) ENGINE = MaterializedView", 0, false}, + {"CREATE VIEW `test_db`.`source_table` source AS ( SELECT * FROM source_a UNION SELECT * FROM source_b) ENGINE = MaterializedView()", 0, false}, + {"CREATE VIEW `test_db`.`source_table` source AS ( SELECT * FROM source_a UNION SELECT * FROM source_b) ENGINE = MaterializedView() COMMENT 'for tests'", 0, false}, + {"CREATE DICTIONARY `test_db`.dict1 (k1 UInt64 EXPRESSION(k1 + 1), k2 String DEFAULT 'default', a1 Array(UInt64) DEFAULT []) PRIMARY KEY k1 SOURCE(CLICKHOUSE(db='test_db', table='dict1')) LAYOUT(FLAT()) LIFETIME(MIN 1000 MAX 2000)", 0, false}, + {"CREATE DICTIONARY `test_db`.dict1 (k1 UInt64 (k1 + 1), k2 String DEFAULT 'default', a1 Array(UInt64) DEFAULT []) PRIMARY KEY k1 SOURCE(CLICKHOUSE(db='test_db', table='dict1')) LAYOUT(FLAT()) LIFETIME(MIN 1000 MAX 2000) SETTINGS(cache_size = 1000) COMMENT 'for tests'", 0, false}, + {"CREATE OR REPLACE DICTIONARY IF NOT EXISTS `test_db`.dict1 (k1 UInt64 (k1 + 1), k2 String DEFAULT 'default', a1 Array(UInt64) DEFAULT []) PRIMARY KEY k1 SOURCE(CLICKHOUSE(db='test_db', table='dict1')) LAYOUT(FLAT()) LIFETIME(MIN 1000 MAX 2000) SETTINGS(cache_size = 1000, v='123') COMMENT 'for tests'", 0, false}, + {"CREATE FUNCTION test_func AS () -> 10", 0, false}, + {"CREATE FUNCTION test_func AS (x) -> 10 * x", 0, false}, + {"CREATE FUNCTION test_func AS (x, y) -> y * x", 0, false}, + {"CREATE FUNCTION test_func ON CLUSTER `cluster` AS (x, y) -> y * x", 0, false}, + {"CREATE USER IF NOT EXISTS `user`", 0, false}, + {"CREATE USER IF NOT EXISTS `user` ON CLUSTER `cluster`", 0, false}, + {"CREATE ROLE IF NOT EXISTS `role1` ON CLUSTER 'cluster'", 0, false}, + {"CREATE ROW POLICY pol1 ON mydb.table1 USING b=1 TO mira, peter", 0, false}, + {"CREATE ROW POLICY pol2 ON mydb.table1 USING c=2 TO peter, antonio", 0, false}, + {"CREATE ROW POLICY pol2 ON mydb.table1 USING c=2 AS RESTRICTIVE TO peter, antonio", 0, false}, + {"CREATE QUOTA qA FOR INTERVAL 15 month MAX queries = 123 TO role1, role2", 0, false}, + {"CREATE QUOTA qA FOR INTERVAL 15 month MAX queries = 123 TO ALL EXCEPT role3", 0, false}, + {"CREATE QUOTA qA FOR INTERVAL 15 month MAX queries = 123 TO CURRENT_USER", 0, false}, + {"CREATE QUOTA qB FOR INTERVAL 30 minute MAX execution_time = 0.5, FOR INTERVAL 5 quarter MAX queries = 321, errors = 10 TO default", 0, false}, + {"CREATE SETTINGS PROFILE max_memory_usage_profile SETTINGS max_memory_usage = 100000001 MIN 90000000 MAX 110000000 TO robin", 0, false}, + {"CREATE NAMED COLLECTION foobar AS a = '1', b = '2' OVERRIDABLE", 0, false}, + {"CREATE TABLE IF NOT EXISTS statistics.vast_event_v2 (ip UInt32, url String) ENGINE = ReplacingMergeTree()", 0, false}, + {"CREATE TABLE IF NOT EXISTS `statistics`.vast_event_v2 (ip UInt32, url String) ENGINE = ReplacingMergeTree()", 0, false}, + {"alter table t2 alter column v type Int32", 0, false}, + {"alter table t alter column j default 1", 0, false}, + {"ALTER TABLE t MODIFY COLUMN j default 1", 0, false}, + {"ALTER TABLE t MODIFY COMMENT 'comment'", 0, false}, + {"ALTER TABLE t ADD COLUMN id Int32 AFTER v", 0, false}, + {"ALTER TABLE t ADD COLUMN id Int32 FIRST", 0, false}, + {"DELETE FROM db.table1 ON CLUSTER `default` WHERE max(a, 10) > ?", 1, false}, + {"DELETE FROM table WHERE a = ?", 1, false}, + {"DELETE FROM table WHERE a = ? AND b = ?", 2, false}, + {"DELETE FROM hits WHERE Title LIKE '%hello%';", 0, false}, + {"DELETE FROM t WHERE true", 0, false}, + + {"GRANT SELECT ON db.* TO john", 0, false}, + {"GRANT ON CLUSTER `default` SELECT(a, b) ON db1.tableA TO `user` WITH GRANT OPTION WITH REPLACE OPTION", 0, false}, + {"GRANT SELECT ON db.* TO user01 WITH REPLACE OPTION", 0, false}, + {"GRANT ON CLUSTER `default` role1, role2 TO `user01` WITH ADMIN OPTION WITH REPLACE OPTION", 0, false}, + {"GRANT role1, role2 TO `user01` WITH ADMIN OPTION WITH REPLACE OPTION", 0, false}, + {"GRANT CURRENT GRANTS TO user01", 0, false}, + {"REVOKE SELECT(a,b) ON db1.tableA FROM `user01`", 0, false}, + {"REVOKE SELECT ON db1.* FROM ALL", 0, false}, + {"REVOKE SELECT ON db1.* FROM ALL EXCEPT `admin01`", 0, false}, + {"REVOKE SELECT ON db1.* FROM ALL EXCEPT CURRENT USER", 0, false}, + {"REVOKE ON CLUSTER `default` SELECT ON db1.* FROM ALL EXCEPT CURRENT USER", 0, false}, + {"REVOKE ON CLUSTER `blaster` ADMIN OPTION FOR role1, role3 FROM `user01`", 0, false}, + {"REVOKE ON CLUSTER `blaster` role1, role3 FROM ALL EXCEPT CURRENT USER", 0, false}, + {"REVOKE ON CLUSTER `blaster` role1, role3 FROM ALL EXCEPT `very_nice_user`", 0, false}, + {"UPDATE db.table01 ON CLUSTER `default` SET col1 = ?, col2 = ? WHERE col3 > ?", 3, false}, + {"UPDATE hits SET Title = 'Updated Title' WHERE EventDate = today()", 0, false}, + {"ATTACH TABLE test FROM '01188_attach/test' (s String, n UInt8) ENGINE = File(TSV)", 0, false}, + {"ATTACH TABLE test AS REPLICATED", 0, false}, + {"DETACH TABLE test", 0, false}, + {"ATTACH DICTIONARY IF NOT EXISTS db.dict1 ON CLUSTER `default`", 0, false}, + {"ATTACH DATABASE IF NOT EXISTS db1 ENGINE=MergeTree ON CLUSTER `default`", 0, false}, + {"DROP DATABASE `db1`", 0, false}, + {"DROP TABLE `db1`.`table01`", 0, false}, + {"DROP DICTIONARY `dict1`", 0, false}, + {"DROP ROLE IF EXISTS `role01`", 0, false}, + {"DROP POLICY IF EXISTS `pol1` ON db1.table1 ON CLUSTER `default` FROM `test`", 0, false}, + {"DROP POLICY IF EXISTS `pol1` ON db1.table1 ON CLUSTER `default`", 0, false}, + {"DROP POLICY IF EXISTS `pol1` ON table1", 0, false}, + {"DROP QUOTA IF EXISTS q1", 0, false}, + {"DROP SETTINGS PROFILE IF EXISTS `profile1` ON CLUSTER `default`", 0, false}, + {"DROP VIEW view1 ON CLUSTER `default` SYNC", 0, false}, + {"DROP FUNCTION linear_equation", 0, false}, + {"DROP NAMED COLLECTION foobar", 0, false}, + {"KILL QUERY WHERE query_id='2-857d-4a57-9ee0-327da5d60a90'", 0, false}, + {"KILL QUERY WHERE user='username' SYNC", 0, false}, + {"KILL QUERY ON CLUSTER `default` WHERE user='username' SYNC", 0, false}, + {"KILL QUERY ON CLUSTER `default` WHERE user='username' ASYNC", 0, false}, + {"KILL QUERY ON CLUSTER `default` WHERE user='username' TEST", 0, false}, + {"KILL MUTATION WHERE database = 'default' AND table = 'table'", 0, false}, + {"KILL MUTATION WHERE database = 'default' AND table = 'table' AND mutation_id = 'mutation_3.txt'", 0, false}, + {"OPTIMIZE TABLE table DEDUPLICATE BY colX,colY,colZ", 0, false}, + {"OPTIMIZE TABLE table DEDUPLICATE BY * EXCEPT colX", 0, false}, + {"OPTIMIZE TABLE table DEDUPLICATE BY * EXCEPT (colX, colY)", 0, false}, + {"OPTIMIZE TABLE table DEDUPLICATE BY COLUMNS('column-matched-by-regex')", 0, false}, + {"OPTIMIZE TABLE table DEDUPLICATE BY COLUMNS('column-matched-by-regex') EXCEPT colX", 0, false}, + {"OPTIMIZE TABLE table DEDUPLICATE BY COLUMNS('column-matched-by-regex') EXCEPT (colX, colY)", 0, false}, + {"OPTIMIZE TABLE table DEDUPLICATE", 0, false}, + {"OPTIMIZE TABLE table DEDUPLICATE BY *", 0, false}, + {"RENAME TABLE table_A TO table_A_bak, table_B TO table_B_bak", 0, false}, + {"RENAME TABLE table_A TO table_A_bak, table_B TO table_B_bak ON CLUSTER `default`", 0, false}, + {"RENAME DICTIONARY dictA TO dictB ON CLUSTER `default`", 0, false}, + {"EXCHANGE TABLES table1 AND table2", 0, false}, + {"EXCHANGE TABLES table1 AND table2 ON CLUSTER `default`", 0, false}, + {"EXCHANGE DICTIONARIES dict1 AND dict2", 0, false}, + {"EXCHANGE DICTIONARIES dict1 AND dict2 ON CLUSTER `default`", 0, false}, + {"SET profile = 'profile-name-from-the-settings-file'", 0, false}, + {"SET setting_1 = 'some value'", 0, false}, + {"SET use_some_feature_flag", 0, false}, + {"SET use_some_feature_flag = 'true'", 0, false}, + {"SET ROLE role1", 0, false}, + {"SET DEFAULT ROLE role1 TO user", 0, false}, + {"SET DEFAULT ROLE NONE TO user", 0, false}, + {"SET DEFAULT ROLE ALL EXCEPT role1, role2 TO user", 0, false}, + {"TRUNCATE TABLE IF EXISTS `db1`.`table1` ON CLUSTER `default` SYNC", 0, false}, + {"TRUNCATE TABLE `db1`.`table1` ON CLUSTER `default` SYNC", 0, false}, + {"TRUNCATE TABLE `db1`.`table1` ON CLUSTER `default`", 0, false}, + {"TRUNCATE TABLE `db1`.`table1`", 0, false}, + {"TRUNCATE TEMPORARY TABLE t", 0, false}, + {"TRUNCATE DATABASE IF EXISTS db ON CLUSTER `cluster`", 0, false}, + {"TRUNCATE DATABASE IF EXISTS db", 0, false}, + {"TRUNCATE DATABASE `db`", 0, false}, + {"TRUNCATE ALL TABLES FROM IF EXISTS `db` NOT LIKE 'tmp%' ON CLUSTER `cluster`", 0, false}, + {"TRUNCATE ALL TABLES FROM IF EXISTS `db` NOT LIKE 'tmp%'", 0, false}, + {"TRUNCATE ALL TABLES FROM `db` NOT LIKE 'tmp%' ON CLUSTER `cluster`", 0, false}, + {"TRUNCATE TABLES FROM `db` LIKE 'tmp%' ON CLUSTER `cluster`", 0, false}, + {"TRUNCATE TABLES FROM `db` LIKE 'tmp%'", 0, false}, + {"USE test_db", 0, false}, + {"MOVE USER test TO local_directory", 0, false}, + {"MOVE ROLE test TO memory", 0, false}, + {"UNDROP TABLE tab", 0, false}, + {"UNDROP TABLE db.tab ON CLUSTER `default`", 0, false}, + {"UNDROP TABLE db.tab UUID '857d-4a57-9ee0-327da5d60a90' ON CLUSTER `default`", 0, false}, + + }; + } +} diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/BaseSqlParserFacadeTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/BaseSqlParserFacadeTest.java index cdb26a519..541cabcc9 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/BaseSqlParserFacadeTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/BaseSqlParserFacadeTest.java @@ -21,7 +21,7 @@ public abstract class BaseSqlParserFacadeTest { - private SqlParserFacade parser; + protected SqlParserFacade parser; public BaseSqlParserFacadeTest(String name) throws Exception { parser = SqlParserFacade.getParser(name, new JdbcConfiguration("jdbc:ch:http://localhost:8123", new Properties())); @@ -203,52 +203,52 @@ public void testQuotedIdentifiersWithDots() { */ // Case 1: Unquoted database + unquoted table - testCase("SELECT * FROM db.table WHERE id = ?", "db.table"); + testParsedTableName("SELECT * FROM db.table WHERE id = ?", "db.table"); // Case 2: Quoted database + quoted table - testCase("SELECT * FROM `db`.`table` WHERE id = ?", "db.table"); + testParsedTableName("SELECT * FROM `db`.`table` WHERE id = ?", "db.table"); // Case 3: Dots inside quoted table name - testCase("SELECT * FROM db.`table.name` WHERE id = ?", "db.table.name"); + testParsedTableName("SELECT * FROM db.`table.name` WHERE id = ?", "db.table.name"); // Case 4: Dots inside quoted database name - testCase("SELECT * FROM `db.part1`.`table` WHERE id = ?", "db.part1.table"); + testParsedTableName("SELECT * FROM `db.part1`.`table` WHERE id = ?", "db.part1.table"); // Case 5: Mixed quoted/unquoted identifiers - testCase("SELECT * FROM db.`table.name` WHERE id = ?", "db.table.name"); + testParsedTableName("SELECT * FROM db.`table.name` WHERE id = ?", "db.table.name"); // Case 6: Mixed quoted/unquoted (reverse) - testCase("SELECT * FROM `db.part1`.table WHERE id = ?", "db.part1.table"); + testParsedTableName("SELECT * FROM `db.part1`.table WHERE id = ?", "db.part1.table"); // Case 7: Escaped backticks inside quoted identifier - testCase("SELECT * FROM db.`tab``le` WHERE id = ?", "db.tab`le"); + testParsedTableName("SELECT * FROM db.`tab``le` WHERE id = ?", "db.tab`le"); // Case 8: Weird characters inside quoted identifiers (spaces, symbols) - testCase("SELECT * FROM `my db`.`table name!@#` WHERE id = ?", "my db.table name!@#"); + testParsedTableName("SELECT * FROM `my db`.`table name!@#` WHERE id = ?", "my db.table name!@#"); // Case 9: Alias on table identifier - testCase("SELECT * FROM `db.part1`.`table.name` AS t WHERE id = ?", "db.part1.table.name"); + testParsedTableName("SELECT * FROM `db.part1`.`table.name` AS t WHERE id = ?", "db.part1.table.name"); // Case 10: Quoted table name containing multiple dots - testCase("SELECT * FROM db.`a.b.c.d` WHERE id = ?", "db.a.b.c.d"); + testParsedTableName("SELECT * FROM db.`a.b.c.d` WHERE id = ?", "db.a.b.c.d"); // Case 11: Quoted database name containing multiple dots - testCase("SELECT * FROM `db.part1.part2`.`table` WHERE id = ?", "db.part1.part2.table"); + testParsedTableName("SELECT * FROM `db.part1.part2`.`table` WHERE id = ?", "db.part1.part2.table"); // Case 12: Multi-part unquoted chain (3-part identifier) - testCase("SELECT * FROM db.part1.table2 WHERE id = ?", "db.part1.table2"); + testParsedTableName("SELECT * FROM db.part1.table2 WHERE id = ?", "db.part1.table2"); // Case 13: Multi-part quoted chain - testCase("SELECT * FROM `db.part1`.`part2`.`table` WHERE id = ?", "db.part1.part2.table"); + testParsedTableName("SELECT * FROM `db.part1`.`part2`.`table` WHERE id = ?", "db.part1.part2.table"); // Case 14: Mixed multi-part unquoted + quoted - testCase("SELECT * FROM db.part1.`table.name` WHERE id = ?", "db.part1.table.name"); + testParsedTableName("SELECT * FROM db.part1.`table.name` WHERE id = ?", "db.part1.table.name"); // Case 15: Mixed multi-part quoted + unquoted - testCase("SELECT * FROM `db.part1`.part2.table3 WHERE id = ?", "db.part1.part2.table3"); + testParsedTableName("SELECT * FROM `db.part1`.part2.table3 WHERE id = ?", "db.part1.part2.table3"); } - private void testCase(String sql, String expectedTableName) { + protected void testParsedTableName(String sql, String expectedTableName) { ParsedPreparedStatement stmt = parser.parsePreparedStatement(sql); Assert.assertFalse(stmt.isHasErrors(), "Query should parse without errors: " + sql); Assert.assertEquals(stmt.getTable(), expectedTableName, "Table name mismatch for: " + sql);