packages feed

selda 0.1.7.0 → 0.1.8.0

raw patch · 19 files changed

+749/−254 lines, 19 filesdep +bytestringPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: bytestring

API changes (from Hackage documentation)

- Database.Selda: class SqlType a => MinMax a
- Database.Selda: instance (Database.Selda.SqlType.SqlType a, GHC.Num.Num a) => Database.Selda.MinMax a
- Database.Selda: instance Database.Selda.MinMax Data.Text.Internal.Text
- Database.Selda: instance Database.Selda.MinMax a => Database.Selda.MinMax (GHC.Base.Maybe a)
- Database.Selda.Backend: S :: StateT SeldaState m a -> SeldaT m a
- Database.Selda.Backend: [customColType] :: SeldaBackend -> Text -> [ColAttr] -> Maybe Text
- Database.Selda.Backend: [dbIdentifier] :: SeldaBackend -> Text
- Database.Selda.Backend: [defaultKeyword] :: SeldaBackend -> Text
- Database.Selda.Backend: [unS] :: SeldaT m a -> StateT SeldaState m a
- Database.Selda.Backend: class Monad m => MonadIO (m :: * -> *)
- Database.Selda.Backend: compileColAttr :: ColAttr -> Text
- Database.Selda.Backend: instance Control.Monad.Catch.MonadCatch m => Control.Monad.Catch.MonadCatch (Database.Selda.Backend.SeldaT m)
- Database.Selda.Backend: instance Control.Monad.Catch.MonadMask m => Control.Monad.Catch.MonadMask (Database.Selda.Backend.SeldaT m)
- Database.Selda.Backend: instance Control.Monad.Catch.MonadThrow m => Control.Monad.Catch.MonadThrow (Database.Selda.Backend.SeldaT m)
- Database.Selda.Backend: instance Control.Monad.IO.Class.MonadIO m => Control.Monad.IO.Class.MonadIO (Database.Selda.Backend.SeldaT m)
- Database.Selda.Backend: instance Control.Monad.IO.Class.MonadIO m => Database.Selda.Backend.MonadSelda (Database.Selda.Backend.SeldaT m)
- Database.Selda.Backend: instance Control.Monad.Trans.Class.MonadTrans Database.Selda.Backend.SeldaT
- Database.Selda.Backend: instance GHC.Base.Functor m => GHC.Base.Functor (Database.Selda.Backend.SeldaT m)
- Database.Selda.Backend: instance GHC.Base.Monad m => GHC.Base.Applicative (Database.Selda.Backend.SeldaT m)
- Database.Selda.Backend: instance GHC.Base.Monad m => GHC.Base.Monad (Database.Selda.Backend.SeldaT m)
- Database.Selda.Backend: instance GHC.Classes.Eq Database.Selda.Backend.SeldaError
- Database.Selda.Backend: instance GHC.Exception.Exception Database.Selda.Backend.SeldaError
- Database.Selda.Backend: instance GHC.Show.Show Database.Selda.Backend.SeldaError
- Database.Selda.Backend: liftIO :: MonadIO m => IO a -> m a
- Database.Selda.Backend: newtype SeldaT m a
+ Database.Selda: class Preparable q
+ Database.Selda: class Prepare q f
+ Database.Selda: class SqlType a => SqlOrd a
+ Database.Selda: data Day :: *
+ Database.Selda: data TimeOfDay :: *
+ Database.Selda: data UTCTime :: *
+ Database.Selda: defaultValue :: SqlType a => Lit a
+ Database.Selda: fromRowId :: RowID -> Int
+ Database.Selda: fromSql :: SqlType a => SqlValue -> a
+ Database.Selda: instance (Database.Selda.SqlType.SqlType a, GHC.Num.Num a) => Database.Selda.SqlOrd a
+ Database.Selda: instance Database.Selda.SqlOrd Data.Text.Internal.Text
+ Database.Selda: instance Database.Selda.SqlOrd Data.Time.Calendar.Days.Day
+ Database.Selda: instance Database.Selda.SqlOrd Data.Time.Clock.UTC.UTCTime
+ Database.Selda: instance Database.Selda.SqlOrd Data.Time.LocalTime.TimeOfDay.TimeOfDay
+ Database.Selda: instance Database.Selda.SqlOrd a => Database.Selda.SqlOrd (GHC.Base.Maybe a)
+ Database.Selda: mkLit :: SqlType a => a -> Lit a
+ Database.Selda: prepared :: (Preparable q, Prepare q f, Equiv q f) => q -> f
+ Database.Selda: sqlType :: SqlType a => Proxy a -> SqlTypeRep
+ Database.Selda.Backend: Other :: Text -> BackendID
+ Database.Selda.Backend: PPConfig :: (SqlTypeRep -> Text) -> (Int -> Text) -> ([ColAttr] -> Text) -> Text -> PPConfig
+ Database.Selda.Backend: PostgreSQL :: BackendID
+ Database.Selda.Backend: SQLite :: BackendID
+ Database.Selda.Backend: TBlob :: SqlTypeRep
+ Database.Selda.Backend: TBool :: SqlTypeRep
+ Database.Selda.Backend: TDate :: SqlTypeRep
+ Database.Selda.Backend: TDateTime :: SqlTypeRep
+ Database.Selda.Backend: TFloat :: SqlTypeRep
+ Database.Selda.Backend: TInt :: SqlTypeRep
+ Database.Selda.Backend: TRowID :: SqlTypeRep
+ Database.Selda.Backend: TText :: SqlTypeRep
+ Database.Selda.Backend: TTime :: SqlTypeRep
+ Database.Selda.Backend: [LBlob] :: !ByteString -> Lit ByteString
+ Database.Selda.Backend: [SqlBlob] :: !ByteString -> SqlValue
+ Database.Selda.Backend: [backendId] :: SeldaBackend -> BackendID
+ Database.Selda.Backend: [closeConnection] :: SeldaBackend -> SeldaConnection -> IO ()
+ Database.Selda.Backend: [ppAutoIncInsert] :: PPConfig -> Text
+ Database.Selda.Backend: [ppColAttrs] :: PPConfig -> [ColAttr] -> Text
+ Database.Selda.Backend: [ppConfig] :: SeldaBackend -> PPConfig
+ Database.Selda.Backend: [ppPlaceholder] :: PPConfig -> Int -> Text
+ Database.Selda.Backend: [ppType] :: PPConfig -> SqlTypeRep -> Text
+ Database.Selda.Backend: [prepareStmt] :: SeldaBackend -> StmtID -> [SqlTypeRep] -> Text -> IO Dynamic
+ Database.Selda.Backend: [runPrepared] :: SeldaBackend -> Dynamic -> [Param] -> IO (Int, [[SqlValue]])
+ Database.Selda.Backend: allStmts :: SeldaConnection -> IO [(StmtID, Dynamic)]
+ Database.Selda.Backend: class Typeable a => SqlType a where sqlType _ = litType (defaultValue :: Lit a)
+ Database.Selda.Backend: data BackendID
+ Database.Selda.Backend: data PPConfig
+ Database.Selda.Backend: data SeldaConnection
+ Database.Selda.Backend: data SeldaT m a
+ Database.Selda.Backend: data SqlTypeRep
+ Database.Selda.Backend: data StmtID
+ Database.Selda.Backend: defPPConfig :: PPConfig
+ Database.Selda.Backend: defaultValue :: SqlType a => Lit a
+ Database.Selda.Backend: fromSql :: SqlType a => SqlValue -> a
+ Database.Selda.Backend: mkLit :: SqlType a => a -> Lit a
+ Database.Selda.Backend: newConnection :: MonadIO m => SeldaBackend -> Text -> m SeldaConnection
+ Database.Selda.Backend: seldaClose :: MonadIO m => SeldaConnection -> m ()
+ Database.Selda.Backend: seldaConnection :: MonadSelda m => m SeldaConnection
+ Database.Selda.Backend: sqlType :: SqlType a => Proxy a -> SqlTypeRep
- Database.Selda: (.<) :: SqlType a => Col s a -> Col s a -> Col s Bool
+ Database.Selda: (.<) :: SqlOrd a => Col s a -> Col s a -> Col s Bool
- Database.Selda: (.<=) :: SqlType a => Col s a -> Col s a -> Col s Bool
+ Database.Selda: (.<=) :: SqlOrd a => Col s a -> Col s a -> Col s Bool
- Database.Selda: (.>) :: SqlType a => Col s a -> Col s a -> Col s Bool
+ Database.Selda: (.>) :: SqlOrd a => Col s a -> Col s a -> Col s Bool
- Database.Selda: (.>=) :: SqlType a => Col s a -> Col s a -> Col s Bool
+ Database.Selda: (.>=) :: SqlOrd a => Col s a -> Col s a -> Col s Bool
- Database.Selda: class Typeable a => SqlType a
+ Database.Selda: class Typeable a => SqlType a where sqlType _ = litType (defaultValue :: Lit a)
- Database.Selda: compileCreateTable :: (Text -> [ColAttr] -> Maybe Text) -> OnError -> Table a -> Text
+ Database.Selda: compileCreateTable :: PPConfig -> OnError -> Table a -> Text
- Database.Selda: compileInsert :: Insert a => Text -> Table a -> [a] -> (Text, [Param])
+ Database.Selda: compileInsert :: Insert a => PPConfig -> Table a -> [a] -> (Text, [Param])
- Database.Selda: compileUpdate :: forall s a. (Columns (Cols s a), Result (Cols s a)) => (Text -> Text) -> Table a -> (Cols s a -> Cols s a) -> (Cols s a -> Col s Bool) -> (Text, [Param])
+ Database.Selda: compileUpdate :: forall s a. (Columns (Cols s a), Result (Cols s a)) => PPConfig -> Table a -> (Cols s a -> Cols s a) -> (Cols s a -> Col s Bool) -> (Text, [Param])
- Database.Selda: liftIO :: MonadIO m => IO a -> m a
+ Database.Selda: liftIO :: MonadIO m => forall a. IO a -> m a
- Database.Selda: limit :: Int -> Int -> Query (Inner s) a -> Query s a
+ Database.Selda: limit :: Int -> Int -> Query (Inner s) a -> Query s (OuterCols a)
- Database.Selda: max_ :: MinMax a => Col s a -> Aggr s a
+ Database.Selda: max_ :: SqlOrd a => Col s a -> Aggr s a
- Database.Selda: min_ :: MinMax a => Col s a -> Aggr s a
+ Database.Selda: min_ :: SqlOrd a => Col s a -> Aggr s a
- Database.Selda.Backend: SeldaBackend :: QueryRunner (Int, [[SqlValue]]) -> QueryRunner Int -> (Text -> [ColAttr] -> Maybe Text) -> Text -> Text -> SeldaBackend
+ Database.Selda.Backend: SeldaBackend :: (Text -> [Param] -> IO (Int, [[SqlValue]])) -> (Text -> [Param] -> IO Int) -> (StmtID -> [SqlTypeRep] -> Text -> IO Dynamic) -> (Dynamic -> [Param] -> IO (Int, [[SqlValue]])) -> PPConfig -> (SeldaConnection -> IO ()) -> BackendID -> SeldaBackend
- Database.Selda.Backend: [LCustom] :: !(Lit a) -> Lit b
+ Database.Selda.Backend: [LCustom] :: Lit a -> Lit b
- Database.Selda.Backend: [LJust] :: !(Lit a) -> Lit (Maybe a)
+ Database.Selda.Backend: [LJust] :: SqlType a => !(Lit a) -> Lit (Maybe a)
- Database.Selda.Backend: [LNull] :: Lit (Maybe a)
+ Database.Selda.Backend: [LNull] :: SqlType a => Lit (Maybe a)
- Database.Selda.Backend: [runStmtWithPK] :: SeldaBackend -> QueryRunner Int
+ Database.Selda.Backend: [runStmtWithPK] :: SeldaBackend -> Text -> [Param] -> IO Int
- Database.Selda.Backend: [runStmt] :: SeldaBackend -> QueryRunner (Int, [[SqlValue]])
+ Database.Selda.Backend: [runStmt] :: SeldaBackend -> Text -> [Param] -> IO (Int, [[SqlValue]])
- Database.Selda.Backend: runSeldaT :: MonadIO m => SeldaT m a -> SeldaBackend -> m a
+ Database.Selda.Backend: runSeldaT :: (MonadIO m, MonadMask m) => SeldaT m a -> SeldaConnection -> m a

Files

ChangeLog.md view
@@ -1,6 +1,18 @@ # Revision history for Selda  +## 0.1.8.0 -- 2017-06-10++* Move SQL pretty-printing config into a single type.+* Support for binary blobs.+* Support for prepared statements.+* Support for connection reuse across Selda computations.+* Cleaner and more robust backend API.+* Stricter type constraints on comparisons.+* Allow limit on inner queries.+* Allow inspecting row identifiers.++ ## 0.1.7.0 -- 2017-05-17  * Add specialized insertUnless upsert variant.
README.md view
@@ -26,6 +26,7 @@ * Inserting, updating and deleting rows from tables. * Conditional insert/update. * Transactions, uniqueness constraints and foreign keys.+* Seamless prepared statements. * Configurable, automatic, consistent in-process caching of query results. * Lightweight and modular: non-essential features are optional or split into   add-on packages.@@ -634,8 +635,48 @@   return (map fromRel ps) ``` ++Prepared statements+-------------------++While Selda makes use of prepared statements internally to ensure that any and+all input is safely escaped, it does not reuse those statements by default.+Every query is recompiler and replanned each time it is executed.+To improve the performance of your code, you should make use of the `prepared`+function, to mark performance-critical queries as reusable.++The `prepared` function converts any function `f` in the `Query` monad into an+equivalent function `f'` in some `MonadSelda`, provided that all of `f`'s+arguments are column expressions.+When `f'` is called for the first time during a connection to a database, it+automatically gets compiled, prepared and cached before being executed.+Any subsequent calls to `f'` from the same connection will reuse the prepared+version.++Note that since most database engines don't allow prepared statements to persist+across connections, a previously cached statement will get prepared once more if+called from another connection.++As an example, we modify the `grownupsIn` function we saw earlier to use prepared+statements.++```+preparedGrownupsIn :: Text -> SeldaM [Text]+preparedGrownupsIn = prepared $ \city -> do+  (name :*: age :*: _) <- select people+  restrict (age .> 20)+  (name' :*: home) <- select addresses+  restrict (home .== city .&& name .== name')+  return name+```++Note that the type of the `city` argument is `Col s Text` within the query, but+when *calling* `preparedGrownupsIn`, we instead pass in a value of type `Text`;+for convenience, `prepared` automatically converts all arguments to+prepared functions into their equivalent column types.+ And with that, we conclude this tutorial. Hopefully it has been enough to get-you comfortable started using Selda.+you comfortably started using Selda. For a more detailed API reference, please see Selda's [Haddock documentation](http://hackage.haskell.org/package/selda). 
selda.cabal view
@@ -1,5 +1,5 @@ name:                selda-version:             0.1.7.0+version:             0.1.8.0 synopsis:            Type-safe, high-level EDSL for interacting with relational databases. description:         This package provides an EDSL for writing portable, type-safe, high-level                      database code. Its feature set includes querying and modifying databases,@@ -49,17 +49,20 @@     Database.Selda.Generic     Database.Selda.Unsafe   other-modules:+    Database.Selda.Backend.Internal     Database.Selda.Caching     Database.Selda.Column     Database.Selda.Compile     Database.Selda.Exp     Database.Selda.Frontend     Database.Selda.Inner+    Database.Selda.Prepared     Database.Selda.Query     Database.Selda.Query.Type     Database.Selda.Selectors     Database.Selda.SQL     Database.Selda.SQL.Print+    Database.Selda.SQL.Print.Config     Database.Selda.SqlType     Database.Selda.Table     Database.Selda.Table.Compile@@ -79,19 +82,20 @@     GeneralizedNewtypeDeriving     FlexibleContexts   build-depends:-      base       >=4.8 && <5-    , exceptions >=0.8 && <0.9-    , mtl        >=2.0 && <2.3-    , text       >=1.0 && <1.3-    , time       >=1.5 && <1.9+      base                 >=4.8   && <5+    , bytestring           >=0.10  && <0.11+    , exceptions           >=0.8   && <0.9+    , hashable             >=1.1   && <1.3+    , mtl                  >=2.0   && <2.3+    , text                 >=1.0   && <1.3+    , time                 >=1.5   && <1.9+    , unordered-containers >=0.2.6 && <0.3   if impl(ghc < 7.11)     build-depends:       transformers  >=0.4 && <0.6   if !flag(haste) && flag(localcache)     build-depends:-        hashable             >=1.1   && <1.3-      , psqueues             >=0.2   && <0.3-      , unordered-containers >=0.2.6 && <0.3+      psqueues >=0.2 && <0.3   else     cpp-options:       -DNO_LOCALCACHE
src/Database/Selda.hs view
@@ -63,14 +63,14 @@ --   for a more comprehensive tutorial. module Database.Selda   ( -- * Running queries-    MonadIO (..), MonadSelda+    MonadSelda   , SeldaError (..), ValidationError   , SeldaT, SeldaM, Table, Query, Col, Res, Result   , query, transaction, setLocalCache     -- * Constructing queries   , Selector, (!), Assignment(..), with-  , SqlType-  , Text, Cols, Columns+  , SqlType (..)+  , Cols, Columns   , Order (..)   , (:*:)(..)   , select, selectValues, from@@ -79,7 +79,7 @@   , inner, suchThat     -- * Expressions over columns   , Set (..)-  , RowID, invalidRowId, isInvalidRowId+  , RowID, invalidRowId, isInvalidRowId, fromRowId   , (.==), (./=), (.>), (.<), (.>=), (.<=), like   , (.&&), (.||), not_   , literal, int, float, text, true, false, null_@@ -87,7 +87,7 @@     -- * Converting between column types   , round_, just, fromBool, fromInt, toString     -- * Inner queries-  , Aggr, Aggregates, OuterCols, LeftCols, Inner, MinMax+  , Aggr, Aggregates, OuterCols, LeftCols, Inner, SqlOrd   , innerJoin, leftJoin   , aggregate, groupBy   , count, avg, sum_, max_, min_@@ -96,6 +96,9 @@   , insert, insert_, insertWithPK, tryInsert, def   , update, update_, upsert   , deleteFrom, deleteFrom_+    -- * Prepared statements+  , Preparable, Prepare+  , prepared     -- * Defining schemas   , TableSpec, ColSpecs, ColSpec, TableName, ColName   , NonNull, IsNullable, Nullable, NotNullable@@ -116,12 +119,16 @@     -- * Tuple convenience functions   , Tup, Head   , first, second, third, fourth, fifth, sixth, seventh, eighth, ninth, tenth+    -- * Useful re-exports+  , MonadIO, liftIO+  , Text, Day, TimeOfDay, UTCTime   ) where import Database.Selda.Backend import Database.Selda.Column import Database.Selda.Compile import Database.Selda.Frontend import Database.Selda.Inner+import Database.Selda.Prepared import Database.Selda.Query import Database.Selda.Query.Type import Database.Selda.Selectors@@ -134,14 +141,18 @@ import Database.Selda.Unsafe import Control.Exception (throw) import Data.Text (Text)+import Data.Time (Day, TimeOfDay, UTCTime) import Data.Typeable (eqT, (:~:)(..)) import Unsafe.Coerce  -- | Any column type that can be used with the 'min_' and 'max_' functions.-class SqlType a => MinMax a-instance {-# OVERLAPPABLE #-} (SqlType a, Num a) => MinMax a-instance MinMax Text-instance MinMax a => MinMax (Maybe a)+class SqlType a => SqlOrd a+instance {-# OVERLAPPABLE #-} (SqlType a, Num a) => SqlOrd a+instance SqlOrd Text+instance SqlOrd Day+instance SqlOrd UTCTime+instance SqlOrd TimeOfDay+instance SqlOrd a => SqlOrd (Maybe a)  -- | Convenient shorthand for @fmap (! sel) q@. --   The following two queries are quivalent:@@ -184,7 +195,8 @@   return x infixr 7 `suchThat` -(.==), (./=), (.>), (.<), (.>=), (.<=) :: SqlType a => Col s a -> Col s a -> Col s Bool+(.==), (./=) :: SqlType a => Col s a -> Col s a -> Col s Bool+(.>), (.<), (.>=), (.<=) :: SqlOrd a => Col s a -> Col s a -> Col s Bool (.==) = liftC2 $ BinOp Eq (./=) = liftC2 $ BinOp Neq (.>)  = liftC2 $ BinOp Gt@@ -231,8 +243,6 @@ --   For an auto-incrementing primary key, the default value is the next key. -- --   Using @def@ in any other context than insertion results in a runtime error.---   Likewise, if @def@ is given for a column that does not have a default---   value, the insertion will fail. def :: SqlType a => a def = throw DefaultValueException @@ -287,11 +297,11 @@ avg = aggr "AVG"  -- | The greatest value in the given column. Texts are compared lexically.-max_ :: MinMax a => Col s a -> Aggr s a+max_ :: SqlOrd a => Col s a -> Aggr s a max_ = aggr "MAX"  -- | The smallest value in the given column. Texts are compared lexically.-min_  :: MinMax a => Col s a -> Aggr s a+min_  :: SqlOrd a => Col s a -> Aggr s a min_ = aggr "MIN"  -- | Sum all values in the given column.
src/Database/Selda/Backend.hs view
@@ -1,126 +1,25 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}--- | API for building Selda backends.+-- | API for building Selda backends and adding support for more types+--   in queries. module Database.Selda.Backend-  ( MonadIO (..)-  , QueryRunner, SeldaBackend (..), MonadSelda (..), SeldaT (..), SeldaM-  , SeldaError (..)-  , Param (..), Lit (..), SqlValue (..), ColAttr (..)-  , compileColAttr+  ( MonadSelda (..), SeldaT, SeldaM, SeldaError (..)+  , StmtID, BackendID (..), QueryRunner, SeldaBackend (..), SeldaConnection+  , SqlType (..), SqlValue (..), SqlTypeRep (..)+  , Param (..), Lit (..), ColAttr (..)+  , PPConfig (..), defPPConfig+  , newConnection, allStmts, seldaBackend+  , runSeldaT, seldaClose   , sqlDateTimeFormat, sqlDateFormat, sqlTimeFormat-  , runSeldaT   ) where-import Database.Selda.Caching (invalidate)-import Database.Selda.SQL (Param (..))-import Database.Selda.SqlType-import Database.Selda.Table (Table, ColAttr (..), tableName)-import Database.Selda.Table.Compile (compileColAttr)-import Database.Selda.Types (TableName)-import Control.Exception (throwIO)-import Control.Monad.Catch+import Database.Selda.Backend.Internal+import Control.Monad import Control.Monad.IO.Class-import Control.Monad.State-import Data.Text (Text)-import Data.Typeable---- | Thrown by any function in 'SeldaT' if an error occurs.-data SeldaError-  = DbError String  -- ^ Unable to open or connect to database.-  | SqlError String -- ^ An error occurred while executing query.-  deriving (Show, Eq, Typeable)--instance Exception SeldaError---- | A function which executes a query and gives back a list of extensible---   tuples; one tuple per result row, and one tuple element per column.-type QueryRunner a = Text -> [Param] -> IO a---- | A collection of functions making up a Selda backend.-data SeldaBackend = SeldaBackend-  { -- | Execute an SQL statement.-    runStmt       :: QueryRunner (Int, [[SqlValue]])--    -- | Execute an SQL statement and return the last inserted primary key,-    --   where the primary key is auto-incrementing.-    --   Backends must take special care to make this thread-safe.-  , runStmtWithPK :: QueryRunner Int--    -- | Generate a custom column type for the column having the given Selda-    --   type and list of attributes.-  , customColType :: Text -> [ColAttr] -> Maybe Text--    -- | The keyword that represents the default value for auto-incrementing-    --   primary keys.-  , defaultKeyword :: Text--    -- | A string uniquely identifying the database used by this invocation-    --   of the backend. This could be, for instance, a PostgreSQL connection-    --   string or the absolute path to an SQLite file.-  , dbIdentifier   :: Text-}--data SeldaState = SeldaState-  { -- | Backend in use by the current computation.-    stBackend :: !SeldaBackend--    -- | Tables modified by the current transaction.-    --   Invariant: always @Just xs@ during a transaction, and always-    --   @Nothing@ when not in a transaction.-  , stTouchedTables :: !(Maybe [TableName])-  }---- | Some monad with Selda SQL capabilitites.-class MonadIO m => MonadSelda m where-  -- | Get the backend in use by the computation.-  seldaBackend :: m SeldaBackend--  -- | Invalidate the given table as soon as the current transaction finishes.-  --   Invalidate the table immediately if no transaction is ongoing.-  invalidateTable :: Table a -> m ()--  -- | Indicates the start of a new transaction.-  --   Starts bookkeeping to invalidate all tables modified during-  --   the transaction at the next call to 'endTransaction'.-  beginTransaction :: m ()--  -- | Indicates the end of the current transaction.-  --   Invalidates all tables that were modified since the last call to-  --   'beginTransaction', unless the transaction was rolled back.-  endTransaction :: Bool -- ^ @True@ if the transaction was committed,-                         --   @False@ if it was rolled back.-                 -> m ()---- | Monad transformer adding Selda SQL capabilities.-newtype SeldaT m a = S {unS :: StateT SeldaState m a}-  deriving ( Functor, Applicative, Monad, MonadIO-           , MonadThrow, MonadCatch, MonadMask, MonadTrans-           )--instance MonadIO m => MonadSelda (SeldaT m) where-  seldaBackend = S $ fmap stBackend get--  invalidateTable tbl = S $ do-    st <- get-    case stTouchedTables st of-      Nothing -> liftIO $ invalidate [tableName tbl]-      Just ts -> put $ st {stTouchedTables = Just (tableName tbl : ts)}--  beginTransaction = S $ do-    st <- get-    case stTouchedTables st of-      Nothing -> put $ st {stTouchedTables = Just []}-      Just _  -> liftIO $ throwIO $ SqlError "attempted to nest transactions"--  endTransaction committed = S $ do-    st <- get-    case stTouchedTables st of-      Just ts | committed -> liftIO $ invalidate ts-      _                   -> return ()-    put $ st {stTouchedTables = Nothing}---- | The simplest form of Selda computation; 'SeldaT' specialized to 'IO'.-type SeldaM = SeldaT IO+import Data.IORef --- | Run a Selda transformer. Backends should use this to implement their---   @withX@ functions.-runSeldaT :: MonadIO m => SeldaT m a -> SeldaBackend -> m a-runSeldaT m b = fst <$> runStateT (unS m) (SeldaState b Nothing)+-- | Close a reusable Selda connection.+--   Closing a connection while in use is undefined.+--   Passing a closed connection to 'runSeldaT' results in a 'SeldaError'+--   being thrown. Closing a connection more than once is a no-op.+seldaClose :: MonadIO m => SeldaConnection -> m ()+seldaClose c = liftIO $ do+  closed <- atomicModifyIORef' (connClosed c) $ \closed -> (True, closed)+  unless closed $ closeConnection (connBackend c) c
+ src/Database/Selda/Backend/Internal.hs view
@@ -0,0 +1,223 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+-- | Internal backend API.+module Database.Selda.Backend.Internal+  ( StmtID, BackendID (..)+  , QueryRunner, SeldaBackend (..), SeldaConnection (..), SeldaStmt (..)+  , MonadSelda (..), SeldaT (..), SeldaM+  , SeldaError (..)+  , Param (..), Lit (..), ColAttr (..)+  , SqlType (..), SqlValue (..), SqlTypeRep (..)+  , PPConfig (..), defPPConfig+  , sqlDateTimeFormat, sqlDateFormat, sqlTimeFormat+  , freshStmtId+  , newConnection, allStmts+  , runSeldaT, seldaBackend+  ) where+import Database.Selda.Caching (invalidate)+import Database.Selda.SQL (Param (..))+import Database.Selda.SqlType+import Database.Selda.Table (Table, ColAttr (..), tableName)+import Database.Selda.SQL.Print.Config+import Database.Selda.Types (TableName)+import Control.Concurrent+import Control.Monad.Catch+import Control.Monad.IO.Class+import Control.Monad.State+import Data.Dynamic+import Data.Hashable+import qualified Data.HashMap.Strict as M+import Data.IORef+import Data.Text (Text)+import System.IO.Unsafe (unsafePerformIO)++-- | Uniquely identifies some particular backend.+--+--   When publishing a new backend, consider submitting a pull request with a+--   constructor for your backend instead of using the @Other@ constructor.+data BackendID = SQLite | PostgreSQL | Other Text+  deriving (Show, Eq, Ord)++-- | Thrown by any function in 'SeldaT' if an error occurs.+data SeldaError+  = DbError String  -- ^ Unable to open or connect to database.+  | SqlError String -- ^ An error occurred while executing query.+  deriving (Show, Eq, Typeable)++instance Exception SeldaError++-- | A prepared statement identifier. Guaranteed to be unique per application.+newtype StmtID = StmtID Int+  deriving (Show, Eq, Ord, Hashable)++-- | A connection identifier. Guaranteed to be unique per application.+newtype ConnID = ConnID Int+  deriving (Show, Eq, Ord)++{-# NOINLINE nextStmtId #-}+nextStmtId :: IORef Int+nextStmtId = unsafePerformIO $ newIORef 1++-- | Generate a fresh statement identifier, guaranteed to be unique per process.+freshStmtId :: MonadIO m => m StmtID+freshStmtId = liftIO $ atomicModifyIORef' nextStmtId $ \n -> (n+1, StmtID n)++-- | A function which executes a query and gives back a list of extensible+--   tuples; one tuple per result row, and one tuple element per column.+type QueryRunner a = Text -> [Param] -> IO a++-- | A prepared statement.+data SeldaStmt = SeldaStmt+ { -- | Backend-specific handle to the prepared statement.+   stmtHandle :: !Dynamic++   -- | The SQL code for the statement.+ , stmtText :: !Text++   -- | All parameters to be passed to the prepared statement.+   --   Parameters that are unique to each invocation are specified as indices+   --   starting at 0.+   --   Backends implementing @runPrepared@ should probably ignore this field.+ , stmtParams :: ![Either Int Param]+   +   -- | All tables touched by the statement.+ , stmtTables :: ![TableName]+ }++data SeldaConnection = SeldaConnection+  { -- | The backend used by the current connection.+    connBackend :: !SeldaBackend++    -- | A string uniquely identifying the database used by this connection.+    --   This could be, for instance, a PostgreSQL connection+    --   string or the absolute path to an SQLite file.+  , connDbId :: Text++    -- | All statements prepared for this connection.+  , connStmts :: !(IORef (M.HashMap StmtID SeldaStmt))++    -- | Is the connection closed?+  , connClosed :: !(IORef Bool)++    -- | Lock to prevent this connection from being used concurrently by+    --   multiple invocations of 'runSeldaT'.+  , connLock :: !(MVar ())+}++-- | Create a new Selda connection for the given backend and database+--   identifier string.+newConnection :: MonadIO m => SeldaBackend -> Text -> m SeldaConnection+newConnection back dbid =+  liftIO $ SeldaConnection back dbid <$> newIORef M.empty+                                     <*> newIORef False+                                     <*> newMVar ()++-- | Get all statements and their corresponding identifiers for the current+--   connection.+allStmts :: SeldaConnection -> IO [(StmtID, Dynamic)]+allStmts =+  fmap (map (\(k, v) -> (k, stmtHandle v)) . M.toList) . readIORef . connStmts++-- | A collection of functions making up a Selda backend.+data SeldaBackend = SeldaBackend+  { -- | Execute an SQL statement.+    runStmt :: Text -> [Param] -> IO (Int, [[SqlValue]])++    -- | Execute an SQL statement and return the last inserted primary key,+    --   where the primary key is auto-incrementing.+    --   Backends must take special care to make this thread-safe.+  , runStmtWithPK :: Text -> [Param] -> IO Int++    -- | Prepare a statement using the given statement identifier.+  , prepareStmt :: StmtID -> [SqlTypeRep] -> Text -> IO Dynamic++    -- | Execute a prepared statement.+  , runPrepared :: Dynamic -> [Param] -> IO (Int, [[SqlValue]])++    -- | SQL pretty-printer configuration.+  , ppConfig :: PPConfig++    -- | Close the currently open connection.+  , closeConnection :: SeldaConnection -> IO ()++    -- | Unique identifier for this backend.+  , backendId :: BackendID+  }++data SeldaState = SeldaState+  { -- | Connection in use by the current computation.+    stConnection :: !SeldaConnection++    -- | Tables modified by the current transaction.+    --   Invariant: always @Just xs@ during a transaction, and always+    --   @Nothing@ when not in a transaction.+  , stTouchedTables :: !(Maybe [TableName])+  }++-- | Some monad with Selda SQL capabilitites.+class MonadIO m => MonadSelda m where+  -- | Get the connection in use by the computation.+  seldaConnection :: m SeldaConnection++  -- | Invalidate the given table as soon as the current transaction finishes.+  --   Invalidate the table immediately if no transaction is ongoing.+  invalidateTable :: Table a -> m ()++  -- | Indicates the start of a new transaction.+  --   Starts bookkeeping to invalidate all tables modified during+  --   the transaction at the next call to 'endTransaction'.+  beginTransaction :: m ()++  -- | Indicates the end of the current transaction.+  --   Invalidates all tables that were modified since the last call to+  --   'beginTransaction', unless the transaction was rolled back.+  endTransaction :: Bool -- ^ @True@ if the transaction was committed,+                         --   @False@ if it was rolled back.+                 -> m ()++-- | Get the backend in use by the computation.+seldaBackend :: MonadSelda m => m SeldaBackend+seldaBackend = connBackend <$> seldaConnection++-- | Monad transformer adding Selda SQL capabilities.+newtype SeldaT m a = S {unS :: StateT SeldaState m a}+  deriving ( Functor, Applicative, Monad, MonadIO+           , MonadThrow, MonadCatch, MonadMask, MonadTrans+           )++instance MonadIO m => MonadSelda (SeldaT m) where+  seldaConnection = S $ fmap stConnection get++  invalidateTable tbl = S $ do+    st <- get+    case stTouchedTables st of+      Nothing -> liftIO $ invalidate [tableName tbl]+      Just ts -> put $ st {stTouchedTables = Just (tableName tbl : ts)}++  beginTransaction = S $ do+    st <- get+    case stTouchedTables st of+      Nothing -> put $ st {stTouchedTables = Just []}+      Just _  -> liftIO $ throwM $ SqlError "attempted to nest transactions"++  endTransaction committed = S $ do+    st <- get+    case stTouchedTables st of+      Just ts | committed -> liftIO $ invalidate ts+      _                   -> return ()+    put $ st {stTouchedTables = Nothing}++-- | The simplest form of Selda computation; 'SeldaT' specialized to 'IO'.+type SeldaM = SeldaT IO++-- | Run a Selda transformer. Backends should use this to implement their+--   @withX@ functions.+runSeldaT :: (MonadIO m, MonadMask m) => SeldaT m a -> SeldaConnection -> m a+runSeldaT m c = do+    liftIO $ takeMVar (connLock c)+    go `finally` liftIO (putMVar (connLock c) ())+  where+    go = do+      closed <- liftIO $ readIORef (connClosed c)+      when closed $ do+        liftIO $ throwM $ DbError "runSeldaT called with a closed connection"+      fst <$> runStateT (unS m) (SeldaState c Nothing)
src/Database/Selda/Caching.hs view
@@ -47,6 +47,7 @@   hashWithSalt s (LDateTime x) = hashWithSalt s x   hashWithSalt s (LDate x)     = hashWithSalt s x   hashWithSalt s (LTime x)     = hashWithSalt s x+  hashWithSalt s (LBlob x)     = hashWithSalt s x   hashWithSalt s (LJust x)     = hashWithSalt s x   hashWithSalt _ (LNull)       = 0   hashWithSalt s (LCustom l)   = hashWithSalt s l
src/Database/Selda/Compile.hs view
@@ -12,6 +12,7 @@ import Database.Selda.Query.Type import Database.Selda.SQL import Database.Selda.SQL.Print+import Database.Selda.SQL.Print.Config import Database.Selda.SqlType import Database.Selda.Table import Database.Selda.Table.Compile@@ -26,36 +27,36 @@ --   The types given are tailored for SQLite. To translate SQLite types into --   whichever types are used by your backend, use 'compileWith'. compile :: Result a => Query s a -> (Text, [Param])-compile = snd . compileWithTables id+compile = snd . compileWithTables defPPConfig  -- | Compile a query using the given type translation function.-compileWith :: Result a => (Text -> Text) -> Query s a -> (Text, [Param])-compileWith ttr = snd . compileWithTables ttr+compileWith :: Result a => PPConfig -> Query s a -> (Text, [Param])+compileWith cfg = snd . compileWithTables cfg  -- | Compile a query into a parameterised SQL statement. Also returns all --   tables depended on by the query. compileWithTables :: Result a-                  => (Text -> Text)+                  => PPConfig                   -> Query s a                   -> ([TableName], (Text, [Param]))-compileWithTables ttr = compSql ttr . snd . compQuery+compileWithTables cfg = compSql cfg . snd . compQuery  -- | Compile an @INSERT@ query, given the keyword representing default values --   in the target SQL dialect, a table and a list of items corresponding --   to the table.-compileInsert :: Insert a => Text -> Table a -> [a] -> (Text, [Param])-compileInsert _ _ []         = (empty, [])-compileInsert defkw tbl rows = compInsert defkw tbl (map params rows)+compileInsert :: Insert a => PPConfig -> Table a -> [a] -> (Text, [Param])+compileInsert _ _ []       = (empty, [])+compileInsert cfg tbl rows = compInsert cfg tbl (map params rows)  -- | Compile an @UPDATE@ query. compileUpdate :: forall s a. (Columns (Cols s a), Result (Cols s a))-              => (Text -> Text)           -- ^ Type translation function.+              => PPConfig                 -- ^ SQL pretty-printer config.               -> Table a                  -- ^ The table to update.               -> (Cols s a -> Cols s a)   -- ^ Update function.               -> (Cols s a -> Col s Bool) -- ^ Predicate: update only when true.               -> (Text, [Param])-compileUpdate ttr tbl upd check =-    compUpdate ttr (tableName tbl) predicate updated+compileUpdate cfg tbl upd check =+    compUpdate cfg (tableName tbl) predicate updated   where     names = map colName (tableCols tbl)     cs = toTup names@@ -64,8 +65,8 @@  -- | Compile a @DELETE FROM@ query. compileDelete :: Columns (Cols s a)-              => Table a -> (Cols s a -> Col s Bool) -> (Text, [Param])-compileDelete tbl check = compDelete (tableName tbl) predicate+              => PPConfig -> Table a -> (Cols s a -> Col s Bool) -> (Text, [Param])+compileDelete cfg tbl check = compDelete cfg (tableName tbl) predicate   where C predicate = check $ toTup $ map colName $ tableCols tbl  -- | Compile a query to an SQL AST.
src/Database/Selda/Exp.hs view
@@ -20,7 +20,7 @@   BinOp   :: !(BinOp a b) -> !(Exp sql a) -> !(Exp sql a) -> Exp sql b   UnOp    :: !(UnOp a b) -> !(Exp sql a) -> Exp sql b   Fun2    :: !Text -> !(Exp sql a) -> !(Exp sql b) -> Exp sql c-  Cast    :: !Text -> !(Exp sql a) -> Exp sql b+  Cast    :: !SqlTypeRep -> !(Exp sql a) -> Exp sql b   AggrEx  :: !Text -> !(Exp sql a) -> Exp sql b   InList  :: !(Exp sql a) -> ![Exp sql a] -> Exp sql Bool   InQuery :: !(Exp sql a) -> !sql -> Exp sql Bool
src/Database/Selda/Frontend.hs view
@@ -10,7 +10,7 @@   , dropTable, tryDropTable   , transaction, setLocalCache   ) where-import Database.Selda.Backend+import Database.Selda.Backend.Internal import Database.Selda.Caching import Database.Selda.Column import Database.Selda.Compile@@ -23,6 +23,7 @@ import Data.Text (Text) import Control.Monad import Control.Monad.Catch+import Control.Monad.IO.Class  -- | Run a query within a Selda monad. In practice, this is often a 'SeldaT' --   transformer on top of some other monad.@@ -61,8 +62,8 @@ insert _ [] = do   return 0 insert t cs = do-  kw <- defaultKeyword <$> seldaBackend-  res <- uncurry exec $ compileInsert kw t cs+  cfg <- ppConfig <$> seldaBackend+  res <- uncurry exec $ compileInsert cfg t cs   invalidateTable t   return res @@ -139,7 +140,7 @@   if tableHasAutoPK t     then do       res <- liftIO $ do-        uncurry (runStmtWithPK b) $ compileInsert (defaultKeyword b) t cs+        uncurry (runStmtWithPK b) $ compileInsert (ppConfig b) t cs       invalidateTable t       return $ unsafeRowId res     else do@@ -154,8 +155,8 @@        -> (Cols s a -> Cols s a)   -- ^ Update function.        -> m Int update tbl check upd = do-  tt <- typeTrans <$> seldaBackend-  res <- uncurry exec $ compileUpdate tt tbl upd check+  cfg <- ppConfig <$> seldaBackend+  res <- uncurry exec $ compileUpdate cfg tbl upd check   invalidateTable tbl   return res @@ -172,7 +173,8 @@ deleteFrom :: (MonadSelda m, Columns (Cols s a))            => Table a -> (Cols s a -> Col s Bool) -> m Int deleteFrom tbl f = do-  res <- uncurry exec $ compileDelete tbl f+  cfg <- ppConfig <$> seldaBackend+  res <- uncurry exec $ compileDelete cfg tbl f   invalidateTable tbl   return res @@ -184,14 +186,14 @@ -- | Create a table from the given schema. createTable :: MonadSelda m => Table a -> m () createTable tbl = do-  cct <- customColType <$> seldaBackend-  void . flip exec [] $ compileCreateTable cct Fail tbl+  cfg <- ppConfig <$> seldaBackend+  void . flip exec [] $ compileCreateTable cfg Fail tbl  -- | Create a table from the given schema, unless it already exists. tryCreateTable :: MonadSelda m => Table a -> m () tryCreateTable tbl = do-  cct <- customColType <$> seldaBackend-  void . flip exec [] $ compileCreateTable cct Ignore tbl+  cfg <- ppConfig <$> seldaBackend+  void . flip exec [] $ compileCreateTable cfg Ignore tbl  -- | Drop the given table. dropTable :: MonadSelda m => Table a -> m ()@@ -237,10 +239,11 @@ queryWith :: forall s m a. (MonadSelda m, Result a)           => QueryRunner (Int, [[SqlValue]]) -> Query s a -> m [Res a] queryWith qr q = do-  backend <- seldaBackend-  let db = dbIdentifier backend+  conn <- seldaConnection+  let backend = connBackend conn+      db = connDbId conn       cacheKey = (db, qs, ps)-      (tables, qry@(qs, ps)) = compileWithTables (typeTrans backend) q+      (tables, qry@(qs, ps)) = compileWithTables (ppConfig backend) q   mres <- liftIO $ cached cacheKey   case mres of     Just res -> do@@ -250,10 +253,6 @@       let res' = mkResults (Proxy :: Proxy a) res       liftIO $ cache tables cacheKey res'       return res'---- | Translate types for casts. Column attributes are ignored here.-typeTrans :: SeldaBackend -> Text -> Text-typeTrans backend t = maybe t id (customColType backend t [])  -- | Generate the final result of a query from a list of untyped result rows. mkResults :: Result a => Proxy a -> [[SqlValue]] -> [Res a]
src/Database/Selda/Generic.hs view
@@ -37,7 +37,6 @@ import Database.Selda.Table import Database.Selda.Types import Database.Selda.Selectors-import Database.Selda.SqlType  -- | Any type which has a corresponding relation. --   To make a @Relational@ instance for some type, simply derive 'Generic'.
+ src/Database/Selda/Prepared.hs view
@@ -0,0 +1,191 @@+{-# LANGUAGE TypeFamilies, FlexibleInstances, ScopedTypeVariables #-}+{-# LANGUAGE MultiParamTypeClasses, FlexibleContexts #-}+-- | Building and executing prepared statements.+module Database.Selda.Prepared (Preparable, Prepare, prepared) where+import Database.Selda.Backend.Internal+import Database.Selda.Caching+import Database.Selda.Column+import Database.Selda.Compile+import Database.Selda.Query.Type+import Database.Selda.SQL (param, paramType)+import Database.Selda.Types (TableName)+import Control.Exception+import Control.Monad.IO.Class+import qualified Data.HashMap.Strict as M+import Data.IORef+import Data.Proxy+import Data.Text (Text)+import System.IO.Unsafe++data Placeholder = Placeholder Int+  deriving Show+instance Exception Placeholder++-- | Index of first argument parameter to a query.+firstParamIx :: Int+firstParamIx = 0++-- | Result type of a monadic computation.+type family ResultT f where+  ResultT (a -> b) = ResultT b+  ResultT (m a)    = a++type family Equiv q f where+  Equiv (Col s a -> q) (a -> f) = Equiv q f+  Equiv (Query s a)    (m [b])  = Res a ~ b++type CompResult = (Text, [Either Int Param], [SqlTypeRep], [TableName])++class Preparable q where+  -- | Prepare the query and parameter list.+  mkQuery :: MonadSelda m+          => Int -- ^ Next argument index.+          -> q   -- ^ The query.+          -> [SqlTypeRep] -- ^ The list of param types so far.+          -> m CompResult++-- | Some parameterized query @q@ that can be prepared into a function @f@+--   in some @MonadSelda@.+class Prepare q f where+  -- | Build the function that prepares and execute the query.+  mkFun :: Preparable q+        => IORef (Maybe (BackendID, CompResult))+        -> StmtID+        -> q+        -> [Param]+        -> f++instance (SqlType a, Prepare q b) => Prepare q (a -> b) where+  mkFun ref sid qry ps x = mkFun ref sid qry (param x : ps)++instance (MonadSelda m, a ~ Res (ResultT q), Result (ResultT q)) =>+         Prepare q (m [a]) where+  mkFun ref sid qry arguments = do+    conn <- seldaConnection+    let backend = connBackend conn+        args = reverse arguments+    stmts <- liftIO $ readIORef (connStmts conn)+    case M.lookup sid stmts of+      Just stm -> do+        -- Statement already prepared for this connection; just execute it.+        liftIO $ do+          runQuery conn stm args+      _ -> do+        -- Statement wasn't prepared for this connection; check if it was at+        -- least previously compiled for this backend.+        compiled <- liftIO $ readIORef ref+        (q, params, reps, ts) <- case compiled of+          Just (bid, comp) | bid == backendId backend -> do+            return comp+          _ -> do+            comp <- mkQuery firstParamIx qry []+            liftIO $ writeIORef ref (Just (backendId backend, comp))+            return comp++        -- Prepare and execute+        liftIO $ do+          hdl <- prepareStmt backend sid reps q+          let stm = SeldaStmt+                { stmtHandle = hdl+                , stmtParams = params+                , stmtTables = ts+                , stmtText = q+                }+          atomicModifyIORef' (connStmts conn) $ \m -> (M.insert sid stm m, ())+          runQuery conn stm args+    where+      runQuery conn stm args = do+        let backend = connBackend conn+            ps = replaceParams (stmtParams stm) args+            key = (connDbId conn, stmtText stm, ps)+            hdl = stmtHandle stm+        mres <- cached key+        case mres of+          Just res -> do+            return res+          _ -> do+            res <- runPrepared backend hdl ps+            cache (stmtTables stm) key res+            return $ map (toRes (Proxy :: Proxy (ResultT q))) (snd res)++instance (SqlType a, Preparable b) => Preparable (Col s a -> b) where+  mkQuery n f ts = mkQuery (n+1) (f x) (sqlType (Proxy :: Proxy a) : ts)+    where x = C $ Lit $ LCustom (throw (Placeholder n) :: Lit a)++instance Result a => Preparable (Query s a) where+  mkQuery _ q types = do+    b <- seldaBackend+    case compileWithTables (ppConfig b) q of+      (tables, (q', ps)) -> do+        (ps', types') <- liftIO $ inspectParams (reverse types) ps+        return (q', ps', types', tables)++-- | Create a prepared Selda function. A prepared function has zero or more+--   arguments, and will get compiled into a prepared statement by the first+--   backend to execute it. Any subsequent calls to the function for the duration+--   of the connection to the database will reuse the prepared statement.+--+--   Preparable functions are of the form+--   @(SqlType a, SqlType b, ...) => Col s a -> Col s b -> ... -> Query s r@.+--   The resulting prepared function will be of the form+--   @MonadSelda m => a -> b -> ... -> m [Res r]@.+--   Note, however, that when using @prepared@, you must give a concrete type+--   for @m@ due to how Haskell's type class resolution works.+--+--   Prepared functions rely on memoization for just-in-time preparation and+--   caching. This means that if GHC accidentally inlines your prepared function,+--   it may get prepared twice.+--   While this does not affect the correctness of your program, and is+--   fairly unlikely to happen, if you want to be absolutely sure that your+--   queries aren't re-prepared more than absolutely necessary,+--   consider adding a @NOINLINE@ annotation to each prepared function.+--+--   A usage example:+--+-- > ages :: Table (Text :*: Int)+-- > ages = table "ages" $ primary "name" :*: required "age"+-- >+-- > {-# NOINLINE ageOf #-}+-- > ageOf :: Text -> SeldaM [Int]+-- > ageOf = prepared $ \n -> do+-- >   (name :*: age) <- select ages+-- >   restrict $ name .== n+-- >   return age+{-# NOINLINE prepared #-}+prepared :: (Preparable q, Prepare q f, Equiv q f) => q -> f+prepared q = unsafePerformIO $ do+  ref <- newIORef Nothing+  sid <- freshStmtId+  return $ mkFun ref sid q []++-- | Replace every indexed parameter with the corresponding provided parameter.+--   Keep all non-indexed parameters in place.+replaceParams :: [Either Int Param] -> [Param] -> [Param]+replaceParams params = map fromRight . go firstParamIx params+  where+    go n ps (x:xs) = go (n+1) (map (subst n x) ps) xs+    go _ ps _      = ps++    subst n x (Left n') | n == n' = Right x+    subst _ _ old                 = old++    fromRight (Right x) = x+    fromRight _         = error "BUG: query parameter not substituted!"++-- | Inspect a list of parameters, denoting each parameter with either a+--   placeholder index or a literal parameter.+inspectParams :: [SqlTypeRep] -> [Param] -> IO ([Either Int Param], [SqlTypeRep])+inspectParams ts (x:xs) = do+  res <- try $ pure $! forceParam x+  let (x', t) = case res of+        Right p               -> (Right p, paramType p)+        Left (Placeholder ix) -> (Left ix, ts !! ix)+  (xs', ts') <- inspectParams ts xs+  return (x' : xs', t : ts')+inspectParams _ [] = do+  return ([], [])++-- | Force a parameter deep enough to determine whether it is a placeholder.+forceParam :: Param -> Param+forceParam p@(Param (LCustom x)) | x `seq` True = p+forceParam p                                    = p
src/Database/Selda/Query.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE FlexibleContexts, OverloadedStrings #-} -- | Query monad and primitive operations. module Database.Selda.Query-  (select, selectValues+  ( select, selectValues   , restrict, groupBy, limit, order   , aggregate, leftJoin, innerJoin   ) where@@ -12,6 +12,7 @@ import Database.Selda.Table import Database.Selda.Transform import Control.Monad.State.Strict+import Unsafe.Coerce  -- | Query the given table. Result is returned as an inductive tuple, i.e. --   @first :*: second :*: third <- query tableOfThree@.@@ -178,7 +179,7 @@  -- | Drop the first @m@ rows, then get at most @n@ of the remaining rows from the --   given subquery.-limit :: Int -> Int -> Query (Inner s) a -> Query s a+limit :: Int -> Int -> Query (Inner s) a -> Query s (OuterCols a) limit from to q = Query $ do   (lim_st, res) <- isolate q   st <- get@@ -188,7 +189,8 @@         ss ->           SQL (allCols ss) (Product ss) [] [] [] (Just (from, to))   put $ st {sources = sql : sources st}-  return res+  -- TODO: replace with safe coercion+  return $ unsafeCoerce res  -- | Sort the result rows in ascending or descending order on the given row. order :: Col s a -> Order -> Query s ()
src/Database/Selda/SQL.hs view
@@ -63,6 +63,14 @@ instance Ord Param where   compare (Param a) (Param b) = compLit a b +-- | Create a parameter from the given value.+param :: SqlType a => a -> Param+param = Param . mkLit++-- | The SQL type of the given parameter.+paramType :: Param -> SqlTypeRep+paramType (Param p) = litType p+ -- | Exception indicating the use of a default value. --   If any values throwing this during evaluation of @param xs@ will be --   replaced by their default value.
src/Database/Selda/SQL/Print.hs view
@@ -3,6 +3,8 @@ module Database.Selda.SQL.Print where import Database.Selda.Column import Database.Selda.SQL+import Database.Selda.SQL.Print.Config (PPConfig)+import qualified Database.Selda.SQL.Print.Config as Cfg import Database.Selda.SqlType import Database.Selda.Types import Control.Monad.State@@ -24,34 +26,34 @@   , ppTables  :: ![TableName]   , ppParamNS :: !Int   , ppQueryNS :: !Int-  , ppTypeTr  :: !(Text -> Text)+  , ppConfig  :: !PPConfig   }  -- | Run a pretty-printer.-runPP :: (Text -> Text)+runPP :: PPConfig       -> PP Text       -> ([TableName], (Text, [Param]))-runPP typetr pp =-  case runState pp (PPState [] [] 1 0 typetr) of+runPP cfg pp =+  case runState pp (PPState [] [] 1 0 cfg) of     (q, st) -> (snub $ ppTables st, (q, reverse (ppParams st)))  -- | Compile an SQL AST into a parameterized SQL query.-compSql :: (Text -> Text)+compSql :: PPConfig         -> SQL         -> ([TableName], (Text, [Param]))-compSql typetr = runPP typetr . ppSql+compSql cfg = runPP cfg . ppSql  -- | Compile a single column expression.-compExp :: (Text -> Text) -> Exp SQL a -> (Text, [Param])-compExp typetr = snd . runPP typetr . ppCol+compExp :: PPConfig -> Exp SQL a -> (Text, [Param])+compExp cfg = snd . runPP cfg . ppCol  -- | Compile an @UPATE@ statement.-compUpdate :: (Text -> Text)+compUpdate :: PPConfig            -> TableName            -> Exp SQL Bool            -> [(ColName, SomeCol SQL)]            -> (Text, [Param])-compUpdate typetr tbl p cs = snd $ runPP typetr ppUpd+compUpdate cfg tbl p cs = snd $ runPP cfg ppUpd   where     ppUpd = do       updates <- mapM ppUpdate cs@@ -76,19 +78,13 @@         us' -> Text.intercalate ", " us'  -- | Compile a @DELETE@ statement.-compDelete :: TableName -> Exp SQL Bool -> (Text, [Param])-compDelete tbl p = snd $ runPP id ppDelete+compDelete :: PPConfig -> TableName -> Exp SQL Bool -> (Text, [Param])+compDelete cfg tbl p = snd $ runPP cfg ppDelete   where     ppDelete = do       c' <- ppCol p       pure $ Text.unwords ["DELETE FROM", fromTableName tbl, "WHERE", c'] --- | Pretty-print a type name.-ppType :: Text -> PP Text-ppType t = do-  typeTr <- ppTypeTr <$> get-  pure $ typeTr t- -- | Pretty-print a literal as a named parameter and save the --   name-value binding in the environment. ppLit :: Lit a -> PP Text@@ -208,6 +204,11 @@ ppCols cs = do   cs' <- mapM ppCol (reverse cs)   pure $ "(" <> Text.intercalate ") AND (" cs' <> ")"++ppType :: SqlTypeRep -> PP Text+ppType t = do+  c <- ppConfig <$> get+  pure $ Cfg.ppType c t  ppCol :: Exp SQL a -> PP Text ppCol (TblCol xs)    = error $ "compiler bug: ppCol saw TblCol: " ++ show xs
+ src/Database/Selda/SQL/Print/Config.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE OverloadedStrings #-}+module Database.Selda.SQL.Print.Config (PPConfig (..), defPPConfig) where+import Data.Text (Text)+import qualified Data.Text as T+import Database.Selda.SqlType+import Database.Selda.Table++-- | Backend-specific configuration for the SQL pretty-printer.+data PPConfig = PPConfig+  { -- | The SQL type name of the given type.+    ppType :: SqlTypeRep -> Text++    -- | Parameter placeholder for the @n@th parameter.+  , ppPlaceholder :: Int -> Text++    -- | List of column attributes, such as +  , ppColAttrs :: [ColAttr] -> Text++    -- | The value used for the next value for an auto-incrementing column.+    --   For instance, @DEFAULT@ for PostgreSQL, and @NULL@ for SQLite.+  , ppAutoIncInsert :: Text+  }++-- | Default settings for pretty-printing.+--   Geared towards SQLite.+defPPConfig :: PPConfig+defPPConfig = PPConfig+    { ppType = defType+    , ppPlaceholder = T.cons '$' . T.pack . show+    , ppColAttrs = T.unwords . map defColAttr+    , ppAutoIncInsert = "NULL"+    }++-- | Default compilation for SQL types.+defType :: SqlTypeRep -> Text+defType TText     = "TEXT"+defType TRowID    = "INTEGER"+defType TInt      = "INT"+defType TFloat    = "DOUBLE"+defType TBool     = "INT"+defType TDateTime = "DATETIME"+defType TDate     = "DATE"+defType TTime     = "TIME"+defType TBlob     = "BLOB"++-- | Default compilation for a column attribute.+defColAttr :: ColAttr -> Text+defColAttr Primary       = "PRIMARY KEY"+defColAttr AutoIncrement = "AUTOINCREMENT"+defColAttr Required      = "NOT NULL"+defColAttr Optional      = "NULL"+defColAttr Unique        = "UNIQUE"
src/Database/Selda/SqlType.hs view
@@ -1,12 +1,14 @@ {-# LANGUAGE GADTs, OverloadedStrings, ScopedTypeVariables, FlexibleInstances #-} -- | Types representable in Selda's subset of SQL. module Database.Selda.SqlType-  ( Lit (..), RowID, SqlValue (..), SqlType-  , invalidRowId, isInvalidRowId, unsafeRowId-  , mkLit, sqlType, fromSql, defaultValue+  ( Lit (..), RowID, SqlValue (..), SqlType, SqlTypeRep (..)+  , invalidRowId, isInvalidRowId, unsafeRowId, fromRowId+  , mkLit, sqlType, litType, fromSql, defaultValue   , compLit   , sqlDateTimeFormat, sqlDateFormat, sqlTimeFormat   ) where+import Data.ByteString (ByteString, empty)+import qualified Data.ByteString.Lazy as BSL import Data.Proxy import Data.Text (Text, pack, unpack) import Data.Time@@ -27,26 +29,65 @@ sqlTimeFormat :: String sqlTimeFormat = "%H:%M:%S%Q" +-- | Representation of an SQL type.+data SqlTypeRep+  = TText+  | TRowID+  | TInt+  | TFloat+  | TBool+  | TDateTime+  | TDate+  | TTime+  | TBlob+    deriving (Show, Eq, Ord)+ -- | Any datatype representable in (Selda's subset of) SQL. class Typeable a => SqlType a where-  mkLit        :: a -> Lit a-  sqlType      :: Proxy a -> Text-  fromSql      :: SqlValue -> a+  -- | Create a literal of this type.+  mkLit :: a -> Lit a++  -- | The SQL representation for this type.+  sqlType :: Proxy a -> SqlTypeRep+  sqlType _ = litType (defaultValue :: Lit a)++  -- | Convert an SqlValue into this type.+  fromSql :: SqlValue -> a++  -- | Default value when using 'def' at this type.   defaultValue :: Lit a  -- | An SQL literal. data Lit a where-  LText     :: !Text    -> Lit Text-  LInt      :: !Int     -> Lit Int-  LDouble   :: !Double  -> Lit Double-  LBool     :: !Bool    -> Lit Bool-  LDateTime :: !Text    -> Lit UTCTime-  LDate     :: !Text    -> Lit Day-  LTime     :: !Text    -> Lit TimeOfDay-  LJust     :: !(Lit a) -> Lit (Maybe a)-  LNull     :: Lit (Maybe a)-  LCustom   :: !(Lit a) -> Lit b+  LText     :: !Text       -> Lit Text+  LInt      :: !Int        -> Lit Int+  LDouble   :: !Double     -> Lit Double+  LBool     :: !Bool       -> Lit Bool+  LDateTime :: !Text       -> Lit UTCTime+  LDate     :: !Text       -> Lit Day+  LTime     :: !Text       -> Lit TimeOfDay+  LJust     :: SqlType a => !(Lit a) -> Lit (Maybe a)+  LBlob     :: !ByteString -> Lit ByteString+  LNull     :: SqlType a => Lit (Maybe a)+  LCustom   :: Lit a -> Lit b +-- | The SQL type representation for the given literal.+litType :: Lit a -> SqlTypeRep+litType (LText{})     = TText+litType (LInt{})      = TInt+litType (LDouble{})   = TFloat+litType (LBool{})     = TBool+litType (LDateTime{}) = TDateTime+litType (LDate{})     = TDate+litType (LTime{})     = TTime+litType (LJust x)     = litType x+litType (LBlob{})     = TBlob+litType (x@LNull)     = sqlType (proxyFor x)+  where+    proxyFor :: Lit (Maybe a) -> Proxy a+    proxyFor _ = Proxy+litType (LCustom x)   = litType x+ instance Eq (Lit a) where   a == b = compLit a b == EQ @@ -63,8 +104,9 @@ litConTag (LDate{})     = 5 litConTag (LTime{})     = 6 litConTag (LJust{})     = 7-litConTag (LNull)       = 8-litConTag (LCustom{})   = 9+litConTag (LBlob{})     = 8+litConTag (LNull)       = 9+litConTag (LCustom{})   = 10  -- | Compare two literals of different type for equality. compLit :: Lit a -> Lit b -> Ordering@@ -75,16 +117,18 @@ compLit (LDateTime x) (LDateTime x') = x `compare` x' compLit (LDate x)     (LDate x')     = x `compare` x' compLit (LTime x)     (LTime x')     = x `compare` x'+compLit (LBlob x)     (LBlob x')     = x `compare` x' compLit (LJust x)     (LJust x')     = x `compLit` x' compLit (LCustom x)   (LCustom x')   = x `compLit` x' compLit a             b              = litConTag a `compare` litConTag b  -- | Some value that is representable in SQL. data SqlValue where-  SqlInt    :: !Int    -> SqlValue-  SqlFloat  :: !Double -> SqlValue-  SqlString :: !Text   -> SqlValue-  SqlBool   :: !Bool   -> SqlValue+  SqlInt    :: !Int        -> SqlValue+  SqlFloat  :: !Double     -> SqlValue+  SqlString :: !Text       -> SqlValue+  SqlBool   :: !Bool       -> SqlValue+  SqlBlob   :: !ByteString -> SqlValue   SqlNull   :: SqlValue  instance Show SqlValue where@@ -92,6 +136,7 @@   show (SqlFloat f)  = "SqlFloat " ++ show f   show (SqlString s) = "SqlString " ++ show s   show (SqlBool b)   = "SqlBool " ++ show b+  show (SqlBlob b)   = "SqlBlob " ++ show b   show (SqlNull)     = "SqlNull"  instance Show (Lit a) where@@ -102,6 +147,7 @@   show (LDateTime s) = show s   show (LDate s)     = show s   show (LTime s)     = show s+  show (LBlob b)     = show b   show (LJust x)     = "Just " ++ show x   show (LNull)       = "Nothing"   show (LCustom l)   = show l@@ -109,7 +155,7 @@ -- | A row identifier for some table. --   This is the type of auto-incrementing primary keys. newtype RowID = RowID Int-  deriving (Eq, Typeable)+  deriving (Eq, Ord, Typeable) instance Show RowID where   show (RowID n) = show n @@ -129,37 +175,41 @@ unsafeRowId :: Int -> RowID unsafeRowId = RowID +-- | Inspect a row identifier.+fromRowId :: RowID -> Int+fromRowId (RowID n) = n+ instance SqlType RowID where   mkLit (RowID n) = LCustom $ LInt n-  sqlType _ = sqlType (Proxy :: Proxy Int)+  sqlType _ = TRowID   fromSql (SqlInt x) = RowID x   fromSql v          = error $ "fromSql: RowID column with non-int value: " ++ show v   defaultValue = mkLit invalidRowId  instance SqlType Int where   mkLit = LInt-  sqlType _ = "INTEGER"+  sqlType _ = TInt   fromSql (SqlInt x) = x   fromSql v          = error $ "fromSql: int column with non-int value: " ++ show v   defaultValue = LInt 0  instance SqlType Double where   mkLit = LDouble-  sqlType _ = "DOUBLE"+  sqlType _ = TFloat   fromSql (SqlFloat x) = x   fromSql v            = error $ "fromSql: float column with non-float value: " ++ show v   defaultValue = LDouble 0  instance SqlType Text where   mkLit = LText-  sqlType _ = "TEXT"+  sqlType _ = TText   fromSql (SqlString x) = x   fromSql v             = error $ "fromSql: text column with non-text value: " ++ show v   defaultValue = LText ""  instance SqlType Bool where   mkLit = LBool-  sqlType _ = "INT"+  sqlType _ = TBool   fromSql (SqlBool x) = x   fromSql (SqlInt 0)  = False   fromSql (SqlInt _)  = True@@ -168,7 +218,7 @@  instance SqlType UTCTime where   mkLit = LDateTime . pack . formatTime defaultTimeLocale sqlDateTimeFormat-  sqlType _             = "DATETIME"+  sqlType _             = TDateTime   fromSql (SqlString s) =     case parseTimeM True defaultTimeLocale sqlDateTimeFormat (unpack s) of       Just t -> t@@ -178,7 +228,7 @@  instance SqlType Day where   mkLit = LDate . pack . formatTime defaultTimeLocale sqlDateFormat-  sqlType _             = "DATE"+  sqlType _             = TDate   fromSql (SqlString s) =     case parseTimeM True defaultTimeLocale sqlDateFormat (unpack s) of       Just t -> t@@ -188,13 +238,27 @@  instance SqlType TimeOfDay where   mkLit = LTime . pack . formatTime defaultTimeLocale sqlTimeFormat-  sqlType _             = "TIME"+  sqlType _             = TTime   fromSql (SqlString s) =     case parseTimeM True defaultTimeLocale sqlTimeFormat (unpack s) of       Just t -> t       _      -> error $ "fromSql: bad time string: " ++ unpack s   fromSql v             = error $ "fromSql: time column with non-time value: " ++ show v   defaultValue = LTime "00:00:00"++instance SqlType ByteString where+  mkLit = LBlob+  sqlType _ = TBlob+  fromSql (SqlBlob x) = x+  fromSql v           = error $ "fromSql: blob column with non-blob value: " ++ show v+  defaultValue = LBlob empty++instance SqlType BSL.ByteString where+  mkLit = LCustom . LBlob . BSL.toStrict+  sqlType _ = TBlob+  fromSql (SqlBlob x) = BSL.fromStrict x+  fromSql v           = error $ "fromSql: blob column with non-blob value: " ++ show v+  defaultValue = LCustom $ LBlob empty  instance SqlType a => SqlType (Maybe a) where   mkLit (Just x) = LJust $ mkLit x
src/Database/Selda/Table.hs view
@@ -9,7 +9,7 @@ import Data.Dynamic import Data.List (sort, group) import Data.Monoid-import Data.Text (Text, unpack, intercalate, any)+import Data.Text (unpack, intercalate, any)  -- | An error occurred when validating a database table. --   If this error is thrown, there is a bug in your database schema, and the@@ -44,7 +44,7 @@  data ColInfo = ColInfo   { colName  :: !ColName-  , colType  :: !Text+  , colType  :: !SqlTypeRep   , colAttrs :: ![ColAttr]   , colFKs   :: ![(Table (), ColName)]   }@@ -100,7 +100,7 @@ --   Also adds the @PRIMARY KEY@ and @UNIQUE@ attributes on the column. autoPrimary :: ColName -> ColSpec RowID autoPrimary n = ColSpec [c {colAttrs = [Primary, AutoIncrement, Required, Unique]}]-  where ColSpec [c] = newCol n :: ColSpec Int+  where ColSpec [c] = newCol n :: ColSpec RowID  -- | Add a uniqueness constraint to the given column. --   Adding a uniqueness constraint to a column that is already implied to be
src/Database/Selda/Table/Compile.hs view
@@ -6,14 +6,15 @@ import Data.Monoid import Data.Text (Text, intercalate, pack) import qualified Data.Text as Text-import Database.Selda.SQL hiding (params)+import Database.Selda.SQL hiding (params, param)+import Database.Selda.SQL.Print.Config import Database.Selda.Types  data OnError = Fail | Ignore   deriving (Eq, Ord, Show)  -- | Compile a @CREATE TABLE@ query from a table definition.-compileCreateTable :: (Text -> [ColAttr] -> Maybe Text) -> OnError -> Table a -> Text+compileCreateTable :: PPConfig -> OnError -> Table a -> Text compileCreateTable customColType ifex tbl = mconcat   [ "CREATE TABLE ", ifNotExists ifex, fromTableName (tableName tbl), "("   , intercalate ", " (map (compileTableCol customColType) (tableCols tbl))@@ -38,16 +39,11 @@     fkName = fromColName $ addColPrefix col ("fk" <> pack (show n) <> "_")  -- | Compile a table column.-compileTableCol :: (Text -> [ColAttr] -> Maybe Text) -> ColInfo -> Text-compileTableCol customColType ci = Text.unwords+compileTableCol :: PPConfig -> ColInfo -> Text+compileTableCol cfg ci = Text.unwords   [ fromColName (colName ci)-  , case customColType typ attrs of-      Just s -> s-      _      -> typ <> " " <> Text.unwords (map compileColAttr attrs)+  , ppType cfg (colType ci) <> " " <> ppColAttrs cfg (colAttrs ci)   ]-  where-    typ = colType ci-    attrs = colAttrs ci  -- | Compile a @DROP TABLE@ query. compileDropTable :: OnError -> Table a -> Text@@ -60,8 +56,8 @@ --   Note that backends expect insertions to NOT have a semicolon at the end. --   In addition to the compiled query, this function also returns the list of --   parameters to be passed to the backend.-compInsert :: Text -> Table a -> [[Either Param Param]] -> (Text, [Param])-compInsert defaultKeyword tbl defs =+compInsert :: PPConfig -> Table a -> [[Either Param Param]] -> (Text, [Param])+compInsert cfg tbl defs =     (query, parameters)   where     colNames = map colName $ tableCols tbl@@ -94,7 +90,7 @@     mkCol :: Int -> Either Param Param -> ColInfo -> [Param] -> (Int, Text, [Param])     mkCol n (Left def) col ps       | AutoIncrement `elem` colAttrs col =-        (n, defaultKeyword, ps)+        (n, ppAutoIncInsert cfg, ps)       | otherwise =         (n+1, pack ('$':show n), def:ps)     mkCol n (Right val) _ ps =@@ -105,11 +101,3 @@     mkCols (n, names, params) (param, col) =       case mkCol n param col params of         (n', name, params') -> (n', name:names, params')---- | Compile a column attribute.-compileColAttr :: ColAttr -> Text-compileColAttr Primary       = "PRIMARY KEY"-compileColAttr AutoIncrement = "AUTOINCREMENT"-compileColAttr Required      = "NOT NULL"-compileColAttr Optional      = "NULL"-compileColAttr Unique        = "UNIQUE"