Skip to content
Browse files

Merge branch 'compile-time-indexes' of https://github.com/nsanch/rogue

…into nsanch-compile-time-indexes

Conflicts:
	src/main/scala/com/foursquare/rogue/Query.scala
	src/main/scala/com/foursquare/rogue/QueryClause.scala
	src/main/scala/com/foursquare/rogue/QueryField.scala
	src/main/scala/com/foursquare/rogue/Rogue.scala
	src/test/scala/com/foursquare/rogue/QueryTest.scala
  • Loading branch information...
2 parents a5e69d6 + 7a4524b commit 7ecfb9a8473d5fbd79c0bec1aad3c35121058bf8 @jliszka jliszka committed
View
18 src/main/scala/com/foursquare/rogue/IndexChecker.scala
@@ -45,7 +45,7 @@ object MongoIndexChecker extends Loggable with MongoQueryTypes {
}
def normalizeCondition(condition: MongoHelpers.AndCondition): List[List[QueryClause[_]]] = {
- flattenCondition(condition).map(_.filter(_.expectedIndexBehavior != IndexBehavior.DocumentScan))
+ flattenCondition(condition).map(_.filter(_.expectedIndexBehavior != DocumentScan))
}
/**
@@ -88,20 +88,20 @@ object MongoIndexChecker extends Loggable with MongoQueryTypes {
*/
def validateIndexExpectations(query: GenericBaseQuery[_, _], indexes: List[MongoIndex[_]]): Boolean = {
val baseConditions = normalizeCondition(query.condition);
- val conditions = baseConditions.map(_.filter(_.expectedIndexBehavior != IndexBehavior.DocumentScan))
+ val conditions = baseConditions.map(_.filter(_.expectedIndexBehavior != DocumentScan))
conditions.forall(clauses => {
clauses.forall(clause => {
// DocumentScan expectations have been filtered out at this point.
// We just have to worry about expectations being more optimistic than actual.
val badExpectations = List(
- IndexBehavior.Index -> List(IndexBehavior.PartialIndexScan, IndexBehavior.IndexScan,IndexBehavior.DocumentScan),
- IndexBehavior.IndexScan -> List(IndexBehavior.DocumentScan)
+ Index -> List(PartialIndexScan, IndexScan, DocumentScan),
+ IndexScan -> List(DocumentScan)
)
badExpectations.forall{ case (expectation, badActual) => {
if (clause.expectedIndexBehavior == expectation &&
badActual.exists(_ == clause.actualIndexBehavior)) {
- signalError(
+ signalError(
"Query is expecting %s on %s but actual behavior is %s. query = %s" format
(clause.expectedIndexBehavior, clause.fieldName, clause.actualIndexBehavior, query.toString))
} else true
@@ -144,7 +144,7 @@ object MongoIndexChecker extends Loggable with MongoQueryTypes {
private def matchesUniqueIndex(clauses: List[QueryClause[_]]) = {
// Special case for overspecified queries matching on the _id field.
// TODO: Do the same for any overspecified query exactly matching a unique index.
- clauses.exists(clause => clause.fieldName == "_id" && clause.actualIndexBehavior == IndexBehavior.Index)
+ clauses.exists(clause => clause.fieldName == "_id" && clause.actualIndexBehavior == Index)
}
private def matchesIndex(index: List[String],
@@ -181,11 +181,11 @@ object MongoIndexChecker extends Loggable with MongoQueryTypes {
matchingClauses match {
case matchingClause :: _ => {
// If a previous field caused a scan, this field must scan too.
- val expectationOk = !scanning || matchingClause.expectedIndexBehavior == IndexBehavior.IndexScan
+ val expectationOk = !scanning || matchingClause.expectedIndexBehavior == IndexScan
// If this field causes a scan, later fields must scan too.
val nowScanning = scanning ||
- matchingClause.actualIndexBehavior == IndexBehavior.IndexScan ||
- matchingClause.actualIndexBehavior == IndexBehavior.PartialIndexScan
+ matchingClause.actualIndexBehavior == IndexScan ||
+ matchingClause.actualIndexBehavior == PartialIndexScan
expectationOk && matchesCompoundIndex(rest, remainingClauses, scanning = nowScanning)
}
case Nil => {
View
2 src/main/scala/com/foursquare/rogue/MongoHelpers.scala
@@ -40,7 +40,7 @@ object MongoHelpers {
// and can be chained like { a : { $gt : 2, $lt: 6 }}.
// So if there is any equality clause, apply it (only) to the builder;
// otherwise, chain the clauses.
- cs.filter(_.isInstanceOf[EqClause[_]]).headOption match {
+ cs.filter(_.isInstanceOf[EqClause[_, _]]).headOption match {
case Some(eqClause) => eqClause.extend(builder, signature)
case None => {
builder.push(name)
View
248 src/main/scala/com/foursquare/rogue/Query.scala
@@ -5,7 +5,6 @@ package com.foursquare.rogue
import collection.immutable.List._
import com.foursquare.rogue.MongoHelpers._
import com.mongodb.{BasicDBObjectBuilder, DBObject, WriteConcern}
-import net.liftweb.record.Field
import net.liftweb.common.{Box, Full}
import net.liftweb.mongodb.MongoDB
import net.liftweb.mongodb.record._
@@ -38,6 +37,233 @@ abstract sealed class MaybeHasOrClause
abstract sealed class HasOrClause extends MaybeHasOrClause
abstract sealed class HasNoOrClause extends MaybeHasOrClause
+sealed trait MaybeIndexed
+sealed trait Indexable extends MaybeIndexed
+sealed trait IndexScannable extends MaybeIndexed
+
+abstract sealed class NoIndexInfo extends Indexable with IndexScannable
+abstract sealed class Index extends Indexable with IndexScannable
+abstract sealed class PartialIndexScan extends IndexScannable
+abstract sealed class IndexScan extends IndexScannable
+abstract sealed class DocumentScan extends MaybeIndexed
+
+case object NoIndexInfo extends NoIndexInfo
+case object Index extends Index
+case object PartialIndexScan extends PartialIndexScan
+case object IndexScan extends IndexScan
+case object DocumentScan extends DocumentScan
+
+abstract sealed class MaybeUsedIndex
+abstract sealed class UsedIndex extends MaybeUsedIndex
+abstract sealed class HasntUsedIndex extends MaybeUsedIndex
+
+// ***************************************************************************
+// *** Indexes
+// ***************************************************************************
+
+class IndexEnforcerBuilder[M <: MongoRecord[M]](meta: M with MongoMetaRecord[M] with IndexedRecord[M]) {
+ type MetaM = M with MongoMetaRecord[M] with IndexedRecord[M]
+
+ def useIndex[F1 <: Field[_, M]](i: MongoIndex1[M, F1, _]): IndexEnforcer1[M, NoIndexInfo, F1, HasntUsedIndex] = {
+ new IndexEnforcer1[M, NoIndexInfo, F1, HasntUsedIndex](meta, new BaseQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause](meta, None, None, None, None, None, AndCondition(Nil, None), None, None))
+ }
+
+ def useIndex[F1 <: Field[_, M], F2 <: Field[_, M]](i: MongoIndex2[M, F1, _, F2, _]): IndexEnforcer2[M, NoIndexInfo, F1, F2, HasntUsedIndex] = {
+ new IndexEnforcer2[M, NoIndexInfo, F1, F2, HasntUsedIndex](meta, new BaseQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause](meta, None, None, None, None, None, AndCondition(Nil, None), None, None))
+ }
+
+ def useIndex[F1 <: Field[_, M], F2 <: Field[_, M], F3 <: Field[_, M]](i: MongoIndex3[M, F1, _, F2, _, F3, _]): IndexEnforcer3[M, NoIndexInfo, F1, F2, F3, HasntUsedIndex] = {
+ new IndexEnforcer3[M, NoIndexInfo, F1, F2, F3, HasntUsedIndex](meta, new BaseQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause](meta, None, None, None, None, None, AndCondition(Nil, None), None, None))
+ }
+
+ def useIndex[F1 <: Field[_, M], F2 <: Field[_, M], F3 <: Field[_, M], F4 <: Field[_, M]](i: MongoIndex4[M, F1, _, F2, _, F3, _, F4, _]): IndexEnforcer4[M, NoIndexInfo, F1, F2, F3, F4, HasntUsedIndex] = {
+ new IndexEnforcer4[M, NoIndexInfo, F1, F2, F3, F4, HasntUsedIndex](meta, new BaseQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause](meta, None, None, None, None, None, AndCondition(Nil, None), None, None))
+ }
+
+ def useIndex[F1 <: Field[_, M], F2 <: Field[_, M], F3 <: Field[_, M], F4 <: Field[_, M], F5 <: Field[_, M]](i: MongoIndex5[M, F1, _, F2, _, F3, _, F4, _, F5, _]): IndexEnforcer5[M, NoIndexInfo, F1, F2, F3, F4, F5, HasntUsedIndex] = {
+ new IndexEnforcer5[M, NoIndexInfo, F1, F2, F3, F4, F5, HasntUsedIndex](meta, new BaseQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause](meta, None, None, None, None, None, AndCondition(Nil, None), None, None))
+ }
+
+ def useIndex[F1 <: Field[_, M], F2 <: Field[_, M], F3 <: Field[_, M], F4 <: Field[_, M], F5 <: Field[_, M], F6 <: Field[_, M]](i: MongoIndex6[M, F1, _, F2, _, F3, _, F4, _, F5, _, F6, _]): IndexEnforcer6[M, NoIndexInfo, F1, F2, F3, F4, F5, F6, HasntUsedIndex] = {
+ new IndexEnforcer6[M, NoIndexInfo, F1, F2, F3, F4, F5, F6, HasntUsedIndex](meta, new BaseQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause](meta, None, None, None, None, None, AndCondition(Nil, None), None, None))
+ }
+}
+
+case class IndexEnforcer1[M <: MongoRecord[M],
+ Ind <: MaybeIndexed,
+ F1 <: Field[_, M],
+ UsedInd <: MaybeUsedIndex](meta: M with MongoMetaRecord[M],
+ q: AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause]) {
+ def where[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause] = {
+ q.where(_ => clause(f1Func(meta)))
+ }
+
+ def and[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause] = {
+ q.and(_ => clause(f1Func(meta)))
+ }
+
+ def iscan[F, ClauseInd <: IndexScannable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd]): AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause] = {
+ q.iscan(_ => clause(f1Func(meta)))
+ }
+
+ def rangeScan(f1Func: M => F1)(implicit ev: UsedInd <:< UsedIndex): AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause] = q
+}
+
+case class IndexEnforcer2[M <: MongoRecord[M],
+ Ind <: MaybeIndexed,
+ F1 <: Field[_, M],
+ F2 <: Field[_, M],
+ UsedInd <: MaybeUsedIndex](meta: M with MongoMetaRecord[M],
+ q: AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause]) {
+ def where[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): IndexEnforcer1[M, Index, F2, UsedIndex] = {
+ new IndexEnforcer1[M, Index, F2, UsedIndex](meta, q.where(_ => clause(f1Func(meta))))
+ }
+
+ def and[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): IndexEnforcer1[M, Index, F2, UsedIndex] = {
+ new IndexEnforcer1[M, Index, F2, UsedIndex](meta, q.and(_ => clause(f1Func(meta))))
+ }
+
+ def iscan[F, ClauseInd <: IndexScannable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd]): IndexEnforcer1[M, IndexScan, F2, UsedIndex] = {
+ new IndexEnforcer1[M, IndexScan, F2, UsedIndex](meta, q.iscan(_ => clause(f1Func(meta))))
+ }
+
+ def rangeScan(f1Func: M => F1)(implicit ev: UsedInd <:< UsedIndex): IndexEnforcer1[M, IndexScan, F2, UsedIndex] = {
+ new IndexEnforcer1[M, IndexScan, F2, UsedIndex](meta, q)
+ }
+
+ def rangeScan(f1Func: M => F1, f2Func: M => F2)(implicit ev: UsedInd <:< UsedIndex): AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause] = q
+}
+
+case class IndexEnforcer3[M <: MongoRecord[M],
+ Ind <: MaybeIndexed,
+ F1 <: Field[_, M],
+ F2 <: Field[_, M],
+ F3 <: Field[_, M],
+ UsedInd <: MaybeUsedIndex](meta: M with MongoMetaRecord[M],
+ q: AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause]) {
+ def where[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): IndexEnforcer2[M, Index, F2, F3, UsedIndex] = {
+ new IndexEnforcer2[M, Index, F2, F3, UsedIndex](meta, q.where(_ => clause(f1Func(meta))))
+ }
+
+ def and[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): IndexEnforcer2[M, Index, F2, F3, UsedIndex] = {
+ new IndexEnforcer2[M, Index, F2, F3, UsedIndex](meta, q.and(_ => clause(f1Func(meta))))
+ }
+
+ def iscan[F, ClauseInd <: IndexScannable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd]): IndexEnforcer2[M, IndexScan, F2, F3, UsedIndex] = {
+ new IndexEnforcer2[M, IndexScan, F2, F3, UsedIndex](meta, q.iscan(_ => clause(f1Func(meta))))
+ }
+
+ def rangeScan(f1Func: M => F1)(implicit ev: UsedInd <:< UsedIndex): IndexEnforcer2[M, IndexScan, F2, F3, UsedIndex] = {
+ new IndexEnforcer2[M, IndexScan, F2, F3, UsedIndex](meta, q)
+ }
+
+ def rangeScan(f1Func: M => F1, f2Func: M => F2)(implicit ev: UsedInd <:< UsedIndex): IndexEnforcer1[M, IndexScan, F3, UsedIndex] = {
+ new IndexEnforcer1[M, IndexScan, F3, UsedIndex](meta, q)
+ }
+
+ def rangeScan(f1Func: M => F1, f2Func: M => F2, f3Func: M => F3)(implicit ev: UsedInd <:< UsedIndex): AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause] = q
+}
+
+case class IndexEnforcer4[M <: MongoRecord[M],
+ Ind <: MaybeIndexed,
+ F1 <: Field[_, M],
+ F2 <: Field[_, M],
+ F3 <: Field[_, M],
+ F4 <: Field[_, M],
+ UsedInd <: MaybeUsedIndex](meta: M with MongoMetaRecord[M],
+ q: AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause]) {
+ def where[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): IndexEnforcer3[M, Index, F2, F3, F4, UsedIndex] = {
+ new IndexEnforcer3[M, Index, F2, F3, F4, UsedIndex](meta, q.where(_ => clause(f1Func(meta))))
+ }
+
+ def and[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): IndexEnforcer3[M, Index, F2, F3, F4, UsedIndex] = {
+ new IndexEnforcer3[M, Index, F2, F3, F4, UsedIndex](meta, q.and(_ => clause(f1Func(meta))))
+ }
+
+ def iscan[F, ClauseInd <: IndexScannable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd]): IndexEnforcer3[M, IndexScan, F2, F3, F4, UsedIndex] = {
+ new IndexEnforcer3[M, IndexScan, F2, F3, F4, UsedIndex](meta, q.iscan(_ => clause(f1Func(meta))))
+ }
+
+ def rangeScan(f1Func: M => F1)(implicit ev: UsedInd <:< UsedIndex): IndexEnforcer3[M, IndexScan, F2, F3, F4, UsedIndex] = {
+ new IndexEnforcer3[M, IndexScan, F2, F3, F4, UsedIndex](meta, q)
+ }
+
+ def rangeScan(f1Func: M => F1, f2Func: M => F2)(implicit ev: UsedInd <:< UsedIndex): IndexEnforcer2[M, IndexScan, F3, F4, UsedIndex] = {
+ new IndexEnforcer2[M, IndexScan, F3, F4, UsedIndex](meta, q)
+ }
+
+ def rangeScan(f1Func: M => F1, f2Func: M => F2, f3Func: M => F3)(implicit ev: UsedInd <:< UsedIndex): IndexEnforcer1[M, IndexScan, F4, UsedIndex] = {
+ new IndexEnforcer1[M, IndexScan, F4, UsedIndex](meta, q)
+ }
+
+ def rangeScan(f1Func: M => F1, f2Func: M => F2, f3Func: M => F3, f4Func: M => F4)(implicit ev: UsedInd <:< UsedIndex): AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause] = q
+}
+
+case class IndexEnforcer5[M <: MongoRecord[M],
+ Ind <: MaybeIndexed,
+ F1 <: Field[_, M],
+ F2 <: Field[_, M],
+ F3 <: Field[_, M],
+ F4 <: Field[_, M],
+ F5 <: Field[_, M],
+ UsedInd <: MaybeUsedIndex](meta: M with MongoMetaRecord[M],
+ q: AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause]) {
+ def where[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): IndexEnforcer4[M, Index, F2, F3, F4, F5, UsedIndex] = {
+ new IndexEnforcer4[M, Index, F2, F3, F4, F5, UsedIndex](meta, q.where(_ => clause(f1Func(meta))))
+ }
+
+ def and[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): IndexEnforcer4[M, Index, F2, F3, F4, F5, UsedIndex] = {
+ new IndexEnforcer4[M, Index, F2, F3, F4, F5, UsedIndex](meta, q.and(_ => clause(f1Func(meta))))
+ }
+
+ def iscan[F, ClauseInd <: IndexScannable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd]): IndexEnforcer4[M, IndexScan, F2, F3, F4, F5, UsedIndex] = {
+ new IndexEnforcer4[M, IndexScan, F2, F3, F4, F5, UsedIndex](meta, q.iscan(_ => clause(f1Func(meta))))
+ }
+
+ def rangeScan(f1Func: M => F1)(implicit ev: UsedInd <:< UsedIndex): IndexEnforcer4[M, IndexScan, F2, F3, F4, F5, UsedIndex] = {
+ new IndexEnforcer4[M, IndexScan, F2, F3, F4, F5, UsedIndex](meta, q)
+ }
+
+ def rangeScan(f1Func: M => F1, f2Func: M => F2)(implicit ev: UsedInd <:< UsedIndex): IndexEnforcer3[M, IndexScan, F3, F4, F5, UsedIndex] = {
+ new IndexEnforcer3[M, IndexScan, F3, F4, F5, UsedIndex](meta, q)
+ }
+
+ def rangeScan(f1Func: M => F1, f2Func: M => F2, f3Func: M => F3)(implicit ev: UsedInd <:< UsedIndex): IndexEnforcer2[M, IndexScan, F4, F5, UsedIndex] = {
+ new IndexEnforcer2[M, IndexScan, F4, F5, UsedIndex](meta, q)
+ }
+
+ def rangeScan(f1Func: M => F1, f2Func: M => F2, f3Func: M => F3, f4Func: M => F4)(implicit ev: UsedInd <:< UsedIndex): IndexEnforcer1[M, IndexScan, F5, UsedIndex] = {
+ new IndexEnforcer1[M, IndexScan, F5, UsedIndex](meta, q)
+ }
+}
+
+case class IndexEnforcer6[M <: MongoRecord[M],
+ Ind <: MaybeIndexed,
+ F1 <: Field[_, M],
+ F2 <: Field[_, M],
+ F3 <: Field[_, M],
+ F4 <: Field[_, M],
+ F5 <: Field[_, M],
+ F6 <: Field[_, M],
+ UsedInd <: MaybeUsedIndex](meta: M with MongoMetaRecord[M],
+ q: AbstractQuery[M, M, Unordered, Unselected, Unlimited, Unskipped, HasNoOrClause]) {
+ def where[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): IndexEnforcer5[M, Index, F2, F3, F4, F5, F6, UsedIndex] = {
+ new IndexEnforcer5[M, Index, F2, F3, F4, F5, F6, UsedIndex](meta, q.where(_ => clause(f1Func(meta))))
+ }
+
+ def and[F, ClauseInd <: Indexable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd])(implicit ev: Ind <:< Indexable): IndexEnforcer5[M, Index, F2, F3, F4, F5, F6, UsedIndex] = {
+ new IndexEnforcer5[M, Index, F2, F3, F4, F5, F6, UsedIndex](meta, q.and(_ => clause(f1Func(meta))))
+ }
+
+ def iscan[F, ClauseInd <: IndexScannable](f1Func: M => F1)(clause: F1 => IndexableQueryClause[F, ClauseInd]): IndexEnforcer5[M, IndexScan, F2, F3, F4, F5, F6, UsedIndex] = {
+ new IndexEnforcer5[M, IndexScan, F2, F3, F4, F5, F6, UsedIndex](meta, q.iscan(_ => clause(f1Func(meta))))
+ }
+
+ // IndexEnforcer6 doesn't have methods to scan any later fields in the index
+ // because there's no way that we got here via an iscan on a bigger index --
+ // there are no bigger indexes. We require that the first column on the index
+ // gets used.
+}
// ***************************************************************************
// *** Builders
@@ -426,7 +652,7 @@ case class BaseQuery[M <: MongoRecord[M], R,
override lazy val master = meta.meta
private def addClause[F](clause: M => QueryClause[F],
- expectedIndexBehavior: IndexBehavior.Value):
+ expectedIndexBehavior: MaybeIndexed):
AbstractQuery[M, R, Ord, Sel, Lim, Sk, Or] = {
clause(meta) match {
case cl: EmptyQueryClause[_] => new BaseEmptyQuery[M, R, Ord, Sel, Lim, Sk, Or]
@@ -438,17 +664,17 @@ case class BaseQuery[M <: MongoRecord[M], R,
}
override def where[F](clause: M => QueryClause[F]) =
- addClause(clause, expectedIndexBehavior = IndexBehavior.Index)
+ addClause(clause, expectedIndexBehavior = Index)
override def and[F](clause: M => QueryClause[F]) =
- addClause(clause, expectedIndexBehavior = IndexBehavior.Index)
+ addClause(clause, expectedIndexBehavior = Index)
override def iscan[F](clause: M => QueryClause[F]) =
- addClause(clause, expectedIndexBehavior = IndexBehavior.IndexScan)
+ addClause(clause, expectedIndexBehavior = IndexScan)
override def scan[F](clause: M => QueryClause[F]) =
- addClause(clause, expectedIndexBehavior = IndexBehavior.DocumentScan)
+ addClause(clause, expectedIndexBehavior = DocumentScan)
private def addClauseOpt[V, F](opt: Option[V])
(clause: (M, V) => QueryClause[F],
- expectedIndexBehavior: IndexBehavior.Value) = {
+ expectedIndexBehavior: MaybeIndexed) = {
opt match {
case Some(v) => addClause(clause(_, v), expectedIndexBehavior)
case None => this
@@ -456,13 +682,13 @@ case class BaseQuery[M <: MongoRecord[M], R,
}
override def whereOpt[V, F](opt: Option[V])(clause: (M, V) => QueryClause[F]) =
- addClauseOpt(opt)(clause, expectedIndexBehavior = IndexBehavior.Index)
+ addClauseOpt(opt)(clause, expectedIndexBehavior = Index)
override def andOpt[V, F](opt: Option[V])(clause: (M, V) => QueryClause[F]) =
- addClauseOpt(opt)(clause, expectedIndexBehavior = IndexBehavior.Index)
+ addClauseOpt(opt)(clause, expectedIndexBehavior = Index)
override def iscanOpt[V, F](opt: Option[V])(clause: (M, V) => QueryClause[F]) =
- addClauseOpt(opt)(clause, expectedIndexBehavior = IndexBehavior.IndexScan)
+ addClauseOpt(opt)(clause, expectedIndexBehavior = IndexScan)
override def scanOpt[V, F](opt: Option[V])(clause: (M, V) => QueryClause[F]) =
- addClauseOpt(opt)(clause, expectedIndexBehavior = IndexBehavior.DocumentScan)
+ addClauseOpt(opt)(clause, expectedIndexBehavior = DocumentScan)
override def raw(f: BasicDBObjectBuilder => Unit) = {
val newClause = new RawQueryClause(f)
View
131 src/main/scala/com/foursquare/rogue/QueryClause.scala
@@ -4,104 +4,119 @@ package com.foursquare.rogue
import com.mongodb.DBObject
import com.mongodb.BasicDBObjectBuilder
+import net.liftweb.record.Field
-/**
- * An enumeration that defines the possible types of indexing that are supported by rogue
- * query clauses.
- */
-object IndexBehavior extends Enumeration {
- type IndexBehavior = Value
- val Index = Value("Index")
- val PartialIndexScan = Value("Partial index scan")
- val IndexScan = Value("Index scan")
- val DocumentScan = Value("Document scan")
-}
-
-/*
- * For each query clause, we define a class which computes the actual index behavior supported
- * by the underlying object type, and the index behavior expected by the query.
- */
-
-class QueryClause[V](val fieldName: String, conditions: (CondOps.Value, V)*) {
+class QueryClause[V](val fieldName: String, val actualIndexBehavior: MaybeIndexed, conditions: (CondOps.Value, V)*) {
def extend(q: BasicDBObjectBuilder, signature: Boolean): Unit = {
conditions foreach { case (op, v) => q.add(op.toString, if (signature) 0 else v) }
}
-
- lazy val actualIndexBehavior = conditions.head._1 match {
- case CondOps.All | CondOps.In => IndexBehavior.Index
- case CondOps.Gt | CondOps.GtEq | CondOps.Lt | CondOps.LtEq | CondOps.Ne | CondOps.Near =>
- IndexBehavior.PartialIndexScan
- case CondOps.Mod | CondOps.Type => IndexBehavior.IndexScan
- case CondOps.Exists | CondOps.Nin | CondOps.Size => IndexBehavior.DocumentScan
- }
-
- val expectedIndexBehavior = IndexBehavior.Index
-
- def withExpectedIndexBehavior(b: IndexBehavior.Value) = new QueryClause(fieldName, conditions: _*) {
+ val expectedIndexBehavior: MaybeIndexed = Index
+ def withExpectedIndexBehavior(b: MaybeIndexed) = new QueryClause(fieldName, actualIndexBehavior, conditions: _*) {
override val expectedIndexBehavior = b
}
}
-class RawQueryClause(f: BasicDBObjectBuilder => Unit) extends QueryClause("raw") {
+class IndexableQueryClause[V, Ind <: MaybeIndexed](fname: String, actualIB: Ind, conds: (CondOps.Value, V)*)
+ extends QueryClause[V](fname, actualIB, conds: _*)
+
+class AllQueryClause[V](fieldName: String, vs: java.util.List[V])
+ extends IndexableQueryClause[java.util.List[V], Index](fieldName, Index, CondOps.All -> vs)
+
+class InQueryClause[V](fieldName: String, vs: java.util.List[V])
+ extends IndexableQueryClause[java.util.List[V], Index](fieldName, Index, CondOps.In -> vs)
+
+class GtQueryClause[V](fieldName: String, v: V)
+ extends IndexableQueryClause[V, PartialIndexScan](fieldName, PartialIndexScan, CondOps.Gt -> v)
+
+class GtEqQueryClause[V](fieldName: String, v: V)
+ extends IndexableQueryClause[V, PartialIndexScan](fieldName, PartialIndexScan, CondOps.GtEq -> v)
+
+class LtQueryClause[V](fieldName: String, v: V)
+ extends IndexableQueryClause[V, PartialIndexScan](fieldName, PartialIndexScan, CondOps.Lt -> v)
+
+class LtEqQueryClause[V](fieldName: String, v: V)
+ extends IndexableQueryClause[V, PartialIndexScan](fieldName, PartialIndexScan, CondOps.LtEq -> v)
+
+class BetweenQueryClause[V](fieldName: String, lower: V, upper: V)
+ extends IndexableQueryClause[V, PartialIndexScan](fieldName, PartialIndexScan, CondOps.GtEq -> lower, CondOps.LtEq -> upper)
+
+class StrictBetweenQueryClause[V](fieldName: String, lower: V, upper: V)
+ extends IndexableQueryClause[V, PartialIndexScan](fieldName, PartialIndexScan, CondOps.Gt -> lower, CondOps.Lt -> upper)
+
+class NeQueryClause[V](fieldName: String, v: V)
+ extends IndexableQueryClause[V, PartialIndexScan](fieldName, PartialIndexScan, CondOps.Ne -> v)
+
+class NearQueryClause[V](fieldName: String, v: V)
+ extends IndexableQueryClause[V, PartialIndexScan](fieldName, PartialIndexScan, CondOps.Near -> v)
+
+class ModQueryClause[V](fieldName: String, v: java.util.List[V])
+ extends IndexableQueryClause[java.util.List[V], IndexScan](fieldName, IndexScan, CondOps.Mod -> v)
+
+class TypeQueryClause(fieldName: String, v: MongoType.Value)
+ extends IndexableQueryClause[Int, IndexScan](fieldName, IndexScan, CondOps.Type -> v.id)
+
+class ExistsQueryClause(fieldName: String, v: Boolean)
+ extends IndexableQueryClause[Boolean, DocumentScan](fieldName, DocumentScan, CondOps.Exists -> v)
+
+class NinQueryClause[V](fieldName: String, vs: java.util.List[V])
+ extends IndexableQueryClause[java.util.List[V], DocumentScan](fieldName, DocumentScan, CondOps.Nin -> vs)
+
+class SizeQueryClause(fieldName: String, v: Int)
+ extends IndexableQueryClause[Int, DocumentScan](fieldName, DocumentScan, CondOps.Size -> v)
+
+class RawQueryClause(f: BasicDBObjectBuilder => Unit) extends IndexableQueryClause("raw", DocumentScan) {
override def extend(q: BasicDBObjectBuilder, signature: Boolean): Unit = {
f(q)
}
-
- override lazy val actualIndexBehavior = IndexBehavior.DocumentScan
-
- override val expectedIndexBehavior = IndexBehavior.DocumentScan
+ override val expectedIndexBehavior = DocumentScan
}
-class EmptyQueryClause[V](fieldName: String) extends QueryClause[V](fieldName) {
+class EmptyQueryClause[V](fieldName: String) extends IndexableQueryClause[V, Index](fieldName, Index) {
override def extend(q: BasicDBObjectBuilder, signature: Boolean): Unit = {}
-
- override lazy val actualIndexBehavior = IndexBehavior.Index
-
- override def withExpectedIndexBehavior(b: IndexBehavior.Value) =
+ override def withExpectedIndexBehavior(b: MaybeIndexed) = {
new EmptyQueryClause[V](fieldName) {
override val expectedIndexBehavior = b
}
+ }
}
-class EqClause[V](fieldName: String, value: V) extends QueryClause[V](fieldName) {
+class EqClause[V, Ind <: MaybeIndexed](fieldName: String, actualIB: Ind, value: V) extends IndexableQueryClause[V, Ind](fieldName, actualIB) {
override def extend(q: BasicDBObjectBuilder, signature: Boolean): Unit = {
q.add(fieldName, if (signature) 0 else value)
}
+ override def withExpectedIndexBehavior(b: MaybeIndexed) = new EqClause[V, Ind](fieldName, actualIB, value) {
+ override val expectedIndexBehavior = b
+ }
+}
- override lazy val actualIndexBehavior = IndexBehavior.Index
-
- override def withExpectedIndexBehavior(b: IndexBehavior.Value) =
- new EqClause(fieldName, value) { override val expectedIndexBehavior = b }
+object EqClause {
+ def apply[V](fieldName: String, value: V) = {
+ new EqClause[V, Index](fieldName, Index, value)
+ }
}
-class WithinCircleClause[V](fieldName: String, lat: Double, lng: Double, radius: Double)
- extends QueryClause(fieldName) {
+class WithinCircleClause[V](fieldName: String, lat: Double, lng: Double, radius: Double) extends IndexableQueryClause[V, PartialIndexScan](fieldName, PartialIndexScan) {
override def extend(q: BasicDBObjectBuilder, signature: Boolean): Unit = {
val value = if (signature) 0 else QueryHelpers.list(List(QueryHelpers.list(List(lat, lng)), radius))
q.push("$within").add("$center", value).pop
}
-
- override lazy val actualIndexBehavior = IndexBehavior.PartialIndexScan
-
- override def withExpectedIndexBehavior(b: IndexBehavior.Value) =
+ override def withExpectedIndexBehavior(b: MaybeIndexed) = {
new WithinCircleClause[V](fieldName, lat, lng, radius) {
override val expectedIndexBehavior = b
}
+ }
}
-class WithinBoxClause[V](fieldName: String, lat1: Double, lng1: Double, lat2: Double, lng2: Double)
- extends QueryClause(fieldName) {
+class WithinBoxClause[V](fieldName: String, lat1: Double, lng1: Double, lat2: Double, lng2: Double) extends IndexableQueryClause[V, PartialIndexScan](fieldName, PartialIndexScan) {
override def extend(q: BasicDBObjectBuilder, signature: Boolean): Unit = {
val value = if (signature) 0 else {
QueryHelpers.list(List(QueryHelpers.list(lat1, lng1), QueryHelpers.list(lat2, lng2)))
}
q.push("$within").add("$box", value).pop
}
-
- override lazy val actualIndexBehavior = IndexBehavior.PartialIndexScan
-
- override def withExpectedIndexBehavior(b: IndexBehavior.Value) =
- new WithinBoxClause[V](fieldName, lat1, lng1, lat2, lng2) { override val expectedIndexBehavior = b }
+ override def withExpectedIndexBehavior(b: MaybeIndexed) = new WithinBoxClause[V](fieldName, lat1, lng1, lat2, lng2) {
+ override val expectedIndexBehavior = b
+ }
}
class ModifyClause[V](val operator: ModOps.Value, fields: (String, V)*) {
View
90 src/main/scala/com/foursquare/rogue/QueryField.scala
@@ -61,33 +61,29 @@ abstract class AbstractQueryField[V, DB, M <: MongoRecord[M]](val field: Field[V
def valueToDB(v: V): DB
def valuesToDB(vs: Traversable[V]) = vs.map(valueToDB _)
- def eqs(v: V) = new EqClause(field.name, valueToDB(v))
- def neqs(v: V) = new QueryClause(field.name, CondOps.Ne -> valueToDB(v))
+ def eqs(v: V) = EqClause(field.name, valueToDB(v))
+ def neqs(v: V) = new NeQueryClause(field.name, valueToDB(v))
def in[L <% Traversable[V]](vs: L) = QueryHelpers.inListClause(field.name, valuesToDB(vs))
- def nin[L <% Traversable[V]](vs: L) = new QueryClause(field.name, CondOps.Nin -> QueryHelpers.list(valuesToDB(vs)))
+ def nin[L <% Traversable[V]](vs: L) = new NinQueryClause(field.name, QueryHelpers.list(valuesToDB(vs)))
}
class QueryField[V, M <: MongoRecord[M]](field: Field[V, M])
extends AbstractQueryField[V, V, M](field) {
override def valueToDB(v: V) = v
- def exists(b: Boolean) = new QueryClause(field.name, CondOps.Exists -> b)
+ def exists(b: Boolean) = new ExistsQueryClause(field.name, b)
- def hastype(t: MongoType.Value) = new QueryClause(field.name, CondOps.Type -> t.id)
+ def hastype(t: MongoType.Value) = new TypeQueryClause(field.name, t)
}
class CalendarQueryField[M <: MongoRecord[M]](val field: Field[java.util.Calendar, M]) {
- def before(d: DateTime) = new QueryClause(field.name, CondOps.Lt -> d.toDate)
+ def before(d: DateTime) = new LtQueryClause(field.name, d.toDate)
- def after(d: DateTime) = new QueryClause(field.name, CondOps.Gt -> d.toDate)
+ def after(d: DateTime) = new GtQueryClause(field.name, d.toDate)
- def between(d1: DateTime, d2: DateTime) = new QueryClause(field.name,
- CondOps.Gt -> d1.toDate,
- CondOps.Lt -> d2.toDate)
+ def between(d1: DateTime, d2: DateTime) = new StrictBetweenQueryClause(field.name, d1.toDate, d2.toDate)
- def between(range: (DateTime, DateTime)) = new QueryClause(field.name,
- CondOps.Gt -> range._1.toDate,
- CondOps.Lt -> range._2.toDate)
+ def between(range: (DateTime, DateTime)) = new StrictBetweenQueryClause(field.name, range._1.toDate, range._2.toDate)
}
class EnumerationQueryField[M <: MongoRecord[M], E <: Enumeration#Value](field: Field[E, M])
@@ -101,15 +97,13 @@ class GeoQueryField[M <: MongoRecord[M]](field: Field[LatLong, M])
QueryHelpers.list(List(ll.lat, ll.long))
def eqs(lat: Double, lng: Double) =
- new EqClause(field.name, QueryHelpers.list(List(lat, lng)))
+ EqClause(field.name, QueryHelpers.list(List(lat, lng)))
def neqs(lat: Double, lng: Double) =
- new QueryClause(field.name,
- CondOps.Ne -> QueryHelpers.list(List(lat, lng)))
+ new NeQueryClause(field.name, QueryHelpers.list(List(lat, lng)))
def near(lat: Double, lng: Double, radius: Degrees) =
- new QueryClause(field.name,
- CondOps.Near -> QueryHelpers.list(List(lat, lng, QueryHelpers.radius(radius))))
+ new NearQueryClause(field.name, QueryHelpers.list(List(lat, lng, QueryHelpers.radius(radius))))
def withinCircle(lat: Double, lng: Double, radius: Degrees) =
new WithinCircleClause(field.name, lat, lng, QueryHelpers.radius(radius))
@@ -120,13 +114,13 @@ class GeoQueryField[M <: MongoRecord[M]](field: Field[LatLong, M])
abstract class AbstractNumericQueryField[V, DB, M <: MongoRecord[M]](field: Field[V, M])
extends AbstractQueryField[V, DB, M](field) {
- def lt(v: V) = new QueryClause(field.name, CondOps.Lt -> valueToDB(v))
+ def lt(v: V) = new LtQueryClause(field.name, valueToDB(v))
- def gt(v: V) = new QueryClause(field.name, CondOps.Gt -> valueToDB(v))
+ def gt(v: V) = new GtQueryClause(field.name, valueToDB(v))
- def lte(v: V) = new QueryClause(field.name, CondOps.LtEq -> valueToDB(v))
+ def lte(v: V) = new LtEqQueryClause(field.name, valueToDB(v))
- def gte(v: V) = new QueryClause(field.name, CondOps.GtEq -> valueToDB(v))
+ def gte(v: V) = new GtEqQueryClause(field.name, valueToDB(v))
def <(v: V) = lt(v)
@@ -137,13 +131,10 @@ abstract class AbstractNumericQueryField[V, DB, M <: MongoRecord[M]](field: Fiel
def >=(v: V) = gte(v)
def between(v1: V, v2: V) =
- new QueryClause(field.name,
- CondOps.GtEq -> valueToDB(v1),
- CondOps.LtEq -> valueToDB(v2))
+ new BetweenQueryClause(field.name, valueToDB(v1), valueToDB(v2))
def mod(by: Int, eq: Int) =
- new QueryClause(field.name,
- CondOps.Mod -> QueryHelpers.list(List(by, eq)))
+ new ModQueryClause(field.name, QueryHelpers.list(List(by, eq)))
}
class NumericQueryField[V, M <: MongoRecord[M]](field: Field[V, M])
@@ -154,22 +145,16 @@ class NumericQueryField[V, M <: MongoRecord[M]](field: Field[V, M])
class ObjectIdQueryField[M <: MongoRecord[M]](override val field: Field[ObjectId, M])
extends NumericQueryField(field) {
def before(d: DateTime) =
- new QueryClause(field.name,
- CondOps.Lt -> new ObjectId(d.toDate, 0, 0))
+ new LtQueryClause(field.name, new ObjectId(d.toDate, 0, 0))
def after(d: DateTime) =
- new QueryClause(field.name,
- CondOps.Gt -> new ObjectId(d.toDate, 0, 0))
+ new GtQueryClause(field.name, new ObjectId(d.toDate, 0, 0))
def between(d1: DateTime, d2: DateTime) =
- new QueryClause(field.name,
- CondOps.Gt -> new ObjectId(d1.toDate, 0, 0),
- CondOps.Lt -> new ObjectId(d2.toDate, 0, 0))
+ new StrictBetweenQueryClause(field.name, new ObjectId(d1.toDate, 0, 0), new ObjectId(d2.toDate, 0, 0))
def between(range: (DateTime, DateTime)) =
- new QueryClause(field.name,
- CondOps.Gt -> new ObjectId(range._1.toDate, 0, 0),
- CondOps.Lt -> new ObjectId(range._2.toDate, 0, 0))
+ new StrictBetweenQueryClause(field.name, new ObjectId(range._1.toDate, 0, 0), new ObjectId(range._2.toDate, 0, 0))
}
class ForeignObjectIdQueryField[M <: MongoRecord[M], T <: MongoRecord[T]
@@ -178,29 +163,30 @@ class ForeignObjectIdQueryField[M <: MongoRecord[M], T <: MongoRecord[T]
extends ObjectIdQueryField[M](field) {
def eqs(obj: T) =
- new EqClause(field.name, obj.id)
+ EqClause(field.name, obj.id)
def neqs(obj: T) =
- new QueryClause(field.name, CondOps.Ne -> obj.id)
+ new NeQueryClause(field.name, obj.id)
def in(objs: Traversable[T]) =
QueryHelpers.inListClause(field.name, objs.map(_.id))
def nin(objs: Traversable[T]) =
- new QueryClause(field.name,
- CondOps.Nin -> QueryHelpers.list(objs.map(_.id)))
+ new NinQueryClause(field.name, QueryHelpers.list(objs.map(_.id)))
}
class StringQueryField[M <: MongoRecord[M]](val field: Field[String, M]) {
def startsWith(s: String) =
- new EqClause(field.name, Pattern.compile("^" + Pattern.quote(s)))
+ new EqClause[Pattern, PartialIndexScan](field.name, PartialIndexScan, Pattern.compile("^" + Pattern.quote(s)))
- def matches(p: Pattern): EqClause[Pattern] =
- new EqClause(field.name, p) {
- override lazy val actualIndexBehavior = IndexBehavior.DocumentScan
- }
- def matches(r: Regex): EqClause[Pattern] = matches(r.pattern)
- def regexWarningNotIndexed(p: Pattern) = new EqClause(field.name, p)
+ def matches(p: Pattern): EqClause[Pattern, DocumentScan] =
+ new EqClause[Pattern, DocumentScan](field.name, DocumentScan, p)
+
+ def matches(r: Regex): EqClause[Pattern, DocumentScan] =
+ matches(r.pattern)
+
+ def regexWarningNotIndexed(p: Pattern) =
+ new EqClause[Pattern, DocumentScan](field.name, DocumentScan, p)
}
class CaseClassQueryField[V, M <: MongoRecord[M]](val field: MongoCaseClassField[M, V]) {
@@ -248,16 +234,16 @@ abstract class AbstractListQueryField[V, DB, M <: MongoRecord[M]](val field: Fie
QueryHelpers.inListClause(field.name, valuesToDB(vs))
def nin(vs: Traversable[V]) =
- new QueryClause(field.name, CondOps.Nin -> QueryHelpers.list(valuesToDB(vs)))
+ new NinQueryClause(field.name, QueryHelpers.list(valuesToDB(vs)))
def size(s: Int) =
- new QueryClause(field.name, CondOps.Size -> s)
+ new SizeQueryClause(field.name, s)
def contains(v: V) =
- new EqClause(field.name, valueToDB(v))
+ EqClause(field.name, valueToDB(v))
def notcontains(v: V) =
- new QueryClause(field.name, CondOps.Ne -> valueToDB(v))
+ new NeQueryClause(field.name, valueToDB(v))
def at(i: Int): DummyField[V, M] =
new DummyField[V, M](field.owner, field.name + "." + i.toString)
View
4 src/main/scala/com/foursquare/rogue/QueryHelpers.scala
@@ -111,14 +111,14 @@ object QueryHelpers {
if (vs.isEmpty)
new EmptyQueryClause[java.util.List[V]](fieldName)
else
- new QueryClause(fieldName, CondOps.In -> QueryHelpers.list(vs))
+ new InQueryClause(fieldName, QueryHelpers.list(vs))
}
def allListClause[V](fieldName: String, vs: Traversable[V]) = {
if (vs.isEmpty)
new EmptyQueryClause[java.util.List[V]](fieldName)
else
- new QueryClause(fieldName, CondOps.All -> QueryHelpers.list(vs))
+ new AllQueryClause(fieldName, QueryHelpers.list(vs))
}
def asDBObject[T](x: T): DBObject = {
View
2 src/main/scala/com/foursquare/rogue/Rogue.scala
@@ -61,6 +61,8 @@ trait Rogue {
implicit def metaRecordToIndexBuilder[M <: MongoRecord[M]](rec: M with MongoMetaRecord[M]): IndexBuilder[M] =
IndexBuilder(rec)
+ implicit def metaRecordToIndexEnforcer[M <: MongoRecord[M]](meta: M with MongoMetaRecord[M] with IndexedRecord[M]): IndexEnforcerBuilder[M] =
+ new IndexEnforcerBuilder(meta)
/* A couple of implicit conversions that take a query builder, and convert it to a modify. This allows
View
86 src/test/scala/com/foursquare/rogue/QueryTest.scala
@@ -450,6 +450,26 @@ class QueryTest extends SpecsMatchers {
}
@Test
+ def testEnforceIndexes {
+ metaRecordToIndexEnforcer(Venue).useIndex(Venue.idIdx)
+ .where(_._id)(_ eqs new ObjectId("4eb3aeee31dafb11203d4984"))
+ .scan(_.legacyid eqs 4)
+ .toString() must_== """db.venues.find({ "_id" : { "$oid" : "4eb3aeee31dafb11203d4984"} , "legid" : 4})"""
+
+ Venue.useIndex(Venue.mayorIdIdx)
+ .where(_.mayor)(_ in List(2097L))
+ .and(_._id)(_ eqs new ObjectId("4eb3aeee31dafb11203d4984"))
+ .scan(_.legacyid eqs 4)
+ .toString() must_== """db.venues.find({ "mayor" : { "$in" : [ 2097]} , "_id" : { "$oid" : "4eb3aeee31dafb11203d4984"} , "legid" : 4})"""
+
+ Venue.useIndex(Venue.mayorIdClosedIdx)
+ .where(_.mayor)(_ eqs 2097)
+ .rangeScan(_._id)
+ .iscan(_.closed)(_ eqs true)
+ .toString() must_== """db.venues.find({ "mayor" : 2097 , "closed" : true})"""
+ }
+
+ @Test
def testWhereOpt {
val someId = Some(1L)
val noId: Option[Long] = None
@@ -521,13 +541,16 @@ class QueryTest extends SpecsMatchers {
@Test
def thingsThatShouldntCompile {
val compiler = new Compiler
- def check(code: String, shouldTypeCheck: Boolean = false): Unit = {
- compiler.typeCheck(code) aka "'%s' compiles!".format(code) must_== shouldTypeCheck
+ def check(code: String, expectedErrorREOpt: Option[String] = Some("")): Unit = {
+ (expectedErrorREOpt, compiler.typeCheck(code)) aka "'%s' compiles!".format(code) must beLike {
+ case (Some(expectedErrorRE), Some(actualError)) => expectedErrorRE.r.findFirstIn(actualError.replaceAll("\n", "")).isDefined
+ case (None, None) => true
+ }
}
// For sanity
// Venue where (_.legacyid eqs 3)
- check("""Venue where (_.legacyid eqs 3)""", shouldTypeCheck = true)
+ check("""Venue where (_.legacyid eqs 3)""", None)
// Basic operator and operand type matching
check("""Venue where (_.legacyid eqs "hi")""")
@@ -552,10 +575,10 @@ class QueryTest extends SpecsMatchers {
// Foreign keys
// first make sure that each type-safe foreign key works as expected
- check("""VenueClaim where (_.venueid eqs Venue.createRecord)""", true)
- check("""VenueClaim where (_.venueid neqs Venue.createRecord)""", true)
- check("""VenueClaim where (_.venueid in List(Venue.createRecord))""", true)
- check("""VenueClaim where (_.venueid nin List(Venue.createRecord))""", true)
+ check("""VenueClaim where (_.venueid eqs Venue.createRecord)""", None)
+ check("""VenueClaim where (_.venueid neqs Venue.createRecord)""", None)
+ check("""VenueClaim where (_.venueid in List(Venue.createRecord))""", None)
+ check("""VenueClaim where (_.venueid nin List(Venue.createRecord))""", None)
// now check that they reject invalid args
check("""VenueClaim where (_.venueid eqs Tip.createRecord)""")
check("""VenueClaim where (_.venueid neqs Tip.createRecord)""")
@@ -609,6 +632,40 @@ class QueryTest extends SpecsMatchers {
check("""Venue or (_ where (_.legacyid eqs 1), _ where (_.legacyid eqs 2) limit 10)""")
check("""Venue or (_ where (_.legacyid eqs 1), _ where (_.legacyid eqs 2) skip 10)""")
check("""OrQuery(Venue.where(_.legacyid eqs 1), Tip.where(_.legacyid eqs 2))""")
+
+ // Indexes
+
+ // Can't say useIndex and then not use that field.
+ check("""Venue.useIndex(Venue.idIdx).where(_.legacyid eqs 4)""",
+ Some("found.*EqClause.*required.*Venue#_id"))
+ // Can't use where with an IndexScan'ing operation.
+ check("""Venue.useIndex(Venue.idIdx).where(_._id)(_ after new DateTime())""",
+ Some("do not conform to method where.*com.foursquare.rogue.Indexable"))
+ // But you can use iscan with an IndexScan'ing operation.
+ check("""Venue.useIndex(Venue.idIdx).iscan(_._id)(_ after new DateTime())""",
+ None)
+
+ // Can't skip past the first field in an index.
+ check("""Venue.useIndex(Venue.idIdx).rangeScan(_._id)""",
+ Some("could not find implicit value for parameter ev.*com.foursquare.rogue.UsedIndex"))
+
+ // Can't skip past the first field in an index.
+ check("""Venue.useIndex(Venue.mayorIdIdx).rangeScan(_.mayor).iscan(_._id)(_ eqs new ObjectId())""",
+ Some("could not find implicit value for parameter ev.*com.foursquare.rogue.UsedIndex"))
+
+ // If first column is index-scanned, other fields must be marked as iscan too.
+ check("""Venue.useIndex(Venue.mayorIdIdx).iscan(_.mayor)(_ lt 10).where(_._id)(_ eqs new ObjectId())""",
+ Some("could not find implicit value for parameter ev.*com.foursquare.rogue.Indexable"))
+ // Query should compile fine when the second clause is marked as iscan.
+ check("""Venue.useIndex(Venue.mayorIdIdx).iscan(_.mayor)(_ lt 10).iscan(_._id)(_ eqs new ObjectId())""",
+ None)
+
+ // If you rangeScan past a column, you must iscan all index fields after.
+ check("""Venue.useIndex(Venue.mayorIdClosedIdx).where(_.mayor)(_ eqs 10).rangeScan(_._id).where(_.closed)(_ eqs true)""",
+ Some("could not find implicit value for parameter ev.*com.foursquare.rogue.Indexable"))
+ // Version of the above with an iscan of later fields.
+ check("""Venue.useIndex(Venue.mayorIdClosedIdx).where(_.mayor)(_ eqs 10).rangeScan(_._id).iscan(_.closed)(_ eqs true)""",
+ None)
}
class Compiler {
@@ -630,6 +687,7 @@ class QueryTest extends SpecsMatchers {
settings.unchecked.value = true // enable detailed unchecked warnings
// This is deprecated in 2.9.x, but we need to use it for compatibility with 2.8.x
+ val stringWriter = new java.io.StringWriter()
private val interpreter =
new Interpreter(
settings,
@@ -637,20 +695,20 @@ class QueryTest extends SpecsMatchers {
* It's a good idea to comment out this second parameter when adding or modifying
* tests that shouldn't compile, to make sure that the tests don't compile for the
* right reason.
- *
- * TODO(jorge): Consider comparing string output for each test to ensure the
- * actual compile error matches the expected compile error.
**/
- new PrintWriter(new NullWriter()))
+ new PrintWriter(stringWriter))
interpreter.interpret("""import com.foursquare.rogue._""")
interpreter.interpret("""import com.foursquare.rogue.Rogue._""")
+ interpreter.interpret("""import org.bson.types.ObjectId""")
+ interpreter.interpret("""import org.joda.time.DateTime""")
- def typeCheck(code: String): Boolean = {
+ def typeCheck(code: String): Option[String] = {
+ stringWriter.getBuffer.delete(0, stringWriter.getBuffer.length)
val thunked = "() => { %s }".format(code)
interpreter.interpret(thunked) match {
- case InterpreterResults.Success => true
- case InterpreterResults.Error => false
+ case InterpreterResults.Success => None
+ case InterpreterResults.Error => Some(stringWriter.toString)
case InterpreterResults.Incomplete => throw new Exception("Incomplete code snippet")
}
}
View
5 src/test/scala/com/foursquare/rogue/TestModels.scala
@@ -38,7 +38,7 @@ object VenueStatus extends Enumeration {
val closed = Value("Closed")
}
-class Venue extends MongoRecord[Venue] with MongoId[Venue] {
+class Venue extends MongoRecord[Venue] with MongoId[Venue] with IndexedRecord[Venue] {
def meta = Venue
object legacyid extends LongField(this) { override def name = "legid" }
object userid extends LongField(this)
@@ -61,9 +61,12 @@ object Venue extends Venue with MongoMetaRecord[Venue] {
object CustomIndex extends IndexModifier("custom")
val idIdx = Venue.index(_._id, Asc)
+ val mayorIdIdx = Venue.index(_.mayor, Asc, _._id, Asc)
+ val mayorIdClosedIdx = Venue.index(_.mayor, Asc, _._id, Asc, _.closed, Asc)
val legIdx = Venue.index(_.legacyid, Desc)
val geoIdx = Venue.index(_.geolatlng, TwoD)
val geoCustomIdx = Venue.index(_.geolatlng, CustomIndex, _.tags, Asc)
+ override val mongoIndexList = List(idIdx, mayorIdIdx, mayorIdClosedIdx, legIdx, geoIdx, geoCustomIdx)
trait FK[T <: FK[T]] extends MongoRecord[T] {
self: T=>

0 comments on commit 7ecfb9a

Please sign in to comment.
Something went wrong with that request. Please try again.