packages feed

beam-sqlite 0.5.5.0 → 0.5.6.0

raw patch · 7 files changed

+455/−50 lines, 7 filesdep +containersPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: containers

API changes (from Hackage documentation)

+ Database.Beam.Sqlite.Connection: data SqliteDeleteReturning a
+ Database.Beam.Sqlite.Connection: data SqliteInsertReturning a
+ Database.Beam.Sqlite.Connection: data SqliteUpdateReturning a
+ Database.Beam.Sqlite.Connection: deleteReturning :: forall a table (be :: (Type -> Type) -> Type). Projectible Sqlite a => DatabaseEntity Sqlite be (TableEntity table) -> (forall s. () => table (QExpr Sqlite s) -> QExpr Sqlite s Bool) -> (table (QExpr Sqlite Void) -> a) -> SqliteDeleteReturning (QExprToIdentity a)
+ Database.Beam.Sqlite.Connection: insertOnConflictReturning :: forall s a table (db :: (Type -> Type) -> Type). (Beamable table, Projectible Sqlite a) => DatabaseEntity Sqlite db (TableEntity table) -> SqlInsertValues Sqlite (table (QExpr Sqlite s)) -> SqlConflictTarget Sqlite table -> SqlConflictAction Sqlite table -> (table (QExpr Sqlite Void) -> a) -> SqliteInsertReturning (QExprToIdentity a)
+ Database.Beam.Sqlite.Connection: runDeleteReturningList :: (MonadBeam be m, BeamSqlBackendSyntax be ~ SqliteCommandSyntax, FromBackendRow be a) => SqliteDeleteReturning a -> m [a]
+ Database.Beam.Sqlite.Connection: runUpdateReturningList :: (MonadBeam be m, BeamSqlBackendSyntax be ~ SqliteCommandSyntax, FromBackendRow be a) => SqliteUpdateReturning a -> m [a]
+ Database.Beam.Sqlite.Connection: updateReturning :: forall a table (db :: (Type -> Type) -> Type). Projectible Sqlite a => DatabaseEntity Sqlite db (TableEntity table) -> (forall s. () => table (QField s) -> QAssignment Sqlite s) -> (forall s. () => table (QExpr Sqlite s) -> QExpr Sqlite s Bool) -> (table (QExpr Sqlite Void) -> a) -> SqliteUpdateReturning (QExprToIdentity a)
- Database.Beam.Sqlite.Connection: insertReturning :: forall table (db :: (Type -> Type) -> Type) s. Beamable table => DatabaseEntity Sqlite db (TableEntity table) -> SqlInsertValues Sqlite (table (QExpr Sqlite s)) -> SqlInsert Sqlite table
+ Database.Beam.Sqlite.Connection: insertReturning :: forall table (db :: (Type -> Type) -> Type) s. Beamable table => DatabaseEntity Sqlite db (TableEntity table) -> SqlInsertValues Sqlite (table (QExpr Sqlite s)) -> SqliteInsertReturning (table Identity)
- Database.Beam.Sqlite.Connection: runInsertReturningList :: (Beamable table, FromBackendRow Sqlite (table Identity)) => SqlInsert Sqlite table -> SqliteM [table Identity]
+ Database.Beam.Sqlite.Connection: runInsertReturningList :: (MonadBeam be m, BeamSqlBackendSyntax be ~ SqliteCommandSyntax, FromBackendRow be a) => SqliteInsertReturning a -> m [a]

Files

ChangeLog.md view
@@ -1,3 +1,10 @@+# 0.5.6.0++## Performance optimizations++* `insertReturning` / `deleteReturning` / `updateReturning` now generate optimal SQL queries, minimizing the number+  of separate statements, by grouping similar statements in the presence of the `DEFAULT` keyword (#785).+ # 0.5.5.0  ## Added features
Database/Beam/Sqlite/Connection.hs view
@@ -4,6 +4,7 @@ {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE TypeOperators #-}  module Database.Beam.Sqlite.Connection   ( Sqlite(..), SqliteM(..)@@ -11,7 +12,19 @@    , runBeamSqlite, runBeamSqliteDebug -  , insertReturning, runInsertReturningList+    -- * Support for @RETURNING@ with additional projection++    -- ** @INSERT ... RETURNING@+  , SqliteInsertReturning+  , insertReturning, insertOnConflictReturning, runInsertReturningList++    -- ** @DELETE ... RETURNING@+  , SqliteDeleteReturning+  , deleteReturning, runDeleteReturningList++    -- ** @UPDATE ... RETURNING@+  , SqliteUpdateReturning+  , updateReturning, runUpdateReturningList   ) where  import           Prelude hiding (fail)@@ -25,11 +38,13 @@ import qualified Database.Beam.Migrate.SQL as Beam import           Database.Beam.Migrate.SQL.BeamExtensions import           Database.Beam.Query ( SqlInsert(..), SqlInsertValues(..)+                                     , SqlDelete(..)                                      , HasQBuilder(..), HasSqlEqualityCheck                                      , HasSqlQuantifiedEqualityCheck                                      , DataType(..)                                      , HasSqlInTable(..)-                                     , insert, current_ )+                                     , QExprToIdentity+                                     , insert, current_, delete, SqlUpdate (..), update ) import           Database.Beam.Query.Internal import           Database.Beam.Query.SQL92 import           Database.Beam.Schema.Tables ( Beamable@@ -71,6 +86,9 @@ import qualified Data.DList as D import           Data.Int import           Data.IORef (newIORef, atomicModifyIORef')+import           Data.IntSet (IntSet)+import qualified Data.IntSet as IntSet+import qualified Data.List.NonEmpty as NE import           Data.Maybe (mapMaybe) import           Data.Proxy (Proxy(..)) import           Data.Scientific (Scientific)@@ -80,6 +98,7 @@ import           Data.Time ( LocalTime, UTCTime, Day                            , ZonedTime, utc, utcToLocalTime ) import           Data.Typeable (cast)+import           Data.Void (Void) import           Data.Word import           GHC.IORef (atomicModifyIORef'_) import           GHC.TypeLits@@ -332,26 +351,15 @@                                 Nothing -> pure Nothing                                 Just (BeamSqliteRow row) -> pure row                runReaderT (runSqliteM (action nextRow')) (logger, conn)-  runReturningMany (SqliteCommandInsert (SqliteInsertSyntax tbl fields vs onConflict)) action-    | SqliteInsertExpressions es <- vs, any (any (== SqliteExpressionDefault)) es =-      -- SQLite's handling of default values differs from other DBMses because-      -- it lacks support for DEFAULT. In order to insert a default value in a column,-      -- the column's name should be omitted from the INSERT statement.-      ---      -- This is problematic if you insert multiple rows, some of which have defaults;-      -- you must use multiple INSERT statements. This is what we do below.-      ---      -- However, to respect the 'runReturningMany' interface, be must accumulate the-      -- results of all those inserts into an 'IORef [a]', and then feed the results-      -- incrementally to 'action'.+  runReturningMany (SqliteCommandInsert (SqliteInsertSyntax tbl fields vs onConflict)) action =+      -- The DEFAULT grouping happens at query construction time, so we just+      -- handle the already-grouped statements here.       SqliteM $ do         (logger, conn) <- ask         resultsRef <- liftIO (newIORef [])-        forM_ es $ \row -> do-          -- RETURNING is only supported by SQLite 3.35+, which requires direct-sqlite 2.3.27++        forM_ (sqliteGroupByDefaults fields vs) $ \(fields', vs') -> do           let returningClause = emit " RETURNING " <> commas (map quotedIdentifier fields)-              (insertFields, insertRow) = unzip $ filter ((/= SqliteExpressionDefault) . snd) $ zip fields row-              SqliteSyntax cmd vals = formatSqliteInsertOnConflict tbl insertFields (SqliteInsertExpressions [ insertRow ]) onConflict <> returningClause+              SqliteSyntax cmd vals = formatSqliteInsertOnConflict tbl fields' vs' onConflict <> returningClause               cmdString = BL.unpack (toLazyByteString (withPlaceholders cmd))            liftIO $ do@@ -369,21 +377,6 @@                 (BeamSqliteRow h:rest) -> (rest, Just h)                 [] -> ([], Nothing)         runSqliteM (action nextRow')-    | otherwise =-      SqliteM $ do-        (logger, conn) <- ask-        let returningClause = emit " RETURNING " <> commas (map quotedIdentifier fields)-            SqliteSyntax cmd vals = formatSqliteInsertOnConflict tbl fields vs onConflict <> returningClause-            cmdString = BL.unpack (toLazyByteString (withPlaceholders cmd))-        liftIO $ do-          logger (cmdString ++ ";\n-- With values: " ++ show (D.toList vals))-          withStatement conn (fromString cmdString) $ \stmt ->-            do bind stmt (BeamSqliteParams (D.toList vals))-               let nextRow' = liftIO (nextRow stmt) >>= \x ->-                              case x of-                                Nothing -> pure Nothing-                                Just (BeamSqliteRow row) -> pure row-               runReaderT (runSqliteM (action nextRow')) (logger, conn)   unfoldM :: Monad m => m (Maybe a) -> m [a]@@ -392,8 +385,89 @@     go acc = f >>= maybe (pure acc) (\x -> go (x : acc))  instance Beam.MonadBeamInsertReturning Sqlite SqliteM where-  runInsertReturningList = runInsertReturningList+  runInsertReturningList SqlInsertNoRows = pure []+  runInsertReturningList (SqlInsert _ insertCommand) = runReturningList $ SqliteCommandInsert insertCommand +newtype SqliteInsertReturning a = SqliteInsertReturning [SqliteSyntax]+newtype SqliteDeleteReturning a = SqliteDeleteReturning SqliteSyntax+newtype SqliteUpdateReturning a = SqliteUpdateReturning (Maybe SqliteSyntax)++-- | SQLite cannot deal with DEFAULT in insertion. Instead, it expects these+-- fields to be omitted from the INSERT.+--+-- This means that, for any given INSERT query, the row value being inserted+-- must have a fixed set of fields with DEFAULT value.+--+-- This function groups the inserted values by the set of DEFAULT fields, so+-- that we can perform a separate INSERT for each set of DEFAULT fields.+sqliteGroupByDefaults :: [T.Text] -> SqliteInsertValuesSyntax -> [([T.Text], SqliteInsertValuesSyntax)]+sqliteGroupByDefaults flds (SqliteInsertExpressions es) =+  let+    partitionDefaultsFrom :: (IntSet, [SqliteExpressionSyntax]) -> Int -> [SqliteExpressionSyntax] -> (IntSet, [SqliteExpressionSyntax])+    partitionDefaultsFrom (dflts, acc) _ [] = (dflts, reverse acc)+    partitionDefaultsFrom (dflts, acc) i (x:xs)+      | x == SqliteExpressionDefault+      = partitionDefaultsFrom (IntSet.insert i dflts, acc) (i + 1) xs+      | otherwise+      = partitionDefaultsFrom (dflts, x : acc) (i + 1) xs++    grouped :: [(IntSet, NE.NonEmpty [SqliteExpressionSyntax])]+    grouped =+      map (\xs -> (fst (NE.head xs), fmap snd xs)) $+      NE.groupBy (\(d1, _) (d2, _) -> d1 == d2)+        [ partitionDefaultsFrom (IntSet.empty, []) 0 e+        | e <- es+        ]++    mkSyntax :: IntSet -> NE.NonEmpty [SqliteExpressionSyntax] -> ([T.Text], SqliteInsertValuesSyntax)+    mkSyntax dflts xs =+      ( [fld | (i, fld) <- zip [0..] flds+             , not $ i `IntSet.member` dflts+             ]+      , SqliteInsertExpressions $ NE.toList xs+      )+  in+    map (uncurry mkSyntax) grouped+sqliteGroupByDefaults flds orig@(SqliteInsertFromSql {}) =+  -- Preserve manually-written queries+  [(flds, orig)]++runDeleteReturningList+  :: ( MonadBeam be m+     , BeamSqlBackendSyntax be ~ SqliteCommandSyntax+     , FromBackendRow be a+     )+  => SqliteDeleteReturning a+  -> m [a]+runDeleteReturningList (SqliteDeleteReturning syntax) =+  runReturningList $ SqliteCommandSyntax syntax++-- | SQLite @DELETE ... RETURNING@ statement support. The last+-- argument takes the deleted row and returns the values to be returned.+--+-- Use 'runDeleteReturningList' to get the results.+deleteReturning :: forall a table be. Projectible Sqlite a+                => DatabaseEntity Sqlite be (TableEntity table)+                      -- ^ table to delete from+                -> (forall s. table (QExpr Sqlite s) -> QExpr Sqlite s Bool)+                      -- ^ predicate selecting rows to delete+                -> (table (QExpr Sqlite Void) -> a)+                      -- ^ projection describing what to return+                -> SqliteDeleteReturning (QExprToIdentity a)+deleteReturning table@(DatabaseEntity (DatabaseTable { dbTableSettings = tblSettings }))+                mkWhere+                mkProjection =+  SqliteDeleteReturning $+    fromSqliteDelete sqliteDelete+      <>+    emit " RETURNING " <> commas (map fromSqliteExpression (project (Proxy @Sqlite) (mkProjection tblQ) "t"))+  where++    SqlDelete _ sqliteDelete = delete table (\t -> mkWhere t) -- eta-expand for compatibility with shallow subsumption+    tblQ = changeBeamRep getFieldName tblSettings+    getFieldName (Columnar' fd) =+      Columnar' $ QExpr $ pure $ fieldE (unqualifiedField (_fieldName fd))+ runSqliteInsert :: (String -> IO ()) -> Connection -> SqliteInsertSyntax -> IO () runSqliteInsert logger conn (SqliteInsertSyntax tbl fields vs onConflict)     -- If all expressions are simple expressions (no default), then just@@ -411,23 +485,73 @@       logger (cmdString ++ ";\n-- With values: " ++ show (D.toList vals))       execute conn (fromString cmdString) (D.toList vals) --- * INSERT returning support- -- | Build a 'SqliteInsertReturning' representing inserting the given values--- into the given table. Use 'runInsertReturningList'+-- into the given table and returning all inserted rows.+--+-- Use 'runInsertReturningList' to get the results. insertReturning :: Beamable table                 => DatabaseEntity Sqlite db (TableEntity table)+                      -- ^ table to insert into                 -> SqlInsertValues Sqlite (table (QExpr Sqlite s))-                -> SqlInsert Sqlite table-insertReturning = insert+                      -- ^ values/expressions to insert+                -> SqliteInsertReturning (table Identity)+insertReturning table vals =+  SqliteInsertReturning $+    case insert table vals of+      SqlInsert _ (SqliteInsertSyntax tbl fields values onConflict) ->+        [ formatSqliteInsertOnConflict tbl fields' values' onConflict <>+          emit " RETURNING " <> commas (map quotedIdentifier fields)+        | (fields', values') <- sqliteGroupByDefaults fields values+        ]+      SqlInsertNoRows -> [] +-- | SQLite @INSERT ... RETURNING@ statement support with conflict handling.+-- The last argument takes the newly inserted row and returns the values to be+-- returned.+--+-- Use 'runInsertReturningList' to get the results.+insertOnConflictReturning+  :: forall s a table db+  .  (Beamable table, Projectible Sqlite a)+  => DatabaseEntity Sqlite db (TableEntity table)+      -- ^ table to insert into+  -> SqlInsertValues Sqlite (table (QExpr Sqlite s))+      -- ^ values/expressions to insert+  -> Beam.SqlConflictTarget Sqlite table+      -- ^ the target of the conflict (e.g. the row primary key)+  -> Beam.SqlConflictAction Sqlite table+      -- ^ what to do on conflict (e.g. nothing, override, etc)+  -> (table (QExpr Sqlite Void) -> a)+        -- ^ projection describing what to return+  -> SqliteInsertReturning (QExprToIdentity a)+insertOnConflictReturning+  table@(DatabaseEntity (DatabaseTable { dbTableSettings = tblSettings }))+  vals tgt action mkProjection =+  SqliteInsertReturning $+    case Beam.insertOnConflict table vals tgt action of+      SqlInsert _ (SqliteInsertSyntax tbl fields values onConflict) ->+        [ formatSqliteInsertOnConflict tbl fields' values' onConflict <>+          emit " RETURNING " <> commas (map fromSqliteExpression (project (Proxy @Sqlite) (mkProjection tblQ) "t"))+        | (fields', values') <- sqliteGroupByDefaults fields values+        ]+      SqlInsertNoRows -> []+  where+    tblQ = changeBeamRep getFieldName tblSettings+    getFieldName (Columnar' fd) =+      Columnar' $ QExpr $ pure $ fieldE (unqualifiedField (_fieldName fd))+ -- | Runs a 'SqliteInsertReturning' statement and returns a result for each -- inserted row.-runInsertReturningList :: (Beamable table, FromBackendRow Sqlite (table Identity))-                       => SqlInsert Sqlite table-                       -> SqliteM [ table Identity ]-runInsertReturningList SqlInsertNoRows = pure []-runInsertReturningList (SqlInsert _ insertCommand) = runReturningList $ SqliteCommandInsert insertCommand+runInsertReturningList+  :: ( MonadBeam be m+     , BeamSqlBackendSyntax be ~ SqliteCommandSyntax+     , FromBackendRow be a+     )+  => SqliteInsertReturning a+  -> m [a]+runInsertReturningList (SqliteInsertReturning syntaxes) =+  concat <$>+    traverse (\syntax -> runReturningList $ SqliteCommandSyntax syntax) syntaxes  instance Beam.BeamHasInsertOnConflict Sqlite where   newtype SqlConflictTarget Sqlite table = SqliteConflictTarget@@ -508,3 +632,45 @@ excluded table = changeBeamRep excludedField table   where excludedField (Columnar' (QField _ _ name)) =           Columnar' $ QExpr $ const $ fieldE $ qualifiedField "excluded" name++-- | Use in conjunction with 'updateReturning'.+runUpdateReturningList+  :: ( MonadBeam be m+     , BeamSqlBackendSyntax be ~ SqliteCommandSyntax+     , FromBackendRow be a+     )+  => SqliteUpdateReturning a+  -> m [a]+runUpdateReturningList (SqliteUpdateReturning Nothing) = pure []+runUpdateReturningList (SqliteUpdateReturning (Just syntax)) =+  runReturningList $ SqliteCommandSyntax syntax++-- | SQLite @UPDATE ... RETURNING@ statement support. The last+-- argument takes the updated row and returns the values to be returned.+--+-- Use 'runUpdateReturningList' to get the results.+updateReturning :: forall a table db. Projectible Sqlite a+                => DatabaseEntity Sqlite db (TableEntity table)+                      -- ^ table to update+                -> (forall s. table (QField s) -> QAssignment Sqlite s)+                      -- ^ assignment for the update+                -> (forall s. table (QExpr Sqlite s) -> QExpr Sqlite s Bool)+                      -- ^ predicate selecting rows to update+                -> (table (QExpr Sqlite Void) -> a)+                      -- ^ projection describing what to return+                -> SqliteUpdateReturning (QExprToIdentity a)+updateReturning table@(DatabaseEntity (DatabaseTable { dbTableSettings = tblSettings }))+                mkAssignments+                mkWhere+                mkProjection =+  SqliteUpdateReturning $+    case update table mkAssignments mkWhere of+      SqlIdentityUpdate -> Nothing+      SqlUpdate _ sqliteUpdate ->+        Just $ fromSqliteUpdate sqliteUpdate+          <> emit " RETURNING "+          <> commas (map fromSqliteExpression (project (Proxy @Sqlite) (mkProjection tblQ) "t"))+  where+    tblQ = changeBeamRep getFieldName tblSettings+    getFieldName (Columnar' fd) =+      Columnar' $ QExpr $ pure $ fieldE (unqualifiedField (_fieldName fd))
Database/Beam/Sqlite/Syntax.hs view
@@ -133,7 +133,7 @@ -- | Convert the first argument of 'SQLiteSyntax' to a 'ByteString' 'Builder', -- where all the data has been replaced by @"?"@ placeholders. withPlaceholders :: ((SQLData -> Builder) -> Builder) -> Builder-withPlaceholders build = build (\_ -> "?")+withPlaceholders build = build (const "?")  -- | Embed a 'ByteString' directly in the syntax emit :: ByteString -> SqliteSyntax
beam-sqlite.cabal view
@@ -1,5 +1,5 @@ name:                beam-sqlite-version:             0.5.5.0+version:             0.5.6.0 synopsis:            Beam driver for SQLite description:         Beam driver for the <https://sqlite.org/ SQLite> embedded database.                      See <https://haskell-beam.github.io/beam/user-guide/backends/beam-sqlite/ here>@@ -14,7 +14,7 @@ build-type:          Simple extra-source-files:  README.md extra-doc-files:     ChangeLog.md-bug-reports:          https://github.com/haskell-beam/beam/issues+bug-reports:         https://github.com/haskell-beam/beam/issues cabal-version:       1.18  library@@ -29,6 +29,7 @@                       beam-migrate  >=0.5.3.2  && <0.6,                        sqlite-simple >=0.4  && <0.5,+                      containers    >=0.6  && <0.9,                       text          >=1.0  && <2.2,                       bytestring    >=0.10 && <0.13,                       hashable      >=1.2  && <1.6,@@ -76,6 +77,7 @@     Database.Beam.Sqlite.Test.Insert     Database.Beam.Sqlite.Test.InsertOnConflictReturning     Database.Beam.Sqlite.Test.Migrate+    Database.Beam.Sqlite.Test.Returning     Database.Beam.Sqlite.Test.Select   default-language: Haskell2010   default-extensions:
test/Database/Beam/Sqlite/Test/Insert.hs view
@@ -18,6 +18,7 @@   [ testInsertReturningColumnOrder   , testInsertOnlyDefaults   , expectFail testUpsertOnlyDefaults+  , testInsertSomeDefaults   ]  data TestTableT f@@ -67,7 +68,7 @@                    , TestTable 1 "sally" "apple" 33 dateJoined oneUtcTime                    , TestTable 4 "blah" "blah" (-1) dateJoined now ] -    assertEqual "insert values" inserted expected+    assertEqual "insert values" expected inserted  data WithDefaultsT f = WithDefaults   { _id :: C f (SqlSerial Int32)@@ -99,10 +100,11 @@         [ WithDefaults default_ default_         , WithDefaults default_ $ val_ "other"         ]-    assertEqual "inserted values" inserted+    assertEqual "inserted values"       [ WithDefaults 1 "unknown"       , WithDefaults 2 "other"       ]+      inserted  testUpsertOnlyDefaults :: TestTree testUpsertOnlyDefaults = testCase "upsert only default values" $@@ -117,7 +119,55 @@         )         anyConflict         onConflictDoNothing-    assertEqual "inserted values" inserted+    assertEqual "inserted values"       [ WithDefaults 1 "unknown"       , WithDefaults 2 "other"       ]+      inserted+++data MixTableT f+  = MixTable+  { mtId :: C f (SqlSerial Int32)+  , mtField1 :: C f Int32+  , mtField2 :: C f Int32+  , mtField3 :: C f Int32+  } deriving (Generic, Beamable)++deriving instance Show (MixTableT Identity)+deriving instance Eq (MixTableT Identity)++instance Table MixTableT where+  data PrimaryKey MixTableT f = MixTableKey (C f (SqlSerial Int32))+    deriving (Generic, Beamable)+  primaryKey (MixTable { mtId = i }) = MixTableKey i++data MixTableDb entity+  = MixTableDb+  { dbMixTable :: entity (TableEntity MixTableT)+  } deriving (Generic, Database Sqlite)++mixDatabase :: DatabaseSettings be MixTableDb+mixDatabase = defaultDbSettings++-- | Test grouping logic for queries containing different DEFAULT column layouts.+testInsertSomeDefaults :: TestTree+testInsertSomeDefaults = testCase "insert mix of default values" $ do+  withTestDb $ \conn -> do+    execute_ conn "CREATE TABLE mix_table ( id INTEGER PRIMARY KEY AUTOINCREMENT, field1 INT DEFAULT 11, field2 INT DEFAULT 22, field3 INT DEFAULT 33)"+    inserted <-+      runBeamSqlite conn $ runInsertReturningList $+      insert (dbMixTable mixDatabase) $+      insertExpressions [ MixTable default_ default_ 1001     10001+                        , MixTable 202      2        default_ default_+                        , MixTable 303      3        default_ default_+                        , MixTable default_ default_ 4004     40004+                        ]++    let expected = [ MixTable 1   11 1001 10001+                   , MixTable 202 2  22   33+                   , MixTable 303 3  22   33+                   , MixTable 304 11 4004 40004+                   ]++    assertEqual "insert values" expected inserted
+ test/Database/Beam/Sqlite/Test/Returning.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE DeriveAnyClass #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE UndecidableInstances #-}++module Database.Beam.Sqlite.Test.Returning (tests) where++import Data.Int (Int32)+import Data.Text (Text)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (testCase, (@?=))++import Database.Beam+import Database.Beam.Backend.SQL.BeamExtensions+  (conflictingFields, onConflictUpdateSet, onConflictUpdateSetWhere)+import Database.Beam.Migrate (defaultMigratableDbSettings)+import Database.Beam.Migrate.Simple (CheckedDatabaseSettings, autoMigrate)+import Database.Beam.Sqlite (Sqlite, runBeamSqlite, insertOnConflictReturning)+import Database.Beam.Sqlite.Migrate (migrationBackend)++import Database.Beam.Sqlite+    ( deleteReturning+    , insertReturning+    , runDeleteReturningList+    , runInsertReturningList+    , runUpdateReturningList+    , updateReturning+    )++import Database.Beam.Sqlite.Test (withTestDb)+++tests :: TestTree+tests =+    testGroup+        "SQLite RETURNING statement tests"+        [ testInsertOnConflictReturning+        , testUpdateReturning+        , testDeleteReturning+        ]++-- | Database Schema Definition+data TestDb f+    = TestDb+    { usersTable :: f (TableEntity User)+    }+    deriving stock (Generic)++deriving anyclass instance Database be TestDb++testDb :: DatabaseSettings be TestDb+testDb = defaultDbSettings++checkedDb :: CheckedDatabaseSettings Sqlite TestDb+checkedDb = defaultMigratableDbSettings++data User f+    = User+    { userId    :: !( Columnar f Int32 )+    , userName  :: !( Columnar f Text  )+    , userInfo1 :: !( Columnar f Text  )+    , userInfo2 :: !( Columnar f Int32 )+    }+    deriving stock (Generic)+deriving stock instance (Eq (Columnar f Int32), Eq (Columnar f Text)) => Eq (User f)+deriving stock instance (Show (Columnar f Int32), Show (Columnar f Text)) => Show (User f)++instance Table User where+    newtype PrimaryKey User f = UserId (Columnar f Int32)+        deriving stock (Generic)+    primaryKey = UserId . userId++deriving anyclass instance Beamable User+deriving anyclass instance Beamable (PrimaryKey User)++-- | Test @INSERT .. ON CONFLICT .. RETURNING@+testInsertOnConflictReturning :: TestTree+testInsertOnConflictReturning = testCase "INSERT .. ON CONFLICT .. RETURNING" $+  withTestDb $ \conn -> do+    results <-+      runBeamSqlite conn $ do+        autoMigrate migrationBackend checkedDb++        -- Seed data+        runInsert $+          insert+            (usersTable testDb)+            $ insertValues+              [ User {userId = 1, userName = "initial_user1", userInfo1 = "user1Info", userInfo2 = 11 }+              , User {userId = 3, userName = "initial_user3", userInfo1 = "user3Info", userInfo2 = 33 }+              , User {userId = 4, userName = "initial_user4", userInfo1 = "user4Info", userInfo2 = 44 }+              ]++        let+          newUsers =+            [ User {userId = 1, userName = "upserted_user1", userInfo1 = "user1Info2", userInfo2 = 111 }+            , User {userId = 2, userName = "new_user2"     , userInfo1 = "info1"     , userInfo2 = 222 }+            , User {userId = 3, userName = "upserted_user3", userInfo1 = "info1b"    , userInfo2 = 333 }+            , User {userId = 4, userName = "not_upserted_4", userInfo1 = "ignore"    , userInfo2 = 444 }+            ]++        -- Run 'INSERT .. ON CONFLICT (id) DO UPDATE SET .. WHERE .. RETURNING ..'+        runInsertReturningList $+          insertOnConflictReturning+            (usersTable testDb)+            (insertValues newUsers)+            (conflictingFields userId)+            (onConflictUpdateSetWhere+              ( \(User{userName = fld}) (User{userName = excl}) ->+                  fld <-. excl+              )+              ( \(User{userName = fld}) (User{userName = excl, userInfo1 = exclInfo1 }) ->+                  (current_ fld /=. excl) &&. (exclInfo1 /=. "ignore")+              )+            )+            (\u -> (userId u, userName u, userInfo2 u))++    results @?=+      [ (1, "upserted_user1",  11)+      , (2, "new_user2"     , 222)+      , (3, "upserted_user3",  33)+      ]++-- | Test @UPDATE ... RETURNING@+testUpdateReturning :: TestTree+testUpdateReturning = testCase "UPDATE .. RETURNING" $+  withTestDb $ \conn -> do+    updatedUsers <-+      runBeamSqlite conn $ do+        autoMigrate migrationBackend checkedDb++        -- Seed data+        runInsert $+          insert (usersTable testDb) $+            insertValues+              [ User {userId = 1, userName = "user1", userInfo1 = "user1Info1", userInfo2 = 11 }+              , User {userId = 2, userName = "user2", userInfo1 = "user2Info1", userInfo2 = 22 }+              , User {userId = 3, userName = "user3", userInfo1 = "user3Info1", userInfo2 = 33 }+              ]++        -- Update user 2 and return projected columns from the updated row+        runUpdateReturningList $+          updateReturning+            (usersTable testDb)+            (\u -> userName u <-. val_ "updated_user2")+            (\u -> userId u ==. val_ 2)+            (\u -> (userId u, userName u, userInfo2 u))++    updatedUsers @?= [(2, "updated_user2", 22)]++-- | Test @DELETE .. RETURNING@+testDeleteReturning :: TestTree+testDeleteReturning = testCase "DELETE .. RETURNING" $+  withTestDb $ \conn -> do+    deletedUsers <-+      runBeamSqlite conn $ do+        autoMigrate migrationBackend checkedDb++        -- Seed data+        runInsert $+          insert (usersTable testDb) $+            insertValues+              [ User { userId = 1, userName = "user1", userInfo1 = "user1Info1", userInfo2 = 11 }+              , User { userId = 2, userName = "user2", userInfo1 = "user2Info1", userInfo2 = 22 }+              , User { userId = 3, userName = "user3", userInfo1 = "user3Info1", userInfo2 = 33 }+              ]++        -- Delete user 3 and return projected columns from the deleted row+        runDeleteReturningList $+          deleteReturning+            (usersTable testDb)+            (\u -> userId u ==. val_ 3)+            (\u -> (userName u, userInfo2 u))++    deletedUsers @?= [("user3", 33)]
test/Main.hs view
@@ -6,6 +6,7 @@ import qualified Database.Beam.Sqlite.Test.Insert as Insert import qualified Database.Beam.Sqlite.Test.InsertOnConflictReturning as InsertOnConflictReturning import qualified Database.Beam.Sqlite.Test.Select as Select+import qualified Database.Beam.Sqlite.Test.Returning as Returning  main :: IO () main = defaultMain $ testGroup "beam-sqlite tests"@@ -13,4 +14,5 @@       , Select.tests       , Insert.tests       , InsertOnConflictReturning.tests+      , Returning.tests       ]