Skip to content

Commit 7c1b88e

Browse files
authored
Merge pull request #2471 from ClickHouse/fix_statement_impl
[jdbc-v2] Fix statement impl
2 parents 8a797c9 + 517a0cc commit 7c1b88e

File tree

18 files changed

+993
-397
lines changed

18 files changed

+993
-397
lines changed

client-v2/src/main/java/com/clickhouse/client/api/internal/ServerSettings.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public final class ServerSettings {
3333
*/
3434
public static final String RESULT_OVERFLOW_MODE = "result_overflow_mode";
3535

36+
public static final String RESULT_OVERFLOW_MODE_THROW = "throw";
37+
38+
public static final String RESULT_OVERFLOW_MODE_BREAK = "break";
39+
3640
public static final String ASYNC_INSERT = "async_insert";
3741

3842
public static final String WAIT_ASYNC_INSERT = "wait_for_async_insert";
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package com.clickhouse.client.api.sql;
2+
3+
import java.util.regex.Matcher;
4+
import java.util.regex.Pattern;
5+
6+
public class SQLUtils {
7+
/**
8+
* Escapes and quotes a string literal for use in SQL queries.
9+
*
10+
* @param str the string to be quoted, cannot be null
11+
* @return the quoted and escaped string
12+
* @throws IllegalArgumentException if the input string is null
13+
*/
14+
public static String enquoteLiteral(String str) {
15+
if (str == null) {
16+
throw new IllegalArgumentException("Input string cannot be null");
17+
}
18+
return "'" + str.replace("'", "''") + "'";
19+
}
20+
21+
/**
22+
* Escapes and quotes an SQL identifier (e.g., table or column name) by enclosing it in double quotes.
23+
* Any existing double quotes in the identifier are escaped by doubling them.
24+
*
25+
* @param identifier the identifier to be quoted, cannot be null
26+
* @param quotesRequired if false, the identifier will only be quoted if it contains special characters
27+
* @return the quoted and escaped identifier, or the original identifier if quoting is not required
28+
* @throws IllegalArgumentException if the input identifier is null
29+
*/
30+
public static String enquoteIdentifier(String identifier, boolean quotesRequired) {
31+
if (identifier == null) {
32+
throw new IllegalArgumentException("Identifier cannot be null");
33+
}
34+
35+
if (!quotesRequired && !needsQuoting(identifier)) {
36+
return identifier;
37+
}
38+
return "\"" + identifier.replace("\"", "\"\"") + "\"";
39+
}
40+
41+
/**
42+
* Escapes and quotes an SQL identifier, always adding quotes.
43+
*
44+
* @param identifier the identifier to be quoted, cannot be null
45+
* @return the quoted and escaped identifier
46+
* @throws IllegalArgumentException if the input identifier is null
47+
* @see #enquoteIdentifier(String, boolean)
48+
*/
49+
public static String enquoteIdentifier(String identifier) {
50+
return enquoteIdentifier(identifier, true);
51+
}
52+
53+
/**
54+
* Checks if an identifier needs to be quoted.
55+
* An identifier needs quoting if it:
56+
* - Is empty
57+
* - Contains any non-alphanumeric characters except underscore
58+
* - Starts with a digit
59+
* - Is a reserved keyword (not implemented in this basic version)
60+
*
61+
* @param identifier the identifier to check
62+
* @return true if the identifier needs to be quoted, false otherwise
63+
*/
64+
private static boolean needsQuoting(String identifier) {
65+
if (identifier == null) {
66+
throw new IllegalArgumentException("identifier cannot be null");
67+
}
68+
69+
if (identifier.isEmpty()) {
70+
return true;
71+
}
72+
73+
// Check if first character is a digit
74+
if (Character.isDigit(identifier.charAt(0))) {
75+
return true;
76+
}
77+
78+
// Check all characters are alphanumeric or underscore
79+
for (int i = 0; i < identifier.length(); i++) {
80+
char c = identifier.charAt(i);
81+
if (!(Character.isLetterOrDigit(c) || c == '_')) {
82+
return true;
83+
}
84+
}
85+
86+
return false;
87+
}
88+
89+
/**
90+
* Checks if the given string is a valid simple SQL identifier that doesn't require quoting.
91+
* A simple identifier must:
92+
* <ul>
93+
* <li>Not be null or empty</li>
94+
* <li>Be between 1 and 128 characters in length (inclusive)</li>
95+
* <li>Start with an alphabetic character (a-z, A-Z)</li>
96+
* <li>Contain only alphanumeric characters or underscores</li>
97+
* <li>Not be enclosed in double quotes</li>
98+
* </ul>
99+
*
100+
* @param identifier the identifier to check
101+
* @return true if the identifier is a valid simple SQL identifier, false otherwise
102+
* @throws IllegalArgumentException if the input identifier is null
103+
*/
104+
// Compiled pattern for simple SQL identifiers
105+
private static final java.util.regex.Pattern SIMPLE_IDENTIFIER_PATTERN =
106+
java.util.regex.Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]{0,127}$");
107+
108+
/**
109+
* Checks if the given string is a valid simple SQL identifier using a compiled regex pattern.
110+
* A simple identifier must match the pattern: ^[a-zA-Z][a-zA-Z0-9_]{0,127}$
111+
*
112+
* @param identifier the identifier to check
113+
* @return true if the identifier is a valid simple SQL identifier, false otherwise
114+
* @throws IllegalArgumentException if the input identifier is null
115+
*/
116+
public static boolean isSimpleIdentifier(String identifier) {
117+
if (identifier == null) {
118+
throw new IllegalArgumentException("Identifier cannot be null");
119+
}
120+
return SIMPLE_IDENTIFIER_PATTERN.matcher(identifier).matches();
121+
}
122+
123+
private final static Pattern UNQUOTE_INDENTIFIER = Pattern.compile(
124+
"^[\\\"`]?(.+?)[\\\"`]?$"
125+
);
126+
127+
public static String unquoteIdentifier(String str) {
128+
Matcher matcher = UNQUOTE_INDENTIFIER.matcher(str.trim());
129+
if (matcher.find()) {
130+
return matcher.group(1);
131+
} else {
132+
return str;
133+
}
134+
}
135+
}

client-v2/src/test/java/com/clickhouse/client/api/data_formats/internal/SerializerUtilsTests.java

Lines changed: 0 additions & 5 deletions
This file was deleted.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package com.clickhouse.client.api.sql;
2+
3+
import org.apache.commons.lang3.StringUtils;
4+
import org.testng.annotations.DataProvider;
5+
import org.testng.annotations.Test;
6+
7+
import static org.testng.Assert.assertEquals;
8+
9+
@Test(groups = {"unit"})
10+
public class SQLUtilsTest {
11+
// Test data for enquoteLiteral
12+
@DataProvider(name = "enquoteLiteralTestData")
13+
public Object[][] enquoteLiteralTestData() {
14+
return new Object[][] {
15+
// input, expected output
16+
{"test 123", "'test 123'"},
17+
{"こんにちは世界", "'こんにちは世界'"},
18+
{"O'Reilly", "'O''Reilly'"},
19+
{"😊👍", "'😊👍'"},
20+
{"", "''"},
21+
{"single'quote'double''quote\"", "'single''quote''double''''quote\"'"}
22+
};
23+
}
24+
25+
// Test data for enquoteIdentifier
26+
@DataProvider(name = "enquoteIdentifierTestData")
27+
public Object[][] enquoteIdentifierTestData() {
28+
return new Object[][] {
29+
// input, expected output
30+
{"column1", "\"column1\""},
31+
{"table.name", "\"table.name\""},
32+
{"column with spaces", "\"column with spaces\""},
33+
{"column\"with\"quotes", "\"column\"\"with\"\"quotes\""},
34+
{"UPPERCASE", "\"UPPERCASE\""},
35+
{"1column", "\"1column\""},
36+
{"column-with-hyphen", "\"column-with-hyphen\""},
37+
{"😊👍", "\"😊👍\""},
38+
{"", "\"\""}
39+
};
40+
}
41+
42+
@Test(dataProvider = "enquoteLiteralTestData")
43+
public void testEnquoteLiteral(String input, String expected) {
44+
assertEquals(SQLUtils.enquoteLiteral(input), expected);
45+
}
46+
47+
@Test(expectedExceptions = IllegalArgumentException.class)
48+
public void testEnquoteLiteral_NullInput() {
49+
SQLUtils.enquoteLiteral(null);
50+
}
51+
52+
@Test(dataProvider = "enquoteIdentifierTestData")
53+
public void testEnquoteIdentifier(String input, String expected) {
54+
// Test with quotesRequired = true (always quote)
55+
assertEquals(SQLUtils.enquoteIdentifier(input), expected);
56+
assertEquals(SQLUtils.enquoteIdentifier(input, true), expected);
57+
58+
// Test with quotesRequired = false (quote only if needed)
59+
boolean needsQuoting = !input.matches("[a-zA-Z_][a-zA-Z0-9_]*");
60+
String expectedUnquoted = needsQuoting ? expected : input;
61+
assertEquals(SQLUtils.enquoteIdentifier(input, false), expectedUnquoted);
62+
}
63+
64+
@Test(expectedExceptions = IllegalArgumentException.class)
65+
public void testEnquoteIdentifier_NullInput() {
66+
SQLUtils.enquoteIdentifier(null);
67+
}
68+
69+
@Test(expectedExceptions = IllegalArgumentException.class)
70+
public void testEnquoteIdentifier_NullInput_WithQuotesRequired() {
71+
SQLUtils.enquoteIdentifier(null, true);
72+
}
73+
74+
@Test
75+
public void testEnquoteIdentifier_NoQuotesWhenNotNeeded() {
76+
// These identifiers don't need quoting
77+
String[] simpleIdentifiers = {
78+
"column1", "table_name", "_id", "a1b2c3", "ColumnName"
79+
};
80+
81+
for (String id : simpleIdentifiers) {
82+
// With quotesRequired=false, should return as-is
83+
assertEquals(SQLUtils.enquoteIdentifier(id, false), id);
84+
// With quotesRequired=true, should be quoted
85+
assertEquals(SQLUtils.enquoteIdentifier(id, true), "\"" + id + "\"");
86+
}
87+
}
88+
89+
@DataProvider(name = "simpleIdentifierTestData")
90+
public Object[][] simpleIdentifierTestData() {
91+
return new Object[][] {
92+
// identifier, expected result
93+
{"Hello", true},
94+
{"hello_world", true},
95+
{"Hello123", true},
96+
{"H", true}, // minimum length
97+
{StringUtils.repeat("a", 128), true}, // maximum length
98+
99+
// Test cases from requirements
100+
{"G'Day", false},
101+
{"\"\"Bruce Wayne\"\"", false},
102+
{"GoodDay$", false},
103+
{"Hello\"\"World", false},
104+
{"\"\"Hello\"\"World\"\"", false},
105+
106+
// Additional test cases
107+
{"", false}, // empty string
108+
{"123test", false}, // starts with number
109+
{"_test", false}, // starts with underscore
110+
{"test-name", false}, // contains hyphen
111+
{"test name", false}, // contains space
112+
{"test\"name", false}, // contains quote
113+
{"test.name", false}, // contains dot
114+
{StringUtils.repeat("a", 129), false}, // exceeds max length
115+
{"testName", true},
116+
{"TEST_NAME", true},
117+
{"test123", true},
118+
{"t123", true},
119+
{"t", true}
120+
};
121+
}
122+
123+
@Test(dataProvider = "simpleIdentifierTestData")
124+
public void testIsSimpleIdentifier(String identifier, boolean expected) {
125+
assertEquals(SQLUtils.isSimpleIdentifier(identifier), expected,
126+
String.format("Failed for identifier: %s", identifier));
127+
}
128+
129+
@Test(expectedExceptions = IllegalArgumentException.class)
130+
public void testIsSimpleIdentifier_NullInput() {
131+
SQLUtils.isSimpleIdentifier(null);
132+
}
133+
134+
@Test
135+
public void testUnquoteIdentifier() {
136+
String[] names = new String[]{"test", "`test name1`", "\"test name 2\""};
137+
String[] expected = new String[]{"test", "test name1", "test name 2"};
138+
139+
for (int i = 0; i < names.length; i++) {
140+
assertEquals(SQLUtils.unquoteIdentifier(names[i]), expected[i]);
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)