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

skunk.tables.ColumnSelect.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.deriving.Mirror
import scala.quoted.*

import cats.data.NonEmptyList

import quotidian.{MacroMirror, MirrorElem}

import skunk.tables.internal.{MacroTable, Constants, MacroColumn}

/** `Columns` is a "selectable" trait, which means the members of it are created dynamically at
  * compile-time. Every member maps a member of case class (`T` param from `Table[T]`) into a
  * Postgres column, also preservin the type information.
  *
  * It's typically created as part of `Table` and used in its API, but nothing stops you from
  * accessing typed columns of `table.columns` directly
  */
trait ColumnSelect extends Selectable:

  /** Either `TypedColumn` or `TypedColumn.Insert` */
  type In

  /** A function to extract column name */
  def getName(in: In): String

  // TODO: do the same about `codec` and use it in `toString`

  /** Homongenous tuple of `In`s */
  type TypedColumns <: NonEmptyTuple

  /** A tuple of `TypedColumn`. The actual type is inferred dur synthesis `all` is reserved word in
    * Postgres, so should not cause collisions
    */
  def all: TypedColumns

  /** A list of columns. Used as a shortcur only `get` is reserved word in Postgres, so should not
    * cause collisions
    */
  def get: List[In] =
    all.toList.asInstanceOf[List[In]]

  transparent inline def selectDynamic(name: String): Any =
    get.find(column => getName(column) == name).get

  override def toString: String =
    s"ColumnSelect(${get.map(getName).mkString(", ")})"

object ColumnSelect:

  /** A constructor to get a `Selectable` for all known columns */
  private[tables] def buildAllImpl[T: Type]
    (using quotes: Quotes)
    (macroTable: MacroTable.FinalPhase[quotes.type, T])
    : Expr[ColumnSelect] =
    import quotes.reflect.*

    val nameTypeMap = macroTable.columnMap

    val typedColumns: Expr[Tuple] = macroTable.getTypedColumns

    val tableName = ConstantType(StringConstant(macroTable.tableName))

    val refinement = nameTypeMap
      .map { case (name, tpr) => (name, tpr) }
      .foldLeft(TypeRepr.of[ColumnSelect])
      .apply { case (acc, (name, tpr)) =>
        val constraint = macroTable.getConstraints(name)
        (tpr.asType, Singleton(Expr(name).asTerm).tpe.asType) match
          case ('[tpe], '[name]) =>
            Refinement(parent = acc,
                       name = name,
                       info = TypeRepr
                         .of[TypedColumn]
                         .appliedTo(List(Singleton(Expr(name).asTerm).tpe, TypeRepr.of[tpe], tableName, constraint))
            )
      }

    (typedColumns.asTerm.tpe.asType, refinement.asType) match
      case ('[typedColumns], '[refinement]) =>
        '{
          (new ColumnSelect:
            self =>
            type In = TypedColumn[?, ?, ?, ?] // Necessary for `getName`
            val all                                  = ${ typedColumns }.asInstanceOf[self.TypedColumns]
            def getName(in: TypedColumn[?, ?, ?, ?]) = in.n
          ).asInstanceOf[
            ColumnSelect { type In = TypedColumn[?, ?, ?, ?]; type TypedColumns = typedColumns } & refinement
          ]
        }

  /** A constructor to get a `Selectable` only for default and primary columns. It will be used in
    * `table.get`
    */
  private[tables] def buildGetImpl[T: Type]
    (using quotes: Quotes)
    (macroTable: MacroTable.FinalPhase[quotes.type, T])
    : Expr[ColumnSelect] =
    import quotes.reflect.*

    val nameTypeMap = macroTable.columnMap

    val typedColumns: Expr[Tuple] = macroTable.getPrimUniqColumns

    val tableName = ConstantType(StringConstant(macroTable.tableName))

    val refinement = nameTypeMap
      .foldLeft(TypeRepr.of[ColumnSelect])
      .apply { case (acc, (name, tpr)) =>
        if macroTable.isPrimUniq(name) then
          val constraint = macroTable.getConstraints(name)
          (tpr.asType, Singleton(Expr(name).asTerm).tpe.asType) match
            case ('[tpe], '[name]) =>
              Refinement(parent = acc,
                         name = name,
                         info = TypeRepr
                           .of[TypedColumn]
                           .appliedTo(List(Singleton(Expr(name).asTerm).tpe, TypeRepr.of[tpe], tableName, constraint))
              )
        else acc
      }

    (typedColumns.asTerm.tpe.asType, refinement.asType) match
      case ('[typedColumns], '[refinement]) =>
        '{
          (new ColumnSelect:
            self =>
            type In = TypedColumn[?, ?, ?, ?] // Necessary for `getName`
            val all                                  = ${ typedColumns }.asInstanceOf[self.TypedColumns]
            def getName(in: TypedColumn[?, ?, ?, ?]) = in.n
          ).asInstanceOf[
            ColumnSelect { type In = TypedColumn[?, ?, ?, ?]; type TypedColumns = typedColumns } & refinement
          ]
        }

  transparent inline def buildInsert[TT, Insert](table: TT) =
    ${ buildInsertImpl[TT, Insert]('table) }

  import CanInsert.CanInsertPartialFinal

  private def buildInsertImpl[T: Type, Insert: Type](using Quotes)(tableExpr: Expr[T]) =
    import quotes.reflect.*

    val macroTable = MacroTable.buildFromExpr(tableExpr)

    val columnSelect              = buildColumnSelect[T, Insert](tableExpr)
    val typedColumns: Expr[Tuple] = Expr.ofTupleFromSeq(macroTable.getInsertColumnsList[Insert])

    (macroTable.tpe, columnSelect.asTerm.tpe.asType, typedColumns.asTerm.tpe.asType) match
      case ('[t], '[s], '[c]) =>
        '{
          new CanInsertPartialFinal[Insert, t, s, c & NonEmptyTuple]($columnSelect.asInstanceOf[s]) {}
            .asInstanceOf[CanInsertPartialFinal[Insert, t, s, c & NonEmptyTuple]]
        }

  private def buildColumnSelect[T: Type, Insert: Type](using Quotes)(tableExpr: Expr[T]) =
    import quotes.reflect.*

    val macroTable = MacroTable.buildFromExpr(tableExpr)

    val nameTypeMap = macroTable.columns.map(column =>
      (column.name, column.tpe, MacroColumn.constraintsTuple(quotes)(column.constraints))
    )

    val typedColumns: Expr[Tuple] = Expr.ofTupleFromSeq(macroTable.getInsertColumnsList[Insert])

    val refinement = nameTypeMap
      .foldLeft(TypeRepr.of[ColumnSelect])
      .apply { case (acc, (name, tpr, c)) =>
        (tpr.asType, Singleton(Expr(name).asTerm).tpe.asType, c.asType) match
          case ('[tpe], '[name], '[constraints]) =>
            Refinement(parent = acc,
                       name = name,
                       info = TypeRepr
                         .of[TypedColumn.Insert]
                         .appliedTo(
                           List(Singleton(Expr(name).asTerm).tpe,
                                TypeRepr.of[tpe],
                                TypeRepr.of[constraints],
                                TypeRepr.of[Insert]
                           )
                         )
            )
      }

    (typedColumns.asTerm.tpe.asType, refinement.asType) match
      case ('[typedColumns], '[refinement]) =>
        '{
          (new ColumnSelect:
            self =>
            type In = TypedColumn.Insert[?, ?, ?, Insert] // Necessary for `getName`
            def getName(in: TypedColumn.Insert[?, ?, ?, Insert]) = in.n
            val all                                              = ${ typedColumns }.asInstanceOf[self.TypedColumns]
          ).asInstanceOf[
            ColumnSelect {
              type In = TypedColumn.Insert[?, ?, ?, Insert]; type TypedColumns = typedColumns
            } & refinement
          ]
        }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy