Skip to content

Commit

Permalink
Add seek (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
oyvindberg authored May 15, 2024
1 parent 03aaac8 commit a261a65
Show file tree
Hide file tree
Showing 27 changed files with 742 additions and 133 deletions.
14 changes: 12 additions & 2 deletions typo-dsl-anorm/src/scala/typo/dsl/SelectBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,18 @@ trait SelectBuilder[Fields[_], Row] {
* .orderBy { case ((_, _), productModel) => productModel(_.name).desc.withNullsFirst }
* }}}
*/
final def orderBy(v: Fields[Hidden] => SortOrder[?, Hidden]): SelectBuilder[Fields, Row] =
withParams(params.orderBy(v.asInstanceOf[Fields[Row] => SortOrder[?, Row]]))
final def orderBy[T, N[_]](v: Fields[Hidden] => SortOrder[T, N, Hidden]): SelectBuilder[Fields, Row] =
withParams(params.orderBy(v.asInstanceOf[Fields[Row] => SortOrderNoHkt[?, Row]]))

final def seek[T, N[_]](v: Fields[Row] => SortOrder[T, N, Row])(value: SqlExpr.Const[T, N, Row]): SelectBuilder[Fields, Row] =
withParams(params.seek(SelectParams.Seek[Fields, Row, T, N](v, value)))

final def maybeSeek[T, N[_]](v: Fields[Row] => SortOrder[T, N, Row])(maybeValue: Option[SqlExpr.Const[T, N, Row]]): SelectBuilder[Fields, Row] =
maybeValue match {
case Some(value) => seek(v)(value)
case None => orderBy(v.asInstanceOf[Fields[Hidden] => SortOrder[T, N, Hidden]])
}

final def offset(v: Int): SelectBuilder[Fields, Row] =
withParams(params.offset(v))
final def limit(v: Int): SelectBuilder[Fields, Row] =
Expand Down
16 changes: 15 additions & 1 deletion typo-dsl-anorm/src/scala/typo/dsl/SelectBuilderMock.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package typo.dsl

import java.sql.Connection
import typo.dsl.internal.seeks
import typo.dsl.internal.mocks.RowOrdering

case class SelectBuilderMock[Fields[_], Row](
structure: Structure[Fields, Row],
Expand All @@ -11,7 +13,7 @@ case class SelectBuilderMock[Fields[_], Row](
copy(params = sqlParams)

override def toList(implicit c: Connection): List[Row] =
SelectParams.applyParams(structure.fields, all(), params)
SelectBuilderMock.applyParams(structure.fields, all(), params)

override def joinOn[Fields2[_], N[_]: Nullability, Row2](
other: SelectBuilder[Fields2, Row2]
Expand Down Expand Up @@ -55,3 +57,15 @@ case class SelectBuilderMock[Fields[_], Row](

override def sql: Option[Fragment] = None
}

object SelectBuilderMock {
def applyParams[Fields[_], Row](fields: Fields[Row], rows: List[Row], params: SelectParams[Fields, Row]): List[Row] = {
val (filters, orderBys) = seeks.expand(fields, params)
implicit val rowOrdering: Ordering[Row] = new RowOrdering(orderBys)
rows
.filter(row => filters.forall(_.eval(row).getOrElse(false)))
.sorted
.drop(params.offset.getOrElse(0))
.take(params.limit.getOrElse(Int.MaxValue))
}
}
50 changes: 20 additions & 30 deletions typo-dsl-anorm/src/scala/typo/dsl/SelectParams.scala
Original file line number Diff line number Diff line change
@@ -1,35 +1,48 @@
package typo.dsl

import typo.dsl.Fragment.FragmentStringInterpolator
import typo.dsl.internal.seeks

import java.util.concurrent.atomic.AtomicInteger

case class SelectParams[Fields[_], Row](
final case class SelectParams[Fields[_], Row](
where: List[Fields[Row] => SqlExpr[Boolean, Option, Row]],
orderBy: List[Fields[Row] => SortOrder[?, Row]],
orderBy: List[Fields[Row] => SortOrderNoHkt[?, Row]],
seeks: List[SelectParams.SeekNoHkt[Fields, Row, ?]],
offset: Option[Int],
limit: Option[Int]
) {
def where(v: Fields[Row] => SqlExpr[Boolean, Option, Row]): SelectParams[Fields, Row] = copy(where = where :+ v)
def orderBy(v: Fields[Row] => SortOrder[?, Row]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ v)
def orderBy(v: Fields[Row] => SortOrderNoHkt[?, Row]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ v)
def seek(v: SelectParams.SeekNoHkt[Fields, Row, ?]): SelectParams[Fields, Row] = copy(seeks = seeks :+ v)
def offset(v: Int): SelectParams[Fields, Row] = copy(offset = Some(v))
def limit(v: Int): SelectParams[Fields, Row] = copy(limit = Some(v))
}

object SelectParams {
sealed trait SeekNoHkt[Fields[_], Row, NT] {
val f: Fields[Row] => SortOrderNoHkt[NT, Row]
}

case class Seek[Fields[_], Row, T, N[_]](
f: Fields[Row] => SortOrder[T, N, Row],
value: SqlExpr.Const[T, N, Row]
) extends SeekNoHkt[Fields, Row, N[T]]

def empty[Fields[_], Row]: SelectParams[Fields, Row] =
SelectParams[Fields, Row](List.empty, List.empty, None, None)
SelectParams[Fields, Row](List.empty, List.empty, List.empty, None, None)

def render[Row, Fields[_]](fields: Fields[Row], baseSql: Fragment, counter: AtomicInteger, params: SelectParams[Fields, Row]): Fragment = {
val (filters, orderBys) = seeks.expand(fields, params)

val maybeEnd: Option[Fragment] =
List[Option[Fragment]](
params.where.map(w => w(fields)).reduceLeftOption(_.and(_)).map { where =>
filters.reduceLeftOption(_.and(_)).map { where =>
Fragment(" where ") ++ where.render(counter)
},
params.orderBy match {
orderBys match {
case Nil => None
case nonEmpty => Some(frag" order by ${nonEmpty.map(x => x(fields).render(counter)).mkFragment(", ")}")
case nonEmpty => Some(frag" order by ${nonEmpty.map(x => x.render(counter)).mkFragment(", ")}")
},
params.offset.map(value => Fragment(" offset " + value)),
params.limit.map { value => Fragment(" limit " + value) }
Expand All @@ -44,27 +57,4 @@ object SelectParams {

completeSql
}

def applyParams[Fields[_], Row](fields: Fields[Row], rows: List[Row], params: SelectParams[Fields, Row]): List[Row] = {
// precompute filters and order bys for this row structure
val filters: List[SqlExpr[Boolean, Option, Row]] =
params.where.map(f => f(fields))
val orderBys: List[SortOrder[?, Row]] =
params.orderBy.map(f => f(fields))

rows
.filter(row => filters.forall(_.eval(row).getOrElse(false)))
.sorted((row1: Row, row2: Row) =>
orderBys.foldLeft(0) {
case (acc, so: SortOrder[t, Row]) if acc == 0 =>
val t1: t = so.expr.eval(row1)
val t2: t = so.expr.eval(row2)
val ordering: Ordering[t] = if (so.ascending) so.ordering else so.ordering.reverse
ordering.compare(t1, t2)
case (acc, _) => acc
}
)
.drop(params.offset.getOrElse(0))
.take(params.limit.getOrElse(Int.MaxValue))
}
}
19 changes: 15 additions & 4 deletions typo-dsl-anorm/src/scala/typo/dsl/SortOrder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,24 @@ import typo.dsl.Fragment.FragmentStringInterpolator

import java.util.concurrent.atomic.AtomicInteger

// sort by a field
case class SortOrder[NT, R](expr: SqlExpr.SqlExprNoHkt[NT, R], ascending: Boolean, nullsFirst: Boolean)(implicit val ordering: Ordering[NT]) {
def withNullsFirst: SortOrder[NT, R] = copy(nullsFirst = true)(ordering)
def render(counter: AtomicInteger): Fragment =
sealed trait SortOrderNoHkt[NT, R] {
val expr: SqlExpr.SqlExprNoHkt[NT, R]
val ascending: Boolean
val nullsFirst: Boolean

def withNullsFirst: SortOrderNoHkt[NT, R]

final def render(counter: AtomicInteger): Fragment = {
List[Fragment](
expr.render(counter),
if (ascending) frag"ASC" else frag"DESC",
if (nullsFirst) frag"NULLS FIRST" else Fragment.empty
).mkFragment(" ")
}
}

// sort by a field
final case class SortOrder[T, N[_], R](expr: SqlExpr[T, N, R], ascending: Boolean, nullsFirst: Boolean)(implicit val ordering: Ordering[T], val nullability: Nullability[N])
extends SortOrderNoHkt[N[T], R] {
def withNullsFirst: SortOrder[T, N, R] = copy(nullsFirst = true)(ordering, nullability)
}
17 changes: 11 additions & 6 deletions typo-dsl-anorm/src/scala/typo/dsl/SqlExpr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ object SqlExpr {
override def eval(row: R): N[O] =
N.mapN(left.eval(row), right.eval(row))(op.eval)
override def render(counter: AtomicInteger): Fragment =
frag"${left.render(counter)} ${Fragment(op.name)} ${right.render(counter)}"
frag"(${left.render(counter)} ${Fragment(op.name)} ${right.render(counter)})"
}

case class Underlying[T, TT, N[_], R](expr: SqlExpr[T, N, R], bijection: Bijection[T, TT], N: Nullability[N]) extends SqlExpr[TT, N, R] {
Expand Down Expand Up @@ -238,16 +238,21 @@ object SqlExpr {
}

// automatically put values in a constant expression
implicit def asConstOpt[T, R](t: Option[T])(implicit T: ToParameterValue[Option[T]], P: ParameterMetaData[T]): SqlExpr[T, Option, R] =
implicit def asConstOpt[T, R](t: Option[T])(implicit T: ToParameterValue[Option[T]], P: ParameterMetaData[T]): SqlExpr.Const[T, Option, R] =
Const(t, T, P)

implicit def asConstRequired[T, R](t: T)(implicit T: ToParameterValue[T], P: ParameterMetaData[T]): SqlExpr[T, Required, R] =
implicit def asConstRequired[T, R](t: T)(implicit T: ToParameterValue[T], P: ParameterMetaData[T]): SqlExpr.Const[T, Required, R] =
Const[T, Required, R](t, T, P)

// some syntax to construct field sort order
implicit class SqlExprSortSyntax[NT, R](private val expr: SqlExprNoHkt[NT, R]) extends AnyVal {
def asc(implicit O: Ordering[NT]): SortOrder[NT, R] = SortOrder(expr, ascending = true, nullsFirst = false)
def desc(implicit O: Ordering[NT]): SortOrder[NT, R] = SortOrder(expr, ascending = false, nullsFirst = false)
implicit class SqlExprSortSyntax[T, N[_], R](private val expr: SqlExpr[T, N, R]) extends AnyVal {
def asc(implicit O: Ordering[T], N: Nullability[N]): SortOrder[T, N, R] = SortOrder(expr, ascending = true, nullsFirst = false)
def desc(implicit O: Ordering[T], N: Nullability[N]): SortOrder[T, N, R] = SortOrder(expr, ascending = false, nullsFirst = false)
}

final case class RowExpr[R](exprs: List[SqlExpr.SqlExprNoHkt[?, R]]) extends SqlExpr[List[?], Required, R] {
override def eval(row: R): List[?] = exprs.map(_.eval(row))
override def render(counter: AtomicInteger): Fragment = frag"(" ++ exprs.map(_.render(counter)).mkFragment(",") ++ frag")"
}

implicit class SqlExprArraySyntax[T, N[_], R](private val expr: SqlExpr[Array[T], N, R]) extends AnyVal {
Expand Down
13 changes: 11 additions & 2 deletions typo-dsl-doobie/src/scala/typo/dsl/SelectBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,17 @@ trait SelectBuilder[Fields[_], Row] {
* .orderBy { case ((_, _), productModel) => productModel(_.name).desc.withNullsFirst }
* }}}
*/
final def orderBy(v: Fields[Hidden] => SortOrder[?, Hidden]): SelectBuilder[Fields, Row] =
withParams(params.orderBy(v.asInstanceOf[Fields[Row] => SortOrder[?, Row]]))
final def orderBy[T, N[_]](v: Fields[Hidden] => SortOrder[T, N, Hidden]): SelectBuilder[Fields, Row] =
withParams(params.orderBy(v.asInstanceOf[Fields[Row] => SortOrderNoHkt[?, Row]]))

final def seek[T, N[_]](v: Fields[Row] => SortOrder[T, N, Row])(value: SqlExpr.Const[T, N, Row]): SelectBuilder[Fields, Row] =
withParams(params.seek(SelectParams.Seek[Fields, Row, T, N](v, value)))

final def maybeSeek[T, N[_]](v: Fields[Row] => SortOrder[T, N, Row])(maybeValue: Option[SqlExpr.Const[T, N, Row]]): SelectBuilder[Fields, Row] =
maybeValue match {
case Some(value) => seek(v)(value)
case None => orderBy(v.asInstanceOf[Fields[Hidden] => SortOrder[T, N, Hidden]])
}
final def offset(v: Int): SelectBuilder[Fields, Row] =
withParams(params.offset(v))
final def limit(v: Int): SelectBuilder[Fields, Row] =
Expand Down
16 changes: 15 additions & 1 deletion typo-dsl-doobie/src/scala/typo/dsl/SelectBuilderMock.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package typo.dsl

import doobie.free.connection.ConnectionIO
import doobie.util.fragment.Fragment
import typo.dsl.internal.mocks.RowOrdering
import typo.dsl.internal.seeks

case class SelectBuilderMock[Fields[_], Row](
structure: Structure[Fields, Row],
Expand All @@ -12,7 +14,7 @@ case class SelectBuilderMock[Fields[_], Row](
copy(params = sqlParams)

override def toList: ConnectionIO[List[Row]] =
all.map(all => SelectParams.applyParams(structure.fields, all, params))
all.map(all => SelectBuilderMock.applyParams(structure.fields, all, params))

override def joinOn[Fields2[_], N[_]: Nullability, Row2](
other: SelectBuilder[Fields2, Row2]
Expand Down Expand Up @@ -63,3 +65,15 @@ case class SelectBuilderMock[Fields[_], Row](

override def sql: Option[Fragment] = None
}

object SelectBuilderMock {
def applyParams[Fields[_], Row](fields: Fields[Row], rows: List[Row], params: SelectParams[Fields, Row]): List[Row] = {
val (filters, orderBys) = seeks.expand(fields, params)
implicit val rowOrdering: Ordering[Row] = new RowOrdering(orderBys)
rows
.filter(row => filters.forall(_.eval(row).getOrElse(false)))
.sorted
.drop(params.offset.getOrElse(0))
.take(params.limit.getOrElse(Int.MaxValue))
}
}
49 changes: 20 additions & 29 deletions typo-dsl-doobie/src/scala/typo/dsl/SelectParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,46 @@ import cats.data.NonEmptyList
import doobie.implicits.toSqlInterpolator
import doobie.util.fragment.Fragment
import doobie.util.fragments
import typo.dsl.internal.seeks

import java.util.concurrent.atomic.AtomicInteger

case class SelectParams[Fields[_], Row](
final case class SelectParams[Fields[_], Row](
where: List[Fields[Row] => SqlExpr[Boolean, Option, Row]],
orderBy: List[Fields[Row] => SortOrder[?, Row]],
orderBy: List[Fields[Row] => SortOrderNoHkt[?, Row]],
seeks: List[SelectParams.SeekNoHkt[Fields, Row, ?]],
offset: Option[Int],
limit: Option[Int]
) {
def where(v: Fields[Row] => SqlExpr[Boolean, Option, Row]): SelectParams[Fields, Row] = copy(where = where :+ v)
def orderBy(v: Fields[Row] => SortOrder[?, Row]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ v)
def orderBy(v: Fields[Row] => SortOrderNoHkt[?, Row]): SelectParams[Fields, Row] = copy(orderBy = orderBy :+ v)
def seek(v: SelectParams.SeekNoHkt[Fields, Row, ?]): SelectParams[Fields, Row] = copy(seeks = seeks :+ v)
def offset(v: Int): SelectParams[Fields, Row] = copy(offset = Some(v))
def limit(v: Int): SelectParams[Fields, Row] = copy(limit = Some(v))
}

object SelectParams {
def empty[Fields[_], Row]: SelectParams[Fields, Row] =
SelectParams[Fields, Row](List.empty, List.empty, None, None)
SelectParams[Fields, Row](List.empty, List.empty, List.empty, None, None)

sealed trait SeekNoHkt[Fields[_], Row, NT] {
val f: Fields[Row] => SortOrderNoHkt[NT, Row]
}

case class Seek[Fields[_], Row, T, N[_]](
f: Fields[Row] => SortOrder[T, N, Row],
value: SqlExpr.Const[T, N, Row]
) extends SeekNoHkt[Fields, Row, N[T]]

def render[Row, Fields[_]](fields: Fields[Row], baseSql: Fragment, counter: AtomicInteger, params: SelectParams[Fields, Row]): Fragment = {
val (filters, orderBys) = seeks.expand(fields, params)

List[Option[Fragment]](
Some(baseSql),
NonEmptyList.fromFoldable(params.where.map(f => f(fields).render(counter))).map(fragments.whereAnd(_)),
NonEmptyList.fromFoldable(params.orderBy.map(f => f(fields).render(counter))).map(fragments.orderBy(_)),
NonEmptyList.fromFoldable(filters.map(f => f.render(counter))).map(fragments.whereAnd(_)),
NonEmptyList.fromFoldable(orderBys.map(f => f.render(counter))).map(fragments.orderBy(_)),
params.offset.map(value => fr"offset $value"),
params.limit.map(value => fr"limit $value")
).flatten.reduce(_ ++ _)
}

def applyParams[Fields[_], Row](fields: Fields[Row], rows: List[Row], params: SelectParams[Fields, Row]): List[Row] = {
// precompute filters and order bys for this row structure
val filters: List[SqlExpr[Boolean, Option, Row]] =
params.where.map(f => f(fields))
val orderBys: List[SortOrder[?, Row]] =
params.orderBy.map(f => f(fields))

rows
.filter(row => filters.forall(_.eval(row).getOrElse(false)))
.sorted((row1: Row, row2: Row) =>
orderBys.foldLeft(0) {
case (acc, so: SortOrder[t, Row]) if acc == 0 =>
val t1: t = so.expr.eval(row1)
val t2: t = so.expr.eval(row2)
val ordering: Ordering[t] = if (so.ascending) so.ordering else so.ordering.reverse
ordering.compare(t1, t2)
case (acc, _) => acc
}
)
.drop(params.offset.getOrElse(0))
.take(params.limit.getOrElse(Int.MaxValue))
}
}
16 changes: 13 additions & 3 deletions typo-dsl-doobie/src/scala/typo/dsl/SortOrder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@ import doobie.util.fragment.Fragment

import java.util.concurrent.atomic.AtomicInteger

// sort by a field
case class SortOrder[NT, R](expr: SqlExpr.SqlExprNoHkt[NT, R], ascending: Boolean, nullsFirst: Boolean)(implicit val ordering: Ordering[NT]) {
def withNullsFirst: SortOrder[NT, R] = copy(nullsFirst = true)(ordering)
sealed trait SortOrderNoHkt[NT, R] {
val expr: SqlExpr.SqlExprNoHkt[NT, R]
val ascending: Boolean
val nullsFirst: Boolean

def withNullsFirst: SortOrderNoHkt[NT, R]

def render(counter: AtomicInteger): Fragment =
List[Fragment](
expr.render(counter),
if (ascending) fr"ASC" else fr"DESC",
if (nullsFirst) fr"NULLS FIRST" else Fragment.empty
).intercalate(fr" ")
}

// sort by a field
final case class SortOrder[T, N[_], R](expr: SqlExpr[T, N, R], ascending: Boolean, nullsFirst: Boolean)(implicit val ordering: Ordering[T], val nullability: Nullability[N])
extends SortOrderNoHkt[N[T], R] {
def withNullsFirst: SortOrder[T, N, R] = copy(nullsFirst = true)(ordering, nullability)
}
Loading

0 comments on commit a261a65

Please sign in to comment.