packages feed

txt-sushi 0.3.0 → 0.4.0

raw patch · 16 files changed

+880/−551 lines, 16 filesdep +binarydep +bytestringdep −haskell98dep ~basenew-component:exe:transposecsvnew-component:exe:transposetabPVP ok

version bump matches the API change (PVP)

Dependencies added: binary, bytestring

Dependencies removed: haskell98

Dependency ranges changed: base

API changes (from Hackage documentation)

- Database.TxtSushi.SQLExecution: instance Eq ExpressionType
- Database.TxtSushi.Transform: fileBasedSortTable :: [Int] -> [[String]] -> IO ([[[String]]], [Handle], [FilePath])
- Database.TxtSushi.Transform: mergeAllBy :: (a -> a -> Ordering) -> [[a]] -> [a]
- Database.TxtSushi.Transform: selectColumns :: [Int] -> [[a]] -> [[a]]
- Database.TxtSushi.Util.IOUtil: getAll :: [[t]] -> [[t]]
+ Database.TxtSushi.ExternalSort: defaultByteQuota :: Int
+ Database.TxtSushi.ExternalSort: defaultMaxOpenFiles :: Int
+ Database.TxtSushi.ExternalSort: externalSort :: (Binary b, Ord b) => [b] -> [b]
+ Database.TxtSushi.ExternalSort: externalSortBy :: (Binary b) => (b -> b -> Ordering) -> [b] -> [b]
+ Database.TxtSushi.ExternalSort: externalSortByConstrained :: (Binary b, Integral i) => i -> i -> (b -> b -> Ordering) -> [b] -> [b]
+ Database.TxtSushi.SQLExecution: UseExternalSort :: SortConfiguration
+ Database.TxtSushi.SQLExecution: UseInMemorySort :: SortConfiguration
+ Database.TxtSushi.SQLExecution: data SortConfiguration
+ Database.TxtSushi.SQLExecution: instance Binary EvaluatedExpression
+ Database.TxtSushi.SQLExecution: instance Show EvaluatedExpression
+ Database.TxtSushi.SQLExecution: instance Show SortConfiguration
+ Database.TxtSushi.SQLParser: SelectExpression :: SelectStatement -> Maybe String -> TableExpression
+ Database.TxtSushi.SQLParser: absFunction :: SQLFunction
+ Database.TxtSushi.SQLParser: maybeColumnAlias :: ColumnSelection -> Maybe String
+ Database.TxtSushi.SQLParser: selectStatement :: TableExpression -> SelectStatement
+ Database.TxtSushi.Transform: crossJoinTables :: [[a]] -> [[a]] -> [[a]]
+ Database.TxtSushi.Util.IOUtil: printSingleFileUsage :: IO ()
+ Paths_txt_sushi: getBinDir :: IO FilePath
+ Paths_txt_sushi: getDataDir :: IO FilePath
+ Paths_txt_sushi: getDataFileName :: FilePath -> IO FilePath
+ Paths_txt_sushi: getLibDir :: IO FilePath
+ Paths_txt_sushi: getLibexecDir :: IO FilePath
+ Paths_txt_sushi: version :: Version
- Database.TxtSushi.IO: formatTableWithWidths :: [Char] -> [Int] -> [[[Char]]] -> [Char]
+ Database.TxtSushi.IO: formatTableWithWidths :: String -> [Int] -> [[String]] -> String
- Database.TxtSushi.SQLExecution: select :: SelectStatement -> (Map String DatabaseTable) -> DatabaseTable
+ Database.TxtSushi.SQLExecution: select :: SortConfiguration -> SelectStatement -> (Map String DatabaseTable) -> DatabaseTable
- Database.TxtSushi.SQLParser: ExpressionColumn :: Expression -> ColumnSelection
+ Database.TxtSushi.SQLParser: ExpressionColumn :: Expression -> Maybe String -> ColumnSelection
- Database.TxtSushi.SQLParser: withTrailing :: (Monad m) => m a1 -> m a -> m a
+ Database.TxtSushi.SQLParser: withTrailing :: (Monad m) => m a -> m b -> m b
- Database.TxtSushi.SQLParser: withoutTrailing :: (Show a1) => GenParser tok st a1 -> GenParser tok st a -> GenParser tok st a
+ Database.TxtSushi.SQLParser: withoutTrailing :: (Show s) => GenParser tok st s -> GenParser tok st a -> GenParser tok st a
- Database.TxtSushi.Util.CommandLineArgument: formatCommandLine :: CommandLineDescription -> [Char]
+ Database.TxtSushi.Util.CommandLineArgument: formatCommandLine :: CommandLineDescription -> String
- Database.TxtSushi.Util.IOUtil: getContentsFromFileOrStdin :: [Char] -> IO String
+ Database.TxtSushi.Util.IOUtil: getContentsFromFileOrStdin :: String -> IO String

Files

+ Database/TxtSushi/ExternalSort.hs view
@@ -0,0 +1,165 @@+module Database.TxtSushi.ExternalSort (+    externalSort,+    externalSortBy,+    externalSortByConstrained,+    defaultByteQuota,+    defaultMaxOpenFiles) where++import Control.Monad+import Data.Binary+import Data.Binary.Get+import Data.Int+import qualified Data.ByteString.Lazy as BS+import Data.List+import System.IO+import System.IO.Unsafe+import System.Directory++-- | performs an external sort on the given list using the default resource+--   constraints+externalSort :: (Binary b, Ord b) => [b] -> [b]+externalSort = externalSortBy compare++-- | performs an external sort on the given list using the given comparison+--   function and the default resource constraints+externalSortBy :: (Binary b) => (b -> b -> Ordering) -> [b] -> [b]+externalSortBy = externalSortByConstrained defaultByteQuota defaultMaxOpenFiles++-- | Currently 16 MB. Don't rely on this value staying the same in future+--   releases!+defaultByteQuota :: Int+defaultByteQuota = 16 * 1024 * 1024++-- | Currently 17 files. Don't rely on this value staying the same in future+--   releases!+defaultMaxOpenFiles :: Int+defaultMaxOpenFiles = 17++-- | performs an external sort on the given list using the given resource+--   constraints+{-# NOINLINE externalSortByConstrained #-}+externalSortByConstrained :: (Binary b, Integral i) => i -> i -> (b -> b -> Ordering) -> [b] -> [b]+externalSortByConstrained byteQuota maxOpenFiles cmp xs = unsafePerformIO $ do+    partialSortFiles <- bufferPartialSortsBy (fromIntegral byteQuota) cmp xs+    +    -- now we must merge together the partial sorts+    externalMergeAllBy (fromIntegral maxOpenFiles) cmp partialSortFiles++-- | merge a list of sorted lists into a single sorted list+mergeAllBy :: (a -> a -> Ordering) -> [[a]] -> [a]+mergeAllBy _ [] = []+mergeAllBy _ [singletonList] = singletonList+mergeAllBy cmp (fstList:sndList:[]) = mergeBy cmp fstList sndList+mergeAllBy cmp listList =+    -- recurse after breking the list down by about 1/2 the size+    mergeAllBy cmp (partitionAndMerge 2 cmp listList)++-- TODO add a smart adjustment so that the last partition will not ever+--      be more than 1 element different than the others++-- | partitions the given sorted lists into groupings containing `partitionSize`+--   or fewer lists then merges each of those partitions. So the returned+--   list should normally be shorter than the given list+partitionAndMerge :: Int -> (a -> a -> Ordering) -> [[a]] -> [[a]]+partitionAndMerge _ _ [] = []+partitionAndMerge partitionSize cmp listList =+    map (mergeAllBy cmp) (regularPartitions partitionSize listList)++-- | chops up the given list at regular intervals+regularPartitions :: Int -> [a] -> [[a]]+regularPartitions _ [] = []+regularPartitions partitionSize xs =+    let (currPartition, otherXs) = splitAt partitionSize xs+    in  currPartition : regularPartitions partitionSize otherXs++-- | merge two sorted lists into a single sorted list+mergeBy :: (a -> a -> Ordering) -> [a] -> [a] -> [a]+mergeBy _ []    list2   = list2+mergeBy _ list1 []      = list1+mergeBy comparisonFunction list1@(head1:tail1) list2@(head2:tail2) =+    case head1 `comparisonFunction` head2 of+        GT  -> head2 : mergeBy comparisonFunction list1 tail2+        _   -> head1 : mergeBy comparisonFunction tail1 list2++externalMergePass :: Binary b => Int -> (b -> b -> Ordering) -> [String] -> IO [String]+externalMergePass _ _ [] = return []+externalMergePass maxOpenFiles cmp files = do+    -- we use (maxOpenFiles - 1) because we need to account for the file+    -- handle that we're reading from+    let (mergeNowFiles, mergeLaterFiles) = splitAt (maxOpenFiles - 1) files+    +    mergeNowBinStrs <- readThenDelBinFiles mergeNowFiles+    let mergeNowBinaries = map decodeAll mergeNowBinStrs+    mergedNowFile <- bufferToTempFile $ mergeAllBy cmp mergeNowBinaries+    +    mergedLaterFiles <- externalMergePass maxOpenFiles cmp mergeLaterFiles+    +    return $ mergedNowFile : mergedLaterFiles++externalMergeAllBy :: Binary b => Int -> (b -> b -> Ordering) -> [String] -> IO [b]+externalMergeAllBy _ _ [] = return []+-- TODO do i need to write singleton lists to file in order to keep the max open file promise??+externalMergeAllBy _ _ [singletonFile] =+    readThenDelBinFile singletonFile >>= return . decodeAll+externalMergeAllBy maxOpenFiles cmp files = do+    partiallyMergedFiles <- externalMergePass maxOpenFiles cmp files+    externalMergeAllBy maxOpenFiles cmp partiallyMergedFiles++-- | create a list of parial sorts+bufferPartialSortsBy :: (Binary b) => Int64 -> (b -> b -> Ordering) -> [b] -> IO [String]+bufferPartialSortsBy _ _ [] = return []+bufferPartialSortsBy byteQuota cmp xs = do+    let (sortNowList, sortLaterList) = splitAfterQuota byteQuota xs+        sortedRows = sortBy cmp sortNowList+    sortBuffer <- bufferToTempFile sortedRows+    otherSortBuffers <- bufferPartialSortsBy byteQuota cmp sortLaterList+    return (sortBuffer:otherSortBuffers)++-- TODO not efficiet! we're converting to binary twice so that we don't have+-- the bytestrings buffered to memory during the sort (that would about double+-- our mem usage). I think the right answer is to add a class extending binary+-- that has a sizeOf function+splitAfterQuota :: (Binary b) => Int64 -> [b] -> ([b], [b])+splitAfterQuota _ [] = ([], [])+splitAfterQuota quotaInBytes (binaryHead:binaryTail) =+    let+        quotaRemaining = quotaInBytes - BS.length (encode binaryHead)+        (fstBinsTail, sndBins) = splitAfterQuota quotaRemaining binaryTail+    in+        if quotaRemaining <= 0+        then ([binaryHead], binaryTail)+        else (binaryHead:fstBinsTail, sndBins)++-- | lazily reads then deletes the given files+readThenDelBinFiles :: [String] -> IO [BS.ByteString]+readThenDelBinFiles = sequence . map readThenDelBinFile++-- | lazily reads then deletes the given file after the last byte is read+readThenDelBinFile :: String -> IO BS.ByteString+readThenDelBinFile fileName = do+    binStr <- BS.readFile fileName+    emptyStr <- unsafeInterleaveIO $ removeFile fileName >> return BS.empty+    +    return $ binStr `BS.append` emptyStr++-- | buffer the binaries to a temporary file and return a handle to that file+bufferToTempFile :: (Binary b) => [b] -> IO String+bufferToTempFile [] = return []+bufferToTempFile xs = do+    tempDir <- getTemporaryDirectory+    (tempFilePath, tempFileHandle) <- openBinaryTempFile tempDir "sort.txt"+    +    BS.hPut tempFileHandle (encodeAll xs)+    hClose tempFileHandle+    +    return tempFilePath++encodeAll :: (Binary b) => [b] -> BS.ByteString+encodeAll = BS.concat . map encode++decodeAll :: (Binary b) => BS.ByteString -> [b]+decodeAll bs+    | BS.null bs = []+    | otherwise =+        let (decodedBin, remainingBs, _) = runGetState get bs 0+        in  decodedBin : decodeAll remainingBs
Database/TxtSushi/IO.hs view
@@ -24,7 +24,10 @@     fieldDelimiter :: String,     rowDelimiter :: String} deriving (Show) +csvFormat :: Format csvFormat = Format "\"" "," "\n"++tabDelimitedFormat :: Format tabDelimitedFormat = Format "\"" "\t" "\n"  {- |@@ -33,6 +36,7 @@ doubleQuote :: Format -> String doubleQuote format = (quote format) ++ (quote format) +formatTableWithWidths :: String -> [Int] -> [[String]] -> String formatTableWithWidths _ _ [] = [] formatTableWithWidths boundaryString widths (row:tableTail) =     let@@ -65,10 +69,11 @@  -- this filthy little function is for making the list strict... otherwise -- we run out of memory+seqList :: [a] -> Bool seqList [] = False-seqList (head:tail)-    | head `seq` False = undefined-    | otherwise = seqList tail+seqList (x:xt)+    | x `seq` False = undefined+    | otherwise = seqList xt  maxRowFieldWidths :: [String] -> [Int] -> [Int] maxRowFieldWidths row prevMaxValues =@@ -106,6 +111,7 @@ {- | encode the given text field if it contains any special formatting characters -}+encodeField :: Format -> String -> String encodeField format field =     if (quote format) `isInfixOf` field then         let escapedField = replaceAll field (quote format) (doubleQuote format)
Database/TxtSushi/SQLExecution.hs view
@@ -14,17 +14,30 @@ module Database.TxtSushi.SQLExecution (     select,     databaseTableToTextTable,-    textTableToDatabaseTable) where+    textTableToDatabaseTable,+    SortConfiguration(..)) where +import Data.Binary import Data.Char import Data.List import qualified Data.Map as Map import Text.Regex.Posix +import Database.TxtSushi.ExternalSort import Database.TxtSushi.SQLParser import Database.TxtSushi.Transform import Database.TxtSushi.Util.ListUtil +-- | We will use the sort configuration to determine whether tables should+--   be sorted external or in memory+data SortConfiguration =+    UseInMemorySort |+    UseExternalSort deriving Show++sortByCfg :: (Binary b) => SortConfiguration -> (b -> b -> Ordering) -> [b] -> [b]+sortByCfg UseInMemorySort = sortBy+sortByCfg UseExternalSort = externalSortBy+ -- | 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@@ -36,188 +49,271 @@     -- | the actual table data     tableRows :: [[EvaluatedExpression]]} -emptyTable = DatabaseTable [] []+data GroupedTable = GroupedTable {+    groupColumnIdentifiers :: [ColumnIdentifier],+    tableGroups :: [[[EvaluatedExpression]]]} -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")}+data EvaluatedExpression =+    StringExpression    String |+    RealExpression      Double |+    IntExpression       Int |+    BoolExpression      Bool deriving Show -intExpression int = EvaluatedExpression {-    preferredType   = IntType,-    maybeIntValue   = Just int,-    maybeRealValue  = Just $ fromIntegral int,-    stringValue     = show int,-    maybeBoolValue  = Just $ int /= 0}+-- order evaluated expressions using our type coercion rules where possible+instance Ord EvaluatedExpression where+    compare expr1@(RealExpression _) expr2 = expr1 `realCompare` expr2+    compare expr1 expr2@(RealExpression _) = expr1 `realCompare` expr2+    +    compare expr1@(IntExpression _) expr2 = expr1 `intCompare` expr2+    compare expr1 expr2@(IntExpression _) = expr1 `intCompare` expr2+    +    compare expr1@(BoolExpression _) expr2 = expr1 `boolCompare` expr2+    compare expr1 expr2@(BoolExpression _) = expr1 `boolCompare` expr2+    +    compare expr1 expr2 = expr1 `stringCompare` expr2 -realExpression real = EvaluatedExpression {-    preferredType   = RealType,-    maybeIntValue   = Just $ floor real,-    maybeRealValue  = Just real,-    stringValue     = show real,-    maybeBoolValue  = Just $ real /= 0.0}+realCompare :: EvaluatedExpression -> EvaluatedExpression -> Ordering+realCompare expr1 expr2 =+    maybeCoerceReal expr1 `myCompare` maybeCoerceReal expr2+    where+        myCompare (Just r1) (Just r2) = r1 `compare` r2+        myCompare _ _ = expr1 `stringCompare` expr2 -boolExpression bool = EvaluatedExpression {-    preferredType   = BoolType,-    maybeIntValue   = Nothing,-    maybeRealValue  = Nothing,-    stringValue     = show bool,-    maybeBoolValue  = Just bool}+intCompare :: EvaluatedExpression -> EvaluatedExpression -> Ordering+intCompare expr1 expr2 =+    maybeCoerceInt expr1 `myCompare` maybeCoerceInt expr2+    where+        myCompare (Just i1) (Just i2) = i1 `compare` i2+        myCompare _ _ = expr1 `realCompare` expr2 -intValue :: EvaluatedExpression -> Int-intValue evalExpr = case maybeIntValue evalExpr of+boolCompare :: EvaluatedExpression -> EvaluatedExpression -> Ordering+boolCompare expr1 expr2 =+    maybeCoerceBool expr1 `myCompare` maybeCoerceBool expr2+    where+        myCompare (Just b1) (Just b2) = b1 `compare` b2+        myCompare _ _ = expr1 `stringCompare` expr2++stringCompare :: EvaluatedExpression -> EvaluatedExpression -> Ordering+stringCompare expr1 expr2 = coerceString expr1 `compare` coerceString expr2++-- base equality off of the Ord definition. pretty simple huh?+instance Eq EvaluatedExpression where+    expr1 == expr2 = expr1 `compare` expr2 == EQ++instance Binary EvaluatedExpression where+    put (StringExpression  s)   = put (0 :: Word8) >> put s+    put (RealExpression r)      = put (1 :: Word8) >> put r+    put (IntExpression i)       = put (2 :: Word8) >> put i+    put (BoolExpression b)      = put (3 :: Word8) >> put b+    +    get = do+        typeWord <- get :: Get Word8+        case typeWord of+            0 -> get >>= return . StringExpression+            1 -> get >>= return . RealExpression+            2 -> get >>= return . IntExpression+            3 -> get >>= return . BoolExpression+            _ -> error $ "unexpected type word value: " ++ show typeWord++coerceString :: EvaluatedExpression -> String+coerceString (StringExpression string)  = string+coerceString (RealExpression real)      = show real+coerceString (IntExpression int)        = show int+coerceString (BoolExpression bool)      = if bool then "true" else "false"++maybeCoerceInt :: EvaluatedExpression -> Maybe Int+maybeCoerceInt (StringExpression string) = maybeReadInt string+maybeCoerceInt (RealExpression real)     = Just $ floor real -- TOOD: floor OK for negatives too?+maybeCoerceInt (IntExpression int)       = Just int+maybeCoerceInt (BoolExpression _)        = Nothing++coerceInt :: EvaluatedExpression -> Int+coerceInt evalExpr = case maybeCoerceInt evalExpr of     Just int -> int     Nothing ->-        error $ "could not convert \"" ++ (stringValue evalExpr) +++        error $ "could not convert \"" ++ (coerceString evalExpr) ++                 "\" to an integer value" -realValue :: EvaluatedExpression -> Double-realValue evalExpr = case maybeRealValue evalExpr of+maybeCoerceReal :: EvaluatedExpression -> Maybe Double+maybeCoerceReal (StringExpression string) = maybeReadReal string+maybeCoerceReal (RealExpression real)     = Just real+maybeCoerceReal (IntExpression int)       = Just $ fromIntegral int+maybeCoerceReal (BoolExpression _)        = Nothing++coerceReal :: EvaluatedExpression -> Double+coerceReal evalExpr = case maybeCoerceReal evalExpr of     Just real -> real     Nothing ->-        error $ "could not convert \"" ++ (stringValue evalExpr) +++        error $ "could not convert \"" ++ (coerceString evalExpr) ++                 "\" to a numeric value" -boolValue :: EvaluatedExpression -> Bool-boolValue evalExpr = case maybeBoolValue evalExpr of-    Just bool -> bool-    Nothing ->-        error $ "could not convert \"" ++ (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+maybeReadBool boolStr = case map toLower $ trimSpace 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 expr1 expr2-        | type1 == RealType || type2 == RealType    = realCompare expr1 expr2-        | type1 == IntType || type2 == IntType      = intCompare expr1 expr2-        | type1 == BoolType || type2 == BoolType    = boolCompare expr1 expr2-        | otherwise                                 = stringCompare expr1 expr2-        -        where-            type1 = preferredType expr1-            type2 = preferredType expr2--realCompare (EvaluatedExpression _ _ (Just r1) _ _) (EvaluatedExpression _ _ (Just r2) _ _) =-    compare r1 r2-realCompare expr1 expr2 = stringCompare expr1 expr2--intCompare (EvaluatedExpression _ _ _ (Just i1) _) (EvaluatedExpression _ _ _ (Just i2) _) =-    compare i1 i2-intCompare expr1 expr2 = realCompare expr1 expr2+    _           -> Nothing -boolCompare (EvaluatedExpression _ _ _ _ (Just b1)) (EvaluatedExpression _ _ _ _ (Just b2)) =-    compare b1 b2-boolCompare expr1 expr2 = stringCompare expr1 expr2+maybeCoerceBool :: EvaluatedExpression -> Maybe Bool+maybeCoerceBool (StringExpression string) = maybeReadBool string+maybeCoerceBool (RealExpression _)        = Nothing+maybeCoerceBool (IntExpression _)         = Nothing+maybeCoerceBool (BoolExpression bool)     = Just bool -stringCompare expr1 expr2 = stringValue expr1 `compare` stringValue expr2+coerceBool :: EvaluatedExpression -> Bool+coerceBool evalExpr = case maybeCoerceBool evalExpr of+    Just bool -> bool+    Nothing ->+        error $ "could not convert \"" ++ (coerceString evalExpr) +++                "\" to a boolean value"  -- 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)+textTableToDatabaseTable tblName (headerNames:tblRows) =+    DatabaseTable (map makeColId headerNames) (map (map StringExpression) tblRows)     where-        makeColId colName = ColumnIdentifier (Just tableName) colName+        makeColId colName = ColumnIdentifier (Just tblName) colName+textTableToDatabaseTable tblName [] =+    error $ "invalid table \"" ++ tblName ++ "\". There is no header row"  databaseTableToTextTable :: DatabaseTable -> [[String]] databaseTableToTextTable dbTable =     let-        headerRow = (map columnId (columnIdentifiers dbTable))-        tailRows = map (map stringValue) (tableRows dbTable)+        headerRow = map columnId (columnIdentifiers dbTable)+        tailRows = map (map coerceString) (tableRows dbTable)     in         headerRow:tailRows +{-+optimizeClassicJoins :: SelectStatement -> SelectStatement+optimizeClassicJoins selectStmt@(SelectStatement _ (Just fromTbl) (Just whereFilter) _ _) =+    let (optFromTbl, optMaybeWhereFilter) = optimizeFromWhere Nothing fromTbl whereFilter+    in  selectStmt {+            maybeFromTable = Just optFromTbl,+            maybeWhereFilter = optMaybeWhereFilter}+optimizeClassicJoins selectStmt = selectStmt++optimizeFromWhere :: Maybe String -> TableExpression -> Maybe Expression -> (TableExpression, Maybe Expression)+optimizeFromWhere maybeParentAlias _ Nothing =+    (fromTbl, Just expr)+optimizeFromWhere maybeParentAlias fromTbl@(InnerJoin leftJoinTbl rightJoinTbl _ maybeChildAlias) (Just expr) =+    let+        maybeAlias = case maybeParentAlias of+            Nothing -> maybeChildAlias+            Just -  -> maybeParentAlias+        (optLeftTbl, expr2) = optimizeFromWhere maybeAlias leftJoinTbl expr+        (optRightTbl, expr3) = optimizeFromWhere maybeAlias rightJoinTbl expr2+        optFromTbl = fromTbl {+            leftJoinTable,+            rightJoinTable}+    in+        (optFromTbl, Just expr3)+-}+ -- | perform a SQL select with the given select statement on the --   given table map-select :: SelectStatement -> (Map.Map String DatabaseTable) -> DatabaseTable-select selectStatement tableMap =+select :: SortConfiguration -> SelectStatement -> (Map.Map String DatabaseTable) -> DatabaseTable+select sortCfg selectStmt 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+        fromTbl = case maybeFromTable selectStmt of+            Nothing -> DatabaseTable [] []+            Just fromTblExpr -> evalTableExpression sortCfg fromTblExpr tableMap+        fromTblWithAliases =+            appendAliasColumns (columnSelections selectStmt) fromTbl+        filteredTbl = case maybeWhereFilter selectStmt of+            Nothing -> fromTblWithAliases+            Just expr -> filterRowsBy expr fromTblWithAliases     in-        case maybeGroupByHaving selectStatement of+        case maybeGroupByHaving selectStmt of             Nothing ->-                if selectStatementContainsAggregates selectStatement then-                    finishWithAggregateSelect selectStatement [filteredTbl]+                if selectStatementContainsAggregates selectStmt then+                    -- for the case where we find aggregate functions but+                    -- no "GROUP BY" part, that means we should apply the+                    -- aggregate to the table as a single group+                    finishWithAggregateSelect+                        sortCfg+                        selectStmt+                        (GroupedTable (columnIdentifiers filteredTbl) [tableRows filteredTbl])                 else-                    finishWithNormalSelect selectStatement filteredTbl+                    finishWithNormalSelect sortCfg selectStmt filteredTbl             Just groupByPart ->                 let-                    tblGroups = performGroupBy groupByPart filteredTbl+                    tblGroups = performGroupBy sortCfg groupByPart filteredTbl                 in-                    finishWithAggregateSelect selectStatement tblGroups+                    finishWithAggregateSelect sortCfg selectStmt tblGroups -finishWithNormalSelect selectStatement filteredDbTable =+-- TODO this approach wont let you refer to an alias in the column selection+appendAliasColumns :: [ColumnSelection] -> DatabaseTable -> DatabaseTable+appendAliasColumns [] dbTable = dbTable+appendAliasColumns cols dbTable@(DatabaseTable colIds tblRows) =+    let colAliasExprs = extractColumnAliases cols+        -- TODO which is the right fold here?+        evaluatedColExprsTbl = foldl1' tableConcat (evalAliasCols colAliasExprs)+    in+        if null colAliasExprs+        then dbTable+        else dbTable `tableConcat` evaluatedColExprsTbl+    where+        evalAliasCols :: [(ColumnIdentifier, Expression)] -> [DatabaseTable]+        evalAliasCols [] = []+        evalAliasCols ((aliasColId, aliasExpr) : tailAliasExprs) =+            DatabaseTable [aliasColId] [[evalExpression aliasExpr colIds row] | row <- tblRows] :+            evalAliasCols tailAliasExprs++extractColumnAliases :: [ColumnSelection] -> [(ColumnIdentifier, Expression)]+extractColumnAliases [] = []+extractColumnAliases ((ExpressionColumn expr (Just alias)) : colsTail) =+    (ColumnIdentifier Nothing alias, expr) : extractColumnAliases colsTail+extractColumnAliases xs = extractColumnAliases $ tail xs++finishWithNormalSelect :: SortConfiguration -> SelectStatement -> DatabaseTable -> DatabaseTable+finishWithNormalSelect sortCfg selectStmt filteredDbTable =     let-        orderedTbl = orderRowsBy (orderByItems selectStatement) filteredDbTable+        orderedTbl =+            orderRowsBy sortCfg (orderByItems selectStmt) filteredDbTable         selectedTbl =-            evaluateColumnSelections (columnSelections selectStatement) orderedTbl+            evaluateColumnSelections (columnSelections selectStmt) orderedTbl     in         selectedTbl -finishWithAggregateSelect selectStatement aggregateTbls =+finishWithAggregateSelect :: SortConfiguration -> SelectStatement -> GroupedTable -> DatabaseTable+finishWithAggregateSelect sortCfg selectStmt aggregateTbls =     let-        orderedTbls = orderTablesBy (orderByItems selectStatement) aggregateTbls+        orderedTbls =+            orderGroupsBy sortCfg (orderByItems selectStmt) aggregateTbls         selectedTbl =-            evaluateAggregateColumnSelections (columnSelections selectStatement) orderedTbls+            evaluateAggregateColumnSelections (columnSelections selectStmt) orderedTbls     in         selectedTbl -performGroupBy :: ([Expression], Maybe Expression) -> DatabaseTable -> [DatabaseTable]-performGroupBy (groupByExprs, maybeExpr) dbTable =+performGroupBy :: SortConfiguration -> ([Expression], Maybe Expression) -> DatabaseTable -> GroupedTable+performGroupBy sortCfg (groupByExprs, maybeExpr) dbTable =     let-        tblGroups = groupRowsBy groupByExprs dbTable+        tblGroups = groupRowsBy sortCfg groupByExprs dbTable     in         case maybeExpr of             Nothing -> tblGroups-            Just expr -> filterTablesBy expr tblGroups+            Just expr -> filterGroupsBy expr tblGroups  -- | sorts table rows by the given order by items-orderRowsBy :: [OrderByItem] -> DatabaseTable -> DatabaseTable-orderRowsBy [] dbTable = dbTable-orderRowsBy orderBys dbTable =+orderRowsBy :: SortConfiguration -> [OrderByItem] -> DatabaseTable -> DatabaseTable+orderRowsBy _ [] dbTable = dbTable+orderRowsBy sortCfg orderBys dbTable =     let         -- curry in the order and col ID params to make a row comparison function         compareRows = compareRowsOnOrderItems orderBys (columnIdentifiers dbTable)-        sortedRows = sortBy compareRows (tableRows dbTable)+        sortedRows = sortByCfg sortCfg compareRows (tableRows dbTable)     in         dbTable {tableRows = sortedRows} -orderTablesBy :: [OrderByItem] -> [DatabaseTable] -> [DatabaseTable]-orderTablesBy [] dbTables = dbTables-orderTablesBy orderBys dbTables =-    sortBy (compareTablesOnOrderItems orderBys) dbTables+orderGroupsBy :: SortConfiguration -> [OrderByItem] -> GroupedTable -> GroupedTable+orderGroupsBy _ [] groupedTable = groupedTable+orderGroupsBy sortCfg orderBys groupedTable =+    let+        -- curry in the order and col ID params to make a group comparison function+        compareGroups = compareGroupsOnOrderItems orderBys (groupColumnIdentifiers groupedTable)+        sortedGroups = sortByCfg sortCfg compareGroups (tableGroups groupedTable)+    in+        groupedTable {tableGroups = sortedGroups}  -- | Compares two rows using the given OrderByItems and column ID's compareRowsOnOrderItems :: [OrderByItem] -> [ColumnIdentifier] -> [EvaluatedExpression] -> [EvaluatedExpression] -> Ordering@@ -240,24 +336,24 @@         else             reverseOrdering rowComp -compareTablesOnOrderItems :: [OrderByItem] -> DatabaseTable -> DatabaseTable -> Ordering-compareTablesOnOrderItems orderBys dbTable1 dbTable2 =+compareGroupsOnOrderItems :: [OrderByItem] -> [ColumnIdentifier] -> [[EvaluatedExpression]] -> [[EvaluatedExpression]] -> Ordering+compareGroupsOnOrderItems orderBys colIds group1 group2 =     cascadingOrder $ toOrderList orderBys     where         toOrderList [] = []         toOrderList (orderBy:orderByTail) =-            (compareTablesOnOrderItem orderBy dbTable1 dbTable2):(toOrderList orderByTail)+            (compareGroupsOnOrderItem orderBy colIds group1 group2):(toOrderList orderByTail) -compareTablesOnOrderItem :: OrderByItem -> DatabaseTable -> DatabaseTable -> Ordering-compareTablesOnOrderItem orderBy dbTable1 dbTable2 =+compareGroupsOnOrderItem :: OrderByItem -> [ColumnIdentifier] -> [[EvaluatedExpression]] -> [[EvaluatedExpression]] -> Ordering+compareGroupsOnOrderItem orderBy colIds group1 group2 =     let         orderExpr = orderExpression orderBy-        rowComp = compareTablesOnExpression orderExpr dbTable1 dbTable2+        grpComp = compareGroupsOnExpression orderExpr colIds group1 group2     in         if orderAscending orderBy then-            rowComp+            grpComp         else-            reverseOrdering rowComp+            reverseOrdering grpComp  -- | reverses the given ordering. pretty CRAZY huh??? reverseOrdering :: Ordering -> Ordering@@ -283,18 +379,15 @@     in         row1Eval `compare` row2Eval -compareTablesOnExpression :: Expression -> DatabaseTable -> DatabaseTable -> Ordering-compareTablesOnExpression expr tbl1 tbl2 =-    let-        tbl1Eval = evalAggregateExpression expr tbl1-        tbl2Eval = evalAggregateExpression expr tbl2-    in-        tbl1Eval `compare` tbl2Eval+compareGroupsOnExpression :: Expression -> [ColumnIdentifier] -> [[EvaluatedExpression]] -> [[EvaluatedExpression]] -> Ordering+compareGroupsOnExpression expr colIds grp1 grp2 =+    evalExprOn grp1 `compare` evalExprOn grp2+    where+        evalExprOn grp = evalAggregateExpression expr (DatabaseTable colIds grp) -groupRowsBy :: [Expression] -> DatabaseTable -> [DatabaseTable]-groupRowsBy groupByExprs dbTable =-    -- create a new table for every row grouping-    map replaceTableRows rowGroups+groupRowsBy :: SortConfiguration -> [Expression] -> DatabaseTable -> GroupedTable+groupRowsBy sortCfg groupByExprs dbTable =+    GroupedTable (columnIdentifiers dbTable) rowGroups     where         tblRows = tableRows dbTable         @@ -302,14 +395,13 @@         compareRows = compareRowsOnExpressions groupByExprs (columnIdentifiers dbTable)         row1 `rowsEq` row2 = (row1 `compareRows` row2) == EQ         -        sortedRows = sortBy compareRows tblRows+        sortedRows = sortByCfg sortCfg compareRows tblRows         rowGroups = groupBy rowsEq sortedRows-        replaceTableRows newRows = dbTable {tableRows = newRows}  -- | 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 =+evalTableExpression :: SortConfiguration -> TableExpression -> (Map.Map String DatabaseTable) -> DatabaseTable+evalTableExpression sortCfg tblExpr tableMap =     case tblExpr of         TableIdentifier tblName maybeTblAlias ->             let@@ -322,30 +414,40 @@         -- TODO inner join should allow joining on expressions too!!         InnerJoin leftJoinTblExpr rightJoinTblExpr onConditionExpr maybeTblAlias ->             let-                leftJoinTbl = evalTableExpression leftJoinTblExpr tableMap-                rightJoinTbl = evalTableExpression rightJoinTblExpr tableMap+                leftJoinTbl = evalTableExpression sortCfg leftJoinTblExpr tableMap+                rightJoinTbl = evalTableExpression sortCfg rightJoinTblExpr tableMap                 joinCols = extractJoinCols onConditionExpr                 joinIndices = joinColumnIndices leftJoinTbl rightJoinTbl joinCols                 joinedTbl = innerJoin joinIndices leftJoinTbl rightJoinTbl             in                 maybeRename maybeTblAlias joinedTbl         +        SelectExpression selectStmt maybeTblAlias ->+            maybeRename maybeTblAlias (select sortCfg selectStmt tableMap)+                 -- TODO implement me-        CrossJoin leftJoinTbl maybeTblAlias rightJoinTbl ->-            error "Sorry! CROSS JOIN is not yet implemented"+        CrossJoin leftJoinTblExpr rightJoinTblExpr maybeTblAlias ->+            let+                leftJoinTbl = evalTableExpression sortCfg leftJoinTblExpr tableMap+                rightJoinTbl = evalTableExpression sortCfg rightJoinTblExpr tableMap+                joinedTbl = crossJoin leftJoinTbl rightJoinTbl+            in+                maybeRename maybeTblAlias joinedTbl+         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 :: Expression -> [(ColumnIdentifier, ColumnIdentifier)] extractJoinCols (FunctionExpression sqlFunc [arg1, arg2]) =     case sqlFunc of         SQLFunction "AND" _ _   -> extractJoinCols arg1 ++ extractJoinCols arg2         SQLFunction "=" _ _     -> extractJoinColPair arg1 arg2                  -- Only expecting "AND" or "="-        otherwise -> onPartFormattingError+        _ -> onPartFormattingError     where         extractJoinColPair (ColumnExpression col1) (ColumnExpression col2) = [(col1, col2)]         @@ -355,6 +457,7 @@ -- Only expecting "AND" or "=" extractJoinCols _ = onPartFormattingError +onPartFormattingError :: a onPartFormattingError =     error $ "The \"ON\" part of a join must only contain column equalities " ++             "joined together by \"AND\" like: " ++@@ -367,6 +470,13 @@     columnIdentifiers = (columnIdentifiers leftJoinTbl) ++ (columnIdentifiers rightJoinTbl),     tableRows = joinTables joinIndices (tableRows leftJoinTbl) (tableRows rightJoinTbl)} +-- | perform a cross join using the given join indices on the given+--   tables+crossJoin :: DatabaseTable -> DatabaseTable -> DatabaseTable+crossJoin leftJoinTbl rightJoinTbl = DatabaseTable {+    columnIdentifiers = (columnIdentifiers leftJoinTbl) ++ (columnIdentifiers rightJoinTbl),+    tableRows = crossJoinTables (tableRows leftJoinTbl) (tableRows rightJoinTbl)}+ -- | convert the column ID pairs into index pairs joinColumnIndices :: DatabaseTable -> DatabaseTable -> [(ColumnIdentifier, ColumnIdentifier)] -> [(Int, Int)] joinColumnIndices leftJoinTbl rightJoinTbl joinCols =@@ -411,27 +521,29 @@     in         DatabaseTable concatIds concatRows -evaluateAggregateColumnSelections :: [ColumnSelection] -> [DatabaseTable] -> DatabaseTable+evaluateAggregateColumnSelections :: [ColumnSelection] -> GroupedTable -> DatabaseTable evaluateAggregateColumnSelections colSelections tblGroups =     let         selectionTbls = map ($ tblGroups) (map evaluateAggregateColumnSelection colSelections)     in         foldl1' tableConcat selectionTbls -evaluateAggregateColumnSelection :: ColumnSelection -> [DatabaseTable] -> DatabaseTable-evaluateAggregateColumnSelection AllColumns tblGroups =+evaluateAggregateColumnSelection :: ColumnSelection -> GroupedTable -> DatabaseTable+evaluateAggregateColumnSelection AllColumns _ =     error "* is not allowed for aggregate column selections"-evaluateAggregateColumnSelection (AllColumnsFrom srcTblName) tblGroups =+evaluateAggregateColumnSelection (AllColumnsFrom srcTblName) _ =     error $ srcTblName ++ ".* is not allowed for aggregate column selections"-evaluateAggregateColumnSelection (ExpressionColumn expr) [] =-    error $ "internal error: empty aggregate table group"-evaluateAggregateColumnSelection (ExpressionColumn expr) tblGroups@(headGrp:tailGrp) =+evaluateAggregateColumnSelection (ExpressionColumn expr maybeAlias) groupedTbl =     let-        tblColIds = columnIdentifiers headGrp-        exprColId = expressionIdentifier expr-        evaluatedExprs = map (evalAggregateExpression expr) tblGroups+        tbls = map makeTbl (tableGroups groupedTbl)+        evaluatedExprs = map (evalAggregateExpression expr) tbls+        exprColId = case maybeAlias of+            Nothing     -> expressionIdentifier expr+            Just alias  -> (expressionIdentifier expr) {columnId = alias}     in         DatabaseTable [exprColId] (transpose [evaluatedExprs])+    where+        makeTbl grp = DatabaseTable (groupColumnIdentifiers groupedTbl) grp  evaluateColumnSelection :: ColumnSelection -> DatabaseTable -> DatabaseTable evaluateColumnSelection AllColumns dbTable = dbTable@@ -447,10 +559,12 @@         matchesSrcTblName Nothing           = False         matchesSrcTblName (Just tblName)    = tblName == srcTblName         selectIndices indices xs = [xs !! i | i <- indices]-evaluateColumnSelection (ExpressionColumn expr) dbTable =+evaluateColumnSelection (ExpressionColumn expr maybeAlias) dbTable =     let         tblColIds = columnIdentifiers dbTable-        exprColId = expressionIdentifier expr+        exprColId = case maybeAlias of+            Nothing     -> expressionIdentifier expr+            Just alias  -> (expressionIdentifier expr) {columnId = alias}         evaluatedExprs = map (evalExpression expr tblColIds) (tableRows dbTable)     in         DatabaseTable [exprColId] (transpose [evaluatedExprs])@@ -474,21 +588,23 @@ filterRowsBy filterExpr table =     table {tableRows = filter myBoolEvalExpr (tableRows table)}     where myBoolEvalExpr row =-            boolValue $ evalExpression filterExpr (columnIdentifiers table) row+            coerceBool $ evalExpression filterExpr (columnIdentifiers table) row -filterTablesBy :: Expression -> [DatabaseTable] -> [DatabaseTable]-filterTablesBy expr dbTables =-    filter filterFunc dbTables+filterGroupsBy :: Expression -> GroupedTable -> GroupedTable+filterGroupsBy expr groupedTbl =+    groupedTbl {tableGroups = map tableRows filteredTbls}     where-        filterFunc = boolValue . evalAggregateExpression expr+        makeTbl grp = DatabaseTable (groupColumnIdentifiers groupedTbl) grp+        filterFunc = coerceBool . evalAggregateExpression expr+        filteredTbls = filter filterFunc (map makeTbl (tableGroups groupedTbl))  -- | evaluate the given expression against a table --   TODO need better error detection and reporting for non-aggregate --   expressions evalAggregateExpression :: Expression -> DatabaseTable -> EvaluatedExpression-evalAggregateExpression (StringConstantExpression string) _ = stringExpression string-evalAggregateExpression (IntegerConstantExpression int) _   = intExpression int-evalAggregateExpression (RealConstantExpression real) _     = realExpression real+evalAggregateExpression (StringConstantExpression string) _ = StringExpression string+evalAggregateExpression (IntegerConstantExpression int) _   = IntExpression int+evalAggregateExpression (RealConstantExpression real) _     = RealExpression real evalAggregateExpression (ColumnExpression col) dbTable =     case findIndex (columnMatches col) (columnIdentifiers dbTable) of         Just colIndex -> (head $ tableRows dbTable) !! colIndex@@ -509,9 +625,9 @@  -- | evaluate the given expression against a table row evalExpression :: Expression -> [ColumnIdentifier] -> [EvaluatedExpression] -> EvaluatedExpression-evalExpression (StringConstantExpression string) _ _    = stringExpression string-evalExpression (IntegerConstantExpression int) _ _      = intExpression int-evalExpression (RealConstantExpression real) _ _        = realExpression real+evalExpression (StringConstantExpression string) _ _    = StringExpression string+evalExpression (IntegerConstantExpression int) _ _      = IntExpression int+evalExpression (RealConstantExpression real) _ _        = RealExpression real evalExpression (ColumnExpression col) columnIds tblRow =     case findIndex (columnMatches col) columnIds of         Just colIndex -> tblRow !! colIndex@@ -521,59 +637,54 @@     where         evalArgExpr expr = evalExpression expr columnIds tblRow +-- TODO this ugly function needs to be modularized+evalSQLFunction :: SQLFunction -> [EvaluatedExpression] -> EvaluatedExpression evalSQLFunction sqlFun evaluatedArgs     -- Global validation     -- TODO this error should be more helpful than it is-    | not $ argCountIsValid =+    | argCountIsInvalid =         error $ "cannot apply " ++ show (length evaluatedArgs) ++                 " arguments to " ++ functionName sqlFun          -- String functions-    | sqlFun == upperFunction = stringExpression $ map toUpper (stringValue arg1)-    | sqlFun == lowerFunction = stringExpression $ map toLower (stringValue arg1)-    | sqlFun == trimFunction = stringExpression $ trimSpace (stringValue arg1)-    | sqlFun == concatenateFunction = stringExpression $ concat (map stringValue evaluatedArgs)+    | sqlFun == upperFunction = StringExpression $ map toUpper (coerceString arg1)+    | sqlFun == lowerFunction = StringExpression $ map toLower (coerceString arg1)+    | sqlFun == trimFunction = StringExpression $ trimSpace (coerceString arg1)+    | sqlFun == concatenateFunction = StringExpression $ concat (map coerceString evaluatedArgs)     | sqlFun == substringFromToFunction =-        stringExpression $ take (intValue arg3) (drop (intValue arg2 - 1) (stringValue arg1))+        StringExpression $ take (coerceInt arg3) (drop (coerceInt arg2 - 1) (coerceString arg1))     | sqlFun == substringFromFunction =-        stringExpression $ drop (intValue arg2 - 1) (stringValue arg1)-    | sqlFun == regexMatchFunction = boolExpression $ (stringValue arg1) =~ (stringValue arg2)+        StringExpression $ drop (coerceInt arg2 - 1) (coerceString arg1)+    | sqlFun == regexMatchFunction = BoolExpression $ (coerceString arg1) =~ (coerceString arg2)     -    -- negate-    | sqlFun == negateFunction =-        if length evaluatedArgs /= 1 then-            error $-                "internal error: found a negate function with multiple args. " ++-                "please report this error"-        else-            let evaldArg = head evaluatedArgs-            in-                if useRealAlgebra evaldArg then-                    realExpression $ negate (realValue evaldArg)-                else-                    intExpression $ negate (intValue evaldArg)+    -- unary functions+    | sqlFun == absFunction = evalUnaryAlgebra abs abs+    | sqlFun == negateFunction = evalUnaryAlgebra negate negate          -- algebraic     | sqlFun == multiplyFunction = algebraWithCoercion (*) (*) evaluatedArgs-    | sqlFun == divideFunction = realExpression $ (realValue arg1) / (realValue arg2)+    | sqlFun == divideFunction = RealExpression $ (coerceReal arg1) / (coerceReal arg2)     | sqlFun == plusFunction = algebraWithCoercion (+) (+) evaluatedArgs     | sqlFun == minusFunction = algebraWithCoercion (-) (-) evaluatedArgs          -- boolean-    | 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)-    | sqlFun == notFunction = boolExpression $ not (boolValue arg1)+    | 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 $ (coerceBool arg1) && (coerceBool arg2)+    | sqlFun == orFunction = BoolExpression $ (coerceBool arg1) || (coerceBool arg2)+    | sqlFun == notFunction = BoolExpression $ not (coerceBool arg1)          -- aggregate+    -- TODO AVG(...) holds the whole arg list in memory. reimplement!     | sqlFun == avgFunction =-        realExpression $ foldl1' (+) (map realValue evaluatedArgs) / (fromIntegral $ length evaluatedArgs)-    | sqlFun == countFunction = intExpression $ length evaluatedArgs+        RealExpression $+            foldl1' (+) (map coerceReal evaluatedArgs) /+            (fromIntegral $ length evaluatedArgs)+    | sqlFun == countFunction = IntExpression $ length evaluatedArgs     | sqlFun == firstFunction = head evaluatedArgs     | sqlFun == lastFunction = last evaluatedArgs     | sqlFun == maxFunction = maximum evaluatedArgs@@ -589,67 +700,40 @@         arg1 = head evaluatedArgs         arg2 = evaluatedArgs !! 1         arg3 = evaluatedArgs !! 2-        subStringF start extent string = take extent (drop start string)         algebraWithCoercion intFunc realFunc args =             if any useRealAlgebra args then-                realExpression $ foldl1' realFunc (map realValue args)+                RealExpression $ foldl1' realFunc (map coerceReal args)             else-                intExpression $ foldl1' intFunc (map intValue args)+                IntExpression $ foldl1' intFunc (map coerceInt args)         -        useRealAlgebra expr =-            let-                prefType = preferredType expr-                maybeInt = maybeIntValue expr-            in-                prefType == RealType || maybeInt == Nothing+        useRealAlgebra (RealExpression _) = True+        useRealAlgebra expr = case maybeCoerceInt expr of+            Nothing -> True+            Just _  -> False         -        argCountIsValid =+        argCountIsInvalid =             let+                -- TODO the use of length is bad (unnecessarily traversing+                -- the entire arg list and keeping it in memory). Redo this+                -- so that we only check length w.r.t. minArgs                 argCount = length evaluatedArgs                 minArgs = minArgCount sqlFun                 argsFixed = argCountIsFixed sqlFun             in-                argCount == minArgs || (not argsFixed && argCount > minArgs)--{---- aggregates-avgFunction = SQLFunction {-    functionName    = "AVG",-    minArgCount     = 1,-    argCountIsFixed = True}--countFunction = SQLFunction {-    functionName    = "COUNT",-    minArgCount     = 1,-    argCountIsFixed = True}--firstFunction = SQLFunction {-    functionName    = "FIRST",-    minArgCount     = 1,-    argCountIsFixed = True}--lastFunction = SQLFunction {-    functionName    = "LAST",-    minArgCount     = 1,-    argCountIsFixed = True}--maxFunction = SQLFunction {-    functionName    = "MAX",-    minArgCount     = 1,-    argCountIsFixed = True}--minFunction = SQLFunction {-    functionName    = "MIN",-    minArgCount     = 1,-    argCountIsFixed = True}--sumFunction = SQLFunction {-    functionName    = "SUM",-    minArgCount     = 1,-    argCountIsFixed = True}--}+                argCount < minArgs || (argCount > minArgs && argsFixed)+        +        evalUnaryAlgebra intFunc realFunc =+            if length evaluatedArgs /= 1 then+                error $+                    "internal error: found a " ++ show sqlFun +++                    " function with multiple args. please report this error"+            else+                if useRealAlgebra arg1 then+                    RealExpression $ realFunc (coerceReal arg1)+                else+                    IntExpression $ intFunc (coerceInt arg1)  -- | trims leading and trailing spaces trimSpace :: String -> String trimSpace = f . f-   where f = reverse . dropWhile isSpace+    where f = reverse . dropWhile isSpace
Database/TxtSushi/SQLParser.hs view
@@ -40,6 +40,7 @@          -- String SQL function     concatenateFunction,+    absFunction,     upperFunction,     lowerFunction,     trimFunction,@@ -73,7 +74,6 @@ import Data.List import Text.ParserCombinators.Parsec import Text.ParserCombinators.Parsec.Expr-import Database.TxtSushi.Util.ListUtil  -------------------------------------------------------------------------------- -- The data definition for select statements@@ -101,6 +101,9 @@     CrossJoin {         leftJoinTable :: TableExpression,         rightJoinTable :: TableExpression,+        maybeTableAlias :: Maybe String} |+    SelectExpression {+        selectStatement :: SelectStatement,         maybeTableAlias :: Maybe String}     deriving (Show, Ord, Eq) @@ -110,16 +113,21 @@ allMaybeTableNames Nothing = [] allMaybeTableNames (Just tblExp) = allTableNames tblExp +allTableNames :: TableExpression -> [String] allTableNames (TableIdentifier tblName _) = [tblName] allTableNames (InnerJoin lftTbl rtTbl _ _) =     (allTableNames lftTbl) ++ (allTableNames rtTbl) allTableNames (CrossJoin lftTbl rtTbl _) =     (allTableNames lftTbl) ++ (allTableNames rtTbl)+allTableNames (SelectExpression selectStmt _) =+    allMaybeTableNames $ maybeFromTable selectStmt  data ColumnSelection =     AllColumns |     AllColumnsFrom {sourceTableName :: String} |-    ExpressionColumn {expression :: Expression}+    ExpressionColumn {+        expression :: Expression,+        maybeColumnAlias :: Maybe String}     --QualifiedColumn {     --    qualifiedColumnId :: ColumnIdentifier}     deriving (Show, Ord, Eq)@@ -161,7 +169,7 @@ containsAggregates _ = False  selectionContainsAggregates :: ColumnSelection -> Bool-selectionContainsAggregates (ExpressionColumn expr) =+selectionContainsAggregates (ExpressionColumn expr _) =     containsAggregates expr selectionContainsAggregates _ = False @@ -204,6 +212,9 @@       sqlFunc == substringFromFunction ||       sqlFunc == notFunction =         prettyFormatNormalFunctionExpression sqlFunc funcArgs+    | otherwise =+        error $ "don't know how to format the given SQL function : " +++                show sqlFunc  prettyFormatInfixFunctionExpression :: SQLFunction -> [Expression] -> String prettyFormatInfixFunctionExpression sqlFunc funcArgs =@@ -293,6 +304,7 @@         parseAscending  = parseToken "ASCENDING" <|> parseToken "ASC"         parseDescending = parseToken "DESCENDING" <|> parseToken "DESC" +maybeParseGroupByPart :: GenParser Char st (Maybe ([Expression], Maybe Expression)) maybeParseGroupByPart =     ifParseThen         -- if we see a "GROUP BY"@@ -307,28 +319,38 @@             maybeHavingExpr <- ifParseThen (parseToken "HAVING") parseExpression             return (groupExprs, maybeHavingExpr) +atLeastOneExpr :: GenParser Char st [Expression] atLeastOneExpr = sepByAtLeast 1 parseExpression commaSeparator  -------------------------------------------------------------------------------- -- Functions for parsing the column names specified after "SELECT" -------------------------------------------------------------------------------- +parseColumnSelections :: GenParser Char st [ColumnSelection] parseColumnSelections =     sepBy1 parseAnyColType (try commaSeparator)     where parseAnyColType = parseAllCols <|>                             (try parseAllColsFromTbl) <|>                             (try parseColExpression) +parseAllCols :: GenParser Char st ColumnSelection parseAllCols = parseToken "*" >> return AllColumns +parseAllColsFromTbl :: GenParser Char st ColumnSelection parseAllColsFromTbl = do     tableVal <- parseIdentifier     string "." >> spaces >> parseToken "*"          return $ AllColumnsFrom tableVal -parseColExpression = parseExpression >>= \expr -> return $ ExpressionColumn expr+parseColExpression :: GenParser Char st ColumnSelection+parseColExpression = do+    expr <- parseExpression+    maybeAlias <- maybeParseAlias+    +    return $ ExpressionColumn expr maybeAlias +parseColumnId :: GenParser Char st ColumnIdentifier parseColumnId = do     firstId <- parseIdentifier     @@ -344,37 +366,54 @@ -- Functions for parsing the table part (after "FROM") -------------------------------------------------------------------------------- -parseTableExpression = do-    nextTblChunk <- parseNextTblExpChunk+parseTableExpression :: GenParser Char a TableExpression+parseTableExpression =+    parenthesize parseTableExpression <|>+    parseSelectExpression <|>+    parseTableIdentifierOrJoin <?> "Table Expression"++parseSelectExpression :: GenParser Char a TableExpression+parseSelectExpression = do+    selectStmt <- parseSelectStatement+    maybeAlias <- maybeParseAlias     -    let ifInnerJoinParse = ifParseThenElse-            -- if-            ((maybeParse $ parseToken "INNER") >> parseToken "JOIN")-            -- then-            (parseInnerJoinRemainder nextTblChunk)-            -- else-            (return nextTblChunk)-        +    return $ SelectExpression selectStmt maybeAlias++parseTableIdentifierOrJoin :: GenParser Char a TableExpression+parseTableIdentifierOrJoin = do+    nextTblId <- parseTableIdentifier+    +    let         ifCrossOrInnerJoinParse = ifParseThenElse             -- if-            (parseToken "CROSS" >> parseToken "JOIN")+            crossJoinSep -- TODO commit to join             -- then-            (parseCrossJoinRemainder nextTblChunk)+            (parseCrossJoinRemainder nextTblId)             -- else             ifInnerJoinParse     +        ifInnerJoinParse = ifParseThenElse+            -- if+            innerJoinSep -- TODO commit to join+            -- then+            (parseInnerJoinRemainder nextTblId)+            -- else+            (return nextTblId)+             ifCrossOrInnerJoinParse--parseNextTblExpChunk =-    parenthesize parseTableExpression <|>  parseTableIdentifier+    +    where+        crossJoinSep = (commaSeparator >> return "") <|> (parseToken "CROSS" >> parseToken "JOIN")+        innerJoinSep = ((maybeParse $ parseToken "INNER") >> parseToken "JOIN") +parseInnerJoinRemainder :: TableExpression -> GenParser Char a TableExpression parseInnerJoinRemainder leftTblExpr = do     rightTblExpr <- parseTableExpression          parseToken "ON"     onPart <- parseExpression     -    maybeAlias <- maybeParse parseTableAlias+    maybeAlias <- maybeParseAlias          return InnerJoin {             leftJoinTable=leftTblExpr,@@ -382,21 +421,24 @@             onCondition=onPart,             maybeTableAlias=maybeAlias} +parseCrossJoinRemainder :: TableExpression -> GenParser Char a TableExpression parseCrossJoinRemainder leftTblExpr = do     rightTblExpr <- parseTableExpression-    maybeAlias <- maybeParse parseTableAlias+    maybeAlias <- maybeParseAlias          return CrossJoin {             leftJoinTable=leftTblExpr,             rightJoinTable=rightTblExpr,             maybeTableAlias=maybeAlias} +parseTableIdentifier :: GenParser Char st TableExpression parseTableIdentifier = do     theId <- parseIdentifier-    maybeAlias <- maybeParse parseTableAlias+    maybeAlias <- maybeParseAlias     return $ TableIdentifier theId maybeAlias -parseTableAlias = parseToken "AS" >> parseIdentifier+maybeParseAlias :: GenParser Char st (Maybe [Char])+maybeParseAlias = ifParseThen (parseToken "AS") parseIdentifier  -------------------------------------------------------------------------------- -- Expression parsing: These can be after "SELECT", "WHERE" or "HAVING"@@ -418,16 +460,15 @@     parseSubstringFunction <|>     parseNotFunction <|>     parseCountStar <|>-    (parseColumnId >>= (\colId -> return $ ColumnExpression colId))+    (parseColumnId >>= return . ColumnExpression)  parseStringConstant :: GenParser Char st Expression parseStringConstant =     (quotedText True '"' <|> quotedText True '\'') >>=-    (\txt -> return $ StringConstantExpression txt)+    (return . StringConstantExpression)  parseIntConstant :: GenParser Char st Expression-parseIntConstant =-    parseInt >>= (\int -> return $ IntegerConstantExpression int)+parseIntConstant = parseInt >>= return . IntegerConstantExpression  parseInt :: GenParser Char st Int parseInt = eatSpacesAfter . try . (withoutTrailing alphaNum) $ do@@ -439,7 +480,7 @@         signedParseTxt = do             char '-'             unsignedDigitTxt <- unsignedParseTxt-            return ('-':unsignedDigitTxt)+            return $ '-' : unsignedDigitTxt  -- | returns an int if it can be read from the string maybeReadInt :: String -> Maybe Int@@ -461,18 +502,27 @@  parseReal :: GenParser Char st Double parseReal = eatSpacesAfter . try . (withoutTrailing alphaNum) $ do-    realTxt <- anyParseTxt+    realTxt <- anyParseTxt <?> "real"     return $ read realTxt     where-        anyParseTxt = signedParseTxt <|> unsignedParseTxt <?> "real"-        unsignedParseTxt = do+        anyParseTxt = do+            txtWithoutExp <- txtWithoutExponent+            expPart <- try exponentPart <|> return ""+            return $ txtWithoutExp ++ expPart+        exponentPart = do+            e <- (char 'e' <|> char 'E')+            negPart <- (char '-' >> return "-") <|> return ""+            numPart <- many1 digit+            return $ (e:negPart) ++ numPart+        txtWithoutExponent = signedTxt <|> unsignedTxt <?> "real"+        unsignedTxt = do             intTxt <- many1 digit             char '.'             fracTxt <- many1 digit             return $ intTxt ++ "." ++ fracTxt-        signedParseTxt = do+        signedTxt = do             char '-'-            unsignedDigitTxt <- unsignedParseTxt+            unsignedDigitTxt <- unsignedTxt             return ('-':unsignedDigitTxt)  parseAnyNormalFunction :: GenParser Char st Expression@@ -480,74 +530,94 @@     let allParsers = map parseNormalFunction normalSyntaxFunctions     in choice allParsers +parseNormalFunction :: SQLFunction -> GenParser Char st Expression parseNormalFunction sqlFunc =     try (parseToken $ functionName sqlFunc) >> parseNormalFunctionArgs sqlFunc +parseNormalFunctionArgs :: SQLFunction -> GenParser Char st Expression parseNormalFunctionArgs sqlFunc = do     args <- parenthesize $ argSepBy (minArgCount sqlFunc) parseExpression commaSeparator     return $ FunctionExpression sqlFunc args     where argSepBy = if argCountIsFixed sqlFunc then sepByExactly else sepByAtLeast  -- Functions with "normal" syntax --+normalSyntaxFunctions :: [SQLFunction] normalSyntaxFunctions =-    [upperFunction, lowerFunction, trimFunction,+    [absFunction, upperFunction, lowerFunction, trimFunction,      -- all aggregates except count which accepts a (*)      avgFunction, firstFunction, lastFunction, maxFunction,      minFunction, sumFunction]  -- non aggregates+absFunction :: SQLFunction+absFunction = SQLFunction {+    functionName    = "ABS",+    minArgCount     = 1,+    argCountIsFixed = True}++upperFunction :: SQLFunction upperFunction = SQLFunction {     functionName    = "UPPER",     minArgCount     = 1,     argCountIsFixed = True} +lowerFunction :: SQLFunction lowerFunction = SQLFunction {     functionName    = "LOWER",     minArgCount     = 1,     argCountIsFixed = True} +trimFunction :: SQLFunction trimFunction = SQLFunction {     functionName    = "TRIM",     minArgCount     = 1,     argCountIsFixed = True}  -- aggregates+avgFunction :: SQLFunction avgFunction = SQLFunction {     functionName    = "AVG",     minArgCount     = 1,     argCountIsFixed = False} +countFunction :: SQLFunction countFunction = SQLFunction {     functionName    = "COUNT",     minArgCount     = 1,     argCountIsFixed = False} +firstFunction :: SQLFunction firstFunction = SQLFunction {     functionName    = "FIRST",     minArgCount     = 1,     argCountIsFixed = False} +lastFunction :: SQLFunction lastFunction = SQLFunction {     functionName    = "LAST",     minArgCount     = 1,     argCountIsFixed = False} +maxFunction :: SQLFunction maxFunction = SQLFunction {     functionName    = "MAX",     minArgCount     = 1,     argCountIsFixed = False} +minFunction :: SQLFunction minFunction = SQLFunction {     functionName    = "MIN",     minArgCount     = 1,     argCountIsFixed = False} +sumFunction :: SQLFunction sumFunction = SQLFunction {     functionName    = "SUM",     minArgCount     = 1,     argCountIsFixed = False}  -- Infix functions --+infixFunctions :: [[SQLFunction]] infixFunctions =     [[multiplyFunction, divideFunction],      [plusFunction, minusFunction],@@ -560,6 +630,7 @@ -- | This function parses the operator part of the infix function and returns --   a function that excepts a left expression and right expression to form --   an Expression from the FunctionExpression constructor+parseInfixOp :: SQLFunction -> Operator Char st Expression parseInfixOp infixFunc =     -- use the magic infix type, always assuming left associativity     Infix opParser AssocLeft@@ -570,78 +641,93 @@             functionArguments = [leftSubExpr, rightSubExpr]}  -- Algebraic+multiplyFunction :: SQLFunction multiplyFunction = SQLFunction {     functionName    = "*",     minArgCount     = 2,     argCountIsFixed = True} +divideFunction :: SQLFunction divideFunction = SQLFunction {     functionName    = "/",     minArgCount     = 2,     argCountIsFixed = True} +plusFunction :: SQLFunction plusFunction = SQLFunction {     functionName    = "+",     minArgCount     = 2,     argCountIsFixed = True} +minusFunction :: SQLFunction minusFunction = SQLFunction {     functionName    = "-",     minArgCount     = 2,     argCountIsFixed = True}  -- Boolean+isFunction :: SQLFunction isFunction = SQLFunction {     functionName    = "=",     minArgCount     = 2,     argCountIsFixed = True} +isNotFunction :: SQLFunction isNotFunction = SQLFunction {     functionName    = "<>",     minArgCount     = 2,     argCountIsFixed = True} +lessThanFunction :: SQLFunction lessThanFunction = SQLFunction {     functionName    = "<",     minArgCount     = 2,     argCountIsFixed = True} +lessThanOrEqualToFunction :: SQLFunction lessThanOrEqualToFunction = SQLFunction {     functionName    = "<=",     minArgCount     = 2,     argCountIsFixed = True} +greaterThanFunction :: SQLFunction greaterThanFunction = SQLFunction {     functionName    = ">",     minArgCount     = 2,     argCountIsFixed = True} +greaterThanOrEqualToFunction :: SQLFunction greaterThanOrEqualToFunction = SQLFunction {     functionName    = ">=",     minArgCount     = 2,     argCountIsFixed = True} +andFunction :: SQLFunction andFunction = SQLFunction {     functionName    = "AND",     minArgCount     = 2,     argCountIsFixed = True} +orFunction :: SQLFunction orFunction = SQLFunction {     functionName    = "OR",     minArgCount     = 2,     argCountIsFixed = True} +concatenateFunction :: SQLFunction concatenateFunction = SQLFunction {     functionName    = "||",     minArgCount     = 2,     argCountIsFixed = True} +regexMatchFunction :: SQLFunction regexMatchFunction = SQLFunction {     functionName    = "=~",     minArgCount     = 2,     argCountIsFixed = True}  -- Functions with special syntax --+specialFunctions :: [SQLFunction] specialFunctions = [substringFromFunction,                     substringFromToFunction,                     negateFunction,@@ -650,10 +736,13 @@ -- | SUBSTRING(extraction_string FROM starting_position [FOR length] --             [COLLATE collation_name]) --   TODO implement COLLATE part+substringFromFunction :: SQLFunction substringFromFunction = SQLFunction {     functionName    = "SUBSTRING",     minArgCount     = 2,     argCountIsFixed = True}++substringFromToFunction :: SQLFunction substringFromToFunction = SQLFunction {     functionName    = "SUBSTRING",     minArgCount     = 3,@@ -673,6 +762,7 @@         Nothing     -> FunctionExpression substringFromFunction [strExpr, startExpr]         Just len    -> FunctionExpression substringFromToFunction [strExpr, startExpr, len] +negateFunction :: SQLFunction negateFunction = SQLFunction {     functionName    = "-",     minArgCount     = 1,@@ -684,16 +774,19 @@     expr <- parseAnyNonInfixExpression     return $ FunctionExpression negateFunction [expr] +notFunction :: SQLFunction notFunction = SQLFunction {     functionName    = "NOT",     minArgCount     = 1,     argCountIsFixed = True} +parseNotFunction :: GenParser Char st Expression parseNotFunction = do     parseToken $ functionName notFunction     expr <- parseAnyNonInfixExpression     return $ FunctionExpression notFunction [expr] +parseCountStar :: GenParser Char st Expression parseCountStar = do     try (parseToken $ functionName countFunction)     try parseStar <|> parseNormalFunctionArgs countFunction@@ -707,19 +800,25 @@ -- Parse utility functions -------------------------------------------------------------------------------- +parseOpChar :: CharParser st Char parseOpChar = oneOf opChars +opChars :: [Char] opChars = "~!@#$%^&*-+=|\\<>/?" +withoutTrailing :: (Show s) => GenParser tok st s -> GenParser tok st a -> GenParser tok st a withoutTrailing end p = p >>= (\x -> genNotFollowedBy end >> return x) +withTrailing :: (Monad m) => m a -> m b -> m b withTrailing end p = p >>= (\x -> end >> return x)  -- | like the lexeme function, this function eats all spaces after the given --   parser, but this one works for me and lexeme doesn't+eatSpacesAfter :: GenParser Char st a -> GenParser Char st a eatSpacesAfter p = p >>= (\x -> spaces >> return x)  -- | find out if the given string ends with an op char+endsWithOp :: String -> Bool endsWithOp strToTest = last strToTest `elem` opChars  -- | A token parser that allows either upper or lower case. all trailing@@ -729,10 +828,12 @@     eatSpacesAfter (try $ if endsWithOp tokStr then parseOpTok else parseAlphaNumTok)     where         parseOpTok = withoutTrailing parseOpChar (string tokStr)-        parseAlphaNumTok = withoutTrailing alphaNum (upperOrLower tokStr)+        parseAlphaNumTok =+            withoutTrailing (alphaNum <|> char '_') (upperOrLower tokStr)  -- | parses an identifier. you can use a tick '`' as a quote for --   an identifier with white-space+parseIdentifier :: GenParser Char st String parseIdentifier = do     let parseId = do             let idChar = alphaNum <|> char '_'@@ -742,6 +843,7 @@  -- | quoted text which allows escaping by doubling the quote char --   like "escaped quote char here:"""+quotedText :: Bool -> Char -> GenParser Char st String quotedText allowEmpty quoteChar = do     let quote = char quoteChar         manyFunc = if allowEmpty then many else many1@@ -754,10 +856,10 @@          return textValue -exceptChar parser theException = notFollowedBy theException >> parser-+escapedQuote :: Char -> GenParser Char st Char escapedQuote quoteChar = string [quoteChar, quoteChar] >> return quoteChar +commaSeparator :: GenParser Char st Char commaSeparator = eatSpacesAfter $ char ','  -- | Wraps parentheses parsers around the given inner parser@@ -768,15 +870,13 @@     eatSpacesAfter $ char ')'     return innerParseResults +{- -- | Either parses the left or right parser returning the result of the --   successful parser eitherParse :: GenParser tok st a -> GenParser tok st b -> GenParser tok st (Either a b) eitherParse leftParser rightParser =-    do {parseResult <- try leftParser; return $ Left parseResult} <|>-    do {parseResult <- rightParser; return $ Right parseResult}---- parses 1 or more spaces-spaces1 = skipMany1 space <?> "whitespace"+    (try leftParser >>= return . Left) <|> (rightParser >>= return . Right)+-}  -- | if the ifParse parser succeeds return the result of thenParse, else --   return Nothing without parsing any input@@ -784,7 +884,7 @@ ifParseThen ifParse thenPart = do     ifResult <- maybeParse ifParse     case ifResult of-        Just _ ->   thenPart >>= (\x -> return $ Just x)+        Just _ ->   thenPart >>= return . Just         Nothing ->  return Nothing  -- | if ifParse succeeds then parse thenPart otherwise parse elsePart@@ -795,11 +895,13 @@         Just _ -> thenPart         Nothing -> elsePart -parseReservedWord = do+parseReservedWord :: GenParser Char st String+parseReservedWord =     let reservedWordParsers = map parseToken reservedWords-    choice reservedWordParsers+    in  choice reservedWordParsers  -- TODO are function names reserved... i don't think so+reservedWords :: [String] reservedWords =     map functionName normalSyntaxFunctions ++     map functionName (concat infixFunctions) ++@@ -831,13 +933,12 @@ -- | returns Just parseResult if the parse succeeds and Nothing if it fails maybeParse :: GenParser tok st a -> GenParser tok st (Maybe a) maybeParse parser =-    (try parser >>= (\x -> return $ Just x)) <|>-    return Nothing+    (try parser >>= return . Just) <|> return Nothing  -- | parse `itemParser`s seperated by exactly `minCount` `sepParser`s sepByExactly :: Int -> GenParser tok st a -> GenParser tok st sep -> GenParser tok st [a]-sepByExactly count itemParser sepParser =-    let itemParsers = replicate count itemParser+sepByExactly itemCount itemParser sepParser =+    let itemParsers = replicate itemCount itemParser     in parseEach itemParsers     where         -- for an empty parser list return an empty result@@ -858,7 +959,7 @@ sepByAtLeast :: Int -> GenParser tok st a -> GenParser tok st sep -> GenParser tok st [a] sepByAtLeast minCount itemParser sepParser = do     minResults <- sepByExactly minCount itemParser sepParser-    ifParseThenElse-        sepParser-        (sepBy1 itemParser sepParser >>= (\tailResults -> return $ minResults ++ tailResults))-        (return minResults)+    tailResults <-+        ifParseThenElse sepParser (sepBy itemParser sepParser) (return [])+    +    return $ minResults ++ tailResults
Database/TxtSushi/Transform.hs view
@@ -2,103 +2,28 @@ Simple table transformations -} module Database.TxtSushi.Transform (-    selectColumns,     sortColumns,-    fileBasedSortTable,-    mergeAllBy,     joinTables,+    crossJoinTables,     joinPresortedTables,     rowComparison) where  import Data.List-import System.Directory-import System.IO -import Database.TxtSushi.IO--{- |-Create a new table by selecting the given 'columnIndices'--}-selectColumns _ [] = []-selectColumns columnIndices (headRow:tableTail) =-    ([headRow !! i | i <- columnIndices]):(selectColumns columnIndices tableTail)--removeColumns _ [] = []-removeColumns columnIndices (headRow:tableTail) =-    let indexesToSelect = [0 .. ((length headRow) - 1)] \\ columnIndices-    in ([headRow !! i | i <- indexesToSelect]):(removeColumns columnIndices tableTail)- -- | sort the given 'table' on the given columns+sortColumns :: (Ord a) => [Int] -> [[a]] -> [[a]] sortColumns columns table =     sortBy (rowComparison columns) table  -- | compare two rows based on given column balues+rowComparison :: (Ord a) => [Int] -> [a] -> [a] -> Ordering rowComparison [] _ _ = EQ rowComparison (columnHead:columnsTail) row1 row2 =     let colComparison = (row1 !! columnHead) `compare` (row2 !! columnHead)     in         case colComparison of-            EQ          -> rowComparison columnsTail row1 row2-            otherwise   -> colComparison---- | merge two sorted lists into a single sorted list-mergeBy comparisonFunction []    list2   = list2-mergeBy comparisonFunction list1 []      = list1-mergeBy comparisonFunction list1@(head1:tail1) list2@(head2:tail2) =-    case head1 `comparisonFunction` head2 of-        GT          -> (head2:(mergeBy comparisonFunction list1 tail2))-        otherwise   -> (head1:(mergeBy comparisonFunction tail1 list2))---- | merge the sorted lists in the list to a list about 1/2 the size-mergePairsBy _ [] = []-mergePairsBy comparisonFunction singletonListList@(headList:[]) = singletonListList-mergePairsBy comparisonFunction (list1:list2:listListTail) =-    let mergedPair = mergeBy comparisonFunction list1 list2-    in mergedPair:(mergePairsBy comparisonFunction listListTail)---- | merge a list of sorted lists into a single sorted list-mergeAllBy _ [] = []-mergeAllBy comparisonFunction listList =-    let mergedPairs = mergePairsBy comparisonFunction listList-    in-        case mergedPairs of-            singletonListHead:[]    -> singletonListHead-            otherwise               -> mergeAllBy comparisonFunction mergedPairs--{- |-perform a table sort using files to keep from holding the whole list in memory--}-fileBasedSortTable columns table = do-    partialSortFiles <- bufferPartialSorts columns table-    partialSortFileHandles <- (unwrapIOList [openFile file ReadMode | file <- partialSortFiles])-    partialSortContents <- (unwrapIOList [hGetContents handle | handle <- partialSortFileHandles])-    let partialSortTables = map (parseTable csvFormat) partialSortContents-    return (partialSortTables, partialSortFileHandles, partialSortFiles)---- | unwrap a list of 'IO' boxed items-unwrapIOList [] = do return []-unwrapIOList (ioHead:ioTail) = do-    unwrappedHead <- ioHead-    unwrappedTail <- unwrapIOList ioTail-    return (unwrappedHead:unwrappedTail)---- | create a list of parial sorts-bufferPartialSorts columns [] = return []-bufferPartialSorts columns table = do-    let rowLimit = 100000-        (rowsToSortNow, rowsToSortLater) = splitAt rowLimit table-        sortedRows = sortColumns columns rowsToSortNow-    sortBuffer <- bufferToTempFile sortedRows-    otherSortBuffers <- bufferPartialSorts columns rowsToSortLater-    return (sortBuffer:otherSortBuffers)---- | buffer the table to a temporary file and return a handle to that file-bufferToTempFile table = do-    tempDir <- getTemporaryDirectory-    (tempFilePath, tempFileHandle) <- openTempFile tempDir "buffer.txt"-    hPutStr tempFileHandle (formatTable csvFormat table)-    hClose tempFileHandle-    return tempFilePath+            EQ  -> rowComparison columnsTail row1 row2+            _   -> colComparison  -- | join together two tables on the given column index pairs joinTables :: (Ord o) => [(Int, Int)] -> [[o]] -> [[o]] -> [[o]]@@ -122,15 +47,17 @@     in         joinGroupedTables joinColumnZipList tableGroups1 tableGroups2 -permutePrependRows [] _ = []-permutePrependRows _ [] = []-permutePrependRows (table1HeadRow:table1Tail) table2 =+crossJoinTables :: [[a]] -> [[a]] -> [[a]]+crossJoinTables [] _ = []+crossJoinTables _ [] = []+crossJoinTables (table1HeadRow:table1Tail) table2 =     let         prependHead = (table1HeadRow ++)         newTable2 = map prependHead table2     in-        newTable2 ++ (permutePrependRows table1Tail table2)+        newTable2 ++ (crossJoinTables table1Tail table2) +joinGroupedTables :: (Ord a) => [(Int, Int)] -> [[[a]]] -> [[[a]]] -> [[a]] joinGroupedTables _ [] _  = [] joinGroupedTables _ _  [] = [] joinGroupedTables joinColumnZipList tableGroups1@(headTableGroup1:tableGroupsTail1) tableGroups2@(headTableGroup2:tableGroupsTail2) =@@ -146,10 +73,11 @@             GT -> joinGroupedTables joinColumnZipList tableGroups1 tableGroupsTail2                          -- the two groups are equal so permute-            otherwise ->-                (permutePrependRows headTableGroup1 headTableGroup2) +++            _  ->+                (crossJoinTables headTableGroup1 headTableGroup2) ++                 (joinGroupedTables joinColumnZipList tableGroupsTail1 tableGroupsTail2) +asymmetricRowComparison :: (Ord a) => [(Int, Int)] -> [a] -> [a] -> Ordering asymmetricRowComparison [] _ _ = EQ asymmetricRowComparison (columnsZipHead:columnsZipTail) row1 row2 =     let@@ -157,6 +85,5 @@         colComparison = (row1 !! columnHead1) `compare` (row2 !! columnHead2)     in         case colComparison of-            EQ          -> asymmetricRowComparison columnsZipTail row1 row2-            otherwise   -> colComparison-+            EQ  -> asymmetricRowComparison columnsZipTail row1 row2+            _   -> colComparison
Database/TxtSushi/Util/CommandLineArgument.hs view
@@ -16,7 +16,6 @@ import Data.List import Data.Map (Map) import qualified Data.Map as Map---import Test.HUnit  data CommandLineDescription = CommandLineDescription {     options :: [OptionDescription],@@ -53,9 +52,15 @@     -}     argumentCountIsFixed :: Bool} deriving (Show, Eq, Ord) -space = " ";-etc = "...";+space :: String+space = " " +etc :: String+etc = "..."++-- | converts a command line description into a string version that+--   you can show the user+formatCommandLine :: CommandLineDescription -> String formatCommandLine commandLine =     let formattedOptions = formatOptions (options commandLine)         formattedTailArgs = formatTailArguments commandLine@@ -65,6 +70,7 @@         else             formattedOptions ++ space ++ formattedTailArgs +formatTailArguments :: CommandLineDescription -> String formatTailArguments commandLine =     let tailArgs = tailArgumentNames commandLine         minTailArgs = minTailArgumentCount commandLine@@ -89,6 +95,7 @@         else             "[" ++ requiredOptionString ++ "]" ++ formattedOptionsTail +argumentSubstring :: OptionDescription -> String argumentSubstring option =     let minArgs = minArgumentCount option     in@@ -125,12 +132,12 @@     (Map.Map OptionDescription [[String]], [String]) extractOptions [] argValues = (Map.empty, argValues) extractOptions _ [] = (Map.empty, [])-extractOptions optDescs argValues@(argHead:argTail) =+extractOptions optDescs argValues@(argHead:_) =     case (find (\optDesc -> optionFlag optDesc == argHead) optDescs) of         Nothing ->             (Map.empty, argValues)         Just optDesc ->-            let (optArgs, afterOptArgs) = extractOption optDesc optDescs argValues+            let (optArgs, afterOptArgs) = extractOption optDesc optDescs (tail argValues)                 (tailArgsMap, afterTailArgs) = extractOptions optDescs afterOptArgs             in (addOptionArgsToMap tailArgsMap optDesc optArgs, afterTailArgs) @@ -139,9 +146,9 @@     [OptionDescription] ->     [String] ->     ([String], [String])-extractOption optDesc allOptDescs (argHead:argTail) =-    let optArgExtent = argumentExtent optDesc allOptDescs argTail-    in splitAt optArgExtent argTail+extractOption optDesc allOptDescs optArgsEtc =+    let optArgExtent = argumentExtent optDesc allOptDescs optArgsEtc+    in splitAt optArgExtent optArgsEtc  argumentExtent :: OptionDescription -> [OptionDescription] -> [String] -> Int argumentExtent optionDescription allOptDescs afterOptArgs =@@ -174,40 +181,3 @@     case (Map.lookup opt optArgMap) of         Nothing ->          Map.insert opt [args] optArgMap         Just currArgs ->    Map.insert opt (currArgs ++ [args]) optArgMap---- Test code-{--byNameOption = OptionDescription-    False       -- isRequired-    "-by-name"  -- optionFlag-    []          -- argumentNames-    0           -- minArgumentCount-    True        -- argumentCountIsFixed--helpOption = OptionDescription-    False       -- isRequired-    "-help"     -- optionFlag-    []          -- argumentNames-    0           -- minArgumentCount-    True        -- argumentCountIsFixed--fancyPantsOption = OptionDescription-    False           -- isRequired-    "-fancy"        -- optionFlag-    ["f1", "f2"]    -- argumentNames-    2               -- minArgumentCount-    False           -- argumentCountIsFixed--cmdLineOps1 = [byNameOption, helpOption, fancyPantsOption]--cmdLineDesc1 = CommandLineDescription-    cmdLineOps1 -- options-    1           -- minTailArgumentCount-    "file/-"    -- tailArgumentNames-    True        -- tailArgumentCountIsFixed-cmdLine1 = ["-by-name", "-fancy", "f1arg", "f2arg", "-help", "-"]-parsedCmd1 = extractOptions (options cmdLineDesc1) cmdLine1--test1 = TestCase $ assertEqual-    "options test" cmdLineOps1 (options cmdLineDesc1)--}
Database/TxtSushi/Util/IOUtil.hs view
@@ -1,11 +1,18 @@ module Database.TxtSushi.Util.IOUtil (     bufferStdioToTempFile,     getContentsFromFileOrStdin,-    getAll) where+    printSingleFileUsage) where +import Data.List+import Data.Version (Version(..)) import System.Directory+import System.Environment import System.IO +import Paths_txt_sushi++-- | buffers standard input to a temp file and returns a path to that file+bufferStdioToTempFile :: IO FilePath bufferStdioToTempFile = do     stdioText <- getContents     tempDir <- getTemporaryDirectory@@ -14,22 +21,21 @@     hClose tempFileHandle     return tempFilePath -getContentsFromFileOrStdin filePath = do+-- | if given "-" this file reads from stdin otherwise it reads from the named+--   file+getContentsFromFileOrStdin :: String -> IO String+getContentsFromFileOrStdin filePath =     if filePath == "-"-        then do-            getContents-        else do-            handle <- openFile filePath ReadMode-            hGetContents handle--getAll [] = []-getAll (x:xt) = do-    y <- x-    yt <- getAll xt-    return (y:yt)+        then getContents+        else readFile filePath --- | returns an infinite list of gets from a monad-keepGetting getAction = do-    headVal <- getAction-    tailVal <- keepGetting getAction-    return (headVal:tailVal)+-- | print a cookie-cutter usage message for the command line utilities+--   that take a single file name or "-" as input+printSingleFileUsage :: IO ()+printSingleFileUsage = do+    progName <- getProgName+    putStrLn $ progName ++ " (" ++ versionStr ++ ")"+    putStrLn $ "Usage: " ++ progName ++ " file_name_or_dash"+    +    where+        versionStr = intercalate "." (map show . versionBranch $ version)
csvtopretty.hs view
@@ -1,24 +1,17 @@-import Data.List import System.Directory import System.Environment-import System.Exit import System.IO  import Database.TxtSushi.IO import Database.TxtSushi.Util.IOUtil +main :: IO () main = do     args <- getArgs     -    if (length args) /= 1-        then do-            hPutStrLn stderr $-                    "ERROR: program requires a single command line argument " ++-                    "(filename or '-')"-            exitFailure-        else do-            let fileArg = head args-                useStdio = (fileArg == "-")+    case args of+        [fileArg] -> do+            let useStdio = (fileArg == "-")                          file <- if useStdio then bufferStdioToTempFile else return fileArg             @@ -36,3 +29,6 @@             hClose handle1             hClose handle2             if useStdio then removeFile file else return ()+            +        -- we were expecting a single file name arg+        _ -> printSingleFileUsage
csvtotab.hs view
@@ -1,23 +1,20 @@-import Data.List import System.Environment-import System.Exit import System.IO  import Database.TxtSushi.IO import Database.TxtSushi.Util.IOUtil +main :: IO () main = do     args <- getArgs     -    if (length args) /= 1-        then do-            hPutStrLn stderr $-                    "ERROR: program requires a single command line argument " ++-                    "(filename or '-')"-            exitFailure-        else do-            contents <- getContentsFromFileOrStdin (last args)+    case args of+        [fileArg] -> do+            contents <- getContentsFromFileOrStdin fileArg                          let table = parseTable csvFormat contents                 tabDelimitedText = formatTable tabDelimitedFormat table             putStr tabDelimitedText+        +        -- we were expecting a single file name arg+        _ -> printSingleFileUsage
namecolumns.hs view
@@ -1,25 +1,30 @@-import Data.List import System.Environment-import System.Exit import System.IO  import Database.TxtSushi.IO import Database.TxtSushi.Util.IOUtil +main :: IO () main = do     args <- getArgs     -    if (length args) /= 1-        then do-            hPutStrLn stderr $-                    "ERROR: program requires a single command line argument " ++-                    "(filename or '-')"-            exitFailure-        else do-            contents <- getContentsFromFileOrStdin (last args)-            -            let table@(headRow:tableTail) = parseTable csvFormat contents-                colNames = map ("col" ++) (map show [1 .. length headRow])-                namedTable = colNames:table+    case args of+        [fileArg] -> do+            contents <- getContentsFromFileOrStdin fileArg             -            putStr $ if null table then "" else formatTable csvFormat namedTable+            let table = parseTable csvFormat contents+            case table of+                +                -- we only need the 1st row's column count to name the columns+                (headRow : _) ->+                    let colNames = map ("col" ++) (map show [1 .. length headRow])+                        namedTable = formatTable csvFormat [colNames] ++ contents+                    in+                        putStr namedTable+                +                -- the given table was empty (is it better for this to be an+                -- error ?)+                _ -> return ()+        +        -- we were expecting a single file name arg+        _ -> printSingleFileUsage
tabtocsv.hs view
@@ -1,23 +1,20 @@-import Data.List import System.Environment-import System.Exit import System.IO  import Database.TxtSushi.IO import Database.TxtSushi.Util.IOUtil +main :: IO () main = do     args <- getArgs     -    if (length args) /= 1-        then do-            hPutStrLn stderr $-                    "ERROR: program requires a single command line argument " ++-                    "(filename or '-')"-            exitFailure-        else do-            contents <- getContentsFromFileOrStdin (last args)+    case args of+        [fileArg] -> do+            contents <- getContentsFromFileOrStdin fileArg                          let table = parseTable tabDelimitedFormat contents                 csvText = formatTable csvFormat table             putStr csvText+        +        -- we were expecting a single file name arg+        _ -> printSingleFileUsage
tabtopretty.hs view
@@ -1,24 +1,17 @@-import Data.List import System.Directory import System.Environment-import System.Exit import System.IO  import Database.TxtSushi.IO import Database.TxtSushi.Util.IOUtil +main :: IO () main = do     args <- getArgs     -    if (length args) /= 1-        then do-            hPutStrLn stderr $-                    "ERROR: program requires a single command line argument " ++-                    "(filename or '-')"-            exitFailure-        else do-            let fileArg = head args-                useStdio = (fileArg == "-")+    case args of+        [fileArg] -> do+            let useStdio = (fileArg == "-")                          file <- if useStdio then bufferStdioToTempFile else return fileArg             @@ -36,3 +29,6 @@             hClose handle1             hClose handle2             if useStdio then removeFile file else return ()+            +        -- we were expecting a single file name arg+        _ -> printSingleFileUsage
+ transposecsv.hs view
@@ -0,0 +1,21 @@+import Data.List+import System.Environment+import System.IO++import Database.TxtSushi.IO+import Database.TxtSushi.Util.IOUtil++main :: IO ()+main = do+    args <- getArgs+    +    case args of+        [fileArg] -> do+            contents <- getContentsFromFileOrStdin fileArg+            +            let table = transpose . parseTable csvFormat $ contents+                csvText = formatTable csvFormat table+            putStr csvText+        +        -- we were expecting a single file name arg+        _ -> printSingleFileUsage
+ transposetab.hs view
@@ -0,0 +1,21 @@+import Data.List+import System.Environment+import System.IO++import Database.TxtSushi.IO+import Database.TxtSushi.Util.IOUtil++main :: IO ()+main = do+    args <- getArgs+    +    case args of+        [fileArg] -> do+            contents <- getContentsFromFileOrStdin fileArg+            +            let table = transpose . parseTable tabDelimitedFormat $ contents+                tabText = formatTable tabDelimitedFormat table+            putStr tabText+        +        -- we were expecting a single file name arg+        _ -> printSingleFileUsage
tssql.hs view
@@ -10,6 +10,7 @@ -- ----------------------------------------------------------------------------- import Data.List+import Data.Version (Version(..)) import qualified Data.Map as Map import System.Environment import System.Exit@@ -20,10 +21,12 @@ import Database.TxtSushi.IO import Database.TxtSushi.SQLExecution import Database.TxtSushi.SQLParser-import Database.TxtSushi.Transform import Database.TxtSushi.Util.CommandLineArgument import Database.TxtSushi.Util.IOUtil +import Paths_txt_sushi++helpOption :: OptionDescription helpOption = OptionDescription {     isRequired              = False,     optionFlag              = "-help",@@ -31,19 +34,30 @@     minArgumentCount        = 0,     argumentCountIsFixed    = True} +externalSortOption :: OptionDescription+externalSortOption = OptionDescription {+    isRequired              = False,+    optionFlag              = "-external-sort",+    argumentNames           = [],+    minArgumentCount        = 0,+    argumentCountIsFixed    = True}++tableDefOption :: OptionDescription tableDefOption = OptionDescription {     isRequired              = False,     optionFlag              = "-table",-    argumentNames           = ["CSV-file-name", "table-name"],+    argumentNames           = ["table_name", "CSV_file_name"],     minArgumentCount        = 2,     argumentCountIsFixed    = True} -turtleSqlOpts = [helpOption, tableDefOption]+allOpts :: [OptionDescription]+allOpts = [helpOption, externalSortOption, tableDefOption] +sqlCmdLine :: CommandLineDescription sqlCmdLine = CommandLineDescription {-    options                     = turtleSqlOpts,+    options                     = allOpts,     minTailArgumentCount        = 1,-    tailArgumentNames           = ["SQL-select-statement"],+    tailArgumentNames           = ["SQL_select_statement"],     tailArgumentCountIsFixed    = True}  validateTableNames :: [String] -> [String] -> Bool@@ -55,24 +69,35 @@         error $ "The given table name \"" ++ argTblHead ++                 "\" does not appear in the SELECT statement" +tableArgsToMap :: [[String]] -> Map.Map String String tableArgsToMap [] = Map.empty tableArgsToMap (currTableArgs:tailTableArgs) =     case currTableArgs of-        [fileName, tableName] ->-            Map.insert fileName tableName (tableArgsToMap tailTableArgs)-        otherwise ->+        [fileName, tblName] ->+            Map.insert fileName tblName (tableArgsToMap tailTableArgs)+        _ ->             error $ "the \"" ++ (optionFlag tableDefOption) ++                     "\" option should have exactly two arguments" +unwrapMapList :: (Monad m) => [(t, m t1)] -> m [(t, t1)] unwrapMapList [] = return [] unwrapMapList ((key, value):mapTail) = do     unwrappedValue <- value     unwrappedTail <- unwrapMapList mapTail     return $ (key, unwrappedValue):unwrappedTail -printUsage progName =+printUsage :: String -> IO ()+printUsage progName = do+    putStrLn $ progName ++ " (" ++ versionStr ++ ")"     putStrLn $ "Usage: " ++ progName ++ " " ++ formatCommandLine sqlCmdLine+    where+        versionStr = intercalate "." (map show $ versionBranch version) +argsToSortConfig :: Map.Map OptionDescription a -> SortConfiguration+argsToSortConfig argMap =+    if Map.member externalSortOption argMap then UseExternalSort else UseInMemorySort++main :: IO () main = do     args <- getArgs     progName <- getProgName@@ -108,7 +133,8 @@                             let unwrappedContentsMap = Map.fromList unwrappedContents                                 textTableMap = Map.map (parseTable csvFormat) unwrappedContentsMap                                 dbTableMap = Map.mapWithKey textTableToDatabaseTable textTableMap-                                selectedDbTable = select selectStmt dbTableMap+                                sortCfg = argsToSortConfig argMap+                                selectedDbTable = select sortCfg selectStmt dbTableMap                                 selectedTxtTable = databaseTableToTextTable selectedDbTable                                                          putStr $ formatTable csvFormat selectedTxtTable
txt-sushi.cabal view
@@ -1,5 +1,5 @@ Name:                txt-sushi-Version:             0.3.0+Version:             0.4.0 Synopsis:            Spreadsheets are databases! Description:     TxtSushi is a collection of command line utilities for processing@@ -7,54 +7,65 @@     The most important utility (tssql) lets you perform SQL selects on CSV files.     By focusing exclusively on processing text files with a tabular structure,     TxtSushi simplifies common tasks like filtering, joining and transformation-    that would take some effort to accomplish with a more powerful scripting-    language.+    that would take more effort to accomplish with a general purpose text+    processing language. License:             GPL License-File:        COPYING Author:              Keith Sheppard Maintainer:          keithshep@gmail.com Homepage:            http://keithsheppard.name/txt-sushi+Bug-Reports:         http://code.google.com/p/txt-sushi/issues/list Build-Type:          Simple Category:            Database, Utils, Text-Cabal-Version:       >=1.2+Cabal-Version:       >= 1.6 +Source-Repository head+  type:     darcs+  location: http://patch-tag.com/r/keithshep/txt-sushi/pullrepo++Source-Repository this+  type:     darcs+  location: http://patch-tag.com/r/keithshep/txt-sushi/pullrepo+  tag:      0.4.0+ Executable tssql   Main-Is:          tssql.hs-  Build-Depends:    base,haskell98,directory,containers,parsec,regex-posix-  -  -- creates highly optimized code (slow compile)-  GHC-Options:      -O2 -fvia-C+  GHC-Options:      -O2 -Wall      -- combine the following with the +RTS -xc -RTS runtime option for stack taces   --GHC-Options:      -prof -auto-all  Executable csvtotab   Main-Is:          csvtotab.hs-  Build-Depends:    base,haskell98,directory,containers-  GHC-Options:      -O2 -fvia-C+  GHC-Options:      -O2 -Wall  Executable tabtocsv   Main-Is:          tabtocsv.hs-  Build-Depends:    base,haskell98,directory,containers-  GHC-Options:      -O2 -fvia-C+  GHC-Options:      -O2 -Wall  Executable csvtopretty   Main-Is:          csvtopretty.hs-  Build-Depends:    base,haskell98,directory,containers-  GHC-Options:      -O2 -fvia-C+  GHC-Options:      -O2 -Wall  Executable tabtopretty   Main-Is:          tabtopretty.hs-  Build-Depends:    base,haskell98,directory,containers-  GHC-Options:      -O2 -fvia-C+  GHC-Options:      -O2 -Wall  Executable namecolumns   Main-Is:          namecolumns.hs-  Build-Depends:    base,haskell98,directory,containers-  GHC-Options:      -O2 -fvia-C+  GHC-Options:      -O2 -Wall +Executable transposecsv+  Main-Is:          transposecsv.hs+  GHC-Options:      -O2 -Wall++Executable transposetab+  Main-Is:          transposetab.hs+  GHC-Options:      -O2 -Wall+ Library   Exposed-Modules:+    Database.TxtSushi.ExternalSort     Database.TxtSushi.IO     Database.TxtSushi.SQLExecution     Database.TxtSushi.SQLParser@@ -63,5 +74,5 @@     Database.TxtSushi.Util.IOUtil     Database.TxtSushi.Util.ListUtil   -  Build-Depends:    base,haskell98,directory,containers-  GHC-Options:      -O2 -fvia-C+  Build-Depends:    base >= 3 && < 5,binary,bytestring,containers,directory,parsec,regex-posix+  GHC-Options:      -O2 -Wall