Skip to content

Commit

Permalink
Updated readme, added some documentation in code, version bump
Browse files Browse the repository at this point in the history
  • Loading branch information
MateuszKubuszok committed Aug 2, 2019
1 parent b787d72 commit eb880c4
Show file tree
Hide file tree
Showing 13 changed files with 116 additions and 160 deletions.
196 changes: 53 additions & 143 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ Doobie queries generated using higher-kinded data.

1. add in your sbt:
```scala
libraryDependencies += "io.scalaland" %% "ocdquery-core" % "0.3.0"
libraryDependencies += "io.scalaland" %% "ocdquery-core" % "0.4.0"
```
(and maybe some optica library like [Quicklens](https://github.com/softwaremill/quicklens)
or [Monocle](https://github.com/julien-truffaut/Monocle))

2. create kigner-kinded data representation:
2. create higner-kinded data representation:

```scala
import java.time.LocalDate
Expand All @@ -32,108 +34,61 @@ final case class TicketF[F[_], C[_]](
3. create a repository:

```scala
import doobie._
import doobie.implicits._
import cats.Id
import com.softwaremill.quicklens._
import doobie.h2.implicits._
import io.scalaland.ocdquery._

// only have to do it once!
object TicketRepo extends Repo(
RepoMeta.forEntity("tickets",
TicketF[ColumnNameF, ColumnNameF](
id = "id",
name = "name",
surname = "surname",
from = "from_",
to = "to",
date = "date"
))
)

// but you can make your life even easier with a bit of default magic
// and lenses (they don't mess up type inference like .copy does)

// (lenses, not provided, pick and add them on your own!)
// (both monocle with macro syntax and quicklenses are nice!)
import com.softwaremill.quicklens._

object TicketRepo extends Repo(
RepoMeta.forEntity("tickets",
DefaultColumnNames.forEntity[TicketF].modify(_.from).setTo("from_")
)
)
val TicketRepo: Repo.EntityRepo[TicketF] = {
// I have no idea why shapeless cannot find this Generic on its own :/
// if you figure it out, please PR!!!
implicit val ticketRead: doobie.Read[Repo.ForEntity[TicketF]#Entity] =
QuasiAuto.read(shapeless.Generic[TicketF[Id, Id]])

Repo.forEntity[TicketF](
"tickets".tableName,
// I suggest using quicklens or monocle's extension methods
// as they are more reliable than .copy
DefaultColumnNames.forEntity[TicketF].modify(_.from).setTo("from_".columnName)
)
}
```

4. generate queries

```scala
// build these in you services with type safety!

TicketRepo.update(
select = TicketF[Selectable, Selectable](
id = Skipped,
name: = Fixed("John"),
surname: = Fixed("Smith"),
from: = Skipped,
to: = Skipped,
date: = Skipped
),
update = TicketF[Selectable, Selectable](
id = Skipped,
name: = Skipped,
surname: = Skipped,
from: = Skipped,
to: = Skipped,
date: = Fixed(LocalDate.now)
)
TicketRepo.insert(
// no need to pass "empty" fields like "id = Unit"!
Create.fromTuple(("John", "Smith", "London", "Paris", LocalDate.now))
).run

TicketRepo.fetch(
select = TicketF[Selectable, Selectable](
id = Skipped,
name: = Skipped,
surname: = Skipped,
from: = Fixed("London"),
to: = Skipped,
date: = Skipped
),
sort = Some("name" -> Sort.Ascending),
limit = Some(5)
).to[List]

// or (with defaults and lenses)

// (lenses, not provided, pick and add them on your own!)
import com.softwaremill.quicklens._
import io.scalaland.ocdquery.sql._ // common filter syntax like `=`, `<>`

TicketRepo.update(
select = TicketRepo.emptySelect
.modify(_.name).setTo("John")
.modify(_.surname).setTo("Smith"),
update = TicketRepo.emptySelect
.modify(_.data).setTo(LocalDate.now)
TicketRepo.update.withFilter { columns =>
(columns.name `=` "John") and (columns.surname `=` "Smith")
}(
TicketRepo.emptyUpdate.modify(_.data).setTo(LocalDate.now)
).run

TicketRepo.fetch(
select = TicketRepo.emptySelect
.modify(_.from).setTo("London"),
sort = Some("name" -> Sort.Ascending),
limit = Some(5)
).to[List]
TicketRepo.fetch.withSort(_.name, Sort.Ascending).withLimit(5) {
_.from `=` "London"
}.to[List]

TicketRepo.delete(_.id `=` deletedId).run
```

5. perform even joins returning tuples of entities:

```scala
import TicketRepo.meta.Names

val joiner = TicketRepo
.join(TicketRepo, (TicketRepo.col(_.id), TicketRepo.col(_.id)))
.join(TicketRepo, (_._2.id, TicketRepo.col(_.id)))

joiner.fetch((filter1, filter2, filter2),
sort = Some(((_: (Names, Names, Names))._1.name) -> Sort.Ascending),
offset = None,
limit = Some(5)).to[List] // ConnectionIO[(Entity, Entity, Entity)]
.join(TicketRepo).on(_._1.id, _._2.id) // after .join() we have a tuple!
.join(TicketRepo).on(_._2.id, _._3.id) // and now triple!
.fetch.withSort(_._1.name, Sort.Ascending).withLimit(5) { columns =>
columns._1.name `=` "John"
}.to[List] // ConnectionIO[(Entity, Entity, Entity)]
```

## How does it exactly works?
Expand Down Expand Up @@ -269,9 +224,9 @@ And we can do this! The idea is called higher-kinded data and looks like this:
import java.time.LocalDate
import java.util.UUID

type Id[A] = A
type UnitF[A] = Unit
type ColumnNameF[A] = String
type Id[A] = A // turns
type UnitF[A] = Unit // make fields not available at creation "disappear"
case class ColumnName[A](name: String) // preserves name of column in DB and its type

// F is for normal columns which should be available in some what for all lifecycle
// C is for these that should be empty during creation and available from then on
Expand All @@ -286,8 +241,8 @@ final case class TicketF[F[_], C[_]](

type TicketCreate = TicketF[Id, UnitF] // C[_] fields are Units, the rest as of type inside of F[_]
type Ticket = TicketF[Id, Id] // all fields are of the type inside of F[_]/C[_]
type TicketFilter = TicketF[Option, Option] // all fields are of Option of inside of F[_]/C[_]
type TicketColumns = TicketF[ColumnNameF, ColumnNameF] // all fields are Strings
type TicketUpdate = TicketF[Option, Option] // all fields are of Option of inside of F[_]/C[_]
type TicketColumns = TicketF[ColumnName, ColumnName] // all fields are column names
```

Higher-kinded data is data with higher-kinded types as type parameters.
Expand All @@ -299,7 +254,7 @@ basic CRUD queries for it.

During implementation some decisions had to be made:

* instead of `Option` we use our own `Selectable` type which could be `Fixed(to)` or `Skipped `
* instead of `Option` we use our own `Updatable` type which could be `UpdatedTo(to)` or `Skip`
to avoid issues during derivation that would occur if you had e.g. `O[Option[String]]`
as one of field types,
* derivation metadata is stored inside `RepoMeta[EntityF]` instance - you can reuse
Expand All @@ -311,66 +266,18 @@ During implementation some decisions had to be made:

val ticketRepoMeta =
RepoMeta.forEntity("tickets",
TicketF[ColumnNameF, ColumnNameF](
id = "id",
name = "name",
surname = "surname",
from = "from_",
to = "to",
date = "date"
))
```
* if however default way suit your taste feel free to use `Repo` implementation!
```scala
object TicketRepo extends Repo(ticketRepoMeta)

val ticketCreate = TicketF[Id, UnitF](
id = (),
name = "John",
surname = "Smith",
from = "New York",
to = "London",
date = LocalDate.now()
)

val byName = TicketF[Selectable, Selectable](
id = Skipped,
name = Fixed(ticketCreate.name),
surname = Fixed(ticketCreate.surname),
from = Skipped,
to = Skipped,
date = Skipped
)

val update = TicketF[Selectable, Selectable](
id = Skipped,
name = Fixed("Jane"),
surname = Skipped,
from = Fixed("London"),
to = Fixed("New York"),
date = Skipped
)
DefaultColumnNames.forEntity[TicketF].modify(_.from).setTo("from_".columnName))

for {
// that's how you can insert data
_ <- TicketRepo.insert(ticketCreate).run
// that's how you can fetch existing data
ticket <- TicketRepo.fetch(byName).unique
// that's how you can update existing data
_ <- TicketRepo.update(byName, update).run
updatedTicket <- TicketRepo.fetch(fetch).unique
// this is how you can delete existing data
_ <- TicketRepo.delete(byName).run
} yield ()
val ticketRepo = new EntityRepo(ticketRepoMeta)
```
* to avoid writing `EntityF[Id, Id]`, `EntityF[Selectable, Selectable]` and `EntityF[Id, UnitF]`
manually, some type aliases were introduced:
```scala
import io.scalaland.ocdquery._

type TicketCreate = TicketRepo.meta.Create
type Ticket = TicketRepo.meta.Entity
type TicketSelect = TicketRepo.meta.Select
type TicketCreate = Repo.ForEntity[TicketF]#EntityCreate
type Ticket = Repo.ForEntity[TicketF]#Entity
type TicketUpdate = Repo.ForEntity[TicketF]#EntityUpdate
```

## Limitations
Expand All @@ -382,4 +289,7 @@ During implementation some decisions had to be made:
define default values like e.g. `None`/`Skipped` for optional fields. So use them
internally, as entities to work with your database and separate them from
entities exposed in your API/published language. You can use [chimney](https://github.com/scalalandio/chimney)
for turning public instances to and from internal instances.
for turning public instances to and from internal instances,
* types sometimes confuse compiler, so while it can derive something like `shapeless.Generic[TicketF[Id, Id]]`,
it has issues finding `Generic.Aux`, so doobie sometimes get's confused - `QuasiAuto` let you provide
the right values explicitly, so that the derivation is not blocked by such silly issue.
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package io.scalaland.ocdquery

import magnolia._
import scala.language.experimental.macros

trait DefaultColumnNames[A] {
import scala.annotation.implicitNotFound
import scala.language.experimental.macros

def get(): A
@implicitNotFound(
"Couldn't find/derive DefaultColumnNames[${Names}]\n" +
" - make sure that all fields are wrapped in obligatory or selectable F[_], " +
"so that ${Names} is made of ColumnNames only"
)
trait DefaultColumnNames[Names] {
def get(): Names
}

object DefaultColumnNames {
Expand All @@ -22,7 +28,7 @@ object DefaultColumnNames {
def combine[T](caseClass: CaseClass[Typeclass, T]): Typeclass[T] =
() =>
caseClass.construct { param =>
ColumnName(param.label)
ColumnName(param.label) // should run it only for classes with only ColumnNames anyway
}

def dispatch[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] = ???
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.scalaland.ocdquery

/*
* Virtually Option, but as a new type, to avoid confusion around
* Option[Option[A]] if you were trying to update optional field.
*/
sealed trait Updatable[+A] extends Product with Serializable {
def toOption: Option[A] = this match {
case UpdateTo(a) => Some(a)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import scala.annotation.implicitNotFound
@implicitNotFound(
"Couldn't find/derive AllColumns[${Names}]\n" +
" - make sure that all fields are wrapped in obligatory or selectable F[_], " +
"so that ${Names} is made of Strings only"
"so that ${Names} is made of ColumnNames only"
)
trait AllColumns[Names] {
def getList(names: Names): List[ColumnName[Any]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import magnolia._
import scala.annotation.implicitNotFound
import scala.language.experimental.macros

@implicitNotFound("")
@implicitNotFound(
"Couldn't find/derive ColumnNameByField[${Names}]\n" +
" - make sure that all fields are wrapped in obligatory or selectable F[_], " +
"so that ${Names} is made of ColumnNames only"
)
trait ColumnNameByField[Names] {
def apply(names: Names): List[(String, ColumnName[Any])]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ package io.scalaland.ocdquery.internal
import doobie.util.fragment.Fragment
import io.scalaland.ocdquery.ColumnName

import scala.annotation.implicitNotFound
import scala.collection.immutable.ListMap

@implicitNotFound(
"Couldn't find/derive ColumnNameFragmentList[${Values}, ${Names}]\n" +
" - make sure that all fields are wrapped in obligatory or selectable F[_], " +
"so that ${Values} is correctly substituted with ColumnName and $Names with ColumnName"
)
trait ColumnNameFragmentList[Values, Names] {
def apply(values: Values, names: Names): List[(ColumnName[Any], Fragment)]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@ import doobie._
import doobie.implicits._
import io.scalaland.ocdquery.{ Skip, Updatable, UpdateTo }
import magnolia.{ CaseClass, Magnolia, SealedTrait }

import scala.annotation.implicitNotFound
import scala.language.experimental.macros

trait FragmentByField[T] {
def apply(t: T): List[(String, Fragment)]
@implicitNotFound(
"Couldn't find/derive FragmentByField[${Values}]\n" +
" - make sure that all fields are wrapped in obligatory or selectable F[_], " +
"so that ${Values} is correctly substituted with Id/UnitF/ColumnName"
)
trait FragmentByField[Values] {
def apply(t: Values): List[(String, Fragment)]
}

object FragmentByField extends FragmentByFieldLowLevelImplicit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ package io.scalaland.ocdquery.internal

import io.scalaland.ocdquery.ColumnName
import magnolia._

import scala.annotation.implicitNotFound
import scala.language.experimental.macros

trait PrefixColumns[C] {
@implicitNotFound(
"Couldn't find/derive PrefixColumns[${Names}]\n" +
" - make sure that all fields are wrapped in obligatory or selectable F[_], " +
"so that ${Names} is correctly substituted with ColumnName"
)
trait PrefixColumns[Names] {

def prepend(columns: C, prefix: String): C
def prepend(columns: Names, prefix: String): Names
}

object PrefixColumns {
Expand Down
Loading

0 comments on commit eb880c4

Please sign in to comment.