Skip to content

Commit 91c0598

Browse files
committed
JAVA-1086: Support Cassandra 3.2 CAST function in QueryBuilder.
Also expose select().raw(...) in the top-level API. With contributions by @tolbertam.
1 parent 61045f9 commit 91c0598

7 files changed

Lines changed: 167 additions & 79 deletions

File tree

changelog/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [improvement] JAVA-866: Support tuple notation in QueryBuilder.eq/in.
1414
- [bug] JAVA-1140: Use same connection to check for schema agreement after a DDL query.
1515
- [improvement] JAVA-1113: Support Cassandra 3.4 LIKE operator in QueryBuilder.
16+
- [improvement] JAVA-1086: Support Cassandra 3.2 CAST function in QueryBuilder.
1617

1718
Merged from 2.1 branch:
1819

driver-core/src/main/java/com/datastax/driver/core/querybuilder/QueryBuilder.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.datastax.driver.core.querybuilder;
1717

18+
import com.datastax.driver.core.DataType;
1819
import com.datastax.driver.core.Metadata;
1920
import com.datastax.driver.core.RegularStatement;
2021
import com.datastax.driver.core.TableMetadata;
@@ -1008,6 +1009,17 @@ public static Object fcall(String name, Object... parameters) {
10081009
return new Utils.FCall(name, parameters);
10091010
}
10101011

1012+
/**
1013+
* Creates a Cast of a column using the given dataType.
1014+
*
1015+
* @param column the column to cast.
1016+
* @param dataType the data type to cast to.
1017+
* @return the casted column.
1018+
*/
1019+
public static Object cast(Object column, DataType dataType) {
1020+
return new Utils.Cast(column, dataType);
1021+
}
1022+
10111023
/**
10121024
* Creates a {@code now()} function call.
10131025
*

driver-core/src/main/java/com/datastax/driver/core/querybuilder/Select.java

Lines changed: 38 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import com.datastax.driver.core.CodecRegistry;
1919
import com.datastax.driver.core.ColumnMetadata;
20+
import com.datastax.driver.core.DataType;
2021
import com.datastax.driver.core.TableMetadata;
2122

2223
import java.util.ArrayList;
@@ -388,6 +389,34 @@ public Selection distinct() {
388389
* @return this in-build SELECT statement
389390
*/
390391
public abstract SelectionOrAlias fcall(String name, Object... parameters);
392+
393+
/**
394+
* Creates a cast of an expression to a given CQL type.
395+
*
396+
* @param column the expression to cast. It can be a complex expression like a
397+
* {@link QueryBuilder#fcall(String, Object...) function call}.
398+
* @param targetType the target CQL type to cast to. Use static methods such as {@link DataType#text()}.
399+
* @return this in-build SELECT statement.
400+
*/
401+
public SelectionOrAlias cast(Object column, DataType targetType) {
402+
// This method should be abstract like others here. But adding an abstract method is not binary-compatible,
403+
// so we add this dummy implementation to make Clirr happy.
404+
throw new UnsupportedOperationException("Not implemented. This should only happen if you've written your own implementation of Selection");
405+
}
406+
407+
/**
408+
* Selects the provided raw expression.
409+
* <p/>
410+
* The provided string will be appended to the query as-is, without any form of escaping or quoting.
411+
*
412+
* @param rawString the raw expression to add.
413+
* @return this in-build SELECT statement
414+
*/
415+
public SelectionOrAlias raw(String rawString) {
416+
// This method should be abstract like others here. But adding an abstract method is not binary-compatible,
417+
// so we add this dummy implementation to make Clirr happy.
418+
throw new UnsupportedOperationException("Not implemented. This should only happen if you've written your own implementation of Selection");
419+
}
391420
}
392421

393422
/**
@@ -430,12 +459,6 @@ private SelectionOrAlias queueName(Object name) {
430459
return this;
431460
}
432461

433-
/**
434-
* Selects all columns (i.e. "SELECT * ...")
435-
*
436-
* @return an in-build SELECT statement.
437-
* @throws IllegalStateException if some columns had already been selected for this builder.
438-
*/
439462
@Override
440463
public Builder all() {
441464
if (columnNames != null)
@@ -446,12 +469,6 @@ public Builder all() {
446469
return (Builder) this;
447470
}
448471

449-
/**
450-
* Selects the count of all returned rows (i.e. "SELECT count(*) ...").
451-
*
452-
* @return an in-build SELECT statement.
453-
* @throws IllegalStateException if some columns had already been selected for this builder.
454-
*/
455472
@Override
456473
public Builder countAll() {
457474
if (columnNames != null)
@@ -463,94 +480,42 @@ public Builder countAll() {
463480
return (Builder) this;
464481
}
465482

466-
/**
467-
* Selects the provided column.
468-
*
469-
* @param name the new column name to add.
470-
* @return this in-build SELECT statement
471-
*/
472483
@Override
473484
public SelectionOrAlias column(String name) {
474485
return queueName(name);
475486
}
476487

477-
/**
478-
* Selects the provided raw expression.
479-
* <p/>
480-
* This method is used internally by the mapper module. It is not exposed on the parent class {@link Selection} to
481-
* avoid breaking binary compatibility. This means that it is not accessible directly via the fluent API (you need
482-
* a cast). This shouldn't be a problem for regular clients because they will use other methods of the DSL
483-
* ({@code fcall}, etc.) rather than provide a raw string.
484-
*
485-
* @param rawString the raw expression to add.
486-
* @return this in-build SELECT statement
487-
*/
488-
public SelectionOrAlias raw(String rawString) {
489-
return queueName(QueryBuilder.raw(rawString));
490-
}
491-
492-
/**
493-
* Selects the write time of provided column.
494-
* <p/>
495-
* This is a shortcut for {@code fcall("writetime", QueryBuilder.column(name))}.
496-
*
497-
* @param name the name of the column to select the write time of.
498-
* @return this in-build SELECT statement
499-
*/
500488
@Override
501489
public SelectionOrAlias writeTime(String name) {
502490
return queueName(new Utils.FCall("writetime", new Utils.CName(name)));
503491
}
504492

505-
/**
506-
* Selects the ttl of provided column.
507-
* <p/>
508-
* This is a shortcut for {@code fcall("ttl", QueryBuilder.column(name))}.
509-
*
510-
* @param name the name of the column to select the ttl of.
511-
* @return this in-build SELECT statement
512-
*/
513493
@Override
514494
public SelectionOrAlias ttl(String name) {
515495
return queueName(new Utils.FCall("ttl", new Utils.CName(name)));
516496
}
517497

518-
/**
519-
* Creates a function call.
520-
* <p/>
521-
* Please note that the parameters are interpreted as values, and so
522-
* {@code fcall("textToBlob", "foo")} will generate the string
523-
* {@code "textToBlob('foo')"}. If you want to generate
524-
* {@code "textToBlob(foo)"}, i.e. if the argument must be interpreted
525-
* as a column name (in a select clause), you will need to use the
526-
* {@link QueryBuilder#column} method, and so
527-
* {@code fcall("textToBlob", QueryBuilder.column(foo)}.
528-
*/
529498
@Override
530499
public SelectionOrAlias fcall(String name, Object... parameters) {
531500
return queueName(new Utils.FCall(name, parameters));
532501
}
533502

534-
/**
535-
* Adds the table to select from.
536-
*
537-
* @param keyspace the name of the keyspace to select from.
538-
* @param table the name of the table to select from.
539-
* @return a newly built SELECT statement that selects from {@code keyspace.table}.
540-
*/
503+
public SelectionOrAlias cast(Object column, DataType targetType) {
504+
return queueName(new Utils.Cast(column, targetType));
505+
}
506+
507+
@Override
508+
public SelectionOrAlias raw(String rawString) {
509+
return queueName(QueryBuilder.raw(rawString));
510+
}
511+
541512
@Override
542513
public Select from(String keyspace, String table) {
543514
if (previousSelection != null)
544515
addName(previousSelection);
545516
return super.from(keyspace, table);
546517
}
547518

548-
/**
549-
* Adds the table to select from.
550-
*
551-
* @param table the table to select from.
552-
* @return a newly built SELECT statement that selects from {@code table}.
553-
*/
554519
@Override
555520
public Select from(TableMetadata table) {
556521
if (previousSelection != null)

driver-core/src/main/java/com/datastax/driver/core/querybuilder/Utils.java

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@
1515
*/
1616
package com.datastax.driver.core.querybuilder;
1717

18-
import com.datastax.driver.core.CodecRegistry;
19-
import com.datastax.driver.core.ProtocolVersion;
20-
import com.datastax.driver.core.Token;
21-
import com.datastax.driver.core.TypeCodec;
18+
import com.datastax.driver.core.*;
2219
import com.datastax.driver.core.exceptions.InvalidTypeException;
2320

2421
import java.math.BigDecimal;
@@ -78,6 +75,11 @@ static StringBuilder appendValue(Object value, CodecRegistry codecRegistry, Stri
7875
appendValue(fcall.parameters[i], codecRegistry, sb, variables);
7976
}
8077
sb.append(')');
78+
} else if (value instanceof Cast) {
79+
Cast cast = (Cast) value;
80+
sb.append("CAST(");
81+
appendName(cast.column, codecRegistry, sb);
82+
sb.append(" AS ").append(cast.targetType).append(")");
8183
} else if (value instanceof CName) {
8284
appendName(((CName) value).name, codecRegistry, sb);
8385
} else if (value instanceof RawString) {
@@ -264,6 +266,11 @@ static StringBuilder appendName(Object name, CodecRegistry codecRegistry, String
264266
Alias alias = (Alias) name;
265267
appendName(alias.column, codecRegistry, sb);
266268
sb.append(" AS ").append(alias.alias);
269+
} else if (name instanceof Cast) {
270+
Cast cast = (Cast) name;
271+
sb.append("CAST(");
272+
appendName(cast.column, codecRegistry, sb);
273+
sb.append(" AS ").append(cast.targetType).append(")");
267274
} else if (name instanceof RawString) {
268275
sb.append(((RawString) name).str);
269276
} else {
@@ -426,4 +433,19 @@ public String toString() {
426433
return String.format("%s AS %s", column, alias);
427434
}
428435
}
436+
437+
static class Cast {
438+
private final Object column;
439+
private final DataType targetType;
440+
441+
Cast(Object column, DataType targetType) {
442+
this.column = column;
443+
this.targetType = targetType;
444+
}
445+
446+
@Override
447+
public String toString() {
448+
return String.format("CAST(%s AS %s)", column, targetType);
449+
}
450+
}
429451
}

driver-core/src/test/java/com/datastax/driver/core/querybuilder/QueryBuilderExecutionTest.java

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,20 @@
3434
public class QueryBuilderExecutionTest extends CCMTestsSupport {
3535

3636
private static final String TABLE1 = "test1";
37+
private static final String TABLE2 = "test2";
3738

3839
@Override
3940
public void onTestContextInitialized() {
4041
execute(
4142
String.format("CREATE TABLE %s (k text PRIMARY KEY, t text, i int, f float)", TABLE1),
43+
String.format("CREATE TABLE %s (k text, t text, i int, f float, PRIMARY KEY (k, t))", TABLE2),
4244
"CREATE TABLE dateTest (t timestamp PRIMARY KEY)",
43-
"CREATE TABLE test_coll (k int PRIMARY KEY, a list<int>, b map<int,text>, c set<text>)");
45+
"CREATE TABLE test_coll (k int PRIMARY KEY, a list<int>, b map<int,text>, c set<text>)",
46+
insertInto(TABLE2).value("k", "cast_t").value("t", "a").value("i", 1).value("f", 1.1).toString(),
47+
insertInto(TABLE2).value("k", "cast_t").value("t", "b").value("i", 2).value("f", 2.5).toString(),
48+
insertInto(TABLE2).value("k", "cast_t").value("t", "c").value("i", 3).value("f", 3.7).toString(),
49+
insertInto(TABLE2).value("k", "cast_t").value("t", "d").value("i", 4).value("f", 5.0).toString()
50+
);
4451
}
4552

4653
@Test(groups = "short")
@@ -185,6 +192,75 @@ public void should_delete_map_entry_with_bind_marker() throws Exception {
185192
assertThat(actual).containsExactly(entry(2, "bar"));
186193
}
187194

195+
/**
196+
* Validates that {@link QueryBuilder} may be used to create a query that casts a column from one type to another,
197+
* i.e.:
198+
* <p/>
199+
* <code>select CAST(f as int) as fint, i from table2 where k='cast_t'</code>
200+
* <p/>
201+
* and validates that the query executes successfully with the anticipated results.
202+
*
203+
* @jira_ticket JAVA-1086
204+
* @test_category queries:builder
205+
* @since 3.0.1
206+
*/
207+
@Test(groups = "short")
208+
@CassandraVersion(major = 3.2)
209+
public void should_support_cast_function_on_column() {
210+
//when
211+
ResultSet r = session().execute(select().cast("f", DataType.cint()).as("fint").column("i").from(TABLE2).where(eq("k", "cast_t")));
212+
//then
213+
assertThat(r.getAvailableWithoutFetching()).isEqualTo(4);
214+
for (Row row : r) {
215+
Integer i = row.getInt("i");
216+
assertThat(row.getColumnDefinitions().getType("fint")).isEqualTo(DataType.cint());
217+
Integer f = row.getInt("fint");
218+
switch (i) {
219+
case 1:
220+
assertThat(f).isEqualTo(1);
221+
break;
222+
case 2:
223+
assertThat(f).isEqualTo(2);
224+
break;
225+
case 3:
226+
assertThat(f).isEqualTo(3);
227+
break;
228+
case 4:
229+
assertThat(f).isEqualTo(5);
230+
break;
231+
default:
232+
fail("Unexpected values: " + i + "," + f);
233+
}
234+
}
235+
}
236+
237+
/**
238+
* Validates that {@link QueryBuilder} may be used to create a query that makes an aggregate function call, casting
239+
* the column(s) that the function operates on from one type to another.
240+
* i.e.:
241+
* <p/>
242+
* <code>select avg(CAST(i as float)) as iavg from table2 where k='cast_t'</code>
243+
* <p/>
244+
* and validates that the query executes successfully with the anticipated results.
245+
*
246+
* @jira_ticket JAVA-1086
247+
* @test_category queries:builder
248+
* @since 3.0.1
249+
*/
250+
@Test(groups = "short")
251+
@CassandraVersion(major = 3.2)
252+
public void should_support_fcall_on_cast_column() {
253+
//when
254+
ResultSet ar = session().execute(select().fcall("avg", cast(column("i"), DataType.cfloat())).as("iavg").from(TABLE2).where(eq("k", "cast_t")));
255+
//then
256+
assertThat(ar.getAvailableWithoutFetching()).isEqualTo(1);
257+
Row row = ar.one();
258+
assertThat(row.getColumnDefinitions().getType("iavg")).isEqualTo(DataType.cfloat());
259+
Float f = row.getFloat("iavg");
260+
// (1.0+2.0+3.0+4.0) / 4 = 2.5
261+
assertThat(f).isEqualTo(2.5f);
262+
}
263+
188264
/**
189265
* Validates that {@link QueryBuilder} can construct a query using the 'LIKE' operator to retrieve data from a
190266
* table on a column that has a SASI index, i.e.:

driver-core/src/test/java/com/datastax/driver/core/querybuilder/QueryBuilderTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,18 @@ public void selectTest() throws Exception {
143143
select = select().from("foo").where(containsKey("e", "key1"));
144144
assertEquals(select.toString(), query);
145145

146+
query = "SELECT CAST(writetime(country) AS text) FROM artists LIMIT 2;";
147+
select = select().cast(fcall("writetime", column("country")), DataType.text()).from("artists").limit(2);
148+
assertEquals(select.toString(), query);
149+
150+
query = "SELECT avg(CAST(v AS float)) FROM e;";
151+
select = select().fcall("avg", cast(column("v"), DataType.cfloat())).from("e");
152+
assertEquals(select.toString(), query);
153+
154+
query = "SELECT CAST(writetime(country) AS text) FROM artists LIMIT 2;";
155+
select = select().raw("CAST(writetime(country) AS text)").from("artists").limit(2);
156+
assertEquals(select.toString(), query);
157+
146158
query = "SELECT * FROM foo WHERE e LIKE 'a%';";
147159
select = select().from("foo").where(like("e", "a%"));
148160
assertEquals(select.toString(), query);

driver-mapping/src/main/java/com/datastax/driver/mapping/QueryType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ String makePreparedQueryString(TableMetadata table, EntityMapper<?> mapper, Mapp
7878
Select.Selection selection = select();
7979
for (ColumnMapper cm : mapper.allColumns()) {
8080
Select.SelectionOrAlias column = (cm.kind == ColumnMapper.Kind.COMPUTED)
81-
? ((Select.SelectionOrAlias) selection).raw(cm.getColumnName())
81+
? selection.raw(cm.getColumnName())
8282
: selection.column(cm.getColumnName());
8383

8484
if (cm.getAlias() == null) {

0 commit comments

Comments
 (0)