skunk.tables.Table.scala Maven / Gradle / Ivy
/*
* Copyright 2023 Foldables
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package skunk.tables
import scala.annotation.tailrec
import scala.language.implicitConversions
import scala.quoted.*
import skunk.{Codec, Decoder, Fragment, Void}
import skunk.implicits.*
import skunk.tables.internal.{TableBuilder, TwiddleTCN}
/** `Table` is the core entity for skunk-tables API. It links a product type `T` to a Postgres table
* with specific name and constraints and provides several utility methods to query that table in a
* type-safe manner
*
* Apart from building ready-to-be-executed tables, it also exposes even lower-level API to e.g.
* check/get all column names at compile-time
*/
trait Table[T <: Product]:
self =>
/** Homogenous tuple of `TypedColumn`; Copy of Select#TypedColumns */
type TypedColumns <: NonEmptyTuple
def typedColumns: TypedColumns
/** Table name */
def name: Table.Name
/** Union type of all coumn names */
type ColumnName
/** Flat tuple of Scala types used in columns */
type Columns
/** `Columns` selectable trait, allowing to statically access all typed columns */
type Select
def select: Select
/** `Columns` selectable trait, allowing to statically access primary or unique typed columns */
type SelectGet
def selectGet: SelectGet
given Conversion[ColumnName, String] =
name => name.asInstanceOf[String]
given Conversion[List[ColumnName], List[String]] =
names => names.map(_.asInstanceOf[String])
/** Skunk twiddler to transform twiddled tuple, received from `select` into concrete `T` */
def dissect: Dissect.AuxT[T, Columns, TwiddleTCN[TypedColumns]]
def count[F[_]]: Query[F, "single", Long] =
Query.count[F](name)
def all[F[_]]: Query[F, "many", T] { type Input = Void } =
Query.all[F, T](name, getColumnNames, decoder)
def query[F[_], A](q: Select => TypedColumn.Op[A]): Query[F, "many", T] =
val ops = q.apply(select)
Query.select[F, A, T](name, getColumnNames, ops, decoder)
def get[F[_], A](q: SelectGet => TypedColumn.Op[A]): Query[F, "optional", T] =
val ops = q.apply(selectGet)
Query.get[F, A, T](name, getColumnNames, ops, decoder)
def insert[F[_], A](using CanInsert[A, T])(a: A) =
Insert.insert[F, T, A, TypedColumns](name, a, summon[CanInsert[A, T]])
def decoder: Decoder[T] =
Table
.getCodecs(typedColumns)
.imap(twiddled => dissect.untwiddle(twiddled))(columns => dissect.twiddle(columns))
.imap(columns => dissect.from(columns))(t => dissect.to(t))
/** All column names, in their order */
def getColumnNames: List[ColumnName] =
getColumns.map(_.n.toString).asInstanceOf[List[ColumnName]]
override def toString: String =
s"Table($name, $select)"
private def getColumns: List[TypedColumn[?, ?, ?, ?]] =
typedColumns.toList.asInstanceOf[List[TypedColumn[?, ?, ?, ?]]]
/** Lower-level API to work with `Fragment` */
object low:
/** Table name as a `Fragment` */
def name: Fragment[Void] =
self.name.toFragment
/** Build `Fragment` of `table_name as s` */
def nameAs(short: String): Fragment[Void] =
sql"${name} AS #${short}"
/** All comma-separated columns */
def columns: Fragment[Void] =
sql"#${self.getColumnNames.mkString(", ")}"
/** All comma-separated columns with a prefix (like "p.age, p.name, p.email") */
def columnsOf(prefix: String): Fragment[Void] =
sql"#${self.getColumnNames.map(n => s"$prefix.$n").mkString(", ")}"
/** Pick a (type-checked) set of columns as a comma-separated `Fragment` */
def pick(toInclude: ColumnName*): Fragment[Void] =
sql"#${toInclude.mkString(", ")}"
/** Pick columns as a comma-separated `Fragment`, except some (type-checked) set */
def except(toExclude: ColumnName*): Fragment[Void] =
sql"#${getColumnNames.filterNot(name => toExclude.contains(name)).mkString(", ")}"
object Table:
/** The constructor of `Table` */
inline transparent def of[T <: Product] =
${ TableBuilder.init[T] }
opaque type Name = String
object Name:
def apply(str: String): Name = str
extension (name: Name)
def unbox: String = name
def toFragment: Fragment[Void] = sql"#${unbox}"
private def getCodecs[T <: NonEmptyTuple](t: T): Codec[TwiddleTCN[T]] =
@tailrec
def go[TT <: Tuple](tt: TT, codecs: Codec[Any]): Codec[Any] =
tt match
case EmptyTuple => codecs.asInstanceOf[Codec[Any]]
case h *: tail =>
val prod = codecs
.product(
h.asInstanceOf[TypedColumn[?, ?, ?, ?]]
.primitive
.codec
.asInstanceOf[Codec[Any]]
)
.imap[Any *: Any *: EmptyTuple] { case (a, b) =>
a *: b *: EmptyTuple
} { case a *: b *: EmptyTuple => (a, b) }
.asInstanceOf[Codec[Any]]
go(tail, prod)
t match
case h *: tail =>
go(tail,
h.asInstanceOf[TypedColumn[?, ?, ?, ?]]
.primitive
.codec
.asInstanceOf[Codec[Any]]
).asInstanceOf[Codec[TwiddleTCN[T]]]
© 2015 - 2025 Weber Informatics LLC | Privacy Policy