Skip to content

Commit 5f42e52

Browse files
Dmitry Konstantinovolim7t
authored andcommitted
JAVA-1661: Avoid String.toLowerCase if possible in Metadata
This method is invoked very frequently, minimize CPU usage and GC overheads.
1 parent 95b2039 commit 5f42e52

File tree

3 files changed

+109
-17
lines changed

3 files changed

+109
-17
lines changed

changelog/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- [bug] JAVA-1666: Fix keyspace export when a UDT has case-sensitive field names.
66
- [improvement] JAVA-1196: Include hash of result set metadata in prepared statement id.
77
- [improvement] JAVA-1670: Support user-provided JMX ports for CCMBridge.
8+
- [improvement] JAVA-1661: Avoid String.toLowerCase if possible in Metadata.
89

910

1011
### 3.3.1

driver-core/src/main/java/com/datastax/driver/core/Metadata.java

Lines changed: 90 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.google.common.collect.ImmutableMap;
1919
import com.google.common.collect.ImmutableSet;
2020
import com.google.common.collect.Maps;
21+
import io.netty.util.collection.IntObjectHashMap;
2122
import org.slf4j.Logger;
2223
import org.slf4j.LoggerFactory;
2324

@@ -45,7 +46,7 @@ public class Metadata {
4546
final ReentrantLock lock = new ReentrantLock();
4647

4748
// See https://github.com/apache/cassandra/blob/trunk/doc/cql3/CQL.textile#appendixA
48-
private static final Set<String> RESERVED_KEYWORDS = ImmutableSet.of(
49+
private static final IntObjectHashMap<List<char[]>> RESERVED_KEYWORDS = indexByCaseInsensitiveHash(
4950
"add", "allow", "alter", "and", "any", "apply", "asc", "authorize", "batch", "begin", "by",
5051
"columnfamily", "create", "delete", "desc", "drop", "each_quorum", "from", "grant", "in",
5152
"index", "inet", "infinity", "insert", "into", "keyspace", "keyspaces", "limit", "local_one",
@@ -125,25 +126,32 @@ static String handleId(String id) {
125126
if (id == null)
126127
return null;
127128

128-
if (isAlphanumeric(id))
129-
return id.toLowerCase();
130-
131-
// Check if it's enclosed in quotes. If it is, remove them and unescape internal double quotes
132-
return ParseUtils.unDoubleQuote(id);
133-
}
134-
135-
private static boolean isAlphanumeric(String s) {
136-
for (int i = 0; i < s.length(); i++) {
137-
char c = s.charAt(i);
138-
if (!(
129+
boolean isAlphanumericLowCase = true;
130+
boolean isAlphanumeric = true;
131+
for (int i = 0; i < id.length(); i++) {
132+
char c = id.charAt(i);
133+
if (c >= 65 && c <= 90) { // A-Z
134+
isAlphanumericLowCase = false;
135+
} else if (!(
139136
(c >= 48 && c <= 57) // 0-9
140-
|| (c >= 65 && c <= 90) // A-Z
141137
|| (c == 95) // _ (underscore)
142138
|| (c >= 97 && c <= 122) // a-z
143-
))
144-
return false;
139+
)) {
140+
isAlphanumeric = false;
141+
isAlphanumericLowCase = false;
142+
break;
143+
}
145144
}
146-
return true;
145+
146+
if (isAlphanumericLowCase) {
147+
return id;
148+
}
149+
if (isAlphanumeric) {
150+
return id.toLowerCase();
151+
}
152+
153+
// Check if it's enclosed in quotes. If it is, remove them and unescape internal double quotes
154+
return ParseUtils.unDoubleQuote(id);
147155
}
148156

149157
/**
@@ -273,8 +281,73 @@ public static String quote(String id) {
273281
* @return {@code true} if the given identifier is a known reserved
274282
* CQL keyword, {@code false} otherwise.
275283
*/
284+
276285
public static boolean isReservedCqlKeyword(String id) {
277-
return id != null && RESERVED_KEYWORDS.contains(id.toLowerCase());
286+
if (id == null) {
287+
return false;
288+
}
289+
int hash = caseInsensitiveHash(id);
290+
List<char[]> keywords = RESERVED_KEYWORDS.get(hash);
291+
if (keywords == null) {
292+
return false;
293+
} else {
294+
for (char[] keyword : keywords) {
295+
if (equalsIgnoreCaseAscii(id, keyword)) {
296+
return true;
297+
}
298+
}
299+
return false;
300+
}
301+
}
302+
303+
private static int caseInsensitiveHash(String str) {
304+
int hashCode = 17;
305+
for (int i = 0; i < str.length(); i++) {
306+
char c = toLowerCaseAscii(str.charAt(i));
307+
hashCode = 31 * hashCode + c;
308+
}
309+
return hashCode;
310+
}
311+
312+
// keyword is expected as a second argument always in low case
313+
private static boolean equalsIgnoreCaseAscii(String str1, char[] str2LowCase) {
314+
if (str1.length() != str2LowCase.length) return false;
315+
316+
for (int i = 0; i < str1.length(); i++) {
317+
char c1 = str1.charAt(i);
318+
char c2Low = str2LowCase[i];
319+
if (c1 == c2Low) {
320+
continue;
321+
}
322+
char low1 = toLowerCaseAscii(c1);
323+
if (low1 == c2Low) {
324+
continue;
325+
}
326+
return false;
327+
}
328+
return true;
329+
}
330+
331+
private static char toLowerCaseAscii(char c) {
332+
if (c >= 65 && c <= 90) { // A-Z
333+
c ^= 0x20; // convert to low case
334+
}
335+
return c;
336+
}
337+
338+
private static IntObjectHashMap<List<char[]>> indexByCaseInsensitiveHash(String... words) {
339+
IntObjectHashMap<List<char[]>> result = new IntObjectHashMap<List<char[]>>();
340+
for (String word : words) {
341+
char[] wordAsCharArray = word.toLowerCase().toCharArray();
342+
int hash = caseInsensitiveHash(word);
343+
List<char[]> list = result.get(hash);
344+
if (list == null) {
345+
list = new ArrayList<char[]>();
346+
result.put(hash, list);
347+
}
348+
list.add(wordAsCharArray);
349+
}
350+
return result;
278351
}
279352

280353
/**

driver-core/src/test/java/com/datastax/driver/core/MetadataTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ private Map<Host, Token> getTokenForHosts(Metadata metadata) {
125125
public void handleId_should_lowercase_unquoted_alphanumeric_identifiers() {
126126
assertThat(Metadata.handleId("FooBar1")).isEqualTo("foobar1");
127127
assertThat(Metadata.handleId("Foo_Bar_1")).isEqualTo("foo_bar_1");
128+
assertThat(Metadata.handleId("foo_bar_1")).isEqualTo("foo_bar_1");
128129
}
129130

130131
@Test(groups = "unit")
@@ -161,4 +162,21 @@ public void escapeId_should_quote_reserved_cql_keywords() {
161162
assertThat(Metadata.quoteIfNecessary("columnfamily")).isEqualTo("\"columnfamily\"");
162163
}
163164

165+
@Test(groups = "unit")
166+
public void should_detect_reserved_keywords_in_upper_case() {
167+
assertThat(Metadata.isReservedCqlKeyword("COLUMNFAMILY")).isTrue();
168+
assertThat(Metadata.isReservedCqlKeyword("TEST_COLUMNFAMILY")).isFalse();
169+
}
170+
171+
@Test(groups = "unit")
172+
public void should_detect_reserved_keywords_in_lower_case() {
173+
assertThat(Metadata.isReservedCqlKeyword("columnfamily")).isTrue();
174+
assertThat(Metadata.isReservedCqlKeyword("test_columnfamily")).isFalse();
175+
}
176+
177+
@Test(groups = "unit")
178+
public void should_detect_reserved_keywords_in_mixed_case() {
179+
assertThat(Metadata.isReservedCqlKeyword("ColumnFamily")).isTrue();
180+
assertThat(Metadata.isReservedCqlKeyword("Test_ColumnFamily")).isFalse();
181+
}
164182
}

0 commit comments

Comments
 (0)