From d8092650e55d421dce932fbdb4ba40a7be4b527a Mon Sep 17 00:00:00 2001 From: Flavian Alexandru Date: Sun, 7 Aug 2016 13:54:42 +0100 Subject: [PATCH] Feature/select json (#532) * Adding JSON selection. * Spacing tokens and adding support for JSON selection. * Adding the ability to extract JSON columns. * Adding tests for selecting JSON. * Adding the right versions. * Removing Cassandra lift parsers. * Adding more fine-grained version checking. * Oops * Fixing test errors. * Fixing post fix syntax. * Fixing final match. --- README.md | 2 +- build.sbt | 2 +- .../com/websudos/phantom/SelectTable.scala | 5 +- .../phantom/builder/ops/Operators.scala | 22 +++-- .../phantom/builder/query/SelectQuery.scala | 30 +++++- .../query/prepared/PreparedBuilder.scala | 1 + .../builder/serializers/IndexModifiers.scala | 4 - .../serializers/SelectQueryBuilder.scala | 48 +++++++++- .../phantom/builder/syntax/CQLSyntax.scala | 2 + .../com/websudos/phantom/dsl/package.scala | 33 +++---- .../com/websudos/phantom/PhantomSuite.scala | 4 +- .../query/db/crud/SelectJsonTest.scala | 91 +++++++++++++++++++ .../builder/query/db/crud/SelectTest.scala | 50 ++++------ .../specialized/IndexedCollectionsTest.scala | 27 +++--- .../SelectQuerySerialisationTest.scala | 43 +++++++-- ...hereClauseOperatorsSerializationTest.scala | 2 +- .../tables/IndexedCollectionsTable.scala | 49 +++++++++- .../phantom/tables/TestDatabase.scala | 1 + 18 files changed, 325 insertions(+), 91 deletions(-) create mode 100644 phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/crud/SelectJsonTest.scala diff --git a/README.md b/README.md index 06f5bbde4..73b6e259f 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ We publish phantom in 2 formats, stable releases and bleeding edge. The latest versions are available here. The badges automatically update when a new version is released. - Latest stable version: [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.websudos/phantom-dsl_2.11/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.websudos/phantom-dsl_2.11) (Maven Central) -- Bleeding edge: [![Bintray](https://api.bintray.com/packages/websudos/oss-releases/phantom/images/download.svg) ](https://bintray.com/websudos/oss-releases/phantom/_latestVersion) (Websudos OSS releases on Bintray) +- Bleeding edge: [![Bintray](https://api.bintray.com/packages/websudos/oss-releases/phantom-dsl/images/download.svg)](https://bintray.com/websudos/oss-releases/phantom-dsl/_latestVersion) (OSS releases on Bintray) Tutorials on phantom and Cassandra ====================================================================== diff --git a/build.sbt b/build.sbt index 5d0d8d85e..448c43cb6 100644 --- a/build.sbt +++ b/build.sbt @@ -118,7 +118,7 @@ lazy val defaultCredentials: Seq[Credentials] = { val sharedSettings: Seq[Def.Setting[_]] = Defaults.coreDefaultSettings ++ Seq( organization := "com.websudos", - scalaVersion := "2.10.6", + scalaVersion := "2.11.8", credentials ++= defaultCredentials, crossScalaVersions := Seq("2.10.6", "2.11.8"), resolvers ++= Seq( diff --git a/phantom-dsl/src/main/scala/com/websudos/phantom/SelectTable.scala b/phantom-dsl/src/main/scala/com/websudos/phantom/SelectTable.scala index 5057b0418..f38c1467f 100644 --- a/phantom-dsl/src/main/scala/com/websudos/phantom/SelectTable.scala +++ b/phantom-dsl/src/main/scala/com/websudos/phantom/SelectTable.scala @@ -62,8 +62,7 @@ trait SelectTable[T <: CassandraTable[T, R], R] { RootSelectBlock[T, (A, B, C)](t, List(c1.col.name, c2.col.name, c3.col.name), r => (c1(r), c2(r), c3(r))) } - def select[A, B, C, D]( - f1: T =>SelectColumn[A], + def select[A, B, C, D](f1: T =>SelectColumn[A], f2: T => SelectColumn[B], f3: T => SelectColumn[C], f4: T => SelectColumn[D]): RootSelectBlock[T, (A, B, C, D)] = { @@ -89,7 +88,7 @@ trait SelectTable[T <: CassandraTable[T, R], R] { } def select[A, B, C, D, E, F]( - f1: T =>SelectColumn[A], + f1: T => SelectColumn[A], f2: T => SelectColumn[B], f3: T => SelectColumn[C], f4: T => SelectColumn[D], diff --git a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/ops/Operators.scala b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/ops/Operators.scala index 75759a058..4d26eb4c2 100644 --- a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/ops/Operators.scala +++ b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/ops/Operators.scala @@ -47,9 +47,12 @@ sealed class CqlFunction extends SessionAugmenterImplicits sealed class UnixTimestampOfCqlFunction extends CqlFunction { - def apply(pf: TimeUUIDColumn[_, _])(implicit ev: Primitive[Long], session: Session): TypedClause.Condition[Option[Long]] = { + def apply(pf: TimeUUIDColumn[_, _])( + implicit ev: Primitive[Long], + session: Session + ): TypedClause.Condition[Option[Long]] = { new TypedClause.Condition(QueryBuilder.Select.unixTimestampOf(pf.name), row => { - if (session.v3orNewer) { + if (row.getColumnDefinitions.contains(s"system.unixtimestampof(${pf.name})")) { ev.fromRow(s"system.unixtimestampof(${pf.name})", row).toOption } else { ev.fromRow(s"unixtimestampof(${pf.name})", row).toOption @@ -68,10 +71,12 @@ sealed class TTLOfFunction extends CqlFunction { sealed class DateOfCqlFunction extends CqlFunction { - def apply(pf: TimeUUIDColumn[_, _])(implicit ev: Primitive[DateTime], session: Session): TypedClause.Condition[Option[DateTime]] = { + def apply(pf: TimeUUIDColumn[_, _])( + implicit ev: Primitive[DateTime], + session: Session + ): TypedClause.Condition[Option[DateTime]] = { new TypedClause.Condition(QueryBuilder.Select.dateOf(pf.name), row => { - if (session.v3orNewer) { - + if (row.getColumnDefinitions.contains(s"system.dateof(${pf.name})")) { ev.fromRow(s"system.dateof(${pf.name})", row).toOption } else { ev.fromRow(s"dateof(${pf.name})", row).toOption @@ -79,11 +84,14 @@ sealed class DateOfCqlFunction extends CqlFunction { }) } - def apply(op: OperatorClause.Condition)(implicit ev: Primitive[DateTime], session: Session): TypedClause.Condition[Option[DateTime]] = { + def apply(op: OperatorClause.Condition)( + implicit ev: Primitive[DateTime], + session: Session + ): TypedClause.Condition[Option[DateTime]] = { val pf = op.qb.queryString new TypedClause.Condition(QueryBuilder.Select.dateOf(pf), row => { - if (session.v3orNewer) { + if (row.getColumnDefinitions.contains(s"system.dateof(${pf})")) { ev.fromRow(s"system.dateof($pf)", row).toOption } else { ev.fromRow(s"dateof($pf)", row).toOption diff --git a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/query/SelectQuery.scala b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/query/SelectQuery.scala index ac8df10c0..7a71bb774 100644 --- a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/query/SelectQuery.scala +++ b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/query/SelectQuery.scala @@ -31,9 +31,10 @@ package com.websudos.phantom.builder.query import com.datastax.driver.core.{ConsistencyLevel, Row, Session} import com.websudos.phantom.CassandraTable -import com.websudos.phantom.builder._ +import com.websudos.phantom.builder.{ConsistencyBound, LimitBound, OrderBound, WhereBound, _} import com.websudos.phantom.builder.clauses._ import com.websudos.phantom.builder.query.prepared.PreparedSelectBlock +import com.websudos.phantom.builder.syntax.CQLSyntax import com.websudos.phantom.connectors.KeySpace import shapeless.ops.hlist.Reverse import shapeless.{::, =:!=, HList, HNil} @@ -345,8 +346,7 @@ private[phantom] class RootSelectBlock[ ](table: T, val rowFunc: Row => R, columns: List[String], clause: Option[CQLQuery] = None) { @implicitNotFound("You haven't provided a KeySpace in scope. Use a Connector to automatically inject one.") - private[phantom] def all()(implicit keySpace: KeySpace): SelectQuery.Default[T, R] = { - + def all()(implicit keySpace: KeySpace): SelectQuery.Default[T, R] = { clause match { case Some(opt) => { new SelectQuery( @@ -379,6 +379,30 @@ private[phantom] class RootSelectBlock[ Try(r.getLong("writetime")).getOrElse(0L) } + def json()(implicit keySpace: KeySpace): SelectQuery.Default[T, String] = { + + val jsonParser: (Row) => String = row => { + row.getString(CQLSyntax.JSON_EXTRACTOR) + } + + clause match { + case Some(opt) => { + new SelectQuery( + table, + jsonParser, + QueryBuilder.Select.selectJson(table.tableName, keySpace.name) + ) + } + case None => { + new SelectQuery( + table, + jsonParser, + QueryBuilder.Select.selectJson(table.tableName, keySpace.name, columns: _*) + ) + } + } + } + def function[RR](f1: T => TypedClause.Condition[RR])(implicit keySpace: KeySpace): SelectQuery.Default[T, RR] = { new SelectQuery( table, diff --git a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/query/prepared/PreparedBuilder.scala b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/query/prepared/PreparedBuilder.scala index 5188667dd..93465f9bf 100644 --- a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/query/prepared/PreparedBuilder.scala +++ b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/query/prepared/PreparedBuilder.scala @@ -38,6 +38,7 @@ import org.joda.time.DateTime import shapeless.HList import shapeless.ops.hlist.Tupler +import scala.annotation.implicitNotFound import scala.collection.JavaConverters._ import scala.concurrent.{ExecutionContextExecutor, blocking, Future => ScalaFuture} diff --git a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/serializers/IndexModifiers.scala b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/serializers/IndexModifiers.scala index 28b1a6b43..18c516c7f 100644 --- a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/serializers/IndexModifiers.scala +++ b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/serializers/IndexModifiers.scala @@ -44,10 +44,6 @@ private[builder] class IndexModifiers extends BaseModifiers { modifier(column, CQLSyntax.Operators.notEqs, value) } - def ==(column: String, value: String): CQLQuery = { - modifier(column, CQLSyntax.Operators.eqs, value) - } - def lt(column: String, value: String): CQLQuery = { modifier(column, CQLSyntax.Operators.lt, value) } diff --git a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/serializers/SelectQueryBuilder.scala b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/serializers/SelectQueryBuilder.scala index 4cbedc5f8..b9155a640 100644 --- a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/serializers/SelectQueryBuilder.scala +++ b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/serializers/SelectQueryBuilder.scala @@ -187,6 +187,52 @@ private[builder] class SelectQueryBuilder { .pad.append(QueryBuilder.keyspace(keyspace, tableName)) } + /** + * Creates a select JSON query builder from a table name, a keyspace, and an arbitrary clause. + * This is used to serialise SELECT functions, such as WRITETIME or other valid expressions. + * Will return a query in the following format: + * + * {{{ + * SELECT JSON $clause FROM $keyspace.$tableName + * }}} + * @param tableName The name of the table. + * @param keyspace The name of the keyspace. + * @param clause The CQL clause to use as the select list value. + * @return + */ + def selectJson(tableName: String, keyspace: String, clause: CQLQuery): CQLQuery = { + CQLQuery(CQLSyntax.select) + .forcePad.append(CQLSyntax.json) + .pad.append(clause) + .pad.append(CQLSyntax.from) + .pad.append(QueryBuilder.keyspace(keyspace, tableName)) + } + + /** + * Selects an arbitrary number of columns given a table name and a keyspace. + * Return all the columns as JSON. + * Will return a query in the following format: + * + * {{{ + * SELECT JSON ($name1, $name2, ..) FROM $keyspace.$tableName + * }}} + * + * @param tableName The name of the table. + * @param keyspace The name of the keyspace. + * @param names The names of the columns to include in the select. + * @return A CQLQuery matching the described pattern. + */ + def selectJson(tableName: String, keyspace: String, names: String*): CQLQuery = { + val cols = if (names.nonEmpty) CQLQuery(names) else CQLQuery(CQLSyntax.Symbols.`*`) + + CQLQuery(CQLSyntax.select) + .forcePad.append(CQLSyntax.json) + .pad.append(cols) + .forcePad.append(CQLSyntax.from) + .forcePad.append(QueryBuilder.keyspace(keyspace, tableName)) + } + + def allowFiltering(): CQLQuery = { CQLQuery(CQLSyntax.allowFiltering) } @@ -242,6 +288,4 @@ private[builder] class SelectQueryBuilder { def blobAsText(column: String): CQLQuery = { CQLQuery(CQLSyntax.Selection.BlobAsText).wrapn(column) } - - } diff --git a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/syntax/CQLSyntax.scala b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/syntax/CQLSyntax.scala index 72614970b..fd3221f72 100644 --- a/phantom-dsl/src/main/scala/com/websudos/phantom/builder/syntax/CQLSyntax.scala +++ b/phantom-dsl/src/main/scala/com/websudos/phantom/builder/syntax/CQLSyntax.scala @@ -31,6 +31,8 @@ package com.websudos.phantom.builder.syntax object CQLSyntax { val Select = "SELECT" + val json = "JSON" + val JSON_EXTRACTOR = "[json]" val Where = "WHERE" val And = "AND" val Or = "OR" diff --git a/phantom-dsl/src/main/scala/com/websudos/phantom/dsl/package.scala b/phantom-dsl/src/main/scala/com/websudos/phantom/dsl/package.scala index cd3b3b83d..032b3507c 100644 --- a/phantom-dsl/src/main/scala/com/websudos/phantom/dsl/package.scala +++ b/phantom-dsl/src/main/scala/com/websudos/phantom/dsl/package.scala @@ -42,6 +42,7 @@ import com.websudos.phantom.builder.ops._ import com.websudos.phantom.builder.primitives.{DefaultPrimitives, Primitive} import com.websudos.phantom.builder.query.{CQLQuery, CreateImplicits, DeleteImplicits, SelectImplicits} import com.websudos.phantom.builder.syntax.CQLSyntax +import com.websudos.phantom.column.AbstractColumn import shapeless.{::, HNil} import scala.concurrent.ExecutionContextExecutor @@ -183,49 +184,49 @@ package object dsl extends ImplicitMechanism with CreateImplicits implicit lazy val context: ExecutionContextExecutor = Manager.scalaExecutor - implicit class PartitionTokenHelper[T](val p: Column[_, _, T] with PartitionKey[T]) extends AnyVal { + implicit class PartitionTokenHelper[T](val col: AbstractColumn[T] with PartitionKey[T]) extends AnyVal { - def ltToken (value: T): WhereClause.Condition = { + def ltToken(value: T): WhereClause.Condition = { new WhereClause.Condition( QueryBuilder.Where.lt( - QueryBuilder.Where.token(p.name).queryString, - QueryBuilder.Where.fcall(CQLSyntax.token, p.asCql(value)).queryString + QueryBuilder.Where.token(col.name).queryString, + QueryBuilder.Where.fcall(CQLSyntax.token, col.asCql(value)).queryString ) ) } - def lteToken (value: T): WhereClause.Condition = { + def lteToken(value: T): WhereClause.Condition = { new WhereClause.Condition( QueryBuilder.Where.lte( - QueryBuilder.Where.token(p.name).queryString, - QueryBuilder.Where.fcall(CQLSyntax.token, p.asCql(value)).queryString + QueryBuilder.Where.token(col.name).queryString, + QueryBuilder.Where.fcall(CQLSyntax.token, col.asCql(value)).queryString ) ) } - def gtToken (value: T): WhereClause.Condition = { + def gtToken(value: T): WhereClause.Condition = { new WhereClause.Condition( QueryBuilder.Where.gt( - QueryBuilder.Where.token(p.name).queryString, - QueryBuilder.Where.fcall(CQLSyntax.token, p.asCql(value)).queryString + QueryBuilder.Where.token(col.name).queryString, + QueryBuilder.Where.fcall(CQLSyntax.token, col.asCql(value)).queryString ) ) } - def gteToken (value: T): WhereClause.Condition = { + def gteToken(value: T): WhereClause.Condition = { new WhereClause.Condition( QueryBuilder.Where.gte( - QueryBuilder.Where.token(p.name).queryString, - QueryBuilder.Where.fcall(CQLSyntax.token, p.asCql(value)).queryString + QueryBuilder.Where.token(col.name).queryString, + QueryBuilder.Where.fcall(CQLSyntax.token, col.asCql(value)).queryString ) ) } - def eqsToken (value: T): WhereClause.Condition = { + def eqsToken(value: T): WhereClause.Condition = { new WhereClause.Condition( QueryBuilder.Where.eqs( - QueryBuilder.Where.token(p.name).queryString, - QueryBuilder.Where.fcall(CQLSyntax.token, p.asCql(value)).queryString + QueryBuilder.Where.token(col.name).queryString, + QueryBuilder.Where.fcall(CQLSyntax.token, col.asCql(value)).queryString ) ) } diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/PhantomSuite.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/PhantomSuite.scala index 546363331..cc350bf5c 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/PhantomSuite.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/PhantomSuite.scala @@ -31,7 +31,7 @@ package com.websudos.phantom import java.util.concurrent.TimeUnit -import com.websudos.phantom.connectors.RootConnector +import com.websudos.phantom.connectors.{RootConnector, VersionNumber} import com.websudos.phantom.tables.TestDatabase import com.outworkers.util.lift.{DateTimeSerializer, UUIDSerializer} import org.scalatest._ @@ -64,6 +64,8 @@ trait PhantomBaseSuite extends Suite with Matchers trait PhantomSuite extends FlatSpec with PhantomBaseSuite with TestDatabase.connector.Connector { val database = TestDatabase + + def requireVersion[T](v: VersionNumber)(fn: => T): Unit = if (cassandraVersion.value.compareTo(v) >= 0) fn else () } diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/crud/SelectJsonTest.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/crud/SelectJsonTest.scala new file mode 100644 index 000000000..96a201925 --- /dev/null +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/crud/SelectJsonTest.scala @@ -0,0 +1,91 @@ +/* + * Copyright 2013-2015 Websudos, Limited. + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * - Explicit consent must be obtained from the copyright owner, Outworkers Limited before any redistribution is made. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package com.websudos.phantom.builder.query.db.crud + +import com.datastax.driver.core.exceptions.SyntaxError +import com.websudos.phantom.PhantomSuite +import com.websudos.phantom.dsl._ +import com.websudos.phantom.tables._ +import com.outworkers.util.testing._ +import net.liftweb.json.JsonParser + +class SelectJsonTest extends PhantomSuite { + override def beforeAll(): Unit = { + super.beforeAll() + TestDatabase.primitives.insertSchema() + } + + "A JSON selection clause" should "select an entire row as JSON" in { + val row = gen[Primitive] + + val chain = for { + store <- TestDatabase.primitives.store(row).future() + b <- TestDatabase.primitives.select.json().where(_.pkey eqs row.pkey).one + } yield b + + + if (cassandraVersion.value >= Version.`2.2.0`) { + chain successful { + res => { + res shouldBe defined + val parsed = JsonParser.parse(res.value) + parsed.children.size shouldEqual row.productArity + } + } + } else { + chain.failing[SyntaxError] + } + + } + + "A JSON selection clause" should "8 columns as JSON" in { + val row = gen[Primitive] + val expected = (row.pkey, row.long, row.boolean, row.bDecimal, row.double, row.float, row.inet, row.int) + + val chain = for { + store <- TestDatabase.primitives.store(row).future() + get <- TestDatabase.primitives.select(_.pkey, _.long, _.boolean, _.bDecimal, _.double, _.float, _.inet, _.int) + .json() + .where(_.pkey eqs row.pkey).one() + } yield get + + if (cassandraVersion.value >= Version.`2.2.0`) { + chain successful { + res => { + res shouldBe defined + val parsed = JsonParser.parse(res.value) + parsed.children.size shouldEqual expected.productArity + } + } + } else { + chain.failing[SyntaxError] + } + } +} \ No newline at end of file diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/crud/SelectTest.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/crud/SelectTest.scala index 56a4ea1bc..e8acb36f0 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/crud/SelectTest.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/crud/SelectTest.scala @@ -33,6 +33,8 @@ import com.websudos.phantom.PhantomSuite import com.websudos.phantom.dsl._ import com.websudos.phantom.tables._ import com.outworkers.util.testing._ +import net.liftweb.http.js.JsObj +import net.liftweb.json.JsonParser class SelectTest extends PhantomSuite { @@ -44,21 +46,17 @@ class SelectTest extends PhantomSuite { "Selecting the whole row" should "work fine" in { val row = gen[Primitive] - TestDatabase.primitives.select.distinct() - val chain = for { store <- TestDatabase.primitives.store(row).future() b <- TestDatabase.primitives.select.where(_.pkey eqs row.pkey).one } yield b chain successful { - res => { - res.value shouldEqual row - } + res => res.value shouldEqual row } } - "Selecting 2 columns" should "work fine" in { + "Partial selects" should "select 2 columns" in { val row = gen[Primitive] val expected = (row.pkey, row.long) @@ -68,13 +66,11 @@ class SelectTest extends PhantomSuite { } yield get chain successful { - res => { - res.value shouldEqual expected - } + res => res.value shouldEqual expected } } - "Selecting 3 columns" should "work fine" in { + "Partial selects" should "select 3 columns" in { val row = gen[Primitive] val expected = (row.pkey, row.long, row.boolean) @@ -85,13 +81,11 @@ class SelectTest extends PhantomSuite { } yield get chain successful { - r => { - r.value shouldEqual expected - } + r => r.value shouldEqual expected } } - "Selecting 4 columns" should "work fine" in { + "Partial selects" should "select 4 columns" in { val row = gen[Primitive] val expected = (row.pkey, row.long, row.boolean, row.bDecimal) @@ -101,13 +95,11 @@ class SelectTest extends PhantomSuite { } yield get chain successful { - r => { - r.value shouldBe expected - } + r => r.value shouldBe expected } } - "Selecting 5 columns" should "work fine" in { + "Partial selects" should "select 5 columns" in { val row = gen[Primitive] val expected = (row.pkey, row.long, row.boolean, row.bDecimal, row.double) @@ -117,13 +109,11 @@ class SelectTest extends PhantomSuite { } yield get chain successful { - r => { - r.value shouldBe expected - } + r => r.value shouldBe expected } } - "Selecting 6 columns" should "work fine" in { + "Partial selects" should "select 6 columns" in { val row = gen[Primitive] val expected = (row.pkey, row.long, row.boolean, row.bDecimal, row.double, row.float) @@ -133,13 +123,11 @@ class SelectTest extends PhantomSuite { } yield get chain successful { - r => { - r.value shouldBe expected - } + r => r.value shouldBe expected } } - "Selecting 7 columns" should "work fine" in { + "Partial selects" should "select 7 columns" in { val row = gen[Primitive] val expected = (row.pkey, row.long, row.boolean, row.bDecimal, row.double, row.float, row.inet) @@ -149,13 +137,11 @@ class SelectTest extends PhantomSuite { } yield get chain successful { - r => { - r.value shouldBe expected - } + r => r.value shouldBe expected } } - "Selecting 8 columns" should "work fine" in { + "Partial selects" should "select 8 columns" in { val row = gen[Primitive] val expected = (row.pkey, row.long, row.boolean, row.bDecimal, row.double, row.float, row.inet, row.int) @@ -166,9 +152,7 @@ class SelectTest extends PhantomSuite { } yield get chain successful { - r => { - r.value shouldBe expected - } + r => r.value shouldBe expected } } } diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/IndexedCollectionsTest.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/IndexedCollectionsTest.scala index 3fa6376bd..cbe10b46b 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/IndexedCollectionsTest.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/query/db/specialized/IndexedCollectionsTest.scala @@ -29,20 +29,23 @@ */ package com.websudos.phantom.builder.query.db.specialized -import com.datastax.driver.core.exceptions.SyntaxError +import com.datastax.driver.core.exceptions.{InvalidQueryException, SyntaxError} import com.websudos.phantom.PhantomSuite import com.websudos.phantom.dsl._ import com.websudos.phantom.tables.{TestDatabase, TestRow} import com.outworkers.util.testing._ -import scala.concurrent.Await -import scala.concurrent.duration._ - class IndexedCollectionsTest extends PhantomSuite { override def beforeAll(): Unit = { super.beforeAll() - Await.ready(TestDatabase.indexedCollectionsTable.create.ifNotExists().future(), defaultScalaTimeout) + if (cassandraVersion.value >= Version.`2.1.0`) { + database.indexedCollectionsTable.insertSchema() + } + + if (cassandraVersion.value >= Version.`2.2.0`) { + database.indexedEntriesTable.insertSchema() + } } it should "store a record and retrieve it with a CONTAINS query on the SET" in { @@ -63,7 +66,7 @@ class IndexedCollectionsTest extends PhantomSuite { } } } else { - chain.failing[SyntaxError] + chain.failing[InvalidQueryException] } } @@ -86,7 +89,7 @@ class IndexedCollectionsTest extends PhantomSuite { } } } else { - chain.failing[SyntaxError] + chain.failing[InvalidQueryException] } } @@ -109,7 +112,7 @@ class IndexedCollectionsTest extends PhantomSuite { } } } else { - chain.failing[SyntaxError] + chain.failing[InvalidQueryException] } } @@ -117,11 +120,11 @@ class IndexedCollectionsTest extends PhantomSuite { val record = gen[TestRow].copy(mapIntToInt = Map(5 -> 10, 10 -> 15, 20 -> 25)) val chain = for { - store <- TestDatabase.indexedCollectionsTable.store(record).future() - result <- TestDatabase.indexedCollectionsTable.select.where(_.mapIntToInt(20) eqs 25).fetch() + store <- TestDatabase.indexedEntriesTable.store(record).future() + result <- TestDatabase.indexedEntriesTable.select.where(_.mapIntToInt(20) eqs 25).fetch() } yield result - if (cassandraVersion.value > Version.`2.1.0`) { + if (cassandraVersion.value > Version.`2.2.0`) { whenReady(chain) { res => { res.nonEmpty shouldEqual true @@ -129,7 +132,7 @@ class IndexedCollectionsTest extends PhantomSuite { } } } else { - chain.failing[SyntaxError] + chain.failing[InvalidQueryException] } } diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/serializers/SelectQuerySerialisationTest.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/serializers/SelectQuerySerialisationTest.scala index 0b3daa4bd..c9f66306c 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/serializers/SelectQuerySerialisationTest.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/serializers/SelectQuerySerialisationTest.scala @@ -37,6 +37,8 @@ import com.websudos.phantom.dsl._ import com.websudos.phantom.tables.TestDatabase import com.outworkers.util.testing._ +import scala.collection.SeqLike + class SelectQuerySerialisationTest extends QueryBuilderTest { val BasicTable = TestDatabase.basicTable @@ -68,6 +70,24 @@ class SelectQuerySerialisationTest extends QueryBuilderTest { } } + "should allow serialising JSON selection clauses" - { + "should allow a SELECT JSON * syntax" in { + val id = gen[UUID] + + val qb = BasicTable.select.json().where(_.id eqs id).queryString + + qb shouldEqual s"SELECT JSON * FROM phantom.basicTable WHERE id = ${id.toString};" + } + + "should allow a SELECT JSON col1, col2, .. syntax" in { + val id = gen[UUID] + + val qb = BasicTable.select(_.id, _.id2).json().where(_.id eqs id).queryString + + qb shouldEqual s"SELECT JSON id, id2 FROM phantom.basicTable WHERE id = ${id.toString};" + } + } + "should allow serialising USING clause syntax" - { "should allow specifying USING IGNORE NULLS" in { @@ -78,7 +98,7 @@ class SelectQuerySerialisationTest extends QueryBuilderTest { } } - "should serialize " - { + "should serialize combinations of limits and allow filtering clauses " - { "serialise an allow filtering clause in the init position" in { val id = gen[UUID] @@ -145,7 +165,10 @@ class SelectQuerySerialisationTest extends QueryBuilderTest { } "a multiple column token clause" in { - val qb = ArticlesByAuthor.select.where(t => { token(gen[UUID], gen[UUID]) > token(t.author_id, t.category) }).queryString + val qb = ArticlesByAuthor.select.where(t => { + token(gen[UUID], gen[UUID]) > token(t.author_id, t.category) + }).queryString + info(qb) } "a single column token clause" in { @@ -154,20 +177,28 @@ class SelectQuerySerialisationTest extends QueryBuilderTest { } "a consistency level setting" in { - val qb = ArticlesByAuthor.select.where(_.author_id eqs gen[UUID]) + val id = gen[UUID] + + val qb = ArticlesByAuthor.select.where(_.author_id gtToken id) .consistencyLevel_=(ConsistencyLevel.EACH_QUORUM) .queryString + + if (session.v3orNewer) { + qb shouldEqual s"SELECT * FROM phantom.articlesByAuthor WHERE TOKEN (author_id) > TOKEN($id);" + } else { + qb shouldEqual s"SELECT * FROM phantom.articlesByAuthor WHERE TOKEN (author_id) > TOKEN($id) USING CONSISTENCY EACH_QUORUM;" + } } "a single dateOf column apply" in { + val id = UUIDs.timeBased() val qb = TestDatabase.timeuuidTable.select .function(t => dateOf(t.id)) - .where(_.id eqs UUIDs.timeBased()) + .where(_.id eqs id) .qb.queryString - info(qb) + qb shouldEqual s"SELECT dateOf(id) FROM phantom.timeuuidTable WHERE id = $id" } } } - } diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/serializers/WhereClauseOperatorsSerializationTest.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/serializers/WhereClauseOperatorsSerializationTest.scala index 9c523a49a..22dbf9b1d 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/builder/serializers/WhereClauseOperatorsSerializationTest.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/builder/serializers/WhereClauseOperatorsSerializationTest.scala @@ -47,7 +47,7 @@ class WhereClauseOperatorsSerializationTest extends FlatSpec with Matchers { val column = gen[String] val value = gen[String] - QueryBuilder.Where.==(column, value).queryString shouldEqual s"$column = $value" + QueryBuilder.Where.eqs(column, value).queryString shouldEqual s"$column = $value" } "The Where.Builder" should "serialise a Where.lt clause" in { diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/tables/IndexedCollectionsTable.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/tables/IndexedCollectionsTable.scala index 3338dea16..9ab4ab85b 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/tables/IndexedCollectionsTable.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/tables/IndexedCollectionsTable.scala @@ -48,7 +48,7 @@ sealed class IndexedCollectionsTable extends CassandraTable[ConcreteIndexedColle object mapIntToText extends MapColumn[Int, String](this) with Index[Map[Int, String]] with Keys - object mapIntToInt extends MapColumn[Int, Int](this) with Index[Map[Int, Int]] with Entries + object mapIntToInt extends MapColumn[Int, Int](this) def fromRow(r: Row): TestRow = { TestRow( @@ -80,3 +80,50 @@ abstract class ConcreteIndexedCollectionsTable extends IndexedCollectionsTable w } +sealed class IndexedEntriesTable extends CassandraTable[ConcreteIndexedEntriesTable, TestRow] { + + object key extends StringColumn(this) with PartitionKey[String] + + object list extends ListColumn[String](this) + + object setText extends SetColumn[String](this) with Index[Set[String]] + + object mapTextToText extends MapColumn[String, String](this) with Index[Map[String, String]] + + object setInt extends SetColumn[Int](this) + + object mapIntToText extends MapColumn[Int, String](this) with Index[Map[Int, String]] with Keys + + object mapIntToInt extends MapColumn[Int, Int](this) with Index[Map[Int, Int]] with Entries + + def fromRow(r: Row): TestRow = { + TestRow( + key = key(r), + list = list(r), + setText = setText(r), + mapTextToText = mapTextToText(r), + setInt = setInt(r), + mapIntToText = mapIntToText(r), + mapIntToInt = mapIntToInt(r) + ) + } +} + +abstract class ConcreteIndexedEntriesTable extends IndexedEntriesTable with RootConnector { + override val tableName = "indexed_collections" + + def store(row: TestRow): InsertQuery.Default[ConcreteIndexedEntriesTable, TestRow] = { + insert + .value(_.key, row.key) + .value(_.list, row.list) + .value(_.setText, row.setText) + .value(_.mapTextToText, row.mapTextToText) + .value(_.setInt, row.setInt) + .value(_.mapIntToText, row.mapIntToText) + .value(_.mapIntToInt, row.mapIntToInt) + } + +} + + + diff --git a/phantom-dsl/src/test/scala/com/websudos/phantom/tables/TestDatabase.scala b/phantom-dsl/src/test/scala/com/websudos/phantom/tables/TestDatabase.scala index 46e0b378f..73fa28243 100644 --- a/phantom-dsl/src/test/scala/com/websudos/phantom/tables/TestDatabase.scala +++ b/phantom-dsl/src/test/scala/com/websudos/phantom/tables/TestDatabase.scala @@ -56,6 +56,7 @@ class TestDatabase(override val connector: KeySpaceDef) extends DatabaseImpl(con object brokenCounterCounterTable extends ConcreteBrokenCounterTableTest with connector.Connector object indexedCollectionsTable extends ConcreteIndexedCollectionsTable with connector.Connector + object indexedEntriesTable extends ConcreteIndexedEntriesTable with connector.Connector object jsonTable extends ConcreteJsonTable with connector.Connector object listCollectionTable extends ConcreteListCollectionTable with connector.Connector object optionalPrimitives extends ConcreteOptionalPrimitives with connector.Connector