Skip to content

Commit

Permalink
Feature/map remove key (#839)
Browse files Browse the repository at this point in the history
* Adding support for removing multiple map keys at the same time using new syntax.

* Adding tests for key removal

* Adding serialisation tests.

* Adding overload for single call

* Adding multi api for map key deletes
  • Loading branch information
alexflav23 authored Jun 25, 2018
1 parent c7a2996 commit ea3d858
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@
*/
package com.outworkers.phantom.builder.ops

import com.datastax.driver.core.Session
import com.outworkers.phantom.CassandraTable
import com.outworkers.phantom.builder.QueryBuilder
import com.outworkers.phantom.builder.clauses.{CompareAndSetClause, OrderingColumn, WhereClause}
import com.outworkers.phantom.builder.clauses.{CompareAndSetClause, OrderingColumn, UpdateClause, WhereClause}
import com.outworkers.phantom.builder.primitives.Primitive
import com.outworkers.phantom.builder.query.prepared.PrepareMark
import com.outworkers.phantom.builder.query.sasi.{Mode, SASITextOps}
import com.outworkers.phantom.column._
import com.outworkers.phantom.keys._
import shapeless.<:!<
import shapeless.{<:!<, HNil}

import scala.annotation.implicitNotFound

Expand Down Expand Up @@ -110,6 +109,21 @@ sealed class MapEntriesConditionals[K : Primitive, V : Primitive](val col: MapKe
}
}

sealed class MapRemoveKeyQueries[T <: CassandraTable[T, R], R, K, V](val col: AbstractMapColumn[T, R, K, V]) {

def -(elems: Seq[K])(implicit ev: Primitive[K]): UpdateClause.Condition[HNil] = {
new UpdateClause.Condition(
QueryBuilder.Collections.removeAll(col.name, elems.map(ev.asCql))
)
}

def -(head: K, tail: K*)(implicit ev: Primitive[K]): UpdateClause.Condition[HNil] = {
new UpdateClause.Condition(
QueryBuilder.Collections.removeAll(col.name, (head +: tail).map(ev.asCql))
)
}
}

sealed class MapConditionals[T <: CassandraTable[T, R], R, K, V](val col: AbstractMapColumn[T, R, K, V]) {

/**
Expand Down Expand Up @@ -181,6 +195,27 @@ private[phantom] trait ImplicitMechanism extends ModifyMechanism {
new MapConditionals(col)
}

/**
* Definition used to allow removing keys from a map column using UPDATE query syntax.
* Keys are serialised to their CQL value and passed along using SET syntax.
*
* Example: {{{
* UPDATE db.table WHERE a = b SET mapColumn -= { "a", "b", "c" }
* }}}
*
* @param col The map column to cast to a Map column secondary index query.
* @tparam T The Cassandra table inner type.
* @tparam R The record type of the table.
* @tparam K The type of the key held in the map.
* @tparam V The type of the value held in the map.
* @return A MapConditionals class with CONTAINS support.
*/
implicit def mapColumnToRemoveKeysQuery[T <: CassandraTable[T, R], R, K, V](
col: AbstractMapColumn[T, R, K, V]
)(implicit ev: col.type <:!< Keys): MapRemoveKeyQueries[T, R, K, V] = {
new MapRemoveKeyQueries(col)
}

implicit def sasiGenericOps[RR : Primitive](
col: AbstractColumn[RR] with SASIIndex[_ <: Mode]
): QueryColumn[RR] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,25 @@ private[builder] abstract class CollectionModifiers(queryBuilder: QueryBuilder)
)
}

/**
* Used to generate a query that allows removing one or more keys from a map.
* Example: {{{
* UPDATE db.table WHERE a = b SET mapColumn -= {"a", "b", "c"}
* }}}
* @param column The name of the map column to remove from.
* @param keys The keys to remove from the map column.
* @return
*/
def removeAll(column: String, keys: Seq[String]): CQLQuery = {
CQLQuery(column).forcePad.append(CQLSyntax.Symbols.eqs).forcePad.append(
collectionModifier(
column,
CQLSyntax.Symbols.-,
queryBuilder.Utils.set(keys.toSet[String])
)
)
}

def serialize(list: Seq[String]): CQLQuery = {
CQLQuery(CQLSyntax.Symbols.`[`)
.append(list.mkString(", "))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import org.scalatest.{FreeSpec, Matchers, Suite}

trait KeySpaceSuite { self: Suite =>

implicit val keySpace = KeySpace("phantom")
implicit val keySpace: KeySpace = KeySpace("phantom")
}

trait SerializationTest extends Matchers with TestDatabase.connector.Connector {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,47 @@ class MapOperationsTest extends PhantomSuite {
}
}


it should "support removing a single key from a map" in {
val recipe = gen[Recipe]
val removals = recipe.props.keys.headOption.value
val postRemove = recipe.props - removals

val operation = for {
insertDone <- database.recipes.store(recipe).future()
update <- database.recipes.update
.where(_.url eqs recipe.url)
.modify(_.props - removals)
.future()
select <- database.recipes.select(_.props).where(_.url eqs recipe.url).one
} yield select

whenReady(operation) { items =>
items.value shouldEqual postRemove
}
}

it should "support a multiple item map key remove operation" in {
val recipe = gen[Recipe]
val removals = recipe.props.take(2).keys.toSeq
val postRemove = removals.foldLeft(recipe.props) { case (map, el) =>
map - el
}

val operation = for {
insertDone <- database.recipes.store(recipe).future()
update <- database.recipes.update
.where(_.url eqs recipe.url)
.modify(_.props - removals)
.future()
select <- database.recipes.select(_.props).where(_.url eqs recipe.url).one
} yield select

whenReady(operation) { items =>
items.value shouldEqual postRemove
}
}

it should "support maps of nested primitives" in {
val event = gen[SampleEvent]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,21 @@ class UpdateQueryBuilderTest extends QueryBuilderTest {
QueryBuilder.Update.and(QueryBuilder.Where.eqs("a", "b")).queryString shouldEqual "AND a = b"
}
}


"should allow creating MAP update queries" - {

"remove a single key from a map column" in {
val qb = QueryBuilder.Collections.removeAll("mapColumn", Seq("a")).queryString
qb shouldEqual "mapColumn = mapColumn - {a}"
}

"remove multiple keys from a map column" in {
val qb = QueryBuilder.Collections.removeAll("mapColumn", Seq("a", "b", "c")).queryString
qb shouldEqual "mapColumn = mapColumn - {a, b, c}"
}

}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ package com.outworkers.phantom.builder.serializers

import com.outworkers.phantom.PhantomBaseSuite
import com.outworkers.phantom.dsl._
import com.outworkers.phantom.tables.TestDatabase
import com.outworkers.phantom.tables.{Recipe, TestDatabase}
import com.outworkers.util.samplers._
import org.joda.time.{DateTime, DateTimeZone}

Expand Down Expand Up @@ -192,6 +192,22 @@ class UpdateQuerySerializationTest extends FreeSpec with PhantomBaseSuite with T
query shouldEqual s"UPDATE phantom.events SET map[$key] = ${dt.asCql()} WHERE id = $id;"
}
}

"allow using Collection update operators" - {
"remove a single key from a Map Column using the - syntax" in {
val recipe = gen[Recipe]
val url = gen[String]
val headKey = recipe.props.keys.headOption.value

val serialized = Primitive[String].asCql(headKey)

val query = TestDatabase.recipes.update.where(_.url eqs url)
.modify(_.props - Seq(headKey))
.queryString

query shouldEqual s"UPDATE phantom.recipes SET props = props - {$serialized} WHERE url = '$url';"
}
}
}

}

0 comments on commit ea3d858

Please sign in to comment.