.grackle-sql_2.13.0.22.0.source-code.SqlMapping.scala Maven / Gradle / Ivy
The newest version!
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// Copyright (c) 2016-2023 Grackle Contributors
//
// 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 grackle
package sql
import scala.annotation.tailrec
import scala.collection.Factory
import scala.util.control.NonFatal
import cats.MonadThrow
import cats.data.{NonEmptyList, OptionT, StateT}
import cats.implicits._
import io.circe.Json
import org.tpolecat.sourcepos.SourcePos
import org.tpolecat.typename.typeName
import circe.CirceMappingLike
import syntax._
import Predicate._
import Query._
import ValidationFailure.Severity
abstract class SqlMapping[F[_]](implicit val M: MonadThrow[F]) extends Mapping[F] with SqlMappingLike[F]
/** An abstract mapping that is backed by a SQL database. */
trait SqlMappingLike[F[_]] extends CirceMappingLike[F] with SqlModule[F] { self =>
import SqlQuery.{EmptySqlQuery, SqlJoin, SqlSelect, SqlUnion}
import TableExpr.{DerivedTableRef, SubqueryRef, TableRef, WithRef}
case class TableName(name: String)
object TableName {
val rootName = ""
val rootTableName = TableName(rootName)
def isRoot(table: String): Boolean = table == rootName
}
class TableDef(name: String) {
implicit val tableName: TableName = TableName(name)
}
class RootDef {
implicit val tableName: TableName = TableName.rootTableName
}
/**
* Name of a SQL schema column and its associated codec, Scala type an defining
* source position within an `SqlMapping`.
*
* `Column`s are considered equal if their table and column names are equal.
*
* Note that `ColumnRef` primarily play a role in mappings. During compilation
* they will be used to construct `SqlColumns`.
*/
case class ColumnRef(table: String, column: String, codec: Codec, scalaTypeName: String, pos: SourcePos) {
override def equals(other: Any) =
other match {
case cr: ColumnRef => table == cr.table && column == cr.column
case _ => false
}
override def hashCode(): Int =
table.hashCode() + column.hashCode()
}
type Aliased[T] = StateT[Result, AliasState, T]
object Aliased {
def pure[T](t: T): Aliased[T] = StateT.pure(t)
def liftR[T](rt: Result[T]): Aliased[T] = StateT.liftF(rt)
def tableDef(table: TableExpr): Aliased[String] = StateT(_.tableDef(table).success)
def tableRef(table: TableExpr): Aliased[String] = StateT(_.tableRef(table).success)
def columnDef(column: SqlColumn): Aliased[(Option[String], String)] = StateT(_.columnDef(column).success)
def columnRef(column: SqlColumn): Aliased[(Option[String], String)] = StateT(_.columnRef(column).success)
def pushOwner(owner: ColumnOwner): Aliased[Unit] = StateT(_.pushOwner(owner).success)
def popOwner: Aliased[ColumnOwner] = StateT(_.popOwner.success)
def internalError[T](msg: String): Aliased[T] = StateT(_ => Result.internalError[(AliasState, T)](msg))
def internalError[T](err: Throwable): Aliased[T] = StateT(_ => Result.internalError[(AliasState, T)](err))
}
/**
* State required to assign table and column aliases.
*
* Used when rendering an `SqlQuery` as a `Fragment`. Table aliases are assigned
* as needed for recursive queries. Column aliases are assigned to disambiguate
* collections of columns generated by subqueries and unions.
*/
case class AliasState(
next: Int,
seenTables: Set[String],
tableAliases: Map[(List[String], String), String],
seenColumns: Set[String],
columnAliases: Map[(List[String], String), String],
ownerChain: List[ColumnOwner]
) {
/** Update state to reflect a defining occurence of a table */
def tableDef(table: TableExpr): (AliasState, String) =
tableAliases.get((table.context.resultPath, table.name)) match {
case Some(alias) => (this, alias)
case None =>
if (seenTables(table.name)) {
val alias = s"${table.name}_alias_$next"
val newState =
copy(
next = next+1,
tableAliases = tableAliases + ((table.context.resultPath, table.name) -> alias)
)
(newState, alias)
} else {
val newState =
copy(
seenTables = seenTables + table.name,
tableAliases = tableAliases + ((table.context.resultPath, table.name) -> table.name)
)
(newState, table.name)
}
}
/** Yields the possibly aliased name of the supplied table */
def tableRef(table: TableExpr): (AliasState, String) =
tableAliases.get((table.context.resultPath, table.name)) match {
case Some(alias) => (this, alias)
case None => (this, table.name)
}
/** Update state to reflect a defining occurence of a column */
def columnDef(column: SqlColumn): (AliasState, (Option[String], String)) = {
val (newState0, table0) = column.namedOwner.map(named => tableDef(named).fmap(Option(_))).getOrElse((this, None))
columnAliases.get((column.underlying.owner.context.resultPath, column.underlying.column)) match {
case Some(name) => (newState0, (table0, name))
case None =>
val (next0, seenColumns0, column0) =
if (newState0.seenColumns(column.column))
(newState0.next+1, newState0.seenColumns, s"${column.column}_alias_${newState0.next}")
else
(newState0.next, newState0.seenColumns + column.column, column.column)
val columnAliases0 =
newState0.columnAliases + ((column.underlying.owner.context.resultPath, column.underlying.column) -> column0)
val newState = newState0.copy(next = next0, seenColumns = seenColumns0, columnAliases = columnAliases0)
(newState, (table0, column0))
}
}
/** Yields the possibly aliased name of the supplied column */
def columnRef(column: SqlColumn): (AliasState, (Option[String], String)) = {
if (ownerChain.exists(_.directlyOwns(column))) {
column.namedOwner.map(named => tableRef(named).
fmap(Option(_))).getOrElse((this, None)).
fmap(table0 => (table0, column.column))
} else {
val name = columnAliases.get((column.underlying.owner.context.resultPath, column.underlying.column)).getOrElse(column.column)
column.namedOwner.map(named => tableRef(named).
fmap(Option(_))).getOrElse((this, None)).
fmap(table0 => (table0, name))
}
}
/** Update state to reflect the current column owner while traversing
* the `SqlQuery` being rendered
*/
def pushOwner(owner: ColumnOwner): (AliasState, Unit) = (copy(ownerChain = owner :: ownerChain), ())
/** Update state to restore the current column owner while traversing
* the `SqlQuery` being rendered
*/
def popOwner: (AliasState, ColumnOwner) = (copy(ownerChain = ownerChain.tail), ownerChain.head)
}
object AliasState {
def empty: AliasState =
AliasState(
0,
Set.empty[String],
Map.empty[(List[String], String), String],
Set.empty[String],
Map.empty[(List[String], String), String],
List.empty[ColumnOwner]
)
}
/** Trait representing an owner of an `SqlColumn
*
* ColumnOwners are tables, SQL queries and subqueries, common
* table expressions and the like. Most, but not all have a
* name (SqlSelect, SqlUnion and SqlJoin being unnamed
* examples)
*/
sealed trait ColumnOwner extends Product with Serializable {
def context: Context
def owns(col: SqlColumn): Boolean
def contains(other: ColumnOwner): Boolean
def directlyOwns(col: SqlColumn): Boolean
def findNamedOwner(col: SqlColumn): Option[TableExpr]
/** The name, if any, of this `ColumnOwner` */
def nameOption: Option[String] =
this match {
case named: TableExpr => Some(named.name)
case _ => None
}
def isSameOwner(other: ColumnOwner): Boolean =
(this, other) match {
case (n1: TableExpr, n2: TableExpr) =>
(n1.name == n2.name) && (context == other.context)
case _ => false
}
def debugShow: String =
(this match {
case tr: TableExpr => tr.toDefFragment
case sq: SqlQuery => sq.toFragment
case sj: SqlJoin => sj.toFragment
}).runA(AliasState.empty).toOption.get.toString
}
/** Trait representing an SQL column */
trait SqlColumn {
def owner: ColumnOwner
def column: String
def codec: Codec
def scalaTypeName: String
def pos: SourcePos
def resultPath: List[String]
/** The named owner of this column, if any */
def namedOwner: Option[TableExpr] =
owner.findNamedOwner(this)
/** If this column is derived, the column it was derived from, itself otherwise */
def underlying: SqlColumn = this
/** Is this column a reference to a column of a table */
def isRef: Boolean = false
/** Yields a copy of this column with all occurences of `from` replaced by `to` */
def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn
/** Yields a copy of this column in `other`
*
* Only well defined if the move doesn't lose an owner name
*/
def in(other: ColumnOwner): SqlColumn = {
assert(other.nameOption.isDefined || owner.nameOption.isEmpty)
subst(owner, other)
}
/** Derives a new column with a different owner with this column as underlying.
*
* Used to represent columns on the outside of subqueries and common table
* expressions. Note that column aliases are tracked across derivation so
* that derived columns will continue to refer to the same underlying data
* irrespective of renaming.
*/
def derive(other: ColumnOwner): SqlColumn =
if(other == owner) this
else SqlColumn.DerivedColumn(other, this)
/** Equality on `SqlColumns`
*
* Two `SqlColumns` are equal if their underlyings have the same name and owner.
*/
override def equals(other: Any) =
other match {
case cr: SqlColumn =>
val u0 = underlying
val u1 = cr.underlying
u0.column == u1.column && u0.owner.isSameOwner(u1.owner)
case _ => false
}
override def hashCode(): Int = {
val u = underlying
u.owner.context.hashCode() + u.column.hashCode()
}
/** This column as a `Term` which can appear in a `Predicate` */
def toTerm: Term[Option[Unit]] = SqlColumnTerm(this)
/** Render a defining occurence of this `SqlColumn` */
def toDefFragment(collated: Boolean): Aliased[Fragment]
/** Render a reference to this `SqlColumn` */
def toRefFragment(collated: Boolean): Aliased[Fragment]
override def toString: String =
owner match {
case named: TableExpr => s"${named.name}.$column"
case _ => column
}
}
object SqlColumn {
def mkDefFragment(prefix: Option[String], base: String, collated: Boolean, alias: String): Fragment = {
val prefix0 = prefix.map(_+".").getOrElse("")
val qualified = prefix0+base
val collated0 =
if (collated) s"""($qualified COLLATE "C")"""
else qualified
val aliased =
if (base == alias) collated0
else s"$collated0 AS $alias"
Fragments.const(aliased)
}
def mkDefFragment(base: Fragment, collated: Boolean, alias: String): Fragment = {
val collated0 =
if (collated) Fragments.parentheses(base |+| Fragments.const(s""" COLLATE "C""""))
else base
collated0 |+| Fragments.const(s"AS $alias")
}
def mkRefFragment(prefix: Option[String], alias: String, collated: Boolean): Fragment = {
val prefix0 = prefix.map(_+".").getOrElse("")
val qualified = prefix0+alias
val base = Fragments.const(qualified)
if (collated) Fragments.parentheses(base |+| Fragments.const(s""" COLLATE "C""""))
else base
}
/** Representation of a column of a table/view */
case class TableColumn(owner: ColumnOwner, cr: ColumnRef, resultPath: List[String]) extends SqlColumn {
def column: String = cr.column
def codec: Codec = cr.codec
def scalaTypeName: String = cr.scalaTypeName
def pos: SourcePos = cr.pos
def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn =
if(!owner.isSameOwner(from)) this
else to match {
case _: DerivedTableRef => derive(to)
case _: SubqueryRef => derive(to)
case _ => copy(owner = to)
}
override def isRef: Boolean = true
def toDefFragment(collated: Boolean): Aliased[Fragment] =
Aliased.columnDef(this).map {
case (table0, column0) => mkDefFragment(table0, column, collated, column0)
}
def toRefFragment(collated: Boolean): Aliased[Fragment] =
Aliased.columnRef(this).map {
case (table0, column0) => mkRefFragment(table0, column0, collated)
}
}
object TableColumn {
def apply(context: Context, cr: ColumnRef, resultPath: List[String]): TableColumn =
TableColumn(TableRef(context, cr.table), cr, resultPath)
}
/** Representation of a synthetic null column
*
* Primarily used to pad the disjuncts of an `SqlUnion`.
*/
case class NullColumn(owner: ColumnOwner, col: SqlColumn) extends SqlColumn {
def column: String = col.column
def codec: Codec = col.codec
def scalaTypeName: String = col.scalaTypeName
def pos: SourcePos = col.pos
def resultPath: List[String] = col.resultPath
override def underlying: SqlColumn = col.underlying
def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn =
copy(owner = if(owner.isSameOwner(from)) to else owner, col = col.subst(from, to))
def toDefFragment(collated: Boolean): Aliased[Fragment] =
Aliased.columnDef(this).map {
case (_, column0) =>
val ascribed =
Fragments.sqlTypeName(codec) match {
case Some(name) => Fragments.const(s"(NULL :: $name)")
case None => Fragments.const("NULL")
}
mkDefFragment(ascribed, collated, column0)
}
def toRefFragment(collated: Boolean): Aliased[Fragment] =
Aliased.columnRef(this).map {
case (table0, column0) => mkRefFragment(table0, column0, collated)
}
}
/** Representation of a scalar subquery */
case class SubqueryColumn(col: SqlColumn, subquery: SqlSelect) extends SqlColumn {
def owner: ColumnOwner = col.owner
def column: String = col.column
def codec: Codec = col.codec
def scalaTypeName: String = col.scalaTypeName
def pos: SourcePos = col.pos
def resultPath: List[String] = col.resultPath
def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn = {
val subquery0 =
(from, to) match {
case (tf: TableExpr, tt: TableExpr) => subquery.subst(tf, tt)
case _ => subquery
}
copy(col = col.subst(from, to), subquery = subquery0)
}
def toDefFragment(collated: Boolean): Aliased[Fragment] =
for {
sub0 <- subquery.toFragment
tc0 <- Aliased.columnDef(this)
} yield mkDefFragment(Fragments.parentheses(sub0), collated, tc0._2)
def toRefFragment(collated: Boolean): Aliased[Fragment] =
Aliased.columnRef(this).map {
case (table0, column0) => mkRefFragment(table0, column0, collated)
}
}
/** Representation of COUNT aggregation */
case class CountColumn(col: SqlColumn, cols: List[SqlColumn]) extends SqlColumn {
def owner: ColumnOwner = col.owner
def column: String = col.column
def codec: Codec = col.codec
def scalaTypeName: String = col.scalaTypeName
def pos: SourcePos = col.pos
def resultPath: List[String] = col.resultPath
def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn =
copy(col.subst(from, to), cols.map(_.subst(from, to)))
def toDefFragment(collated: Boolean): Aliased[Fragment] =
for {
cols0 <- cols.traverse(_.toRefFragment(false))
ct0 <- Aliased.columnDef(this)
} yield {
val count = Fragments.const("COUNT(DISTINCT(") |+| cols0.intercalate(Fragments.const(", ")) |+| Fragments.const(s"))")
mkDefFragment(count, collated, ct0._2)
}
def toRefFragment(collated: Boolean): Aliased[Fragment] =
Aliased.columnRef(this).map {
case (table0, column0) => mkRefFragment(table0, column0, collated)
}
}
/** Representation of a window aggregation */
case class PartitionColumn(owner: ColumnOwner, column: String, partitionCols: List[SqlColumn], orders: List[OrderSelection[_]]) extends SqlColumn {
def codec: Codec = intCodec
def scalaTypeName: String = "Int"
def pos: SourcePos = null
def resultPath: List[String] = Nil
def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn =
copy(owner = if(owner.isSameOwner(from)) to else owner, partitionCols = partitionCols.map(_.subst(from, to)))
def partitionColsToFragment: Aliased[Fragment] =
if (partitionCols.isEmpty) Aliased.pure(Fragments.empty)
else
partitionCols.traverse(_.toRefFragment(false)).map { fcols =>
Fragments.const("PARTITION BY ") |+| fcols.intercalate(Fragments.const(", "))
}
def toDefFragment(collated: Boolean): Aliased[Fragment] =
for {
cols0 <- partitionColsToFragment
tc0 <- Aliased.columnDef(this)
orderBy <- SqlQuery.ordersToFragment(orders)
} yield {
//val base = Fragments.const("row_number() OVER ") |+| Fragments.parentheses(cols0 |+| orderBy)
val base = Fragments.const("dense_rank() OVER ") |+| Fragments.parentheses(cols0 |+| orderBy)
mkDefFragment(base, false, tc0._2)
}
def toRefFragment(collated: Boolean): Aliased[Fragment] =
Aliased.columnRef(this).map {
case (table0, column0) => mkRefFragment(table0, column0, collated)
}
}
/** Representation of a column of an embedded subobject
*
* Columns of embedded subobjects have a different context path from columns of
* their enclosing object, however they resolve to columns of the same `SqlSelect`.
* To satisfy the `SqlSelect` invariant that all its columns must share the same
* context path we have to wrap the embedded column so that its context path
* conforms.
*/
case class EmbeddedColumn(owner: ColumnOwner, col: SqlColumn) extends SqlColumn {
def column: String = col.column
def codec: Codec = col.codec
def scalaTypeName: String = col.scalaTypeName
def pos: SourcePos = col.pos
def resultPath: List[String] = col.resultPath
override def underlying: SqlColumn = col.underlying
def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn =
copy(owner = if(owner.isSameOwner(from)) to else owner, col = col.subst(from, to))
override def isRef: Boolean = col.isRef
def toDefFragment(collated: Boolean): Aliased[Fragment] =
col match {
case _: SubqueryColumn =>
col.in(owner).toDefFragment(collated)
case _ =>
Aliased.columnDef(this).map {
case (table0, column0) => mkDefFragment(table0, column, collated, column0)
}
}
def toRefFragment(collated: Boolean): Aliased[Fragment] =
Aliased.columnRef(this).map {
case (table0, column0) => mkRefFragment(table0, column0, collated)
}
}
/** Representation of a derived column
*
* Used to represent columns on the outside of subqueries and common table
* expressions. Note that column aliases are tracked across derivation so
* that derived columns will continue to refer to the same underlying data
* irrespective of renaming.
*/
case class DerivedColumn(owner: ColumnOwner, col: SqlColumn) extends SqlColumn {
def column: String = col.column
def codec: Codec = col.codec
def scalaTypeName: String = col.scalaTypeName
def pos: SourcePos = col.pos
def resultPath: List[String] = col.resultPath
override def underlying: SqlColumn = col.underlying
def subst(from: ColumnOwner, to: ColumnOwner): SqlColumn =
copy(owner = if(owner.isSameOwner(from)) to else owner, col = col.subst(from, to))
override def isRef: Boolean = col.isRef
def toDefFragment(collated: Boolean): Aliased[Fragment] = {
for {
table0 <- namedOwner.map(named => Aliased.tableDef(named).map(Option(_))).getOrElse(Aliased.pure(None))
tc <- Aliased.columnDef(col)
} yield mkDefFragment(table0, tc._2, collated, tc._2)
}
def toRefFragment(collated: Boolean): Aliased[Fragment] =
Aliased.columnRef(this).map {
case (table0, column0) => mkRefFragment(table0, column0, collated)
}
}
}
/** Wraps an `SqlColumn` as a `Term` which can appear in a `Predicate` */
case class SqlColumnTerm(col: SqlColumn) extends Term[Option[Unit]] {
def apply(c: Cursor): Result[Option[Unit]] = Result(Option(()))
def children: List[Term[_]] = Nil
}
/** A pair of `ColumnRef`s, representing a SQL join. */
case class Join(conditions: List[(ColumnRef, ColumnRef)]) {
def parentTable(parentContext: Context): TableRef =
TableRef(parentContext, conditions.head._1.table)
def childTable(parentContext: Context): TableRef =
TableRef(parentContext, conditions.head._2.table)
def parentCols(parentTable0: TableExpr): List[SqlColumn] =
toSqlColumns(parentTable0, conditions.map(_._1))
def childCols(childTable0: TableExpr): List[SqlColumn] =
toSqlColumns(childTable0, conditions.map(_._2))
def toSqlColumns(parentTable0: TableExpr, childTable0: TableExpr): List[(SqlColumn, SqlColumn)] = {
parentCols(parentTable0).zip(childCols(childTable0))
}
def toSqlColumns(parentContext: Context, childContext: Context): List[(SqlColumn, SqlColumn)] =
toSqlColumns(parentTable(parentContext), childTable(childContext))
def toSqlJoin(parentTable0: TableExpr, childTable0: TableExpr, inner: Boolean): SqlJoin = {
val on = toSqlColumns(parentTable0, childTable0)
SqlJoin(parentTable0, childTable0, on, inner)
}
def toSqlJoin(parentContext: Context, childContext: Context, inner: Boolean): SqlJoin =
toSqlJoin(parentTable(parentContext), childTable(childContext), inner)
def toConstraints(parentContext: Context, childContext: Context): List[(SqlColumn, SqlColumn)] =
parentCols(parentTable(parentContext)).zip(childCols(childTable(childContext)))
def toSqlColumns(table: TableExpr, cols: List[ColumnRef]): List[SqlColumn] =
cols.map(col => SqlColumn.TableColumn(table, col, Nil))
}
object Join {
def apply(parent: ColumnRef, child: ColumnRef): Join =
new Join(List((parent, child)))
}
class SqlMappingException(msg: String) extends RuntimeException(msg)
/**
* Operators which can be compiled to SQL are eliminated here, partly to avoid duplicating
* work programmatically, but also because the operation isn't necessarily idempotent and
* the result set doesn't necessarily contain the fields required for the filter predicates.
*/
def stripCompiled(query: Query, context: Context): Result[Query] = {
def loop(query: Query, context: Context): Query =
query match {
// Preserved non-Sql filters
case FilterOrderByOffsetLimit(p@Some(pred), oss, off, lim, child) if !isSqlTerm(context, pred).getOrElse(throw new SqlMappingException(s"Unmapped term $pred")) =>
FilterOrderByOffsetLimit(p, oss, off, lim, loop(child, context))
case Filter(_, child) => loop(child, context)
case Offset(_, child) => loop(child, context)
case Limit(_, child) => loop(child, context)
// Preserve OrderBy
case o: OrderBy => o.copy(child = loop(o.child, context))
case s@Select(fieldName, _, Count(_)) =>
if(context.tpe.underlying.hasField(fieldName)) s.copy(child = Empty)
else Empty
case s@Select(fieldName, resultName, _) =>
val fieldContext = context.forField(fieldName, resultName).getOrElse(throw new SqlMappingException(s"No field '$fieldName' of type ${context.tpe}"))
s.copy(child = loop(s.child, fieldContext))
case s@UntypedSelect(fieldName, resultName, _, _, _) =>
val fieldContext = context.forField(fieldName, resultName).getOrElse(throw new SqlMappingException(s"No field '$fieldName' of type ${context.tpe}"))
s.copy(child = loop(s.child, fieldContext))
case Group(queries) => Group(queries.map(q => loop(q, context)).filterNot(_ == Empty))
case u: Unique => u.copy(child = loop(u.child, context.asType(context.tpe.list)))
case e: Environment => e.copy(child = loop(e.child, context))
case t: TransformCursor => t.copy(child = loop(t.child, context))
case n@Narrow(subtpe, _) => n.copy(child = loop(n.child, context.asType(subtpe)))
case other@(_: Component[_] | _: Effect[_] | Empty | _: Introspect | _: Select | _: Count | _: UntypedFragmentSpread | _: UntypedInlineFragment) => other
}
Result.catchNonFatal {
loop(query, context)
}
}
def sqlCursor(query: Query, env: Env): F[Result[Cursor]] =
defaultRootCursor(query, schema.queryType, Some(RootCursor(Context(schema.queryType), None, env))).map(_.map(_._2))
// Overrides definition in Mapping
override def defaultRootCursor(query: Query, tpe: Type, parentCursor: Option[Cursor]): F[Result[(Query, Cursor)]] = {
val context = Context(tpe)
val rootQueries = ungroup(query)
def mkGroup(queries: List[Query]): Query =
if(queries.isEmpty) Empty
else if(queries.sizeCompare(1) == 0) queries.head
else Group(queries)
def mkCursor(query: Query): F[Result[(Query, SqlCursor)]] =
MappedQuery(query, context).flatTraverse { mapped =>
(for {
table <- ResultT(mapped.fetch)
frag <- ResultT(mapped.fragment.pure[F])
_ <- ResultT(monitor.queryMapped(query, frag, table.numRows, table.numCols).map(_.success))
stripped <- ResultT(stripCompiled(query, context).pure[F])
} yield (stripped, SqlCursor(context, table, mapped, parentCursor, Env.empty))).value
}
val (sqlRoots, otherRoots) = rootQueries.partition(isLocallyMapped(context, _))
if(sqlRoots.sizeCompare(1) <= 0)
mkCursor(mkGroup(sqlRoots)).map(_.map {
case (sq, sc) => (mergeQueries(sq :: otherRoots), sc: Cursor)
})
else {
sqlRoots.traverse(mkCursor).map { qcs =>
qcs.sequence.map(_.unzip match {
case (queries, cursors) =>
val mergedQuery = mergeQueries(queries ++ otherRoots)
val cursor = MultiRootCursor(cursors)
(mergedQuery, cursor)
})
}
}
}
def rootFieldMapping(context: Context, query: Query): Option[FieldMapping] =
for {
rn <- Query.rootName(query)
fm <- typeMappings.fieldMapping(context, rn._1)
} yield fm
def isLocallyMapped(context: Context, query: Query): Boolean =
rootFieldMapping(context, query) match {
case Some(_: SqlFieldMapping) => true
case Some(re: EffectMapping) =>
val fieldContext = context.forFieldOrAttribute(re.fieldName, None)
typeMappings.objectMapping(fieldContext).exists { om =>
om.fieldMappings.exists {
case _: SqlFieldMapping => true
case _ => false
}
}
case _ => false
}
sealed trait SqlFieldMapping extends FieldMapping
case class SqlField(
fieldName: String,
columnRef: ColumnRef,
key: Boolean = false,
discriminator: Boolean = false,
hidden: Boolean = false,
associative: Boolean = false // a key which is also associative might occur multiple times in the table, ie. it is not a DB primary key
)(implicit val pos: SourcePos) extends SqlFieldMapping {
def subtree: Boolean = false
}
case class SqlObject(fieldName: String, joins: List[Join])(
implicit val pos: SourcePos
) extends SqlFieldMapping {
final def hidden = false
final def subtree: Boolean = false
}
object SqlObject {
def apply(fieldName: String, joins: Join*)(implicit pos: SourcePos): SqlObject = apply(fieldName, joins.toList)
}
case class SqlJson(fieldName: String, columnRef: ColumnRef)(
implicit val pos: SourcePos
) extends SqlFieldMapping {
def hidden: Boolean = false
def subtree: Boolean = true
}
/**
* Common super type for mappings which have a programmatic discriminator, ie. interface and union mappings.
*/
sealed trait SqlDiscriminatedType {
def discriminator: SqlDiscriminator
}
/** Discriminator for the branches of an interface/union */
trait SqlDiscriminator {
/** yield a predicate suitable for filtering row corresponding to the supplied type */
def narrowPredicate(tpe: Type): Result[Predicate]
/** compute the concrete type of the value at the cursor */
def discriminate(cursor: Cursor): Result[Type]
}
sealed trait SqlInterfaceMapping extends ObjectMapping with SqlDiscriminatedType
object SqlInterfaceMapping {
case class DefaultInterfaceMapping(predicate: MappingPredicate, fieldMappings: Seq[FieldMapping], discriminator: SqlDiscriminator)(
implicit val pos: SourcePos
) extends SqlInterfaceMapping {
override def showMappingType: String = "SqlInterfaceMapping"
}
def apply(
predicate: MappingPredicate,
discriminator: SqlDiscriminator
)(
fieldMappings: FieldMapping*
)(
implicit pos: SourcePos
): ObjectMapping =
DefaultInterfaceMapping(predicate, fieldMappings, discriminator)
def apply(
tpe: NamedType,
discriminator: SqlDiscriminator
)(
fieldMappings: FieldMapping*
)(
implicit pos: SourcePos
): ObjectMapping =
DefaultInterfaceMapping(MappingPredicate.TypeMatch(tpe), fieldMappings, discriminator)
def apply(
path: Path,
discriminator: SqlDiscriminator
)(
fieldMappings: FieldMapping*
)(
implicit pos: SourcePos
): ObjectMapping =
DefaultInterfaceMapping(MappingPredicate.PathMatch(path), fieldMappings, discriminator)
def apply(
tpe: NamedType,
fieldMappings: List[FieldMapping],
discriminator: SqlDiscriminator
)(
implicit pos: SourcePos
): ObjectMapping =
DefaultInterfaceMapping(MappingPredicate.TypeMatch(tpe), fieldMappings, discriminator)
}
sealed trait SqlUnionMapping extends ObjectMapping with SqlDiscriminatedType
object SqlUnionMapping {
case class DefaultUnionMapping(predicate: MappingPredicate, fieldMappings: Seq[FieldMapping], discriminator: SqlDiscriminator)(
implicit val pos: SourcePos
) extends SqlUnionMapping {
override def showMappingType: String = "SqlUnionMapping"
}
def apply(
predicate: MappingPredicate,
discriminator: SqlDiscriminator
)(
fieldMappings: FieldMapping*
)(
implicit pos: SourcePos
): ObjectMapping =
DefaultUnionMapping(predicate, fieldMappings, discriminator)
def apply(
tpe: NamedType,
discriminator: SqlDiscriminator
)(
fieldMappings: FieldMapping*
)(
implicit pos: SourcePos
): ObjectMapping =
DefaultUnionMapping(MappingPredicate.TypeMatch(tpe), fieldMappings, discriminator)
def apply(
path: Path,
discriminator: SqlDiscriminator
)(
fieldMappings: FieldMapping*
)(
implicit pos: SourcePos
): ObjectMapping =
DefaultUnionMapping(MappingPredicate.PathMatch(path), fieldMappings, discriminator)
def apply(
tpe: NamedType,
fieldMappings: List[FieldMapping],
discriminator: SqlDiscriminator,
)(
implicit pos: SourcePos
): ObjectMapping =
DefaultUnionMapping(MappingPredicate.TypeMatch(tpe), fieldMappings, discriminator)
}
override protected def unpackPrefixedMapping(prefix: List[String], om: ObjectMapping): ObjectMapping =
om match {
case im: SqlInterfaceMapping.DefaultInterfaceMapping =>
im.copy(predicate = MappingPredicate.PrefixedTypeMatch(prefix, om.predicate.tpe))
case um: SqlUnionMapping.DefaultUnionMapping =>
um.copy(predicate = MappingPredicate.PrefixedTypeMatch(prefix, om.predicate.tpe))
case _ => super.unpackPrefixedMapping(prefix, om)
}
/** Returns the discriminator columns for the context type */
def discriminatorColumnsForType(context: Context): List[SqlColumn] =
typeMappings.objectMapping(context).map(_.fieldMappings.iterator.collect {
case cm: SqlField if cm.discriminator => SqlColumn.TableColumn(context, cm.columnRef, cm.fieldName :: context.resultPath)
}.toList).getOrElse(Nil)
/** Returns the key columns for the context type */
def keyColumnsForType(context: Context): List[SqlColumn] = {
val cols =
typeMappings.objectMapping(context).map { obj =>
val objectKeys = obj.fieldMappings.iterator.collect {
case cm: SqlField if cm.key => SqlColumn.TableColumn(context, cm.columnRef, cm.fieldName :: context.resultPath)
}.toList
val interfaceKeys = context.tpe.underlyingObject match {
case Some(twf: TypeWithFields) =>
twf.interfaces.flatMap(nt => keyColumnsForType(context.asType(nt)))
case _ => Nil
}
(objectKeys ++ interfaceKeys).distinct
}.getOrElse(Nil)
cols
}
/** Returns the columns for leaf field `fieldName` in `context` */
def columnsForLeaf(context: Context, fieldName: String): Result[List[SqlColumn]] =
typeMappings.fieldMapping(context, fieldName) match {
case Some(SqlField(_, cr, _, _, _, _)) => List(SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath)).success
case Some(SqlJson(_, cr)) => List(SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath)).success
case Some(CursorFieldJson(_, _, required, _)) =>
required.flatTraverse(r => columnsForLeaf(context, r))
case Some(CursorField(_, _, _, required, _)) =>
required.flatTraverse(r => columnsForLeaf(context, r))
case Some(EffectField(_, _, required, _)) =>
required.flatTraverse(r => columnsForLeaf(context, r))
case None =>
Result.internalError(s"No mapping for field '$fieldName' of type ${context.tpe}")
case Some(_: SqlObject) =>
Result.internalError(s"Non-leaf mapping for field '$fieldName' of type ${context.tpe}")
case _ =>
Nil.success
}
/** Returns the aliased columns corresponding to `term` in `context` */
def columnForSqlTerm[T](context: Context, term: Term[T]): Result[SqlColumn] =
term match {
case termPath: PathTerm =>
context.forPath(termPath.path.init).flatMap { parentContext =>
columnForAtomicField(parentContext, termPath.path.last)
}
case SqlColumnTerm(col) => col.success
case _ => Result.internalError(s"No column for term $term in context $context")
}
/** Returns the aliased column corresponding to the atomic field `fieldName` in `context` */
def columnForAtomicField(context: Context, fieldName: String): Result[SqlColumn] = {
typeMappings.fieldMapping(context, fieldName) match {
case Some(SqlField(_, cr, _, _, _, _)) => SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath).success
case Some(SqlJson(_, cr)) => SqlColumn.TableColumn(context, cr, fieldName :: context.resultPath).success
case _ => Result.internalError(s"No column for atomic field '$fieldName' in context $context")
}
}
/** Returns the `Encoder` for the given term in `context` */
def encoderForTerm(context: Context, term: Term[_]): Result[Encoder] =
term match {
case pathTerm: PathTerm =>
for {
cr <- columnForSqlTerm(context, pathTerm) // encoder is independent of query aliases
} yield toEncoder(cr.codec)
case SqlColumnTerm(col) => toEncoder(col.codec).success
case (_: And)|(_: Or)|(_: Not)|(_: Eql[_])|(_: NEql[_])|(_: Lt[_])|(_: LtEql[_])|(_: Gt[_])|(_: GtEql[_]) => booleanEncoder.success
case (_: AndB)|(_: OrB)|(_: XorB)|(_: NotB) => intEncoder.success
case (_: ToUpperCase)|(_: ToLowerCase) => stringEncoder.success
case _ => Result.internalError(s"No encoder for term $term in context $context")
}
/** Returns the discriminator for the type at `context` */
def discriminatorForType(context: Context): Option[SqlDiscriminatedType] =
typeMappings.objectMapping(context) collect {
//case d: SqlDiscriminatedType => d // Fails in 2.13.6 due to https://github.com/scala/bug/issues/12398
case i: SqlInterfaceMapping => i
case u: SqlUnionMapping => u
}
/** Returns the table for the type at `context` */
def parentTableForType(context: Context): Result[TableRef] = {
def noTable = s"No table for type ${context.tpe}"
typeMappings.objectMapping(context).toResultOrError(noTable).flatMap { om =>
om.fieldMappings.collectFirst { case SqlField(_, cr, _, _, _, _) => TableRef(context, cr.table) }.toResultOrError(noTable).orElse {
context.tpe.underlyingObject match {
case Some(ot: ObjectType) =>
ot.interfaces.collectFirstSome(nt => parentTableForType(context.asType(nt)).toOption).toResultOrError(noTable)
case _ => Result.internalError(noTable)
}
}
}
}
/** Is `fieldName` in `context` Jsonb? */
def isJsonb(context: Context, fieldName: String): Boolean =
typeMappings.fieldMapping(context, fieldName) match {
case Some(_: SqlJson) => true
case Some(_: CursorFieldJson) => true
case _ => false
}
/** Is `fieldName` in `context` computed? */
def isComputedField(context: Context, fieldName: String): Boolean =
typeMappings.fieldMapping(context, fieldName) match {
case Some(_: CursorField[_]) => true
case _ => false
}
/** Is `term` in `context`expressible in SQL? */
def isSqlTerm(context: Context, term: Term[_]): Result[Boolean] =
term.forallR {
case termPath: PathTerm =>
context.forPath(termPath.path.init).map { parentContext =>
!isComputedField(parentContext, termPath.path.last)
}
case True | False | _: Const[_] | _: And | _: Or | _: Not | _: Eql[_] | _: NEql[_] | _: Contains[_] | _: Lt[_] | _: LtEql[_] | _: Gt[_] |
_: GtEql[_] | _: In[_] | _: AndB | _: OrB | _: XorB | _: NotB | _: Matches | _: StartsWith | _: IsNull[_] |
_: ToUpperCase | _: ToLowerCase | _: Like | _: SqlColumnTerm => true.success
case _ => false.success
}
/** Is the context type mapped to an associative table? */
def isAssociative(context: Context): Boolean =
typeMappings.objectMapping(context).exists(_.fieldMappings.exists {
case sf: SqlField => sf.associative
case _ => false
})
/** Does the type of `fieldName` in `context` represent a list of subobjects? */
def nonLeafList(context: Context, fieldName: String): Boolean =
context.tpe.underlyingField(fieldName).exists { fieldTpe =>
fieldTpe.nonNull.isList && (
typeMappings.fieldMapping(context, fieldName).exists {
case SqlObject(_, joins) => joins.nonEmpty
case _ => false
}
)
}
/** Does the supplied field correspond to a single, possibly structured, value? */
def isSingular(context: Context, fieldName: String, query: Query): Boolean = {
def loop(query: Query): Boolean =
query match {
case Limit(n, _) => n <= 1
case _: Unique => true
case Filter(_, child) => loop(child)
case Offset(_, child) => loop(child)
case OrderBy(_, child) => loop(child)
case _ => false
}
!nonLeafList(context, fieldName) || loop(query)
}
/** Representation of a table expression */
sealed trait TableExpr extends ColumnOwner {
/** The name of this `TableExpr` */
def name: String
/** Is the supplied column an immediate component of this `TableExpr`? */
def directlyOwns(col: SqlColumn): Boolean = this == col.owner
/** Find the innermost owner of the supplied column within this `TableExpr` */
def findNamedOwner(col: SqlColumn): Option[TableExpr]
/** Render a defining occurence of this `TableExpr` */
def toDefFragment: Aliased[Fragment]
/** Render a reference to this `TableExpr` */
def toRefFragment: Aliased[Fragment]
/** Is this `TableRef` an anoymous root */
def isRoot: Boolean
/** Is this `TableExpr` backed by an SQL union
*
* This is used to determine whether or not non-nullable columns should be weakened
* to being nullable when fetched
*/
def isUnion: Boolean
/** Yields a copy of this `TableExpr` with all occurences of `from` replaced by `to` */
def subst(from: TableExpr, to: TableExpr): TableExpr
}
object TableExpr {
/** Table expression corresponding to a possibly aliased table */
case class TableRef(context: Context, name: String) extends TableExpr {
def owns(col: SqlColumn): Boolean = isSameOwner(col.owner)
def contains(other: ColumnOwner): Boolean = isSameOwner(other)
def findNamedOwner(col: SqlColumn): Option[TableExpr] =
if (this == col.owner) Some(this) else None
def isRoot: Boolean = TableName.isRoot(name)
def isUnion: Boolean = false
def subst(from: TableExpr, to: TableExpr): TableRef = this
def toDefFragment: Aliased[Fragment] =
for {
alias <- Aliased.tableDef(this)
} yield
if (name == alias)
Fragments.const(name)
else
Fragments.const(s"$name AS $alias")
def toRefFragment: Aliased[Fragment] =
Aliased.tableRef(this).map(Fragments.const)
override def toString: String = name
}
/** Table expression corresponding to a subquery */
case class SubqueryRef(context: Context, name: String, subquery: SqlQuery, lateral: Boolean) extends TableExpr {
def owns(col: SqlColumn): Boolean = col.owner.isSameOwner(this) || subquery.owns(col)
def contains(other: ColumnOwner): Boolean = isSameOwner(other) || subquery.contains(other)
def findNamedOwner(col: SqlColumn): Option[TableExpr] =
if (this == col.owner) Some(this) else subquery.findNamedOwner(col)
def isRoot: Boolean = false
def isUnion: Boolean = subquery.isUnion
def subst(from: TableExpr, to: TableExpr): SubqueryRef =
copy(subquery = subquery.subst(from, to))
def toDefFragment: Aliased[Fragment] = {
val lateral0 = if(lateral) Fragments.const("LATERAL ") else Fragments.empty
for {
alias <- Aliased.tableDef(this)
sub <- subquery.toFragment
} yield lateral0 |+| Fragments.parentheses(sub) |+| Fragments.const(s" AS $alias")
}
def toRefFragment: Aliased[Fragment] =
Aliased.tableRef(this).map(Fragments.const)
}
/** Table expression corresponding to a common table expression */
case class WithRef(context: Context, name: String, withQuery: SqlQuery) extends TableExpr {
def owns(col: SqlColumn): Boolean = col.owner.isSameOwner(this) || withQuery.owns(col)
def contains(other: ColumnOwner): Boolean = isSameOwner(other) || withQuery.contains(other)
def findNamedOwner(col: SqlColumn): Option[TableExpr] =
if (this == col.owner) Some(this) else withQuery.findNamedOwner(col)
def isRoot: Boolean = false
def isUnion: Boolean = withQuery.isUnion
def subst(from: TableExpr, to: TableExpr): WithRef =
copy(withQuery = withQuery.subst(from, to))
def toDefFragment: Aliased[Fragment] =
for {
with0 <- withQuery.toFragment
} yield Fragments.const(s" $name AS ") |+| Fragments.parentheses(with0)
def toRefFragment: Aliased[Fragment] =
Aliased.pure(Fragments.const(s"$name"))
}
/** Table expression derived from the given `TableExpr`.
*
* Typically used where we need to refer to a table defined in a subquery or
* common table expression.
*/
case class DerivedTableRef(context: Context, alias: Option[String], underlying: TableExpr, noalias: Boolean = false) extends TableExpr {
assert(!underlying.isInstanceOf[WithRef] || noalias)
def name = alias.getOrElse(underlying.name)
def owns(col: SqlColumn): Boolean = col.owner.isSameOwner(this) || underlying.owns(col)
def contains(other: ColumnOwner): Boolean = isSameOwner(other) || underlying.contains(other)
def findNamedOwner(col: SqlColumn): Option[TableExpr] =
if (this == col.owner) Some(this) else underlying.findNamedOwner(col)
def isRoot: Boolean = underlying.isRoot
def isUnion: Boolean = underlying.isUnion
def subst(from: TableExpr, to: TableExpr): DerivedTableRef =
if(underlying == from) copy(underlying = to)
else copy(underlying = underlying.subst(from, to))
def toDefFragment: Aliased[Fragment] = {
for {
uname <- if (noalias) Aliased.pure(underlying.name) else Aliased.tableDef(underlying)
name <- Aliased.tableDef(this)
} yield {
if (name == uname)
Fragments.const(s"$name")
else
Fragments.const(s"$uname AS $name")
}
}
def toRefFragment: Aliased[Fragment] =
Aliased.tableRef(this).map(Fragments.const)
}
}
/** Representation of a SQL query in a context */
sealed trait SqlQuery extends ColumnOwner {
/** The context for this query */
def context: Context
/** This query in the given context */
def withContext(context: Context, extraCols: List[SqlColumn], extraJoins: List[SqlJoin]): Result[SqlQuery]
/** The columns of this query */
def cols: List[SqlColumn]
/** The codecs corresponding to the columns of this query */
def codecs: List[(Boolean, Codec)]
/** Yields a copy of this query with all occurences of `from` replaced by `to` */
def subst(from: TableExpr, to: TableExpr): SqlQuery
/** Nest this query as a subobject in the enclosing `parentContext` */
def nest(
parentContext: Context,
extraCols: List[SqlColumn],
oneToOne: Boolean,
lateral: Boolean
): Result[SqlQuery]
/** Add WHERE, ORDER BY, OFFSET and LIMIT to this query */
def addFilterOrderByOffsetLimit(
filter: Option[(Predicate, List[SqlJoin])],
orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])],
offset: Option[Int],
limit: Option[Int],
predIsOneToOne: Boolean,
parentConstraints: List[List[(SqlColumn, SqlColumn)]]
): Result[SqlQuery]
/** Yields an equivalent query encapsulating this query as a subquery */
def toSubquery(name: String, lateral: Boolean): Result[SqlSelect]
/** Yields a collection of `SqlSelects` which when combined as a union are equivalent to this query */
def asSelects: List[SqlSelect] =
this match {
case ss: SqlSelect => ss :: Nil
case su: SqlUnion => su.elems
case _: EmptySqlQuery => Nil
}
/** Is this query an SQL Union */
def isUnion: Boolean
/** Does one row of this query correspond to exactly one complete GraphQL value */
def oneToOne: Boolean
/** Render this query as a `Fragment` */
def toFragment: Aliased[Fragment]
}
object SqlQuery {
/** Combine the given queries as a single SQL query */
def combineAll(queries: List[SqlQuery]): Result[SqlQuery] = {
if(queries.sizeCompare(1) <= 0) queries.headOption.toResultOrError("Expected at least one query in combineAll")
else {
val (selects, unions) =
queries.partitionMap {
case s: SqlSelect => Left(s)
case u: SqlUnion => Right(u.elems)
case _: EmptySqlQuery => Right(Nil)
}
def combineSelects(sels: List[SqlSelect]): SqlSelect = {
val fst = sels.head
val withs = sels.flatMap(_.withs).distinct
val cols = sels.flatMap(_.cols).distinct
val joins = sels.flatMap(_.joins).distinct
val wheres = sels.flatMap(_.wheres).distinct
fst.copy(withs = withs, cols = cols, joins = joins, wheres = wheres)
}
val unionSelects = unions.flatten.distinct
val allSelects = selects ++ unionSelects
val ctx = allSelects.head.context
assert(allSelects.forall(sel => sel.context == ctx && sel.limit.isEmpty && sel.offset.isEmpty && sel.orders.isEmpty && !sel.isDistinct))
def combineCompatible(sels: List[SqlSelect]): List[SqlSelect] = {
val (oneToOneSelects, multiRowSelects) = sels.partition(_.oneToOne)
(multiRowSelects, oneToOneSelects) match {
case (Nil, Nil) => Nil
case (Nil, sel :: Nil) => sel :: Nil
case (Nil, sels) => combineSelects(sels) :: Nil
case (psels, Nil) => psels
case (psels, sels) =>
psels.map(psel => combineSelects(psel :: sels))
}
}
val combinedSelects = selects.groupBy(sel => sel.table).values.flatMap(combineCompatible).toList
(combinedSelects ++ unionSelects) match {
case Nil => Result.internalError("Expected at least one select in combineAll")
case sel :: Nil => sel.success
case sels => SqlUnion(sels).success
}
}
}
// TODO: This should be handled in combineAll
def combineRootNodes(context: Context, nodes: List[SqlQuery]): Result[SqlQuery] = {
val (selects, unions) =
nodes.partitionMap {
case s: SqlSelect => Left(s)
case u: SqlUnion => Right(u.elems)
case _: EmptySqlQuery => Right(Nil)
}
val unionSelects = unions.flatten
val allSelects = (selects ++ unionSelects).distinct
(allSelects match {
case Nil => EmptySqlQuery(context)
case List(sel) => sel
case sels => SqlUnion(sels)
}).success
}
/** Compute the set of paths traversed by the given prediate */
def wherePaths(pred: Predicate): List[List[String]] = {
def loop(term: Term[_], acc: List[List[String]]): List[List[String]] = {
term match {
case Const(_) => acc
case pathTerm: PathTerm => pathTerm.path :: acc
case And(x, y) => loop(y, loop(x, acc))
case Or(x, y) => loop(y, loop(x, acc))
case Not(x) => loop(x, acc)
case Eql(x, y) => loop(y, loop(x, acc))
case NEql(x, y) => loop(y, loop(x, acc))
case Contains(x, y) => loop(y, loop(x, acc))
case Lt(x, y) => loop(y, loop(x, acc))
case LtEql(x, y) => loop(y, loop(x, acc))
case Gt(x, y) => loop(y, loop(x, acc))
case GtEql(x, y) => loop(y, loop(x, acc))
case IsNull(x, _) => loop(x, acc)
case In(x, _) => loop(x, acc)
case AndB(x, y) => loop(y, loop(x, acc))
case OrB(x, y) => loop(y, loop(x, acc))
case XorB(x, y) => loop(y, loop(x, acc))
case NotB(x) => loop(x, acc)
case Matches(x, _) => loop(x, acc)
case StartsWith(x, _) => loop(x, acc)
case ToUpperCase(x) => loop(x, acc)
case ToLowerCase(x) => loop(x, acc)
case Like(x, _, _) => loop(x, acc)
case _ => acc
}
}
loop(pred, Nil)
}
/** Compute the set of columns referred to by the given prediate */
def whereCols(f: Term[_] => SqlColumn, pred: Predicate): List[SqlColumn] = {
def loop[T](term: T): List[SqlColumn] =
term match {
case _: PathTerm => f(term.asInstanceOf[Term[_]]) :: Nil
case _: SqlColumnTerm => f(term.asInstanceOf[Term[_]]) :: Nil
case Const(_) => Nil
case And(x, y) => loop(x) ++ loop(y)
case Or(x, y) => loop(x) ++ loop(y)
case Not(x) => loop(x)
case Eql(x, y) => loop(x) ++ loop(y)
case NEql(x, y) => loop(x) ++ loop(y)
case Contains(x, y) => loop(x) ++ loop(y)
case Lt(x, y) => loop(x) ++ loop(y)
case LtEql(x, y) => loop(x) ++ loop(y)
case Gt(x, y) => loop(x) ++ loop(y)
case GtEql(x, y) => loop(x) ++ loop(y)
case IsNull(x, _) => loop(x)
case In(x, _) => loop(x)
case AndB(x, y) => loop(x) ++ loop(y)
case OrB(x, y) => loop(x) ++ loop(y)
case XorB(x, y) => loop(x) ++ loop(y)
case NotB(x) => loop(x)
case Matches(x, _) => loop(x)
case StartsWith(x, _) => loop(x)
case ToUpperCase(x) => loop(x)
case ToLowerCase(x) => loop(x)
case Like(x, _, _) => loop(x)
case _ => Nil
}
loop(pred)
}
/** Contextualise all terms in the given `Predicate` to the given context and owner */
def contextualiseWhereTerms(context: Context, owner: ColumnOwner, pred: Predicate): Result[Predicate] = {
def contextualise(term: Term[_]): SqlColumn =
contextualiseTerm(context, owner, term) match {
case Result.Success(col) => col
case Result.Warning(_, col) => col
case Result.Failure(_) => throw new SqlMappingException(s"Failed to contextualise term $term")
case Result.InternalError(err) => throw err
}
def loop[T](term: T): T =
(term match {
case _: PathTerm => SqlColumnTerm(contextualise(term.asInstanceOf[Term[_]]))
case _: SqlColumnTerm => SqlColumnTerm(contextualise(term.asInstanceOf[Term[_]]))
case Const(_) => term
case And(x, y) => And(loop(x), loop(y))
case Or(x, y) => Or(loop(x), loop(y))
case Not(x) => Not(loop(x))
case e@Eql(x, y) => e.subst(loop(x), loop(y))
case n@NEql(x, y) => n.subst(loop(x), loop(y))
case c@Contains(x, y) => c.subst(loop(x), loop(y))
case l@Lt(x, y) => l.subst(loop(x), loop(y))
case l@LtEql(x, y) => l.subst(loop(x), loop(y))
case g@Gt(x, y) => g.subst(loop(x), loop(y))
case g@GtEql(x, y) => g.subst(loop(x), loop(y))
case IsNull(x, y) => IsNull(loop(x), y)
case i@In(x, _) => i.subst(loop(x))
case AndB(x, y) => AndB(loop(x), loop(y))
case OrB(x, y) => OrB(loop(x), loop(y))
case XorB(x, y) => XorB(loop(x), loop(y))
case NotB(x) => NotB(loop(x))
case Matches(x, y) => Matches(loop(x), y)
case StartsWith(x, y) => StartsWith(loop(x), y)
case ToUpperCase(x) => ToUpperCase(loop(x))
case ToLowerCase(x) => ToLowerCase(loop(x))
case Like(x, y, z) => Like(loop(x), y, z)
case _ => term
}).asInstanceOf[T]
Result.catchNonFatal {
loop(pred)
}
}
/** Embed all terms in the given `Predicate` in the given table and parent table */
def embedWhereTerms(table: TableExpr, parentTable: TableRef, pred: Predicate): Result[Predicate] = {
def embed(term: Term[_]): SqlColumn =
embedTerm(table, parentTable, term) match {
case Result.Success(col) => col
case Result.Warning(_, col) => col
case Result.Failure(_) => throw new SqlMappingException(s"Failed to embed term $term")
case Result.InternalError(err) => throw err
}
def loop[T](term: T): T =
(term match {
case _: PathTerm => SqlColumnTerm(embed(term.asInstanceOf[Term[_]]))
case _: SqlColumnTerm => SqlColumnTerm(embed(term.asInstanceOf[Term[_]]))
case Const(_) => term
case And(x, y) => And(loop(x), loop(y))
case Or(x, y) => Or(loop(x), loop(y))
case Not(x) => Not(loop(x))
case e@Eql(x, y) => e.subst(loop(x), loop(y))
case n@NEql(x, y) => n.subst(loop(x), loop(y))
case c@Contains(x, y) => c.subst(loop(x), loop(y))
case l@Lt(x, y) => l.subst(loop(x), loop(y))
case l@LtEql(x, y) => l.subst(loop(x), loop(y))
case g@Gt(x, y) => g.subst(loop(x), loop(y))
case g@GtEql(x, y) => g.subst(loop(x), loop(y))
case IsNull(x, y) => IsNull(loop(x), y)
case i@In(x, _) => i.subst(loop(x))
case AndB(x, y) => AndB(loop(x), loop(y))
case OrB(x, y) => OrB(loop(x), loop(y))
case XorB(x, y) => XorB(loop(x), loop(y))
case NotB(x) => NotB(loop(x))
case Matches(x, y) => Matches(loop(x), y)
case StartsWith(x, y) => StartsWith(loop(x), y)
case ToUpperCase(x) => ToUpperCase(loop(x))
case ToLowerCase(x) => ToLowerCase(loop(x))
case Like(x, y, z) => Like(loop(x), y, z)
case _ => term
}).asInstanceOf[T]
Result.catchNonFatal {
loop(pred)
}
}
/** Yields a copy of the given `Predicate` with all occurences of `from` replaced by `to` */
def substWhereTables(from: TableExpr, to: TableExpr, pred: Predicate): Predicate = {
def loop[T](term: T): T =
(term match {
case SqlColumnTerm(col) => SqlColumnTerm(col.subst(from, to))
case _: PathTerm => term
case Const(_) => term
case And(x, y) => And(loop(x), loop(y))
case Or(x, y) => Or(loop(x), loop(y))
case Not(x) => Not(loop(x))
case e@Eql(x, y) => e.subst(loop(x), loop(y))
case n@NEql(x, y) => n.subst(loop(x), loop(y))
case c@Contains(x, y) => c.subst(loop(x), loop(y))
case l@Lt(x, y) => l.subst(loop(x), loop(y))
case l@LtEql(x, y) => l.subst(loop(x), loop(y))
case g@Gt(x, y) => g.subst(loop(x), loop(y))
case g@GtEql(x, y) => g.subst(loop(x), loop(y))
case IsNull(x, y) => IsNull(loop(x), y)
case i@In(x, _) => i.subst(loop(x))
case AndB(x, y) => AndB(loop(x), loop(y))
case OrB(x, y) => OrB(loop(x), loop(y))
case XorB(x, y) => XorB(loop(x), loop(y))
case NotB(x) => NotB(loop(x))
case Matches(x, y) => Matches(loop(x), y)
case StartsWith(x, y) => StartsWith(loop(x), y)
case ToUpperCase(x) => ToUpperCase(loop(x))
case ToLowerCase(x) => ToLowerCase(loop(x))
case Like(x, y, z) => Like(loop(x), y, z)
case _ => term
}).asInstanceOf[T]
loop(pred)
}
/** Render the given `Predicate` as a `Fragment` representing a where clause conjunct */
def whereToFragment(context: Context, pred: Predicate): Aliased[Fragment] = {
def encoder1(enc: Option[Encoder], x: Term[_]): Encoder =
enc.getOrElse(encoderForTerm(context, x).getOrElse(throw new SqlMappingException(s"No encoder for term $x")))
def encoder2(enc: Option[Encoder], x: Term[_], y: Term[_]): Encoder =
enc.getOrElse(encoderForTerm(context, x).getOrElse(encoderForTerm(context, y).getOrElse(throw new SqlMappingException(s"No encoder for terms $x or $y"))))
def loop(term: Term[_], e: Encoder): Aliased[Fragment] = {
def unaryOp(x: Term[_])(op: Fragment, enc: Option[Encoder]): Aliased[Fragment] = {
val e = encoder1(enc, x)
for {
fx <- loop(x, e)
} yield op |+| fx
}
def binaryOp(x: Term[_], y: Term[_])(op: Fragment, enc: Option[Encoder] = None): Aliased[Fragment] = {
val e = encoder2(enc, x, y)
for {
fx <- loop(x, e)
fy <- loop(y, e)
} yield Fragments.const("(") |+| fx |+| op |+| fy |+| Fragments.const(")")
}
def binaryOp2(x: Term[_])(op: Fragment => Fragment, enc: Option[Encoder] = None): Aliased[Fragment] = {
val e = encoder1(enc, x)
for {
fx <- loop(x, e)
} yield op(fx)
}
term match {
case Const(value) =>
Aliased.pure(Fragments.bind(e, value))
case SqlColumnTerm(col) =>
col.toRefFragment(false)
case pathTerm: PathTerm =>
throw new SqlMappingException(s"Unresolved term $pathTerm in WHERE clause")
case True =>
Aliased.pure(Fragments.const("true"))
case False =>
Aliased.pure(Fragments.const("false"))
case And(x, y) =>
binaryOp(x, y)(Fragments.const(" AND "), Some(booleanEncoder))
case Or(x, y) =>
binaryOp(x, y)(Fragments.const(" OR "), Some(booleanEncoder))
case Not(x) =>
unaryOp(x)(Fragments.const(" NOT "), Some(booleanEncoder))
case Eql(x, y) =>
binaryOp(x, y)(Fragments.const(" = "))
case Contains(x, y) =>
binaryOp(x, y)(Fragments.const(" = "))
case NEql(x, y) =>
binaryOp(x, y)(Fragments.const(" != "))
case Lt(x, y) =>
binaryOp(x, y)(Fragments.const(" < "))
case LtEql(x, y) =>
binaryOp(x, y)(Fragments.const(" <= "))
case Gt(x, y) =>
binaryOp(x, y)(Fragments.const(" > "))
case GtEql(x, y) =>
binaryOp(x, y)(Fragments.const(" >= "))
case In(x, y) =>
val e = encoder1(None, x)
NonEmptyList.fromList(y) match {
case Some(ys) =>
binaryOp2(x)(fx => Fragments.in(fx, ys, e))
case None =>
Aliased.pure(Fragments.const("false"))
}
case AndB(x, y) =>
binaryOp(x, y)(Fragments.const(" & "), Some(intEncoder))
case OrB(x, y) =>
binaryOp(x, y)(Fragments.const(" | "), Some(intEncoder))
case XorB(x, y) =>
binaryOp(x, y)(Fragments.const(" # "), Some(intEncoder))
case NotB(x) =>
unaryOp(x)(Fragments.const(" NOT "), Some(intEncoder))
case Matches(x, regex) =>
binaryOp2(x)(
fx =>
Fragments.const("regexp_matches(") |+|
fx |+| Fragments.const(s", ") |+| Fragments.bind(stringEncoder, regex.toString) |+|
Fragments.const(s")"),
Some(stringEncoder)
)
case StartsWith(x, prefix) =>
binaryOp2(x)(
fx =>
fx |+| Fragments.const(s" LIKE ") |+| Fragments.bind(stringEncoder, prefix + "%"),
Some(stringEncoder)
)
case ToUpperCase(x) =>
binaryOp2(x)(
fx =>
Fragments.const("upper(") |+| fx |+| Fragments.const(s")"),
Some(stringEncoder)
)
case ToLowerCase(x) =>
binaryOp2(x)(
fx =>
Fragments.const("lower(") |+| fx |+| Fragments.const(s")"),
Some(stringEncoder)
)
case IsNull(x, isNull) =>
val sense = if (isNull) "" else "NOT"
binaryOp2(x)(
fx =>
fx |+| Fragments.const(s" IS $sense NULL "),
)
case Like(x, pattern, caseInsensitive) =>
val op = if(caseInsensitive) " ILIKE " else " LIKE "
binaryOp2(x)(
fx =>
fx |+| Fragments.const(s" $op ") |+| Fragments.bind(stringEncoder, pattern),
Some(stringEncoder)
)
case other => throw new SqlMappingException(s"Unexpected term $other")
}
}
try {
loop(pred, booleanEncoder)
} catch {
case NonFatal(e) => Aliased.internalError(e)
}
}
/** Render the given `Predicates` as a where clause `Fragment` */
def wheresToFragment(context: Context, wheres: List[Predicate]): Aliased[Fragment] =
wheres.traverse(pred => whereToFragment(context, pred)).map(fwheres => Fragments.const(" ") |+| Fragments.whereAnd(fwheres: _*))
/** Contextualise all terms in the given `OrderSelection` to the given context and owner */
def contextualiseOrderTerms[T](context: Context, owner: ColumnOwner, os: OrderSelection[T]): Result[OrderSelection[T]] =
contextualiseTerm(context, owner, os.term).map { col => os.subst(SqlColumnTerm(col).asInstanceOf[Term[T]]) }
def embedOrderTerms[T](table: TableExpr, parentTable: TableRef, os: OrderSelection[T]): Result[OrderSelection[T]] =
embedTerm(table, parentTable, os.term).map { col => os.subst(SqlColumnTerm(col).asInstanceOf[Term[T]]) }
/** Yields a copy of the given `OrderSelection` with all occurences of `from` replaced by `to` */
def substOrderTables[T](from: TableExpr, to: TableExpr, os: OrderSelection[T]): OrderSelection[T] =
os.term match {
case SqlColumnTerm(col) => os.subst(SqlColumnTerm(col.subst(from, to)))
case _ => os
}
/** Render the given `OrderSelections` as a `Fragment` */
def ordersToFragment(orders: List[OrderSelection[_]]): Aliased[Fragment] =
if (orders.isEmpty) Aliased.pure(Fragments.empty)
else
orders.traverse {
case OrderSelection(term, ascending, nullsLast) =>
term match {
case SqlColumnTerm(col) =>
val dir = if(ascending) "" else " DESC"
val nulls = s" NULLS ${if(nullsLast) "LAST" else "FIRST"}"
val res =
for {
fc <- col.toRefFragment(Fragments.needsCollation(col.codec))
} yield {
fc |+| Fragments.const(s"$dir$nulls")
}
res
case other => Aliased.internalError[Fragment](s"Unresolved term $other in ORDER BY")
}
}.map(forders => Fragments.const(" ORDER BY ") |+| forders.intercalate(Fragments.const(",")))
def isEmbeddedIn(inner: Context, outer: Context): Boolean = {
def directlyEmbedded(child: Context, parent: Context): Boolean =
typeMappings.fieldMapping(parent, child.path.head) match {
case Some(_: CursorFieldJson) | Some(SqlObject(_, Nil)) => true
case _ => false
}
@tailrec
def loop(inner: Context, outer: Context): Boolean =
if(inner.path.tail == outer.path) directlyEmbedded(inner, outer)
else
inner.parent match {
case Some(parent) => directlyEmbedded(inner, parent) && loop(parent, outer)
case _ => false
}
if(!inner.path.endsWith(outer.path) || inner.path.sizeCompare(outer.path) == 0) false
else loop(inner, outer)
}
/** Yield a copy of the given `Term` with all referenced `SqlColumns` relativised to the given
* context and owned by by the given owner */
def contextualiseTerm(context: Context, owner: ColumnOwner, term: Term[_]): Result[SqlColumn] = {
def subst(col: SqlColumn): SqlColumn =
if(!owner.owns(col)) col
else col.derive(owner)
term match {
case SqlColumnTerm(col) => subst(col).success
case pathTerm: PathTerm =>
columnForSqlTerm(context, pathTerm).map { col =>
val col0 = subst(col)
if(isEmbeddedIn(col0.owner.context, owner.context)) SqlColumn.EmbeddedColumn(owner, col0)
else col0
}
case other =>
Result.internalError(s"Expected contextualisable term but found $other")
}
}
def embedColumn(table: TableExpr, parentTable: TableRef, col: SqlColumn): SqlColumn =
if(table.owns(col)) SqlColumn.EmbeddedColumn(parentTable, col)
else col
def embedTerm(table: TableExpr, parentTable: TableRef, term: Term[_]): Result[SqlColumn] = {
term match {
case SqlColumnTerm(col) => embedColumn(table, parentTable, col).success
case pathTerm: PathTerm =>
columnForSqlTerm(table.context, pathTerm).map(embedColumn(table, parentTable, _))
case other =>
Result.internalError(s"Expected embeddable term but found $other")
}
}
case class EmptySqlQuery(context: Context) extends SqlQuery {
def withContext(context: Context, extraCols: List[SqlColumn], extraJoins: List[SqlJoin]): Result[SqlQuery] =
if (extraCols.isEmpty && extraJoins.isEmpty) EmptySqlQuery(context).success
else mkSelect.flatMap(_.withContext(context, extraCols, extraJoins))
def contains(other: ColumnOwner): Boolean = false
def directlyOwns(col: SqlColumn): Boolean = false
def findNamedOwner(col: SqlColumn): Option[TableExpr] = None
def owns(col: SqlColumn): Boolean = false
def cols: List[SqlColumn] = Nil
def codecs: List[(Boolean, Codec)] = Nil
def subst(from: TableExpr, to: TableExpr): SqlQuery = this
def nest(parentContext: Context, extraCols: List[SqlColumn], oneToOne: Boolean, lateral: Boolean): Result[SqlQuery] =
if(extraCols.isEmpty) this.success
else mkSelect.flatMap(_.nest(parentContext, extraCols, oneToOne, lateral))
def addFilterOrderByOffsetLimit(
filter: Option[(Predicate, List[SqlJoin])],
orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])],
offset: Option[Int],
limit: Option[Int],
predIsOneToOne: Boolean,
parentConstraints: List[List[(SqlColumn, SqlColumn)]]
): Result[SqlQuery] =
mkSelect.flatMap(_.addFilterOrderByOffsetLimit(filter, orderBy, offset, limit, predIsOneToOne, parentConstraints))
def toSubquery(name: String, lateral: Boolean): Result[SqlSelect] =
mkSelect.flatMap(_.toSubquery(name, lateral))
def isUnion: Boolean = false
def oneToOne: Boolean = false
def toFragment: Aliased[Fragment] = Aliased.internalError("Attempt to render empty query as fragment")
def mkSelect: Result[SqlSelect] =
parentTableForType(context).map { parentTable =>
SqlSelect(context, Nil, parentTable, keyColumnsForType(context), Nil, Nil, Nil, None, None, Nil, false, false)
}
}
/** Representation of an SQL SELECT */
case class SqlSelect(
context: Context, // the GraphQL context of the query
withs: List[WithRef], // the common table expressions
table: TableExpr, // the table/subquery
cols: List[SqlColumn], // the requested columns
joins: List[SqlJoin], // joins for predicates/subobjects
wheres: List[Predicate],
orders: List[OrderSelection[_]],
offset: Option[Int],
limit: Option[Int],
distinct: List[SqlColumn], // columns this query is DISTINCT on
oneToOne: Boolean, // does one row represent exactly one complete GraphQL value
predicate: Boolean // does this SqlSelect represent a predicate
) extends SqlQuery {
assert(SqlJoin.checkOrdering(table, joins))
assert(cols.forall(owns0))
assert(cols.nonEmpty)
assert(cols.sizeCompare(cols.distinct) == 0)
assert(joins.sizeCompare(joins.distinct) == 0)
assert(distinct.diff(cols).isEmpty)
private def owns0(col: SqlColumn): Boolean =
isSameOwner(col.owner) || table.owns(col) || withs.exists(_.owns(col)) || joins.exists(_.owns(col))
/** This query in the given context */
def withContext(context: Context, extraCols: List[SqlColumn], extraJoins: List[SqlJoin]): Result[SqlSelect] =
copy(context = context, cols = (cols ++ extraCols).distinct, joins = extraJoins ++ joins).success
def isUnion: Boolean = table.isUnion
def isDistinct: Boolean = distinct.nonEmpty
override def isSameOwner(other: ColumnOwner): Boolean = other.isSameOwner(TableRef(context, table.name))
def owns(col: SqlColumn): Boolean = cols.contains(col) || owns0(col)
def contains(other: ColumnOwner): Boolean = isSameOwner(other) || table.contains(other) || joins.exists(_.contains(other)) || withs.exists(_.contains(other))
def directlyOwns(col: SqlColumn): Boolean =
(table match {
case tr: TableRef => tr.directlyOwns(col)
case _ => false
}) || joins.exists(_.directlyOwns(col)) || withs.exists(_.directlyOwns(col))
def findNamedOwner(col: SqlColumn): Option[TableExpr] =
table.findNamedOwner(col).orElse(joins.collectFirstSome(_.findNamedOwner(col))).orElse(withs.collectFirstSome(_.findNamedOwner(col)))
def codecs: List[(Boolean, Codec)] =
if (isUnion)
cols.map(col => (true, col.codec))
else {
def nullable(col: SqlColumn): Boolean =
!col.owner.isSameOwner(table)
cols.map(col => (nullable(col), col.codec))
}
/** The columns, if any, on which this select is ordered */
lazy val orderCols: Result[List[SqlColumn]] = orders.traverse(os => columnForSqlTerm(context, os.term)).map(_.distinct)
/** Does the given column need collation? */
def needsCollation(col: SqlColumn): Result[Boolean] = {
if(Fragments.needsCollation(col.codec)) orderCols.map(_.contains(col))
else false.success
}
/** Yield a name for this select derived from any names associated with its
* from clauses or joins
*/
def syntheticName(suffix: String): String = {
val joinNames = joins.map(_.child.name)
(table.name :: joinNames).mkString("_").take(50-suffix.length)+suffix
}
/** Yields a copy of this select with all occurences of `from` replaced by `to` */
def subst(from: TableExpr, to: TableExpr): SqlSelect = {
copy(
withs = withs.map(_.subst(from, to)),
table = if(table.isSameOwner(from)) to else table.subst(from, to),
cols = cols.map(_.subst(from, to)),
joins = joins.map(_.subst(from, to)),
wheres = wheres.map(o => substWhereTables(from, to, o)),
orders = orders.map(o => substOrderTables(from, to, o)),
distinct = distinct.map(_.subst(from, to))
)
}
/** Nest this query as a subobject in the enclosing `parentContext` */
def nest(
parentContext: Context,
extraCols: List[SqlColumn],
oneToOne: Boolean,
lateral: Boolean
): Result[SqlSelect] = {
parentTableForType(parentContext).flatMap { parentTable =>
val inner = !context.tpe.isNullable && !context.tpe.isList
def mkSubquery(multiTable: Boolean, nested: SqlSelect, joinCols: List[SqlColumn], suffix: String): Result[SqlSelect] = {
def isMergeable: Boolean =
!multiTable && !nested.joins.exists(_.isPredicate) && nested.wheres.isEmpty && nested.orders.isEmpty && nested.offset.isEmpty && nested.limit.isEmpty && !nested.isDistinct
if(isMergeable) nested.success
else {
val exposeCols = nested.table match {
case _: TableRef => joinCols
case _ => Nil
}
nested.copy(cols = (exposeCols ++ nested.cols).distinct).toSubquery(syntheticName(suffix), lateral).map { base0 =>
base0.copy(cols = nested.cols.map(_.derive(base0.table)))
}
}
}
def mkJoins(joins0: List[Join], multiTable: Boolean): Result[SqlSelect] = {
val lastJoin = joins0.last
mkSubquery(multiTable, this, lastJoin.childCols(table), "_nested").map { base =>
val initialJoins =
joins0.init.map(_.toSqlJoin(parentContext, parentContext, inner))
val finalJoins = {
val parentTable = lastJoin.parentTable(parentContext)
if(!isAssociative(context)) {
val finalJoin = lastJoin.toSqlJoin(parentTable, base.table, inner)
finalJoin :: Nil
} else {
val assocTable = TableExpr.DerivedTableRef(context, Some(base.table.name+"_assoc"), base.table, true)
val assocJoin = lastJoin.toSqlJoin(parentTable, assocTable, inner)
val finalJoin =
SqlJoin(
assocTable,
base.table,
keyColumnsForType(context).map { key => (key.in(assocTable), key) },
false
)
List(assocJoin, finalJoin)
}
}
val allJoins = initialJoins ++ finalJoins ++ base.joins
SqlSelect(
context = parentContext,
withs = base.withs,
table = allJoins.head.parent,
cols = base.cols,
joins = allJoins,
wheres = base.wheres,
orders = Nil,
offset = None,
limit = None,
distinct = Nil,
oneToOne = oneToOne,
predicate = false
)
}
}
val fieldName = context.path.head
val nested =
typeMappings.fieldMapping(parentContext, fieldName) match {
case Some(_: CursorFieldJson) | Some(SqlObject(_, Nil)) =>
val embeddedCols = cols.map { col =>
if(table.owns(col)) SqlColumn.EmbeddedColumn(parentTable, col)
else col
}
val embeddedJoins = joins.map { join =>
if(join.parent.isSameOwner(table)) {
val newOn = join.on match {
case (p, c) :: tl => (SqlColumn.EmbeddedColumn(parentTable, p), c) :: tl
case _ => join.on
}
join.copy(parent = parentTable, on = newOn)
} else join
}
for {
embeddedWheres <- wheres.traverse(pred => embedWhereTerms(table, parentTable, pred))
embeddedOrders <- orders.traverse(os => embedOrderTerms(table, parentTable, os))
} yield
copy(
context = parentContext,
table = parentTable,
cols = embeddedCols,
wheres = embeddedWheres,
orders = embeddedOrders,
joins = embeddedJoins
)
case Some(SqlObject(_, single@(_ :: Nil))) =>
mkJoins(single, false)
case Some(SqlObject(_, firstJoin :: tail)) =>
for {
nested <- mkJoins(tail, true)
base <- mkSubquery(false, nested, firstJoin.childCols(nested.table), "_multi")
} yield {
val initialJoin = firstJoin.toSqlJoin(parentTable, base.table, inner)
SqlSelect(
context = parentContext,
withs = base.withs,
table = parentTable,
cols = base.cols,
joins = initialJoin :: base.joins,
wheres = base.wheres,
orders = Nil,
offset = None,
limit = None,
distinct = Nil,
oneToOne = oneToOne,
predicate = false
)
}
case _ =>
Result.internalError(s"Non-subobject mapping for field '$fieldName' of type ${parentContext.tpe}")
}
nested.map { nested0 =>
assert(cols.lengthCompare(nested0.cols) == 0)
nested0.copy(cols = (nested0.cols ++ extraCols).distinct)
}
}
}
/** Add WHERE, ORDER BY and LIMIT to this query */
def addFilterOrderByOffsetLimit(
filter: Option[(Predicate, List[SqlJoin])],
orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])],
offset0: Option[Int],
limit0: Option[Int],
predIsOneToOne: Boolean,
parentConstraints: List[List[(SqlColumn, SqlColumn)]]
): Result[SqlSelect] = {
assert(orders.isEmpty && offset.isEmpty && limit.isEmpty && !isDistinct)
assert(filter.isDefined || orderBy.isDefined || offset0.isDefined || limit0.isDefined)
assert(filter.forall(f => isSqlTerm(context, f._1).getOrElse(false)))
val keyCols = keyColumnsForType(context)
def mkPredSubquery(base0: SqlSelect, predQuery: SqlSelect): SqlSelect = {
val baseRef = base0.table
val predName = syntheticName("_pred")
val predSub = SubqueryRef(context, predName, predQuery, parentConstraints.nonEmpty)
val on = keyCols.map(key => (key.derive(baseRef), key.derive(predSub)))
val predJoin = SqlJoin(baseRef, predSub, on, true)
val joinCols = cols.filterNot(col => table.owns(col))
base0.copy(
table = baseRef,
cols = (base0.cols ++ joinCols).distinct,
joins = predJoin :: base0.joins,
wheres = Nil
)
}
def mkWindowPred(partitionTerm: Term[Int]): Predicate =
(offset0, limit0) match {
case (Some(off), Some(lim)) =>
And(GtEql(partitionTerm, Const(off)), LtEql(partitionTerm, Const(off+lim)))
case (None, Some(lim)) =>
LtEql(partitionTerm, Const(lim))
case (Some(off), None) =>
GtEql(partitionTerm, Const(off))
case (None, None) => True
}
val (pred, filterJoins) = filter.map { case (pred, joins) => (pred :: Nil, joins) }.getOrElse((Nil, Nil))
val pred0 = parentConstraints.flatMap(_.map {
case (p, c) => Eql(p.toTerm, c.toTerm)
}) ++ pred
val (oss, orderJoins) = orderBy.map { case (oss, joins) => (oss, joins) }.getOrElse((Nil, Nil))
val orderColsR =
oss.traverse { os =>
columnForSqlTerm(context, os.term).map { col =>
orderJoins.collectFirstSome(_.findNamedOwner(col)).map(owner => col.in(owner)).getOrElse(col.in(table))
}
}
orderColsR.flatMap { orderCols =>
val initialKeyOrder = orderCols.take(keyCols.size).diff(keyCols).isEmpty
val useWindow = parentConstraints.nonEmpty && (offset0.isDefined || limit0.isDefined)
if (useWindow) {
// Use window functions for offset and limit where we have a parent constraint ...
val partitionBy = parentConstraints.head.map(_._2)
if(oneToOne && predIsOneToOne) {
// Case 1) one row is one object in this context
pred0.traverse(p => contextualiseWhereTerms(context, table, p)).flatMap { pred1 =>
oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { oss0 =>
val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.toTerm, true, true))
val nonNullKeys = keyCols.map(col => IsNull(col.toTerm, false))
// We could use row_number in this case
val partitionCol = SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders)
val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map {
case (_, col) => findNamedOwner(col).map(owner => col.derive(owner)).getOrElse(col.derive(table))
}
val selWithRowItem =
SqlSelect(
context = context,
withs = withs,
table = table,
cols = (partitionCol :: exposeCols ++ cols ++ orderCols).distinct,
joins = (filterJoins ++ orderJoins ++ joins).distinct,
wheres = (pred1 ++ nonNullKeys ++ wheres).distinct,
orders = Nil,
offset = None,
limit = None,
distinct = distinct,
oneToOne = true,
predicate = true
)
val numberedName = syntheticName("_numbered")
val subWithRowItem = SubqueryRef(context, numberedName, selWithRowItem, true)
val partitionTerm = partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]]
val windowPred = mkWindowPred(partitionTerm)
val predQuery =
SqlSelect(
context = context,
withs = Nil,
table = subWithRowItem,
cols = (cols ++ orderCols).distinct.map(_.derive(subWithRowItem)),
joins = Nil,
wheres = windowPred :: Nil,
orders = Nil,
offset = None,
limit = None,
distinct = Nil,
oneToOne = true,
predicate = true
)
predQuery.success
}
}
} else if ((orderBy.isEmpty || initialKeyOrder) && predIsOneToOne) {
// Case 2a) No order, or key columns at the start of the order; simple predicate means we can elide a subquery
pred0.traverse(p => contextualiseWhereTerms(context, table, p)).flatMap { pred1 =>
oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { oss0 =>
val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.toTerm, true, true))
val nonNullKeys = keyCols.map(col => IsNull(col.toTerm, false))
val partitionCol = SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders)
val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map {
case (_, col) => findNamedOwner(col).map(owner => col.derive(owner)).getOrElse(col.derive(table))
}
val selWithRowItem =
SqlSelect(
context = context,
withs = withs,
table = table,
cols = (partitionCol :: exposeCols ++ cols ++ orderCols).distinct,
joins = joins,
wheres = (pred1 ++ nonNullKeys ++ wheres).distinct,
orders = Nil,
offset = None,
limit = None,
distinct = distinct,
oneToOne = oneToOne,
predicate = true
)
val numberedName = syntheticName("_numbered")
val subWithRowItem = SubqueryRef(context, numberedName, selWithRowItem, true)
val partitionTerm = partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]]
val windowPred = mkWindowPred(partitionTerm)
val predQuery =
SqlSelect(
context = context,
withs = Nil,
table = subWithRowItem,
cols = (cols ++ orderCols).distinct.map(_.derive(subWithRowItem)),
joins = Nil,
wheres = windowPred :: Nil,
orders = Nil,
offset = None,
limit = None,
distinct = Nil,
oneToOne = oneToOne,
predicate = true
)
predQuery.success
}
}
} else if (orderBy.isEmpty || initialKeyOrder) {
// Case 2b) No order, or key columns at the start of the order
val base0 = subqueryToWithQuery
val baseRef = base0.table
pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { pred1 =>
oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { oss0 =>
val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.derive(baseRef).toTerm, true, true))
val predCols = keyCols.map(_.derive(baseRef))
val nonNullKeys = predCols.map(col => IsNull(col.toTerm, false))
val partitionCol = SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders)
val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map {
case (_, col) => baseRef.findNamedOwner(col).map(owner => col.derive(owner)).getOrElse(col.derive(baseRef))
}
val selWithRowItem =
SqlSelect(
context = context,
withs = Nil,
table = baseRef,
cols = (partitionCol :: exposeCols ++ predCols).distinct,
joins = (filterJoins ++ orderJoins).distinct,
wheres = (pred1 ++ nonNullKeys ++ wheres).distinct,
orders = Nil,
offset = None,
limit = None,
distinct = Nil,
oneToOne = true,
predicate = true
)
val numberedName = syntheticName("_numbered")
val subWithRowItem = SubqueryRef(context, numberedName, selWithRowItem, true)
val partitionTerm = partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]]
val windowPred = mkWindowPred(partitionTerm)
val numberedPredCols = keyCols.map(_.derive(subWithRowItem))
val predQuery =
SqlSelect(
context = context,
withs = Nil,
table = subWithRowItem,
cols = numberedPredCols,
joins = Nil,
wheres = windowPred :: Nil,
orders = Nil,
offset = None,
limit = None,
distinct = Nil,
oneToOne = true,
predicate = true
)
mkPredSubquery(base0, predQuery).success
}
}
} else if (predIsOneToOne) {
// Case 3a) There is an order orthogonal to the key; simple predicate means we can elide a subquery
pred0.traverse(p => contextualiseWhereTerms(context, table, p)).flatMap { pred1 =>
oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { oss0 =>
oss0.filterA(os => columnForSqlTerm(context, os.term).map(keyCols.contains)).flatMap { nonKeyOrders =>
val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.toTerm, true, true))
val nonNullKeys = keyCols.map(col => IsNull(col.toTerm, false))
val distOrders = keyCols.map(col => OrderSelection(col.toTerm, true, true)) ++ nonKeyOrders
val partitionCol = SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders)
val distPartitionCol = SqlColumn.PartitionColumn(table, "row_item_dist", partitionBy ++ keyCols, distOrders)
val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map {
case (_, col) => findNamedOwner(col).map(owner => col.derive(owner)).getOrElse(col.derive(table))
}
val selWithRowItem =
SqlSelect(
context = context,
withs = withs,
table = table,
cols = (partitionCol :: distPartitionCol :: exposeCols ++ cols ++ orderCols).distinct,
joins = joins,
wheres = (pred1 ++ nonNullKeys ++ wheres).distinct,
orders = Nil,
offset = None,
limit = None,
distinct = distinct,
oneToOne = oneToOne,
predicate = true
)
val numberedName = syntheticName("_numbered")
val subWithRowItem = SubqueryRef(context, numberedName, selWithRowItem, true)
val partitionTerm = partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]]
val distPartitionTerm = distPartitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]]
val windowPred = And(mkWindowPred(partitionTerm), LtEql(distPartitionTerm, Const(1)))
val predQuery =
SqlSelect(
context = context,
withs = Nil,
table = subWithRowItem,
cols = (cols ++ orderCols).distinct.map(_.derive(subWithRowItem)),
joins = Nil,
wheres = windowPred :: Nil,
orders = Nil,
offset = None,
limit = None,
distinct = Nil,
oneToOne = oneToOne,
predicate = true
)
predQuery.success
}
}
}
} else {
// Case 3b) There is an order orthogonal to the key
val base0 = subqueryToWithQuery
val baseRef = base0.table
pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { pred1 =>
oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { oss0 =>
oss0.filterA(os => columnForSqlTerm(context, os.term).map(keyCols.contains)).flatMap { nonKeyOrders =>
val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.derive(baseRef).toTerm, true, true))
val predCols = keyCols.map(_.derive(baseRef))
val nonNullKeys = predCols.map(col => IsNull(col.toTerm, false))
val distOrders = keyCols.map(col => OrderSelection(col.derive(baseRef).toTerm, true, true)) ++ nonKeyOrders
val partitionCol = SqlColumn.PartitionColumn(table, "row_item", partitionBy, orders)
val distPartitionCol = SqlColumn.PartitionColumn(table, "row_item_dist", partitionBy ++ predCols, distOrders)
val exposeCols = parentConstraints.lastOption.getOrElse(Nil).map {
case (_, col) => baseRef.findNamedOwner(col).map(owner => col.derive(owner)).getOrElse(col.derive(baseRef))
}
val selWithRowItem =
SqlSelect(
context = context,
withs = Nil,
table = baseRef,
cols = (partitionCol :: distPartitionCol :: exposeCols ++ predCols).distinct,
joins = (filterJoins ++ orderJoins).distinct,
wheres = (pred1 ++ nonNullKeys ++ wheres).distinct,
orders = Nil,
offset = None,
limit = None,
distinct = Nil,
oneToOne = true,
predicate = true
)
val numberedName = syntheticName("_numbered")
val subWithRowItem = SubqueryRef(context, numberedName, selWithRowItem, true)
val partitionTerm = partitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]]
val distPartitionTerm = distPartitionCol.derive(subWithRowItem).toTerm.asInstanceOf[Term[Int]]
val windowPred = And(mkWindowPred(partitionTerm), LtEql(distPartitionTerm, Const(1)))
val numberedPredCols = keyCols.map(_.derive(subWithRowItem))
val predQuery =
SqlSelect(
context = context,
withs = Nil,
table = subWithRowItem,
cols = numberedPredCols,
joins = Nil,
wheres = windowPred :: Nil,
orders = Nil,
offset = None,
limit = None,
distinct = Nil,
oneToOne = true,
predicate = true
)
mkPredSubquery(base0, predQuery).success
}
}
}
}
} else {
// No parent constraint so nothing to be gained from using window functions
if((oneToOne && predIsOneToOne) || (offset0.isEmpty && limit0.isEmpty && filterJoins.isEmpty && orderJoins.isEmpty)) {
// Case 1) one row is one object or query is simple enough to not require subqueries
pred0.traverse(p => contextualiseWhereTerms(context, table, p)).flatMap { pred1 =>
oss.traverse(os => contextualiseOrderTerms(context, table, os)).flatMap { oss0 =>
val (nonNullKeys, keyOrder) =
offset0.orElse(limit0).map { _ =>
val nonNullKeys0 = keyCols.map(col => IsNull(col.toTerm, false))
val keyOrder0 = keyCols.diff(orderCols).map(col => OrderSelection(col.toTerm, true, true))
(nonNullKeys0, keyOrder0)
}.getOrElse((Nil, Nil))
val orders = oss0 ++ keyOrder
val predQuery =
SqlSelect(
context = context,
withs = withs,
table = table,
cols = cols,
joins = (filterJoins ++ orderJoins ++ joins).distinct,
wheres = (pred1 ++ nonNullKeys ++ wheres).distinct,
orders = orders,
offset = offset0,
limit = limit0,
distinct = distinct,
oneToOne = true,
predicate = true
)
predQuery.success
}
}
} else {
val base0 = subqueryToWithQuery
val baseRef = base0.table
val predCols = keyCols.map(_.derive(baseRef))
val nonNullKeys = predCols.map(col => IsNull(col.toTerm, false))
if (orderBy.isEmpty || initialKeyOrder) {
// Case 2) No order, or key columns at the start of the order
pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { pred1 =>
oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { oss0 =>
val orders = oss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.derive(baseRef).toTerm, true, true))
val predQuery = SqlSelect(
context = context,
withs = Nil,
table = baseRef,
cols = predCols,
joins = (filterJoins ++ orderJoins).distinct,
wheres = (pred1 ++ nonNullKeys ++ wheres).distinct,
orders = orders,
offset = offset0,
limit = limit0,
distinct = predCols,
oneToOne = true,
predicate = true
)
mkPredSubquery(base0, predQuery).success
}
}
} else {
// Case 3) There is an order orthogonal to the key
pred0.traverse(p => contextualiseWhereTerms(context, baseRef, p)).flatMap { pred1 =>
oss.traverse(os => contextualiseOrderTerms(context, baseRef, os)).flatMap { oss0 =>
oss0.filterA(os => columnForSqlTerm(context, os.term).map(keyCols.contains)).flatMap { nonKeyOrders =>
val distOrders = keyCols.map(col => OrderSelection(col.derive(baseRef).toTerm, true, true)) ++ nonKeyOrders
val distOrderCols = orderCols.diff(keyCols).map(_.derive(baseRef))
val distQuery = SqlSelect(
context = context,
withs = Nil,
table = baseRef,
cols = predCols ++ distOrderCols, // these two are individually distinct and also disjoint, hence no .distinct
joins = (filterJoins ++ orderJoins).distinct,
wheres = (pred1 ++ nonNullKeys ++ wheres).distinct,
orders = distOrders,
offset = None,
limit = None,
distinct = predCols,
oneToOne = true,
predicate = true
)
val distName = "dist"
val distSub = SubqueryRef(context, distName, distQuery, parentConstraints.nonEmpty)
val predCols0 = keyCols.map(_.derive(distSub))
oss.traverse(os => contextualiseOrderTerms(context, distSub, os)).flatMap { outerOss0 =>
val orders = outerOss0 ++ keyCols.diff(orderCols).map(col => OrderSelection(col.derive(distSub).toTerm, true, true))
val predQuery = SqlSelect(
context = context,
withs = Nil,
table = distSub,
cols = predCols0,
joins = Nil,
wheres = Nil,
orders = orders,
offset = offset0,
limit = limit0,
distinct = Nil,
oneToOne = true,
predicate = true
)
mkPredSubquery(base0, predQuery).success
}
}
}
}
}
}
}
}
}
/** Yields an equivalent query encapsulating this query as a subquery */
def toSubquery(name: String, lateral: Boolean): Result[SqlSelect] = {
val ref = SubqueryRef(context, name, this, lateral)
SqlSelect(context, Nil, ref, cols.map(_.derive(ref)), Nil, Nil, Nil, None, None, Nil, oneToOne, predicate).success
}
/** If the from clause of this query is a subquery, convert it to a
* common table expression
*/
def subqueryToWithQuery: SqlSelect = {
table match {
case SubqueryRef(_, name, sq, _) =>
val with0 = WithRef(context, name+"_base", sq)
val ref = TableExpr.DerivedTableRef(context, Some(name), with0, true)
copy(withs = with0 :: withs, table = ref)
case _ =>
this
}
}
/** Render this `SqlSelect` as a `Fragment` */
def toFragment: Aliased[Fragment] = {
for {
_ <- Aliased.pushOwner(this)
withs0 <- if (withs.isEmpty) Aliased.pure(Fragments.empty)
else withs.traverse(_.toDefFragment).map(fwiths => Fragments.const("WITH ") |+| fwiths.intercalate(Fragments.const(",")))
table0 <- table.toDefFragment
cols0 <- cols.traverse(col => Aliased.liftR(needsCollation(col)).flatMap(col.toDefFragment))
dcols <- distinct.traverse(col => Aliased.liftR(needsCollation(col)).flatMap(col.toRefFragment))
dist = if (dcols.isEmpty) Fragments.empty else Fragments.const("DISTINCT ON ") |+| Fragments.parentheses(dcols.intercalate(Fragments.const(", ")))
joins0 <- joins.traverse(_.toFragment).map(_.combineAll)
select = Fragments.const(s"SELECT ") |+| dist |+| cols0.intercalate(Fragments.const(", "))
from = if(table.isRoot) Fragments.empty else Fragments.const(" FROM ") |+| table0
where <- wheresToFragment(context, wheres)
orderBy <- ordersToFragment(orders)
off = offset.map(o => Fragments.const(s" OFFSET $o")).getOrElse(Fragments.empty)
lim = limit.map(l => Fragments.const(s" LIMIT $l")).getOrElse(Fragments.empty)
_ <- Aliased.popOwner
} yield
withs0 |+| select |+| from |+| joins0 |+| where |+| orderBy |+| off |+| lim
}
}
object SqlSelect {
def apply(
context: Context, // the GraphQL context of the query
withs: List[WithRef], // the common table expressions
table: TableExpr, // the table/subquery
cols: List[SqlColumn], // the requested columns
joins: List[SqlJoin], // joins for predicates/subobjects
wheres: List[Predicate],
orders: List[OrderSelection[_]],
offset: Option[Int],
limit: Option[Int],
distinct: List[SqlColumn], // columns this query is DISTINCT on
oneToOne: Boolean, // does one row represent exactly one complete GraphQL value
predicate: Boolean // does this SqlSelect represent a predicate
): SqlSelect =
new SqlSelect(
context,
withs,
table,
cols.sortBy(col => (col.owner.context.resultPath, col.column)),
joins,
wheres,
orders,
offset,
limit,
distinct,
oneToOne,
predicate
)
}
/** Representation of a UNION ALL of SQL SELECTs */
case class SqlUnion(elems: List[SqlSelect]) extends SqlQuery {
assert(elems.sizeCompare(2) >= 0)
def isUnion: Boolean = true
/** Does one row of this query correspond to exactly one complete GraphQL value */
def oneToOne: Boolean = elems.forall(_.oneToOne)
/** The context for this query */
val context = elems.head.context
val topLevel = context.path.sizeCompare(1) == 0
if (topLevel)
assert(elems.tail.forall(elem => elem.context.path.sizeCompare(1) == 0 || schema.isRootType(elem.context.tpe)))
else
assert(elems.tail.forall(elem => elem.context == context))
/** This query in the given context */
def withContext(context: Context, extraCols: List[SqlColumn], extraJoins: List[SqlJoin]): Result[SqlUnion] =
elems.traverse(_.withContext(context, extraCols, extraJoins)).map(SqlUnion(_))
def owns(col: SqlColumn): Boolean = cols.contains(col) || elems.exists(_.owns(col))
def contains(other: ColumnOwner): Boolean = isSameOwner(other) || elems.exists(_.contains(other))
def directlyOwns(col: SqlColumn): Boolean = owns(col)
def findNamedOwner(col: SqlColumn): Option[TableExpr] = None
override def isSameOwner(other: ColumnOwner): Boolean = other eq this
/** The union of the columns of the underlying SELECTs in the order they will be
* yielded as the columns of this UNION
*/
lazy val cols: List[SqlColumn] = elems.flatMap(_.cols).distinct
def codecs: List[(Boolean, Codec)] =
cols.map(col => (true, col.codec))
def subst(from: TableExpr, to: TableExpr): SqlUnion =
SqlUnion(elems.map(_.subst(from, to)))
def toSubquery(name: String, lateral: Boolean): Result[SqlSelect] = {
val sub = SubqueryRef(context, name, this, lateral)
SqlSelect(context, Nil, sub, cols.map(_.derive(sub)), Nil, Nil, Nil, None, None, Nil, false, false).success
}
/** Nest this query as a subobject in the enclosing `parentContext` */
def nest(
parentContext: Context,
extraCols: List[SqlColumn],
oneToOne: Boolean,
lateral: Boolean
): Result[SqlQuery] =
elems.traverse(_.nest(parentContext, extraCols, oneToOne, lateral)).map(SqlUnion(_))
/** Add WHERE, ORDER BY, OFFSET, and LIMIT to this query */
def addFilterOrderByOffsetLimit(
filter: Option[(Predicate, List[SqlJoin])],
orderBy: Option[(List[OrderSelection[_]], List[SqlJoin])],
offset: Option[Int],
limit: Option[Int],
predIsOneToOne: Boolean,
parentConstraints: List[List[(SqlColumn, SqlColumn)]]
): Result[SqlQuery] = {
val withFilter =
(filter, limit) match {
case (None, None) => this.success
// Push filters, offset and limit through into the branches of a union ...
case _ =>
val branchLimit = limit.map(_+offset.getOrElse(0))
val branchOrderBy = limit.flatMap(_ => orderBy)
val elems0 =
elems.foldLeft(List.empty[SqlSelect].success) { case (elems0, elem) =>
elems0.flatMap { elems0 =>
elem.addFilterOrderByOffsetLimit(filter, branchOrderBy, None, branchLimit, predIsOneToOne, parentConstraints).map {
elem0 => elem0 :: elems0
}
}
}
elems0.map(SqlUnion(_))
}
(orderBy, offset, limit) match {
case (None, None, None) => withFilter
case _ =>
for {
withFilter0 <- withFilter
table <- parentTableForType(context)
sel <- withFilter0.toSubquery(table.name, parentConstraints.nonEmpty)
res <- sel.addFilterOrderByOffsetLimit(None, orderBy, offset, limit, predIsOneToOne, parentConstraints)
} yield res
}
}
/** Render this `SqlUnion` as a `Fragment` */
def toFragment: Aliased[Fragment] = {
val alignedElems = {
elems.map { elem =>
val cols0 = cols.map { col =>
elem.cols.find(_ == col).getOrElse(SqlColumn.NullColumn(elem, col))
}
elem.copy(cols = cols0)
}
}
for {
frags <- alignedElems.traverse(_.toFragment)
} yield {
frags.reduce((x, y) => Fragments.parentheses(x) |+| Fragments.const(" UNION ALL ") |+| Fragments.parentheses(y))
}
}
}
object SqlUnion {
def apply(elems: List[SqlSelect]): SqlUnion =
new SqlUnion(elems.distinct)
}
/** Representation of an SQL join */
case class SqlJoin(
parent: TableExpr, // name of parent table
child: TableExpr, // child table/subquery
on: List[(SqlColumn, SqlColumn)], // join conditions
inner: Boolean
) extends ColumnOwner {
assert(on.nonEmpty)
assert(on.forall { case (p, c) => parent.owns(p) && child.owns(c) })
def context = child.context
def owns(col: SqlColumn): Boolean = child.owns(col)
def contains(other: ColumnOwner): Boolean = isSameOwner(other) || child.contains(other)
def directlyOwns(col: SqlColumn): Boolean =
child match {
case tr: TableRef => tr.directlyOwns(col)
case dr: DerivedTableRef => dr.directlyOwns(col)
case _ => false
}
def findNamedOwner(col: SqlColumn): Option[TableExpr] = child.findNamedOwner(col)
override def isSameOwner(other: ColumnOwner): Boolean = other eq this
/** Replace references to `from` with `to` */
def subst(from: TableExpr, to: TableExpr): SqlJoin = {
val newParent = if(parent.isSameOwner(from)) to else parent
val newChild =
if(!child.isSameOwner(from)) child
else {
(child, to) match {
case (sr: SubqueryRef, to: TableRef) => sr.copy(context = to.context, name = to.name)
case _ => to
}
}
val newOn = on.map { case (p, c) => (p.subst(from, to), c.subst(from, to)) }
copy(parent = newParent, child = newChild, on = newOn)
}
/** Return the columns of `table` referred to by the parent side of the conditions of this join */
def colsOf(other: ColumnOwner): List[SqlColumn] =
if (other.isSameOwner(parent)) on.map(_._1)
else Nil
/** Does this `SqlJoin` represent a predicate? */
def isPredicate: Boolean =
child match {
case SubqueryRef(_, _, sq: SqlSelect, _) => sq.predicate
case _ => false
}
/** Render this `SqlJoin` as a `Fragment` */
def toFragment: Aliased[Fragment] = {
val kind = if (inner) "INNER" else "LEFT"
val join = s"$kind JOIN"
val onFrag =
on.traverse {
case (p, c) =>
for {
fc <- c.toRefFragment(false)
fp <- p.toRefFragment(false)
} yield {
fc |+| Fragments.const(" = ") |+| fp
}
}.map { fons => Fragments.const(s" ON ") |+| Fragments.and(fons: _*) }
for {
fchild <- child.toDefFragment
fon <- onFrag
} yield Fragments.const(s" $join ") |+| fchild |+| fon
}
}
object SqlJoin {
/** Check that the given joins are correctly ordered relative to the given parent table */
def checkOrdering(parent: TableExpr, joins: List[SqlJoin]): Boolean = {
@tailrec
def loop(joins: List[SqlJoin], seen: List[TableExpr]): Boolean = {
joins match {
case Nil => true
case hd :: tl =>
seen.exists(_.isSameOwner(hd.parent)) && loop(tl, hd.child :: seen)
}
}
loop(joins, parent :: Nil)
}
}
}
/** Represents the mapping of a GraphQL query to an SQL query */
sealed trait MappedQuery {
/** Execute this query in `F` */
def fetch: F[Result[Table]]
/** The query rendered as a `Fragment` with all table and column aliases applied */
def fragment: Result[Fragment]
/** Return the value of the field `fieldName` in `context` from `table` */
def selectAtomicField(context: Context, fieldName: String, table: Table): Result[Any]
/** Does `table` contain subobjects of the type of the `narrowedContext` type */
def narrowsTo(narrowedContext: Context, table: Table): Boolean
/** Yield a `Table` containing only subojects of the `narrowedContext` type */
def narrow(narrowedContext: Context, table: Table): Table
/** Yield a list of `Tables` one for each of the subobjects of the context type
* contained in `table`.
*/
def group(context: Context, table: Table): Iterator[Table]
/** Return the number of subobjects of the context type contained in `table`. */
def count(context: Context, table: Table): Int
/** Does this query contain a root with the given possibly aliased name */
def containsRoot(fieldName: String, resultName: Option[String]): Boolean
}
object MappedQuery {
/** Compile the given GraphQL query to SQL in the given `Context` */
def apply(q: Query, context: Context): Result[MappedQuery] = {
def loop(q: Query, context: Context, parentConstraints: List[List[(SqlColumn, SqlColumn)]], exposeJoins: Boolean): Result[SqlQuery] = {
object TypeCase {
def unapply(q: Query): Option[(Query, List[Narrow])] = {
def isPolySelect(q: Query): Boolean =
q match {
case Select(fieldName, _, _) =>
typeMappings.fieldIsPolymorphic(context, fieldName)
case _ => false
}
def branch(q: Query): Option[TypeRef] =
q match {
case Narrow(subtpe, _) => Some(subtpe)
case _ => None
}
val ungrouped = ungroup(q).flatMap {
case sel@Select(fieldName, _, _) if isPolySelect(sel) =>
typeMappings.rawFieldMapping(context, fieldName) match {
case Some(TypeMappings.PolymorphicFieldMapping(cands)) =>
cands.map { case (pred, _) => Narrow(schema.uncheckedRef(pred.tpe), sel) }
case _ => Seq(sel)
}
case other => Seq(other)
}
val grouped = ungrouped.groupBy(branch).toList
val (default0, narrows0) = grouped.partition(_._1.isEmpty)
if (narrows0.isEmpty) None
else {
val default = default0.flatMap(_._2) match {
case Nil => Empty
case children => Group(children)
}
val narrows = narrows0.collect {
case (Some(subtpe), narrows) =>
narrows.collect { case Narrow(_, child) => child } match {
case List(child) => Narrow(subtpe, child)
case children => Narrow(subtpe, Group(children))
}
}
Some((default, narrows))
}
}
}
def group(queries: List[Query]): Result[SqlQuery] = {
queries.foldLeft(List.empty[SqlQuery].success) {
case (nodes, q) =>
loop(q, context, parentConstraints, exposeJoins).flatMap {
case _: EmptySqlQuery =>
nodes
case n =>
nodes.map(n :: _)
}
}.flatMap {
case Nil => EmptySqlQuery(context).success
case List(node) => node.success
case nodes =>
if(schema.isRootType(context.tpe))
SqlQuery.combineRootNodes(context, nodes)
else {
parentTableForType(context).flatMap { parentTable =>
if(parentTable.isRoot) SqlQuery.combineRootNodes(context, nodes)
else SqlQuery.combineAll(nodes)
}
}
}
}
def isEmbedded(context: Context, fieldName: String): Boolean =
typeMappings.fieldMapping(context, fieldName) match {
case Some(_: CursorFieldJson) | Some(SqlObject(_, Nil)) => true
case _ => false
}
def unembed(context: Context): Context = {
if(context.path.sizeCompare(1) <= 0) context
else {
val parentContext = context.copy(path = context.path.tail, resultPath = context.resultPath.tail, typePath = context.typePath.tail)
val fieldName = context.path.head
if(isEmbedded(parentContext, fieldName))
unembed(parentContext)
else
context
}
}
/* Compute the set of parent constraints to be inherited by the query for the value for `fieldName` */
// Either return List[List[(SqlColumn, SqlColumn)]] or maybe List[SqlJoin]?
def parentConstraintsFromJoins(parentContext: Context, fieldName: String, resultName: String): Result[List[List[(SqlColumn, SqlColumn)]]] = {
val tableContext = unembed(parentContext)
parentContext.forField(fieldName, resultName).map { childContext =>
typeMappings.fieldMapping(parentContext, fieldName) match {
case Some(SqlObject(_, Nil)) => Nil
case Some(SqlObject(_, join :: Nil)) =>
List(join.toConstraints(tableContext, childContext))
case Some(SqlObject(_, joins)) =>
val init = joins.init.map(_.toConstraints(tableContext, tableContext))
val last = joins.last.toConstraints(tableContext, childContext)
init ++ (last :: Nil)
case _ => Nil
}
}
}
def parentConstraintsToSqlJoins(parentTable: TableRef, parentConstraints: List[List[(SqlColumn, SqlColumn)]]): Result[List[SqlJoin]] =
if (parentConstraints.sizeCompare(1) <= 0) Nil.success
else {
val constraints = parentConstraints.last
val (p, c) = constraints.head
(p.owner, c.owner) match {
case (pt: TableExpr, _) =>
val on = constraints.map {
case (p, c) =>
// Intentionally reversed
val fc = c.in(parentTable)
(fc, p)
}
List(SqlJoin(parentTable, pt, on, true)).success
case _ => Result.internalError(s"Unnamed owner(s) for parent constraint ($p, $c)")
}
}
object NonSubobjectSelect {
def unapply(q: Query): Option[String] =
q match {
case Select(fieldName, _, child) if child == Empty || isJsonb(context, fieldName) || !isLocallyMapped(context, q) =>
Some(fieldName)
case Select(fieldName, _, Effect(_, _)) =>
Some(fieldName)
case _ =>
None
}
}
q match {
case Select(fieldName, _, Count(child)) =>
def childContext(q: Query): Result[Context] =
q match {
case Select(fieldName, resultName, _) =>
context.forField(fieldName, resultName)
case FilterOrderByOffsetLimit(_, _, _, _, child) =>
childContext(child)
case _ => Result.internalError(s"No context for count of ${child}")
}
for {
fieldContext <- childContext(child)
countCol <- columnForAtomicField(context, fieldName)
sq <- loop(child, context, parentConstraints, exposeJoins)
parentTable <- parentTableForType(context)
res <-
sq match {
case sq: SqlSelect =>
sq.joins match {
case hd :: tl =>
val keyCols = keyColumnsForType(fieldContext)
val parentCols0 = hd.colsOf(parentTable)
val wheres = hd.on.map { case (p, c) => Eql(c.toTerm, p.toTerm) }
val ssq = sq.copy(table = hd.child, cols = SqlColumn.CountColumn(countCol.in(hd.child), keyCols.map(_.in(hd.child))) :: Nil, joins = tl, wheres = wheres)
val ssqCol = SqlColumn.SubqueryColumn(countCol, ssq)
SqlSelect(context, Nil, parentTable, (ssqCol :: parentCols0).distinct, Nil, Nil, Nil, None, None, Nil, true, false).success
case _ =>
val keyCols = keyColumnsForType(fieldContext)
val countTable = sq.table
val ssq = sq.copy(cols = SqlColumn.CountColumn(countCol.in(countTable), keyCols.map(_.in(countTable))) :: Nil)
val ssqCol = SqlColumn.SubqueryColumn(countCol, ssq)
SqlSelect(context, Nil, parentTable, ssqCol :: Nil, Nil, Nil, Nil, None, None, Nil, true, false).success
}
case _ =>
Result.internalError("Implementation restriction: cannot count an SQL union")
}
} yield res
// Leaf, Json, Effect or mixed-in element: no SQL subobjects
case NonSubobjectSelect(fieldName) =>
columnsForLeaf(context, fieldName).flatMap { cols =>
val constraintCols = if(exposeJoins) parentConstraints.lastOption.getOrElse(Nil).map(_._2) else Nil
val extraCols = keyColumnsForType(context) ++ constraintCols
for {
parentTable <- parentTableForType(context)
extraJoins <- parentConstraintsToSqlJoins(parentTable, parentConstraints)
} yield {
val allCols = (cols ++ extraCols).distinct
if (allCols.isEmpty) EmptySqlQuery(context)
else SqlSelect(context, Nil, parentTable, allCols, extraJoins, Nil, Nil, None, None, Nil, true, false)
}
}
// Non-leaf non-Json element: compile subobject queries
case s@Select(fieldName, resultName, child) =>
context.forField(fieldName, resultName).flatMap { fieldContext =>
if(schema.isRootType(context.tpe)) loop(child, fieldContext, Nil, false)
else {
val keyCols = keyColumnsForType(context)
val constraintCols = if(exposeJoins) parentConstraints.lastOption.getOrElse(Nil).map(_._2) else Nil
val extraCols = keyCols ++ constraintCols
for {
parentTable <- parentTableForType(context)
parentConstraints0 <- parentConstraintsFromJoins(context, fieldName, s.resultName)
extraJoins <- parentConstraintsToSqlJoins(parentTable, parentConstraints)
res <-
loop(child, fieldContext, parentConstraints0, false).flatMap { sq =>
if(parentTable.isRoot) {
assert(parentConstraints0.isEmpty && extraCols.isEmpty && extraJoins.isEmpty)
sq.withContext(context, Nil, Nil)
} else
sq.nest(context, extraCols, sq.oneToOne && isSingular(context, fieldName, child), parentConstraints0.nonEmpty).flatMap { sq0 =>
sq0.withContext(sq0.context, Nil, extraJoins)
}
}
} yield res
}
}
case TypeCase(default, narrows0) =>
def isSimple(query: Query): Boolean = {
def loop(query: Query): Boolean =
query match {
case Empty => true
case Select(_, _, Empty) => true
case Group(children) => children.forall(loop)
case _ => false
}
loop(query)
}
val supertpe = context.tpe.underlying
val narrows = narrows0.filter(_.subtpe <:< supertpe)
val subtpes = narrows.map(_.subtpe)
assert(supertpe.underlying.isInterface || supertpe.underlying.isUnion || (subtpes.sizeCompare(1) == 0 && subtpes.head =:= supertpe))
subtpes.foreach(subtpe => assert(subtpe <:< supertpe))
val exhaustive = schema.exhaustive(supertpe, subtpes)
val exclusive = default == Empty
val allSimple = narrows.forall(narrow => isSimple(narrow.child))
val extraCols = (keyColumnsForType(context) ++ discriminatorColumnsForType(context)).distinct
if (allSimple) {
for {
dquery <- loop(default, context, parentConstraints, exposeJoins).flatMap(_.withContext(context, extraCols, Nil))
nqueries <-
narrows.traverse { narrow =>
val subtpe0 = narrow.subtpe.withModifiersOf(context.tpe)
loop(narrow.child, context.asType(subtpe0), parentConstraints, exposeJoins).flatMap(_.withContext(context, extraCols, Nil))
}
res <- SqlQuery.combineAll(dquery :: nqueries)
} yield res
} else {
val nqueries =
narrows.traverse { narrow =>
val subtpe0 = narrow.subtpe.withModifiersOf(context.tpe)
val child = Group(List(default, narrow.child))
loop(child, context.asType(subtpe0), parentConstraints, exposeJoins).flatMap(_.withContext(context, extraCols, Nil))
}
val dquery =
if(exhaustive) EmptySqlQuery(context).success
else
discriminatorForType(context).map { disc =>
subtpes.traverse(disc.discriminator.narrowPredicate)
}.getOrElse(Nil.success).flatMap { allPreds =>
if (exclusive) {
for {
parentTable <- parentTableForType(context)
allPreds0 <- allPreds.traverse(pred => SqlQuery.contextualiseWhereTerms(context, parentTable, pred).map(Not(_)))
} yield {
val defaultPredicate = And.combineAll(allPreds0)
SqlSelect(context, Nil, parentTable, extraCols, Nil, defaultPredicate :: Nil, Nil, None, None, Nil, true, false)
}
} else {
val allPreds0 = allPreds.map(Not(_))
val defaultPredicate = And.combineAll(allPreds0)
loop(Filter(defaultPredicate, default), context, parentConstraints, exposeJoins).flatMap(_.withContext(context, extraCols, Nil))
}
}
for {
dquery0 <- dquery
nqueries0 <- nqueries
} yield {
val allSels = (dquery0 :: nqueries0).flatMap(_.asSelects).distinct
allSels match {
case Nil => EmptySqlQuery(context)
case sel :: Nil => sel
case sels => SqlUnion(sels)
}
}
}
case n: Narrow =>
Result.internalError(s"Narrow not matched by extractor: $n")
case Group(queries) => group(queries)
case Unique(child) =>
loop(child, context.asType(context.tpe.nonNull.list), parentConstraints, exposeJoins).flatMap(_.withContext(context, Nil, Nil))
case Filter(False, _) => EmptySqlQuery(context).success
case Filter(In(_, Nil), _) => EmptySqlQuery(context).success
case Filter(True, child) => loop(child, context, parentConstraints, exposeJoins)
case FilterOrderByOffsetLimit(pred, oss, offset, lim, child) =>
val filterPaths = pred.map(SqlQuery.wherePaths).getOrElse(Nil).distinct
val orderPaths = oss.map(_.map(_.term).collect { case path: PathTerm => path.path }).getOrElse(Nil).distinct
val filterOrderPaths = (filterPaths ++ orderPaths).distinct
if (pred.exists(p => !isSqlTerm(context, p).getOrElse(false))) {
// If the predicate must be evaluated programatically then there's
// nothing we can do here, so just collect up all the columns/joins
// needed for the filter/order and loop
val expandedChildQuery = mergeQueries(child :: mkPathQuery(filterOrderPaths))
loop(expandedChildQuery, context, parentConstraints, exposeJoins)
} else {
val filterQuery = mergeQueries(mkPathQuery(filterPaths))
val orderQuery = mergeQueries(mkPathQuery(orderPaths))
def extractJoins(sq: SqlQuery): List[SqlJoin] =
sq match {
case sq: SqlSelect => sq.joins
case su: SqlUnion => su.elems.flatMap(_.joins)
case _: EmptySqlQuery => Nil
}
// Ordering will be repeated programmatically, so include the columns/
// joins for ordering in the child query
val expandedChildQuery =
if (orderPaths.isEmpty) child
else mergeQueries(child :: mkPathQuery(orderPaths))
val filter0 =
(for {
pred0 <- OptionT(pred.success)
sq <- OptionT(loop(filterQuery, context, parentConstraints, exposeJoins).map(_.some))
} yield ((pred0, extractJoins(sq)), sq.oneToOne)).value
val orderBy0 =
(for {
oss0 <- OptionT(oss.success)
sq <- OptionT(loop(orderQuery, context, parentConstraints, exposeJoins).map(_.some))
} yield ((oss0, extractJoins(sq)), sq.oneToOne)).value
for {
filter <- filter0
orderBy <- orderBy0
predIsOneToOne = filter.forall(_._2) && orderBy.forall(_._2)
expandedChild <- loop(expandedChildQuery, context, parentConstraints, true)
res <- expandedChild.addFilterOrderByOffsetLimit(filter.map(_._1), orderBy.map(_._1), offset, lim, predIsOneToOne, parentConstraints)
} yield res
}
case fool@(_: Filter | _: OrderBy | _: Offset | _: Limit) =>
Result.internalError(s"Filter/OrderBy/Offset/Limit not matched by extractor: $fool")
case _: Introspect =>
val extraCols = (keyColumnsForType(context) ++ discriminatorColumnsForType(context)).distinct
if(extraCols.isEmpty) EmptySqlQuery(context).success
else
parentTableForType(context).map { parentTable =>
SqlSelect(context, Nil, parentTable, extraCols.distinct, Nil, Nil, Nil, None, None, Nil, true, false)
}
case Environment(_, child) =>
loop(child, context, parentConstraints, exposeJoins)
case TransformCursor(_, child) =>
loop(child, context, parentConstraints, exposeJoins)
case _: Count => Result.internalError("Count node must be a child of a Select node")
case Effect(_, _) => Result.internalError("Effect node must be a child of a Select node")
case Empty | Query.Component(_, _, _) | (_: UntypedSelect) | (_: UntypedFragmentSpread) | (_: UntypedInlineFragment) | (_: Select) =>
EmptySqlQuery(context).success
}
}
loop(q, context, Nil, false).map {
case _: EmptySqlQuery => EmptyMappedQuery
case query => new NonEmptyMappedQuery(query)
}
}
/** MappedQuery implementation for a non-trivial SQL query */
final class NonEmptyMappedQuery(query: SqlQuery) extends MappedQuery {
val index: Map[SqlColumn, Int] = query.cols.zipWithIndex.toMap
val colsByResultPath: Map[List[String], List[(SqlColumn, Int)]] =
query.cols.filter(_.resultPath.nonEmpty).groupMap(_.resultPath)(col => (col, index(col)))
/** Execute this query in `F` */
def fetch: F[Result[Table]] = {
(for {
frag <- ResultT(fragment.pure[F])
rows <- ResultT(self.fetch(frag, query.codecs).map(_.success))
} yield Table(rows)).value
}
/** The query rendered as a `Fragment` with all table and column aliases applied */
lazy val fragment: Result[Fragment] = query.toFragment.runA(AliasState.empty)
def selectAtomicField(context: Context, fieldName: String, table: Table): Result[Any] =
leafIndex(context, fieldName) match {
case -1 =>
val obj = context.tpe.dealias
Result.internalError(s"Expected mapping for field '$fieldName' of type $obj")
case col =>
table.select(col).toResultOrError(
s"Expected single value for field '$fieldName' of type ${context.tpe.dealias} at ${context.path}, found many"
)
}
/** Does `table` contain subobjects of the type of the `narrowedContext` type */
def narrowsTo(narrowedContext: Context, table: Table): Boolean =
keyColumnsForType(narrowedContext) match {
case Nil => false
case cols =>
table.definesAll(cols)
}
/** Yield a `Table` containing only subojects of the `narrowedContext` type */
def narrow(narrowedContext: Context, table: Table): Table =
keyColumnsForType(narrowedContext) match {
case Nil => table
case cols =>
table.filterDefined(cols)
}
/** Yield a list of `Tables` one for each of the subobjects of the context type
* contained in `table`.
*/
def group(context: Context, table: Table): Iterator[Table] =
table.group(keyColumnsForType(context))
def count(context: Context, table: Table): Int =
table.count(keyColumnsForType(context))
def containsRoot(fieldName: String, resultName: Option[String]): Boolean = {
val name = resultName.orElse(Some(fieldName))
query.cols.exists(_.resultPath.lastOption == name)
}
def keyColumnsForType(context: Context): List[Int] = {
val key = context.resultPath
keyColumnsMemo.get(context.resultPath) match {
case Some(cols) => cols
case None =>
val keys = SqlMappingLike.this.keyColumnsForType(context).map(index)
keyColumnsMemo += key -> keys
keys
}
}
val keyColumnsMemo: scala.collection.mutable.HashMap[List[String], List[Int]] =
scala.collection.mutable.HashMap.empty[List[String], List[Int]]
def leafIndex(context: Context, fieldName: String): Int =
colsByResultPath.get(fieldName :: context.resultPath) match {
case None =>
columnForAtomicField(context, fieldName) match {
case Result.Success(col) => index.getOrElse(col, -1)
case Result.Warning(_, col) => index.getOrElse(col, -1)
case _ => -1
}
case Some(Nil) => -1
case Some(List((_, i))) => i
case Some(cols) =>
columnForAtomicField(context, fieldName) match {
case Result.Success(cursorCol) => cols.find(_._1 == cursorCol).map(_._2).getOrElse(-1)
case Result.Warning(_, cursorCol) => cols.find(_._1 == cursorCol).map(_._2).getOrElse(-1)
case _ => -1
}
}
}
/** MappedQuery implementation for a trivial SQL query */
object EmptyMappedQuery extends MappedQuery {
def fetch: F[Result[Table]] = Table.EmptyTable.success.pure[F].widen
def fragment: Result[Fragment] = Fragments.empty.success
def selectAtomicField(context: Context, fieldName: String, table: Table): Result[Any] =
Result.internalError(s"Expected mapping for field '$fieldName' of type ${context.tpe}")
def narrowsTo(narrowedContext: Context, table: Table): Boolean = true
def narrow(narrowedContext: Context, table: Table): Table = table
def group(context: Context, table: Table): Iterator[Table] = Iterator.empty
def count(context: Context, table: Table): Int = 0
def containsRoot(fieldName: String, resultName: Option[String]): Boolean = false
}
}
/** Representation of an SQL query result */
sealed trait Table {
def numRows: Int
def numCols: Int
/** Yield the value of the given column */
def select(col: Int): Option[Any]
/** A copy of this `Table` containing only the rows for which all the given columns are defined */
def filterDefined(cols: List[Int]): Table
/** True if all the given columns are defined, false otherwise */
def definesAll(cols: List[Int]): Boolean
/** Group this `Table` by the values of the given columns */
def group(cols: List[Int]): Iterator[Table]
def count(cols: List[Int]): Int
def isEmpty: Boolean = false
}
object Table {
def apply(rows: Vector[Array[Any]]): Table = {
if (rows.sizeCompare(1) == 0) OneRowTable(rows.head)
else if (rows.isEmpty) EmptyTable
else MultiRowTable(rows)
}
/** Specialized representation of an empty table */
case object EmptyTable extends Table {
def numRows: Int = 0
def numCols: Int = 0
def select(col: Int): Option[Any] = Some(FailedJoin)
def filterDefined(cols: List[Int]): Table = this
def definesAll(cols: List[Int]): Boolean = false
def group(cols: List[Int]): Iterator[Table] = Iterator.empty[Table]
def count(cols: List[Int]): Int = 0
override def isEmpty = true
}
/** Specialized representation of a table with exactly one row */
case class OneRowTable(row: Array[Any]) extends Table {
def numRows: Int = 1
def numCols = row.length
def select(col: Int): Option[Any] =
Some(row(col))
def filterDefined(cols: List[Int]): Table =
if(definesAll(cols)) this else EmptyTable
def definesAll(cols: List[Int]): Boolean = {
val cs = cols
cs.forall(c => row(c) != FailedJoin)
}
def group(cols: List[Int]): Iterator[Table] = {
cols match {
case Nil => Iterator.single(this)
case cols =>
if (definesAll(cols)) Iterator.single(this)
else Iterator.empty[Table]
}
}
def count(cols: List[Int]): Int = {
cols match {
case Nil => 1
case cols =>
if (definesAll(cols)) 1
else 0
}
}
}
case class MultiRowTable(rows: Vector[Array[Any]]) extends Table {
def numRows = rows.size
def numCols = rows.headOption.map(_.length).getOrElse(0)
def select(col: Int): Option[Any] = {
val c = col
var value: Any = FailedJoin
val ir = rows.iterator
while(ir.hasNext) {
ir.next()(c) match {
case FailedJoin =>
case v if value == FailedJoin => value = v
case v if value == v =>
case None =>
case v@Some(_) if value == None => value = v
case _ => return None
}
}
Some(value)
}
def filterDefined(cols: List[Int]): Table = {
val cs = cols
Table(rows.filter(r => cs.forall(c => r(c) != FailedJoin)))
}
def definesAll(cols: List[Int]): Boolean = {
val cs = cols
rows.exists(r => cs.forall(c => r(c) != FailedJoin))
}
def group(cols: List[Int]): Iterator[Table] = {
cols match {
case Nil => rows.iterator.map(r => OneRowTable(r))
case cols =>
val cs = cols
val discrim: Array[Any] => Any =
cs match {
case c :: Nil => row => row(c)
case cs => row => cs.map(c => row(c))
}
val nonNull = rows.filter(r => cs.forall(c => r(c) != FailedJoin))
val grouped = nonNull.groupBy(discrim)
grouped.iterator.map { case (_, rows) => Table(rows) }
}
}
def count(cols: List[Int]): Int = {
cols match {
case Nil => rows.size
case cols =>
val cs = cols
val discrim: Array[Any] => Any =
cs match {
case c :: Nil => row => row(c)
case cs => row => cs.map(c => row(c))
}
val nonNull = rows.filter(r => cs.forall(c => r(c) != FailedJoin))
nonNull.map(discrim).distinct.size
}
}
}
}
/** Cursor positioned at a GraphQL result non-leaf */
case class SqlCursor(context: Context, focus: Any, mapped: MappedQuery, parent: Option[Cursor], env: Env) extends Cursor {
assert(focus != Table.EmptyTable || context.tpe.isNullable || context.tpe.isList || schema.isRootType(context.tpe) || parentTableForType(context).map(_.isRoot).getOrElse(false))
def withEnv(env0: Env): SqlCursor = copy(env = env.add(env0))
def mkChild(context: Context = context, focus: Any = focus): SqlCursor =
SqlCursor(context, focus, mapped, Some(this), Env.empty)
def asTable: Result[Table] = focus match {
case table: Table => table.success
case _ => Result.internalError(s"Not a table")
}
def isLeaf: Boolean = false
def asLeaf: Result[Json] =
Result.internalError(s"Not a leaf: $tpe")
def preunique: Result[Cursor] = {
val listTpe = tpe.nonNull.list
mkChild(context.asType(listTpe), focus).success
}
def isList: Boolean =
tpe.isList
def asList[C](factory: Factory[Cursor, C]): Result[C] =
tpe.item.map(_.dealias).map(itemTpe =>
asTable.map { table =>
val itemContext = context.asType(itemTpe)
mapped.group(itemContext, table).map(table => mkChild(itemContext, focus = table)).to(factory)
}
).getOrElse(Result.internalError(s"Not a list: $tpe"))
def listSize: Result[Int] =
tpe.item.map(_.dealias).map(itemTpe =>
asTable.map { table =>
val itemContext = context.asType(itemTpe)
mapped.count(itemContext, table)
}
).getOrElse(Result.internalError(s"Not a list: $tpe"))
def isNullable: Boolean =
tpe.isNullable
def asNullable: Result[Option[Cursor]] =
(tpe, focus) match {
case (NullableType(_), table: Table) if table.isEmpty => None.success
case (NullableType(tpe), _) => Some(mkChild(context.asType(tpe))).success // non-nullable column as nullable schema type (ok)
case _ => Result.internalError(s"Not nullable at ${context.path}")
}
def isDefined: Result[Boolean] =
(tpe, focus) match {
case (NullableType(_), table: Table) => table.isEmpty.success
case _ => Result.internalError(s"Not nullable at ${context.path}")
}
def narrowsTo(subtpe: TypeRef): Result[Boolean] = {
def check(ctpe: Type): Boolean =
if (ctpe =:= tpe) asTable.map(table => mapped.narrowsTo(context.asType(subtpe), table)).toOption.getOrElse(false)
else ctpe <:< subtpe
if (!(subtpe <:< tpe)) false.success
else
discriminatorForType(context) match {
case Some(disc) => disc.discriminator.discriminate(this).map(check)
case _ => check(tpe).success
}
}
def narrow(subtpe: TypeRef): Result[Cursor] = {
narrowsTo(subtpe).flatMap { n =>
if (n) {
val narrowedContext = context.asType(subtpe)
asTable.map { table =>
mkChild(context = narrowedContext, focus = mapped.narrow(narrowedContext, table))
}
} else Result.internalError(s"Cannot narrow $tpe to $subtpe")
}
}
def field(fieldName: String, resultName: Option[String]): Result[Cursor] = {
val localField =
typeMappings.fieldMapping(this, fieldName).flatMap {
case Some((np, _: SqlJson)) =>
val fieldContext = np.context.forFieldOrAttribute(fieldName, resultName)
val fieldTpe = fieldContext.tpe
asTable.flatMap { table =>
def mkCirceCursor(f: Json): Result[Cursor] =
CirceCursor(fieldContext, focus = f, parent = Some(np), Env.empty).success
mapped.selectAtomicField(context, fieldName, table).flatMap(_ match {
case Some(j: Json) if fieldTpe.isNullable => mkCirceCursor(j)
case None => mkCirceCursor(Json.Null)
case j: Json if !fieldTpe.isNullable => mkCirceCursor(j)
case other =>
Result.internalError(s"$fieldTpe: expected jsonb value found ${other.getClass}: $other")
})
}
case Some((np, _: SqlField)) =>
val fieldContext = np.context.forFieldOrAttribute(fieldName, resultName)
val fieldTpe = fieldContext.tpe
asTable.flatMap(table =>
mapped.selectAtomicField(context, fieldName, table).map { leaf =>
val leafFocus = leaf match {
case Some(f) if tpe.variantField(fieldName) && !fieldTpe.isNullable => f
case other => other
}
assert(leafFocus != FailedJoin)
LeafCursor(fieldContext, leafFocus, Some(np), Env.empty)
}
)
case Some((np, (_: SqlObject | _: EffectMapping))) =>
val fieldContext = np.context.forFieldOrAttribute(fieldName, resultName)
asTable.map { table =>
val focussed = mapped.narrow(fieldContext, table)
mkChild(context = fieldContext, focus = focussed)
}
case _ =>
Result.failure(s"No field '$fieldName' for type ${context.tpe}")
}
// Fall back to the general implementation if it's a logic failure,
// but retain success and internal errors.
if (localField.isFailure)
mkCursorForField(this, fieldName, resultName)
else
localField
}
}
case class MultiRootCursor(roots: List[SqlCursor]) extends Cursor.AbstractCursor {
def parent: Option[Cursor] = None
def env: Env = Env.EmptyEnv
def focus: Any = Result.internalError(s"MultiRootCursor cursor has no focus")
def withEnv(env0: Env): MultiRootCursor = copy(roots = roots.map(_.withEnv(env0)))
def context: Context = roots.head.context
override def field(fieldName: String, resultName: Option[String]): Result[Cursor] = {
roots.find(_.mapped.containsRoot(fieldName, resultName)).map(_.field(fieldName, resultName)).
getOrElse(Result.internalError(s"No field '$fieldName' for type ${context.tpe}"))
}
}
/** Check SqlMapping specific TypeMapping validity */
override protected def validateTypeMapping(mappings: TypeMappings, context: Context, tm: TypeMapping): List[ValidationFailure] = {
// ObjectMappings must have a key column
// Associative fields must be keys
// Unions and interfaces must have a discriminator
// ObjectMappings must have columnRefs in the same table
// Implementors of interfaces must have columns in the same table
// Members of unions must have columns in the same table
// Union mappings have no SqlObjects or SqlJson fields
// Union field mappings must be hidden
def checkKey(om: ObjectMapping): List[ValidationFailure] =
if (keyColumnsForType(context).nonEmpty || parentTableForType(context).map(_.isRoot).getOrElse(false)) Nil
else List(NoKeyInObjectTypeMapping(om))
def checkAssoc(om: ObjectMapping): List[ValidationFailure] =
om.fieldMappings.iterator.collect {
case sf: SqlField if sf.associative && !sf.key =>
AssocFieldNotKey(om, sf)
}.toList
def checkDiscriminator(om: ObjectMapping): List[ValidationFailure] = {
val dnmes = om.fieldMappings.collect { case sf: SqlField if sf.discriminator => sf.fieldName }
dnmes.collectFirstSome { dnme =>
typeMappings.rawFieldMapping(context, dnme) match {
case Some(pf: TypeMappings.PolymorphicFieldMapping) => Some((pf.candidates.head._2, pf.candidates.map(_._1.tpe.name)))
case _ => None
}
} match {
case None => Nil
case Some((dfm, impls)) =>
List(IllegalPolymorphicDiscriminatorFieldMapping(om, dfm, impls.toList))
}
}
def hasDiscriminator(om: ObjectMapping): List[ValidationFailure] =
if (discriminatorColumnsForType(context).nonEmpty) Nil
else List(NoDiscriminatorInObjectTypeMapping(om))
def checkSplit(om: ObjectMapping): List[ValidationFailure] = {
val tables = allTables(List(om))
val split = tables.sizeCompare(1) > 0
if (!split) Nil
else List(SplitObjectTypeMapping(om, tables))
}
def checkSuperInterfaces(om: ObjectMapping): List[ValidationFailure] = {
val allMappings = om.tpe.dealias match {
case twf: TypeWithFields => om :: twf.interfaces.flatMap(nt => mappings.objectMapping(context.asType(nt)))
case _ => Nil
}
val tables = allTables(allMappings)
val split = tables.sizeCompare(1) > 0
if (!split) Nil
else List(SplitInterfaceTypeMapping(om, allMappings, tables))
}
def checkUnionMembers(om: ObjectMapping): List[ValidationFailure] = {
om.tpe.dealias match {
case ut: UnionType =>
val allMappings = ut.members.flatMap(nt => mappings.objectMapping(context.asType(nt)))
val tables = allTables(allMappings)
val split = tables.sizeCompare(1) > 0
if (!split) Nil
else List(SplitUnionTypeMapping(om, allMappings, tables))
case _ => Nil
}
}
def checkUnionFields(om: ObjectMapping): List[ValidationFailure] =
om.fieldMappings.iterator.collect {
case so: SqlObject =>
IllegalSubobjectInUnionTypeMapping(om, so)
case sj: SqlJson =>
IllegalJsonInUnionTypeMapping(om, sj)
case sf: SqlField if !sf.hidden =>
NonHiddenUnionFieldMapping(om, sf)
}.toList
def isSql(om: ObjectMapping): Boolean =
om.fieldMappings.exists {
case sf: SqlField => !TableName.isRoot(sf.columnRef.table)
case sj: SqlJson => !TableName.isRoot(sj.columnRef.table)
case SqlObject(_, joins) => joins.nonEmpty
case _ => false
}
tm match {
case im: SqlInterfaceMapping =>
checkKey(im) ++
checkAssoc(im) ++
hasDiscriminator(im) ++
checkDiscriminator(im) ++
checkSplit(im) ++
checkSuperInterfaces(im)
case um: SqlUnionMapping =>
checkKey(um) ++
checkAssoc(um) ++
hasDiscriminator(um) ++
checkSplit(um) ++
checkUnionMembers(um) ++
checkUnionFields(um)
case om: ObjectMapping if isSql(om) =>
(if(schema.isRootType(om.tpe)) Nil else checkKey(om)) ++
checkAssoc(om) ++
checkSplit(om) ++
checkSuperInterfaces(om)
case _ =>
super.validateTypeMapping(mappings, context, tm)
}
}
/** Check SqlMapping specific FieldMapping validity */
override protected def validateFieldMapping(mappings: TypeMappings, context: Context, om: ObjectMapping, fm: FieldMapping): List[ValidationFailure] = {
// GraphQL and DB schema nullability must be compatible
// GraphQL and DB schema types must be compatible
// Embedded objects are in the same table as their parent
// Joins must have at least one join condition
// Parallel joins must relate the same tables
// Serial joins must chain correctly
val IntTypeName = typeName[Int]
val LongTypeName = typeName[Long]
val FloatTypeName = typeName[Float]
val DoubleTypeName = typeName[Double]
val BigDecimalTypeName = typeName[BigDecimal]
val JsonTypeName = typeName[Json]
val tpe = om.tpe.dealias
(fm, tpe.fieldInfo(fm.fieldName)) match {
case (sf: SqlField, Some(field)) =>
val fieldIsNullable = field.tpe.isNullable
val colIsNullable = isNullable(sf.columnRef.codec)
val fieldContext = context.forFieldOrAttribute(sf.fieldName, None)
val leafMapping0 = mappings.typeMapping(fieldContext).collectFirst { case lm: LeafMapping[_] => lm }
(field.tpe.dealias.nonNull, leafMapping0) match {
case ((_: ScalarType)|(_: EnumType), Some(leafMapping)) =>
if(colIsNullable != fieldIsNullable)
List(InconsistentlyNullableFieldMapping(om, sf, field, sf.columnRef, colIsNullable))
else
(sf.columnRef.scalaTypeName, leafMapping.scalaTypeName) match {
case (t0, t1) if t0 == t1 => Nil
case (LongTypeName, IntTypeName) => Nil
case (DoubleTypeName, FloatTypeName) => Nil
case (BigDecimalTypeName, FloatTypeName) => Nil
case _ =>
List(InconsistentFieldLeafMapping(om, sf, field, sf.columnRef, leafMapping))
}
case _ =>
// Fallback to check only matching top level nullability
// Missing LeafMapping will be reported elsewhere
if(colIsNullable != fieldIsNullable)
List(InconsistentlyNullableFieldMapping(om, sf, field, sf.columnRef, colIsNullable))
else Nil
}
case (sj: SqlJson, Some(field)) =>
if(sj.columnRef.scalaTypeName != JsonTypeName)
List(InconsistentFieldTypeMapping(om, sj, field, sj.columnRef, JsonTypeName))
else Nil
case (fm@SqlObject(fieldName, Nil), _) if !schema.isRootType(tpe) =>
val parentTables0 = allTables(List(om))
if(parentTables0.forall(TableName.isRoot)) Nil
else {
val parentTables = parentTables0.filterNot(TableName.isRoot)
(for {
fieldContext <- context.forField(fieldName, None).toOption
com <- mappings.objectMapping(fieldContext)
} yield {
val childTables = allTables(List(com))
if (parentTables.sameElements(childTables)) Nil
else List(SplitEmbeddedObjectTypeMapping(om, fm, com, parentTables, childTables))
}).getOrElse(Nil)
}
case (SqlObject(fieldName, joins), _) if !schema.isRootType(tpe) =>
val com0 =
for {
fieldContext <- context.forField(fieldName, None).toOption
com <- mappings.objectMapping(fieldContext)
} yield com
com0 match {
case None => Nil // Missing mapping will be reported elsewhere
case Some(com) =>
val parentTables = allTables(List(om)).filterNot(TableName.isRoot)
val childTables = allTables(List(com)).filterNot(TableName.isRoot)
(parentTables, childTables) match {
case (parentTable :: _, childTable :: _) =>
val nonEmpty =
if(joins.forall(_.conditions.nonEmpty)) Nil
else List(NoJoinConditions(om, fm))
def consistentConditions(j: Join): Boolean =
j.conditions match {
case Nil => true
case hd :: tl =>
val parent = hd._1.table
val child = hd._2.table
tl.forall { case (p, c) => p.table == parent && c.table == child }
}
val parConsistent = joins.filterNot(consistentConditions).map { j =>
InconsistentJoinConditions(om, fm, j.conditions.map(_._1.table).distinct, j.conditions.map(_._2.table).distinct)
}
val serConsistent = {
val nonEmptyJoins = joins.filter(_.conditions.nonEmpty)
nonEmptyJoins match {
case Nil => Nil // Empty joins will be reported elsewhere
case hd :: tl =>
val headIsParent = hd.conditions.head._1.table == parentTable
val lastIsChild = nonEmptyJoins.last.conditions.head._2.table == childTable
val consistentChain =
nonEmptyJoins.zip(tl).forall {
case (j0, j1) => j0.conditions.head._2.table == j1.conditions.head._1.table
}
if(headIsParent && lastIsChild && consistentChain) Nil
else {
val path = nonEmptyJoins.map(j => (j.conditions.head._1.table, j.conditions.last._2.table))
List(MisalignedJoins(om, fm, parentTable, childTable, path))
}
}
}
nonEmpty ++ parConsistent ++ serConsistent
case _ => Nil // No or multiple tables will be reported elsewhere
}
}
case (other, _) =>
super.validateFieldMapping(mappings, context, om, other)
}
}
private def allTables(oms: List[ObjectMapping]): List[String] =
oms.flatMap(_.fieldMappings.flatMap {
case SqlField(_, columnRef, _, _, _, _) => List(columnRef.table)
case SqlJson(_, columnRef) => List(columnRef.table)
case SqlObject(_, Nil) => Nil
case SqlObject(_, joins) => joins.head.conditions.map(_._1.table)
case _ => Nil
}).distinct
abstract class SqlValidationFailure(severity: Severity) extends ValidationFailure(severity) {
protected def sql(a: Any) = s"$GREEN$a$RESET"
protected override def key: String =
s"Color Key: ${scala("◼")} Scala | ${graphql("◼")} GraphQL | ${sql("◼")} SQL"
}
/* Join has no join conditions */
case class NoJoinConditions(objectMapping: ObjectMapping, fieldMapping: FieldMapping)
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, ${fieldMapping.fieldName})"
override def formattedMessage: String =
s"""|No join conditions in field mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has a ${scala("SqlObject")} field mapping for the field ${graphql(fieldMapping.fieldName)} at (2) with a ${scala("Join")} with no join conditions.
|- ${UNDERLINED}Joins must include at least one join condition.$RESET
|
|(1) ${objectMapping.pos}
|(2) ${fieldMapping.pos}
|""".stripMargin
}
/** Parallel joins relate different tables */
case class InconsistentJoinConditions(objectMapping: ObjectMapping, fieldMapping: FieldMapping, parents: List[String], children: List[String])
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, ${fieldMapping.fieldName}, (${parents.mkString(", ")}), (${children.mkString(", ")}))"
override def formattedMessage: String =
s"""|Inconsistent join conditions in field mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has a ${scala("SqlObject")} field mapping for the field ${graphql(fieldMapping.fieldName)} at (2) with a Join with inconsistent join conditions: ${sql(s"(${parents.mkString(", ")}) -> (${children.mkString(", ")})")}.
|- ${UNDERLINED}All join conditions must relate the same tables.$RESET
|
|(1) ${objectMapping.pos}
|(2) ${fieldMapping.pos}
|""".stripMargin
}
/** Serial joins are misaligned */
case class MisalignedJoins(objectMapping: ObjectMapping, fieldMapping: FieldMapping, parent: String, child: String, path: List[(String, String)])
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, ${fieldMapping.fieldName}, $parent, $child, ${path.mkString(", ")})"
override def formattedMessage: String =
s"""|Misaligned joins in field mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has a ${scala("SqlObject")} field mapping for the field ${graphql(fieldMapping.fieldName)} at (2) with misaligned joins: ${sql(s"$parent, $child, ${path.mkString(", ")}")}.
|- ${UNDERLINED}Sequential joins must relate the parent table to the child table and chain correctly.$RESET
|
|(1) ${objectMapping.pos}
|(2) ${fieldMapping.pos}
|""".stripMargin
}
/** Object type mapping has no key */
case class NoKeyInObjectTypeMapping(objectMapping: ObjectMapping)
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)})"
override def formattedMessage: String =
s"""|No key field mapping in object type mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has no direct or inherited key field mapping.
|- ${UNDERLINED}Object type mappings must include at least one direct or inherited key field mapping.$RESET
|
|(1) ${objectMapping.pos}
|""".stripMargin
}
/** Object type mapping is split across multiple tables */
case class SplitObjectTypeMapping(objectMapping: ObjectMapping, tables: List[String])
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, (${tables.mkString(", ")}))"
override def formattedMessage: String =
s"""|Object type mapping is split across multiple tables.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} defined at (1) is split across multiple tables: ${sql(s"${tables.mkString(", ")}")}.
|- ${UNDERLINED}Object types must map to a single database table.$RESET
|
|(1) ${objectMapping.pos}
|""".stripMargin
}
/** Embedded object type mapping is split across non-parent tables */
case class SplitEmbeddedObjectTypeMapping(parent: ObjectMapping, parentField: FieldMapping, child: ObjectMapping, parentTables: List[String], childTables: List[String])
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${parent.showMappingType}, ${showNamedType(parent.tpe)}.${parentField.fieldName}, ${child.showMappingType}, ${showNamedType(child.tpe)}, (${childTables.mkString(", ")}))"
override def formattedMessage: String =
s"""|Embedded object type maps to non-parent tables.
|
|- The ${scala(parent.showMappingType)} for type ${graphql(showNamedType(parent.tpe))} defined at (1) embeds the ${scala(child.showMappingType)} for type ${graphql(showNamedType(child.tpe))} defined at (2) via field mapping ${graphql(s"${showNamedType(parent.tpe)}.${parentField.fieldName}")} at (3).
|- The parent object is in table(s) ${sql(parentTables.mkString(", "))}.
|- The embedded object is in non-parent table(s) ${sql(childTables.mkString(", "))}.
|- ${UNDERLINED}Embedded objects must map to the same database tables as their parents.$RESET
|
|(1) ${parent.pos}
|(2) ${child.pos}
|(3) ${parentField.pos}
|""".stripMargin
}
/** Interface/union implementation mappings split across multiple tables */
case class SplitInterfaceTypeMapping(objectMapping: ObjectMapping, intrfs: List[ObjectMapping], tables: List[String])
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, (${intrfs.map(_.tpe.name).mkString(", ")}), (${tables.mkString(", ")}))"
override def formattedMessage: String =
s"""|Interface implementors are split across multiple tables.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has implementors (${intrfs.map(_.tpe.name).mkString(", ")}) which are split across multiple tables: ${sql(s"${tables.mkString(", ")}")}.
|- ${UNDERLINED}All implementors of an interface must map to a single database table.$RESET
|
|(1) ${objectMapping.pos}
|""".stripMargin
}
/** Interface/union implementation mappings split across multiple tables */
case class SplitUnionTypeMapping(objectMapping: ObjectMapping, members: List[ObjectMapping], tables: List[String])
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}, (${members.map(_.tpe.name).mkString(", ")}), (${tables.mkString(", ")}))"
override def formattedMessage: String =
s"""|Union member mappings are split across multiple tables.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has members (${members.map(_.tpe.name).mkString(", ")}) which are split across multiple tables: ${sql(s"${tables.mkString(", ")}")}.
|- ${UNDERLINED}All members of a union must map to a single database table.$RESET
|
|(1) ${objectMapping.pos}
|""".stripMargin
}
/** Interface/union type mapping has no discriminator */
case class NoDiscriminatorInObjectTypeMapping(objectMapping: ObjectMapping)
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)})"
override def formattedMessage: String =
s"""|No discriminator field mapping in interface/union type mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) has no discriminator field mapping.
|- ${UNDERLINED}interface/union type mappings must include at least one discriminator field mapping.$RESET
|
|(1) ${objectMapping.pos}
|""".stripMargin
}
/** Interface/union type mapping has a polymorphic discriminator */
case class IllegalPolymorphicDiscriminatorFieldMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping, impls: List[String])
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}, (${impls.mkString(", ")}))"
override def formattedMessage: String =
s"""|Illegal polymorphic discriminator field mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains a discriminator field mapping ${graphql(fieldMapping.fieldName)} at (2).
|- This discriminator has variant field mappings in the type mappings for subtypes: ${impls.mkString(", ")}.
|- ${UNDERLINED}Discriminator field mappings must not be polymorphic.$RESET
|
|(1) ${objectMapping.pos}
|(2) ${fieldMapping.pos}
|""".stripMargin
}
/** Subobject field mappings not allowed in union type mappings */
case class IllegalSubobjectInUnionTypeMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping)
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName})"
override def formattedMessage: String =
s"""|Illegal subobject field mapping in union type mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains a subobject field mapping ${graphql(fieldMapping.fieldName)} at (2).
|- ${UNDERLINED}Subobject field mappings are not allowed in union type mappings.$RESET
|
|(1) ${objectMapping.pos}
|(2) ${fieldMapping.pos}
|""".stripMargin
}
/** SqlJson field mappings not allowed in union type mappings */
case class IllegalJsonInUnionTypeMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping)
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName})"
override def formattedMessage: String =
s"""|Illegal json field mapping in union type mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains a subobject field mapping ${graphql(fieldMapping.fieldName)} at (2).
|- ${UNDERLINED}SqlJson field mappings are not allowed in union type mappings.$RESET
|
|(1) ${objectMapping.pos}
|(2) ${fieldMapping.pos}
|""".stripMargin
}
/** Associative field must be a key */
case class AssocFieldNotKey(objectMapping: ObjectMapping, fieldMapping: FieldMapping)
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName})"
override def formattedMessage: String =
s"""|Non-key associatitve field mapping in object type mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains an associative field mapping ${graphql(fieldMapping.fieldName)} at (2) which is not a key.
|- ${UNDERLINED}All associative field mappings must be keys.$RESET
|
|(1) ${objectMapping.pos}
|(2) ${fieldMapping.pos}
|""".stripMargin
}
/** Union field mappings must be hidden */
case class NonHiddenUnionFieldMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping)
extends SqlValidationFailure(Severity.Error) {
override def toString: String =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName})"
override def formattedMessage: String =
s"""|Non-hidden field mapping in union type mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains a field mapping ${graphql(fieldMapping.fieldName)} at (2) which is not hidden.
|- ${UNDERLINED}All fields mappings in a union type mapping must be hidden.$RESET
|
|(1) ${objectMapping.pos}
|(2) ${fieldMapping.pos}
|""".stripMargin
}
/** SqlField codec and LeafMapping are inconsistent. */
case class InconsistentFieldLeafMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping, field: Field, columnRef: ColumnRef, leafMapping: LeafMapping[_])
extends SqlValidationFailure(Severity.Error) {
override def toString() =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}, ${columnRef.table}.${columnRef.column}:${columnRef.scalaTypeName}, ${showNamedType(leafMapping.tpe)}:${leafMapping.scalaTypeName})"
override def formattedMessage: String = {
s"""|Inconsistent field leaf mapping.
|
|- The field ${graphql(s"${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}: ${showType(field.tpe)}")} is defined by a Schema at (1).
|- The ${scala(fieldMapping.showMappingType)} at (2) and ColumnRef for ${sql(s"${columnRef.table}.${columnRef.column}")} at (3) map ${graphql(showNamedType(leafMapping.tpe))} to Scala type ${scala(columnRef.scalaTypeName)}.
|- A ${scala(leafMapping.showMappingType)} at (4) maps ${graphql(showNamedType(leafMapping.tpe))} to Scala type ${scala(leafMapping.scalaTypeName)}.
|- ${UNDERLINED}The Scala types are inconsistent.$RESET
|
|(1) ${schema.pos}
|(2) ${fieldMapping.pos}
|(3) ${columnRef.pos}
|(4) ${leafMapping.pos}
|""".stripMargin
}
}
/** SqlField codec and LeafMapping are inconsistent. */
case class InconsistentFieldTypeMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping, field: Field, columnRef: ColumnRef, scalaTypeName: String)
extends SqlValidationFailure(Severity.Error) {
override def toString() =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}:${showType(field.tpe)}, ${columnRef.table}.${columnRef.column}:${columnRef.scalaTypeName}, ${scalaTypeName})"
override def formattedMessage: String = {
s"""|Inconsistent field type mapping.
|
|- The field ${graphql(s"${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}: ${showType(field.tpe)}")} is defined by a Schema at (1).
|- The ${scala(fieldMapping.showMappingType)} at (2) and ColumnRef for ${sql(s"${columnRef.table}.${columnRef.column}")} at (3) map to Scala type ${scala(columnRef.scalaTypeName)}.
|- The expected Scala type is ${scala(scalaTypeName)}.
|- ${UNDERLINED}The Scala types are inconsistent.$RESET
|
|(1) ${schema.pos}
|(2) ${fieldMapping.pos}
|(3) ${columnRef.pos}
|""".stripMargin
}
}
/** SqlField codec and LeafMapping are inconsistent. */
case class InconsistentlyNullableFieldMapping(objectMapping: ObjectMapping, fieldMapping: FieldMapping, field: Field, columnRef: ColumnRef, colIsNullable: Boolean)
extends SqlValidationFailure(Severity.Error) {
override def toString() =
s"$productPrefix(${objectMapping.showMappingType}, ${showNamedType(objectMapping.tpe)}.${fieldMapping.fieldName}, ${columnRef.table}.${columnRef.column})"
override def formattedMessage: String = {
val fieldNullability = if(field.tpe.isNullable) "nullable" else "not nullable"
val colNullability = if(colIsNullable) "nullable" else "not nullable"
s"""|Inconsistently nullable field mapping.
|
|- The ${scala(objectMapping.showMappingType)} for type ${graphql(showNamedType(objectMapping.tpe))} at (1) contains a field mapping ${graphql(fieldMapping.fieldName)} at (2).
|- In the schema at (3) the field is ${fieldNullability}.
|- The corresponding ColumnRef for ${sql(s"${columnRef.table}.${columnRef.column}")} at (4) is ${colNullability}.
|- ${UNDERLINED}The nullabilities are inconsistent.$RESET
|
|(1) ${objectMapping.pos}
|(2) ${fieldMapping.pos}
|(3) ${schema.pos}
|(3) ${columnRef.pos}
|""".stripMargin
}
}
}