Last active
August 29, 2015 14:06
-
-
Save gabro/5f88aa672bebc25ee3d7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package buildo | |
import anorm._ | |
import shapeless._ | |
import shapeless.ops.hlist._ | |
import shapeless.record._ | |
import shapeless.syntax.std.traversable._ | |
import shapeless.syntax.singleton._ | |
/** | |
* A module for convenient SQL querying and parsing | |
* | |
* It allows for: | |
* - select queries on all the columns over a single table | |
* - optional where queries, featuring only comparison | |
* | |
* The query results are packed into extensible-records | |
* which can be accessed using the Symbol name of the column | |
* as key | |
* | |
* Example: | |
* | |
* r('FirstName) | |
* // returns the value of the column "FirstName" if present | |
* // in the query, or a compile-time error otherwise | |
* | |
* The SQL querying is performed using anorm | |
*/ | |
trait QueryParserModule { | |
/** Utility class used to hold a column name and its parser */ | |
sealed trait ColParser[T, K <: Symbol] { | |
val column: Witness.Aux[K] | |
} | |
/** Trick for simulating partial type parameter inference | |
* We want to specify T, but infer K, so they must be separated into two | |
* parameter lists | |
* See: | |
* http://stackoverflow.com/questions/10726222/simulate-partial-type-parameter-inference-with-implicits | |
*/ | |
class NullableC[T] { | |
def apply[K <: Symbol](column: Witness.Aux[K])(implicit ev: Column[T]) = | |
Nullable[T, K](column) | |
} | |
def nullable[T] = new NullableC[T] | |
class NonNullableC[T] { | |
def apply[K <: Symbol](column: Witness.Aux[K])(implicit ev: Column[T]) = | |
NonNullable[T, K](column) | |
} | |
def nonNullable[T] = new NonNullableC[T] | |
case class Nullable[T: Column, K <: Symbol](column: Witness.Aux[K]) | |
extends ColParser[T, K] | |
case class NonNullable[T: Column, K <: Symbol](column: Witness.Aux[K]) | |
extends ColParser[T, K] | |
type PV = ParameterValue | |
/** Available SQL where clauses */ | |
sealed trait WhereOP[+T] { val s: Symbol; val p: T } | |
sealed trait WhereOP1 extends WhereOP[PV] | |
object WhereOP1 { def unapply(wop: WhereOP1) = Some((wop.s, wop.p)) } | |
sealed trait WhereOP2 extends WhereOP[(PV, PV)] | |
object WhereOP2 { def unapply(wop: WhereOP2) = Some((wop.s, wop.p)) } | |
case class WhereEQ(s: Symbol, p: PV) extends WhereOP1 | |
case class WhereNEQ(s: Symbol, p: PV) extends WhereOP1 | |
case class WhereGT(s: Symbol, p: PV) extends WhereOP1 | |
case class WhereGTE(s: Symbol, p: PV) extends WhereOP1 | |
case class WhereLT(s: Symbol, p: PV) extends WhereOP1 | |
case class WhereLTE(s: Symbol, p: PV) extends WhereOP1 | |
case class WhereBTW(s: Symbol, p: (PV, PV)) extends WhereOP2 | |
/** DSL for expressing SQL where clauses */ | |
implicit class WhereComparable(s: Symbol) { | |
def <->(p: PV) = WhereEQ(s, p) | |
def >(p: PV) = WhereGT(s, p) | |
def >=(p: PV) = WhereGTE(s, p) | |
def <(p: PV) = WhereLT(s, p) | |
def <=(p: PV) = WhereLTE(s, p) | |
def between(t: (PV, PV)) = WhereBTW(s, t) | |
} | |
/** Syntactic sugar for converting a Tuple2 to a WhereEQ */ | |
implicit def tupleToWhereEQ[T](t: (Symbol, T))( | |
implicit ev: T => PV | |
) = WhereEQ(t._1, t._2) | |
/** The main class which holds the table name and the list of columns | |
* along with their types | |
*/ | |
class QueryParser[L <: HList](table: Symbol, columns: L) { | |
type NameExtractor = extractNames.type | |
type ToRecordFolder = toRecord.type | |
/** A select query of all the columns on the specified table */ | |
final def query( | |
implicit mapper: Mapper[NameExtractor, L] | |
) = SQL(selectQuery) | |
/** A select and where query of all the columns on the specified table */ | |
final def query[T](where: WhereOP[T]*)( | |
implicit mapper: Mapper[NameExtractor, L] | |
) = { | |
val whereClauses = where.map { | |
case WhereEQ(s, _) => s"${s.name} = {${s.name}}" // "DeviceCode = {DeviceCode}" | |
case WhereNEQ(s, _) => s"${s.name} <> {${s.name}}" // "DeviceCode <> {DeviceCode}" | |
case WhereGT(s, _) => s"${s.name} > {${s.name}}" // "DeviceCode > {DeviceCode}" | |
case WhereGTE(s, _) => s"${s.name} >= {${s.name}}" // "DeviceCode >= {DeviceCode}" | |
case WhereLT(s, _) => s"${s.name} < {${s.name}}" // "DeviceCode < {DeviceCode}" | |
case WhereLTE(s, _) => s"${s.name} <= {${s.name}}" // "DeviceCode <= {DeviceCode}" | |
case WhereBTW(s, _) => s"${s.name} BETWEEN {${s.name}_from} AND {${s.name}_to}" | |
}.mkString(" AND ") | |
SQL(s"$selectQuery WHERE $whereClauses") | |
.on(where.foldLeft(List[NamedParameter]()) { | |
case (acc, WhereOP1(s, c)) => NamedParameter.symbol((s, c)) :: acc | |
case (acc, WhereBTW(s, (from, to))) => | |
NamedParameter(s"${s.name}_from", from) :: | |
NamedParameter(s"${s.name}_to", to) :: acc | |
}: _*) | |
} | |
final def executeQuery[O <: HList](q: SimpleSql[Row])(implicit | |
conn: java.sql.Connection, | |
folder: RightFolder.Aux[L, (Row, HNil.type), ToRecordFolder, (Row, O)] | |
): Stream[O] = q.apply.map { row => | |
columns.foldRight((row, HNil))(toRecord)._2 | |
} | |
/** Extracts the column name from a ColParser[T] */ | |
object extractNames extends Poly1 { | |
// in order for this to work for any subclasses of ColParser, there must be | |
// implicit evidence that C is a subclass of ColParser | |
// see: http://stackoverflow.com/q/16701159/846273 | |
implicit def caseColParser[C, T, K <: Symbol](implicit ev: C <:< ColParser[T, K]) = | |
at[C](_.column.value.name) | |
} | |
/** Transforms a Row into an extensible record | |
* This is a Poly2, since it needs to carry the 'row' around | |
* for fetching the column name to be used as record field key | |
* see: http://stackoverflow.com/q/25288806/846273 | |
*/ | |
object toRecord extends Poly2 { | |
implicit def caseNonNullable[T: Column, K <: Symbol, A <: HList] = | |
at[NonNullable[T, K], (Row, A)] { | |
case (x, (row, rec)) => (row, field[K](row[T](x.column.value.name)) :: rec) | |
} | |
implicit def caseNullable[T: Column, K <: Symbol, A <: HList] = | |
at[Nullable[T, K], (Row, A)] { | |
case (x, (row, rec)) => (row, field[K](row[Option[T]](x.column.value.name)) :: rec) | |
} | |
} | |
// create a vanilla select query string on the specified table | |
private def selectQuery( | |
implicit mapper: Mapper[NameExtractor, L] | |
) = s"SELECT ${columns.map(extractNames).toList.mkString(", ")} FROM ${table.name}" | |
} | |
object QueryParser { | |
def apply[P <: Product, L <: HList](table: Symbol)(columns: P)( | |
implicit gen: Generic.Aux[P, L]) = new QueryParser(table, gen.to(columns)) | |
} | |
implicit class TableSymbol(table: Symbol) { | |
/** syntactic sugar for passing HList elements as an argument list, using | |
* Generic and auto-tupling | |
* see: http://stackoverflow.com/a/25475401/846273 | |
*/ | |
def describe[P <: Product, L <: HList](columns: P)( | |
implicit gen: Generic.Aux[P, L]) = new QueryParser(table, gen.to(columns)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment