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

.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
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy