| 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.
> {-# OPTIONS -fglasgow-exts #-}
> module Database.InternalEnumerator
>   (
>     -- * Session object.
>       ISession(..), ConnectA(..)
>     , Statement(..), Command(..), EnvInquiry(..)
>     , PreparationA(..), IPrepared(..)
>     , 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, catchDyn, 
>		    dynExceptions, throwDyn, throwIO, bracket, Exception)
> import qualified Control.Exception (catch)
> data IsolationLevel =
>     ReadUncommitted
>   | ReadCommitted
>   | RepeatableRead
>   | Serialisable
>   | Serializable  -- ^ for alternative spellers
>   deriving Show
| 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)
| Throw a DBException. It's just a type-specific 'Control.Exception.throwDyn'.
> throwDB :: DBException -> a
> throwDB = throwDyn
-------------------------------------------------------------------- -- ** 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 | 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