queryparser-0.1.0.0: src/Database/Sql/Util/Eval/Concrete.hs
-- Copyright (c) 2017 Uber Technologies, Inc.
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-- THE SOFTWARE.
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Database.Sql.Util.Eval.Concrete where
import Database.Sql.Util.Eval
import Database.Sql.Type.Names
import Database.Sql.Type.Query
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text.Lazy as TL
import Data.List (nub, sort)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map (Map)
import qualified Data.Map as M
import qualified Data.Set as S
import Control.Monad.Identity
import Control.Monad.Except
import Data.Proxy
data Concrete
deriving instance Eq (RecordSet Concrete)
deriving instance Show (RecordSet Concrete)
data SqlValue
= SqlInt Integer
| SqlStr ByteString
| SqlBool Bool
| SqlStruct (Map (StructFieldName ()) SqlValue)
| SqlNull
deriving (Eq, Ord, Show)
-- | truthy tells us whether our Haskell code should consider a SqlValue
-- "true", mostly for the purpose of filtering. It should not be used to
-- construct a SqlValue, as it does not handle NULL correctly for that
-- purpose.
truthy :: SqlValue -> Bool
truthy (SqlBool bool) = bool
truthy (SqlInt int) = int /= 0
truthy (SqlStr str) = not $ BL.null str
truthy (SqlStruct _) = True -- pigeon?
truthy SqlNull = False
instance Evaluation Concrete where
type EvalValue Concrete = SqlValue
type EvalRow Concrete = []
type EvalMonad Concrete = Identity
addItems _ = (pure .) . (++)
removeItems _ exclude unfiltered = pure $ filter (not . (`S.member` S.fromList exclude)) unfiltered
unionItems _ xs ys = pure $ S.toList $ S.union (S.fromList xs) (S.fromList ys)
intersectItems _ xs ys = pure $ S.toList $ S.intersection (S.fromList xs) (S.fromList ys)
distinctItems _ = nub
offsetItems p offset RecordSet{..} = makeRecordSet p recordSetLabels $ drop offset recordSetItems
limitItems p limit RecordSet{..} = makeRecordSet p recordSetLabels $ take limit recordSetItems
filterBy expr (RecordSet cs rs) = do
rs' <- filterM ((truthy <$>) . exprToTable (eval Proxy expr) . makeRowMap cs) rs
pure $ makeRecordSet Proxy cs rs'
handleGroups cs gs = map (makeRecordSet Proxy cs) $ M.elems $ M.fromListWith (++) gs
inList x xs = pure $ SqlBool $ elem x xs
inSubquery x xss = pure $ SqlBool $ elem x $ concat xss
existsSubquery = pure . SqlBool . not . null
atTimeZone _ _ = throwError "AT TIME ZONE not yet handled in concrete evaluation"
handleConstant _ (StringConstant _ str) = pure $ SqlStr str
handleConstant _ (NumericConstant _ num) = pure $ SqlInt $ read $ TL.unpack num
handleConstant _ (NullConstant _) = pure SqlNull
handleConstant _ (BooleanConstant _ bool) = pure $ SqlBool bool
handleConstant _ (TypedConstant _ text dataType) = error "typed constant expression not yet supported" text dataType
handleConstant _ (ParameterConstant _) = throwError "no way to evaluate unsubstituted parameter"
handleCases p ((when_, then_):cases) else_ = do
truthy <$> eval p when_ >>= \case
True -> eval p then_
False -> handleCases p cases else_
handleCases _ [] Nothing = throwError "fell through case with no else"
handleCases p [] (Just expr) = eval p expr
handleFunction _ _ _ _ _ _ _ = throwError "function exprs not yet supported"
handleLike _ _ _ _ _ = throwError "concrete evaluation for LIKE expressions not yet supported"
handleOrder p orders (RecordSet cs rs) = do
pairs <- forM rs $ \ r -> do
k <- (`exprToTable` makeRowMap cs r) $ forM orders $ \case
Order _ (PositionOrExprPosition _ pos _) (OrderAsc _) _ -> Ascending <$> return (r !! (pos - 1))
Order _ (PositionOrExprPosition _ pos _) (OrderDesc _) _ -> Descending <$> return (r !! (pos - 1))
Order _ (PositionOrExprExpr expr) (OrderAsc _) _ -> Ascending <$> eval p expr
Order _ (PositionOrExprExpr expr) (OrderDesc _) _ -> Descending <$> eval p expr
pure (k, r)
pure $ makeRecordSet p cs $ map snd $ sort pairs
handleSubquery [[x]] = pure x
handleSubquery [] = throwError "no rows returned from subquery"
handleSubquery [_] = throwError "wrong number of columns from subquery"
handleSubquery _ = throwError "multiple rows returned from subquery"
handleJoin p (JoinInner _) cond x y = eval p cond x y
handleJoin p (JoinLeft _) cond x y = do
case x of
RecordSet _ [] -> eval p cond x y
RecordSet lcols lrows -> do
set:sets <- forM lrows $ \ lrow -> do
let x' = makeRecordSet p lcols [lrow]
eval p cond x' y >>= \case
RecordSet cols [] -> pure $ makeRecordSet p cols [extendWithNulls cols lrow]
set -> pure set
appendRecordSets p (set:|sets)
handleJoin p (JoinRight _) cond x y = do
case y of
RecordSet _ [] -> eval p cond x y
RecordSet rcols rrows -> do
set:sets <- forM rrows $ \ rrow -> do
let y' = makeRecordSet p rcols [rrow]
eval p cond x y' >>= \case
RecordSet cols [] -> pure $ makeRecordSet p cols [reverse $ extendWithNulls cols $ reverse rrow]
set -> pure set
appendRecordSets p (set:|sets)
handleJoin p (JoinFull info) cond x y = do
RecordSet cs rs <- handleJoin p (JoinLeft info) cond x y
RecordSet _ rs' <- do -- get those from the RHS with no matches on LHS
case y of
RecordSet _ [] -> eval p cond x y
RecordSet rcols rrows -> do
set:sets <- forM rrows $ \ rrow -> do
let y' = makeRecordSet p rcols [rrow]
eval p cond x y' >>= \case
RecordSet cols [] -> pure $ makeRecordSet p cols [reverse $ extendWithNulls cols $ reverse rrow]
RecordSet cols _ -> pure $ makeRecordSet p cols []
appendRecordSets p (set:|sets)
pure $ makeRecordSet p cs $ rs ++ rs'
handleJoin _ (JoinSemi _) cond x y = error "semi joins not yet supported" cond x y
handleStructField expr field = eval (Proxy :: Proxy Concrete) expr >>= \case
SqlStruct m
| Just val <- M.lookup (void field) m
-> pure val
| otherwise
-> throwError "missing field in SQL struct"
_ -> throwError "field access of non-struct value"
handleTypeCast _ _ _ = throwError "concrete evaluation for type cast expressions not yet supported"
binop _ op = M.lookup op $ M.fromList
[ ("+", opAdd)
, ("=", opEq)
, ("AND", opAnd)
]
where
opAdd (SqlInt x) (SqlInt y) = pure $ SqlInt (x + y)
opAdd (SqlInt _) SqlNull = pure SqlNull
opAdd SqlNull (SqlInt _) = pure SqlNull
opAdd _ _ = throwError "unsupported arguments to + operator"
opEq SqlNull _ = pure SqlNull
opEq _ SqlNull = pure SqlNull
opEq x y = pure $ SqlBool $ x == y -- TODO coerce
opAnd SqlNull _ = pure SqlNull
opAnd _ SqlNull = pure SqlNull
opAnd x y = pure $ SqlBool $ truthy x && truthy y
unop _ op = M.lookup op $ M.fromList
[ ("-", neg)
]
where
neg (SqlInt int) = pure $ SqlInt (-int)
neg SqlNull = pure SqlNull
neg _ = throwError "unsupported argument to - operator"
extendWithNulls :: [a] -> [SqlValue] -> [SqlValue]
extendWithNulls (_:xs) (y:ys) = y:extendWithNulls xs ys
extendWithNulls xs [] = map (const SqlNull) xs
extendWithNulls [] _ = error "more values than columns - this should never happen"