-- |
-- 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