Skip to content

Instantly share code, notes, and snippets.

@gabro
Last active August 29, 2015 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gabro/5f88aa672bebc25ee3d7 to your computer and use it in GitHub Desktop.
Save gabro/5f88aa672bebc25ee3d7 to your computer and use it in GitHub Desktop.
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