qudb-0.0.0: Database/QUDB/Structure.hs
module Database.QUDB.Structure (new, load, dump, query, DB) where
import Database.QUDB.EntityTypes
import Database.QUDB.Query
import Data.List (elemIndex, sortBy)
import qualified Data.ByteString.Char8 as C (ByteString, pack, unpack, writeFile,
readFile)
import Codec.Compression.Snappy (compress, decompress)
import Control.DeepSeq
-- |A database has some metadata and tables.
data DB = DB Meta [Table] deriving (Show, Read, Eq)
instance NFData (DB) where
rnf (DB _ tables) = map rnf tables `deepseq` ()
-- |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)
instance NFData (Table) where
rnf (Table _ cols rows) = map rnf cols `deepseq` map rnf rows `deepseq` ()
-- |A table's column which has a name and a type.
data Column = Column String Type deriving (Read, Show, Eq)
instance NFData (Column) where
rnf (Column n t) = n `seq` t `seq` ()
-- |Row consists of a list of values.
data Row = Row [Value] deriving (Read, Show, Eq)
instance NFData (Row) where
rnf (Row values) = map rnf values `deepseq` ()
-- |query is function responsible for executing Query tokens.
query :: DB -> [Query] -> Maybe (DB, [[Value]])
query db (CreateTable name rows : _) = Just $!! (createTable db name rows, [])
query db (DropTable name : _) = fmap (const (db, [])) $ dropTable db name
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) =
Just (db, selRows)
where selRows = map (\(Row values) -> values) newQRows
newQRows = map colSelect rows
colSelect (Row values) = Row $ map (values !!) colIds
maybeColIds = map (`elemIndex` colNames) selectedColumns
colNames = map (\(Column cName _)-> cName) columns
colIds = map (\(Just int)->int) maybeColIds
(Table _ columns rows) = fst $ foldl constrain (table, emptyTable) stmts
emptyTable = Table "" [] []
table = head $ filter (\(Table n _ _) -> n == tableName) tables
query db (Insert name values : stmts) =
insertRow db name values >>= \db' -> Just $!! (db', [])
query db@(DB _ tables) (Delete tableName : stmts) = Just (modifiedDB, [])
where Just modifiedDB = modifyTable db tableName deleteRows
deleteRows _ = Table tableName cols rej
table = head $ filter (\(Table n _ _) -> n == tableName) tables
emptyTable = Table "" [] []
(Table _ cols _, (Table _ _ rej)) = foldl constrain (table, emptyTable) stmts
-- |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) = Just (modifiedDB, [])
where
(Table _ columns rows) = fst $ foldl constrain (table, emptyTable) stmts
emptyTable = Table "" [] []
table = head $ filter (\(Table n _ _) -> n == name) tables
(Table _ cols acc, (Table _ _ rej)) = foldl constrain (table, emptyTable) stmts
Just modifiedDB = modifyTable db name (modValues columns newValues acc rej)
modValues ::
[Column] -> [(String, Value)] -> [Row] -> [Row] -> Table -> Table
modValues columns newValues acc rej table = newTable table
newTable (Table name cols _) = Table name cols newRows
newRows = updatedRows ++ rej
updatedRows = map rowUpdate acc
indexValues = map extract newValues
columnNames = map (\(Column name _)->name) columns
extract (string, value) = case (elemIndex string columnNames) of
Nothing -> error $ "No such column: " ++ string
Just int -> (int, value)
rowUpdate (Row values) = Row $ correct values indexValues
correct :: [Value] -> [(Int, Value)] -> [Value]
correct values [] = values
correct values ((index, value):indexValues) = correct
(checkAndCorrectRow values index value (columns !! index))
indexValues
checkAndCorrectRow :: [Value] -> Int -> Value -> Column -> [Value]
checkAndCorrectRow values index
(StringValue str) (Column _ String) =
correctRow values index (StringValue str)
checkAndCorrectRow values index (IntValue int) (Column _ Int) =
correctRow values index (IntValue int)
checkAndCorrectRow _ _ _ _ = error "Incorrect types!"
correctRow values index value =
((take index values)
++ [value]
++ (snd $ splitAt (index + 1) values))
constrain :: (Table, Table) -> Query -> (Table, Table)
constrain ((Table name cols qRows), (Table _ _ notQRows)) (Where conds) =
((Table name cols acc), (Table name cols rej))
where
(acc, rej) =
foldr rowWalker ([], notQRows) qRows where
rowWalker (row@(Row values)) (qRows, notQRows) =
if whereWalker conds values
then (row:qRows, notQRows)
else (qRows, row:notQRows)
whereWalker :: WhereConditions -> [Value] -> Bool
whereWalker (OrConditions conditions) values =
or (map (`whereWalker` values) conditions)
whereWalker (AndConditions conditions) values =
and (map (`whereWalker` values) conditions)
whereWalker (Condition colName comparer) values =
comparer $ getCell colName values
getCell colName values = values !! getColId colName
getColId colName =
case elemIndex colName (map (\(Column name _)->name) cols) of
Nothing -> error $ "No such column: " ++ colName
Just int -> int
-- |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 'sortBy'.
constrain (Table name cols qRows, rej) (OrderBy orderBy) =
(Table name cols sortedQRows, rej)
where sortedQRows = colSort qRows $ reverse orderBy
colSort qrows [] = qrows
colSort rows ((cName, ord):orderBy) =
colSort (sortBy (cmp cName ord) rows) orderBy
cmp colName Ascending (Row valuesTwo) (Row valuesOne) =
orderingValue (valuesTwo !! colIndex colName)
(valuesOne !! colIndex colName)
cmp colName Descending (Row valuesTwo) (Row valuesOne) =
orderingValue (valuesOne !! colIndex colName)
(valuesTwo !! colIndex colName)
orderingValue :: Value -> Value -> Ordering
orderingValue valOne valTwo | valOne == valTwo = EQ
| valOne > valTwo = GT
| valOne < valTwo = LT
colIndex name = case elemIndex name columnNames of
Nothing -> error $ "No such column: " ++ name
Just int -> int
columnNames = map (\(Column name _)->name) cols
constrain ((Table name cols acc), (Table _ _ rej)) (Limit num) =
((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
-- |Adds a table to a given database.
createTable :: DB
-> String -- The name of the added table
-> [(String, Type)] -- Names and types of values stored in the table
-> DB
createTable _ _ [] = error "Tables without columns are illegal."
createTable _ "" _ = error "Table's name is mandatory."
createTable db@(DB meta tables) name cols =
case findTable db name of
Just _ -> error $ "Table: '" ++ name ++ "' already exists."
Nothing -> DB meta newTables
where newTables = addedTable : tables
addedTable = Table name (map (uncurry Column) cols) []
-- |Drops a table from a given database.
dropTable :: DB
-> String -- The name of the dropped table
-> Maybe DB
dropTable _ "" = error "Table's name is mandatory."
dropTable db@(DB meta tables) name =
findTable db name >> Just (DB meta (drop tables))
where drop = filter (\(Table thisName _ _) -> name /= thisName)
-- |Used to apply a given function to the table with a given name.
modifyTable :: DB
-> String -- The name of a table to modify
-> (Table -> Table) -- The modifying function
-> Maybe DB
modifyTable db@(DB meta tables) name fun =
findTable db name >> Just (DB meta (modTable tables))
where modTable [] = []
modTable (t@(Table thisName _ _):ts)
| thisName == name = fun t : ts
| otherwise = 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] -> Maybe DB
insertRow db name values = modifyTable db name addRow
where addRow :: Table -> Table
addRow (Table _ columns rows) = Table name columns (rows ++ [newRow])
where newRow = Row $ buildNewRow (types columns) values
types = map (\(Column _ t) -> t)
buildNewRow [] [] = []
buildNewRow (String:restTs) (val@(StringValue _):restVs) =
val:buildNewRow restTs restVs
buildNewRow (Int:restTs) (val@(IntValue _):restVs) =
val:buildNewRow restTs restVs
buildNewRow _ _ = error "Incorrect types!"
-- |Returns a table with a given name. Returns Nothing if there's no table with
-- such name.
findTable :: DB -> String -> Maybe Table
findTable (DB _ tables) name = findByName tables
where findByName :: [Table] -> Maybe Table
findByName [] = Nothing
findByName (table@(Table thisName _ _):ts)
| thisName == name = Just table
| otherwise = findByName ts