From 8b9d4ea8f3432bf0ff9f168780954efb44f341b4 Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Sun, 16 Feb 2014 22:39:30 -0600 Subject: [PATCH 1/2] DATACASS-96 - integrated CqlIdentifier --- .../cassandra/core/CqlIdentifier.java | 117 ---------- .../cassandra/core/ReservedKeyword.java | 41 ++++ .../cassandra/core/cql/CqlConstantType.java | 42 ++++ .../cassandra/core/cql/CqlIdentifier.java | 202 ++++++++++++++++++ .../cassandra/core/cql/CqlStringUtils.java | 30 ++- .../cql/generator/AddColumnCqlGenerator.java | 4 +- .../generator/AlterColumnCqlGenerator.java | 3 +- .../generator/AlterKeyspaceCqlGenerator.java | 2 +- .../cql/generator/AlterTableCqlGenerator.java | 2 +- .../generator/CreateIndexCqlGenerator.java | 4 +- .../generator/CreateKeyspaceCqlGenerator.java | 2 +- .../generator/CreateTableCqlGenerator.java | 2 +- .../cql/generator/DropColumnCqlGenerator.java | 2 +- .../cql/generator/DropIndexCqlGenerator.java | 2 +- .../generator/DropKeyspaceCqlGenerator.java | 2 +- .../cql/generator/DropTableCqlGenerator.java | 2 +- .../core/keyspace/AddColumnSpecification.java | 6 + .../keyspace/AlterColumnSpecification.java | 6 + .../keyspace/AlterTableSpecification.java | 16 +- .../keyspace/ColumnChangeSpecification.java | 26 ++- .../core/keyspace/ColumnSpecification.java | 27 +-- .../ColumnTypeChangeSpecification.java | 7 + .../keyspace/CreateIndexSpecification.java | 52 +++-- .../keyspace/CreateKeyspaceSpecification.java | 22 +- .../keyspace/CreateTableSpecification.java | 16 +- .../keyspace/DropColumnSpecification.java | 14 ++ .../keyspace/DropKeyspaceSpecification.java | 1 - .../core/keyspace/IndexDescriptor.java | 18 +- .../core/keyspace/IndexNameSpecification.java | 31 +-- .../keyspace/KeyspaceActionSpecification.java | 46 ++-- .../core/keyspace/KeyspaceDescriptor.java | 9 +- .../KeyspaceOptionsSpecification.java | 4 +- .../core/keyspace/KeyspaceSpecification.java | 1 + .../core/keyspace/TableDescriptor.java | 9 +- .../core/keyspace/TableNameSpecification.java | 24 ++- .../keyspace/TableOptionsSpecification.java | 12 +- .../core/keyspace/TableSpecification.java | 29 ++- .../CqlIndexSpecificationAssertions.java | 6 +- .../CqlTableSpecificationAssertions.java | 8 +- .../FunkyIdentifierIntegrationTest.java | 42 ++++ .../test/unit/core/cql/CqlIdentifierTest.java | 63 +++++- .../AlterTableCqlGeneratorTests.java | 1 - .../CreateTableCqlGeneratorTests.java | 105 ++++++--- .../TableOperationCqlGeneratorTest.java | 1 - ...raCompositePrimaryKeyIntegrationTests.java | 4 +- 45 files changed, 735 insertions(+), 330 deletions(-) delete mode 100644 spring-cassandra/src/main/java/org/springframework/cassandra/core/CqlIdentifier.java create mode 100644 spring-cassandra/src/main/java/org/springframework/cassandra/core/ReservedKeyword.java create mode 100644 spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlConstantType.java create mode 100644 spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlIdentifier.java create mode 100644 spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/FunkyIdentifierIntegrationTest.java diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/CqlIdentifier.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/CqlIdentifier.java deleted file mode 100644 index 9371948ab..000000000 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/CqlIdentifier.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.springframework.cassandra.core; - -import java.util.regex.Pattern; - -import org.springframework.cassandra.core.cql.CqlStringUtils; - -/** - * This encapsulates the logic for CQL identifiers. - * - * @author John McPeek - * - */ -public class CqlIdentifier { - public static final String UNQUOTED_IDENTIFIER_REGEX = "[a-zA-Z_][a-zA-Z0-9_]*"; - public static final Pattern UNQUOTED_IDENTIFIER_PATTERN = Pattern.compile(UNQUOTED_IDENTIFIER_REGEX); - public static final String QUOTED_IDENTIFIER_REGEX = "[a-zA-Z_]([a-zA-Z0-9_]|\"{2}+)*"; - public static final Pattern QUOTED_IDENTIFIER_PATTERN = Pattern.compile(QUOTED_IDENTIFIER_REGEX); - - private String name; - private boolean quoted; - - public CqlIdentifier(String identifier) { - this(identifier, false); - } - - /** - * Renders the given string as a legal Cassandra identifier. - * - */ - public CqlIdentifier(String name, boolean forceQuoting) { - if (isUnquotedIdentifier(name) && forceQuoting == false) { - this.name = name; - } else if (isQuotedIdentifier(name)) { - this.name = name; - quoted = true; - } else { - throw new IllegalArgumentException("[" + name + "] is not a valid CQL quoted or unquoted identifier"); - } - } - - public String toCql() { - String id = quoted ? CqlStringUtils.doubleQuote(name) : name; - return id; - } - - public StringBuilder toCql(StringBuilder sb) { - return sb.append(toCql()); - } - - @Override - public String toString() { - return toCql(); - } - - public String getName() { - return name; - } - - public boolean isQuoted() { - return quoted; - } - - public static CqlIdentifier cqlId(String identifier) { - CqlIdentifier id = new CqlIdentifier(identifier); - return id; - } - - public static CqlIdentifier quotedCqlId(String identifier) { - CqlIdentifier id = new CqlIdentifier(identifier, true); - return id; - } - - public static boolean isIdentifier(CharSequence chars) { - return isUnquotedIdentifier(chars) || isQuotedIdentifier(chars); - } - - public static boolean isUnquotedIdentifier(CharSequence chars) { - return UNQUOTED_IDENTIFIER_PATTERN.matcher(chars).matches(); - } - - public static boolean isQuotedIdentifier(CharSequence chars) { - return QUOTED_IDENTIFIER_PATTERN.matcher(chars).matches(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((name == null) ? 0 : name.hashCode()); - result = prime * result + (quoted ? 1231 : 1237); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - CqlIdentifier other = (CqlIdentifier) obj; - if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; - if (quoted != other.quoted) - return false; - return true; - } -} diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/ReservedKeyword.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/ReservedKeyword.java new file mode 100644 index 000000000..1b623552d --- /dev/null +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/ReservedKeyword.java @@ -0,0 +1,41 @@ +package org.springframework.cassandra.core; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * CQL keywords. + * + * @see http://cassandra.apache.org/doc/cql3/CQL.html#appendixA + * + * @author Matthew T. Adams + */ +public enum ReservedKeyword { + ADD, ALTER, AND, ANY, APPLY, ASC, AUTHORIZE, BATCH, BEGIN, BY, COLUMNFAMILY, CREATE, DELETE, DESC, DROP, EACH_QUORUM, FROM, GRANT, IN, INDEX, INSERT, INTO, KEYSPACE, LIMIT, LOCAL_ONE, LOCAL_QUORUM, MODIFY, NORECURSIVE, OF, ON, ONE, ORDER, PRIMARY, QUORUM, REVOKE, SCHEMA, SELECT, SET, TABLE, THREE, TOKEN, TRUNCATE, TWO, UPDATE, USE, USING, WHERE, WITH; + + /** + * @see ReservedKeyword#isReserved(String) + */ + public static boolean isReserved(CharSequence candidate) { + Assert.notNull(candidate); + return isReserved(candidate.toString()); + } + + /** + * Returns whether the given string is a CQL reserved keyword. This comparison is done regardless of case. + */ + public static boolean isReserved(String candidate) { + + if (!StringUtils.hasText(candidate)) { + return false; + } + + try { + Enum.valueOf(ReservedKeyword.class, candidate.toUpperCase()); + return true; + } catch (IllegalArgumentException x) { + return false; + } + } +} diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlConstantType.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlConstantType.java new file mode 100644 index 000000000..e1b94447e --- /dev/null +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlConstantType.java @@ -0,0 +1,42 @@ +package org.springframework.cassandra.core.cql; + +import static org.springframework.cassandra.core.cql.CqlConstantType.Regex.*; +import java.util.regex.Pattern; + +public enum CqlConstantType { + + STRING(STRING_PATTERN), INTEGER(INTEGER_PATTERN), FLOAT(FLOAT_PATTERN), BOOLEAN(BOOLEAN_PATTERN), UUID(UUID_PATTERN), BLOB( + BLOB_PATTERN); + + private Pattern pattern; + + private CqlConstantType(Pattern pattern) { + this.pattern = pattern; + } + + public boolean isValid(CharSequence candidate) { + return pattern.matcher(candidate).matches(); + } + + public static class Regex { + + // TODO: any sequence of characters encased in single quotes, as long as single quotes are doubled + public static final String STRING_REGEX = "\\'[.TODO]*+\\'"; + public static final Pattern STRING_PATTERN = Pattern.compile(STRING_REGEX); + + public static final String INTEGER_REGEX = "\\-?[0-9]+"; + public static final Pattern INTEGER_PATTERN = Pattern.compile(INTEGER_REGEX); + + public static final String FLOAT_REGEX = "(\\-?[0-9]+(\\.[0-9]*)?([eE][+-]?[0-9+])?)|NaN|Infinity"; + public static final Pattern FLOAT_PATTERN = Pattern.compile(FLOAT_REGEX); + + public static final String BOOLEAN_REGEX = "(?i)true|false"; + public static final Pattern BOOLEAN_PATTERN = Pattern.compile(BOOLEAN_REGEX); + + public static final String UUID_REGEX = "(?i)[0-9A-F]{8}+\\-[0-9A-F]{4}+\\-[0-9A-F]{4}+\\-[0-9A-F]{4}+\\-[0-9A-F]{12}+"; + public static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX); + + public static final String BLOB_REGEX = "(?i)0[X](0-9A-F)+"; + public static final Pattern BLOB_PATTERN = Pattern.compile(BLOB_REGEX); + } +} \ No newline at end of file diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlIdentifier.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlIdentifier.java new file mode 100644 index 000000000..d327c9706 --- /dev/null +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlIdentifier.java @@ -0,0 +1,202 @@ +package org.springframework.cassandra.core.cql; + +import java.util.regex.Pattern; + +import org.springframework.cassandra.core.ReservedKeyword; +import org.springframework.util.Assert; + +import com.datastax.driver.core.KeyspaceMetadata; +import com.datastax.driver.core.TableMetadata; + +/** + * This encapsulates the logic for CQL quoted and unquoted identifiers. + *

+ * CQL identifiers, when unquoted, are converted to lower case. When quoted, they are returned as-is with no lower + * casing and encased in double quotes. To render, use any of the methods {@link #toCql()}, + * {@link #toCql(StringBuilder)}, or {@link #toString()}. + * + * @see #CqlIdentifier(String) + * @see #CqlIdentifier(String, boolean) + * @see #toCql() + * @see #toCql(StringBuilder) + * @see #toString() + * + * @author John McPeek + * @author Matthew T. Adams + */ +public final class CqlIdentifier implements Comparable { + + public static final String UNQUOTED_REGEX = "(?i)[a-z][\\w]*"; + public static final Pattern UNQUOTED = Pattern.compile(UNQUOTED_REGEX); + + public static final String QUOTED_REGEX = "(?i)[a-z]([\\w]*(\"\")+[\\w]*)+"; + public static final Pattern QUOTED = Pattern.compile(QUOTED_REGEX); + + /** + * Factory method for {@link CqlIdentifier}. Convenient if imported statically. + * + * @see #CqlIdentifier(String) + */ + public static CqlIdentifier cqlId(CharSequence identifier) { + return new CqlIdentifier(identifier); + } + + /** + * Factory method for a force-quoted {@link CqlIdentifier}. Convenient if imported statically. + * + * @see #CqlIdentifier(String, boolean) + */ + public static CqlIdentifier quotedCqlId(CharSequence identifier) { + return new CqlIdentifier(identifier, true); + } + + /** + * Returns true if the given {@link CharSequence} is a legal unquoted identifier. + */ + public static boolean isUnquotedIdentifier(CharSequence chars) { + return UNQUOTED.matcher(chars).matches() && !ReservedKeyword.isReserved(chars); + } + + /** + * Returns true if the given {@link CharSequence} is a legal unquoted identifier. + */ + public static boolean isQuotedIdentifier(CharSequence chars) { + return QUOTED.matcher(chars).matches() || ReservedKeyword.isReserved(chars); + } + + private String identifier; + private String unquoted; + private boolean quoted; + + /** + * Creates a new {@link CqlIdentifier} without force-quoting it. It may end up quoted, depending on its value. + * + * @see #cqlId(String) + */ + public CqlIdentifier(CharSequence identifier) { + this(identifier, false); + } + + /** + * Creates a new CQL identifier, optionally force-quoting it. Force-quoting can be used to preserve identifier case. + *

+ * + * @see #cqlId(String) + * @see #quotedCqlId(String) + */ + public CqlIdentifier(CharSequence identifier, boolean forceQuote) { + setIdentifier(identifier, forceQuote); + } + + /** + * Tests & sets the given identifier. + */ + private void setIdentifier(CharSequence identifier, boolean forceQuoting) { + + Assert.notNull(identifier); + + String string = identifier.toString(); + Assert.hasText(string); + + if (forceQuoting || isQuotedIdentifier(string)) { + this.unquoted = string; + this.identifier = "\"" + string + "\""; + quoted = true; + } else if (isUnquotedIdentifier(string)) { + this.identifier = this.unquoted = string.toLowerCase(); + } else { + throw new IllegalArgumentException(String.format( + "given string [%s] is not a valid quoted or unquoted identifier", identifier)); + } + } + + /** + * Returns the identifier without encasing quotes, regardless of the value of {@link #isQuoted()}. If + * {@link #isQuoted()} is true, then this value will be the same as {@link #toCql()} and + * {@link #toString()}. If {@link #isQuoted()} is false, it will be different. + *

+ * This is needed, for example, to get the correct {@link TableMetadata} from + * {@link KeyspaceMetadata#getTable(String)}: the given string must not be quoted. + */ + public String getUnquoted() { + return unquoted; + } + + /** + * Renders this identifier appropriately. + */ + public String toCql() { + return identifier; + } + + /** + * Appends the rendering of this identifier to the given {@link StringBuilder}, then returns that + * {@link StringBuilder}. If null is given, a new {@link StringBuilder} is created, appended to, and + * returned. + */ + public StringBuilder toCql(StringBuilder sb) { + sb = sb == null ? new StringBuilder() : sb; + return sb.append(toCql()); + } + + /** + * Alias for {@link #toCql()}. + */ + @Override + public String toString() { + return toCql(); + } + + /** + * Whether or not this identifier is quoted. + */ + public boolean isQuoted() { + return quoted; + } + + @Override + public int hashCode() { + return ((Boolean) quoted).hashCode() ^ identifier.hashCode(); + } + + /** + * Compares this {@link CqlIdentifier} to the given object. Note that if a {@link CharSequence} is given, a new + * {@link CqlIdentifier} is created from it and compared, such that a {@link CharSequence} can be effectively equal to + * a {@link CqlIdentifier}. + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + if (that == null) { + return false; + } + if (!(that instanceof CqlIdentifier) && !(that instanceof CharSequence)) { + return false; + } + + CqlIdentifier other = (that instanceof CqlIdentifier) ? (CqlIdentifier) that : cqlId((CharSequence) that); + + return this.quoted == other.quoted && this.identifier.equals(other.identifier); + } + + /** + * Unquoted identifiers sort before quoted ones. Otherwise, they compare according to their identifiers. + */ + @Override + public int compareTo(CqlIdentifier that) { + + int comparison = ((Boolean) this.quoted).compareTo(that.quoted); + if (comparison != 0) { + return comparison; + } + return this.identifier.compareTo(that.identifier); + } +} diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlStringUtils.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlStringUtils.java index 73d0c70e3..31cfcb51c 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlStringUtils.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlStringUtils.java @@ -15,8 +15,6 @@ */ package org.springframework.cassandra.core.cql; -import java.util.regex.Pattern; - import com.datastax.driver.core.DataType; public class CqlStringUtils { @@ -33,9 +31,6 @@ public class CqlStringUtils { return sb == null ? new StringBuilder() : sb; } - public static final String UNESCAPED_DOUBLE_QUOTE_REGEX = "TODO"; - public static final Pattern UNESCAPED_DOUBLE_QUOTE_PATTERN = Pattern.compile(UNESCAPED_DOUBLE_QUOTE_REGEX); - /** * Renders the given string as a legal Cassandra string column or table option value, by escaping single quotes and * encasing the result in single quotes. Given null, returns null. @@ -51,15 +46,15 @@ public class CqlStringUtils { /** * Doubles single quote characters (' -> ''). Given null, returns null. */ - public static String escapeSingle(Object things) { - return things == null ? (String) null : things.toString().replace(SINGLE_QUOTE, DOUBLE_SINGLE_QUOTE); + public static String escapeSingle(Object thing) { + return thing == null ? (String) null : thing.toString().replace(SINGLE_QUOTE, DOUBLE_SINGLE_QUOTE); } /** * Doubles double quote characters (" -> ""). Given null, returns null. */ - public static String escapeDouble(Object things) { - return things == null ? (String) null : things.toString().replace(DOUBLE_QUOTE, DOUBLE_DOUBLE_QUOTE); + public static String escapeDouble(Object thing) { + return thing == null ? (String) null : thing.toString().replace(DOUBLE_QUOTE, DOUBLE_DOUBLE_QUOTE); } /** @@ -116,4 +111,21 @@ public class CqlStringUtils { return s.append(TYPE_PARAMETER_SUFFIX).toString(); } + + public static String unquote(String s) { + return unquote(s, "\""); + } + + public static String unquote(String s, String quoteChar) { + if (s == null) { + return s; + } + if (!s.startsWith(quoteChar) || !s.endsWith(quoteChar)) { + return s; + } + if (s.length() <= 2) { + return s; + } + return s.substring(1, s.length() - 1); + } } diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/generator/AddColumnCqlGenerator.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/generator/AddColumnCqlGenerator.java index 086770417..f2aedc6ed 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/generator/AddColumnCqlGenerator.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/generator/AddColumnCqlGenerator.java @@ -30,8 +30,8 @@ public class AddColumnCqlGenerator extends ColumnChangeCqlGenerator { + /** + * Entry point into the {@link AlterTableSpecification}'s fluent API to alter a table. Convenient if imported + * statically. + */ + public static AlterTableSpecification alterTable() { + return new AlterTableSpecification(); + } + /** * The list of column changes. */ @@ -65,12 +73,4 @@ public class AlterTableSpecification extends TableOptionsSpecification getChanges() { return Collections.unmodifiableList(changes); } - - /** - * Entry point into the {@link AlterTableSpecification}'s fluent API to alter a table. Convenient if imported - * statically. - */ - public static AlterTableSpecification alterTable() { - return new AlterTableSpecification(); - } } diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnChangeSpecification.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnChangeSpecification.java index 0cdc23ee1..96441470f 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnChangeSpecification.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnChangeSpecification.java @@ -15,7 +15,10 @@ */ package org.springframework.cassandra.core.keyspace; -import org.springframework.cassandra.core.CqlIdentifier; +import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; + +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.util.Assert; /** * Base class for column change specifications. @@ -24,21 +27,22 @@ import org.springframework.cassandra.core.CqlIdentifier; */ public abstract class ColumnChangeSpecification { - private CqlIdentifier identifier; + protected CqlIdentifier name; - public ColumnChangeSpecification(String name) { + protected ColumnChangeSpecification(String name) { + this(cqlId(name)); + } + + protected ColumnChangeSpecification(CqlIdentifier name) { setName(name); } - private void setName(String name) { - identifier = new CqlIdentifier(name); + protected void setName(CqlIdentifier name) { + Assert.notNull(name); + this.name = name; } - public String getName() { - return identifier.getName(); - } - - public String getNameAsIdentifier() { - return identifier.toCql(); + public CqlIdentifier getName() { + return name; } } diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnSpecification.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnSpecification.java index ca8a2eec3..4c86dd544 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnSpecification.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnSpecification.java @@ -18,11 +18,13 @@ package org.springframework.cassandra.core.keyspace; import static org.springframework.cassandra.core.Ordering.ASCENDING; import static org.springframework.cassandra.core.PrimaryKeyType.CLUSTERED; import static org.springframework.cassandra.core.PrimaryKeyType.PARTITIONED; +import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; import static org.springframework.cassandra.core.cql.CqlStringUtils.noNull; -import org.springframework.cassandra.core.CqlIdentifier; import org.springframework.cassandra.core.Ordering; import org.springframework.cassandra.core.PrimaryKeyType; +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.util.Assert; import com.datastax.driver.core.DataType; @@ -44,7 +46,7 @@ public class ColumnSpecification { */ public static final Ordering DEFAULT_ORDERING = ASCENDING; - private CqlIdentifier identifier; + private CqlIdentifier name; private DataType type; // TODO: determining if we should be coupling this to Datastax Java Driver type? private PrimaryKeyType keyType; private Ordering ordering; @@ -55,7 +57,12 @@ public class ColumnSpecification { * @return this */ public ColumnSpecification name(String name) { - identifier = new CqlIdentifier(name); + return name(cqlId(name)); + } + + public ColumnSpecification name(CqlIdentifier name) { + Assert.notNull(name); + this.name = name; return this; } @@ -130,7 +137,7 @@ public class ColumnSpecification { * * @return this */ - /* package */ColumnSpecification keyType(PrimaryKeyType keyType) { + ColumnSpecification keyType(PrimaryKeyType keyType) { this.keyType = keyType; return this; } @@ -140,17 +147,13 @@ public class ColumnSpecification { * * @return this */ - /* package */ColumnSpecification ordering(Ordering ordering) { + ColumnSpecification ordering(Ordering ordering) { this.ordering = ordering; return this; } - public String getName() { - return identifier.getName(); - } - - public String getNameAsIdentifier() { - return identifier.toCql(); + public CqlIdentifier getName() { + return name; } public DataType getType() { @@ -170,7 +173,7 @@ public class ColumnSpecification { } public StringBuilder toCql(StringBuilder cql) { - return (cql = noNull(cql)).append(identifier).append(" ").append(type); + return (cql = noNull(cql)).append(name).append(" ").append(type); } @Override diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnTypeChangeSpecification.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnTypeChangeSpecification.java index 814cfcd7a..9c04e382f 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnTypeChangeSpecification.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/ColumnTypeChangeSpecification.java @@ -15,6 +15,9 @@ */ package org.springframework.cassandra.core.keyspace; +import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; + +import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.util.Assert; import com.datastax.driver.core.DataType; @@ -29,6 +32,10 @@ public abstract class ColumnTypeChangeSpecification extends ColumnChangeSpecific private DataType type; public ColumnTypeChangeSpecification(String name, DataType type) { + this(cqlId(name), type); + } + + public ColumnTypeChangeSpecification(CqlIdentifier name, DataType type) { super(name); setType(type); } diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/CreateIndexSpecification.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/CreateIndexSpecification.java index 5744e5c1a..31f6a7dff 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/CreateIndexSpecification.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/CreateIndexSpecification.java @@ -15,7 +15,10 @@ */ package org.springframework.cassandra.core.keyspace; -import org.springframework.cassandra.core.CqlIdentifier; +import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; + +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -27,10 +30,18 @@ import org.springframework.util.StringUtils; public class CreateIndexSpecification extends IndexNameSpecification implements IndexDescriptor { + /** + * Entry point into the {@link CreateIndexSpecification}'s fluent API to create a index. Convenient if imported + * statically. + */ + public static CreateIndexSpecification createIndex() { + return new CreateIndexSpecification(); + } + private boolean ifNotExists = false; private boolean custom = false; - private CqlIdentifier identifier; - private String columnName; + private CqlIdentifier tableName; + private CqlIdentifier columnName; private String using; /** @@ -56,6 +67,7 @@ public class CreateIndexSpecification extends IndexNameSpecification { + /** + * Entry point into the {@link CreateKeyspaceSpecification}'s fluent API to create a keyspace. Convenient if imported + * statically. + */ + public static CreateKeyspaceSpecification createKeyspace() { + return new CreateKeyspaceSpecification(); + } + private boolean ifNotExists = false; /** @@ -31,14 +40,6 @@ public class CreateKeyspaceSpecification extends KeyspaceSpecification { + /** + * Entry point into the {@link CreateTableSpecification}'s fluent API to create a table. Convenient if imported + * statically. + */ + public static CreateTableSpecification createTable() { + return new CreateTableSpecification(); + } + private boolean ifNotExists = false; /** @@ -52,14 +60,6 @@ public class CreateTableSpecification extends TableSpecification The subtype of the {@link IndexNameSpecification} * * @author David Webb - * @param The subtype of the {@link IndexNameSpecification} + * @author Matthew T. Adams */ public abstract class IndexNameSpecification> { /** * The name of the index. */ - private CqlIdentifier identifier; + private CqlIdentifier name; /** * Sets the index name. * * @return this */ - @SuppressWarnings("unchecked") public T name(String name) { - identifier = new CqlIdentifier(name); + return name(cqlId(name)); + } + + @SuppressWarnings("unchecked") + public T name(CqlIdentifier name) { + Assert.notNull(name); + this.name = name; return (T) this; } - public String getName() { - return identifier.getName(); + public CqlIdentifier getName() { + return name; } - - public String getNameAsIdentifier() { - return identifier.toCql(); - } - } diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceActionSpecification.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceActionSpecification.java index f8fd83af7..4583dd47a 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceActionSpecification.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceActionSpecification.java @@ -1,6 +1,9 @@ package org.springframework.cassandra.core.keyspace; -import org.springframework.cassandra.core.CqlIdentifier; +import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; + +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.util.Assert; /** * Abstract builder class to support the construction of keyspace specifications. @@ -14,7 +17,16 @@ public abstract class KeyspaceActionSpecification thatSpec = (KeyspaceActionSpecification) that; - return this.identifier.equals(thatSpec.identifier) && this.getClass().equals(that.getClass()); + KeyspaceActionSpecification other = (KeyspaceActionSpecification) that; + return this.name.equals(other.name) && this.getClass().equals(that.getClass()); } @Override public int hashCode() { - return this.identifier.hashCode() ^ this.getClass().hashCode(); + return name.hashCode() ^ getClass().hashCode(); } - } diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceDescriptor.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceDescriptor.java index 0357c76f8..bed06cc90 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceDescriptor.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceDescriptor.java @@ -17,6 +17,8 @@ package org.springframework.cassandra.core.keyspace; import java.util.Map; +import org.springframework.cassandra.core.cql.CqlIdentifier; + /** * Describes a Keyspace. * @@ -27,12 +29,7 @@ public interface KeyspaceDescriptor { /** * Returns the name of the table. */ - String getName(); - - /** - * Returns the name of the table as an identifier or quoted identifier as appropriate. - */ - String getNameAsIdentifier(); + CqlIdentifier getName(); /** * Returns an unmodifiable {@link Map} of keyspace options. diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceOptionsSpecification.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceOptionsSpecification.java index 54fe4d331..05e4dcd0f 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceOptionsSpecification.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceOptionsSpecification.java @@ -27,6 +27,7 @@ public abstract class KeyspaceOptionsSpecification options = new LinkedHashMap(); + @Override @SuppressWarnings("unchecked") public T name(String name) { return (T) super.name(name); @@ -53,7 +54,7 @@ public abstract class KeyspaceOptionsSpecification getOptions() { return Collections.unmodifiableMap(options); } - } diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceSpecification.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceSpecification.java index ab13b2510..bc397c19c 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceSpecification.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/KeyspaceSpecification.java @@ -20,6 +20,7 @@ package org.springframework.cassandra.core.keyspace; * as a standalone {@link KeyspaceDescriptor}, independent of {@link CreateKeyspaceSpecification}. * * @author John McPeek + * @author Matthew T. Adams */ public class KeyspaceSpecification extends KeyspaceOptionsSpecification> implements KeyspaceDescriptor { diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableDescriptor.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableDescriptor.java index 116e1dedd..31b115b38 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableDescriptor.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableDescriptor.java @@ -18,6 +18,8 @@ package org.springframework.cassandra.core.keyspace; import java.util.List; import java.util.Map; +import org.springframework.cassandra.core.cql.CqlIdentifier; + /** * Describes a table. * @@ -29,12 +31,7 @@ public interface TableDescriptor { /** * Returns the name of the table. */ - String getName(); - - /** - * Returns the name of the table as an identifer or quoted identifier as appropriate. - */ - String getNameAsIdentifier(); + CqlIdentifier getName(); /** * Returns an unmodifiable {@link List} of {@link ColumnSpecification}s. diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableNameSpecification.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableNameSpecification.java index 2553bab61..93f8c5189 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableNameSpecification.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableNameSpecification.java @@ -15,7 +15,10 @@ */ package org.springframework.cassandra.core.keyspace; -import org.springframework.cassandra.core.CqlIdentifier; +import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; + +import org.springframework.cassandra.core.cql.CqlIdentifier; +import org.springframework.util.Assert; /** * Abstract builder class to support the construction of table specifications. @@ -28,24 +31,25 @@ public abstract class TableNameSpecification /** * The name of the table. */ - private CqlIdentifier identifier; + private CqlIdentifier name; /** * Sets the table name. * * @return this */ - @SuppressWarnings("unchecked") public T name(String name) { - identifier = new CqlIdentifier(name); + return name(cqlId(name)); + } + + @SuppressWarnings("unchecked") + public T name(CqlIdentifier name) { + Assert.notNull(name); + this.name = name; return (T) this; } - public String getName() { - return identifier.getName(); - } - - public String getNameAsIdentifier() { - return identifier.toCql(); + public CqlIdentifier getName() { + return name; } } diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableOptionsSpecification.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableOptionsSpecification.java index ad603a118..354fe3810 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableOptionsSpecification.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/keyspace/TableOptionsSpecification.java @@ -22,8 +22,11 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.cql.CqlStringUtils; +import com.datastax.driver.core.DataType; + /** * Abstract builder class to support the construction of table specifications that have table options, that is, those * options normally specified by WITH ... AND .... @@ -42,11 +45,18 @@ public abstract class TableOptionsSpecification options = new LinkedHashMap(); + @Override @SuppressWarnings("unchecked") public T name(String name) { return (T) super.name(name); } + @Override + @SuppressWarnings("unchecked") + public T name(CqlIdentifier name) { + return (T) super.name(name); + } + /** * Convenience method that calls with(option, null). * @@ -68,7 +78,7 @@ public abstract class TableOptionsSpecification extends TableOptionsSpecification extends TableOptionsSpecification extends TableOptionsSpecificationnull meaning ascending). Must * be specified after all partition key columns and before any non-key columns. @@ -98,6 +112,10 @@ public class TableSpecification extends TableOptionsSpecification extends TableOptionsSpecification extends TableOptionsSpecification getColumns() { return Collections.unmodifiableList(columns); } @@ -145,6 +168,7 @@ public class TableSpecification extends TableOptionsSpecification getPartitionKeyColumns() { return Collections.unmodifiableList(partitionKeyColumns); } @@ -152,6 +176,7 @@ public class TableSpecification extends TableOptionsSpecification getClusteredKeyColumns() { return Collections.unmodifiableList(clusteredKeyColumns); } @@ -159,6 +184,7 @@ public class TableSpecification extends TableOptionsSpecification getPrimaryKeyColumns() { ArrayList primaryKeyColumns = new ArrayList(); @@ -171,6 +197,7 @@ public class TableSpecification extends TableOptionsSpecification getNonKeyColumns() { return Collections.unmodifiableList(nonKeyColumns); } diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlIndexSpecificationAssertions.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlIndexSpecificationAssertions.java index 34930f284..f72c924c2 100644 --- a/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlIndexSpecificationAssertions.java +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlIndexSpecificationAssertions.java @@ -29,14 +29,14 @@ public class CqlIndexSpecificationAssertions { public static void assertIndex(IndexDescriptor expected, String keyspace, Session session) { IndexMetadata imd = session.getCluster().getMetadata().getKeyspace(keyspace.toLowerCase()) - .getTable(expected.getTableName()).getColumn(expected.getColumnName()).getIndex(); + .getTable(expected.getTableName().toCql()).getColumn(expected.getColumnName().toCql()).getIndex(); - assertEquals(expected.getName().toLowerCase(), imd.getName().toLowerCase()); + assertEquals(expected.getName(), imd.getName()); } public static void assertNoIndex(IndexDescriptor expected, String keyspace, Session session) { IndexMetadata imd = session.getCluster().getMetadata().getKeyspace(keyspace.toLowerCase()) - .getTable(expected.getTableName()).getColumn(expected.getColumnName()).getIndex(); + .getTable(expected.getTableName().toCql()).getColumn(expected.getColumnName().toCql()).getIndex(); assertNull(imd); } diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlTableSpecificationAssertions.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlTableSpecificationAssertions.java index 70b8b8adc..f8a5e830f 100644 --- a/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlTableSpecificationAssertions.java +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlTableSpecificationAssertions.java @@ -42,9 +42,9 @@ public class CqlTableSpecificationAssertions { public static void assertTable(TableDescriptor expected, String keyspace, Session session) { TableMetadata tmd = session.getCluster().getMetadata().getKeyspace(keyspace.toLowerCase()) - .getTable(expected.getName()); + .getTable(CqlStringUtils.unquote(expected.getName().toCql())); // TODO: talk to Datastax about unquoting - assertEquals(expected.getName().toLowerCase(), tmd.getName().toLowerCase()); + assertEquals(CqlStringUtils.unquote(expected.getName().toCql()), tmd.getName()); // TODO: talk to Datastax assertPartitionKeyColumns(expected, tmd); assertPrimaryKeyColumns(expected, tmd); assertColumns(expected.getColumns(), tmd.getColumns()); @@ -53,7 +53,7 @@ public class CqlTableSpecificationAssertions { public static void assertNoTable(DropTableSpecification expected, String keyspace, Session session) { TableMetadata tmd = session.getCluster().getMetadata().getKeyspace(keyspace.toLowerCase()) - .getTable(expected.getName()); + .getTable(expected.getName().toCql()); assertNull(tmd); } @@ -171,7 +171,7 @@ public class CqlTableSpecificationAssertions { } public static void assertColumn(ColumnSpecification expected, ColumnMetadata actual) { - assertEquals(expected.getName().toLowerCase(), actual.getName().toLowerCase()); + assertEquals(expected.getName().toCql(), actual.getName()); assertEquals(expected.getType(), actual.getType()); } } \ No newline at end of file diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/FunkyIdentifierIntegrationTest.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/FunkyIdentifierIntegrationTest.java new file mode 100644 index 000000000..645bdc507 --- /dev/null +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/FunkyIdentifierIntegrationTest.java @@ -0,0 +1,42 @@ +package org.springframework.cassandra.test.integration.core.cql.generator; + +import org.junit.Test; +import org.springframework.cassandra.core.cql.generator.CreateTableCqlGenerator; +import org.springframework.cassandra.core.keyspace.CreateTableSpecification; +import org.springframework.cassandra.test.integration.AbstractKeyspaceCreatingIntegrationTest; +import org.springframework.cassandra.test.unit.core.cql.generator.CreateTableCqlGeneratorTests.FunkyTableNameTest; + +import com.datastax.driver.core.DataType; + +public class FunkyIdentifierIntegrationTest extends AbstractKeyspaceCreatingIntegrationTest { + + public FunkyIdentifierIntegrationTest() { + super(randomKeyspaceName()); + } + + @Test + public void testFunkyTableName() { + for (String name : FunkyTableNameTest.FUNKY_LEGAL_NAMES) { + SESSION.execute(new CreateTableCqlGenerator(CreateTableSpecification.createTable().name(name) + .partitionKeyColumn("key", DataType.text())).toCql()); + } + } + + @Test + public void testFunkyColumnName() { + String table = "funky"; + int i = 0; + for (String name : FunkyTableNameTest.FUNKY_LEGAL_NAMES) { + SESSION.execute(new CreateTableCqlGenerator(CreateTableSpecification.createTable().name(table + i++) + .partitionKeyColumn(name, DataType.text())).toCql()); + } + } + + @Test + public void testFunkyTableAndColumnName() { + for (String name : FunkyTableNameTest.FUNKY_LEGAL_NAMES) { + SESSION.execute(new CreateTableCqlGenerator(CreateTableSpecification.createTable().name(name) + .partitionKeyColumn(name, DataType.text())).toCql()); + } + } +} \ No newline at end of file diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/CqlIdentifierTest.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/CqlIdentifierTest.java index 8b404ec5e..f9b9f5a65 100644 --- a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/CqlIdentifierTest.java +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/CqlIdentifierTest.java @@ -1,20 +1,63 @@ package org.springframework.cassandra.test.unit.core.cql; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.springframework.cassandra.core.CqlIdentifier.isQuotedIdentifier; -import static org.springframework.cassandra.core.CqlIdentifier.isUnquotedIdentifier; +import static org.junit.Assert.*; +import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; +import static org.springframework.cassandra.core.cql.CqlIdentifier.quotedCqlId; import org.junit.Test; +import org.springframework.cassandra.core.ReservedKeyword; +import org.springframework.cassandra.core.cql.CqlIdentifier; public class CqlIdentifierTest { @Test - public void testIsQuotedIdentifier() throws Exception { - assertFalse(isQuotedIdentifier("my\"id")); - assertTrue(isQuotedIdentifier("my\"\"id")); - assertFalse(isUnquotedIdentifier("my\"id")); - assertTrue(isUnquotedIdentifier("myid")); + public void testUnquotedIdentifiers() { + + String[] ids = new String[] { "foo", "Foo", "FOO", "a_", "a1" }; + + for (String id : ids) { + CqlIdentifier cqlId = cqlId(id); + assertFalse(cqlId.isQuoted()); + assertEquals(id.toLowerCase(), cqlId.toCql()); + } } -} + @Test + public void testForceQuotedIdentifiers() { + + String[] ids = new String[] { "foo", "Foo", "FOO", "a_", "a1" }; + + for (String id : ids) { + CqlIdentifier cqlId = quotedCqlId(id); + assertTrue(cqlId.isQuoted()); + assertEquals("\"" + id + "\"", cqlId.toCql()); + } + } + + @Test + public void testReservedWordsEndUpQuoted() { + + for (ReservedKeyword id : ReservedKeyword.values()) { + CqlIdentifier cqlId = cqlId(id.name()); + assertTrue(cqlId.isQuoted()); + assertEquals("\"" + id.name() + "\"", cqlId.toCql()); + + cqlId = cqlId(id.name().toLowerCase()); + assertTrue(cqlId.isQuoted()); + assertEquals("\"" + id.name().toLowerCase() + "\"", cqlId.toCql()); + } + } + + @Test + public void testIllegals() { + String[] illegals = new String[] { null, "", "a ", "a a", "a\"", "a'", "a''", "\"\"", "''", "-", "a-", "_", "_a" }; + for (String illegal : illegals) { + try { + cqlId(illegal); + fail(String.format("identifier [%s] should have caused IllegalArgumentException", illegal)); + } catch (IllegalArgumentException x) { + // :) + } + } + } +} \ No newline at end of file diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/AlterTableCqlGeneratorTests.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/AlterTableCqlGeneratorTests.java index e1b3fe86e..e281fdaa7 100644 --- a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/AlterTableCqlGeneratorTests.java +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/AlterTableCqlGeneratorTests.java @@ -26,7 +26,6 @@ public class AlterTableCqlGeneratorTests { * Asserts that the preamble is first & correctly formatted in the given CQL string. */ public static void assertPreamble(String tableName, String cql) { - System.out.println("cql: " + cql); assertTrue(cql.startsWith("ALTER TABLE " + tableName + " ")); } diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/CreateTableCqlGeneratorTests.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/CreateTableCqlGeneratorTests.java index e2befa5cb..a85344baa 100644 --- a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/CreateTableCqlGeneratorTests.java +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/CreateTableCqlGeneratorTests.java @@ -1,13 +1,20 @@ package org.springframework.cassandra.test.unit.core.cql.generator; import static org.junit.Assert.assertTrue; +import static org.springframework.cassandra.core.cql.CqlIdentifier.cqlId; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.cassandra.core.ReservedKeyword; +import org.springframework.cassandra.core.cql.CqlIdentifier; import org.springframework.cassandra.core.cql.generator.CreateTableCqlGenerator; import org.springframework.cassandra.core.keyspace.CreateTableSpecification; import org.springframework.cassandra.core.keyspace.Option; @@ -25,7 +32,7 @@ public class CreateTableCqlGeneratorTests { /** * Asserts that the preamble is first & correctly formatted in the given CQL string. */ - public static void assertPreamble(String tableName, String cql) { + public static void assertPreamble(CqlIdentifier tableName, String cql) { assertTrue(cql.startsWith("CREATE TABLE " + tableName + " ")); } @@ -83,6 +90,7 @@ public class CreateTableCqlGeneratorTests { public static abstract class CreateTableTest extends TableOperationCqlGeneratorTest { + @Override public CreateTableCqlGenerator generator() { return new CreateTableCqlGenerator(specification); } @@ -90,12 +98,13 @@ public class CreateTableCqlGeneratorTests { public static class BasicTest extends CreateTableTest { - public String name = "mytable"; + public CqlIdentifier name = cqlId("mytable"); public DataType partitionKeyType0 = DataType.text(); - public String partitionKey0 = "partitionKey0"; + public CqlIdentifier partitionKey0 = cqlId("partitionKey0"); public DataType columnType1 = DataType.text(); public String column1 = "column1"; + @Override public CreateTableSpecification specification() { return CreateTableSpecification.createTable().name(name).partitionKeyColumn(partitionKey0, partitionKeyType0) .column(column1, columnType1); @@ -107,18 +116,18 @@ public class CreateTableCqlGeneratorTests { assertPreamble(name, cql); assertColumns(String.format("%s %s, %s %s", partitionKey0, partitionKeyType0, column1, columnType1), cql); - assertPrimaryKey(partitionKey0, cql); + assertPrimaryKey(partitionKey0.toCql(), cql); } } public static class CompositePartitionKeyTest extends CreateTableTest { - public String name = "composite_partition_key_table"; + public CqlIdentifier name = cqlId("composite_partition_key_table"); public DataType partKeyType0 = DataType.text(); - public String partKey0 = "partKey0"; + public CqlIdentifier partKey0 = cqlId("partKey0"); public DataType partKeyType1 = DataType.text(); - public String partKey1 = "partKey1"; - public String column0 = "column0"; + public CqlIdentifier partKey1 = cqlId("partKey1"); + public CqlIdentifier column0 = cqlId("column0"); public DataType columnType0 = DataType.text(); @Override @@ -147,19 +156,20 @@ public class CreateTableCqlGeneratorTests { */ public static class ReadRepairChanceTest extends CreateTableTest { - public String name = "mytable"; + public CqlIdentifier name = cqlId("mytable"); public DataType partitionKeyType0 = DataType.text(); - public String partitionKey0 = "partitionKey0"; + public CqlIdentifier partitionKey0 = cqlId("partitionKey0"); public DataType partitionKeyType1 = DataType.timestamp(); - public String partitionKey1 = "create_timestamp"; + public CqlIdentifier partitionKey1 = cqlId("create_timestamp"); public DataType columnType1 = DataType.text(); - public String column1 = "column1"; + public CqlIdentifier column1 = cqlId("column1"); public Double readRepairChance = 0.5; + @Override public CreateTableSpecification specification() { - return (CreateTableSpecification) CreateTableSpecification.createTable().name(name) - .partitionKeyColumn(partitionKey0, partitionKeyType0).partitionKeyColumn(partitionKey1, partitionKeyType1) - .column(column1, columnType1).with(TableOption.READ_REPAIR_CHANCE, readRepairChance); + return CreateTableSpecification.createTable().name(name).partitionKeyColumn(partitionKey0, partitionKeyType0) + .partitionKeyColumn(partitionKey1, partitionKeyType1).column(column1, columnType1) + .with(TableOption.READ_REPAIR_CHANCE, readRepairChance); } @Test @@ -182,13 +192,13 @@ public class CreateTableCqlGeneratorTests { */ public static class MultipleOptionsTest extends CreateTableTest { - public String name = "timeseries_table"; + public CqlIdentifier name = cqlId("timeseries_table"); public DataType partitionKeyType0 = DataType.timeuuid(); - public String partitionKey0 = "tid"; + public CqlIdentifier partitionKey0 = cqlId("tid"); public DataType partitionKeyType1 = DataType.timestamp(); - public String partitionKey1 = "create_timestamp"; + public CqlIdentifier partitionKey1 = cqlId("create_timestamp"); public DataType columnType1 = DataType.text(); - public String column1 = "data_point"; + public CqlIdentifier column1 = cqlId("data_point"); public Double readRepairChance = 0.5; public Double dcLocalReadRepairChance = 0.7; public Double bloomFilterFpChance = 0.001; @@ -198,6 +208,7 @@ public class CreateTableCqlGeneratorTests { public Map compactionMap = new LinkedHashMap(); public Map compressionMap = new LinkedHashMap(); + @Override public CreateTableSpecification specification() { // Compaction @@ -208,11 +219,11 @@ public class CreateTableCqlGeneratorTests { compressionMap.put(CompressionOption.CHUNK_LENGTH_KB, 128); compressionMap.put(CompressionOption.CRC_CHECK_CHANCE, 0.75); - return (CreateTableSpecification) CreateTableSpecification.createTable().name(name) - .partitionKeyColumn(partitionKey0, partitionKeyType0).partitionKeyColumn(partitionKey1, partitionKeyType1) - .column(column1, columnType1).with(TableOption.COMPACT_STORAGE) - .with(TableOption.READ_REPAIR_CHANCE, readRepairChance).with(TableOption.COMPACTION, compactionMap) - .with(TableOption.COMPRESSION, compressionMap).with(TableOption.BLOOM_FILTER_FP_CHANCE, bloomFilterFpChance) + return CreateTableSpecification.createTable().name(name).partitionKeyColumn(partitionKey0, partitionKeyType0) + .partitionKeyColumn(partitionKey1, partitionKeyType1).column(column1, columnType1) + .with(TableOption.COMPACT_STORAGE).with(TableOption.READ_REPAIR_CHANCE, readRepairChance) + .with(TableOption.COMPACTION, compactionMap).with(TableOption.COMPRESSION, compressionMap) + .with(TableOption.BLOOM_FILTER_FP_CHANCE, bloomFilterFpChance) .with(TableOption.CACHING, CachingOption.KEYS_ONLY).with(TableOption.REPLICATE_ON_WRITE, replcateOnWrite) .with(TableOption.COMMENT, comment).with(TableOption.DCLOCAL_READ_REPAIR_CHANCE, dcLocalReadRepairChance) .with(TableOption.GC_GRACE_SECONDS, gcGraceSeconds); @@ -241,4 +252,50 @@ public class CreateTableCqlGeneratorTests { } } + public static class FunkyTableNameTest { + + public static final List FUNKY_LEGAL_NAMES; + + static { + List funkies = new ArrayList(Arrays.asList(new String[] {})); + // TODO: should these work? "a \"\" x", "a\"\"\"\"x", "a b" + for (ReservedKeyword funky : ReservedKeyword.values()) { + funkies.add(funky.name()); + } + FUNKY_LEGAL_NAMES = Collections.unmodifiableList(funkies); + } + + @Test + public void test() { + for (String name : FUNKY_LEGAL_NAMES) { + new TableNameTest(name).test(); + } + } + } + + /** + * This class is supposed to be used by other test classes. + */ + public static class TableNameTest extends CreateTableTest { + + public String tableName; + + public TableNameTest(String tableName) { + this.tableName = tableName; + } + + @Override + public CreateTableSpecification specification() { + return CreateTableSpecification.createTable().name(tableName).partitionKeyColumn(cqlId("pk"), DataType.text()); + } + + /** + * There is no @Test annotation on this method on purpose! It's supposed to be called by another test class's @Test + * method so that you can loop, calling this test method as many times as are necessary. + */ + public void test() { + prepare(); + assertPreamble(cqlId(tableName), cql); + } + } } diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/TableOperationCqlGeneratorTest.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/TableOperationCqlGeneratorTest.java index cb5172d2c..da6f746a0 100644 --- a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/TableOperationCqlGeneratorTest.java +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/TableOperationCqlGeneratorTest.java @@ -19,7 +19,6 @@ public abstract class TableOperationCqlGeneratorTest partitionKeyColumns = spec.getPartitionKeyColumns(); assertEquals(1, partitionKeyColumns.size()); ColumnSpecification partitionKeyColumn = partitionKeyColumns.get(0); - assertEquals("z", partitionKeyColumn.getName()); + assertEquals("z", partitionKeyColumn.getName().toCql()); assertEquals(PrimaryKeyType.PARTITIONED, partitionKeyColumn.getKeyType()); assertEquals(DataType.text(), partitionKeyColumn.getType()); List clusteredKeyColumns = spec.getClusteredKeyColumns(); assertEquals(1, clusteredKeyColumns.size()); ColumnSpecification clusteredKeyColumn = clusteredKeyColumns.get(0); - assertEquals("a", clusteredKeyColumn.getName()); + assertEquals("a", clusteredKeyColumn.getName().toCql()); assertEquals(PrimaryKeyType.CLUSTERED, clusteredKeyColumn.getKeyType()); assertEquals(DataType.text(), partitionKeyColumn.getType()); } From 9e968e29e2d78177ced30f2ed4d3031f5307029f Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Mon, 17 Feb 2014 10:11:05 -0600 Subject: [PATCH 2/2] DATACASS-96 - minor edits --- .../cassandra/core/cql/CqlConstantType.java | 9 ++++----- .../cassandra/core/cql/CqlIdentifier.java | 6 +++--- .../cql/generator/CqlTableSpecificationAssertions.java | 6 +++--- .../core/cql/generator/CreateTableCqlGeneratorTests.java | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlConstantType.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlConstantType.java index e1b94447e..9fea872e3 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlConstantType.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlConstantType.java @@ -14,14 +14,13 @@ public enum CqlConstantType { this.pattern = pattern; } - public boolean isValid(CharSequence candidate) { + public boolean matches(CharSequence candidate) { return pattern.matcher(candidate).matches(); } public static class Regex { - // TODO: any sequence of characters encased in single quotes, as long as single quotes are doubled - public static final String STRING_REGEX = "\\'[.TODO]*+\\'"; + public static final String STRING_REGEX = "'((?:[^']+|'')*)'"; public static final Pattern STRING_PATTERN = Pattern.compile(STRING_REGEX); public static final String INTEGER_REGEX = "\\-?[0-9]+"; @@ -33,10 +32,10 @@ public enum CqlConstantType { public static final String BOOLEAN_REGEX = "(?i)true|false"; public static final Pattern BOOLEAN_PATTERN = Pattern.compile(BOOLEAN_REGEX); - public static final String UUID_REGEX = "(?i)[0-9A-F]{8}+\\-[0-9A-F]{4}+\\-[0-9A-F]{4}+\\-[0-9A-F]{4}+\\-[0-9A-F]{12}+"; + public static final String UUID_REGEX = "(?i)[0-9a-f]{8}+\\-[0-9a-f]{4}+\\-[0-9a-f]{4}+\\-[0-9a-f]{4}+\\-[0-9a-f]{12}+"; public static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX); - public static final String BLOB_REGEX = "(?i)0[X](0-9A-F)+"; + public static final String BLOB_REGEX = "(?i)0[x](0-9a-f)+"; public static final Pattern BLOB_PATTERN = Pattern.compile(BLOB_REGEX); } } \ No newline at end of file diff --git a/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlIdentifier.java b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlIdentifier.java index d327c9706..393e8508b 100644 --- a/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlIdentifier.java +++ b/spring-cassandra/src/main/java/org/springframework/cassandra/core/cql/CqlIdentifier.java @@ -117,9 +117,9 @@ public final class CqlIdentifier implements Comparable { } /** - * Returns the identifier without encasing quotes, regardless of the value of {@link #isQuoted()}. If - * {@link #isQuoted()} is true, then this value will be the same as {@link #toCql()} and - * {@link #toString()}. If {@link #isQuoted()} is false, it will be different. + * Returns the identifier without encasing quotes, regardless of the value of {@link #isQuoted()}. For + * example, if {@link #isQuoted()} is true, then this value will be the same as {@link #toCql()} and + * {@link #toString()}. *

* This is needed, for example, to get the correct {@link TableMetadata} from * {@link KeyspaceMetadata#getTable(String)}: the given string must not be quoted. diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlTableSpecificationAssertions.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlTableSpecificationAssertions.java index f8a5e830f..3ec1583cd 100644 --- a/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlTableSpecificationAssertions.java +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/integration/core/cql/generator/CqlTableSpecificationAssertions.java @@ -42,9 +42,9 @@ public class CqlTableSpecificationAssertions { public static void assertTable(TableDescriptor expected, String keyspace, Session session) { TableMetadata tmd = session.getCluster().getMetadata().getKeyspace(keyspace.toLowerCase()) - .getTable(CqlStringUtils.unquote(expected.getName().toCql())); // TODO: talk to Datastax about unquoting + .getTable(expected.getName().getUnquoted()); // TODO: talk to Datastax about unquoting - assertEquals(CqlStringUtils.unquote(expected.getName().toCql()), tmd.getName()); // TODO: talk to Datastax + assertEquals(expected.getName().getUnquoted(), tmd.getName()); // TODO: talk to Datastax assertPartitionKeyColumns(expected, tmd); assertPrimaryKeyColumns(expected, tmd); assertColumns(expected.getColumns(), tmd.getColumns()); @@ -171,7 +171,7 @@ public class CqlTableSpecificationAssertions { } public static void assertColumn(ColumnSpecification expected, ColumnMetadata actual) { - assertEquals(expected.getName().toCql(), actual.getName()); + assertEquals(expected.getName().toCql(), actual.getName()); // TODO: expected.getName().getUnquoted()? assertEquals(expected.getType(), actual.getType()); } } \ No newline at end of file diff --git a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/CreateTableCqlGeneratorTests.java b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/CreateTableCqlGeneratorTests.java index a85344baa..f77fa30c1 100644 --- a/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/CreateTableCqlGeneratorTests.java +++ b/spring-cassandra/src/test/java/org/springframework/cassandra/test/unit/core/cql/generator/CreateTableCqlGeneratorTests.java @@ -257,7 +257,7 @@ public class CreateTableCqlGeneratorTests { public static final List FUNKY_LEGAL_NAMES; static { - List funkies = new ArrayList(Arrays.asList(new String[] {})); + List funkies = new ArrayList(Arrays.asList(new String[] { /* TODO */})); // TODO: should these work? "a \"\" x", "a\"\"\"\"x", "a b" for (ReservedKeyword funky : ReservedKeyword.values()) { funkies.add(funky.name());