diff --git a/.gitignore b/.gitignore index e6c43d215..2cb9e0dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ dependency-reduced-pom.xml java.prof jmh-result.* profile.html +jdbc-v2/gen # Shell scripts *.sh diff --git a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java index 9140b6739..b1eb68607 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/query/QuerySettings.java @@ -42,6 +42,11 @@ public QuerySettings setOption(String option, Object value) { return this; } + public QuerySettings resetOption(String option) { + rawSettings.remove(option); + return this; + } + /** * Gets a configuration option. * diff --git a/jdbc-v2/pom.xml b/jdbc-v2/pom.xml index 307612d6a..b1c1d4ccc 100644 --- a/jdbc-v2/pom.xml +++ b/jdbc-v2/pom.xml @@ -24,6 +24,12 @@ + + org.antlr + antlr4-runtime + 4.9.3 + + ${project.parent.groupId} client-v2 @@ -391,6 +397,20 @@ + + + org.antlr + antlr4-maven-plugin + 4.9.3 + + + generate-sources + + antlr4 + + + + \ No newline at end of file diff --git a/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 new file mode 100644 index 000000000..ac3bbbea6 --- /dev/null +++ b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseLexer.g4 @@ -0,0 +1,300 @@ + +// $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 + +ADD : A D D; +AFTER : A F T E R; +ALIAS : A L I A S; +ALL : A L L; +ALTER : A L T E R; +AND : A N D; +ANTI : A N T I; +ANY : A N 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; +ATTACH : A T T A C H; +BETWEEN : B E T W E E N; +BOTH : B O T H; +BY : B Y; +CASE : C A S E; +CAST : C A S T; +CHECK : C H E C K; +CLEAR : C L E A R; +CLUSTER : C L U S T E R; +CODEC : C O D E C; +COLLATE : C O L L A T E; +COLUMN : C O L U M N; +COMMENT : C O M M E N 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; +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; +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; +DROP : D R O P; +ELSE : E L S E; +END : E N D; +ENGINE : E N G I N E; +EVENTS : E V E N T S; +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; +EXCEPT : E X C E P T; +EXTRACT : E X T R A C T; +FETCHES : F E T C H E S; +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; +GLOBAL : G L O B A L; +GRANULARITY : G R A N U L A R I T Y; +GROUP : G R O U P; +HAVING : H A V I N G; +HIERARCHICAL : H I E R A R C H I C A L; +HOUR : H O U R; +ID : I D; +IF : I F; +ILIKE : I L I K E; +IN : I N; +INDEX : I N D E X; +INF : I N F | I N F I N I T Y; +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; +IS : I S; +IS_OBJECT_ID : I S UNDERSCORE O B J E C T UNDERSCORE I D; +JOIN : J O I N; +KEY : K E Y; +KILL : K I L L; +LAST : L A S T; +LAYOUT : L A Y O U T; +LEADING : L E A D I N G; +LEFT : L E F T; +LIFETIME : L I F E T I M E; +LIKE : L I K E; +LIMIT : L I M I T; +LIVE : L I V E; +LOCAL : L O C A L; +LOGS : L O G S; +MATERIALIZE : M A T E R I A L I Z E; +MATERIALIZED : M A T E R I A L I Z E D; +MAX : M A X; +MERGES : M E R G E S; +MIN : M I N; +MINUTE : M I N U T E; +MODIFY : M O D I F Y; +MONTH : M O N T H; +MOVE : M O V E; +MUTATION : M U T A T I O N; +NAN_SQL : N A N; // conflicts with macro NAN +NO : N O; +NONE : N O N E; +NOT : N O T; +NULL_SQL : N U L L; // conflicts with macro NULL +NULLS : N U L L S; +OFFSET : O F F S E T; +ON : O N; +OPTIMIZE : O P T I M I Z E; +OR : O R; +ORDER : O R D E R; +OUTER : O U T E R; +OUTFILE : O U T F I L E; +OVER : O V E R; +PARTITION : P A R T I T I O N; +POPULATE : P O P U L A T E; +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; +PROJECTION : P R O J E C T I O N; +QUARTER : Q U A R T E R; +RANGE : R A N G E; +RELOAD : R E L O A D; +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; +REPLICATED : R E P L I C A T E D; +RIGHT : R I G H T; +ROLE : R O L E; +ROLLUP : R O L L U P; +ROW : R O W; +ROWS : R O W S; +SAMPLE : S A M P L E; +SECOND : S E C O N D; +SELECT : S E L E C T; +SEMI : S E M I; +SENDS : S E N D S; +SET : S E T; +SETTINGS : S E T T I N G S; +SHOW : S H O W; +SOURCE : S O U R C E; +START : S T A R T; +STOP : S T O P; +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; +TABLE : T A B L E; +TABLES : T A B L E S; +TEMPORARY : T E M P O R A R Y; +TEST : T E S T; +THEN : T H E N; +TIES : T I E S; +TIMEOUT : T I M E O U T; +TIMESTAMP : T I M E S T A M P; +TO : T O; +TOP : T O P; +TOTALS : T O T A L S; +TRAILING : T R A I L I N G; +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; +UNION : U N I O N; +UPDATE : U P D A T E; +USE : U S E; +USING : U S I N G; +UUID : U U I D; +VALUES : V A L U E S; +VIEW : V I E W; +VOLUME : V O L U M E; +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; +YEAR : Y E A R | Y Y Y Y; + +JSON_FALSE : 'false'; +JSON_TRUE : 'true'; + +// Tokens + +IDENTIFIER: (LETTER | UNDERSCORE) (LETTER | UNDERSCORE | DEC_DIGIT)* + | BACKQUOTE ( ~([\\`]) | (BACKSLASH .) | (BACKQUOTE BACKQUOTE))* BACKQUOTE + | QUOTE_DOUBLE (~([\\"]) | (BACKSLASH .) | (QUOTE_DOUBLE QUOTE_DOUBLE))* QUOTE_DOUBLE +; +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+; + +// 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 : '+'; +QUERY : '?'; +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/ClickHouseParser.g4 b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 new file mode 100644 index 000000000..1dc7d6509 --- /dev/null +++ b/jdbc-v2/src/main/antlr4/com/clickhouse/jdbc/internal/ClickHouseParser.g4 @@ -0,0 +1,952 @@ + +// $antlr-format alignColons hanging, alignSemicolons hanging, alignTrailingComments true, allowShortBlocksOnASingleLine true +// $antlr-format allowShortRulesOnASingleLine false, columnLimit 150, maxEmptyLinesToKeep 1, minEmptyLines 1, reflowComments false, useTab false + +parser grammar ClickHouseParser; + +options { + tokenVocab = ClickHouseLexer; +} + +// Top-level statements + +queryStmt + : query (INTO OUTFILE STRING_LITERAL)? (FORMAT identifierOrNull)? (SEMICOLON)? + | insertStmt + ; + +query + : alterStmt // DDL + | attachStmt // DDL + | checkStmt + | createStmt // DDL + | describeStmt + | dropStmt // DDL + | existsStmt + | explainStmt + | killStmt // DDL + | optimizeStmt // DDL + | renameStmt // DDL + | selectUnionStmt + | setStmt + | setRoleStmt + | showStmt + | systemStmt + | truncateStmt // DDL + | useStmt + | watchStmt + | ctes? selectStmt + ; + +// CTE statement +ctes + : WITH namedQuery (',' namedQuery)* + ; + +namedQuery + : name = identifier (columnAliases)? AS '(' query ')' + ; + +columnAliases + : '(' identifier (',' identifier)* ')' + ; + +// ALTER statement + +alterStmt + : ALTER TABLE tableIdentifier clusterClause? alterTableClause (COMMA alterTableClause)* # AlterTableStmt + ; + +alterTableClause + : ADD COLUMN (IF NOT EXISTS)? tableColumnDfnt (AFTER nestedIdentifier)? # AlterTableClauseAddColumn + | ADD INDEX (IF NOT EXISTS)? tableIndexDfnt (AFTER nestedIdentifier)? # AlterTableClauseAddIndex + | ADD PROJECTION (IF NOT EXISTS)? tableProjectionDfnt (AFTER nestedIdentifier)? # AlterTableClauseAddProjection + | ATTACH partitionClause (FROM tableIdentifier)? # AlterTableClauseAttach + | CLEAR COLUMN (IF EXISTS)? nestedIdentifier (IN partitionClause)? # AlterTableClauseClearColumn + | CLEAR INDEX (IF EXISTS)? nestedIdentifier (IN partitionClause)? # AlterTableClauseClearIndex + | CLEAR PROJECTION (IF EXISTS)? nestedIdentifier (IN partitionClause)? # AlterTableClauseClearProjection + | COMMENT COLUMN (IF EXISTS)? nestedIdentifier STRING_LITERAL # AlterTableClauseComment + | DELETE WHERE columnExpr # AlterTableClauseDelete + | DETACH partitionClause # AlterTableClauseDetach + | DROP COLUMN (IF EXISTS)? nestedIdentifier # AlterTableClauseDropColumn + | DROP INDEX (IF EXISTS)? nestedIdentifier # AlterTableClauseDropIndex + | DROP PROJECTION (IF EXISTS)? nestedIdentifier # AlterTableClauseDropProjection + | DROP partitionClause # AlterTableClauseDropPartition + | FREEZE partitionClause? # AlterTableClauseFreezePartition + | MATERIALIZE INDEX (IF EXISTS)? nestedIdentifier (IN partitionClause)? # AlterTableClauseMaterializeIndex + | MATERIALIZE PROJECTION (IF EXISTS)? nestedIdentifier (IN partitionClause)? # AlterTableClauseMaterializeProjection + | MODIFY COLUMN (IF EXISTS)? nestedIdentifier codecExpr # AlterTableClauseModifyCodec + | MODIFY COLUMN (IF EXISTS)? nestedIdentifier COMMENT STRING_LITERAL # AlterTableClauseModifyComment + | MODIFY COLUMN (IF EXISTS)? nestedIdentifier REMOVE tableColumnPropertyType # AlterTableClauseModifyRemove + | MODIFY COLUMN (IF EXISTS)? tableColumnDfnt # AlterTableClauseModify + | MODIFY ORDER BY columnExpr # AlterTableClauseModifyOrderBy + | MODIFY ttlClause # AlterTableClauseModifyTTL + | MOVE partitionClause ( + TO DISK STRING_LITERAL + | TO VOLUME STRING_LITERAL + | TO TABLE tableIdentifier + ) # AlterTableClauseMovePartition + | REMOVE TTL # AlterTableClauseRemoveTTL + | RENAME COLUMN (IF EXISTS)? nestedIdentifier TO nestedIdentifier # AlterTableClauseRename + | REPLACE partitionClause FROM tableIdentifier # AlterTableClauseReplace + | UPDATE assignmentExprList whereClause # AlterTableClauseUpdate + ; + +assignmentExprList + : assignmentExpr (COMMA assignmentExpr)* + ; + +assignmentExpr + : nestedIdentifier EQ_SINGLE columnExpr + ; + +tableColumnPropertyType + : ALIAS + | CODEC + | COMMENT + | DEFAULT + | MATERIALIZED + | TTL + ; + +partitionClause + : PARTITION columnExpr // actually we expect here any form of tuple of literals + | PARTITION ID STRING_LITERAL + ; + +// ATTACH statement +attachStmt + : ATTACH DICTIONARY tableIdentifier clusterClause? # AttachDictionaryStmt + ; + +// CHECK statement + +checkStmt + : CHECK TABLE tableIdentifier partitionClause? + ; + +// CREATE statement + +createStmt + : (ATTACH | CREATE) DATABASE (IF NOT EXISTS)? databaseIdentifier clusterClause? engineExpr? # CreateDatabaseStmt + | (ATTACH | CREATE (OR REPLACE)? | REPLACE) DICTIONARY (IF NOT EXISTS)? tableIdentifier uuidClause? clusterClause? dictionarySchemaClause + dictionaryEngineClause # CreateDictionaryStmt + | (ATTACH | CREATE) LIVE VIEW (IF NOT EXISTS)? tableIdentifier uuidClause? clusterClause? ( + WITH TIMEOUT DECIMAL_LITERAL? + )? destinationClause? tableSchemaClause? subqueryClause # CreateLiveViewStmt + | (ATTACH | CREATE) MATERIALIZED VIEW (IF NOT EXISTS)? tableIdentifier uuidClause? clusterClause? tableSchemaClause? ( + destinationClause + | engineClause POPULATE? + ) subqueryClause # CreateMaterializedViewStmt + | (ATTACH | CREATE (OR REPLACE)? | REPLACE) TEMPORARY? TABLE (IF NOT EXISTS)? tableIdentifier uuidClause? clusterClause? tableSchemaClause? + engineClause? subqueryClause? # CreateTableStmt + | (ATTACH | CREATE) (OR REPLACE)? VIEW (IF NOT EXISTS)? tableIdentifier uuidClause? clusterClause? tableSchemaClause? subqueryClause # + CreateViewStmt + ; + +dictionarySchemaClause + : LPAREN dictionaryAttrDfnt (COMMA dictionaryAttrDfnt)* RPAREN + ; + +dictionaryAttrDfnt + : identifier columnTypeExpr + ; + +dictionaryEngineClause + : dictionaryPrimaryKeyClause? + ; + +dictionaryPrimaryKeyClause + : PRIMARY KEY columnExprList + ; + +dictionaryArgExpr + : identifier (identifier (LPAREN RPAREN)? | literal) + ; + +sourceClause + : SOURCE LPAREN identifier LPAREN dictionaryArgExpr* RPAREN RPAREN + ; + +lifetimeClause + : LIFETIME LPAREN ( + DECIMAL_LITERAL + | MIN DECIMAL_LITERAL MAX DECIMAL_LITERAL + | MAX DECIMAL_LITERAL MIN DECIMAL_LITERAL + ) RPAREN + ; + +layoutClause + : LAYOUT LPAREN identifier LPAREN dictionaryArgExpr* RPAREN RPAREN + ; + +rangeClause + : RANGE LPAREN (MIN identifier MAX identifier | MAX identifier MIN identifier) RPAREN + ; + +dictionarySettingsClause + : SETTINGS LPAREN settingExprList RPAREN + ; + +clusterClause + : ON CLUSTER (identifier | STRING_LITERAL) + ; + +uuidClause + : UUID STRING_LITERAL + ; + +destinationClause + : TO tableIdentifier + ; + +subqueryClause + : AS selectUnionStmt + ; + +tableSchemaClause + : LPAREN tableElementExpr (COMMA tableElementExpr)* RPAREN # SchemaDescriptionClause + | AS tableIdentifier # SchemaAsTableClause + | AS tableFunctionExpr # SchemaAsFunctionClause + ; + +engineClause + : engineExpr + ; + +partitionByClause + : PARTITION BY columnExpr + ; + +primaryKeyClause + : PRIMARY KEY columnExpr + ; + +sampleByClause + : SAMPLE BY columnExpr + ; + +ttlClause + : TTL ttlExpr (COMMA ttlExpr)* + ; + +engineExpr + : ENGINE EQ_SINGLE? identifierOrNull (LPAREN columnExprList? RPAREN)? + ; + +tableElementExpr + : tableColumnDfnt # TableElementExprColumn + | CONSTRAINT identifier CHECK columnExpr # TableElementExprConstraint + | INDEX tableIndexDfnt # TableElementExprIndex + | PROJECTION tableProjectionDfnt # TableElementExprProjection + ; + +tableColumnDfnt + : nestedIdentifier columnTypeExpr tableColumnPropertyExpr? (COMMENT STRING_LITERAL)? codecExpr? ( + TTL columnExpr + )? + | nestedIdentifier columnTypeExpr? tableColumnPropertyExpr (COMMENT STRING_LITERAL)? codecExpr? ( + TTL columnExpr + )? + ; + +tableColumnPropertyExpr + : (DEFAULT | MATERIALIZED | ALIAS) columnExpr + ; + +tableIndexDfnt + : nestedIdentifier columnExpr TYPE columnTypeExpr GRANULARITY DECIMAL_LITERAL + ; + +tableProjectionDfnt + : nestedIdentifier projectionSelectStmt + ; + +codecExpr + : CODEC LPAREN codecArgExpr (COMMA codecArgExpr)* RPAREN + ; + +codecArgExpr + : identifier (LPAREN columnExprList? RPAREN)? + ; + +ttlExpr + : columnExpr (DELETE | TO DISK STRING_LITERAL | TO VOLUME STRING_LITERAL)? + ; + +// DESCRIBE statement + +describeStmt + : (DESCRIBE | DESC) TABLE? tableExpr + ; + +// DROP statement + +dropStmt + : (DETACH | DROP) DATABASE (IF EXISTS)? databaseIdentifier clusterClause? # DropDatabaseStmt + | (DETACH | DROP) (DICTIONARY | TEMPORARY? TABLE | VIEW) (IF EXISTS)? tableIdentifier clusterClause? ( + NO DELAY + )? # DropTableStmt + ; + +// EXISTS statement + +existsStmt + : EXISTS DATABASE databaseIdentifier # ExistsDatabaseStmt + | EXISTS (DICTIONARY | TEMPORARY? TABLE | VIEW)? tableIdentifier # ExistsTableStmt + ; + +// EXPLAIN statement + +explainStmt + : EXPLAIN AST query # ExplainASTStmt + | EXPLAIN SYNTAX query # ExplainSyntaxStmt + ; + +// INSERT statement + +insertStmt + : INSERT INTO TABLE? (tableIdentifier | FUNCTION tableFunctionExpr) columnsClause? dataClause + ; + +columnsClause + : LPAREN nestedIdentifier (COMMA nestedIdentifier)* RPAREN + ; + +dataClause + : FORMAT identifier # DataClauseFormat + | VALUES assignmentValues (COMMA assignmentValues)* # DataClauseValues + | selectUnionStmt SEMICOLON? EOF # DataClauseSelect + ; + +assignmentValues + : LPAREN assignmentValue (COMMA assignmentValue)* RPAREN # AssignmentValuesList + | LPAREN RPAREN # AssignmentValuesEmpty + ; + +assignmentValue + : literal # InsertRawValue + | QUERY # InsertParameter + | identifier (LPAREN columnExprList? RPAREN)? # InsertParameterFuncExpr + + ; + +// KILL statement + +killStmt + : KILL MUTATION clusterClause? whereClause (SYNC | ASYNC | TEST)? # KillMutationStmt + ; + +// OPTIMIZE statement + +optimizeStmt + : OPTIMIZE TABLE tableIdentifier clusterClause? partitionClause? FINAL? DEDUPLICATE? + ; + +// RENAME statement + +renameStmt + : RENAME TABLE tableIdentifier TO tableIdentifier (COMMA tableIdentifier TO tableIdentifier)* clusterClause? + ; + +// PROJECTION SELECT statement + +projectionSelectStmt + : LPAREN withClause? SELECT columnExprList groupByClause? projectionOrderByClause? RPAREN + ; + +// SELECT statement + +selectUnionStmt + : selectStmtWithParens (UNION ALL selectStmtWithParens)* + ; + +selectStmtWithParens + : selectStmt + | LPAREN selectUnionStmt RPAREN + ; + +selectStmt + : withClause? SELECT DISTINCT? topClause? columnExprList fromClause? arrayJoinClause? windowClause? prewhereClause? whereClause? groupByClause? ( + WITH (CUBE | ROLLUP) + )? (WITH TOTALS)? havingClause? orderByClause? limitByClause? limitClause? settingsClause? + ; + +withClause + : WITH columnExprList + ; + +topClause + : TOP DECIMAL_LITERAL (WITH TIES)? + ; + +fromClause + : FROM joinExpr + ; + +arrayJoinClause + : (LEFT | INNER)? ARRAY JOIN columnExprList + ; + +windowClause + : WINDOW identifier AS LPAREN windowExpr RPAREN + ; + +prewhereClause + : PREWHERE columnExpr + ; + +whereClause + : WHERE columnExpr + ; + +groupByClause + : GROUP BY ((CUBE | ROLLUP) LPAREN columnExprList RPAREN | columnExprList) + ; + +havingClause + : HAVING columnExpr + ; + +orderByClause + : ORDER BY orderExprList + ; + +projectionOrderByClause + : ORDER BY columnExprList + ; + +limitByClause + : LIMIT limitExpr BY columnExprList + ; + +limitClause + : LIMIT limitExpr (WITH TIES)? + ; + +settingsClause + : SETTINGS settingExprList + ; + +joinExpr + : joinExpr (GLOBAL | LOCAL)? joinOp? JOIN joinExpr joinConstraintClause # JoinExprOp + | joinExpr joinOpCross joinExpr # JoinExprCrossOp + | tableExpr FINAL? sampleClause? # JoinExprTable + | LPAREN joinExpr RPAREN # JoinExprParens + ; + +joinOp + : ((ALL | ANY | ASOF)? INNER | INNER (ALL | ANY | ASOF)? | (ALL | ANY | ASOF)) # JoinOpInner + | ( + (SEMI | ALL | ANTI | ANY | ASOF)? (LEFT | RIGHT) OUTER? + | (LEFT | RIGHT) OUTER? (SEMI | ALL | ANTI | ANY | ASOF)? + ) # JoinOpLeftRight + | ((ALL | ANY)? FULL OUTER? | FULL OUTER? (ALL | ANY)?) # JoinOpFull + ; + +joinOpCross + : (GLOBAL | LOCAL)? CROSS JOIN + | COMMA + ; + +joinConstraintClause + : ON columnExprList + | USING LPAREN columnExprList RPAREN + | USING columnExprList + ; + +sampleClause + : SAMPLE ratioExpr (OFFSET ratioExpr)? + ; + +limitExpr + : columnExpr ((COMMA | OFFSET) columnExpr)? + ; + +orderExprList + : orderExpr (COMMA orderExpr)* + ; + +orderExpr + : columnExpr (ASCENDING | DESCENDING | DESC)? (NULLS (FIRST | LAST))? (COLLATE STRING_LITERAL)? + ; + +ratioExpr + : numberLiteral (SLASH numberLiteral)? + ; + +settingExprList + : settingExpr (COMMA settingExpr)* + ; + +settingExpr + : identifier EQ_SINGLE literal + ; + +windowExpr + : winPartitionByClause? winOrderByClause? winFrameClause? + ; + +winPartitionByClause + : PARTITION BY columnExprList + ; + +winOrderByClause + : ORDER BY orderExprList + ; + +winFrameClause + : (ROWS | RANGE) winFrameExtend + ; + +winFrameExtend + : winFrameBound # frameStart + | BETWEEN winFrameBound AND winFrameBound # frameBetween + ; + +winFrameBound + : ( + CURRENT ROW + | UNBOUNDED PRECEDING + | UNBOUNDED FOLLOWING + | numberLiteral PRECEDING + | numberLiteral FOLLOWING + ) + ; + +//rangeClause: RANGE LPAREN (MIN identifier MAX identifier | MAX identifier MIN identifier) RPAREN; + +// SET statement + +setStmt + : SET settingExprList + ; + +// SET ROLE statement + +setRoleStmt + : SET (DEFAULT)? ROLE (setRolesList | NONE | ALL (EXCEPT setRolesList)) TO identifier | CURRENT_USER (COMMA identifier | CURRENT_USER)* + ; + +setRolesList + : identifier (COMMA identifier)* + ; + +// SHOW statements + +showStmt + : SHOW CREATE DATABASE databaseIdentifier # showCreateDatabaseStmt + | SHOW CREATE DICTIONARY tableIdentifier # showCreateDictionaryStmt + | SHOW CREATE TEMPORARY? TABLE? tableIdentifier # showCreateTableStmt + | SHOW DATABASES # showDatabasesStmt + | SHOW DICTIONARIES (FROM databaseIdentifier)? # showDictionariesStmt + | SHOW TEMPORARY? TABLES ((FROM | IN) databaseIdentifier)? (LIKE STRING_LITERAL | whereClause)? limitClause? # showTablesStmt + ; + +// SYSTEM statements + +systemStmt + : SYSTEM FLUSH DISTRIBUTED tableIdentifier + | SYSTEM FLUSH LOGS + | SYSTEM RELOAD DICTIONARIES + | SYSTEM RELOAD DICTIONARY tableIdentifier + | SYSTEM (START | STOP) (DISTRIBUTED SENDS | FETCHES | TTL? MERGES) tableIdentifier + | SYSTEM (START | STOP) REPLICATED SENDS + | SYSTEM SYNC REPLICA tableIdentifier + ; + +// TRUNCATE statements + +truncateStmt + : TRUNCATE TEMPORARY? TABLE? (IF EXISTS)? tableIdentifier clusterClause? + ; + +// USE statement + +useStmt + : USE databaseIdentifier + ; + +// WATCH statement + +watchStmt + : WATCH tableIdentifier EVENTS? (LIMIT DECIMAL_LITERAL)? + ; + +// Columns + +columnTypeExpr + : identifier # ColumnTypeExprSimple // UInt64 + | identifier LPAREN identifier columnTypeExpr (COMMA identifier columnTypeExpr)* RPAREN # ColumnTypeExprNested // Nested + | identifier LPAREN enumValue (COMMA enumValue)* RPAREN # ColumnTypeExprEnum // Enum + | identifier LPAREN columnTypeExpr (COMMA columnTypeExpr)* RPAREN # ColumnTypeExprComplex // Array, Tuple + | identifier LPAREN columnExprList? RPAREN # ColumnTypeExprParam // FixedString(N) + ; + +columnExprList + : columnsExpr (COMMA columnsExpr)* + ; + +columnsExpr + : (tableIdentifier DOT)? ASTERISK # ColumnsExprAsterisk + | LPAREN selectUnionStmt RPAREN # ColumnsExprSubquery + // NOTE: asterisk and subquery goes before |columnExpr| so that we can mark them as multi-column expressions. + | columnExpr # ColumnsExprColumn + ; + +columnExpr + : CASE columnExpr? (WHEN columnExpr THEN columnExpr)+ (ELSE columnExpr)? END # ColumnExprCase + | CAST LPAREN columnExpr AS columnTypeExpr RPAREN # ColumnExprCast + | DATE STRING_LITERAL # ColumnExprDate + | EXTRACT LPAREN interval FROM columnExpr RPAREN # ColumnExprExtract + | INTERVAL columnExpr interval # ColumnExprInterval + | SUBSTRING LPAREN columnExpr FROM columnExpr (FOR columnExpr)? RPAREN # ColumnExprSubstring + | TIMESTAMP STRING_LITERAL # ColumnExprTimestamp + | TRIM LPAREN (BOTH | LEADING | TRAILING) STRING_LITERAL FROM columnExpr RPAREN # ColumnExprTrim + | identifier (LPAREN columnExprList? RPAREN) OVER LPAREN windowExpr RPAREN # ColumnExprWinFunction + | identifier (LPAREN columnExprList? RPAREN) OVER identifier # ColumnExprWinFunctionTarget + | identifier (LPAREN columnExprList? RPAREN)? LPAREN DISTINCT? columnArgList? RPAREN # ColumnExprFunction + | literal # ColumnExprLiteral + + // FIXME(ilezhankin): this part looks very ugly, maybe there is another way to express it + | columnExpr LBRACKET columnExpr RBRACKET # ColumnExprArrayAccess + | columnExpr DOT DECIMAL_LITERAL # ColumnExprTupleAccess + | DASH columnExpr # ColumnExprNegate + | columnExpr ( + ASTERISK // multiply + | SLASH // divide + | PERCENT // modulo + ) columnExpr # ColumnExprPrecedence1 + | columnExpr ( + PLUS // plus + | DASH // minus + | CONCAT // concat + ) columnExpr # ColumnExprPrecedence2 + | columnExpr ( + EQ_DOUBLE // equals + | EQ_SINGLE // equals + | NOT_EQ // notEquals + | LE // lessOrEquals + | GE // greaterOrEquals + | LT // less + | GT // greater + | GLOBAL? NOT? IN // in, notIn, globalIn, globalNotIn + | NOT? (LIKE | ILIKE) // like, notLike, ilike, notILike + ) columnExpr # ColumnExprPrecedence3 + | columnExpr IS NOT? NULL_SQL # ColumnExprIsNull + | NOT columnExpr # ColumnExprNot + | columnExpr AND columnExpr # ColumnExprAnd + | columnExpr OR columnExpr # ColumnExprOr + // TODO(ilezhankin): `BETWEEN a AND b AND c` is parsed in a wrong way: `BETWEEN (a AND b) AND c` + | columnExpr NOT? BETWEEN columnExpr AND columnExpr # ColumnExprBetween + | columnExpr QUERY columnExpr COLON columnExpr # ColumnExprTernaryOp + | columnExpr (alias | AS identifier) # ColumnExprAlias + | (tableIdentifier DOT)? ASTERISK # ColumnExprAsterisk // single-column only + | LPAREN selectUnionStmt RPAREN # ColumnExprSubquery // single-column only + | LPAREN columnExpr RPAREN # ColumnExprParens // single-column only + | LPAREN columnExprList RPAREN # ColumnExprTuple + | LBRACKET columnExprList? RBRACKET # ColumnExprArray + | columnIdentifier # ColumnExprIdentifier + | QUERY # ColumnExprParam + ; + +columnArgList + : columnArgExpr (COMMA columnArgExpr)* + | QUERY (COMMA QUERY)* + ; + +columnArgExpr + : columnLambdaExpr + | columnExpr + ; + +columnLambdaExpr + : (LPAREN identifier (COMMA identifier)* RPAREN | identifier (COMMA identifier)*) ARROW columnExpr + ; + +columnIdentifier + : (tableIdentifier DOT)? nestedIdentifier + ; + +nestedIdentifier + : identifier (DOT identifier)? + ; + +// Tables + +tableExpr + : tableIdentifier # TableExprIdentifier + | tableFunctionExpr # TableExprFunction + | LPAREN selectUnionStmt RPAREN # TableExprSubquery + | tableExpr (alias | AS identifier) # TableExprAlias + ; + +tableFunctionExpr + : identifier LPAREN tableArgList? RPAREN + ; + +tableIdentifier + : (databaseIdentifier DOT)? identifier + ; + +tableArgList + : tableArgExpr (COMMA tableArgExpr)* + ; + +tableArgExpr + : nestedIdentifier + | tableFunctionExpr + | literal + ; + +// Databases + +databaseIdentifier + : identifier + ; + +// Basics + +floatingLiteral + : FLOATING_LITERAL + | DOT (DECIMAL_LITERAL | OCTAL_LITERAL) + | DECIMAL_LITERAL DOT (DECIMAL_LITERAL | OCTAL_LITERAL)? // can't move this to the lexer or it will break nested tuple access: t.1.2 + ; + +numberLiteral + : (PLUS | DASH)? ( + floatingLiteral + | OCTAL_LITERAL + | DECIMAL_LITERAL + | HEXADECIMAL_LITERAL + | INF + | NAN_SQL + ) + ; + +literal + : numberLiteral + | STRING_LITERAL + | NULL_SQL + ; + +interval + : SECOND + | MINUTE + | HOUR + | DAY + | WEEK + | MONTH + | QUARTER + | YEAR + ; + +keyword + // except NULL_SQL, INF, NAN_SQL + : AFTER + | ALIAS + | ALL + | ALTER + | AND + | ANTI + | ANY + | ARRAY + | AS + | ASCENDING + | ASOF + | AST + | ASYNC + | ATTACH + | BETWEEN + | BOTH + | BY + | CASE + | CAST + | CHECK + | CLEAR + | CLUSTER + | CODEC + | COLLATE + | COLUMN + | COMMENT + | CONSTRAINT + | CREATE + | CROSS + | CUBE + | CURRENT + | DATABASE + | DATABASES + | DATE + | DEDUPLICATE + | DEFAULT + | DELAY + | DELETE + | DESCRIBE + | DESC + | DESCENDING + | DETACH + | DICTIONARIES + | DICTIONARY + | DISK + | DISTINCT + | DISTRIBUTED + | DROP + | ELSE + | END + | ENGINE + | EVENTS + | EXISTS + | EXPLAIN + | EXPRESSION + | EXTRACT + | FETCHES + | FINAL + | FIRST + | FLUSH + | FOR + | FOLLOWING + | FOR + | FORMAT + | FREEZE + | FROM + | FULL + | FUNCTION + | GLOBAL + | GRANULARITY + | GROUP + | HAVING + | HIERARCHICAL + | ID + | IF + | ILIKE + | IN + | INDEX + | INJECTIVE + | INNER + | INSERT + | INTERVAL + | INTO + | IS + | IS_OBJECT_ID + | JOIN + | JSON_FALSE + | JSON_TRUE + | KEY + | KILL + | LAST + | LAYOUT + | LEADING + | LEFT + | LIFETIME + | LIKE + | LIMIT + | LIVE + | LOCAL + | LOGS + | MATERIALIZE + | MATERIALIZED + | MAX + | MERGES + | MIN + | MODIFY + | MOVE + | MUTATION + | NO + | NOT + | NULLS + | OFFSET + | ON + | OPTIMIZE + | OR + | ORDER + | OUTER + | OUTFILE + | OVER + | PARTITION + | POPULATE + | PRECEDING + | PREWHERE + | PRIMARY + | RANGE + | RELOAD + | REMOVE + | RENAME + | REPLACE + | REPLICA + | REPLICATED + | RIGHT + | ROLLUP + | ROW + | ROWS + | SAMPLE + | SELECT + | SEMI + | SENDS + | SET + | SETTINGS + | SHOW + | SOURCE + | START + | STOP + | SUBSTRING + | SYNC + | SYNTAX + | SYSTEM + | TABLE + | TABLES + | TEMPORARY + | TEST + | THEN + | TIES + | TIMEOUT + | TIMESTAMP + | TOTALS + | TRAILING + | TRIM + | TRUNCATE + | TO + | TOP + | TTL + | TYPE + | UNBOUNDED + | UNION + | UPDATE + | USE + | USING + | UUID + | VALUES + | VIEW + | VOLUME + | WATCH + | WHEN + | WHERE + | WINDOW + | WITH + ; + +keywordForAlias + : DATE + | FIRST + | ID + | KEY + ; + +alias + : IDENTIFIER + | keywordForAlias + ; // |interval| can't be an alias, otherwise 'INTERVAL 1 SOMETHING' becomes ambiguous. + +identifier + : IDENTIFIER + | interval + | keyword + ; + +identifierOrNull + : identifier + | NULL_SQL + ; // NULL_SQL can be only 'Null' here. + +enumValue + : STRING_LITERAL EQ_SINGLE numberLiteral + ; \ No newline at end of file diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java index 86dd1a5bf..ac6cf3db8 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ConnectionImpl.java @@ -9,12 +9,12 @@ import com.clickhouse.data.ClickHouseDataType; import com.clickhouse.jdbc.internal.ClientInfoProperties; import com.clickhouse.jdbc.internal.DriverProperties; -import com.clickhouse.jdbc.internal.JdbcConfiguration; import com.clickhouse.jdbc.internal.ExceptionUtils; +import com.clickhouse.jdbc.internal.JdbcConfiguration; import com.clickhouse.jdbc.internal.JdbcUtils; -import com.clickhouse.jdbc.internal.StatementParser; +import com.clickhouse.jdbc.internal.ParsedPreparedStatement; +import com.clickhouse.jdbc.internal.SqlParser; import com.clickhouse.jdbc.metadata.DatabaseMetaDataImpl; -import com.google.common.collect.Table; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -62,6 +62,8 @@ public class ConnectionImpl implements Connection, JdbcV2Wrapper { private final DatabaseMetaDataImpl metadata; protected final Calendar defaultCalendar; + private final SqlParser sqlParser; + public ConnectionImpl(String url, Properties info) throws SQLException { try { log.debug("Creating connection to {}", url); @@ -107,6 +109,8 @@ public ConnectionImpl(String url, Properties info) throws SQLException { this.metadata = new DatabaseMetaDataImpl(this, false, url); this.defaultCalendar = Calendar.getInstance(); + + this.sqlParser = new SqlParser(); } catch (SQLException e) { throw e; } catch (Exception e) { @@ -114,7 +118,12 @@ public ConnectionImpl(String url, Properties info) throws SQLException { } } + public SqlParser getSqlParser() { + return sqlParser; + } + public QuerySettings getDefaultQuerySettings() { + defaultQuerySettings.setDatabase(schema); return defaultQuerySettings; } @@ -369,16 +378,23 @@ public Statement createStatement(int resultSetType, int resultSetConcurrency, in public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { checkOpen(); - StatementParser.ParsedStatement parsedStatement = StatementParser.parsePreparedStatement(sql); - - if (config.isBetaFeatureEnabled(DriverProperties.BETA_ROW_BINARY_WRITER)) { - if (parsedStatement.getType() == StatementParser.StatementType.INSERT) { - if (!parsedStatement.hasColumnList() && !PreparedStatementImpl.FUNC_DETECT_REGEXP.matcher(sql).find()) { - TableSchema tableSchema = client.getTableSchema(parsedStatement.getTableName(), schema); - if (tableSchema.getColumns().size() == parsedStatement.getArgumentCount()) { - return new WriterStatementImpl(this, sql, tableSchema, parsedStatement); - } - } + ParsedPreparedStatement parsedStatement = sqlParser.parsePreparedStatement(sql); + + if (parsedStatement.isInsert() && config.isBetaFeatureEnabled(DriverProperties.BETA_ROW_BINARY_WRITER)) { + /* + * RowBinary can be used when + * - INSERT INTO t (c1, c2) VALUES (?, ?) + * - INSERT INTO t VALUES (?, ?, ?) + * - number of arguments matches schema or column list + * RowBinary cannot be used when + * - INSERT INTO t VALUES (now(), ?, ?) !# there is a function in the values + * - INSERT INTO t VALUES (now(), ?, 1), (now(), ?, 2) !# multiple values list + * - INSERT INTO t SELECT ?, ?, ? !# insert from select + */ + if (!parsedStatement.isInsertWithSelect() && parsedStatement.getAssignValuesGroups() == 1 + && !parsedStatement.isUseFunction()) { + TableSchema tableSchema = client.getTableSchema(parsedStatement.getTable(), schema); + return new WriterStatementImpl(this, sql, tableSchema, parsedStatement); } } return new PreparedStatementImpl(this, sql, parsedStatement); diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java index e649c827d..5adab9313 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/PreparedStatementImpl.java @@ -1,11 +1,10 @@ package com.clickhouse.jdbc; import com.clickhouse.client.api.metadata.TableSchema; -import com.clickhouse.client.api.query.QuerySettings; import com.clickhouse.data.Tuple; import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; -import com.clickhouse.jdbc.internal.StatementParser; +import com.clickhouse.jdbc.internal.ParsedPreparedStatement; import com.clickhouse.jdbc.metadata.ParameterMetaDataImpl; import com.clickhouse.jdbc.metadata.ResultSetMetaDataImpl; import org.slf4j.Logger; @@ -32,6 +31,7 @@ import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLType; import java.sql.SQLXML; +import java.sql.Statement; import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; @@ -45,7 +45,15 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; import java.util.regex.Pattern; public class PreparedStatementImpl extends StatementImpl implements PreparedStatement, JdbcV2Wrapper { @@ -59,69 +67,74 @@ public class PreparedStatementImpl extends StatementImpl implements PreparedStat private final Calendar defaultCalendar; - // common fields private final String originalSql; - private final String [] sqlSegments; - private final Object [] parameters; - private final StatementParser.StatementType statementType; + private final String[] values; // temp value holder (set can be called > once) + private final List batchValues; // composed value statements + private final ParsedPreparedStatement parsedPreparedStatement; + private final boolean insertStmtWithValues; + private final String valueListTmpl; + private final int[] paramPositionsInDataClause; - // insert - private String [] valueSegments; - private String insertIntoSQL; + private final int argCount; private final ParameterMetaData parameterMetaData; - private ResultSetMetaData resultSetMetaData = null; - // Detects if any of the arguments is within function parameters - static final Pattern FUNC_DETECT_REGEXP = Pattern.compile( - "\\b(?!values?\\b)[A-Za-z_]\\w*\\([^)]*\\?[^)]*\\)", - Pattern.CASE_INSENSITIVE); - - private static final Pattern VALUES_PARAMETER_SPLIT = Pattern.compile("\\?(?=(?:[^']*'[^']*')*[^']*$)"); - - public PreparedStatementImpl(ConnectionImpl connection, String sql, StatementParser.ParsedStatement parsedStatement) throws SQLException { + public PreparedStatementImpl(ConnectionImpl connection, String sql, ParsedPreparedStatement parsedStatement) throws SQLException { super(connection); this.isPoolable = true; // PreparedStatement is poolable by default - this.originalSql = sql.trim(); - //Split the sql string into an array of strings around question mark tokens - this.sqlSegments = parsedStatement.getSqlSegments(); - this.statementType = parsedStatement.getType(); - - if (this.statementType == StatementParser.StatementType.INSERT) { - insertIntoSQL = originalSql.substring(0, originalSql.indexOf("VALUES") + 6); - valueSegments = originalSql.substring(originalSql.indexOf("VALUES") + 6).split("\\?"); - } + this.originalSql = sql; + this.parsedPreparedStatement = parsedStatement; + this.argCount = parsedStatement.getArgCount(); - //Create an array of objects to store the parameters - this.parameters = new Object[sqlSegments.length - 1]; this.defaultCalendar = connection.defaultCalendar; - this.parameterMetaData = new ParameterMetaDataImpl(this.parameters.length); + this.values = new String[argCount]; + this.parameterMetaData = new ParameterMetaDataImpl(this.values.length); + + int valueListStartPos = parsedStatement.getAssignValuesListStartPosition(); + int valueListStopPos = parsedStatement.getAssignValuesListStopPosition(); + if (parsedStatement.getAssignValuesGroups() == 1 && valueListStartPos > -1 && valueListStopPos > -1) { + int[] positions = parsedStatement.getParamPositions(); + paramPositionsInDataClause = new int[argCount]; + for (int i = 0; i < argCount; i++) { + int p = positions[i] - valueListStartPos; + paramPositionsInDataClause[i] = p; + } + + valueListTmpl = originalSql.substring(valueListStartPos, valueListStopPos + 1); + insertStmtWithValues = true; + batchValues = new ArrayList<>(); + } else { + paramPositionsInDataClause = new int[0]; + batchValues = Collections.emptyList(); + valueListTmpl = ""; + insertStmtWithValues = false; + } } - private String compileSql(String []segments) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < segments.length; i++) { - sb.append(segments[i]); - if (i < parameters.length) { - sb.append(parameters[i]); - } + private String buildSQL() { + StringBuilder compiledSql = new StringBuilder(originalSql); + int posOffset = 0; + int[] positions = parsedPreparedStatement.getParamPositions(); + for (int i = 0; i < argCount; i++) { + int p = positions[i] + posOffset; + String val = values[i]; + compiledSql.replace(p, p+1, val == null ? "NULL" : val); + posOffset += val == null ? 0 : val.length() - 1; } - LOG.trace("Compiled SQL: {}", sb); - return sb.toString(); + return compiledSql.toString(); } @Override public ResultSet executeQuery() throws SQLException { checkClosed(); - return super.executeQueryImpl(compileSql(sqlSegments), new QuerySettings().setDatabase(connection.getSchema())); + return super.executeQueryImpl(buildSQL(), localSettings); } @Override public int executeUpdate() throws SQLException { checkClosed(); - return super.executeUpdateImpl(compileSql(sqlSegments), statementType, - new QuerySettings().setDatabase(connection.getSchema())); + return super.executeUpdateImpl(buildSQL(), localSettings); } @Override @@ -133,61 +146,61 @@ public void setNull(int parameterIndex, int sqlType) throws SQLException { @Override public void setBoolean(int parameterIndex, boolean x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setByte(int parameterIndex, byte x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setShort(int parameterIndex, short x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setInt(int parameterIndex, int x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setLong(int parameterIndex, long x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setFloat(int parameterIndex, float x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setDouble(int parameterIndex, double x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setString(int parameterIndex, String x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setBytes(int parameterIndex, byte[] x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override @@ -208,29 +221,29 @@ public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { @Override public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void clearParameters() throws SQLException { checkClosed(); - Arrays.fill(this.parameters, null); + Arrays.fill(this.values, null); } int getParametersCount() { - return parameters.length; + return values.length; } @Override @@ -248,70 +261,85 @@ public void setObject(int parameterIndex, Object x) throws SQLException { @Override public boolean execute() throws SQLException { checkClosed(); - return super.executeImpl(compileSql(sqlSegments), statementType, - new QuerySettings().setDatabase(connection.getSchema())); + if (parsedPreparedStatement.isHasResultSet()) { + super.executeQueryImpl(buildSQL(), localSettings); + return true; + } else { + super.executeUpdateImpl(buildSQL(), localSettings); + return false; + } } @Override public void addBatch() throws SQLException { checkClosed(); - if (statementType == StatementParser.StatementType.INSERT) { - // adding values to the end of big INSERT statement. - super.addBatch(compileSql(valueSegments)); + + if (insertStmtWithValues) { + StringBuilder valuesClause = new StringBuilder(valueListTmpl); + int posOffset = 0; + for (int i = 0; i < argCount; i++) { + int p = paramPositionsInDataClause[i] + posOffset; + valuesClause.replace(p, p + 1, values[i]); + posOffset += values[i].length() - 1; + } + batchValues.add(valuesClause); } else { - super.addBatch(compileSql(sqlSegments)); + super.addBatch(buildSQL()); } } @Override public int[] executeBatch() throws SQLException { checkClosed(); - if (statementType == StatementParser.StatementType.INSERT && !batch.isEmpty()) { - // write insert into as batch to avoid multiple requests - StringBuilder sb = new StringBuilder(); - sb.append(insertIntoSQL).append(" "); + + if (insertStmtWithValues) { + // run executeBatch + return executeInsertBatch().stream().mapToInt(Integer::intValue).toArray(); + } else { + List results = new ArrayList<>(); for (String sql : batch) { - sb.append(sql).append(","); - } - sb.setCharAt(sb.length() - 1, ';'); - int rowsInserted = executeUpdateImpl(sb.toString(), statementType, - new QuerySettings().setDatabase(connection.getSchema())); - // clear batch and re-add insert into - int[] results = new int[batch.size()]; - if (rowsInserted == batch.size()) { - // each batch is effectively 1 row inserted. - Arrays.fill(results, 1); - } else { - // we do not have information what rows are not inserted. - // this should happen only with async insert when we do not wait final result - Arrays.fill(results, PreparedStatement.SUCCESS_NO_INFO); + results.add(executeUpdateImpl(sql, localSettings)); } - batch.clear(); - return results; - } else { - // run executeBatch - return executeBatchImpl().stream().mapToInt(Integer::intValue).toArray(); + return results.stream().mapToInt(Integer::intValue).toArray(); } } @Override public long[] executeLargeBatch() throws SQLException { - return executeBatchImpl().stream().mapToLong(Integer::longValue).toArray(); + checkClosed(); + + if (insertStmtWithValues) { + return executeInsertBatch().stream().mapToLong(Integer::longValue).toArray(); + } else { + List results = new ArrayList<>(); + for (String sql : batch) { + results.add(executeUpdateImpl(sql, localSettings)); + } + return results.stream().mapToLong(Integer::longValue).toArray(); + } } - private List executeBatchImpl() throws SQLException { - List results = new ArrayList<>(); - QuerySettings settings = new QuerySettings().setDatabase(connection.getSchema()); - for (String sql : batch) { - results.add(executeUpdateImpl(sql, statementType, settings)); + private List executeInsertBatch() throws SQLException { + StringBuilder insertSql = new StringBuilder(originalSql.substring(0, + parsedPreparedStatement.getAssignValuesListStartPosition())); + + for (StringBuilder valuesList : batchValues) { + insertSql.append(valuesList).append(','); + } + insertSql.setLength(insertSql.length() - 1); + + int updateCount = super.executeUpdateImpl(insertSql.toString(), localSettings); + if (updateCount == batchValues.size()) { + return Collections.nCopies(batchValues.size(), 1); + } else { + return Collections.nCopies(batchValues.size(), Statement.SUCCESS_NO_INFO); } - return results; } @Override public void setCharacterStream(int parameterIndex, Reader x, int length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override @@ -325,19 +353,19 @@ public void setRef(int parameterIndex, Ref x) throws SQLException { @Override public void setBlob(int parameterIndex, Blob x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setClob(int parameterIndex, Clob x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setArray(int parameterIndex, Array x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override @@ -346,10 +374,10 @@ public ResultSetMetaData getMetaData() throws SQLException { if (resultSetMetaData == null && currentResultSet == null) { // before execution - if (statementType == StatementParser.StatementType.SELECT) { + if (parsedPreparedStatement.isHasResultSet()) { try { // Replace '?' with NULL to make SQL valid for DESCRIBE - String sql = JdbcUtils.replaceQuestionMarks(originalSql, JdbcUtils.NULL); + String sql = replaceQuestionMarks(originalSql, NULL_LITERAL); TableSchema tSchema = connection.getClient().getTableSchemaFromQuery(sql); resultSetMetaData = new ResultSetMetaDataImpl(tSchema.getColumns(), connection.getSchema(), connection.getCatalog(), @@ -371,10 +399,32 @@ public ResultSetMetaData getMetaData() throws SQLException { return resultSetMetaData; } + public static final String NULL_LITERAL = "NULL"; + + private static final Pattern REPLACE_Q_MARK_PATTERN = Pattern.compile("(\"[^\"]*\"|`[^`]*`|'[^']*')|(\\?)"); + + public static String replaceQuestionMarks(String sql, String replacement) { + Matcher matcher = REPLACE_Q_MARK_PATTERN.matcher(sql); + + StringBuilder result = new StringBuilder(); + + while (matcher.find()) { + if (matcher.group(1) != null) { + // Quoted string — keep as-is + matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group(1))); + } else if (matcher.group(2) != null) { + // Question mark outside quotes — replace it + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + } + matcher.appendTail(result); + return result.toString(); + } + @Override public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(sqlDateToInstant(x, cal)); + values[parameterIndex - 1] = encodeObject(sqlDateToInstant(x, cal)); } protected Instant sqlDateToInstant(Date x, Calendar cal) { @@ -388,7 +438,7 @@ protected Instant sqlDateToInstant(Date x, Calendar cal) { @Override public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(sqlTimeToInstant(x, cal)); + values[parameterIndex - 1] = encodeObject(sqlTimeToInstant(x, cal)); } protected Instant sqlTimeToInstant(Time x, Calendar cal) { @@ -402,7 +452,7 @@ protected Instant sqlTimeToInstant(Time x, Calendar cal) { @Override public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(sqlTimestampToZDT(x, cal)); + values[parameterIndex - 1] = encodeObject(sqlTimestampToZDT(x, cal)); } protected ZonedDateTime sqlTimestampToZDT(Timestamp x, Calendar cal) { @@ -416,13 +466,13 @@ protected ZonedDateTime sqlTimestampToZDT(Timestamp x, Calendar cal) { @Override public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(null); + values[parameterIndex - 1] = encodeObject(null); } @Override public void setURL(int parameterIndex, URL x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } /** @@ -449,43 +499,43 @@ public void setRowId(int parameterIndex, RowId x) throws SQLException { @Override public void setNString(int parameterIndex, String x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setNCharacterStream(int parameterIndex, Reader x, long length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setNClob(int parameterIndex, NClob x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setClob(int parameterIndex, Reader x, long length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setBlob(int parameterIndex, InputStream x, long length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setNClob(int parameterIndex, Reader x, long length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setSQLXML(int parameterIndex, SQLXML x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override @@ -497,67 +547,67 @@ public void setObject(int parameterIndex, Object x, int targetSqlType, int scale @Override public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setCharacterStream(int parameterIndex, Reader x, long length) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setCharacterStream(int parameterIndex, Reader x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setNCharacterStream(int parameterIndex, Reader x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setClob(int parameterIndex, Reader x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setBlob(int parameterIndex, InputStream x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setNClob(int parameterIndex, Reader x) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override public void setObject(int parameterIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { checkClosed(); - parameters[parameterIndex - 1] = encodeObject(x); + values[parameterIndex - 1] = encodeObject(x); } @Override diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java index 535f9a495..5e51f4533 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/ResultSetImpl.java @@ -37,7 +37,6 @@ import java.sql.Timestamp; import java.time.ZonedDateTime; import java.util.Calendar; -import java.util.Collections; import java.util.List; import java.util.Map; @@ -90,7 +89,7 @@ private String columnIndexToName(int index) throws SQLException { try { return getSchema().columnIndexToName(index); } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: columnIndexToName(%s) encountered an exception.", index), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: columnIndexToName(%s) encountered an exception.", index), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -238,7 +237,7 @@ public String getString(String columnLabel) throws SQLException { return null; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getString(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getString(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -254,7 +253,7 @@ public boolean getBoolean(String columnLabel) throws SQLException { return false; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getBoolean(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getBoolean(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -270,7 +269,7 @@ public byte getByte(String columnLabel) throws SQLException { return 0; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getByte(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getByte(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -286,7 +285,7 @@ public short getShort(String columnLabel) throws SQLException { return 0; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getShort(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getShort(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -302,7 +301,7 @@ public int getInt(String columnLabel) throws SQLException { return 0; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getInt(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getInt(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -318,7 +317,7 @@ public long getLong(String columnLabel) throws SQLException { return 0; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getLong(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getLong(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -334,7 +333,7 @@ public float getFloat(String columnLabel) throws SQLException { return 0; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getFloat(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getFloat(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -350,7 +349,7 @@ public double getDouble(String columnLabel) throws SQLException { return 0; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getDouble(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getDouble(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -366,7 +365,7 @@ public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLExcepti return null; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getBigDecimal(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getBigDecimal(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -382,7 +381,7 @@ public byte[] getBytes(String columnLabel) throws SQLException { return null; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getBytes(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getBytes(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -472,7 +471,7 @@ public int findColumn(String columnLabel) throws SQLException { try { return reader.getSchema().getColumnByName(columnLabel).getColumnIndex(); } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: findColumn(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: findColumn(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -513,7 +512,7 @@ public BigDecimal getBigDecimal(String columnLabel) throws SQLException { return null; } } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getBigDecimal(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getBigDecimal(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1057,7 +1056,7 @@ public java.sql.Array getArray(String columnLabel) throws SQLException { column.getArrayBaseColumn().getDataType().name(), JdbcUtils.convertToSqlType(column.getArrayBaseColumn().getDataType()).getVendorTypeNumber()); } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getArray(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getArray(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1082,7 +1081,7 @@ public Date getDate(String columnLabel, Calendar cal) throws SQLException { c.set(zdt.getYear(), zdt.getMonthValue() - 1, zdt.getDayOfMonth(), 0, 0, 0); return new Date(c.getTimeInMillis()); } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getDate(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getDate(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1107,7 +1106,7 @@ public Time getTime(String columnLabel, Calendar cal) throws SQLException { c.set(1970, Calendar.JANUARY, 1, zdt.getHour(), zdt.getMinute(), zdt.getSecond()); return new Time(c.getTimeInMillis()); } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getTime(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getTime(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1134,7 +1133,7 @@ public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLExcept timestamp.setNanos(zdt.getNano()); return timestamp; } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getTimestamp(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getTimestamp(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1149,7 +1148,7 @@ public URL getURL(String columnLabel) throws SQLException { try { return new URL(reader.getString(columnLabel)); } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getURL(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getURL(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1320,7 +1319,7 @@ public String getNString(String columnLabel) throws SQLException { try { return reader.getString(columnLabel); } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getNString(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getNString(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1335,7 +1334,7 @@ public Reader getNCharacterStream(String columnLabel) throws SQLException { try { return new StringReader(reader.getString(columnLabel)); } catch (Exception e) { - throw ExceptionUtils.toSqlState(String.format("Method: getNCharacterStream(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastSql()), e); + throw ExceptionUtils.toSqlState(String.format("Method: getNCharacterStream(\"%s\") encountered an exception.", columnLabel), String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1539,7 +1538,7 @@ public T getObject(int columnIndex, Class type) throws SQLException { } catch (Exception e) { throw ExceptionUtils.toSqlState(String.format("Method: getObject(\"%s\", %s) encountered an exception.", reader.getSchema().columnIndexToName(columnIndex), type), - String.format("SQL: [%s]", parentStatement.getLastSql()), e); + String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } @@ -1560,7 +1559,7 @@ public T getObject(String columnLabel, Class type) throws SQLException { } } catch (Exception e) { throw ExceptionUtils.toSqlState(String.format("Method: getObject(\"%s\", %s) encountered an exception.", columnLabel, type), - String.format("SQL: [%s]", parentStatement.getLastSql()), e); + String.format("SQL: [%s]", parentStatement.getLastStatementSql()), e); } } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java index d5a394a89..330b2421b 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/StatementImpl.java @@ -8,19 +8,16 @@ import com.clickhouse.client.api.query.QueryResponse; import com.clickhouse.client.api.query.QuerySettings; import com.clickhouse.jdbc.internal.ExceptionUtils; -import com.clickhouse.jdbc.internal.JdbcUtils; -import com.clickhouse.jdbc.internal.StatementParser; +import com.clickhouse.jdbc.internal.ParsedStatement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -28,17 +25,22 @@ public class StatementImpl implements Statement, JdbcV2Wrapper { private static final Logger LOG = LoggerFactory.getLogger(StatementImpl.class); + // Attributes ConnectionImpl connection; protected int queryTimeout; + protected boolean isPoolable = false; // Statement is not poolable by default + + // State protected boolean closed; protected ResultSetImpl currentResultSet; protected OperationMetrics metrics; protected List batch; - private String lastSql; + private String lastStatementSql; + private ParsedStatement parsedStatement; protected volatile String lastQueryId; - private String schema; private int maxRows; - protected boolean isPoolable = false; // Statement is not poolable by default + protected QuerySettings localSettings; + public StatementImpl(ConnectionImpl connection) throws SQLException { this.connection = connection; this.queryTimeout = 0; @@ -46,8 +48,8 @@ public StatementImpl(ConnectionImpl connection) throws SQLException { this.currentResultSet = null; this.metrics = null; this.batch = new ArrayList<>(); - this.schema = connection.getSchema();// remember DB name this.maxRows = 0; + this.localSettings = QuerySettings.merge(connection.getDefaultQuerySettings(), new QuerySettings()); } protected void checkClosed() throws SQLException { @@ -78,14 +80,14 @@ protected static String parseJdbcEscapeSyntax(String sql) { return sql; } - protected String getLastSql() { - return lastSql; + protected String getLastStatementSql() { + return lastStatementSql; } @Override public ResultSet executeQuery(String sql) throws SQLException { checkClosed(); - return executeQueryImpl(sql, new QuerySettings().setDatabase(schema)); + return executeQueryImpl(sql, localSettings); } private void closePreviousResultSet() { @@ -104,6 +106,9 @@ private void closePreviousResultSet() { protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) throws SQLException { checkClosed(); + + // TODO: method should throw exception if no result set returned + // Closing before trying to do next request. Otherwise, deadlock because previous connection will not be // release before this one completes. closePreviousResultSet(); @@ -121,16 +126,15 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr mergedSettings.setQueryId(lastQueryId); } LOG.debug("Query ID: {}", lastQueryId); - mergedSettings.setDatabase(connection.getSchema()); try { - lastSql = parseJdbcEscapeSyntax(sql); - LOG.debug("SQL Query: {}", lastSql); + lastStatementSql = parseJdbcEscapeSyntax(sql); + LOG.debug("SQL Query: {}", lastStatementSql); QueryResponse response; if (queryTimeout == 0) { - response = connection.client.query(lastSql, mergedSettings).get(); + response = connection.client.query(lastStatementSql, mergedSettings).get(); } else { - response = connection.client.query(lastSql, mergedSettings).get(queryTimeout, TimeUnit.SECONDS); + response = connection.client.query(lastStatementSql, mergedSettings).get(queryTimeout, TimeUnit.SECONDS); } if (response.getFormat().isText()) { @@ -151,17 +155,16 @@ protected ResultSetImpl executeQueryImpl(String sql, QuerySettings settings) thr @Override public int executeUpdate(String sql) throws SQLException { checkClosed(); - return executeUpdateImpl(sql, StatementParser.parsedStatement(sql).getType(), new QuerySettings().setDatabase(schema)); + parsedStatement = connection.getSqlParser().parsedStatement(sql); + int updateCount = executeUpdateImpl(sql, localSettings); + postUpdateActions(); + return updateCount; } - protected int executeUpdateImpl(String sql, StatementParser.StatementType type, QuerySettings settings) throws SQLException { + protected int executeUpdateImpl(String sql, QuerySettings settings) throws SQLException { checkClosed(); - if (type == StatementParser.StatementType.SELECT || type == StatementParser.StatementType.SHOW - || type == StatementParser.StatementType.DESCRIBE || type == StatementParser.StatementType.EXPLAIN) { - throw new SQLException("executeUpdate() cannot be called with a SELECT/SHOW/DESCRIBE/EXPLAIN statement", ExceptionUtils.SQL_STATE_SQL_ERROR); - } - + // TODO: method should throw exception if result set returned // Closing before trying to do next request. Otherwise, deadlock because previous connection will not be // release before this one completes. closePreviousResultSet(); @@ -175,11 +178,11 @@ protected int executeUpdateImpl(String sql, StatementParser.StatementType type, mergedSettings.setQueryId(lastQueryId); } - lastSql = parseJdbcEscapeSyntax(sql); - LOG.debug("SQL Query: {}", lastSql); + lastStatementSql = parseJdbcEscapeSyntax(sql); + LOG.debug("SQL Query: {}", lastStatementSql); int updateCount = 0; - try (QueryResponse response = queryTimeout == 0 ? connection.client.query(lastSql, mergedSettings).get() - : connection.client.query(lastSql, mergedSettings).get(queryTimeout, TimeUnit.SECONDS)) { + try (QueryResponse response = queryTimeout == 0 ? connection.client.query(lastStatementSql, mergedSettings).get() + : connection.client.query(lastStatementSql, mergedSettings).get(queryTimeout, TimeUnit.SECONDS)) { currentResultSet = null; updateCount = Math.max(0, (int) response.getWrittenRows()); // when statement alters schema no result rows returned. metrics = response.getMetrics(); @@ -191,6 +194,17 @@ protected int executeUpdateImpl(String sql, StatementParser.StatementType type, return updateCount; } + protected void postUpdateActions() { + if (parsedStatement.getUseDatabase() != null) { + this.localSettings.setDatabase(parsedStatement.getUseDatabase()); + } + + if (parsedStatement.getRoles() != null) { + this.connection.getClient().setDBRoles(parsedStatement.getRoles()); + this.localSettings.setDBRoles(parsedStatement.getRoles()); + } + } + @Override public void close() throws SQLException { closed = true; @@ -229,6 +243,13 @@ public int getMaxRows() throws SQLException { public void setMaxRows(int max) throws SQLException { checkClosed(); maxRows = max; + if (max > 0) { + localSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.MAX_RESULT_ROWS), maxRows); + localSettings.setOption(ClientConfigProperties.serverSetting(ServerSettings.RESULT_OVERFLOW_MODE), "break"); + } else { + localSettings.resetOption(ClientConfigProperties.serverSetting(ServerSettings.MAX_RESULT_ROWS)); + localSettings.resetOption(ClientConfigProperties.serverSetting(ServerSettings.RESULT_OVERFLOW_MODE)); + } } @Override @@ -283,51 +304,13 @@ public void setCursorName(String name) throws SQLException { @Override public boolean execute(String sql) throws SQLException { checkClosed(); - return executeImpl(sql, StatementParser.parsedStatement(sql).getType(), new QuerySettings().setDatabase(schema)); - } - - public boolean executeImpl(String sql, StatementParser.StatementType type, QuerySettings settings) throws SQLException { - checkClosed(); - if (type == StatementParser.StatementType.SELECT || - type == StatementParser.StatementType.SHOW || - type == StatementParser.StatementType.DESCRIBE || - type == StatementParser.StatementType.EXPLAIN) { - currentResultSet = executeQueryImpl(sql, settings); // keep open to allow getResultSet() + parsedStatement = connection.getSqlParser().parsedStatement(sql); + if (parsedStatement.isHasResultSet()) { + currentResultSet = executeQueryImpl(sql, localSettings); // keep open to allow getResultSet() return true; - } else if(type == StatementParser.StatementType.SET) { - executeUpdateImpl(sql, type, settings); - //SET ROLE - List tokens = JdbcUtils.tokenizeSQL(sql); - if (JdbcUtils.containsIgnoresCase(tokens, "ROLE")) { - List roles = new ArrayList<>(); - int roleIndex = JdbcUtils.indexOfIgnoresCase(tokens, "ROLE"); - if (roleIndex == 1) { - for (int i = 2; i < tokens.size(); i++) { - String token = tokens.get(i); - String[] roleTokens = token.split(","); - for (String roleToken : roleTokens) { - roles.add(roleToken.replace("\"", ""));//Remove double quotes - } - } - - if (JdbcUtils.containsIgnoresCase(roles, "NONE")) { - connection.client.setDBRoles(Collections.emptyList()); - } else { - connection.client.setDBRoles(roles); - } - } - } - return false; - } else if (type == StatementParser.StatementType.USE) { - executeUpdateImpl(sql, type, settings); - //USE Database - List tokens = JdbcUtils.tokenizeSQL(sql); - this.schema = tokens.get(1).replace("\"", ""); - connection.setSchema(schema); - LOG.debug("Changed statement schema to {}", schema); - return false; } else { - executeUpdateImpl(sql, type, settings); + executeUpdateImpl(sql, localSettings); + postUpdateActions(); return false; } } @@ -423,7 +406,7 @@ private List executeBatchImpl() throws SQLException { } @Override - public Connection getConnection() throws SQLException { + public ConnectionImpl getConnection() throws SQLException { return connection; } diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/WriterStatementImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/WriterStatementImpl.java index 6c4d55be5..c910575cf 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/WriterStatementImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/WriterStatementImpl.java @@ -5,9 +5,10 @@ import com.clickhouse.client.api.insert.InsertResponse; import com.clickhouse.client.api.insert.InsertSettings; import com.clickhouse.client.api.metadata.TableSchema; +import com.clickhouse.data.ClickHouseColumn; import com.clickhouse.data.ClickHouseFormat; import com.clickhouse.jdbc.internal.ExceptionUtils; -import com.clickhouse.jdbc.internal.StatementParser; +import com.clickhouse.jdbc.internal.ParsedPreparedStatement; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -29,8 +30,10 @@ import java.sql.SQLXML; import java.sql.Time; import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -45,11 +48,21 @@ public class WriterStatementImpl extends PreparedStatementImpl implements Prepar private ClickHouseBinaryFormatWriter writer; private final TableSchema tableSchema; - public WriterStatementImpl(ConnectionImpl connection, String originalSql, TableSchema tableSchema, StatementParser.ParsedStatement parsedStatement) + public WriterStatementImpl(ConnectionImpl connection, String originalSql, TableSchema tableSchema, + ParsedPreparedStatement parsedStatement) throws SQLException { super(connection, originalSql, parsedStatement); - this.tableSchema = tableSchema; + if (parsedStatement.getInsertColumns() != null) { + List insertColumns = new ArrayList<>(); + for (String column : parsedStatement.getInsertColumns()) { + insertColumns.add(tableSchema.getColumnByName(column)); + } + this.tableSchema = new TableSchema(tableSchema.getTableName(), tableSchema.getQuery(), + tableSchema.getDatabaseName(), insertColumns); + } else { + this.tableSchema = tableSchema; + } try { resetWriter(); } catch (IOException e) { diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java index 6c7a2cdbb..3bc036e82 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcUtils.java @@ -17,14 +17,10 @@ import java.time.OffsetDateTime; import java.time.ZonedDateTime; import java.time.temporal.TemporalAccessor; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; public class JdbcUtils { //Define a map to store the mapping between ClickHouse data types and SQL data types @@ -134,87 +130,6 @@ public static Class convertToJavaClass(ClickHouseDataType clickhouseType) { return DATA_TYPE_CLASS_MAP.get(clickhouseType); } - public static List tokenizeSQL(String sql) { - List tokens = new ArrayList<>(); - - // Remove SQL comments - String withoutComments = sql - .replaceAll("--.*?$", "") // Remove single-line comments - .replaceAll("/\\*.*?\\*/", ""); // Remove multi-line comments - - StringBuilder currentToken = new StringBuilder(); - boolean insideQuotes = false; - - for (int i = 0; i < withoutComments.length(); i++) { - char c = withoutComments.charAt(i); - - if (c == '"') { - insideQuotes = !insideQuotes; // Toggle the insideQuotes flag - currentToken.append(c); // Include the quote in the token - } else if (Character.isWhitespace(c) && !insideQuotes) { - if (currentToken.length() > 0) { - tokens.add(currentToken.toString()); - currentToken.setLength(0); // Clear the current token - } - } else { - currentToken.append(c); - } - } - - // Add the last token if it exists - if (currentToken.length() > 0) { - tokens.add(currentToken.toString()); - } - - return tokens; - } - - public static boolean isBlank(String str) { - return str == null || str.isEmpty() || str.trim().isEmpty(); - } - - public static boolean containsIgnoresCase(List list, String str) { - if (list == null || list.isEmpty() || isBlank(str)) { - return false; - } - - for (String s : list) { - if (s.equalsIgnoreCase(str)) { - return true; - } - } - - return false; - } - - public static int indexOfIgnoresCase(List list, String str) { - if (list == null || list.isEmpty() || isBlank(str)) { - return -1; - } - - for (int i = 0; i < list.size(); i++) { - if (list.get(i).equalsIgnoreCase(str)) { - return i; - } - } - - return -1; - } - - public static String generateSqlTypeSizes(String columnName) { - StringBuilder sql = new StringBuilder("multiIf("); - sql.append("character_octet_length IS NOT NULL, character_octet_length, "); - for (ClickHouseDataType type : ClickHouseDataType.values()) { - if (type.getByteLength() > 0) { - sql.append(columnName).append(" == '").append(type.name()).append("', ").append(type.getByteLength()).append(", "); - } - } - sql.append("numeric_precision IS NOT NULL, numeric_precision, "); - sql.append("0)"); - return sql.toString(); - } - - public static Object convert(Object value, Class type) throws SQLException { if (value == null || type == null) { return value; @@ -271,48 +186,4 @@ public static Object convert(Object value, Class type) throws SQLException { throw new SQLException("Unsupported conversion from " + value.getClass().getName() + " to " + type.getName(), ExceptionUtils.SQL_STATE_DATA_EXCEPTION); } - - public static String escapeQuotes(String str) { - if (str == null || str.isEmpty()) { - return str; - } - return str - .replace("'", "\\'") - .replace("\"", "\\\""); - } - - private static Pattern UNQUOTE_TABLE_NAME = Pattern.compile( - "^[\\\"`]?(.+?)[\\\"`]?$" - ); - - public static String unQuoteTableName(String str) { - Matcher matcher = UNQUOTE_TABLE_NAME.matcher(str.trim()); - if (matcher.find()) { - return matcher.group(1); - } else { - return str; - } - } - - public static final String NULL = "NULL"; - - private static final Pattern REPLACE_Q_MARK_PATTERN = Pattern.compile("(\"[^\"]*\"|`[^`]*`|'[^']*')|(\\?)"); - - public static String replaceQuestionMarks(String sql, String replacement) { - Matcher matcher = REPLACE_Q_MARK_PATTERN.matcher(sql); - - StringBuilder result = new StringBuilder(); - - while (matcher.find()) { - if (matcher.group(1) != null) { - // Quoted string — keep as-is - matcher.appendReplacement(result, Matcher.quoteReplacement(matcher.group(1))); - } else if (matcher.group(2) != null) { - // Question mark outside quotes — replace it - matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); - } - } - matcher.appendTail(result); - return result.toString(); - } } 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 new file mode 100644 index 000000000..99ad33f84 --- /dev/null +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedPreparedStatement.java @@ -0,0 +1,235 @@ +package com.clickhouse.jdbc.internal; + +import com.clickhouse.jdbc.PreparedStatementImpl; +import org.antlr.v4.runtime.tree.ErrorNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Parser listener that collects information for prepared statement. + */ +public class ParsedPreparedStatement extends ClickHouseParserBaseListener { + private static final Logger LOG = LoggerFactory.getLogger(ParsedPreparedStatement.class); + + private String table; + + private String useDatabase; + + private String[] insertColumns; + + private boolean useFunction; + + private boolean hasErrors; + + private boolean hasResultSet; + + private boolean insert; + + private boolean insertWithSelect; + + private List roles; + + private int argCount; + + private int[] paramPositions = new int[16]; + + private int assignValuesListStartPosition = -1; + + private int assignValuesListStopPosition = -1; + + private int assignValuesGroups = -1; + + public void setHasResultSet(boolean hasResultSet) { + this.hasResultSet = hasResultSet; + } + + public boolean isHasResultSet() { + return hasResultSet; + } + + public void setInsert(boolean insert) { + this.insert = insert; + } + + public boolean isInsert() { + return insert; + } + + public void setInsertWithSelect(boolean insertWithSelect) { + this.insertWithSelect = insertWithSelect; + } + + public boolean isInsertWithSelect() { + return insertWithSelect; + } + + public int getArgCount() { + return argCount; + } + + public String[] getInsertColumns() { + return insertColumns; + } + + public String getTable() { + return table; + } + + public int[] getParamPositions() { + return paramPositions; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public List getRoles() { + return roles; + } + + public int getAssignValuesListStartPosition() { + return assignValuesListStartPosition; + } + + public int getAssignValuesListStopPosition() { + return assignValuesListStopPosition; + } + + public void setUseDatabase(String useDatabase) { + this.useDatabase = useDatabase; + } + + public String getUseDatabase() { + return useDatabase; + } + + public void setAssignValuesGroups(int assignValuesGroups) { + this.assignValuesGroups = assignValuesGroups; + } + + public int getAssignValuesGroups() { + return assignValuesGroups; + } + + public boolean isUseFunction() { + return useFunction; + } + + public void setUseFunction(boolean useFunction) { + this.useFunction = useFunction; + } + + public boolean isHasErrors() { + return hasErrors; + } + + public void setHasErrors(boolean hasErrors) { + this.hasErrors = hasErrors; + } + + @Override + public void enterQueryStmt(ClickHouseParser.QueryStmtContext ctx) { + ClickHouseParser.QueryContext qCtx = ctx.query(); + if (qCtx != null) { + if (qCtx.selectStmt() != null || qCtx.selectUnionStmt() != null || qCtx.showStmt() != null || qCtx.describeStmt() != null) { + setHasResultSet(true); + } + } + } + + @Override + public void enterUseStmt(ClickHouseParser.UseStmtContext ctx) { + if (ctx.databaseIdentifier() != null) { + setUseDatabase(SqlParser.unquoteIdentifier(ctx.databaseIdentifier().getText())); + } + } + + @Override + public void enterSetRoleStmt(ClickHouseParser.SetRoleStmtContext ctx) { + if (ctx.NONE() != null) { + setRoles(Collections.emptyList()); + } else { + List roles = new ArrayList<>(); + for (ClickHouseParser.IdentifierContext id : ctx.setRolesList().identifier()) { + roles.add(SqlParser.unquoteIdentifier(id.getText())); + } + setRoles(roles); + } + } + + @Override + public void enterColumnExprParam(ClickHouseParser.ColumnExprParamContext ctx) { + appendParameter(ctx.start.getStartIndex()); + } + + @Override + public void visitErrorNode(ErrorNode node) { + setHasErrors(true); + } + + @Override + public void enterInsertParameterFuncExpr(ClickHouseParser.InsertParameterFuncExprContext ctx) { + setUseFunction(true); + } + + @Override + public void enterAssignmentValuesList(ClickHouseParser.AssignmentValuesListContext ctx) { + assignValuesListStartPosition = ctx.getStart().getStartIndex(); + assignValuesListStopPosition = ctx.getStop().getStopIndex(); + } + + @Override + public void enterInsertParameter(ClickHouseParser.InsertParameterContext ctx) { + appendParameter(ctx.start.getStartIndex()); + } + + private void appendParameter(int startIndex) { + argCount++; + if (argCount > paramPositions.length) { + paramPositions = Arrays.copyOf(paramPositions, paramPositions.length + 10); + } + paramPositions[argCount - 1] = startIndex; + if (LOG.isTraceEnabled()) { + LOG.trace("parameter position {}", startIndex); + } + } + + @Override + public void enterInsertStmt(ClickHouseParser.InsertStmtContext ctx) { + ClickHouseParser.TableIdentifierContext tableId = ctx.tableIdentifier(); + if (tableId != null) { + this.table = tableId.identifier().IDENTIFIER().getText(); + } + + ClickHouseParser.ColumnsClauseContext columns = ctx.columnsClause(); + if (columns != null) { + List names = columns.nestedIdentifier(); + this.insertColumns = new String[names.size()]; + for (int i = 0; i < names.size(); i++) { + this.insertColumns[i] = names.get(i).getText(); + } + } + + setInsert(true); + } + + @Override + public void enterDataClauseSelect(ClickHouseParser.DataClauseSelectContext ctx) { + setInsertWithSelect(true); + } + + @Override + public void enterDataClauseValues(ClickHouseParser.DataClauseValuesContext ctx) { + setAssignValuesGroups(ctx.assignmentValues().size()); + } + + @Override + public void exitInsertParameterFuncExpr(ClickHouseParser.InsertParameterFuncExprContext ctx) { + setUseFunction(true); + } +} 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 new file mode 100644 index 000000000..709fda0ca --- /dev/null +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/ParsedStatement.java @@ -0,0 +1,106 @@ +package com.clickhouse.jdbc.internal; + +import org.antlr.v4.runtime.tree.ErrorNode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ParsedStatement extends ClickHouseParserBaseListener { + + private String useDatabase; + + private boolean hasResultSet; + + private boolean insert; + + private String insertTableId; + + private List roles; + + private boolean hasErrors; + + public void setUseDatabase(String useDatabase) { + this.useDatabase = useDatabase; + } + + public void setHasResultSet(boolean hasResultSet) { + this.hasResultSet = hasResultSet; + } + + public boolean isHasResultSet() { + return hasResultSet; + } + + public void setInsert(boolean insert) { + this.insert = insert; + } + + public boolean isInsert() { + return insert; + } + + public void setInsertTableId(String insertTableId) { + this.insertTableId = insertTableId; + } + + public String getInsertTableId() { + return insertTableId; + } + + public String getUseDatabase() { + return useDatabase; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public List getRoles() { + return roles; + } + + public boolean isHasErrors() { + return hasErrors; + } + + public void setHasErrors(boolean hasErrors) { + this.hasErrors = hasErrors; + } + + @Override + public void visitErrorNode(ErrorNode node) { + setHasErrors(true); + } + + @Override + public void enterQueryStmt(ClickHouseParser.QueryStmtContext ctx) { + ClickHouseParser.QueryContext qCtx = ctx.query(); + if (qCtx != null) { + if (qCtx.selectStmt() != null || qCtx.selectUnionStmt() != null || qCtx.showStmt() != null + || qCtx.describeStmt() != null) { + setHasResultSet(true); + } + } + } + + @Override + public void enterUseStmt(ClickHouseParser.UseStmtContext ctx) { + if (ctx.databaseIdentifier() != null) { + setUseDatabase(SqlParser.unquoteIdentifier(ctx.databaseIdentifier().getText())); + } + } + + @Override + public void enterSetRoleStmt(ClickHouseParser.SetRoleStmtContext ctx) { + if (ctx.NONE() != null) { + setRoles(Collections.emptyList()); + } else { + List roles = new ArrayList<>(); + for (ClickHouseParser.IdentifierContext id : ctx.setRolesList().identifier()) { + roles.add(SqlParser.unquoteIdentifier(id.getText())); + } + setRoles(roles); + } + } +} diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java new file mode 100644 index 000000000..620ff5b4d --- /dev/null +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/SqlParser.java @@ -0,0 +1,57 @@ +package com.clickhouse.jdbc.internal; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.IterativeParseTreeWalker; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class SqlParser { + + public ParsedStatement parsedStatement(String sql) { + + CharStream charStream = CharStreams.fromString(sql); + ClickHouseLexer lexer = new ClickHouseLexer(charStream); + ClickHouseParser parser = new ClickHouseParser(new CommonTokenStream(lexer)); + ClickHouseParser.QueryStmtContext parseTree = parser.queryStmt(); + ParsedStatement parserListener = new ParsedStatement(); + IterativeParseTreeWalker.DEFAULT.walk(parserListener, parseTree); + + + return parserListener; + } + + public ParsedPreparedStatement parsePreparedStatement(String sql) { + CharStream charStream = CharStreams.fromString(sql); + ClickHouseLexer lexer = new ClickHouseLexer(charStream); + ClickHouseParser parser = new ClickHouseParser(new CommonTokenStream(lexer)); + ClickHouseParser.QueryStmtContext parseTree = parser.queryStmt(); + ParsedPreparedStatement parserListener = new ParsedPreparedStatement(); + IterativeParseTreeWalker.DEFAULT.walk(parserListener, parseTree); + return parserListener; + } + + private final static Pattern UNQUOTE_INDENTIFIER = Pattern.compile( + "^[\\\"`]?(.+?)[\\\"`]?$" + ); + + public static String unquoteIdentifier(String str) { + Matcher matcher = UNQUOTE_INDENTIFIER.matcher(str.trim()); + if (matcher.find()) { + return matcher.group(1); + } else { + return str; + } + } + + public static String escapeQuotes(String str) { + if (str == null || str.isEmpty()) { + return str; + } + return str + .replace("'", "\\'") + .replace("\"", "\\\""); + } +} diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/StatementParser.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/StatementParser.java deleted file mode 100644 index 6da1ba403..000000000 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/StatementParser.java +++ /dev/null @@ -1,265 +0,0 @@ -package com.clickhouse.jdbc.internal; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -public class StatementParser { - - - public enum StatementType { - SELECT, INSERT, DELETE, UPDATE, CREATE, DROP, ALTER, TRUNCATE, USE, SHOW, DESCRIBE, EXPLAIN, SET, KILL, OTHER, INSERT_INTO_SELECT - } - - public static ParsedStatement parsedStatement(String sql) { - return parseStatementType(sql); - } - - public static ParsedStatement parsePreparedStatement(String sql) { - ParsedStatement parsedStatement = parseStatementType(sql); - String[] sqlSegments = splitStatement(sql); - parsedStatement.setSqlSegments(sqlSegments); - return parsedStatement; - } - - private static final Pattern BLOCK_COMMENT = Pattern.compile("/\\*.*?\\*/", Pattern.DOTALL); - - static ParsedStatement parseStatementType(String sql) { - String trimmedSql = sql == null ? "" : sql.trim(); - if (trimmedSql.isEmpty()) { - return new ParsedStatement(StatementType.OTHER, ""); - } - - trimmedSql = BLOCK_COMMENT.matcher(trimmedSql).replaceAll("").trim(); // remove comments - String[] lines = trimmedSql.split("\n"); - for (int i = 0; i < lines.length; i++) { - String trimmedLine = lines[i].trim(); - //https://clickhouse.com/docs/en/sql-reference/syntax#comments - if (!trimmedLine.startsWith("--") && !trimmedLine.startsWith("#!") && !trimmedLine.startsWith("#")) { - String[] tokens = trimmedLine.split("\\s+"); - if (tokens.length == 0) { - continue; - } - switch (tokens[0].toUpperCase()) { - case "SELECT": - return new ParsedStatement(StatementType.SELECT, trimmedSql); - case "WITH": - return new ParsedStatement(StatementType.SELECT, trimmedSql); - case "INSERT": - // TODO: it is not optimal to re-parse current line - boolean hasSelect = false; - boolean prevWasInto = false; - boolean prevWasTable = false; - boolean hasValues = false; - boolean hasColumnList = false; - String tableName = ""; - for (int j = i; j < lines.length; j++) { - trimmedLine = lines[j].trim(); - if (!trimmedLine.startsWith("--") && !trimmedLine.startsWith("#!") && !trimmedLine.startsWith("#")) { - tokens = trimmedLine.split("\\s+"); - if (tokens.length == 0) { - continue; - } - } - for (String token : tokens) { - if (!hasColumnList && !hasValues && token.contains("(")) { - hasColumnList = true; - } - if (token.equalsIgnoreCase("SELECT")) { - hasSelect = true; - break; // should be after we have found everything useful - } else if (token.equalsIgnoreCase("INTO")) { - prevWasInto = true; - } else if (token.equalsIgnoreCase("TABLE")) { - prevWasTable = true; - } else if (tableName.isEmpty() && (prevWasTable || prevWasInto)) { - tableName = extractTableNameFromSegment(token); - } else if (token.equalsIgnoreCase("VALUES")) { - hasValues = true; - } - } - } - ParsedStatement parsedStatement = - new ParsedStatement(hasSelect ? StatementType.INSERT_INTO_SELECT : StatementType.INSERT, trimmedSql); - parsedStatement.setTableName(tableName); - parsedStatement.setHasColumnList(hasColumnList); - return parsedStatement; - case "DELETE": - return new ParsedStatement(StatementType.DELETE, trimmedSql); - case "UPDATE": - return new ParsedStatement(StatementType.UPDATE, trimmedSql); - case "CREATE": - return new ParsedStatement(StatementType.CREATE, trimmedSql); - case "DROP": - return new ParsedStatement(StatementType.DROP, trimmedSql); - case "ALTER": - return new ParsedStatement(StatementType.ALTER, trimmedSql); - case "TRUNCATE": - return new ParsedStatement(StatementType.TRUNCATE, trimmedSql); - case "USE": - return new ParsedStatement(StatementType.USE, trimmedSql); - case "SHOW": - return new ParsedStatement(StatementType.SHOW, trimmedSql); - case "DESCRIBE": - return new ParsedStatement(StatementType.DESCRIBE, trimmedSql); - case "EXPLAIN": - return new ParsedStatement(StatementType.EXPLAIN, trimmedSql); - case "SET": - return new ParsedStatement(StatementType.SET, trimmedSql); - case "KILL": - return new ParsedStatement(StatementType.KILL, trimmedSql); - default: - return new ParsedStatement(StatementType.OTHER, trimmedSql); - } - } - } - - return new ParsedStatement(StatementType.OTHER, trimmedSql); - } - - - // DOT NOT USE: part of a parsing algorithm. - static String extractTableNameFromSegment(String segment) { - StringBuilder tableNameBuilder = new StringBuilder(); - char openedQuote = 0; - boolean escaping = false; - - for (char ch : segment.toCharArray()) { - if (escaping) { - tableNameBuilder.append(ch); - escaping = false; - continue; - } - if (openedQuote == ch) { - break; - } - switch (ch) { - case ' ': - continue; - case '\\': - escaping = true; - tableNameBuilder.append(ch); - break; - case '"': - case '`': - openedQuote = ch; - continue; - default: - tableNameBuilder.append(ch); - } - } - return tableNameBuilder.toString(); - } - - public static class ParsedStatement { - private final StatementType type; - private final String trimmedSql; - private String tableName; - private String[] sqlSegments; - private int argumentCount; - private boolean hasColumnList; - - ParsedStatement(StatementType type, String trimmedSql) { - this.type = type; - this.trimmedSql = trimmedSql; - } - - void setTableName(String tableName) { - this.tableName = tableName; - } - - public void setSqlSegments(String[] sqlSegments) { - this.sqlSegments = sqlSegments; - this.argumentCount = sqlSegments == null ? 0 : sqlSegments.length -1; - } - - public String[] getSqlSegments() { - return this.sqlSegments; - } - - public StatementType getType() { - return this.type; - } - - public String getTableName() { - return tableName; - } - - public int getArgumentCount() { - return argumentCount; - } - - public void setHasColumnList(boolean hasColumnList) { - this.hasColumnList = hasColumnList; - } - - public boolean hasColumnList() { - return hasColumnList; - } - } - - private static String[] splitStatement(String sql) { - List segments = new ArrayList<>(); - char[] chars = sql.toCharArray(); - int segmentStart = 0; - for (int i = 0; i < chars.length; i++) { - char c = chars[i]; - if (c == '\'' || c == '"' || c == '`') { - // string literal or identifier - i = skip(chars, i + 1, c, true); - } else if (c == '/' && lookahead(chars, i) == '*') { - // block comment - int end = sql.indexOf("*/", i); - if (end == -1) { - // missing comment end - break; - } - i = end + 1; - } else if (c == '#' || (c == '-' && lookahead(chars, i) == '-')) { - // line comment - i = skip(chars, i + 1, '\n', false); - } else if (c == '?') { - // question mark - segments.add(sql.substring(segmentStart, i)); - segmentStart = i + 1; - } - } - if (segmentStart < chars.length) { - segments.add(sql.substring(segmentStart)); - } else { - // add empty segment in case question mark was last char of sql - segments.add(""); - } - return segments.toArray(new String[0]); - } - - private static int skip(char[] chars, int from, char until, boolean escape) { - for (int i = from; i < chars.length; i++) { - char curr = chars[i]; - if (escape) { - char next = lookahead(chars, i); - if ((curr == '\\' && (next == '\\' || next == until)) || (curr == until && next == until)) { - // should skip: - // 1) double \\ (backslash escaped with backslash) - // 2) \[until] ([until] char, escaped with backslash) - // 3) [until][until] ([until] char, escaped with [until]) - i++; - continue; - } - } - - if (curr == until) { - return i; - } - } - return chars.length; - } - - private static char lookahead(char[] chars, int pos) { - pos = pos + 1; - if (pos >= chars.length) { - return '\0'; - } - return chars[pos]; - } -} diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java index 17c48f58c..ebfe37d50 100644 --- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java +++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/metadata/DatabaseMetaDataImpl.java @@ -11,6 +11,7 @@ import com.clickhouse.jdbc.internal.ExceptionUtils; import com.clickhouse.jdbc.internal.JdbcUtils; import com.clickhouse.jdbc.internal.MetadataResultSet; +import com.clickhouse.jdbc.internal.SqlParser; import com.clickhouse.logging.Logger; import com.clickhouse.logging.LoggerFactory; @@ -839,7 +840,7 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa "name AS COLUMN_NAME, " + "system.columns.type AS DATA_TYPE, " + "type AS TYPE_NAME, " + - JdbcUtils.generateSqlTypeSizes("system.columns.type") + " AS COLUMN_SIZE, " + + generateSqlTypeSizes("system.columns.type") + " AS COLUMN_SIZE, " + "toInt32(0) AS BUFFER_LENGTH, " + "IF (numeric_scale == 0, NULL, numeric_scale) as DECIMAL_DIGITS, " + "toInt32(numeric_precision_radix) AS NUM_PREC_RADIX, " + @@ -858,9 +859,9 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa "'NO' as IS_AUTOINCREMENT, " + "'NO' as IS_GENERATEDCOLUMN " + " FROM system.columns" + - " WHERE database LIKE '" + (schemaPattern == null ? "%" : JdbcUtils.escapeQuotes(schemaPattern)) + "'" + - " AND table LIKE '" + (tableNamePattern == null ? "%" : JdbcUtils.escapeQuotes(tableNamePattern)) + "'" + - " AND name LIKE '" + (columnNamePattern == null ? "%" : JdbcUtils.escapeQuotes(columnNamePattern)) + "'" + + " WHERE database LIKE '" + (schemaPattern == null ? "%" : SqlParser.escapeQuotes(schemaPattern)) + "'" + + " AND table LIKE '" + (tableNamePattern == null ? "%" : SqlParser.escapeQuotes(tableNamePattern)) + "'" + + " AND name LIKE '" + (columnNamePattern == null ? "%" : SqlParser.escapeQuotes(columnNamePattern)) + "'" + " ORDER BY TABLE_SCHEM, TABLE_NAME, ORDINAL_POSITION"; try { return new MetadataResultSet((ResultSetImpl) connection.createStatement().executeQuery(sql)) @@ -870,6 +871,19 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa } } + private static String generateSqlTypeSizes(String columnName) { + StringBuilder sql = new StringBuilder("multiIf("); + sql.append("character_octet_length IS NOT NULL, character_octet_length, "); + for (ClickHouseDataType type : ClickHouseDataType.values()) { + if (type.getByteLength() > 0) { + sql.append(columnName).append(" == '").append(type.name()).append("', ").append(type.getByteLength()).append(", "); + } + } + sql.append("numeric_precision IS NOT NULL, numeric_precision, "); + sql.append("0)"); + return sql.toString(); + } + private static String columnDataTypeToSqlType(String value) { SQLType type = JdbcUtils.CLICKHOUSE_TYPE_NAME_TO_SQL_TYPE_MAP.get(value); if (type == null) { diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java index aaa07b677..470f95bba 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/DataTypeTests.java @@ -28,6 +28,7 @@ import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Map; 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 f9063a88c..2360ea9de 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/PreparedStatementTest.java @@ -1,5 +1,6 @@ package com.clickhouse.jdbc; +import com.clickhouse.data.ClickHouseVersion; import com.clickhouse.jdbc.internal.DriverProperties; import org.apache.commons.lang3.RandomStringUtils; import org.testng.Assert; @@ -9,6 +10,7 @@ import java.sql.Array; import java.sql.Connection; +import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; @@ -16,6 +18,7 @@ import java.sql.Statement; import java.sql.Timestamp; import java.sql.Types; +import java.time.LocalDate; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; @@ -30,6 +33,7 @@ import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertTrue; public class PreparedStatementTest extends JdbcIntegrationTest { @@ -270,6 +274,21 @@ public void testEscapeStrings() throws Exception { } } + @Test(groups = { "integration" }) + public void testTernaryOperator() throws Exception { + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("SELECT ( TRUE ? 1 : 0) as val1, ? as val2")) { + stmt.setString(1, "test\\' OR 1 = 1 --"); + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + assertEquals(rs.getString("val1"), "1"); + assertEquals(rs.getString(2), "test\\' OR 1 = 1 --"); + assertFalse(rs.next()); + } + } + } + } + @Test(groups = "integration") void testWithClause() throws Exception { @@ -591,8 +610,18 @@ void testClearParameters() throws Exception { } } - @Test - void testBatchInsert() 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); @@ -606,52 +635,97 @@ void testBatchInsert() throws Exception { " ( ts DateTime, v1 Int32, v2 Float32, v3 Int32) Engine MergeTree ORDER BY ()"); } - String[] sql = new String[]{ - "INSERT INTO \n `%s` \nVALUES (?, ?, multiply(?, 10), ?)", // only string possible - "INSERT INTO\n `%s` \nVALUES (?, ?, ?, ?)", // row binary writer - " INSERT INTO %s (ts, v1, v2, v3) VALUES (?, ?, ?, ?)", // only string supported now - }; - Class[] impl = new Class[]{ - PreparedStatementImpl.class, - WriterStatementImpl.class, - PreparedStatementImpl.class - }; + 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(); + } - for (int i = 0; i < sql.length; i++) { - final int nBatches = 10; - try (PreparedStatement stmt = conn.prepareStatement(String.format(sql[i], table))) { - Assert.assertEquals(stmt.getClass(), impl[i]); - 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.assertEquals(r, 1); + } + } - int[] result = stmt.executeBatch(); - for (int r : result) { - Assert.assertEquals(r, 1); - } + 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); - try (Statement stmt = conn.createStatement(); - ResultSet rs = stmt.executeQuery("SELECT * FROM " + table);) { + stmt.execute("TRUNCATE " + 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); + @DataProvider + Object[][] testBatchInsertTextStatement_dp() { + return new Object[][]{ + {"INSERT INTO \n `%s` \nVALUES (?, ?, ?, ?)"}, // simple + {" INSERT INTO %s (ts, v1, v2, v3) VALUES (?, ?, ?, ?)"}, + }; + } + + @Test(dataProvider = "testBatchInsertTextStatement_dp") + void testBatchInsertTextStatement(String sql) throws Exception { + String table = "test_batch_text"; + long seed = System.currentTimeMillis(); + Random rnd = new Random(seed); + System.out.println("testBatchInsert seed" + seed); + try (Connection conn = getJdbcConnection()) { + + try (Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE IF NOT EXISTS " + table + + " ( ts DateTime DEFAULT now(), 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(), PreparedStatementImpl.class); + 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.assertEquals(r, 1); + } + } - stmt.execute("TRUNCATE " + table); + 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); } + } } @@ -739,4 +813,128 @@ void testMethodsNotAllowedToBeCalled() throws Exception { Assert.assertThrows(SQLException.class, () -> ps.executeLargeUpdate(sql, new String[]{""})); } } + + @Test(dataProvider = "testReplaceQuestionMark_dataProvider") + public void testReplaceQuestionMark(String sql, String result) { + assertEquals(PreparedStatementImpl.replaceQuestionMarks(sql, "NULL"), result); + } + + @DataProvider(name = "testReplaceQuestionMark_dataProvider") + public static Object[][] testReplaceQuestionMark_dataProvider() { + return new Object[][] { + {"", ""}, + {" ", " "}, + {"SELECT * FROM t WHERE a = '?'", "SELECT * FROM t WHERE a = '?'"}, + {"SELECT `v2?` FROM t WHERE `v1?` = ?", "SELECT `v2?` FROM t WHERE `v1?` = NULL"}, + {"INSERT INTO \"t2?\" VALUES (?, ?, 'some_?', ?)", "INSERT INTO \"t2?\" VALUES (NULL, NULL, 'some_?', NULL)"} + }; + } + + @Test(groups = { "integration" }) + public void testJdbcEscapeSyntax() throws Exception { + if (ClickHouseVersion.of(getServerVersion()).check("(,23.8]")) { + return; // there is no `timestamp` function TODO: fix in JDBC + } + try (Connection conn = getJdbcConnection()) { + try (PreparedStatement stmt = conn.prepareStatement("SELECT {d '2021-11-01'} AS D, {ts '2021-08-01 12:34:56'} AS TS, " + + "toInt32({fn ABS(-1)}) AS FNABS, {fn CONCAT('Hello', 'World')} AS FNCONCAT, {fn UCASE('hello')} AS FNUPPER, " + + "{fn LCASE('HELLO')} AS FNLOWER, {fn LTRIM(' Hello ')} AS FNLTRIM, {fn RTRIM(' Hello ')} AS FNRTRIM, " + + "toInt32({fn LENGTH('Hello')}) AS FNLENGTH, toInt32({fn POSITION('Hello', 'l')}) AS FNPOSITION, toInt32({fn MOD(10, 3)}) AS FNMOD, " + + "{fn SQRT(9)} AS FNSQRT, {fn SUBSTRING('Hello', 3, 2)} AS FNSUBSTRING")) { + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + assertEquals(rs.getDate(1), Date.valueOf(LocalDate.of(2021, 11, 1))); + //assertEquals(rs.getTimestamp(2), java.sql.Timestamp.valueOf(LocalDateTime.of(2021, 11, 1, 12, 34, 56))); + assertEquals(rs.getInt(3), 1); + assertEquals(rs.getInt("FNABS"), 1); + assertEquals(rs.getString(4), "HelloWorld"); + assertEquals(rs.getString("FNCONCAT"), "HelloWorld"); + assertEquals(rs.getString(5), "HELLO"); + assertEquals(rs.getString("FNUPPER"), "HELLO"); + assertEquals(rs.getString(6), "hello"); + assertEquals(rs.getString("FNLOWER"), "hello"); + assertEquals(rs.getString(7), "Hello "); + assertEquals(rs.getString("FNLTRIM"), "Hello "); + assertEquals(rs.getString(8), " Hello"); + assertEquals(rs.getString("FNRTRIM"), " Hello"); + assertEquals(rs.getInt(9), 5); + assertEquals(rs.getInt("FNLENGTH"), 5); + assertEquals(rs.getInt(10), 3); + assertEquals(rs.getInt("FNPOSITION"), 3); + assertEquals(rs.getInt(11), 1); + assertEquals(rs.getInt("FNMOD"), 1); + assertEquals(rs.getDouble(12), 3); + assertEquals(rs.getDouble("FNSQRT"), 3); + assertEquals(rs.getString(13), "ll"); + assertEquals(rs.getString("FNSUBSTRING"), "ll"); + assertThrows(SQLException.class, () -> rs.getString(14)); + assertFalse(rs.next()); + } + } + } + } + + @Test(groups = {"integration "}) + public void testStatementsWithDatabaseInTableIdentifier() throws Exception { + try (Connection conn = getJdbcConnection()) { + final String db1Name = conn.getSchema() + "_db1"; + final String table1Name = "table1"; + try (Statement stmt = conn.createStatement()) { + stmt.execute("CREATE DATABASE IF NOT EXISTS " + db1Name); + stmt.execute("DROP TABLE IF EXISTS " + db1Name + "." + table1Name); + stmt.execute("CREATE TABLE " + db1Name + "." + table1Name + + "(v1 Int32, v2 Int32) Engine MergeTree ORDER BY ()"); + } + + String[] tableIdentifier = new String[]{ + db1Name + "." + table1Name, + "`" + db1Name + "`.`" + table1Name + "`", + "\"" + db1Name + "\".\"" + table1Name + "\"" + }; + + for (int i = 0; i < tableIdentifier.length; i++) { + String tableId = tableIdentifier[i]; + System.out.println(">> " + tableId); + final String insertStmt = "INSERT INTO " + tableId + " VALUES (?, ?)"; + try (PreparedStatement stmt = conn.prepareStatement(insertStmt)) { + stmt.setInt(1, i + 10); + stmt.setInt(2, i + 20); + assertEquals(stmt.executeUpdate(), 1); + } + } + } + } + + @Test(groups = {"integration "}) + public void testNullValues() throws Exception { + try (Connection conn = getJdbcConnection()) { + final String table = "test_null_values"; + try (Statement stmt = conn.createStatement()) { + stmt.execute("DROP TABLE IF EXISTS " + table); + stmt.execute("CREATE TABLE " + table + + "(v1 Int32, v2 Nullable(Int32)) Engine MergeTree ORDER BY ()"); + } + + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO " + table + " VALUES (?, ?)")) { + stmt.setInt(1, 10); + // do not set second value + assertEquals(stmt.executeUpdate(), 1); + stmt.setInt(1, 20); + stmt.setObject(2, null); + assertEquals(stmt.executeUpdate(), 1); + } + + try (Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM " + table)) { + + int count = 0; + while(rs.next()) { + count++; + assertNull(rs.getObject(2)); + } + + assertEquals(count, 2); + } + } + } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java index 70da50fe5..058df6eb4 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/StatementTest.java @@ -535,16 +535,19 @@ public void testSwitchDatabase() throws Exception { assertFalse(stmt.execute("USE \"" + databaseName + "\"")); assertEquals(stmt.executeUpdate(createSql), 0); } - conn.createStatement().execute("USE system"); - ResultSet rs = conn.createStatement().executeQuery("SELECT name FROM settings LIMIT 1;"); - assertTrue(rs.next()); - assertNotNull(rs.getString(1)); - assertFalse(rs.next()); - conn.createStatement().execute("USE \"" + databaseName + "\""); - rs = conn.createStatement().executeQuery("SHOW TABLES LIMIT 1"); - assertTrue(rs.next()); - assertEquals(rs.getString(1), "switchDatabaseWithUse"); - assertFalse(rs.next()); + + try (Statement stmt = conn.createStatement()) { + stmt.execute("USE system"); + ResultSet rs = stmt.executeQuery("SELECT name FROM settings LIMIT 1;"); + assertTrue(rs.next()); + assertNotNull(rs.getString(1)); + assertFalse(rs.next()); + stmt.execute("USE \"" + databaseName + "\""); + rs = stmt.executeQuery("SHOW TABLES LIMIT 1"); + assertTrue(rs.next()); + assertEquals(rs.getString(1), "switchDatabaseWithUse"); + assertFalse(rs.next()); + } } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java index 06601c2e6..b083135c3 100644 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcUtilsTest.java @@ -1,87 +1,5 @@ package com.clickhouse.jdbc.internal; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.util.List; - -import static org.testng.Assert.assertEquals; - public class JdbcUtilsTest { - @Test(groups = { "integration" }) - public void testTokenizeSQL() { - String sql1 = "SELECT * FROM table WHERE id = 1"; - List tokens1 = JdbcUtils.tokenizeSQL(sql1); - assertEquals(tokens1.size(), 8); - assertEquals(tokens1.get(0), "SELECT"); - assertEquals(tokens1.get(1), "*"); - assertEquals(tokens1.get(2), "FROM"); - assertEquals(tokens1.get(3), "table"); - assertEquals(tokens1.get(4), "WHERE"); - assertEquals(tokens1.get(5), "id"); - assertEquals(tokens1.get(6), "="); - assertEquals(tokens1.get(7), "1"); - - String sql2 = "SELECT * FROM table WHERE id = 1 AND name = 'John'"; - List tokens2 = JdbcUtils.tokenizeSQL(sql2); - assertEquals(tokens2.size(), 12); - assertEquals(tokens2.get(0), "SELECT"); - assertEquals(tokens2.get(1), "*"); - assertEquals(tokens2.get(2), "FROM"); - assertEquals(tokens2.get(3), "table"); - assertEquals(tokens2.get(4), "WHERE"); - assertEquals(tokens2.get(5), "id"); - assertEquals(tokens2.get(6), "="); - assertEquals(tokens2.get(7), "1"); - assertEquals(tokens2.get(8), "AND"); - assertEquals(tokens2.get(9), "name"); - assertEquals(tokens2.get(10), "="); - assertEquals(tokens2.get(11), "'John'"); - - String sql3 = "SELECT * FROM table WHERE \"id = 1 AND name = 'John' OR age = 30\"";//Technically, this is not a valid SQL statement - List tokens3 = JdbcUtils.tokenizeSQL(sql3); - assertEquals(tokens3.size(), 6); - assertEquals(tokens3.get(0), "SELECT"); - assertEquals(tokens3.get(1), "*"); - assertEquals(tokens3.get(2), "FROM"); - assertEquals(tokens3.get(3), "table"); - assertEquals(tokens3.get(4), "WHERE"); - assertEquals(tokens3.get(5).replace("\"", ""), "id = 1 AND name = 'John' OR age = 30"); - } - - @Test - public void testEscapeQuotes() { - String[] inStr = new String[]{"%valid_name%", "' OR 1=1 --", "\" OR 1=1 --"}; - String[] outStr = new String[]{"%valid_name%", "\\' OR 1=1 --", "\\\" OR 1=1 --"}; - - for (int i = 0; i < inStr.length; i++) { - assertEquals(JdbcUtils.escapeQuotes(inStr[i]), outStr[i]); - } - } - - @Test - public void testUnQuoteTableName() { - String[] names = new String[]{"test", "`test name1`", "\"test name 2\""}; - String[] expected = new String[]{"test", "test name1", "test name 2"}; - - for (int i = 0; i < names.length; i++) { - assertEquals(JdbcUtils.unQuoteTableName(names[i]), expected[i]); - } - } - - @Test(dataProvider = "testReplaceQuestionMark_dataProvider") - public void testReplaceQuestionMark(String sql, String result) { - assertEquals(JdbcUtils.replaceQuestionMarks(sql, "NULL"), result); - } - @DataProvider(name = "testReplaceQuestionMark_dataProvider") - public static Object[][] testReplaceQuestionMark_dataProvider() { - return new Object[][] { - {"", ""}, - {" ", " "}, - {"SELECT * FROM t WHERE a = '?'", "SELECT * FROM t WHERE a = '?'"}, - {"SELECT `v2?` FROM t WHERE `v1?` = ?", "SELECT `v2?` FROM t WHERE `v1?` = NULL"}, - {"INSERT INTO \"t2?\" VALUES (?, ?, 'some_?', ?)", "INSERT INTO \"t2?\" VALUES (NULL, NULL, 'some_?', NULL)"} - }; - } } diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java new file mode 100644 index 000000000..59fa603dc --- /dev/null +++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/SqlParserTest.java @@ -0,0 +1,196 @@ +package com.clickhouse.jdbc.internal; + + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class SqlParserTest { + +// @Test(groups = {"integration"}) +// public void testWithComments() throws Exception { +// assertEquals(SqlParser.parseStatementType(" /* INSERT TESTING */\n SELECT 1 AS num").getType(), SqlParser.StatementType.SELECT); +// assertEquals(SqlParser.parseStatementType("/* SELECT TESTING */\n INSERT INTO test_table VALUES (1)").getType(), SqlParser.StatementType.INSERT); +// assertEquals(SqlParser.parseStatementType("/* INSERT TESTING */\n\n\n UPDATE test_table SET num = 2").getType(), SqlParser.StatementType.UPDATE); +// assertEquals(SqlParser.parseStatementType("-- INSERT TESTING */\n SELECT 1 AS num").getType(), SqlParser.StatementType.SELECT); +// assertEquals(SqlParser.parseStatementType(" -- SELECT TESTING \n -- SELECT AGAIN \n INSERT INTO test_table VALUES (1)").getType(), SqlParser.StatementType.INSERT); +// assertEquals(SqlParser.parseStatementType(" SELECT 42 -- INSERT TESTING").getType(), SqlParser.StatementType.SELECT); +// assertEquals(SqlParser.parseStatementType("#! INSERT TESTING \n SELECT 1 AS num").getType(), SqlParser.StatementType.SELECT); +// assertEquals(SqlParser.parseStatementType("#!INSERT TESTING \n SELECT 1 AS num").getType(), SqlParser.StatementType.SELECT); +// assertEquals(SqlParser.parseStatementType("# INSERT TESTING \n SELECT 1 AS num").getType(), SqlParser.StatementType.SELECT); +// assertEquals(SqlParser.parseStatementType("#INSERT TESTING \n SELECT 1 AS num").getType(), SqlParser.StatementType.SELECT); +// assertEquals(SqlParser.parseStatementType("\nINSERT TESTING \n SELECT 1 AS num").getType(), SqlParser.StatementType.INSERT_INTO_SELECT); +// assertEquals(SqlParser.parseStatementType(" \n INSERT TESTING \n SELECT 1 AS num").getType(), SqlParser.StatementType.INSERT_INTO_SELECT); +// assertEquals(SqlParser.parseStatementType("INSERT INTO t SELECT 1 AS num").getType(), SqlParser.StatementType.INSERT_INTO_SELECT); +// assertEquals(SqlParser.parseStatementType("select 1 AS num").getType(), SqlParser.StatementType.SELECT); +// assertEquals(SqlParser.parseStatementType("insert into test_table values (1)").getType(), SqlParser.StatementType.INSERT); +// assertEquals(SqlParser.parseStatementType("update test_table set num = 2").getType(), SqlParser.StatementType.UPDATE); +// assertEquals(SqlParser.parseStatementType("delete from test_table where num = 2").getType(), SqlParser.StatementType.DELETE); +// assertEquals(SqlParser.parseStatementType("sElEcT 1 AS num").getType(), SqlParser.StatementType.SELECT); +// assertEquals(SqlParser.parseStatementType(null).getType(), SqlParser.StatementType.OTHER); +// assertEquals(SqlParser.parseStatementType("").getType(), SqlParser.StatementType.OTHER); +// assertEquals(SqlParser.parseStatementType(" ").getType(), SqlParser.StatementType.OTHER); +// } +// +// @Test(groups = {"integration"}) +// public void testParseStatementWithClause() throws Exception { +// assertEquals(SqlParser.parseStatementType("with data as (SELECT number FROM numbers(100)) select * from data").getType(), SqlParser.StatementType.SELECT); +// } + + @Test + public void testParseInsertPrepared() throws Exception { + SqlParser parser = new SqlParser(); + + String sql = "INSERT INTO \n`table` (id, \nnum1, col3) \nVALUES (?, ?, ?) "; + ParsedPreparedStatement parsed = parser.parsePreparedStatement(sql); + System.out.println("table: " + parsed.getTable()); + String dataClause = sql.substring(parsed.getAssignValuesListStartPosition(), parsed.getAssignValuesListStopPosition() + 1); + System.out.println("data clause: '" + dataClause + "'"); + + int[] positions = parsed.getParamPositions(); + int[] paramPositionsInDataClause = new int[parsed.getArgCount()]; + for (int i = 0; i < parsed.getArgCount(); i++) { + int p = positions[i] - parsed.getAssignValuesListStartPosition(); + paramPositionsInDataClause[i] = p; + System.out.println("p in clause: " + p); + } + + long tSBuildingSQL = System.nanoTime(); + StringBuilder insertSql = new StringBuilder(sql.substring(0, parsed.getAssignValuesListStartPosition())); + for (int i = 0; i < 100_000; i++) { + StringBuilder valuesClause = new StringBuilder(dataClause); + int posOffset = 0; + String val = "value_" + i; + for (int j = 0; j < parsed.getArgCount(); j++) { + int p = paramPositionsInDataClause[j] + posOffset; + valuesClause.replace(p, p+1, val); + posOffset += val.length() - 1; + } + insertSql.append(valuesClause).append(','); + } + insertSql.setLength(insertSql.length() -1 ); + long tFBuildingSQL = System.nanoTime(); + System.out.println("built in " + (tFBuildingSQL - tSBuildingSQL) + " ns " + ((tFBuildingSQL - tSBuildingSQL)/1000_000f) + " ms"); +// System.out.println("insertSQL: " + insertSql); + + System.out.println("-------"); + StringBuilder compiledSql = new StringBuilder(sql); + int posOffset = 0; + String val = "test"; + for (int i = 0; i < parsed.getArgCount(); i++) { + int p = positions[i] + posOffset; + + System.out.println("p: " + p); + compiledSql.replace(p, p+1, val); + posOffset += val.length() - 1; + } + + System.out.println(compiledSql); + } + + @Test + public void testParseSelectPrepared() throws Exception { + // development test + SqlParser parser = new SqlParser(); + + String sql = "SELECT c1, c2, (true ? 1 : 0 ) as foo FROM tab1 WHERE c3 = ? AND c4 = abs(?)"; + ParsedPreparedStatement parsed = parser.parsePreparedStatement(sql); + System.out.println("table: " + parsed.getTable()); + + System.out.println("-------"); + StringBuilder compiledSql = new StringBuilder(sql); + int posOffset = 0; + String val = "test"; + int[] positions = parsed.getParamPositions(); + for (int i = 0; i < parsed.getArgCount(); i++) { + int p = positions[i] + posOffset; + + System.out.println("p: " + p); + compiledSql.replace(p, p+1, val); + posOffset += val.length() - 1; + } + + System.out.println(sql); + System.out.println(compiledSql); + } + + @Test + public void testPreparedStatementCreateSQL() { + SqlParser parser = new SqlParser(); + + String sql = "CREATE TABLE IF NOT EXISTS `with_complex_id` (`v?``1` Int32, " + + "\"v?\"\"2\" Int32,`v?\\`3` Int32, \"v?\\\"4\" Int32) ENGINE MergeTree ORDER BY ();"; + ParsedPreparedStatement parsed = parser.parsePreparedStatement(sql); + // TODO: extend test expecting no errors + assertFalse(parsed.isInsert()); + + sql = "CREATE TABLE IF NOT EXISTS `test_stmt_split2` (v1 Int32, v2 String) ENGINE MergeTree ORDER BY (); "; + parsed = parser.parsePreparedStatement(sql); + assertFalse(parsed.isInsert()); + } + + + @Test + public void testPreparedStatementInsertSQL() { + SqlParser parser = new SqlParser(); + + String sql = "INSERT INTO `test_stmt_split2` VALUES (1, 'abc'), (2, '?'), (3, '?')"; + ParsedPreparedStatement parsed = parser.parsePreparedStatement(sql); + // TODO: extend test expecting no errors + assertTrue(parsed.isInsert()); + assertFalse(parsed.isHasResultSet()); + assertFalse(parsed.isInsertWithSelect()); + assertEquals(parsed.getAssignValuesGroups(), 3); + + sql = "-- line comment1 ?\n" + + "# line comment2 ?\n" + + "#! line comment3 ?\n" + + "/* block comment ? \n */" + + "INSERT INTO `with_complex_id`(`v?``1`, \"v?\"\"2\",`v?\\`3`, \"v?\\\"4\") VALUES (?, ?, ?, ?);"; + parsed = parser.parsePreparedStatement(sql); + // TODO: extend test expecting no errors + assertTrue(parsed.isInsert()); + assertFalse(parsed.isHasResultSet()); + assertFalse(parsed.isInsertWithSelect()); + assertEquals(parsed.getAssignValuesGroups(), 1); + + sql = "INSERT INTO tt SELECT now(), 10, 20.0, 30"; + parsed = parser.parsePreparedStatement(sql); + // TODO: extend test expecting no errors + assertTrue(parsed.isInsert()); + assertFalse(parsed.isHasResultSet()); + assertTrue(parsed.isInsertWithSelect()); + + + sql = "INSERT INTO `users` (`name`, `last_login`, `password`, `id`) VALUES\n" + + " (?, `parseDateTimeBestEffort`(?, ?), ?, 1)\n"; + parsed = parser.parsePreparedStatement(sql); + // TODO: extend test expecting no errors + assertTrue(parsed.isInsert()); + assertFalse(parsed.isHasResultSet()); + assertFalse(parsed.isInsertWithSelect()); + assertEquals(parsed.getAssignValuesGroups(), 1); + } + + @Test + public void testUnquoteIdentifier() { + String[] names = new String[]{"test", "`test name1`", "\"test name 2\""}; + String[] expected = new String[]{"test", "test name1", "test name 2"}; + + for (int i = 0; i < names.length; i++) { + assertEquals(SqlParser.unquoteIdentifier(names[i]), expected[i]); + } + } + + @Test + public void testEscapeQuotes() { + String[] inStr = new String[]{"%valid_name%", "' OR 1=1 --", "\" OR 1=1 --"}; + String[] outStr = new String[]{"%valid_name%", "\\' OR 1=1 --", "\\\" OR 1=1 --"}; + + for (int i = 0; i < inStr.length; i++) { + assertEquals(SqlParser.escapeQuotes(inStr[i]), outStr[i]); + } + } +} \ No newline at end of file diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/StatementParserTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/StatementParserTest.java deleted file mode 100644 index 1cbcc6836..000000000 --- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/StatementParserTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.clickhouse.jdbc.internal; - - -import org.testng.annotations.Test; - -import static org.testng.Assert.assertEquals; - -public class StatementParserTest { - - @Test(groups = {"integration"}) - public void testWithComments() throws Exception { - assertEquals(StatementParser.parseStatementType(" /* INSERT TESTING */\n SELECT 1 AS num").getType(), StatementParser.StatementType.SELECT); - assertEquals(StatementParser.parseStatementType("/* SELECT TESTING */\n INSERT INTO test_table VALUES (1)").getType(), StatementParser.StatementType.INSERT); - assertEquals(StatementParser.parseStatementType("/* INSERT TESTING */\n\n\n UPDATE test_table SET num = 2").getType(), StatementParser.StatementType.UPDATE); - assertEquals(StatementParser.parseStatementType("-- INSERT TESTING */\n SELECT 1 AS num").getType(), StatementParser.StatementType.SELECT); - assertEquals(StatementParser.parseStatementType(" -- SELECT TESTING \n -- SELECT AGAIN \n INSERT INTO test_table VALUES (1)").getType(), StatementParser.StatementType.INSERT); - assertEquals(StatementParser.parseStatementType(" SELECT 42 -- INSERT TESTING").getType(), StatementParser.StatementType.SELECT); - assertEquals(StatementParser.parseStatementType("#! INSERT TESTING \n SELECT 1 AS num").getType(), StatementParser.StatementType.SELECT); - assertEquals(StatementParser.parseStatementType("#!INSERT TESTING \n SELECT 1 AS num").getType(), StatementParser.StatementType.SELECT); - assertEquals(StatementParser.parseStatementType("# INSERT TESTING \n SELECT 1 AS num").getType(), StatementParser.StatementType.SELECT); - assertEquals(StatementParser.parseStatementType("#INSERT TESTING \n SELECT 1 AS num").getType(), StatementParser.StatementType.SELECT); - assertEquals(StatementParser.parseStatementType("\nINSERT TESTING \n SELECT 1 AS num").getType(), StatementParser.StatementType.INSERT_INTO_SELECT); - assertEquals(StatementParser.parseStatementType(" \n INSERT TESTING \n SELECT 1 AS num").getType(), StatementParser.StatementType.INSERT_INTO_SELECT); - assertEquals(StatementParser.parseStatementType("INSERT INTO t SELECT 1 AS num").getType(), StatementParser.StatementType.INSERT_INTO_SELECT); - assertEquals(StatementParser.parseStatementType("select 1 AS num").getType(), StatementParser.StatementType.SELECT); - assertEquals(StatementParser.parseStatementType("insert into test_table values (1)").getType(), StatementParser.StatementType.INSERT); - assertEquals(StatementParser.parseStatementType("update test_table set num = 2").getType(), StatementParser.StatementType.UPDATE); - assertEquals(StatementParser.parseStatementType("delete from test_table where num = 2").getType(), StatementParser.StatementType.DELETE); - assertEquals(StatementParser.parseStatementType("sElEcT 1 AS num").getType(), StatementParser.StatementType.SELECT); - assertEquals(StatementParser.parseStatementType(null).getType(), StatementParser.StatementType.OTHER); - assertEquals(StatementParser.parseStatementType("").getType(), StatementParser.StatementType.OTHER); - assertEquals(StatementParser.parseStatementType(" ").getType(), StatementParser.StatementType.OTHER); - } - - @Test(groups = {"integration"}) - public void testParseStatementWithClause() throws Exception { - assertEquals(StatementParser.parseStatementType("with data as (SELECT number FROM numbers(100)) select * from data").getType(), StatementParser.StatementType.SELECT); - } - -} \ No newline at end of file