-- | -- Module : Database.InternalEnumerator -- Copyright : (c) 2004 Oleg Kiselyov, Alistair Bayley -- License : BSD-style -- Maintainer : oleg@pobox.com, alistair@abayley.org -- Stability : experimental -- Portability : non-portable -- This is the interface between the middle Enumerator layer and the -- low-level, Database-specific layer. This file is not exported to the end user. -- Only the programmer for a new back-end needs to consult this file. {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FunctionalDependencies #-} {-# LANGUAGE DeriveDataTypeable #-} module Database.InternalEnumerator ( -- * Session object. ISession(..), ConnectA(..) , Statement(..), Command(..), EnvInquiry(..) , PreparationA(..), IPrepared(..) , PreparedStmt(..) , BindA(..), DBBind(..) , IsolationLevel(..) , Position , IQuery(..) , DBType(..) , throwIfDBNull -- * Exceptions and handlers , DBException(..) , throwDB , ColNum, RowNum , SqlState, SqlStateClass, SqlStateSubClass ) where import Data.Typeable import Control.Exception (throw, Exception) data IsolationLevel = ReadUncommitted | ReadCommitted | RepeatableRead | Serialisable | Serializable -- ^ for alternative spellers deriving (Show, Eq, Ord, Enum) -- | A wrapper around the action to open the database. That wrapper is not -- exported to the end user. The only reason for the wrapper is to -- guarantee that the only thing to do with the result of -- 'Database.Enumerator.Sqlite.connect' function is to pass it out -- directly to 'Database.Enumerator.withSession'. newtype ConnectA sess = ConnectA (IO sess) deriving Typeable -- Position within the result set. Not for the end user. type Position = Int -- Needed for exceptions type RowNum = Int type ColNum = Int -- -------------------------------------------------------------------- -- -- ** Exceptions and handlers -- -------------------------------------------------------------------- type SqlStateClass = String type SqlStateSubClass = String type SqlState = (SqlStateClass, SqlStateSubClass) data DBException -- | DBMS error message. = DBError SqlState Int String | DBFatal SqlState Int String -- | the iteratee function used for queries accepts both nullable (Maybe) and -- non-nullable types. If the query itself returns a null in a column where a -- non-nullable type was specified, we can't handle it, so DBUnexpectedNull is thrown. | DBUnexpectedNull RowNum ColNum -- | Thrown by cursor functions if you try to fetch after the end. | DBNoData deriving (Typeable, Show) instance Exception DBException -- | Throw a DBException. It's just a type-specific 'Control.Exception.throwDyn'. throwDB :: DBException -> a throwDB = throw -- -------------------------------------------------------------------- -- -- ** Session interface -- -------------------------------------------------------------------- -- | The 'ISession' class describes a database session to a particular -- DBMS. Oracle has its own Session object, SQLite has its own -- session object (which maintains the connection handle to the database -- engine and other related stuff). Session objects for different databases -- normally have different types -- yet they all belong to the class ISession -- so we can do generic operations like @commit@, @execDDL@, etc. -- in a database-independent manner. -- Session objects per se are created by database connection\/login functions. -- The class 'ISession' is thus an interface between low-level (and -- database-specific) code and the Enumerator, database-independent -- code. -- The 'ISession' class is NOT visible to the end user -- neither the class, -- nor any of its methods. -- The 'ISession' class describes the mapping from connection object to -- the session object. The connection object is created by the end user -- (and this is how the end user tells which particular back end he wants). -- The session object is not accessible by the end user in any way. -- Even the type of the session object should be hidden! class ISession sess where disconnect :: sess -> IO () beginTransaction :: sess -> IsolationLevel -> IO () commit :: sess -> IO () rollback :: sess -> IO () -- We can have several types of statements: just plain strings, -- strings bundled with tuning parameters, prepared statements. -- BTW, statement with unbound variables should have a different type -- from that of the statement without bound variables or the statement -- with all bound variables. -- | 'Command' is not a query: command deletes or updates rows, creates\/drops -- tables, or changes database state. -- 'executeCommand' returns the number of affected rows (or 0 if DDL i.e. not DML). class ISession sess => Command stmt sess where -- insert/update/delete; returns number of rows affected executeCommand :: sess -> stmt -> IO Int class ISession sess => EnvInquiry inquirykey sess result | inquirykey sess -> result where inquire :: inquirykey -> sess -> IO result -- | 'Statement' defines the API for query objects i.e. -- which types can be queries. class ISession sess => Statement stmt sess q | stmt sess -> q where makeQuery :: sess -> stmt -> IO q -- |The class IQuery describes the class of query objects. Each -- database (that is, each Session object) has its own Query object. -- We may assume that a Query object includes (at least, conceptually) -- a (pointer to) a Session object, so a Query object determines the -- Session object. -- A back-end provides an instance (or instances) of IQuery. -- The end user never seens the IQuery class (let alone its methods). -- Can a session have several types of query objects? -- Let's assume that it can: but a statement plus the session uniquely -- determine the query, -- Note that we explicitly use IO monad because we will have to explicitly -- do FFI. class ISession sess => IQuery q sess b | q -> sess, q -> b where fetchOneRow :: q -> IO Bool currentRowNum :: q -> IO Int freeBuffer :: q -> b -> IO () destroyQuery :: q -> IO () -- |A \'buffer\' means a column buffer: a data structure that points to a -- block of memory allocated for the values of one particular -- column. Since a query normally fetches a row of several columns, we -- typically deal with a list of column buffers. Although the column data -- are typed (e.g., Integer, CalendarDate, etc), column buffers hide that -- type. Think of the column buffer as Dynamics. The class DBType below -- describes marshalling functions, to fetch a typed value out of the -- \'untyped\' columnBuffer. -- Different DBMS's (that is, different session objects) have, in -- general, columnBuffers of different types: the type of Column Buffer -- is specific to a database. -- So, ISession (m) uniquely determines the buffer type (b)?? -- Or, actually, a query uniquely determines the buffer. -- | The class DBType is not used by the end-user. -- It is used to tie up low-level database access and the enumerator. -- A database-specific library must provide a set of instances for DBType. class DBType a q b | q -> b where allocBufferFor :: a -> q -> Position -> IO b fetchCol :: q -> b -> IO a -- | Used by instances of DBType to throw an exception -- when a null (Nothing) is returned. -- Will work for any type, as you pass the fetch action in the fetcher arg. throwIfDBNull :: (Monad m) => m (RowNum, ColNum) -> m (Maybe a) -> m a throwIfDBNull pos fetcher = do v <- fetcher case v of Nothing -> do (row,col) <- pos throwDB (DBUnexpectedNull row col) Just m -> return m -- ------------------------------------------------------------------------ -- Prepared commands and statements newtype PreparedStmt mark stmt = PreparedStmt stmt -- | This type is not visible to the end user (cf. ConnectA). It forms a private -- `communication channel' between Database.Enumerator and a back end. -- Why don't we make a user-visible class with a @prepare@ method? -- Because it means to standardize the preparation method signature -- across all databases. Some databases need more parameters, some -- fewer. There may be several statement preparation functions within one -- database. So, instead of standardizing the signature of the -- preparation function, we standardize on the _result_ of that -- function. To be more precise, we standardize on the properties of the -- result: whatever it is, the eventual prepared statement should be -- suitable to be passed to 'bindRun'. newtype PreparationA sess stmt = PreparationA (sess -> IO stmt) class ISession sess => IPrepared stmt sess bound_stmt bo | stmt -> bound_stmt, stmt -> bo where bindRun :: sess -> stmt -> [BindA sess stmt bo] -> (bound_stmt -> IO a) -> IO a -- Should this be here? we have a need to free statements -- separately from result-sets (which are handled by IQuery.destroyQuery). -- It might be useful to prepare a statement, use it a number of times -- (so many result-sets are created+destroyed), and then destroy it, -- so it has a lifecycle independent of Queries. destroyStmt :: sess -> stmt -> IO () -- | The binding object (bo) below is very abstract, on purpose. -- It may be |IO a|, it may be String, it may be a function, etc. -- The binding object can hold the result of marshalling, -- or bo can hold the current counter, etc. -- Different databases do things very differently: -- compare PostgreSQL and the Stub (which models Oracle). newtype BindA sess stmt bo = BindA (sess -> stmt -> bo) -- | The class DBBind is not used by the end-user. -- It is used to tie up low-level database access and the enumerator. -- A database-specific library must provide a set of instances for DBBind. -- The latter are the dual of DBType. class ISession sess => DBBind a sess stmt bo | stmt -> bo where -- | This is really just a wrapper that lets us write lists of -- heterogenous bind values e.g. @[bindP \"string\", bindP (0::Int), ...]@ bindP :: a -> BindA sess stmt bo