{-# OPTIONS_GHC -fno-warn-name-shadowing -fno-warn-unused-binds #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE CPP #-}

module Database.Beam.Query.SQL92
    ( buildSql92Query' ) where

import           Database.Beam.Query.Internal
import           Database.Beam.Backend.SQL

import           Control.Monad.Free.Church
import           Control.Monad.Free

import           Data.Kind (Type)
import           Data.Maybe
import           Data.Proxy (Proxy(Proxy))
import           Data.String
import qualified Data.Text as T

-- * Beam queries

andE' :: IsSql92ExpressionSyntax expr =>
         Maybe expr -> Maybe expr -> Maybe expr
andE' Nothing Nothing = Nothing
andE' (Just x) Nothing = Just x
andE' Nothing (Just y) = Just y
andE' (Just x) (Just y) = Just (andE x y)

newtype PreserveLeft a b = PreserveLeft { unPreserveLeft :: (a, b) }
instance (Monoid a, ProjectibleWithPredicate c syntax res b) => ProjectibleWithPredicate c syntax res (PreserveLeft a b) where
  project' context be f (PreserveLeft (a, b)) =
    PreserveLeft . (a,) <$> project' context be f b

  projectSkeleton' ctxt be mkM =
    PreserveLeft . (mempty,) <$> projectSkeleton' ctxt be mkM

type SelectStmtFn be
  =  BeamSqlBackendSelectTableSyntax be
 -> [BeamSqlBackendOrderingSyntax be]
 -> Maybe Integer {-^ LIMIT -}
 -> Maybe Integer {-^ OFFSET -}
 -> BeamSqlBackendSelectSyntax be

data QueryBuilder be
  = QueryBuilder
  { qbNextTblRef :: Int
  , qbFrom  :: Maybe (BeamSqlBackendFromSyntax be)
  , qbWhere :: Maybe (BeamSqlBackendExpressionSyntax be) }

data SelectBuilder be (db :: (Type -> Type) -> Type) a where
  SelectBuilderQ :: ( BeamSqlBackend be
                    , Projectible be a )
                 => a -> QueryBuilder be -> SelectBuilder be db a
  SelectBuilderGrouping
      :: ( BeamSqlBackend be
         , Projectible be a )
      => a -> QueryBuilder be
      -> Maybe (BeamSqlBackendGroupingSyntax be)
      -> Maybe (BeamSqlBackendExpressionSyntax be)
      -> Maybe (BeamSqlBackendSetQuantifierSyntax be)
      -> SelectBuilder be db a
  SelectBuilderSelectSyntax :: Bool {- Whether or not this contains UNION, INTERSECT, EXCEPT, etc -}
                            -> a -> BeamSqlBackendSelectTableSyntax be
                            -> SelectBuilder be db a
  SelectBuilderTopLevel ::
    { sbLimit, sbOffset :: Maybe Integer
    , sbOrdering        :: [ BeamSqlBackendOrderingSyntax be ]
    , sbTable           :: SelectBuilder be db a
    , sbSelectFn        :: Maybe (SelectStmtFn be)
                        -- ^ Which 'SelectStmtFn' to use to build this select. If 'Nothing', use the default
    } -> SelectBuilder be db a

sbContainsSetOperation :: SelectBuilder syntax db a -> Bool
sbContainsSetOperation (SelectBuilderSelectSyntax contains _ _) = contains
sbContainsSetOperation (SelectBuilderTopLevel { sbTable = tbl }) = sbContainsSetOperation tbl
sbContainsSetOperation _ = False

fieldNameFunc :: IsSql92ExpressionSyntax expr =>
                 (T.Text -> Sql92ExpressionFieldNameSyntax expr) -> Int
              -> expr
fieldNameFunc mkField i = fieldE (mkField ("res" <> fromString (show i)))

nextTblPfx :: TablePrefix -> TablePrefix
nextTblPfx = ("sub_" <>)

defaultProjection :: Projectible be x
                  => Proxy be -> TablePrefix -> x -> [ ( BeamSqlBackendExpressionSyntax be , Maybe T.Text ) ]
defaultProjection be pfx =
    zipWith (\i e -> (e, Just (fromString "res" <> fromString (show (i :: Integer)))))
            [0..] . flip (project be) (nextTblPfx pfx)

buildSelect :: forall be db a
             . ( BeamSqlBackend be, Projectible be a )
            => TablePrefix -> SelectBuilder be db a -> BeamSqlBackendSelectSyntax be
buildSelect _ (SelectBuilderTopLevel limit offset ordering (SelectBuilderSelectSyntax _ _ table) selectStmt') =
    (fromMaybe selectStmt selectStmt') table ordering limit offset
buildSelect pfx (SelectBuilderTopLevel limit offset ordering (SelectBuilderQ proj (QueryBuilder _ from where_)) selectStmt') =
    (fromMaybe selectStmt selectStmt') (selectTableStmt Nothing (projExprs (defaultProjection (Proxy @be) pfx proj)) from where_ Nothing Nothing) ordering limit offset
buildSelect pfx (SelectBuilderTopLevel limit offset ordering (SelectBuilderGrouping proj (QueryBuilder _ from where_) grouping having distinct) selectStmt') =
    (fromMaybe selectStmt selectStmt') (selectTableStmt distinct (projExprs (defaultProjection (Proxy @be) pfx proj)) from where_ grouping having) ordering limit offset
buildSelect pfx x = buildSelect pfx (SelectBuilderTopLevel Nothing Nothing [] x Nothing)

selectBuilderToTableSource :: forall be db a
                            . ( BeamSqlBackend be, Projectible be a )
                           => TablePrefix -> SelectBuilder be db a -> BeamSqlBackendSelectTableSyntax be
selectBuilderToTableSource _ (SelectBuilderSelectSyntax _ _ x) = x
selectBuilderToTableSource pfx (SelectBuilderQ x (QueryBuilder _ from where_)) =
  selectTableStmt Nothing (projExprs (defaultProjection (Proxy @be) pfx x)) from where_ Nothing Nothing
selectBuilderToTableSource pfx (SelectBuilderGrouping x (QueryBuilder _ from where_) grouping having distinct) =
  selectTableStmt distinct (projExprs (defaultProjection (Proxy @be) pfx x)) from where_ grouping having
selectBuilderToTableSource pfx sb =
    let (x, QueryBuilder _ from where_) = selectBuilderToQueryBuilder pfx sb
    in selectTableStmt Nothing (projExprs (defaultProjection (Proxy @be) pfx x)) from where_ Nothing Nothing

selectBuilderToQueryBuilder :: forall be db a
                             . ( BeamSqlBackend be, Projectible be a)
                            => TablePrefix -> SelectBuilder be db a -> (a, QueryBuilder be)
selectBuilderToQueryBuilder pfx sb =
    let select = buildSelect pfx sb
        x' = reproject (Proxy @be) (fieldNameFunc (qualifiedField t0)) (sbProj sb)
        t0 = pfx <> "0"
    in (x', QueryBuilder 1 (Just (fromTable (tableFromSubSelect select) (Just (t0, Nothing)))) Nothing)

emptyQb :: QueryBuilder select
emptyQb = QueryBuilder 0 Nothing Nothing

sbProj :: SelectBuilder syntax db a -> a
sbProj (SelectBuilderQ proj _) = proj
sbProj (SelectBuilderGrouping proj _ _ _ _) = proj
sbProj (SelectBuilderSelectSyntax _ proj _) = proj
sbProj (SelectBuilderTopLevel _ _ _ sb _) = sbProj sb

setSelectBuilderProjection :: Projectible be b
                           => SelectBuilder be db a -> b -> SelectBuilder be db b
setSelectBuilderProjection (SelectBuilderQ _ q) proj = SelectBuilderQ proj q
setSelectBuilderProjection (SelectBuilderGrouping _ q grouping having d) proj = SelectBuilderGrouping proj q grouping having d
setSelectBuilderProjection (SelectBuilderSelectSyntax containsSetOp _ q) proj = SelectBuilderSelectSyntax containsSetOp proj q
setSelectBuilderProjection (SelectBuilderTopLevel limit offset ord sb s) proj =
    SelectBuilderTopLevel limit offset ord (setSelectBuilderProjection sb proj) s

limitSelectBuilder, offsetSelectBuilder :: Integer -> SelectBuilder syntax db a -> SelectBuilder syntax db a
limitSelectBuilder limit (SelectBuilderTopLevel limit' offset ordering tbl build) =
    SelectBuilderTopLevel (Just $ maybe limit (min limit) limit') offset ordering tbl build
limitSelectBuilder limit x = SelectBuilderTopLevel (Just limit) Nothing [] x Nothing

offsetSelectBuilder offset (SelectBuilderTopLevel Nothing offset' ordering tbl build) =
    SelectBuilderTopLevel Nothing (Just $ offset + fromMaybe 0 offset') ordering tbl build
offsetSelectBuilder offset (SelectBuilderTopLevel (Just limit) offset' ordering tbl build) =
    SelectBuilderTopLevel (Just $ max 0 (limit - offset)) (Just $ offset + fromMaybe 0 offset') ordering tbl build
offsetSelectBuilder offset x = SelectBuilderTopLevel Nothing (Just offset) [] x Nothing

exprWithContext :: TablePrefix -> WithExprContext a -> a
exprWithContext pfx = ($ nextTblPfx pfx)

buildJoinTableSourceQuery
  :: forall be x
   . ( BeamSqlBackend be, Projectible be x )
  => TablePrefix -> BeamSqlBackendSelectSyntax be
  -> x -> QueryBuilder be
  -> (x, QueryBuilder be)
buildJoinTableSourceQuery tblPfx tblSource x qb =
  let qb' = QueryBuilder (tblRef + 1) from' (qbWhere qb)
      !tblRef = qbNextTblRef qb
      from' = case qbFrom qb of
                Nothing -> Just newSource
                Just oldFrom -> Just (innerJoin oldFrom newSource Nothing)
      newSource = fromTable (tableFromSubSelect tblSource) (Just (newTblNm, Nothing))
      newTblNm = tblPfx <> fromString (show tblRef)
  in (reproject (Proxy @be) (fieldNameFunc (qualifiedField newTblNm)) x, qb')

buildInnerJoinQuery
  :: forall be r
   . BeamSqlBackend be
  => TablePrefix -> (TablePrefix -> T.Text -> BeamSqlBackendFromSyntax be)
  -> (T.Text -> r)
  -> (r-> Maybe (WithExprContext (BeamSqlBackendExpressionSyntax be)))
  -> QueryBuilder be -> (T.Text, r, QueryBuilder be)
buildInnerJoinQuery tblPfx mkFrom mkTbl mkOn qb =
  let qb' = QueryBuilder (tblRef + 1) from' where'
      tblRef = qbNextTblRef qb
      newTblNm = tblPfx <> fromString (show tblRef)
      newSource = mkFrom (nextTblPfx tblPfx) newTblNm
      (from', where') =
        case qbFrom qb of
          Nothing -> (Just newSource, andE' (qbWhere qb) (exprWithContext tblPfx <$> mkOn newTbl))
          Just oldFrom -> (Just (innerJoin oldFrom newSource (exprWithContext tblPfx <$> mkOn newTbl)), qbWhere qb)

      newTbl = mkTbl newTblNm
  in (newTblNm, newTbl, qb')

nextTbl :: BeamSqlBackend be
        => QueryBuilder be -> TablePrefix
        -> (T.Text -> r)
        -> ( r, T.Text, QueryBuilder be )
nextTbl qb tblPfx mkTbl =
  let tblRef = qbNextTblRef qb
      newTblNm = tblPfx <> fromString (show tblRef)
      newTbl = mkTbl newTblNm
  in (newTbl, newTblNm, qb { qbNextTblRef = qbNextTblRef qb + 1})

projOrder :: Projectible be x
          => Proxy be -> x -> WithExprContext [ BeamSqlBackendExpressionSyntax be ]
projOrder = project -- (Proxy @AnyType) (\_ x -> tell [x] >> pure x)

-- | Convenience functions to construct an arbitrary SQL92 select syntax type
-- from a 'Q'. Used by most backends as the default implementation of
-- 'buildSqlQuery' in 'HasQBuilder'.
buildSql92Query' :: forall be db s a
                  . ( BeamSqlBackend be, Projectible be a)
                 => Bool {-^ Whether this backend supports arbitrary nested UNION, INTERSECT, EXCEPT -}
                 -> T.Text {-^ Table prefix -}
                 -> Q be db s a
                 -> BeamSqlBackendSelectSyntax be
buildSql92Query' arbitrarilyNestedCombinations baseTblPfx (Q q) =
    buildSelect baseTblPfx (buildQuery baseTblPfx (fromF q))
  where
    be :: Proxy be
    be = Proxy

    buildQuery :: forall s x
                . Projectible be x
               => T.Text
               -> Free (QF be db s) x
               -> SelectBuilder be db x
    buildQuery _ (Pure x) = SelectBuilderQ x emptyQb
    buildQuery tblPfx f@(Free (QGuard _ _)) = buildJoinedQuery tblPfx f emptyQb
    buildQuery tblPfx f@(Free QAll {}) = buildJoinedQuery tblPfx f emptyQb
    buildQuery tblPfx f@(Free QArbitraryJoin {}) = buildJoinedQuery tblPfx f emptyQb
    buildQuery tblPfx f@(Free QTwoWayJoin {}) = buildJoinedQuery tblPfx f emptyQb
    buildQuery tblPfx (Free (QSubSelect q' next)) =
        let sb = buildQuery tblPfx (fromF q')
            (proj, qb) = selectBuilderToQueryBuilder tblPfx sb
        in buildJoinedQuery tblPfx (next proj) qb
    buildQuery tblPfx (Free (QDistinct nubType q' next)) =
      let (proj, qb, gp, hv) =
            case buildQuery tblPfx (fromF q') of
              SelectBuilderQ proj qb ->
                ( proj, qb, Nothing, Nothing)
              SelectBuilderGrouping proj qb gp hv Nothing ->
                ( proj, qb, gp, hv)
              sb ->
                let (proj, qb) = selectBuilderToQueryBuilder tblPfx sb
                in ( proj, qb, Nothing, Nothing)
      in case next proj of
           Pure x -> SelectBuilderGrouping x qb gp hv (Just (exprWithContext tblPfx (nubType proj)))
           _ -> let ( proj', qb' ) = selectBuilderToQueryBuilder tblPfx (SelectBuilderGrouping proj qb gp hv (Just (exprWithContext tblPfx (nubType proj))))
                in buildJoinedQuery tblPfx (next proj') qb'
    buildQuery tblPfx (Free (QAggregate mkAgg q' next)) =
        let sb = buildQuery tblPfx (fromF q')
            (groupingSyntax, aggProj) = mkAgg (sbProj sb) (nextTblPfx tblPfx)
        in case tryBuildGuardsOnly tblPfx (next aggProj) Nothing of
            Just (proj, having) ->
                case sb of
                  SelectBuilderQ _ q'' -> SelectBuilderGrouping proj q'' groupingSyntax having Nothing

                  -- We'll have to generate a subselect
                  _ -> let (subProj, qb) = selectBuilderToQueryBuilder tblPfx sb --(setSelectBuilderProjection sb aggProj)
                           (groupingSyntax, aggProj') = mkAgg subProj (nextTblPfx tblPfx)
                       in case tryBuildGuardsOnly tblPfx (next aggProj') Nothing of
                            Nothing -> error "buildQuery (Free (QAggregate ...)): Impossible"
                            Just (aggProj'', having') ->
                              SelectBuilderGrouping aggProj'' qb groupingSyntax having' Nothing
            Nothing ->
              let (_, having) = tryCollectHaving tblPfx (next aggProj') Nothing
                  (next', _) = tryCollectHaving tblPfx (next x') Nothing
                  (groupingSyntax', aggProj', qb) =
                    case sb of
                      SelectBuilderQ _ q'' -> (groupingSyntax, aggProj, q'')
                      _ -> let (proj', qb''') = selectBuilderToQueryBuilder tblPfx sb
                               (groupingSyntax', aggProj') = mkAgg proj' (nextTblPfx tblPfx)
                           in (groupingSyntax', aggProj', qb''')
                  (x', qb') = selectBuilderToQueryBuilder tblPfx $
                              SelectBuilderGrouping aggProj' qb groupingSyntax' having Nothing
              in buildJoinedQuery tblPfx next' qb'

    buildQuery tblPfx (Free (QOrderBy mkOrdering q' next)) =
        let sb = buildQuery tblPfx (fromF q')
            proj = sbProj sb
            ordering = exprWithContext tblPfx (mkOrdering proj)

            doJoined =
                let sb' = case sb of
                            SelectBuilderQ {} ->
                                SelectBuilderTopLevel Nothing Nothing ordering sb Nothing
                            SelectBuilderGrouping {} ->
                                SelectBuilderTopLevel Nothing Nothing ordering sb Nothing
                            SelectBuilderSelectSyntax {} ->
                                SelectBuilderTopLevel Nothing Nothing ordering sb Nothing
                            SelectBuilderTopLevel Nothing Nothing [] sb' build ->
                                SelectBuilderTopLevel Nothing Nothing ordering sb' build
                            SelectBuilderTopLevel Nothing (Just 0) [] sb' build ->
                                SelectBuilderTopLevel Nothing (Just 0) ordering sb' build
                            SelectBuilderTopLevel {}
                                | (proj'', qb) <- selectBuilderToQueryBuilder tblPfx sb ->
                                    SelectBuilderTopLevel Nothing Nothing (exprWithContext tblPfx (mkOrdering proj'')) (SelectBuilderQ proj'' qb) Nothing
                                | otherwise -> error "buildQuery (Free (QOrderBy ...)): query inspected expression"

                    (joinedProj, qb) = selectBuilderToQueryBuilder tblPfx sb'
                in buildJoinedQuery tblPfx (next joinedProj) qb
        in case next proj of
             Pure proj' ->
               case ordering of
                 [] -> setSelectBuilderProjection sb proj'
                 ordering ->
                     case sb of
                       SelectBuilderQ {} ->
                           SelectBuilderTopLevel Nothing Nothing ordering (setSelectBuilderProjection sb proj') Nothing
                       SelectBuilderGrouping {} ->
                           SelectBuilderTopLevel Nothing Nothing ordering (setSelectBuilderProjection sb proj') Nothing
                       SelectBuilderSelectSyntax {} ->
                           SelectBuilderTopLevel Nothing Nothing ordering (setSelectBuilderProjection sb proj') Nothing
                       SelectBuilderTopLevel Nothing Nothing [] sb' build ->
                           SelectBuilderTopLevel Nothing Nothing ordering (setSelectBuilderProjection sb' proj') build
                       SelectBuilderTopLevel (Just 0) (Just 0) [] sb' build ->
                           SelectBuilderTopLevel (Just 0) (Just 0) ordering (setSelectBuilderProjection sb' proj') build
                       SelectBuilderTopLevel {}
                           | (proj'', qb) <- selectBuilderToQueryBuilder tblPfx sb,
                             Pure proj''' <- next proj'' ->
                               SelectBuilderTopLevel Nothing Nothing (exprWithContext tblPfx (mkOrdering proj'')) (SelectBuilderQ proj''' qb) Nothing
                           | otherwise -> error "buildQuery (Free (QOrderBy ...)): query inspected expression"
             _ -> doJoined

    buildQuery tblPfx (Free (QWindowOver mkWindows mkProjection q' next)) =
        let sb = buildQuery tblPfx (fromF q')

            x = sbProj sb
            windows = mkWindows x
            projection = mkProjection x windows
        in case next projection of
             Pure x' ->
               -- Windowing makes this automatically a top-level (this prevents aggregates from being added directly)
               case setSelectBuilderProjection sb x' of
                 sb'@SelectBuilderTopLevel {} -> sb'
                 sb' -> SelectBuilderTopLevel Nothing Nothing [] sb' Nothing
             _       ->
               let (x', qb) = selectBuilderToQueryBuilder tblPfx (setSelectBuilderProjection sb projection)
               in buildJoinedQuery tblPfx (next x') qb

    buildQuery tblPfx (Free (QLimit limit q' next)) =
        let sb = limitSelectBuilder limit (buildQuery tblPfx (fromF q'))
            x = sbProj sb
        -- In the case of limit, we must directly return whatever was given
        in case next x of
             Pure x' -> setSelectBuilderProjection sb x'

             -- Otherwise, this is going to be part of a join...
             _ -> let (x', qb) = selectBuilderToQueryBuilder tblPfx sb
                  in buildJoinedQuery tblPfx (next x') qb

    buildQuery tblPfx (Free (QOffset offset q' next)) =
        let sb = offsetSelectBuilder offset (buildQuery tblPfx (fromF q'))
            x = sbProj sb
        -- In the case of limit, we must directly return whatever was given
        in case next x of
             Pure x' -> setSelectBuilderProjection sb x'
             -- Otherwise, this is going to be part of a join...
             _ -> let (x', qb) = selectBuilderToQueryBuilder tblPfx sb
                  in buildJoinedQuery tblPfx (next x') qb

    buildQuery tblPfx (Free (QSetOp combine left right next)) =
      buildTableCombination tblPfx combine left right next

    buildQuery tblPfx (Free (QForceSelect selectStmt' over next)) =
      let sb = buildQuery tblPfx (fromF over)
          x = sbProj sb

          selectStmt'' = selectStmt' (sbProj sb)

          sb' = case sb of
                  SelectBuilderTopLevel { sbSelectFn = Nothing } ->
                    sb { sbSelectFn = Just selectStmt'' }
                  SelectBuilderTopLevel { sbSelectFn = Just {} } ->
                    error "Force select too hard"
                  _ -> SelectBuilderTopLevel Nothing Nothing [] sb (Just selectStmt'')
      in case next (sbProj sb') of
           Pure x' -> setSelectBuilderProjection sb' x'
           _ -> let (x', qb) = selectBuilderToQueryBuilder tblPfx sb'
                in buildJoinedQuery tblPfx (next x') qb

    tryBuildGuardsOnly :: forall s x
                        . T.Text
                       -> Free (QF be db s) x
                       -> Maybe (BeamSqlBackendExpressionSyntax be)
                       -> Maybe (x, Maybe (BeamSqlBackendExpressionSyntax be))
    tryBuildGuardsOnly tblPfx next having =
      case tryCollectHaving tblPfx next having of
        (Pure x, having') -> Just (x, having')
        _ -> Nothing

    tryCollectHaving :: forall s x
                      . T.Text
                     -> Free (QF be db s) x
                     -> Maybe (BeamSqlBackendExpressionSyntax be)
                     -> (Free (QF be db s) x, Maybe (BeamSqlBackendExpressionSyntax be))
    tryCollectHaving tblPfx (Free (QGuard cond next)) having = tryCollectHaving tblPfx next (andE' having (Just (exprWithContext tblPfx cond)))
    tryCollectHaving _ next having = (next, having)

    buildTableCombination
      :: forall s x r
       . ( Projectible be r, Projectible be x )
      => T.Text
      -> (BeamSqlBackendSelectTableSyntax be -> BeamSqlBackendSelectTableSyntax be -> BeamSqlBackendSelectTableSyntax be)
      -> QM be db (QNested s) x -> QM be db (QNested s) x -> (x -> Free (QF be db s) r) -> SelectBuilder be db r
    buildTableCombination tblPfx combineTables left right next =
        let leftSb = buildQuery tblPfx (fromF left)
            leftTb = selectBuilderToTableSource tblPfx leftSb
            rightSb = buildQuery tblPfx (fromF right)
            rightTb = selectBuilderToTableSource tblPfx rightSb

            proj = reproject be (fieldNameFunc unqualifiedField) (sbProj leftSb)

            leftTb' | arbitrarilyNestedCombinations = leftTb
                    | sbContainsSetOperation leftSb =
                      let (x', qb) = selectBuilderToQueryBuilder tblPfx leftSb
                      in selectBuilderToTableSource tblPfx (SelectBuilderQ x' qb)
                    | otherwise = leftTb
            rightTb' | arbitrarilyNestedCombinations = rightTb
                     | sbContainsSetOperation rightSb =
                       let (x', qb) = selectBuilderToQueryBuilder tblPfx rightSb
                       in selectBuilderToTableSource tblPfx (SelectBuilderQ x' qb)
                     | otherwise = rightTb

            sb = SelectBuilderSelectSyntax True proj (combineTables leftTb' rightTb')
        in case next proj of
             Pure proj'
               | projOrder be proj (nextTblPfx tblPfx) == projOrder be proj' (nextTblPfx tblPfx) ->
                   setSelectBuilderProjection sb proj'
             _ -> let (x', qb) = selectBuilderToQueryBuilder tblPfx sb
                  in buildJoinedQuery tblPfx (next x') qb

    buildJoinedQuery :: forall s x
                      . Projectible be x
                     => T.Text -> Free (QF be db s) x -> QueryBuilder be -> SelectBuilder be db x
    buildJoinedQuery _ (Pure x) qb = SelectBuilderQ x qb
    buildJoinedQuery tblPfx (Free (QAll mkFrom mkTbl on next)) qb =
        let (newTblNm, newTbl, qb') = buildInnerJoinQuery tblPfx mkFrom mkTbl on qb
        in buildJoinedQuery tblPfx (next (newTblNm, newTbl)) qb'
    buildJoinedQuery tblPfx (Free (QArbitraryJoin q tblNs mkJoin on next)) qb =
      case fromF q of
        Free (QAll mkDbFrom dbMkTbl on' next')
          | (newTbl, newTblNm, qb') <- nextTbl qb tblPfx dbMkTbl,
            Nothing <- exprWithContext tblPfx <$> on' newTbl,
            Pure proj <- next' (newTblNm, newTbl) ->
            let newSource = mkDbFrom (nextTblPfx tblPfx) newTblNm
                on'' = exprWithContext tblPfx <$> on proj
                (from', where') =
                  case qbFrom qb' of
                    Nothing -> (Just newSource, andE' (qbWhere qb) on'')
                    Just oldFrom -> (Just (mkJoin oldFrom newSource on''), qbWhere qb)
            in buildJoinedQuery tblPfx (next proj) (qb' { qbFrom = from', qbWhere = where' })

        q' -> let tblPfx' = tblPfx <> tblNs

                  sb = buildQuery tblPfx' q'
                  tblSource = buildSelect tblPfx' sb
                  newTblNm = tblPfx <> fromString (show (qbNextTblRef qb))

                  newSource = fromTable (tableFromSubSelect tblSource) (Just (newTblNm, Nothing))

                  proj' = reproject be (fieldNameFunc (qualifiedField newTblNm)) (sbProj sb)
                  on' = exprWithContext tblPfx <$> on proj'

                  (from', where') =
                    case qbFrom qb of
                      Nothing -> (Just newSource, andE' (qbWhere qb) on')
                      Just oldFrom -> (Just (mkJoin oldFrom newSource on'), qbWhere qb)

              in buildJoinedQuery tblPfx (next proj') (qb { qbNextTblRef = qbNextTblRef qb + 1
                                                          , qbFrom = from', qbWhere = where' })
    buildJoinedQuery tblPfx (Free (QTwoWayJoin a b mkJoin on next)) qb =
      let (aProj, aSource, qb') =
            case fromF a of
              Free (QAll mkDbFrom dbMkTbl on' next')
                | (newTbl, newTblNm, qb') <- nextTbl qb tblPfx dbMkTbl,
                  Nothing <- on' newTbl, Pure proj <- next' (newTblNm, newTbl) ->
                    (proj, mkDbFrom (nextTblPfx tblPfx) newTblNm, qb')

              a -> let sb = buildQuery tblPfx a
                       tblSource = buildSelect tblPfx sb

                       newTblNm = tblPfx <> fromString (show (qbNextTblRef qb))

                       proj' = reproject be (fieldNameFunc (qualifiedField newTblNm)) (sbProj sb)
                   in (proj', fromTable (tableFromSubSelect tblSource) (Just (newTblNm, Nothing)), qb { qbNextTblRef = qbNextTblRef qb + 1 })

          (bProj, bSource, qb'') =
            case fromF b of
              Free (QAll mkDbFrom dbMkTbl on' next')
                | (newTbl, newTblNm, qb'') <- nextTbl qb' tblPfx dbMkTbl,
                  Nothing <- on' newTbl, Pure proj <- next' (newTblNm, newTbl) ->
                    (proj, mkDbFrom (nextTblPfx tblPfx) newTblNm, qb'')

              b -> let sb = buildQuery tblPfx b
                       tblSource = buildSelect tblPfx sb

                       newTblNm = tblPfx <> fromString (show (qbNextTblRef qb))

                       proj' = reproject be (fieldNameFunc (qualifiedField newTblNm)) (sbProj sb)
                   in (proj', fromTable (tableFromSubSelect tblSource) (Just (newTblNm, Nothing)), qb { qbNextTblRef = qbNextTblRef qb + 1 })

          abSource = mkJoin aSource bSource (exprWithContext tblPfx <$> on (aProj, bProj))

          from' =
            case qbFrom qb'' of
              Nothing -> Just abSource
              Just oldFrom -> Just (innerJoin oldFrom abSource Nothing)

      in buildJoinedQuery tblPfx (next (aProj, bProj)) (qb'' { qbFrom = from' })
    buildJoinedQuery tblPfx (Free (QGuard cond next)) qb =
        buildJoinedQuery tblPfx next (qb { qbWhere = andE' (qbWhere qb) (Just (exprWithContext tblPfx cond)) })
    buildJoinedQuery tblPfx now qb =
      onlyQ now
        (\now' next ->
           let sb = buildQuery tblPfx now'
               tblSource = buildSelect tblPfx sb
               (x', qb') = buildJoinTableSourceQuery tblPfx tblSource (sbProj sb) qb
           in buildJoinedQuery tblPfx (next x') qb')

    onlyQ :: forall s x.
             Free (QF be db s) x
          -> (forall a'. Projectible be a' => Free (QF be db s) a' -> (a' -> Free (QF be db s) x) -> SelectBuilder be db x)
          -> SelectBuilder be db x
    onlyQ (Free (QAll entityNm mkTbl mkOn next)) f =
      f (Free (QAll entityNm mkTbl mkOn (Pure . PreserveLeft))) (next . unPreserveLeft)
--      f (Free (QAll entityNm mkTbl mkOn (Pure . PreserveLeft))) (next . unPreserveLeft)
    onlyQ (Free (QArbitraryJoin entity tblNs mkJoin mkOn next)) f =
      f (Free (QArbitraryJoin entity tblNs mkJoin mkOn Pure)) next
    onlyQ (Free (QTwoWayJoin a b mkJoin mkOn next)) f =
      f (Free (QTwoWayJoin a b mkJoin mkOn Pure)) next
    onlyQ (Free (QSubSelect q' next)) f =
      f (Free (QSubSelect q' Pure)) next
    onlyQ (Free (QLimit limit q' next)) f =
      f (Free (QLimit limit q' Pure)) next
    onlyQ (Free (QOffset offset q' next)) f =
      f (Free (QOffset offset q' Pure)) next
    onlyQ (Free (QSetOp combine a b next)) f =
      f (Free (QSetOp combine a b Pure)) next
    onlyQ (Free (QOrderBy mkOrdering q' next)) f =
      f (Free (QOrderBy mkOrdering q' Pure)) next
    onlyQ (Free (QWindowOver mkWindow mkProj q' next)) f =
      f (Free (QWindowOver mkWindow mkProj q' Pure)) next
    onlyQ (Free (QAggregate mkAgg q' next)) f =
      f (Free (QAggregate mkAgg q' Pure)) next
    onlyQ (Free (QDistinct d q' next)) f =
      f (Free (QDistinct d q' Pure)) next
    onlyQ (Free (QForceSelect s over next)) f =
      f (Free (QForceSelect s over Pure)) next
    onlyQ _ _ = error "impossible"
