----------------------------------------------------------------------------- -- | -- Module : TxtSushi.SQLExecution -- Copyright : (c) Keith Sheppard 2009 -- License : GPL3 or greater -- Maintainer : keithshep@gmail.com -- Stability : experimental -- Portability : portable -- -- Module for executing a SQL statement -- ----------------------------------------------------------------------------- module TxtSushi.SQLExecution ( select, databaseTableToTextTable, textTableToDatabaseTable) where import Data.Char import Data.List import qualified Data.Map as Map import TxtSushi.SQLParser import TxtSushi.Transform import Util.ListUtil -- | an SQL table data structure -- TODO: need allColumnsColumnIdentifiers and allColumnsTableRows so that -- we can filter and order on columns that are selected out. we also -- should track any column ordering that is in place data DatabaseTable = DatabaseTable { -- | the columns in this table columnIdentifiers :: [ColumnIdentifier], -- | the actual table data tableRows :: [[EvaluatedExpression]]} emptyTable = DatabaseTable [] [] stringExpression :: String -> EvaluatedExpression stringExpression string = EvaluatedExpression { preferredType = StringType, maybeIntValue = maybeReadInt string, maybeRealValue = maybeReadReal string, stringValue = string, maybeBoolValue = Just $ (map toLower string /= "false") && (string /= "") && (string /= "0")} intExpression int = EvaluatedExpression { preferredType = IntType, maybeIntValue = Just int, maybeRealValue = Just $ fromIntegral int, stringValue = show int, maybeBoolValue = Just $ int /= 0} realExpression real = EvaluatedExpression { preferredType = RealType, maybeIntValue = Just $ floor real, maybeRealValue = Just real, stringValue = show real, maybeBoolValue = Just $ real /= 0.0} boolExpression bool = EvaluatedExpression { preferredType = BoolType, maybeIntValue = Just $ if bool then 1 else 0, maybeRealValue = Just $ if bool then 1.0 else 0.0, stringValue = show bool, maybeBoolValue = Just bool} intValue :: EvaluatedExpression -> Int intValue evalExpr = case maybeIntValue evalExpr of Just int -> int Nothing -> error $ "failed to parse \"" ++ (stringValue evalExpr) ++ "\" as an integer value" realValue :: EvaluatedExpression -> Double realValue evalExpr = case maybeRealValue evalExpr of Just real -> real Nothing -> error $ "failed to parse \"" ++ (stringValue evalExpr) ++ "\" to a numeric value" boolValue :: EvaluatedExpression -> Bool boolValue evalExpr = case maybeBoolValue evalExpr of Just bool -> bool Nothing -> error $ "failed to parse \"" ++ (stringValue evalExpr) ++ "\" to a boolean value" data ExpressionType = StringType | RealType | IntType | BoolType deriving Eq data EvaluatedExpression = EvaluatedExpression { preferredType :: ExpressionType, stringValue :: String, maybeRealValue :: Maybe Double, maybeIntValue :: Maybe Int, maybeBoolValue :: Maybe Bool} maybeReadBool :: String -> Maybe Bool maybeReadBool boolStr = case map toLower boolStr of "true" -> Just True "1" -> Just True "1.0" -> Just True "false" -> Just False "0" -> Just False "0.0" -> Just False otherwise -> Nothing instance Eq EvaluatedExpression where -- base off of the Ord definition expr1 == expr2 = compare expr1 expr2 == EQ instance Ord EvaluatedExpression where -- compare on bool based on preferred type compare (EvaluatedExpression BoolType _ _ _ (Just b1)) (EvaluatedExpression _ _ _ _ (Just b2)) = compare b1 b2 compare (EvaluatedExpression _ _ _ _ (Just b1)) (EvaluatedExpression BoolType _ _ _ (Just b2)) = compare b1 b2 -- compare on int based on preferred type compare (EvaluatedExpression IntType _ _ (Just i1) _) (EvaluatedExpression _ _ _ (Just i2) _) = compare i1 i2 compare (EvaluatedExpression _ _ _ (Just i1) _) (EvaluatedExpression IntType _ _ (Just i2) _) = compare i1 i2 -- compare on real based on preferred type compare (EvaluatedExpression RealType _ (Just r1) _ _) (EvaluatedExpression _ _ (Just r2) _ _) = compare r1 r2 compare (EvaluatedExpression _ _ (Just r1) _ _) (EvaluatedExpression RealType _ (Just r2) _ _) = compare r1 r2 -- fall back on string type compare expr1 expr2 = compare (stringValue expr1) (stringValue expr2) -- convert a text table to a database table by using the 1st row as column IDs textTableToDatabaseTable :: String -> [[String]] -> DatabaseTable textTableToDatabaseTable tableName (headerNames:tblRows) = DatabaseTable (map makeColId headerNames) (map (map stringExpression) tblRows) where makeColId colName = ColumnIdentifier (Just tableName) colName databaseTableToTextTable :: DatabaseTable -> [[String]] databaseTableToTextTable dbTable = let headerRow = (map columnId (columnIdentifiers dbTable)) tailRows = map (map stringValue) (tableRows dbTable) in headerRow:tailRows -- | perform a SQL select with the given select statement on the -- given table map select :: SelectStatement -> (Map.Map String DatabaseTable) -> DatabaseTable select selectStatement tableMap = let -- TODO: do we need to care about the updated aliases for filtering -- in the "where" part?? fromTbl = case maybeFromTable selectStatement of Nothing -> emptyTable Just fromTblExpr -> evalTableExpression fromTblExpr tableMap filteredTbl = case maybeWhereFilter selectStatement of Nothing -> fromTbl Just expr -> filterRowsBy expr fromTbl orderedTbl = orderRowsBy (orderByItems selectStatement) filteredTbl selectedTbl = evaluateColumnSelections (columnSelections selectStatement) orderedTbl in selectedTbl -- | sorts table rows by the given order by items orderRowsBy :: [OrderByItem] -> DatabaseTable -> DatabaseTable orderRowsBy [] dbTable = dbTable orderRowsBy orderBys dbTable = let -- curry in the order and col ID params to make a row comparison function compareFunc = compareRowsOnOrderItems orderBys (columnIdentifiers dbTable) in dbTable {tableRows = sortBy compareFunc (tableRows dbTable)} -- | Compares two rows using the given OrderByItem and column ID's compareRowsOnOrderItems :: [OrderByItem] -> [ColumnIdentifier] -> [EvaluatedExpression] -> [EvaluatedExpression] -> Ordering compareRowsOnOrderItems orderBys colIds row1 row2 = cascadingOrder $ toOrderList orderBys where toOrderList [] = [] toOrderList (orderBy:orderByTail) = (compareRowsOnOrderItem orderBy colIds row1 row2):(toOrderList orderByTail) -- | Compares two rows using the given OrderByItem and column ID's compareRowsOnOrderItem :: OrderByItem -> [ColumnIdentifier] -> [EvaluatedExpression] -> [EvaluatedExpression] -> Ordering compareRowsOnOrderItem orderBy colIds row1 row2 = let orderExpr = orderExpression orderBy row1Eval = evalExpression orderExpr colIds row1 row2Eval = evalExpression orderExpr colIds row2 rowComp = row1Eval `compare` row2Eval in if orderAscending orderBy then rowComp else reverseOrdering rowComp -- | reverses the given ordering. pretty CRAZY huh??? reverseOrdering :: Ordering -> Ordering reverseOrdering EQ = EQ reverseOrdering LT = GT reverseOrdering GT = LT -- | Evaluate the FROM table part, and returns the FROM table. Also returns -- a mapping of new table names from aliases etc. evalTableExpression :: TableExpression -> (Map.Map String DatabaseTable) -> DatabaseTable evalTableExpression tblExpr tableMap = case tblExpr of TableIdentifier tblName maybeTblAlias -> let -- find the from table map (error if missing) noTblError = error $ "failed to find table named " ++ tblName table = Map.findWithDefault noTblError tblName tableMap in maybeRename maybeTblAlias table -- TODO inner join should allow joining on expressions too!! InnerJoin leftJoinTblExpr rightJoinTblExpr onConditionExpr maybeTblAlias -> let leftJoinTbl = evalTableExpression leftJoinTblExpr tableMap rightJoinTbl = evalTableExpression rightJoinTblExpr tableMap joinCols = extractJoinCols onConditionExpr joinIndices = joinColumnIndices leftJoinTbl rightJoinTbl joinCols joinedTbl = innerJoin joinIndices leftJoinTbl rightJoinTbl in maybeRename maybeTblAlias joinedTbl -- TODO implement me CrossJoin leftJoinTbl maybeTblAlias rightJoinTbl -> error "Sorry! CROSS JOIN is not yet implemented" where maybeRename :: (Maybe String) -> DatabaseTable -> DatabaseTable maybeRename Nothing table = table maybeRename (Just newName) table = table { columnIdentifiers = map (\colId -> colId {maybeTableName = Just newName}) (columnIdentifiers table)} extractJoinCols (FunctionExpression sqlFunc [arg1, arg2]) = case sqlFunc of SQLFunction "AND" _ _ -> extractJoinCols arg1 ++ extractJoinCols arg2 SQLFunction "=" _ _ -> extractJoinColPair arg1 arg2 -- Only expecting "AND" or "=" otherwise -> onPartFormattingError where extractJoinColPair (ColumnExpression col1) (ColumnExpression col2) = [(col1, col2)] -- Only expecting "AND" or "=" extractJoinColPair _ _ = onPartFormattingError -- Only expecting "AND" or "=" extractJoinCols _ = onPartFormattingError onPartFormattingError = error $ "The \"ON\" part of a join must only contain column equalities " ++ "joined together by \"AND\" like: " ++ "\"tbl1.id1 = table2.id1 AND tbl1.firstname = tbl2.name\"" -- | perform an inner join using the given join indices on the given -- tables innerJoin :: [(Int, Int)] -> DatabaseTable -> DatabaseTable -> DatabaseTable innerJoin joinIndices leftJoinTbl rightJoinTbl = DatabaseTable { columnIdentifiers = (columnIdentifiers leftJoinTbl) ++ (columnIdentifiers rightJoinTbl), tableRows = joinTables joinIndices (tableRows leftJoinTbl) (tableRows rightJoinTbl)} -- | convert the column ID pairs into index pairs joinColumnIndices :: DatabaseTable -> DatabaseTable -> [(ColumnIdentifier, ColumnIdentifier)] -> [(Int, Int)] joinColumnIndices leftJoinTbl rightJoinTbl joinCols = let leftHeader = columnIdentifiers leftJoinTbl rightHeader = columnIdentifiers rightJoinTbl in map (idPairToIndexPair leftHeader rightHeader) joinCols -- | convert the column ID pair into an index pair idPairToIndexPair :: [ColumnIdentifier] -> [ColumnIdentifier] -> (ColumnIdentifier, ColumnIdentifier) -> (Int, Int) idPairToIndexPair leftColIds rightColIds joinColPair@(leftColId, rightColId) = let maybePairInOrder = maybeIdPairToIndexPair leftColIds rightColIds joinColPair maybePairSwapped = maybeIdPairToIndexPair leftColIds rightColIds (rightColId, leftColId) in case maybePairInOrder of Just thePairInOrder -> thePairInOrder Nothing -> case maybePairSwapped of Just thePairSwapped -> thePairSwapped Nothing -> error "failed to find given columns" maybeIdPairToIndexPair :: [ColumnIdentifier] -> [ColumnIdentifier] -> (ColumnIdentifier, ColumnIdentifier) -> Maybe (Int, Int) maybeIdPairToIndexPair leftColIds rightColIds (leftColId, rightColId) = do leftIndex <- findIndex (== leftColId) leftColIds rightIndex <- findIndex (== rightColId) rightColIds return (leftIndex, rightIndex) evaluateColumnSelections :: [ColumnSelection] -> DatabaseTable -> DatabaseTable evaluateColumnSelections colSelections dbTable = let selectionTbls = map ($ dbTable) (map evaluateColumnSelection colSelections) in foldl1' tableConcat selectionTbls tableConcat :: DatabaseTable -> DatabaseTable -> DatabaseTable tableConcat dbTable1 dbTable2 = let concatIds = (columnIdentifiers dbTable1) ++ (columnIdentifiers dbTable2) concatRows = zipWith (++) (tableRows dbTable1) (tableRows dbTable2) in DatabaseTable concatIds concatRows evaluateColumnSelection :: ColumnSelection -> DatabaseTable -> DatabaseTable evaluateColumnSelection AllColumns dbTable = dbTable evaluateColumnSelection (AllColumnsFrom srcTblName) dbTable = let colIds = columnIdentifiers dbTable indices = findIndices matchesSrcTblName (map maybeTableName colIds) selectedColIds = selectIndices indices colIds selectedColRows = map (selectIndices indices) (tableRows dbTable) in DatabaseTable selectedColIds selectedColRows where matchesSrcTblName Nothing = False matchesSrcTblName (Just tblName) = tblName == srcTblName selectIndices indices xs = [xs !! i | i <- indices] evaluateColumnSelection (ExpressionColumn expr) dbTable = let tblColIds = columnIdentifiers dbTable exprColId = expressionIdentifier expr evaluatedExprs = map (evalExpression expr tblColIds) (tableRows dbTable) in DatabaseTable [exprColId] (transpose [evaluatedExprs]) -- | This is a little different that a strict equals compare in that it returns -- true if the query column has a Nothing table and the column name part -- matches the reference column's name. Also not that this makes it -- an asymetric comparison columnMatches :: ColumnIdentifier -> ColumnIdentifier -> Bool columnMatches (ColumnIdentifier Nothing queryColIdStr) referenceColumn = -- In this case we don't care about the table name so -- just check to make sure that the column names match up queryColIdStr == columnId referenceColumn columnMatches queryColumn referenceColumn = -- table name is important here so match on the whole object queryColumn == referenceColumn -- | filters the database's table rows on the given expression filterRowsBy :: Expression -> DatabaseTable -> DatabaseTable filterRowsBy filterExpr table = table {tableRows = filter myBoolEvalExpr (tableRows table)} where myBoolEvalExpr row = boolValue $ evalExpression filterExpr (columnIdentifiers table) row evalExpression :: Expression -> [ColumnIdentifier] -> [EvaluatedExpression] -> EvaluatedExpression -- Here's the easy stuff. evaluate constants evalExpression (StringConstantExpression string) _ _ = stringExpression string evalExpression (IntegerConstantExpression int) _ _ = intExpression int evalExpression (RealConstantExpression real) _ _ = realExpression real -- A little bit harder. evaluate a column expression evalExpression (ColumnExpression col) columnIds tblRow = case findIndex (columnMatches col) columnIds of Just colIndex -> tblRow !! colIndex Nothing -> error $ "Failed to find column named: " ++ (prettyFormatColumn col) -- this is where the action is. evaluate a function evalExpression (FunctionExpression sqlFun funArgs) columnIds tblRow -- String functions | sqlFun == upperFunction = stringExpression $ map toUpper (stringValue (head evaluatedArgs)) | sqlFun == lowerFunction = stringExpression $ map toLower (stringValue (head evaluatedArgs)) | sqlFun == trimFunction = stringExpression $ trimSpace (stringValue (head evaluatedArgs)) -- algebraic infix functions | sqlFun == multiplyFunction = algebraWithCoercion (*) (*) evaluatedArgs | sqlFun == divideFunction = realExpression $ foldl1' (/) (map realValue evaluatedArgs) | sqlFun == plusFunction = algebraWithCoercion (+) (+) evaluatedArgs | sqlFun == minusFunction = algebraWithCoercion (-) (-) evaluatedArgs -- boolean infix functions | sqlFun == isFunction = boolExpression (arg1 == arg2) | sqlFun == isNotFunction = boolExpression (arg1 /= arg2) | sqlFun == lessThanFunction = boolExpression (arg1 < arg2) | sqlFun == lessThanOrEqualToFunction = boolExpression (arg1 <= arg2) | sqlFun == greaterThanFunction = boolExpression (arg1 > arg2) | sqlFun == greaterThanOrEqualToFunction = boolExpression (arg1 >= arg2) | sqlFun == andFunction = boolExpression $ (boolValue arg1) && (boolValue arg2) | sqlFun == orFunction = boolExpression $ (boolValue arg1) || (boolValue arg2) where arg1 = head evaluatedArgs arg2 = evaluatedArgs !! 1 evaluatedArgs = map evalArgExpr funArgs evalArgExpr expr = evalExpression expr columnIds tblRow algebraWithCoercion intFunc realFunc args = if any useRealAlgebra args then realExpression $ foldl1' realFunc (map realValue args) else intExpression $ foldl1' intFunc (map intValue args) useRealAlgebra expr = let prefType = preferredType expr in prefType == StringType || prefType == RealType -- | trims leading and trailing spaces trimSpace :: String -> String trimSpace = f . f where f = reverse . dropWhile isSpace