packages feed

selda 0.1.9.0 → 0.1.10.0

raw patch · 14 files changed

+150/−76 lines, 14 filesdep ~basedep ~hashablePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: base, hashable

API changes (from Hackage documentation)

- Database.Selda.Backend: beginTransaction :: MonadSelda m => m ()
- Database.Selda.Backend: endTransaction :: MonadSelda m => Bool -> m ()
- Database.Selda.Generic: instance (Database.Selda.Generic.Traits a, Database.Selda.SqlType.SqlType a) => Database.Selda.Generic.GRelation (GHC.Generics.K1 i a)
- Database.Selda.Generic: instance Database.Selda.Generic.Traits (GHC.Base.Maybe a)
- Database.Selda.Generic: instance Database.Selda.Generic.Traits a
+ Database.Selda.Backend: [ppMaxInsertParams] :: PPConfig -> Maybe Int
+ Database.Selda.Backend: wrapTransaction :: MonadSelda m => m () -> m () -> m a -> m a
+ Database.Selda.Generic: instance (Data.Typeable.Internal.Typeable a, Database.Selda.SqlType.SqlType a) => Database.Selda.Generic.GRelation (GHC.Generics.K1 i a)
- Database.Selda: compileInsert :: Insert a => PPConfig -> Table a -> [a] -> (Text, [Param])
+ Database.Selda: compileInsert :: Insert a => PPConfig -> Table a -> [a] -> [(Text, [Param])]
- Database.Selda: transaction :: (MonadSelda m, MonadThrow m, MonadCatch m) => m a -> m a
+ Database.Selda: transaction :: (MonadSelda m, MonadCatch m) => m a -> m a
- Database.Selda.Backend: PPConfig :: (SqlTypeRep -> Text) -> (Int -> Text) -> ([ColAttr] -> Text) -> Text -> PPConfig
+ Database.Selda.Backend: PPConfig :: (SqlTypeRep -> Text) -> (Int -> Text) -> ([ColAttr] -> Text) -> Text -> Maybe Int -> PPConfig

Files

ChangeLog.md view
@@ -1,6 +1,14 @@ # Revision history for Selda  +## 0.1.10.0 -- 2017-08-01++* Async exception safety.+* Allow MonadSelda instances not built on SeldaT.+* Chunk very large insertions on backends that request it (i.e. SQLite).+* GHC 8.2 support.++ ## 0.1.9.0 -- 2017-06-16  * Properly document semantics of order.
README.md view
@@ -395,7 +395,7 @@ allPeople = do   (people_name :*: _ :*: _) <- select people   (addresses_name :*: city) <- select addresses-  restrict (people_name == addresses_name)+  restrict (people_name .== addresses_name)   return (people_name :*: city) ``` @@ -418,7 +418,7 @@ `addresses` table), you would have to use a join:  ```-allPeople' :: Query s (Col s Text :*: Col s Maybe Text)+allPeople' :: Query s (Col s Text :*: Col s (Maybe Text)) allPeople' = do   (name :*: _ :*: _) <- select people   (_ :*: city) <- leftJoin (\(name' :*: _) -> name .== name')@@ -478,7 +478,7 @@   (owner :*: homes) <- aggregate $ do     (owner :*: city) <- select addresses     owner' <- groupBy owner-    return (count city :*: owner')+    return (owner' :*: count city)   restrict (owner .== name)   order homes descending   return (owner :*: homes)@@ -674,6 +674,43 @@ 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.+++Foreign keys+------------++To add a foreign key constraint on a column, use the `fk` function.+This function takes two parameters: a column of the table being defined, and+a tuple of the `(table, column)` the foreign key refers to.+The table identifier is simply a value of type `Table t`, while the column+is specified using a selector of type `Selector t a`.++The following example creates a table to store users, and one to store blog+posts. The `users` table stores a name, a password, and a unique identifier+for each user.+The `posts` table stores, for each post, the post body, a unique+post identifier, and the identifier of the user who wrote the post.+The column storing a post's author has a foreign key constraint on the `userid`+column of the `users` table, to ensure that each post has a valid author.++```+users :: Table (RowID :*: Text)+users = table "users"+  $   primary "userid"+  :*: required "username"+  :*: required "password"+(userId :*: userName :*: userPass) = selectors users++posts :: Table (RowID :*: RowID :*: Text)+posts = table "posts"+  $   primary "postid"+  :*: required "authorid" `fk` (users, userId)+  :*: required "post_body"+```++Note that a foreign key can *only* refer to a column which is either+a primary key or has a unique constraint. This is not specific to Selda, but+a restriction of SQL.  And with that, we conclude this tutorial. Hopefully it has been enough to get you comfortably started using Selda.
selda.cabal view
@@ -1,5 +1,5 @@ name:                selda-version:             0.1.9.0+version:             0.1.10.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,@@ -20,7 +20,7 @@ build-type:          Simple extra-source-files:  ChangeLog.md cabal-version:       >=1.10-tested-with:         GHC == 7.10.3, GHC == 8.0.2+tested-with:         GHC == 7.10.3, GHC == 8.0.2, GHC == 8.2.1  extra-source-files:   README.md@@ -93,6 +93,9 @@   if impl(ghc < 7.11)     build-depends:       transformers  >=0.4 && <0.6+  if impl(ghc >= 8.2)+    build-depends:+      hashable >= 1.2.6.1   if !flag(haste) && flag(localcache)     build-depends:       psqueues >=0.2 && <0.3
src/Database/Selda.hs view
@@ -250,7 +250,7 @@ --   Useful for creating expressions over optional columns: -- -- > people :: Table (Text :*: Int :*: Maybe Text)--- > people = table "people" $ required "name" ¤ required "age" ¤ optional "pet"+-- > people = table "people" $ required "name" :*: required "age" :*: optional "pet" -- > -- > peopleWithCats = do -- >   name :*: _ :*: pet <- select people
src/Database/Selda/Backend.hs view
@@ -12,6 +12,7 @@   ) where import Database.Selda.Backend.Internal import Control.Monad+import Control.Monad.Catch import Control.Monad.IO.Class import Data.IORef @@ -20,6 +21,6 @@ --   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+seldaClose c = liftIO $ mask_ $ do   closed <- atomicModifyIORef' (connClosed c) $ \closed -> (True, closed)   unless closed $ closeConnection (connBackend c) c
src/Database/Selda/Backend/Internal.hs view
@@ -10,6 +10,7 @@   , PPConfig (..), defPPConfig   , sqlDateTimeFormat, sqlDateFormat, sqlTimeFormat   , freshStmtId+  , invalidate   , newConnection, allStmts   , runSeldaT, seldaBackend   ) where@@ -20,6 +21,7 @@ import Database.Selda.SQL.Print.Config import Database.Selda.Types (TableName) import Control.Concurrent+import Control.Exception (throw) import Control.Monad.Catch import Control.Monad.IO.Class import Control.Monad.State@@ -162,17 +164,26 @@   --   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 ()+  -- | Safely wrap a transaction. To ensure consistency of the in-process cache,+  --   it is important that any cached tables modified during a transaction are+  --   invalidated ONLY if that transaction succeeds, AFTER the changes become+  --   visible in the database.+  --+  --   In order to be thread-safe in the presence of asynchronous exceptions,+  --   instances should:+  --+  --   1. Mask async exceptions.+  --   2. Start bookkeeping of tables invalidated during the transaction.+  --   3. Perform the transaction, with async exceptions restored.+  --   4. Commit transaction, invalidate tables, and disable bookkeeping; OR+  --   5. If an exception was raised, rollback transaction and+  --      disable bookkeeping.+  --+  --   See the instance for 'SeldaT' for an example of how to do this safely.+  wrapTransaction :: m () -- ^ Signal transaction commit to SQL backend.+                  -> m () -- ^ Signal transaction rollback to SQL backend.+                  -> m a  -- ^ Transaction to perform.+                  -> m a  -- | Get the backend in use by the computation. seldaBackend :: MonadSelda m => m SeldaBackend@@ -184,7 +195,7 @@            , MonadThrow, MonadCatch, MonadMask, MonadTrans            ) -instance MonadIO m => MonadSelda (SeldaT m) where+instance (MonadIO m, MonadMask m) => MonadSelda (SeldaT m) where   seldaConnection = S $ fmap stConnection get    invalidateTable tbl = S $ do@@ -193,18 +204,17 @@       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}+  wrapTransaction commit rollback m = mask $ \restore -> do+    S $ modify' $ \st ->+      case stTouchedTables st of+        Nothing -> st {stTouchedTables = Just []}+        Just _  -> throw $ SqlError "attempted to nest transactions"+    x <- restore m `onException` rollback+    commit+    st <- S get+    maybe (return ()) (liftIO . invalidate) (stTouchedTables st)+    S $ put $ st {stTouchedTables = Nothing}+    return x  -- | The simplest form of Selda computation; 'SeldaT' specialized to 'IO'. type SeldaM = SeldaT IO@@ -212,9 +222,10 @@ -- | 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) ())+runSeldaT m c =+    bracket (liftIO $ takeMVar (connLock c))+            (const $ liftIO $ putMVar (connLock c) ())+            (const go)   where     go = do       closed <- liftIO $ readIORef (connClosed c)
src/Database/Selda/Caching.hs view
@@ -121,7 +121,8 @@     updatePrio (Just (_, v)) = (Just v, Just (nextPrio rc, v))     updatePrio _             = (Nothing, Nothing) --- | Invalidate all items in cache that depend on the given table.+-- | Invalidate all items in the per-process cache that depend on+--   the given table. invalidate :: [TableName] -> IO () invalidate tns = atomicModifyIORef' theCache $ \c -> (foldl' (flip invalidate') c tns, ()) 
src/Database/Selda/Compile.hs view
@@ -44,9 +44,21 @@ -- | 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 => PPConfig -> Table a -> [a] -> (Text, [Param])-compileInsert _ _ []       = (empty, [])-compileInsert cfg tbl rows = compInsert cfg tbl (map params rows)+compileInsert :: Insert a => PPConfig -> Table a -> [a] -> [(Text, [Param])]+compileInsert _ _ [] =+  [(empty, [])]+compileInsert cfg tbl rows =+    case ppMaxInsertParams cfg of+      Nothing -> [compInsert cfg tbl rows']+      Just n  -> map (compInsert cfg tbl) (chunk (n `div` rowlen) rows')+  where+    rows' = map params rows+    rowlen = length (head rows')+    chunk chunksize xs =+      case splitAt chunksize xs of+        ([], []) -> []+        (x, [])  -> [x]+        (x, xs') -> x : chunk chunksize xs'  -- | Compile an @UPDATE@ query. compileUpdate :: forall s a. (Columns (Cols s a), Result (Cols s a))
src/Database/Selda/Frontend.hs view
@@ -44,10 +44,10 @@ -- -- > people :: Table (Int :*: Text :*: Int :*: Maybe Text) -- > people = table "ppl"--- >        $ autoPrimary "id"--- >        ¤ required "name"--- >        ¤ required "age"--- >        ¤ optional "pet"+-- >        $   autoPrimary "id"+-- >        :*: required "name"+-- >        :*: required "age"+-- >        :*: optional "pet" -- > -- > main = withSQLite "my_database.sqlite" $ do -- >   insert_ people@@ -63,9 +63,9 @@   return 0 insert t cs = do   cfg <- ppConfig <$> seldaBackend-  res <- uncurry exec $ compileInsert cfg t cs+  res <- mapM (uncurry exec) $ compileInsert cfg t cs   invalidateTable t-  return res+  return (sum res)  -- | Attempt to insert a list of rows into a table, but don't raise an error --   if the insertion fails. Returns @True@ if the insertion succeeded, otherwise@@ -159,9 +159,9 @@   if tableHasAutoPK t     then do       res <- liftIO $ do-        uncurry (runStmtWithPK b) $ compileInsert (ppConfig b) t cs+        mapM (uncurry (runStmtWithPK b)) $ compileInsert (ppConfig b) t cs       invalidateTable t-      return $ unsafeRowId res+      return $ unsafeRowId (last res)     else do       insert_ t cs       return invalidRowId@@ -225,20 +225,10 @@ -- | Perform the given computation atomically. --   If an exception is raised during its execution, the enture transaction --   will be rolled back, and the exception re-thrown.-transaction :: (MonadSelda m, MonadThrow m, MonadCatch m) => m a -> m a-transaction m = do-  beginTransaction-  void $ exec "BEGIN TRANSACTION" []-  res <- try m-  case res of-    Left (SomeException e) -> do-      void $ exec "ROLLBACK" []-      endTransaction False-      throwM e-    Right x -> do-      void $ exec "COMMIT" []-      endTransaction True-      return x+transaction :: (MonadSelda m, MonadCatch m) => m a -> m a+transaction m =+  wrapTransaction (void $ exec "COMMIT" []) (void $ exec "ROLLBACK" []) $ do+    exec "BEGIN TRANSACTION" [] *> m  -- | Set the maximum local cache size to @n@. A cache size of zero disables --   local cache altogether. Changing the cache size will also flush all
src/Database/Selda/Generic.hs view
@@ -29,7 +29,9 @@   ) where import Control.Monad.State import Data.Dynamic+import Data.Proxy import Data.Text (pack)+import Data.Typeable import GHC.Generics hiding (R, (:*:), Selector) import qualified GHC.Generics as G ((:*:)(..), Selector) import Unsafe.Coerce@@ -155,7 +157,7 @@ -- > demoPerson = fromRel ("Miyu" :*: 10) -- > -- > adhoc :: Table (Text :*: Int)--- > adhoc = table "adhoc" $ required "name" ¤ required "age"+-- > adhoc = table "adhoc" $ required "name" :*: required "age" -- > -- > getPersons1 :: MonadSelda m => m [SimplePerson] -- > getPersons1 = map fromRel <$> query (select adhoc)@@ -234,13 +236,6 @@ identify :: Dummy a -> (a -> b) -> Int identify (Dummy d) f = unsafeCoerce $ f d -class Traits a where-  isMaybeType :: Proxy a -> Bool-  isMaybeType _ = False-instance Traits (Maybe a) where-  isMaybeType _ = True-instance {-# OVERLAPPABLE #-} Traits a- -- | The relation corresponding to the given type. type family Rel (rep :: * -> *) where   Rel (M1 t c a)  = Rel a@@ -281,13 +276,15 @@         }   gMkDummy = M1 <$> gMkDummy -instance (Traits a, SqlType a) => GRelation (K1 i a) where+instance (Typeable a, SqlType a) => GRelation (K1 i a) where   gToRel (K1 x) = x   gTblCols _    = [ColInfo "" (sqlType (Proxy :: Proxy a)) optReq []]     where+      -- workaround for GHC 8.2 not resolving overlapping instances properly+      maybeTyCon = typeRepTyCon (typeRep (Proxy :: Proxy (Maybe ())))       optReq-        | isMaybeType (Proxy :: Proxy a) = [Optional]-        | otherwise                      = [Required]+        | typeRepTyCon (typeRep (Proxy :: Proxy a)) == maybeTyCon = [Optional]+        | otherwise                                               = [Required]   gMkDummy = do     n <- get     put (n+1)
src/Database/Selda/Prepared.hs view
@@ -60,6 +60,10 @@  instance (MonadSelda m, a ~ Res (ResultT q), Result (ResultT q)) =>          Prepare q (m [a]) where+  -- This function uses read/writeIORef instead of atomicModifyIORef.+  -- For once, this is actually safe: the IORef points to a single compiled+  -- statement, so the only consequence of a race between the read and the write+  -- is that the statement gets compiled (note: NOT prepared) twice.   mkFun ref sid qry arguments = do     conn <- seldaConnection     let backend = connBackend conn@@ -68,8 +72,7 @@     case M.lookup sid stmts of       Just stm -> do         -- Statement already prepared for this connection; just execute it.-        liftIO $ do-          runQuery conn stm args+        liftIO $ runQuery conn stm args       _ -> do         -- Statement wasn't prepared for this connection; check if it was at         -- least previously compiled for this backend.@@ -83,7 +86,7 @@             return comp          -- Prepare and execute-        liftIO $ do+        liftIO $ mask $ \restore -> do           hdl <- prepareStmt backend sid reps q           let stm = SeldaStmt                 { stmtHandle = hdl@@ -92,7 +95,7 @@                 , stmtText = q                 }           atomicModifyIORef' (connStmts conn) $ \m -> (M.insert sid stm m, ())-          runQuery conn stm args+          restore $ runQuery conn stm args     where       runQuery conn stm args = do         let backend = connBackend conn
src/Database/Selda/SQL/Print/Config.hs view
@@ -19,6 +19,14 @@     -- | The value used for the next value for an auto-incrementing column.     --   For instance, @DEFAULT@ for PostgreSQL, and @NULL@ for SQLite.   , ppAutoIncInsert :: Text++    -- | Insert queries may have at most this many parameters; if an insertion+    --   has more parameters than this, it will be chunked.+    --+    --   Note that only insertions of multiple rows are chunked. If your table+    --   has more than this many columns, you should really rethink+    --   your database design.+  , ppMaxInsertParams :: Maybe Int   }  -- | Default settings for pretty-printing.@@ -29,6 +37,7 @@     , ppPlaceholder = T.cons '$' . T.pack . show     , ppColAttrs = T.unwords . map defColAttr     , ppAutoIncInsert = "NULL"+    , ppMaxInsertParams = Nothing     }  -- | Default compilation for SQL types.
src/Database/Selda/Selectors.hs view
@@ -7,6 +7,7 @@ import Database.Selda.Column import Data.Dynamic import Data.List (foldl')+import Data.Proxy import Unsafe.Coerce  -- | Get the value at the given index from the given inductive tuple.
src/Database/Selda/Table.hs view
@@ -9,6 +9,7 @@ import Data.Dynamic import Data.List (sort, group) import Data.Monoid+import Data.Proxy import Data.Text (unpack, intercalate, any)  -- | An error occurred when validating a database table.@@ -187,7 +188,7 @@     pkDupes =       ["multiple primary keys" | (Primary:_:_) <- soup $ concatMap colAttrs cis]     nonPkFks =-      [ "column is used as a foreign key, but is not a primary key of its table: "+      [ "column is used as a foreign key, but is not primary or unique: "           <> fromTableName ftn <> "." <> fromColName fcn       | ci <- cis       , (Table ftn fcs _, fcn) <- colFKs ci