datalog-0.1.0.0: src/Database/Datalog/Database.hs
{-# LANGUAGE DeriveDataTypeable, FlexibleContexts #-}
module Database.Datalog.Database (
Relation,
Database,
DatabaseBuilder,
Tuple(..),
-- * Functions
makeDatabase,
addRelation,
assertFact,
databaseRelations,
databaseRelation,
dataForRelation,
addTupleToRelation,
addTupleToRelation',
replaceRelation,
ensureDatabaseRelation,
resetRelationDelta,
withDeltaRelation,
databaseHasDelta
) where
import Control.Failure
import Control.Monad.Trans.Class
import Control.Monad.Trans.State.Strict
import Data.Hashable
import Data.HashMap.Strict ( HashMap )
import qualified Data.HashMap.Strict as HM
import Data.HashSet ( HashSet )
import qualified Data.HashSet as HS
import Data.Monoid
import Data.Text ( Text )
import Database.Datalog.Errors
import Database.Datalog.Relation
import Debug.Trace
debug = flip trace
-- | A wrapper around lists that lets us more easily hide length
-- checks
newtype Tuple a = Tuple { unTuple :: [a] }
deriving (Eq, Show)
instance (Hashable a) => Hashable (Tuple a) where
hashWithSalt s (Tuple es) = s `hashWithSalt` es
-- | A relation whose elements are fixed-length lists of a
-- user-defined type. This is only used internally and is not exposed
-- to the user.
data DBRelation a = DBRelation { relationArity :: !Int
, relationName :: !Relation
, relationData :: [Tuple a]
, relationMembers :: !(HashSet (Tuple a))
, relationDelta :: [Tuple a]
, relationIndex :: !(HashMap (Int, a) (Tuple a))
}
deriving (Show)
instance (Eq a, Hashable a) => Eq (DBRelation a) where
(DBRelation arity1 n1 _ ms1 _ _) == (DBRelation arity2 n2 _ ms2 _ _) =
arity1 == arity2 && n1 == n2 && ms1 == ms2
-- | A database is a collection of facts organized into relations
newtype Database a = Database (HashMap Relation (DBRelation a))
instance (Show a) => Show (Database a) where
show (Database db) = show db
instance (Eq a, Hashable a) => Eq (Database a) where
(Database db1) == (Database db2) = db1 == db2
-- | The monad in which databases are constructed and initial facts
-- are asserted
type DatabaseBuilder m a = StateT (Database a) m
-- | Make a new fact Database in a DatabaseBuilder monad. It can
-- fail, and errors will be returned however the caller indicates.
makeDatabase :: (Failure DatalogError m)
=> DatabaseBuilder m a () -> m (Database a)
makeDatabase b = execStateT b (Database mempty)
-- | Add a relation to the 'Database'. If the relation exists, an
-- error will be raised. The function returns a 'RelationHandle' that
-- can be used in conjuction with 'addTuple'.
addRelation :: (Failure DatalogError m, Eq a, Hashable a)
=> Text -> Int -> DatabaseBuilder m a Relation
addRelation name arity = do
Database m <- get
case HM.lookup rel m of
Just _ -> lift $ failure (RelationExistsError name)
Nothing -> do
let r = DBRelation arity rel mempty mempty mempty mempty
put $! Database $! HM.insert rel r m
return rel
where
rel = Relation name
-- | Add a tuple to the named 'Relation' in the database. If the
-- tuple is already present, the original 'Database' is unchanged.
assertFact :: (Failure DatalogError m, Eq a, Hashable a)
=> Relation -> [a] -> DatabaseBuilder m a ()
assertFact relHandle tup = do
db@(Database m) <- get
let rel = databaseRelation db relHandle
wrappedTuple <- toWrappedTuple rel tup
case HS.member wrappedTuple (relationMembers rel) of
True -> return ()
False ->
let rel' = addTupleToRelation' rel wrappedTuple
in put $! Database $ HM.insert relHandle rel' m
-- | Replace a relation in the database. The old relation is
-- discarded completely, so be sure to initialize the replacement with
-- all of the currently known facts.
replaceRelation :: Database a -> DBRelation a -> Database a
replaceRelation (Database db) r =
Database $ HM.insert (relationName r) r db
-- | Add a tuple to the relation without updating the delta table.
-- This is needed for the initial database construction.
addTupleToRelation' :: (Eq a, Hashable a) => DBRelation a -> Tuple a -> DBRelation a
addTupleToRelation' rel t =
case HS.member t (relationMembers rel) of
True -> rel
False -> rel { relationData = t : relationData rel
, relationMembers = HS.insert t (relationMembers rel)
}
-- | Add the given tuple to the given 'Relation'. It updates the
-- index in the process. The 'Tuple' is already validated so this is
-- a total function.
--
-- It has already been verified that the tuple does not exist in the
-- relation (see 'addTuple') so no extra checks are required here.
addTupleToRelation :: (Eq a, Hashable a, Show a) => DBRelation a -> Tuple a -> DBRelation a
addTupleToRelation rel t =
case HS.member t (relationMembers rel) of
True -> rel
False -> rel { relationData = t : relationData rel
, relationMembers = HS.insert t (relationMembers rel)
, relationDelta = t : relationDelta rel
}
-- | If the requested relation is not in the database, just use the
-- original database (the result is the same - an empty relation)
withDeltaRelation :: Database a -> Relation -> (Database a -> b) -> b
withDeltaRelation d@(Database db) r action =
action $ case HM.lookup r db of
Nothing -> d
Just dbrel ->
let rel' = dbrel { relationData = relationDelta dbrel }
in Database $ HM.insert r rel' db
resetRelationDelta :: DBRelation a -> DBRelation a
resetRelationDelta rel = rel { relationDelta = mempty }
-- | Get a relation by name. If it does not exist in the database,
-- return a new relation with the appropriate arity.
ensureDatabaseRelation :: (Eq a, Hashable a)
=> Database a -> Relation -> Int -> DBRelation a
ensureDatabaseRelation (Database m) rel arity =
case HM.lookup rel m of
Just r -> r
Nothing -> DBRelation arity rel mempty mempty mempty mempty
-- | Get an existing relation from the database
databaseRelation :: Database a -> Relation -> DBRelation a
databaseRelation (Database m) rel =
case HM.lookup rel m of
-- This really shouldn't be possible - it would be an error in the
-- API since users can't create them and they can only be obtained
-- in the same monad with the Database
Nothing -> error ("Invalid RelationHandle: " ++ show rel)
Just r -> r
-- | Get all of the predicates referenced in the database
databaseRelations :: Database a -> [Relation]
databaseRelations (Database m) = HM.keys m
-- | Get all of the tuples for the given predicate/relation in the database.
dataForRelation :: (Failure DatalogError m)
=> Database a -> Relation -> m [Tuple a]
dataForRelation (Database m) rel =
case HM.lookup rel m of
Nothing -> failure $ NoRelationError rel
Just r -> return $ relationData r
databaseHasDelta :: Database a -> Bool
databaseHasDelta (Database db) =
any (not . null . relationDelta) (HM.elems db)-- `debug` show (map toDbg (HM.elems db))
-- where
-- toDbg r = show (relationName r) ++ ": " ++ show (not (null (relationDelta r)))
-- | Convert the user-level tuple to a safe length-checked Tuple.
-- Signals failure (according to @m@) if the length is invalid.
--
-- FIXME: It would also be nice to be able to check the column type...
toWrappedTuple :: (Failure DatalogError m)
=> DBRelation a -> [a] -> DatabaseBuilder m a (Tuple a)
toWrappedTuple rel tup =
case relationArity rel == length tup of
False -> lift $ failure (SchemaError (relationName rel))
True -> return $! Tuple tup