Repository: cassandra Updated Branches: refs/heads/trunk b6d5f2cf3 -> ba56b8221
Allows single-column slice restrictions to be merged with multi-columns slice restrictions patch by Benjamin Lerer; reviewed by Sam Tunnicliffe for CASSANDRA-9606 Project: http://git-wip-us.apache.org/repos/asf/cassandra/repo Commit: http://git-wip-us.apache.org/repos/asf/cassandra/commit/de84a5c7 Tree: http://git-wip-us.apache.org/repos/asf/cassandra/tree/de84a5c7 Diff: http://git-wip-us.apache.org/repos/asf/cassandra/diff/de84a5c7 Branch: refs/heads/trunk Commit: de84a5c770ac1a429152dd79f0895b27aa544368 Parents: c9587cd Author: blerer <benjamin.le...@datastax.com> Authored: Sun Aug 9 21:48:04 2015 +0200 Committer: blerer <benjamin.le...@datastax.com> Committed: Sun Aug 9 21:48:04 2015 +0200 ---------------------------------------------------------------------- .../cql3/statements/MultiColumnRestriction.java | 10 +++- .../cassandra/cql3/statements/Restriction.java | 4 ++ .../cql3/statements/SelectStatement.java | 54 ++++++++++++++------ .../statements/SingleColumnRestriction.java | 25 +++++++-- .../cassandra/cql3/MultiColumnRelationTest.java | 49 +++++++++++++++++- .../cql3/SingleColumnRelationTest.java | 53 +++++++++++++++++-- 6 files changed, 165 insertions(+), 30 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/cassandra/blob/de84a5c7/src/java/org/apache/cassandra/cql3/statements/MultiColumnRestriction.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/MultiColumnRestriction.java b/src/java/org/apache/cassandra/cql3/statements/MultiColumnRestriction.java index f643684..ed1e7f1 100644 --- a/src/java/org/apache/cassandra/cql3/statements/MultiColumnRestriction.java +++ b/src/java/org/apache/cassandra/cql3/statements/MultiColumnRestriction.java @@ -24,8 +24,11 @@ import org.apache.cassandra.exceptions.InvalidRequestException; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import org.apache.cassandra.cql3.Term.Terminal; + public interface MultiColumnRestriction extends Restriction { public static class EQ extends SingleColumnRestriction.EQ implements MultiColumnRestriction @@ -128,8 +131,11 @@ public interface MultiColumnRestriction extends Restriction */ public List<ByteBuffer> componentBounds(Bound b, List<ByteBuffer> variables) throws InvalidRequestException { - Tuples.Value value = (Tuples.Value)bounds[b.idx].bind(variables); - return value.getElements(); + Terminal terminal = bounds[b.idx].bind(variables); + if (terminal instanceof Tuples.Value) + return ((Tuples.Value) terminal).getElements(); + + return Collections.singletonList(terminal.get()); } } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/de84a5c7/src/java/org/apache/cassandra/cql3/statements/Restriction.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/Restriction.java b/src/java/org/apache/cassandra/cql3/statements/Restriction.java index 3d33bde..f582c84 100644 --- a/src/java/org/apache/cassandra/cql3/statements/Restriction.java +++ b/src/java/org/apache/cassandra/cql3/statements/Restriction.java @@ -54,6 +54,8 @@ public interface Restriction /** Returns true if the start or end bound (depending on the argument) is set, false otherwise */ public boolean hasBound(Bound b); + public Term bound(Bound b); + public ByteBuffer bound(Bound b, List<ByteBuffer> variables) throws InvalidRequestException; /** Returns true if the start or end bound (depending on the argument) is inclusive, false otherwise */ @@ -64,5 +66,7 @@ public interface Restriction public IndexOperator getIndexOperator(Bound b); public void setBound(Relation.Type type, Term t) throws InvalidRequestException; + + public void setBound(Slice restriction) throws InvalidRequestException; } } http://git-wip-us.apache.org/repos/asf/cassandra/blob/de84a5c7/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java index aaf9579..a9eae7a 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/SelectStatement.java @@ -46,7 +46,6 @@ import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.service.pager.*; import org.apache.cassandra.db.ConsistencyLevel; -import org.apache.cassandra.thrift.ColumnDef; import org.apache.cassandra.thrift.IndexExpression; import org.apache.cassandra.thrift.IndexOperator; import org.apache.cassandra.thrift.ThriftValidation; @@ -1718,13 +1717,6 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache } else { - if (!existing.isMultiColumn()) - { - throw new InvalidRequestException(String.format( - "Column \"%s\" cannot have both tuple-notation inequalities and single-column inequalities: %s", - name, relation)); - } - boolean existingRestrictionStartBefore = (i == 0 && name.position != 0 && stmt.columnRestrictions[name.position - 1] == existing); @@ -1733,7 +1725,7 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache if (existingRestrictionStartBefore || existingRestrictionStartAfter) { throw new InvalidRequestException(String.format( - "Column \"%s\" cannot be restricted by two tuple-notation inequalities not starting with the same column: %s", + "Column \"%s\" cannot be restricted by inequalities not starting with the same column: %s", name, relation)); } @@ -1793,9 +1785,21 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache Term t = relation.getValue().prepare(names); t.collectMarkerSpecification(boundNames); - Restriction.Slice restriction = (Restriction.Slice) getExistingRestriction(stmt, names.get(0)); - if (restriction == null) + Restriction.Slice existingRestriction = (Restriction.Slice) getExistingRestriction(stmt, names.get(0)); + Restriction.Slice restriction; + if (existingRestriction == null) + { restriction = new MultiColumnRestriction.Slice(false); + } + else if (!existingRestriction.isMultiColumn()) + { + restriction = new MultiColumnRestriction.Slice(false); + restriction.setBound(existingRestriction); + } + else + { + restriction = existingRestriction; + } restriction.setBound(relation.operator(), t); for (CFDefinition.Name name : names) @@ -1847,17 +1851,25 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache switch (name.kind) { case KEY_ALIAS: - stmt.keyRestrictions[name.position] = updateSingleColumnRestriction(name, stmt.keyRestrictions[name.position], relation, names); + { + Restriction existingRestriction = stmt.keyRestrictions[name.position]; + Restriction previousRestriction = name.position == 0 ? null : stmt.keyRestrictions[name.position - 1]; + stmt.keyRestrictions[name.position] = updateSingleColumnRestriction(name, existingRestriction, previousRestriction, relation, names); break; + } case COLUMN_ALIAS: - stmt.columnRestrictions[name.position] = updateSingleColumnRestriction(name, stmt.columnRestrictions[name.position], relation, names); + { + Restriction existingRestriction = stmt.columnRestrictions[name.position]; + Restriction previousRestriction = name.position == 0 ? null : stmt.columnRestrictions[name.position - 1]; + stmt.columnRestrictions[name.position] = updateSingleColumnRestriction(name, existingRestriction, previousRestriction, relation, names); break; + } case VALUE_ALIAS: throw new InvalidRequestException(String.format("Predicates on the non-primary-key column (%s) of a COMPACT table are not yet supported", name.name)); case COLUMN_METADATA: case STATIC: // We only all IN on the row key and last clustering key so far, never on non-PK columns, and this even if there's an index - Restriction r = updateSingleColumnRestriction(name, stmt.metadataRestrictions.get(name), relation, names); + Restriction r = updateSingleColumnRestriction(name, stmt.metadataRestrictions.get(name), null, relation, names); if (r.isIN() && !((Restriction.IN)r).canHaveOnlyOneValue()) // Note: for backward compatibility reason, we conside a IN of 1 value the same as a EQ, so we let that slide. throw new InvalidRequestException(String.format("IN predicates on non-primary-key columns (%s) is not yet supported", name)); @@ -1866,7 +1878,7 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache } } - Restriction updateSingleColumnRestriction(CFDefinition.Name name, Restriction existingRestriction, SingleColumnRelation newRel, VariableSpecifications boundNames) throws InvalidRequestException + Restriction updateSingleColumnRestriction(CFDefinition.Name name, Restriction existingRestriction, Restriction previousRestriction, SingleColumnRelation newRel, VariableSpecifications boundNames) throws InvalidRequestException { ColumnSpecification receiver = name; if (newRel.onToken) @@ -1927,6 +1939,10 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache case LT: case LTE: { + // A slice restriction can be merged with another one under some conditions: + // 1) both restrictions are on a token function or non of them are + // (e.g. token(partitionKey) > token(?) AND token(partitionKey) <= token(?) or clustering1 > 1 AND clustering1 <= 2). + // 2) both restrictions needs to start with the same column (e.g clustering1 > 0 AND (clustering1, clustering2) <= (2, 1)). if (existingRestriction == null) existingRestriction = new SingleColumnRestriction.Slice(newRel.onToken); else if (!existingRestriction.isSlice()) @@ -1936,8 +1952,12 @@ public class SelectStatement implements CQLStatement, MeasurableForPreparedCache // processPartitionKeysRestrictions, we shouldn't update the existing restriction by the new one if the old one was using token() // and the new one isn't since that would bypass that later test. throw new InvalidRequestException("Only EQ and IN relation are supported on the partition key (unless you use the token() function)"); - else if (existingRestriction.isMultiColumn()) - throw new InvalidRequestException(String.format("Column \"%s\" cannot be restricted by both a tuple notation inequality and a single column inequality (%s)", name, newRel)); + + if (name.position != 0 && previousRestriction == existingRestriction) + throw new InvalidRequestException(String.format( + "Column \"%s\" cannot be restricted by two inequalities not starting with the same column: %s", + name, newRel)); + Term t = newRel.getValue().prepare(receiver); t.collectMarkerSpecification(boundNames); ((SingleColumnRestriction.Slice)existingRestriction).setBound(newRel.operator(), t); http://git-wip-us.apache.org/repos/asf/cassandra/blob/de84a5c7/src/java/org/apache/cassandra/cql3/statements/SingleColumnRestriction.java ---------------------------------------------------------------------- diff --git a/src/java/org/apache/cassandra/cql3/statements/SingleColumnRestriction.java b/src/java/org/apache/cassandra/cql3/statements/SingleColumnRestriction.java index 2e63272..e326597 100644 --- a/src/java/org/apache/cassandra/cql3/statements/SingleColumnRestriction.java +++ b/src/java/org/apache/cassandra/cql3/statements/SingleColumnRestriction.java @@ -218,6 +218,11 @@ public abstract class SingleColumnRestriction implements Restriction return bounds[b.idx] != null; } + public Term bound(Bound b) + { + return bounds[b.idx]; + } + public ByteBuffer bound(Bound b, List<ByteBuffer> variables) throws InvalidRequestException { return bounds[b.idx].bindAndGet(variables); @@ -279,12 +284,24 @@ public abstract class SingleColumnRestriction implements Restriction throw new AssertionError(); } - if (bounds[b.idx] != null) + setBound(b, inclusive, t); + } + + public void setBound(Restriction.Slice slice) throws InvalidRequestException + { + for (Bound bound : Bound.values()) + if (slice.hasBound(bound)) + setBound(bound, slice.isInclusive(bound), slice.bound(bound)); + } + + private void setBound(Bound bound, boolean inclusive, Term term) throws InvalidRequestException { + + if (bounds[bound.idx] != null) throw new InvalidRequestException(String.format( - "More than one restriction was found for the %s bound", b.name().toLowerCase())); + "More than one restriction was found for the %s bound", bound.name().toLowerCase())); - bounds[b.idx] = t; - boundInclusive[b.idx] = inclusive; + bounds[bound.idx] = term; + boundInclusive[bound.idx] = inclusive; } @Override http://git-wip-us.apache.org/repos/asf/cassandra/blob/de84a5c7/test/unit/org/apache/cassandra/cql3/MultiColumnRelationTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/MultiColumnRelationTest.java b/test/unit/org/apache/cassandra/cql3/MultiColumnRelationTest.java index 65ff3e7..ac3d882 100644 --- a/test/unit/org/apache/cassandra/cql3/MultiColumnRelationTest.java +++ b/test/unit/org/apache/cassandra/cql3/MultiColumnRelationTest.java @@ -296,9 +296,7 @@ public class MultiColumnRelationTest for (String tableSuffix : new String[]{"", "_compact"}) { String[] queries = new String[]{ - "SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a = 0 AND (b, c, d) > (0, 1, 0) AND b < 1", "SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a = 0 AND (b, c, d) > (0, 1, 0) AND c < 1", - "SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a = 0 AND b > 1 AND (b, c, d) < (1, 1, 0)", "SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a = 0 AND c > 1 AND (b, c, d) < (1, 1, 0)", "SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE (a, b, c, d) IN ((0, 1, 2, 3))", "SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE (c, d) IN ((0, 1))", @@ -380,6 +378,12 @@ public class MultiColumnRelationTest checkRow(1, results, 0, 1, 1, 1); results = execute("SELECT * FROM %s.multiple_clustering" + tableSuffix + + " WHERE a = 0 and b = 1 and (c, d) > (0, 0) and c <= 1"); + assertEquals(2, results.size()); + checkRow(0, results, 0, 1, 1, 0); + checkRow(1, results, 0, 1, 1, 1); + + results = execute("SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a = 0 and b = 1 and (c, d) >= (0, 0) and (c, d) < (1, 1)"); assertEquals(2, results.size()); checkRow(0, results, 0, 1, 0, 0); @@ -477,6 +481,12 @@ public class MultiColumnRelationTest checkRow(1, results, 0, 1, 1, 1); results = execute("SELECT * FROM %s.multiple_clustering" + tableSuffix + + " WHERE a = 0 and (b) = (1) and (c, d) > (0, 0) and c <= 1"); + assertEquals(2, results.size()); + checkRow(0, results, 0, 1, 1, 0); + checkRow(1, results, 0, 1, 1, 1); + + results = execute("SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a = 0 and (b) = (1) and (c, d) >= (0, 0) and (c, d) < (1, 1)"); assertEquals(2, results.size()); checkRow(0, results, 0, 1, 0, 0); @@ -516,6 +526,14 @@ public class MultiColumnRelationTest results = execute("SELECT * FROM %s.single_clustering" + tableSuffix + " WHERE a=0 AND (b) > (0) AND (b) < (2)"); assertEquals(1, results.size()); checkRow(0, results, 0, 1, 0); + + results = execute("SELECT * FROM %s.single_clustering" + tableSuffix + " WHERE a=0 AND b > 0 AND (b) < (2)"); + assertEquals(1, results.size()); + checkRow(0, results, 0, 1, 0); + + results = execute("SELECT * FROM %s.single_clustering" + tableSuffix + " WHERE a=0 AND (b) > (0) AND b < 2"); + assertEquals(1, results.size()); + checkRow(0, results, 0, 1, 0); } } @@ -606,6 +624,10 @@ public class MultiColumnRelationTest assertEquals(1, results.size()); checkRow(0, results, 0, 0, 1, 1); + results = execute("SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a=0 AND (b, c, d) > (0, 1, 0) AND b < 1"); + assertEquals(1, results.size()); + checkRow(0, results, 0, 0, 1, 1); + results = execute("SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a=0 AND (b, c, d) > (0, 1, 1) AND (b, c) < (1, 1)"); assertEquals(1, results.size()); checkRow(0, results, 0, 1, 0, 0); @@ -973,6 +995,12 @@ public class MultiColumnRelationTest } @Test(expected=InvalidRequestException.class) + public void testPrepareMixMultipleInequalitiesOnSameBoundWithSingleColumnRestriction() throws Throwable + { + prepare("SELECT * FROM %s.single_clustering WHERE a=0 AND (b) > (?) AND b > ?"); + } + + @Test(expected=InvalidRequestException.class) public void testPrepareClusteringColumnsOutOfOrderInInequality() throws Throwable { prepare("SELECT * FROM %s.multiple_clustering WHERE a=0 AND (d, c, b) > (?, ?, ?)"); @@ -1051,6 +1079,14 @@ public class MultiColumnRelationTest results = executePrepared(prepare("SELECT * FROM %s.single_clustering" + tableSuffix + " WHERE a=0 AND (b) > (?) AND (b) < (?)"), makeIntOptions(0, 2)); assertEquals(1, results.size()); checkRow(0, results, 0, 1, 0); + + results = executePrepared(prepare("SELECT * FROM %s.single_clustering" + tableSuffix + " WHERE a=0 AND (b) > (?) AND b < ?"), makeIntOptions(0, 2)); + assertEquals(1, results.size()); + checkRow(0, results, 0, 1, 0); + + results = executePrepared(prepare("SELECT * FROM %s.single_clustering" + tableSuffix + " WHERE a=0 AND b > ? AND (b) < (?)"), makeIntOptions(0, 2)); + assertEquals(1, results.size()); + checkRow(0, results, 0, 1, 0); } } @@ -1129,6 +1165,10 @@ public class MultiColumnRelationTest assertEquals(1, results.size()); checkRow(0, results, 0, 0, 1, 1); + results = executePrepared(prepare("SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a=0 AND (b, c, d) > (?, ?, ?) AND b < ?"), makeIntOptions(0, 1, 0, 1)); + assertEquals(1, results.size()); + checkRow(0, results, 0, 0, 1, 1); + results = executePrepared(prepare ("SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a=0 AND (b, c, d) > (?, ?, ?) AND (b, c) < (?, ?)"), makeIntOptions(0, 1, 1, 1, 1)); @@ -1195,6 +1235,11 @@ public class MultiColumnRelationTest assertEquals(1, results.size()); checkRow(0, results, 0, 0, 1, 1); + results = executePrepared(prepare("SELECT * FROM %s.multiple_clustering" + tableSuffix + + " WHERE a=0 AND (b, c, d) > ? AND b < ?"), options(tuple(0, 1, 0), ByteBufferUtil.bytes(1))); + assertEquals(1, results.size()); + checkRow(0, results, 0, 0, 1, 1); + results = executePrepared(prepare ("SELECT * FROM %s.multiple_clustering" + tableSuffix + " WHERE a=0 AND (b, c, d) > ? AND (b, c) < ?"), options(tuple(0, 1, 1), tuple(1, 1))); http://git-wip-us.apache.org/repos/asf/cassandra/blob/de84a5c7/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java ---------------------------------------------------------------------- diff --git a/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java b/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java index c8c67aa..8ce4a36 100644 --- a/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java +++ b/test/unit/org/apache/cassandra/cql3/SingleColumnRelationTest.java @@ -21,6 +21,7 @@ import java.util.Iterator; import java.util.List; import org.junit.AfterClass; +import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -45,7 +46,8 @@ public class SingleColumnRelationTest { SchemaLoader.loadSchema(); executeSchemaChange("CREATE KEYSPACE IF NOT EXISTS %s WITH replication = {'class': 'SimpleStrategy', 'replication_factor': '1'}"); - + executeSchemaChange("CREATE TABLE IF NOT EXISTS %s.single_partition (a int PRIMARY KEY, b int, c text)"); + executeSchemaChange("CREATE TABLE IF NOT EXISTS %s.compound_partition (a int, b int, c text, PRIMARY KEY ((a, b)))"); executeSchemaChange("CREATE TABLE IF NOT EXISTS %s.partition_with_indices (a int, b int, c int, d int, e int, f int, PRIMARY KEY ((a, b), c, d, e))"); executeSchemaChange("CREATE INDEX ON %s.partition_with_indices (c)"); executeSchemaChange("CREATE INDEX ON %s.partition_with_indices (f)"); @@ -133,16 +135,44 @@ public class SingleColumnRelationTest checkRow(0, results, 0, 0, 1, 1, 1, 5); } - @Test(expected=InvalidRequestException.class) + @Test + public void testSliceRestrictionOnPartitionKey() throws Throwable + { + assertInvalidMessage("Only EQ and IN relation are supported on the partition key (unless you use the token() function)", + "SELECT * FROM %s.single_partition WHERE a >= 1 and a < 4"); + } + + @Test + public void testMulticolumnSliceRestrictionOnPartitionKey() throws Throwable + { + assertInvalidMessage("Multi-column relations can only be applied to clustering columns: a", + "SELECT * FROM %s.single_partition WHERE (a) >= (1) and (a) < (4)"); + assertInvalidMessage("Multi-column relations can only be applied to clustering columns: a", + "SELECT * FROM %s.compound_partition WHERE (a, b) >= (1, 1) and (a, b) < (4, 1)"); + assertInvalidMessage("Multi-column relations can only be applied to clustering columns: a", + "SELECT * FROM %s.compound_partition WHERE a >= 1 and (a, b) < (4, 1)"); + assertInvalidMessage("Multi-column relations can only be applied to clustering columns: a", + "SELECT * FROM %s.compound_partition WHERE b >= 1 and (a, b) < (4, 1)"); + assertInvalidMessage("Multi-column relations can only be applied to clustering columns: a", + "SELECT * FROM %s.compound_partition WHERE (a, b) >= (1, 1) and (b) < (4)"); + assertInvalidMessage("Multi-column relations can only be applied to clustering columns: b", + "SELECT * FROM %s.compound_partition WHERE (b) < (4) and (a, b) >= (1, 1)"); + assertInvalidMessage("Multi-column relations can only be applied to clustering columns: a", + "SELECT * FROM %s.compound_partition WHERE (a, b) >= (1, 1) and a = 1"); + } + + @Test public void testMissingPartitionComponentAndFileringOnTheSecondClusteringColumnWithoutAllowFiltering() throws Throwable { - execute("SELECT * FROM %s.partition_with_indices WHERE d >= 1 AND f = 5"); + assertInvalidMessage("Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING", + "SELECT * FROM %s.partition_with_indices WHERE d >= 1 AND f = 5"); } - @Test(expected=InvalidRequestException.class) + @Test public void testMissingPartitionComponentWithSliceRestrictionOnIndexedColumn() throws Throwable { - execute("SELECT * FROM %s.partition_with_indices WHERE a = 0 AND c >= 1 ALLOW FILTERING"); + assertInvalidMessage("Partition key part b must be restricted since preceding part is", + "SELECT * FROM %s.partition_with_indices WHERE a = 0 AND c >= 1 ALLOW FILTERING"); } private static void checkRow(int rowIndex, UntypedResultSet results, Integer... expectedValues) @@ -158,4 +188,17 @@ public class SingleColumnRelationTest (long) expected, actual); } } + + private static void assertInvalidMessage(String expectedMsg, String query) throws Throwable + { + try + { + execute(query); + Assert.fail("The statement should trigger an InvalidRequestException but did not"); + } + catch (InvalidRequestException e) + { + assertEquals("The error message is not the expected one.",expectedMsg, e.getMessage()); + } + } }