-- 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"