packages feed

algebra-sql-0.1.0.0: src/Database/Algebra/SQL/Materialization/CTE.hs

{-# LANGUAGE DoAndIfThenElse #-}
module Database.Algebra.SQL.Materialization.CTE
    ( materialize
    , legacyMaterialize
    ) where

import Control.Monad.State
import Control.Monad.Reader
import qualified Data.IntSet as IntSet
import qualified Data.IntMap as IntMap
import Data.Maybe

import Database.Algebra.SQL.Materialization
import Database.Algebra.SQL.Materialization.Util
import qualified Database.Algebra.SQL.Materialization.Graph as G
import Database.Algebra.SQL.Query
import Database.Algebra.SQL.Tile.Flatten
import Database.Algebra.SQL.Query.Substitution

-- TODO remove after testing
-- | Create a CTE for every root tile and add a binding for every dependency.
legacyMaterialize :: MatFun
legacyMaterialize transformResult =
    ( []
    , if null bindings
      then map (QValueQuery . VQSelect . fst) selects
      else map
           (QValueQuery . VQWith bindings . VQSelect . fst)
           selects
    )
  where bindings            = map f deps
        f (name, (body, _)) = (name, Nothing, VQSelect body)

        selects :: [FlatTile String]
        deps    :: [(String, FlatTile String)]
        (selects, deps)     =
            flattenTransformResult transformResult

-- | 'Gather' provides a state for the current bindings which are gathered and a
-- reader to have access to the set which contains all vertices reachable by the
-- current root vertex.
type Gather = StateT (IntMap.IntMap SelectStmt) (Reader IntSet.IntSet)

materialize :: MatFun
materialize transformResult =
    ( []
    , map (QValueQuery . gather) rootVertices
    )
  where
    (rootTiles, enumDeps)  = flattenTransformResultWith id
                                                        FEVariable
                                                        transformResult
    graph                  = graphFromFlatResult $ enumRootTiles ++ enumDeps
    -- Enumerated root tiles.
    enumRootTiles          = zip [-1, -2 ..] rootTiles
    rootVertices           = map fst enumRootTiles


    -- Create a WITH query for a root vertex.
    gather :: G.Vertex -> ValueQuery
    gather v = case mSelect of
        Just (_, s) ->
            if IntMap.null bindings
            then VQSelect s
            else VQWith withQueryBindings $ VQSelect s
                                         
        Nothing -> error "gather: v is not a root vertex"
      where
        withQueryBindings = IntMap.foldrWithKey toBinding [] bindings

        (mSelect, bindings) = runReader (runStateT (visit v) IntMap.empty)
                                        $ IntSet.fromList $ G.reachable v graph
        
        -- TODO factor out
        toBinding ref select l = ('t' : show ref, Nothing, VQSelect select) : l

    -- The return value indicates whether the parent should inline.
    visit :: G.Vertex -> Gather (Maybe (Int, SelectStmt))
    visit v = do

        alreadyBound <- hasBinding v
        
        if alreadyBound
        -- Binding already added.
        then return Nothing
        else do

            doInline <- shouldInline v

            allResults <- mapM visit $ G.children v graph

            let -- Get those results which need inlining.
                results    = catMaybes allResults
                -- Inline those results into the current label.
                select     =
                    replaceReferencesSelectStmt
                    replace
                    (fromMaybe errorMsg $ G.node v graph)
                -- The lookup function used for substitution.
                replace ref = case lookup ref results of
                    -- It will be available from the bindings of the with query.
                    Nothing -> FETableReference $ 't' : show ref
                    -- We will provide it inline.
                    Just s  -> FESubQuery $ VQSelect s

                errorMsg = error "visit: invalid vertex, no label found"

            if doInline
            then -- Inline this vertex within one or zero parents.
                return $ Just (v, select)
            else do
                -- This vertex is referenced by multiple ones, there is no way
                -- we could inline it.
                addBinding v select

                return Nothing
                
    addBinding :: G.Vertex -> SelectStmt -> Gather ()
    addBinding v select =
        modify $ IntMap.insert v select

    hasBinding :: G.Vertex -> Gather Bool
    hasBinding v = gets $ IntMap.member v

    reachableByRoot :: G.Vertex -> Gather Bool
    reachableByRoot v = asks $ IntSet.member v

    shouldInline :: G.Vertex -> Gather Bool
    shouldInline v = do

        knownParents <- filterM reachableByRoot $ G.parents v graph

        return $ case knownParents of
            []  -> True
            -- Has just one known parent.
            [_] -> True
            _   -> False