packages feed

qudb-0.0.1: Database/QUDB/Structure.hs

module Database.QUDB.Structure (new, load, dump, query, DB) where

import Database.QUDB.EntityTypes
import Database.QUDB.Query
import Database.QUDB.Utils (sortByM)
import Data.List (elemIndex)
import Data.Foldable (foldlM, foldrM)
import qualified Data.ByteString.Char8 as C (ByteString, pack, unpack)
import qualified Control.Monad.Error as E (Error, throwError)
import Codec.Compression.Snappy (compress, decompress)

-- |A database has some metadata and tables.
data DB = DB Meta [Table] deriving (Show, Read, Eq)

-- |Database's metadata consists of the format's version identifier.
data Meta = Meta Version deriving (Show, Read, Eq)

-- |Version identifier is a (major, minor) pair.
data Version = Version Int Int deriving (Show, Read, Eq)

-- |A table has a name, a list of columns' types and rows.
data Table = Table String [Column] [Row] deriving (Read, Show, Eq)

-- |A table's column which has a name and a type.
data Column = Column String Type deriving (Read, Show, Eq)

-- |Row consists of a list of values.
data Row = Row [Value] deriving (Read, Show, Eq)

-- |A monad in which queries are executed.
type DBMonad = Either Error

data Error = NoSuchColumn String
           | NoSuchTable String
           | TableMustHaveColumns
           | NoTableNameGiven
           | TableExists String
           | TypeMismatch
           deriving Show

instance E.Error Error

-- |query is function responsible for executing Query tokens.
query :: DB -> [Query] -> DBMonad (DB, [[Value]])

query db@(DB meta tables) (CreateTable name rows : _) =
  createTable name rows >>= \x -> return (x, [])
  where createTable _ [] = E.throwError TableMustHaveColumns
        createTable "" _ = E.throwError NoTableNameGiven
        createTable _ _ =
          case findTable db name of
            Right _  -> E.throwError $ TableExists name
            Left _ -> return (DB meta newTables)
        newTables  = addedTable : tables
        addedTable = Table name (map (uncurry Column) rows) []

query db (DropTable name : _) = dropTable name >> return (db, [])
  where
  DB meta tables = db
  dropTable "" = E.throwError NoTableNameGiven
  dropTable name = findTable db name >> return (DB meta (drop tables))
  drop = filter (\(Table thisName _ _) -> name /= thisName)

query db@(DB _ tables) (SelectAll tableName : stmts) =
  query db (Select tableName colNames : stmts)
  where (Table _ cols _) = head $ filter (\(Table n _ _) -> n == tableName) tables
        colNames = map (\(Column n _) -> n) cols

query db@(DB _ tables) (Select tableName selectedColumns : stmts) =
  do (Table _ columns rows, _) <- constrain table stmts
     values <- mapM (colSelect columns) rows
     return (db, values)
  where colSelect cols (Row values) = do ids <- columnIDs cols
                                         return $ map (values !!) ids
        columnIDs cols = mapM (colIDByName cols) selectedColumns
        colIDByName :: [Column] -> String -> DBMonad Int
        colIDByName cols name = case name `elemIndex` colNames cols of
                                  Nothing -> E.throwError $ NoSuchColumn name
                                  Just id -> return id
        colNames = map $ \(Column name _) -> name
        table = head $ filter (\(Table n _ _) -> n == tableName) tables

query db (Insert name values : _) = do db' <- insertRow db name values
                                       return (db', [])

query db@(DB _ tables) (Delete tableName : stmts) =
  do (Table _ cols _, Table _ _ rej) <- constrain table stmts
     db' <- modifyTable db tableName (deleteRows cols rej)
     return (db', [])
  where deleteRows cols rej _ = return $ Table tableName cols rej
        table = head $ filter (\(Table n _ _) -> n == tableName) tables

-- |Update query set new values in selected columns of the qRows list.
-- |After update concatenation of qRows and notQRows is placed as new
-- |table rows set.
query db@(DB _ tables) (Update name newValues : stmts) =
  do (Table _ cols acc, Table _ _ rej) <- constrain table stmts
     db' <- modifyTable db name $ newTable cols acc rej
     return (db', [])
  where table = head $ filter (\(Table n _ _) -> n == name) tables
        newTable cols acc rej _ = do newRows <- fmap (++rej) (updatedRows acc cols)
                                     return $ Table name cols newRows
        updatedRows acc cols = mapM (rowUpdate cols) acc
        indexedValues cols = mapM (colNamesToIndices cols) newValues
        columnNames = map (\(Column name _) -> name)
        colNamesToIndices :: [Column] -> (String, Value) -> DBMonad (Int, Value)
        colNamesToIndices cols (name, value) =
          case elemIndex name (columnNames cols) of
            Nothing    -> E.throwError $ NoSuchColumn name
            Just index -> return (index, value)
        rowUpdate cols (Row values) = do ivs <- indexedValues cols
                                         updated <- correct cols values ivs
                                         return $ Row updated
        correct :: [Column] -> [Value] -> [(Int, Value)] -> DBMonad [Value]
        correct cols values [] = return values
        correct cols values ((index, val):rest) =
          do updated <- checkAndCorrectRow values index val (cols !! index)
             correct cols updated rest
        checkAndCorrectRow :: [Value] -> Int -> Value -> Column -> DBMonad [Value]
        checkAndCorrectRow values index (StringValue str) (Column _ String) =
          return $ correctRow values index (StringValue str)
        checkAndCorrectRow values index (IntValue int) (Column _ Int) =
          return $ correctRow values index (IntValue int)
        checkAndCorrectRow _ _ _ _ = E.throwError TypeMismatch
        correctRow values index value =
            ((take index values)
            ++ [value]
            ++ (snd $ splitAt (index + 1) values))

constrain :: Table -> [Query] -> DBMonad (Table, Table)
constrain table stmts = foldlM constrainStep (table, emptyTable) stmts
  where emptyTable = Table "" [] []

constrainStep :: (Table, Table) -> Query -> DBMonad (Table, Table)

constrainStep ((Table name cols qRows), (Table _ _ notQRows)) (Where conds) =
  do (acc, rej) <- foldrM rowWalker ([], notQRows) qRows
     return (Table name cols acc, Table name cols rej)
  where rowWalker :: Row -> ([Row], [Row]) -> DBMonad ([Row], [Row])
        rowWalker (row@(Row values)) (accepted, rejected) =
          do matches <- whereWalker conds values
             return $ if matches
                           then (row:accepted, rejected)
                           else (accepted, row:rejected)
        whereWalker :: WhereConditions -> [Value] -> DBMonad Bool
        whereWalker (OrConditions conditions) values =
             fmap or (mapM (`whereWalker` values) conditions)
        whereWalker (AndConditions conditions) values =
             fmap and (mapM (`whereWalker` values) conditions)
        whereWalker (Condition colName comparer) values =
          do cell <- getCell colName values
             return $ comparer cell
        getCell name values = fmap (values !!) $ getColId name
        getColId name = case name `elemIndex` (columnNames cols) of
                          Nothing  -> E.throwError $ NoSuchColumn name
                          Just int -> return int
        columnNames = map (\(Column name _)->name)

-- |OrderBy query sorts a qRows list, comparing values from
-- |provided columns with selected order. The list of columns used to sort
-- |a qRows list is reversed. Each column and its order is used in comparing
-- |function used in stable sorting algorithm provided by 'sortByM'.
constrainStep (Table name cols qRows, rej) (OrderBy orderBy) =
  do sortedRows <- colSort qRows $ reverse orderBy
     return (Table name cols sortedRows, rej)
    where colSort rows [] = return rows
          colSort rows ((colName, ord):orderBy) =
            do sorted <- sortByM (cmp colName ord) rows
               colSort sorted orderBy
          cmp colName Ascending (Row valuesTwo) (Row valuesOne) =
            do index <- colIndex colName
               return $ orderingValue (valuesTwo !! index) (valuesOne !! index)
          cmp colName Descending x y = cmp colName Ascending y x
          orderingValue :: Value -> Value -> Ordering
          orderingValue valOne valTwo | valOne == valTwo  = EQ
                                      | valOne >  valTwo  = GT
                                      | valOne <  valTwo  = LT
          colIndex :: String -> DBMonad Int
          colIndex name = case name `elemIndex` columnNames of
              Nothing  -> E.throwError $ NoSuchColumn name
              Just int -> return int
          columnNames = map (\(Column name _)->name) cols

constrainStep ((Table name cols acc), (Table _ _ rej)) (Limit num) =
  return ((Table name cols acc'), (Table name cols (rej' ++ rej)))
  where (acc', rej') = splitAt num acc

-- |The current DB format's version identifier.
version :: Version
version = Version 0 0

-- |Creates a new DB instance.
new :: DB
new = DB (Meta version) []

-- |Loads an existing DB from a serialised form.
load :: C.ByteString -> DB
load = read . C.unpack . decompress

-- |Used to apply a given function to the table with a given name.
modifyTable :: DB
            -> String                   -- The name of a table to modify
            -> (Table -> DBMonad Table) -- The modifying function
            -> DBMonad DB
modifyTable db@(DB meta tables) name fun =
  findTable db name >> modTable tables >>= \tables' -> return (DB meta tables')
  where modTable :: [Table] -> DBMonad [Table]
        modTable [] = return []
        modTable (t@(Table thisName _ _):ts)
          | thisName == name = do t' <- fun t
                                  return $ t' : ts
          | otherwise        = fmap (t:) $ modTable ts

-- |Dumps the database to a bytestring, which can be later loaded using the
-- load function.
dump :: DB -> C.ByteString
dump = compress . C.pack . show

---- |Inserts a new row to a given table. It should check all types and constraints.
insertRow :: DB -> String -> [Value] -> DBMonad DB
insertRow db name values = modifyTable db name addRow
    where addRow :: Table -> DBMonad Table
          addRow (Table _ columns rows) = do new <- newRow
                                             return $ Table name columns (rows ++ [new])
              where newRow = fmap (Row) $ buildNewRow (types columns) values
                    types = map (\(Column _ t) -> t)
                    buildNewRow [] [] = return []
                    buildNewRow (String:restTs) (val@(StringValue _):restVs) =
                      do rest <- buildNewRow restTs restVs
                         return $ val:rest
                    buildNewRow (Int:restTs) (val@(IntValue _):restVs) =
                      do rest <- buildNewRow restTs restVs
                         return $ val:rest
                    buildNewRow _ _ = E.throwError TypeMismatch

-- |Returns a table with a given name. Throws an error if there's no table with
-- such name.
findTable :: DB -> String -> DBMonad Table
findTable (DB _ tables) name = findByName tables
    where findByName :: [Table] -> DBMonad Table
          findByName [] = E.throwError $ NoSuchTable name
          findByName (table@(Table thisName _ _):ts)
            | thisName == name = return table
            | otherwise        = findByName ts