-
Notifications
You must be signed in to change notification settings - Fork 885
JAVA-1064: getTable create statement doesn't properly handle quotes in primary key. #582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5e88498
6745c0f
2c7fdd2
caa8f83
882f2cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,6 +47,15 @@ public class Metadata { | |
| private static final Pattern alphanumeric = Pattern.compile("\\w+"); // this includes _ | ||
| private static final Pattern lowercaseAlphanumeric = Pattern.compile("[a-z][a-z0-9_]*"); | ||
|
|
||
| private static final Set<String> RESERVED_KEYWORDS = ImmutableSet.of( | ||
| "add", "allow", "alter", "and", "any", "apply", "asc", "authorize", "batch", "begin", "by", | ||
| "columnfamily", "create", "delete", "desc", "drop", "each_quorum", "from", "grant", "in", | ||
| "index", "inet", "infinity", "insert", "into", "keyspace", "keyspaces", "limit", "local_one", | ||
| "local_quorum", "modify", "nan", "norecursive", "of", "on", "one", "order", "password", | ||
| "primary", "quorum", "rename", "revoke", "schema", "select", "set", "table", "to", | ||
| "token", "three", "truncate", "two", "unlogged", "update", "use", "using", "where", "with" | ||
| ); | ||
|
|
||
| Metadata(Cluster.Manager cluster) { | ||
| this.cluster = cluster; | ||
| } | ||
|
|
@@ -404,7 +413,7 @@ static String handleId(String id) { | |
| return id.toLowerCase(); | ||
|
|
||
| // Check if it's enclosed in quotes. If it is, remove them and unescape internal double quotes | ||
| if (id.charAt(0) == '"' && id.charAt(id.length() - 1) == '"') | ||
| if (!id.isEmpty() && id.charAt(0) == '"' && id.charAt(id.length() - 1) == '"') | ||
| return id.substring(1, id.length() - 1).replaceAll("\"\"", "\""); | ||
|
|
||
| // Otherwise, just return the id. | ||
|
|
@@ -418,8 +427,11 @@ static String handleId(String id) { | |
| // tables. Because it comes from Cassandra, we could just always quote it, | ||
| // but to get a nicer output we don't do it if it's not necessary. | ||
| static String escapeId(String ident) { | ||
| // we don't need to escape if it's lowercase and match non-quoted CQL3 ids. | ||
| return lowercaseAlphanumeric.matcher(ident).matches() ? ident : quote(ident); | ||
| // we don't need to escape if it's lowercase and match non-quoted CQL3 ids, | ||
| // and if it's not a CQL reserved keyword | ||
| return lowercaseAlphanumeric.matcher(ident).matches() | ||
| && !isReservedCqlKeyword(ident) ? | ||
| ident : quote(ident); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -430,16 +442,40 @@ static String escapeId(String ident) { | |
| * the identifier in double quotes (see the | ||
| * <a href="http://cassandra.apache.org/doc/cql3/CQL.html#identifiers">CQL documentation</a> | ||
| * for details). If you are using case sensitive identifiers, this method | ||
| * can be used to enclose such identifier in double quotes, making it case | ||
| * can be used to enclose such identifiers in double quotes, making them case | ||
| * sensitive. | ||
| * <p/> | ||
| * Note that | ||
| * <a href="https://docs.datastax.com/en/cql/3.0/cql/cql_reference/keywords_r.html">reserved CQL keywords</a> | ||
| * should also be quoted. You can check if a given identifier is a reserved keyword | ||
| * by calling {@link #isReservedCqlKeyword(String)}. | ||
| * | ||
| * @param id the keyspace or table identifier. | ||
| * @return {@code id} enclosed in double-quotes, for use in methods like | ||
| * {@link #getReplicas}, {@link #getKeyspace}, {@link KeyspaceMetadata#getTable} | ||
| * or even {@link Cluster#connect(String)}. | ||
| */ | ||
| public static String quote(String id) { | ||
| return '"' + id + '"'; | ||
| return '"' + id.replace("\"", "\"\"") + '"'; | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also backported from 3.0; I don't understand why this wasn't already present in 2.1 as without this replacement any identifier containing quotes will be wrongly quoted. |
||
| } | ||
|
|
||
| /** | ||
| * Checks whether an identifier is a known reserved CQL keyword or not. | ||
| * <p/> | ||
| * The check is case-insensitive, i.e., the word "{@code KeYsPaCe}" | ||
| * would be considered as a reserved CQL keyword just as "{@code keyspace}". | ||
| * <p/> | ||
| * Note: The list of reserved CQL keywords is subject to change in future | ||
| * versions of Cassandra. As a consequence, this method is provided solely as a | ||
| * convenience utility and should not be considered as an authoritative | ||
| * source of truth for checking reserved CQL keywords. | ||
| * | ||
| * @param id the identifier to check; should not be {@code null}. | ||
| * @return {@code true} if the given identifier is a known reserved | ||
| * CQL keyword, {@code false} otherwise. | ||
| */ | ||
| public static boolean isReservedCqlKeyword(String id) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small addition to the API, required because it is accessed by |
||
| return id != null && RESERVED_KEYWORDS.contains(id.toLowerCase()); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -435,12 +435,12 @@ private String asCQLQuery(boolean formatted) { | |
| sb.append("CREATE TABLE ").append(Metadata.escapeId(keyspace.getName())).append('.').append(Metadata.escapeId(name)).append(" ("); | ||
| newLine(sb, formatted); | ||
| for (ColumnMetadata cm : columns.values()) | ||
| newLine(sb.append(spaces(4, formatted)).append(cm).append(','), formatted); | ||
| newLine(sb.append(spaces(4, formatted)).append(cm).append(',').append(spaces(1, !formatted)), formatted); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for a better spacing when outputting columns in non-formatted mode. |
||
|
|
||
| // PK | ||
| sb.append(spaces(4, formatted)).append("PRIMARY KEY ("); | ||
| if (partitionKey.size() == 1) { | ||
| sb.append(partitionKey.get(0).getName()); | ||
| sb.append(Metadata.escapeId(partitionKey.get(0).getName())); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the "real" fix for the bug reported in JAVA-1064. |
||
| } else { | ||
| sb.append('('); | ||
| boolean first = true; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ | |
| */ | ||
| package com.datastax.driver.core.querybuilder; | ||
|
|
||
| import com.datastax.driver.core.Metadata; | ||
| import com.datastax.driver.core.RegularStatement; | ||
| import com.datastax.driver.core.TableMetadata; | ||
| import com.datastax.driver.core.exceptions.InvalidQueryException; | ||
|
|
@@ -226,9 +227,10 @@ public static Truncate truncate(TableMetadata table) { | |
| * | ||
| * @param columnName the column name to quote. | ||
| * @return the quoted column name. | ||
| * @see Metadata#quote(String) | ||
| */ | ||
| public static String quote(String columnName) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @al3xandru drew my attention to this method recently; I suggest that we delegate to |
||
| return '"' + columnName + '"'; | ||
| return Metadata.quote(columnName); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,13 +15,12 @@ | |
| */ | ||
| package com.datastax.driver.core.schemabuilder; | ||
|
|
||
| import com.datastax.driver.core.Metadata; | ||
| import com.datastax.driver.core.ProtocolVersion; | ||
| import com.datastax.driver.core.RegularStatement; | ||
| import com.google.common.base.Strings; | ||
|
|
||
| import java.nio.ByteBuffer; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * A DDL statement generated by {@link SchemaBuilder}. | ||
|
|
@@ -31,12 +30,6 @@ public abstract class SchemaStatement extends RegularStatement { | |
| static final String STATEMENT_START = "\n\t"; | ||
| static final String COLUMN_FORMATTING = "\n\t\t"; | ||
|
|
||
| static final List<String> RESERVED_KEYWORDS = Arrays.asList( | ||
| ("add,allow,alter,and,any,apply,asc,authorize,batch,begin,by,columnfamily,create,delete,desc,drop,each_quorum,from,grant,in,index,inet,infinity," + | ||
| "insert,into,keyspace,keyspaces,limit,local_one,local_quorum,modify,nan,norecursive,of,on,order,password,primary,quorum,rename,revoke,schema," + | ||
| "select,set,table,three,to,token,truncate,two,unlogged,update,use,using,where,with") | ||
| .split(",")); | ||
|
|
||
| private volatile String cache; | ||
|
|
||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. replaced with the equivalent in |
||
| abstract String buildInternal(); | ||
|
|
@@ -85,7 +78,7 @@ static void validateNotNull(Object value, String label) { | |
| } | ||
|
|
||
| static void validateNotKeyWord(String label, String message) { | ||
| if (RESERVED_KEYWORDS.contains(label)) { | ||
| if (Metadata.isReservedCqlKeyword(label)) { | ||
| throw new IllegalArgumentException(message); | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,7 @@ | |
|
|
||
| import org.testng.annotations.Test; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static com.datastax.driver.core.Assertions.assertThat; | ||
|
|
||
| @CCMConfig(clusterProvider = "createClusterBuilderNoDebouncing") | ||
| public class TableMetadataTest extends CCMTestsSupport { | ||
|
|
@@ -35,4 +35,76 @@ public void should_escape_single_quote_table_comment() { | |
| TableMetadata table = cluster().getMetadata().getKeyspace(keyspace).getTable("single_quote"); | ||
| assertThat(table.asCQLQuery()).contains("comment with single quote '' should work"); | ||
| } | ||
|
|
||
| /** | ||
| * Validates that a table with case-sensitive column names and column names | ||
| * consisting of (quoted) reserved keywords is correctly parsed | ||
| * and that the generated CQL is valid. | ||
| * | ||
| * @jira_ticket JAVA-1064 | ||
| * @test_category metadata | ||
| */ | ||
| @Test(groups = "short") | ||
| public void should_parse_table_with_case_sensitive_column_names_and_reserved_keywords() throws Exception { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||
| // given | ||
| String c1 = Metadata.quote("quotes go \"\" here \"\" "); | ||
| String c2 = Metadata.quote("\\x00\\x25"); | ||
| String c3 = Metadata.quote("columnfamily"); | ||
| String c4 = Metadata.quote("select"); | ||
| String c5 = Metadata.quote("who''s there'? "); | ||
| String c6 = Metadata.quote("faux )"); | ||
| String c7 = Metadata.quote("COMPACT STORAGE"); | ||
| // single partition key | ||
| String cql1 = String.format("CREATE TABLE %s.\"MyTable1\" (" | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "PRIMARY KEY (%s, %s, %s, %s, %s, %s)" | ||
| + ")", keyspace, c1, c2, c3, c4, c5, c6, c7, c1, c2, c3, c4, c5, c6); | ||
| // composite partition key | ||
| String cql2 = String.format("CREATE TABLE %s.\"MyTable2\" (" | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "%s text, " | ||
| + "PRIMARY KEY ((%s, %s), %s, %s, %s, %s)" | ||
| + ")", keyspace, c1, c2, c3, c4, c5, c6, c7, c1, c2, c3, c4, c5, c6); | ||
| // when | ||
| execute(cql1, cql2); | ||
| TableMetadata table1 = cluster().getMetadata().getKeyspace(keyspace).getTable("\"MyTable1\""); | ||
| TableMetadata table2 = cluster().getMetadata().getKeyspace(keyspace).getTable("\"MyTable2\""); | ||
| // then | ||
| assertThat(table1) | ||
| .hasColumn(c1) | ||
| .hasColumn(c2) | ||
| .hasColumn(c3) | ||
| .hasColumn(c4) | ||
| .hasColumn(c5) | ||
| .hasColumn(c6) | ||
| .hasColumn(c7); | ||
| assertThat(table1.asCQLQuery()).startsWith(cql1); | ||
| assertThat(table2) | ||
| .hasColumn(c1) | ||
| .hasColumn(c2) | ||
| .hasColumn(c3) | ||
| .hasColumn(c4) | ||
| .hasColumn(c5) | ||
| .hasColumn(c6) | ||
| .hasColumn(c7); | ||
| assertThat(table2.asCQLQuery()).startsWith(cql2); | ||
| execute( | ||
| "DROP TABLE \"MyTable1\"", | ||
| "DROP TABLE \"MyTable2\"", | ||
| table1.asCQLQuery(), | ||
| table2.asCQLQuery() | ||
| ); | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -865,7 +865,7 @@ public void should_quote_complex_column_names() { | |
| public void should_quote_column_names_with_escaped_quotes() { | ||
| // A column name can include quotes as long as it is escaped with another set of quotes, so "foo""bar" is a valid name. | ||
| String query = "SELECT * FROM foo WHERE \"foo \"\" bar\"=1;"; | ||
| Statement statement = select().from("foo").where(eq(quote("foo \"\" bar"), 1)); | ||
| Statement statement = select().from("foo").where(eq(quote("foo \" bar"), 1)); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. broken implementation + broken test = test succeeded :) |
||
|
|
||
| assertThat(statement.toString()).isEqualTo(query); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -545,54 +545,54 @@ public void should_fail_if_static_column_in_non_clustered_table() throws Excepti | |
| } | ||
|
|
||
| @Test(groups = "unit", expectedExceptions = IllegalArgumentException.class, | ||
| expectedExceptionsMessageRegExp = "The keyspace name 'add' is not allowed because it is a reserved keyword") | ||
| expectedExceptionsMessageRegExp = "The keyspace name 'ADD' is not allowed because it is a reserved keyword") | ||
| public void should_fail_if_keyspace_name_is_a_reserved_keyword() throws Exception { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tests modified to test case-insensitiveness of keywords. |
||
| createTable("add", "test") | ||
| createTable("ADD", "test") | ||
| .addPartitionKey("pk", DataType.bigint()) | ||
| .addColumn("col", DataType.text()).getQueryString(); | ||
| } | ||
|
|
||
| @Test(groups = "unit", expectedExceptions = IllegalArgumentException.class, | ||
| expectedExceptionsMessageRegExp = "The table name 'add' is not allowed because it is a reserved keyword") | ||
| expectedExceptionsMessageRegExp = "The table name 'ADD' is not allowed because it is a reserved keyword") | ||
| public void should_fail_if_table_name_is_a_reserved_keyword() throws Exception { | ||
| createTable("add") | ||
| createTable("ADD") | ||
| .addPartitionKey("pk", DataType.bigint()) | ||
| .addColumn("col", DataType.text()).getQueryString(); | ||
| } | ||
|
|
||
| @Test(groups = "unit", expectedExceptions = IllegalArgumentException.class, | ||
| expectedExceptionsMessageRegExp = "The partition key name 'add' is not allowed because it is a reserved keyword") | ||
| expectedExceptionsMessageRegExp = "The partition key name 'ADD' is not allowed because it is a reserved keyword") | ||
| public void should_fail_if_partition_key_is_a_reserved_keyword() throws Exception { | ||
| createTable("test") | ||
| .addPartitionKey("add", DataType.bigint()) | ||
| .addPartitionKey("ADD", DataType.bigint()) | ||
| .addColumn("col", DataType.text()).getQueryString(); | ||
| } | ||
|
|
||
| @Test(groups = "unit", expectedExceptions = IllegalArgumentException.class, | ||
| expectedExceptionsMessageRegExp = "The clustering column name 'add' is not allowed because it is a reserved keyword") | ||
| expectedExceptionsMessageRegExp = "The clustering column name 'ADD' is not allowed because it is a reserved keyword") | ||
| public void should_fail_if_clustering_key_is_a_reserved_keyword() throws Exception { | ||
| createTable("test") | ||
| .addPartitionKey("pk", DataType.bigint()) | ||
| .addClusteringColumn("add", DataType.uuid()) | ||
| .addClusteringColumn("ADD", DataType.uuid()) | ||
| .addColumn("col", DataType.text()).getQueryString(); | ||
| } | ||
|
|
||
| @Test(groups = "unit", expectedExceptions = IllegalArgumentException.class, | ||
| expectedExceptionsMessageRegExp = "The column name 'add' is not allowed because it is a reserved keyword") | ||
| expectedExceptionsMessageRegExp = "The column name 'ADD' is not allowed because it is a reserved keyword") | ||
| public void should_fail_if_simple_column_is_a_reserved_keyword() throws Exception { | ||
| createTable("test") | ||
| .addPartitionKey("pk", DataType.bigint()) | ||
| .addClusteringColumn("cluster", DataType.uuid()) | ||
| .addColumn("add", DataType.text()).getQueryString(); | ||
| .addColumn("ADD", DataType.text()).getQueryString(); | ||
| } | ||
|
|
||
| @Test(groups = "unit", expectedExceptions = IllegalArgumentException.class, | ||
| expectedExceptionsMessageRegExp = "The static column name 'add' is not allowed because it is a reserved keyword") | ||
| expectedExceptionsMessageRegExp = "The static column name 'ADD' is not allowed because it is a reserved keyword") | ||
| public void should_fail_if_static_column_is_a_reserved_keyword() throws Exception { | ||
| createTable("test") | ||
| .addPartitionKey("pk", DataType.bigint()) | ||
| .addClusteringColumn("cluster", DataType.uuid()) | ||
| .addStaticColumn("add", DataType.text()) | ||
| .addStaticColumn("ADD", DataType.text()) | ||
| .addColumn("col", DataType.text()).getQueryString(); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small enhancement backported from 3.0: this line would throw an OutOfBoundsException for the empty string.