All Downloads are FREE. Search and download functionalities are using the official Maven repository.

skunk.tables.internal.TableBuilder.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.internal

import scala.quoted.*

import cats.data.NonEmptyList

import skunk.tables.{TypedColumn, Table, ColumnSelect, Dissect}

/** A factory, providing set of configuration methods, such as `withUnique`, `withName` to configure
  * the actual table
  *
  * The class itself supposed to be internal. Most members exist only at type-level
  */
trait TableBuilder[T <: Product]:
  self =>

  type Name <: String

  /** Union of all column names */
  type ColumnName

  type Primary <: Tuple
  type Unique <: Tuple
  type Default <: Tuple
  type Nullable <: Tuple

  transparent inline def withName[N <: String & Singleton](label: N): TableBuilder[T] =
    new TableBuilder[T]:
      type Name       = N
      type ColumnName = self.ColumnName
      type Primary    = self.Primary
      type Unique     = self.Unique
      type Default    = self.Default
      type Nullable   = self.Nullable

  transparent inline def withPrimary[Label <: self.ColumnName & Singleton](label: Label): TableBuilder[T] =
    new TableBuilder[T]:
      type Name       = self.Name
      type ColumnName = self.ColumnName
      type Primary    = Label *: self.Primary
      type Unique     = self.Unique
      type Default    = self.Default
      type Nullable   = self.Nullable

  transparent inline def withUnique[Label <: self.ColumnName & Singleton](label: Label): TableBuilder[T] =
    new TableBuilder[T]:
      type Name       = self.Name
      type ColumnName = self.ColumnName
      type Primary    = self.Primary
      type Unique     = Label *: self.Unique
      type Default    = self.Default
      type Nullable   = self.Nullable

  transparent inline def withDefault[Label <: self.ColumnName & Singleton](label: Label): TableBuilder[T] =
    new TableBuilder[T]:
      type Name       = self.Name
      type ColumnName = self.ColumnName
      type Primary    = self.Primary
      type Unique     = self.Unique
      type Default    = Label *: self.Default
      type Nullable   = self.Nullable

object TableBuilder:

  /** This where `Table.of` leads to */
  def init[T <: Product: Type](using Quotes): Expr[TableBuilder[T]] =
    import quotes.reflect.*

    val macroTable = MacroTable.build[T]

    val namesUnion = macroTable.getNamesUnion.asType

    val nullableColumns = macroTable.getNullableColumns.asTerm.tpe

    (namesUnion, nullableColumns.asType) match
      case ('[namesUnion], '[nullableColumns]) =>
        type Init = TableBuilder[T] {
          type ColumnName = namesUnion
          type Primary    = EmptyTuple
          type Unique     = EmptyTuple
          type Default    = EmptyTuple
          type Nullable   = nullableColumns
        }

        '{ (new TableBuilder[T] {}).asInstanceOf[Init] }

  extension [P <: Product, N, A, U, D, O]
    (builder: TableBuilder[P] {
      type Name     = N; type Primary = A; type Unique = U; type Default = D;
      type Nullable = O
    })
    inline transparent def build: Table[P] =
      ${ buildImpl[P, N, A, U, D, O] }

  /** @tparam P
    *   the product we're describing a table for
    * @tparam N
    *   a name of the table (a string singleton)
    * @tparam A
    *   a tuple of names which we'd like to make primary keys
    * @tparam U
    *   a tuple of names which we'd like to make unique
    * @tparam D
    *   a tuple of names which have defaults for
    * @tparam O
    *   a tuple of nullable (optional) column names in the table
    */
  private def buildImpl[P <: Product: Type, N: Type, A: Type, U: Type, D: Type, O: Type](using q: Quotes) =
    import quotes.reflect.*

    def addConstraint
      (constraint: TypedColumn.Constraint, labels: List[String])
      (columns: NonEmptyList[MacroColumn.FinalPhase[q.type]])
      : NonEmptyList[MacroColumn.FinalPhase[q.type]] =
      columns.map(col => if labels.contains(col.name) then col.addConstraint(constraint) else col)

    val tableName = TypeRepr.of[N] match
      case ConstantType(StringConstant(name)) => name

    val primary  = materializeTuple(TypeRepr.of[A])
    val unique   = materializeTuple(TypeRepr.of[U])
    val default  = materializeTuple(TypeRepr.of[D])
    val nullable = materializeTuple(TypeRepr.of[O])

    val initTable   = MacroTable.build[P]
    val initColumns = initTable.getTypedColumns.asTerm.tpe

    val unconstrainedColumns =
      MacroColumn.InitPhase.fromTypedColumns(initColumns).map(_.next(tableName))

    val columns = addConstraint(TypedColumn.Constraint.Primary, primary)
      .andThen(addConstraint(TypedColumn.Constraint.Unique, unique))
      .andThen(addConstraint(TypedColumn.Constraint.Default, default))
      .andThen(addConstraint(TypedColumn.Constraint.Nullable, nullable))
      .apply(unconstrainedColumns)

    val macroTable = initTable.next(columns, tableName)

    val allColumnsSelect = ColumnSelect.buildAllImpl[P](macroTable)
    val getColumnsSelect = ColumnSelect.buildGetImpl[P](macroTable)

    val namesUnion: TypeRepr = macroTable.getNamesUnion

    val mTypedColumns = macroTable.getTypedColumns

    val macroDissect = MacroDissect.build[P]

    val dissectS = Dissect.buildImpl[P]

    (allColumnsSelect.asTerm.tpe.asType,
     getColumnsSelect.asTerm.tpe.asType,
     namesUnion.asType,
     mTypedColumns.asTerm.tpe.asType,
     macroDissect.outType) match
      case ('[allSelectType], '[getSelectType], '[namesUnion], '[typedColumnsType], '[dissectOutType]) =>
        type Final = Table[P] {
          type TypedColumns = typedColumnsType
          type Select       = allSelectType
          type SelectGet    = getSelectType
          type ColumnName   = namesUnion
          type Columns      = dissectOutType
        }

        '{
          (new Table[P]:
            self =>
            def typedColumns =
              ${ mTypedColumns }.asInstanceOf[self.TypedColumns]
            val select    = ${ allColumnsSelect }.asInstanceOf[self.Select]
            val selectGet = ${ getColumnsSelect }.asInstanceOf[self.SelectGet]
            val dissect = ${ dissectS }
              .asInstanceOf[Dissect.AuxT[P, self.Columns, TwiddleTCN[self.TypedColumns]]]
            val name = Table.Name(${ Expr(tableName) })
          ).asInstanceOf[Final]

        }

  def deconstruct(using quotes: Quotes)(columns: quotes.reflect.TypeRepr): NonEmptyList[quotes.reflect.TypeRepr] =
    import quotes.reflect.*

    columns match
      case AppliedType(_, typedColumns) =>
        val list = typedColumns.map { case AppliedType(_, List(_, _, _, constraints)) => constraints }

        NonEmptyList.fromList(list) match
          case Some(nel) => nel
          case None      => report.errorAndAbort("Columns cannot be an EmptyTuple")

  /** Transform tuple of literal string types into `List` */
  def materializeTuple(using Quotes)(repr: quotes.reflect.TypeRepr): List[String] =
    import quotes.reflect.*

    repr match
      case AppliedType(_, ConstantType(StringConstant(c)) :: Nil) =>
        c :: Nil
      case ConstantType(StringConstant(c)) =>
        c :: Nil
      case AppliedType(_, List(ConstantType(StringConstant(c)), at: AppliedType)) =>
        c :: materializeTuple(at)
      case AppliedType(_, cts) =>
        cts.flatMap(materializeTuple)
      case TermRef(_, _) =>
        Nil // EmptyTuple
      case TypeRef(_, _) =>
        Nil




© 2015 - 2025 Weber Informatics LLC | Privacy Policy