packages feed

beam-sqlite 0.5.7.0 → 0.6.0.0

raw patch · 12 files changed

+600/−34 lines, 12 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Database.Beam.Sqlite.Connection: runUpdateReturningList :: (MonadBeam be m, BeamSqlBackendSyntax be ~ SqliteCommandSyntax, FromBackendRow be a) => SqliteUpdateReturning a -> m [a]
+ Database.Beam.Sqlite.Connection: instance Database.Beam.Backend.SQL.BeamExtensions.MonadBeamUpdateReturning Database.Beam.Sqlite.Connection.Sqlite Database.Beam.Sqlite.Connection.SqliteM
+ Database.Beam.Sqlite.Connection: runSqliteUpdateReturningList :: (MonadBeam be m, BeamSqlBackendSyntax be ~ SqliteCommandSyntax, FromBackendRow be a) => SqliteUpdateReturning a -> m [a]
+ Database.Beam.Sqlite.TempTable: CreateIfNotExists :: TempTableCreateMode
+ Database.Beam.Sqlite.TempTable: DropAndCreate :: TempTableCreateMode
+ Database.Beam.Sqlite.TempTable: TempTableOptions :: TempTableCreateMode -> TempTableOptions
+ Database.Beam.Sqlite.TempTable: [tempTableCreateMode] :: TempTableOptions -> TempTableCreateMode
+ Database.Beam.Sqlite.TempTable: data TempTableCreateMode
+ Database.Beam.Sqlite.TempTable: data TempTableOptions
+ Database.Beam.Sqlite.TempTable: defaultTempTableOptions :: TempTableOptions
+ Database.Beam.Sqlite.TempTable: instance Database.Beam.Migrate.TempTable.BeamHasTempTables Database.Beam.Sqlite.Connection.Sqlite
+ Database.Beam.Sqlite.TempTable: runCreateTempTable :: forall be (db :: (Type -> Type) -> Type) (tbl :: (Type -> Type) -> Type) m. (HasDefaultTempTableSchema be tbl, MonadBeam be m) => TempTableOptions -> Text -> m (DatabaseEntity be db (TableEntity tbl))
+ Database.Beam.Sqlite.TempTable: type HasDefaultSqliteTempTableSchema (tbl :: Type -> Type -> Type) = HasDefaultTempTableSchema Sqlite tbl

Files

ChangeLog.md view
@@ -1,3 +1,20 @@+# 0.6.0.0++## Added features++* `SqliteM` now supports `runUpdateReturningList` via a `MonadBeamUpdateReturning` instance. The type is almost identical,+  but not quite, as the `runUpdateReturningList` function from previous releases. Instead, `runUpdateReturningList` has been+  renamed `runSqliteUpdateReturningList`.++* Support for temporary tables.++* `getDbConstraints` now discovers foreign key constraints via `PRAGMA foreign_key_list`, including `ON DELETE` / `ON UPDATE` actions.++## Bug fixes++* The SQLite migration framework no longer incorrectly concludes a migration+  would lose data due to the presence of internal `sqlite_*` tables.+ # 0.5.7.0  ## Added features
Database/Beam/Sqlite.hs view
@@ -1,6 +1,7 @@ module Database.Beam.Sqlite   ( module Database.Beam.Sqlite.Connection   , module Database.Beam.Sqlite.Migrate+  , module Database.Beam.Sqlite.TempTable      -- * SQLite syntaxes   , SqliteCommandSyntax(..), SqliteSyntax@@ -19,3 +20,4 @@ import Database.Beam.Sqlite.SqliteSpecific import Database.Beam.Sqlite.Connection import Database.Beam.Sqlite.Migrate (sqliteText, sqliteBlob, sqliteBigInt)+import Database.Beam.Sqlite.TempTable
Database/Beam/Sqlite/Connection.hs view
@@ -24,7 +24,7 @@      -- ** @UPDATE ... RETURNING@   , SqliteUpdateReturning-  , updateReturning, runUpdateReturningList+  , updateReturning, runSqliteUpdateReturningList   ) where  import           Prelude hiding (fail)@@ -53,6 +53,7 @@                                              , DatabaseEntityDescriptor(..)                                              , TableEntity                                              , TableField(..)+                                             , TableSettings                                              , changeBeamRep ) import           Database.Beam.Sqlite.Syntax @@ -388,6 +389,18 @@   runInsertReturningList SqlInsertNoRows = pure []   runInsertReturningList (SqlInsert _ insertCommand) = runReturningList $ SqliteCommandInsert insertCommand +-- |+-- @since 0.6.0.0+instance Beam.MonadBeamUpdateReturning Sqlite SqliteM where+  runUpdateReturningList :: forall table. (+    Beamable table, Projectible Sqlite (table (QExpr Sqlite ())), FromBackendRow Sqlite (table Identity)+    ) => SqlUpdate Sqlite table -> SqliteM [table Identity]+  runUpdateReturningList SqlIdentityUpdate = pure []+  runUpdateReturningList (SqlUpdate tblSettings sqliteUpdate) =+    runReturningList $ SqliteCommandSyntax $+      fromSqliteUpdate sqliteUpdate+        <> returningClauseWithProjection tblSettings (id :: table (QExpr Sqlite ()) -> table (QExpr Sqlite ()))+ newtype SqliteInsertReturning a = SqliteInsertReturning [SqliteSyntax] newtype SqliteDeleteReturning a = SqliteDeleteReturning SqliteSyntax newtype SqliteUpdateReturning a = SqliteUpdateReturning (Maybe SqliteSyntax)@@ -458,15 +471,9 @@                 mkWhere                 mkProjection =   SqliteDeleteReturning $-    fromSqliteDelete sqliteDelete-      <>-    emit " RETURNING " <> commas (map fromSqliteExpression (project (Proxy @Sqlite) (mkProjection tblQ) "t"))+    fromSqliteDelete sqliteDelete <> returningClauseWithProjection tblSettings mkProjection   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)@@ -634,21 +641,23 @@           Columnar' $ QExpr $ const $ fieldE $ qualifiedField "excluded" name  -- | Use in conjunction with 'updateReturning'.-runUpdateReturningList+--+-- @since 0.6.0.0+runSqliteUpdateReturningList   :: ( MonadBeam be m      , BeamSqlBackendSyntax be ~ SqliteCommandSyntax      , FromBackendRow be a      )   => SqliteUpdateReturning a   -> m [a]-runUpdateReturningList (SqliteUpdateReturning Nothing) = pure []-runUpdateReturningList (SqliteUpdateReturning (Just syntax)) =+runSqliteUpdateReturningList (SqliteUpdateReturning Nothing) = pure []+runSqliteUpdateReturningList (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.+-- Use 'runSqliteUpdateReturningList' to get the results. updateReturning :: forall a table db. Projectible Sqlite a                 => DatabaseEntity Sqlite db (TableEntity table)                       -- ^ table to update@@ -667,10 +676,14 @@     case update table mkAssignments mkWhere of       SqlIdentityUpdate -> Nothing       SqlUpdate _ sqliteUpdate ->-        Just $ fromSqliteUpdate sqliteUpdate-          <> emit " RETURNING "-          <> commas (map fromSqliteExpression (project (Proxy @Sqlite) (mkProjection tblQ) "t"))+        Just $ fromSqliteUpdate sqliteUpdate <> returningClauseWithProjection tblSettings mkProjection++-- | Build a SQLite @RETURNING@ clause from table settings and a projection.+returningClauseWithProjection :: (Beamable table, Projectible Sqlite a) => TableSettings table -> (table (QExpr Sqlite s) -> a) -> SqliteSyntax+returningClauseWithProjection tblSettings mkProjection =+  emit " RETURNING " <> commas (map fromSqliteExpression (project (Proxy @Sqlite) (mkProjection (tableFieldExprs tblSettings)) "t"))   where-    tblQ = changeBeamRep getFieldName tblSettings-    getFieldName (Columnar' fd) =-      Columnar' $ QExpr $ pure $ fieldE (unqualifiedField (_fieldName fd))+    -- | Build column expressions from table settings, for use in @RETURNING@ clauses.+    tableFieldExprs :: Beamable table => TableSettings table -> table (QExpr Sqlite s)+    tableFieldExprs = changeBeamRep (\(Columnar' fd) ->+      Columnar' $ QExpr $ pure $ fieldE (unqualifiedField (_fieldName fd)))
Database/Beam/Sqlite/Migrate.hs view
@@ -40,9 +40,10 @@ import qualified Data.ByteString.Lazy.Char8 as BL import           Data.Char (isSpace) import           Data.Int (Int64)-import           Data.List (sortBy)+import           Data.List (sortBy, groupBy) import qualified Data.List.NonEmpty as NE (nonEmpty) import           Data.Maybe (mapMaybe, isJust)+import           Data.Function (on) import           Data.Monoid (Endo(..)) import           Data.Ord (comparing) import           Data.String (fromString)@@ -262,8 +263,6 @@         let hdl = connectionHandle conn         in exec hdl t --- TODO constraints and foreign keys- -- | Get a list of database predicates for the current database. This is beam's -- best guess at providing a schema for the current database. Note that SQLite -- type names are not standardized, and the so-called column "affinities" are@@ -275,7 +274,20 @@ getDbConstraints :: A.Parser SqliteDataTypeSyntax -> SqliteM [Db.SomeDatabasePredicate] getDbConstraints extraParser =   SqliteM . ReaderT $ \(_, conn) -> do-    tblNames <- query_ conn "SELECT name, sql from sqlite_master where type='table'"+    -- Exclude SQLite-internal tables (sqlite_sequence, sqlite_stat1, etc.).+    -- These are created automatically by SQLite (e.g. sqlite_sequence appears+    -- whenever any table uses AUTOINCREMENT).+    --+    -- Failing to filter them out would mean we would try to drop them, which+    -- would incorrectly look like data loss.+    --+    -- NB: (https://www.sqlite.org/lang_createtable.html)+    --+    --   Table names that begin with "sqlite_" are reserved for internal use.+    --   It is an error to attempt to create a table with a name that starts with "sqlite_".+    tblNames <-+      query_ conn+        "SELECT name, sql FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'"     tblPreds <-       fmap mconcat . forM tblNames $ \(tblNameStr, sql) -> do         let tblName = QualifiedName Nothing tblNameStr@@ -348,10 +360,43 @@                       [ Db.SomeDatabasePredicate                           (Db.TableHasIndex @Sqlite tblName idxNm colsNE opts) ] +        -- Collect foreign key constraints via PRAGMA foreign_key_list.+        -- Each row: (id, seq, table, from, to, on_update, on_delete, match)+        -- Rows sharing the same 'id' form a composite foreign key,+        -- with 'seq' providing the ordering.+        fkRows <- query_ conn (fromString ("PRAGMA foreign_key_list('" <> T.unpack tblNameStr <> "')")+                          ) :: IO [(Int, Int, T.Text, T.Text, T.Text, T.Text, T.Text, T.Text)]+        let fkPreds =+              concatMap mkFkPred $+              groupBy ((==) `on` (\(fkId, _, _, _, _, _, _, _) -> fkId)) $+              sortBy (comparing (\(fkId, seqNo, _, _, _, _, _, _) -> (fkId, seqNo))) fkRows+            mkFkPred [] = []+            mkFkPred rows@((_, _, refTblStr, _, _, onUpdateStr, onDeleteStr, _):_) =+              let localCols = map (\(_, _, _, from, _, _, _, _) -> from) rows+                  refCols   = map (\(_, _, _, _, to,   _, _, _) -> to)   rows+                  refTable  = QualifiedName Nothing refTblStr+                  onUpdate  = parseForeignKeyAction onUpdateStr+                  onDelete  = parseForeignKeyAction onDeleteStr+              in case (NE.nonEmpty localCols, NE.nonEmpty refCols) of+                   (Just lcNE, Just rcNE) ->+                     [ Db.SomeDatabasePredicate+                         (Db.TableHasForeignKey tblName lcNE refTable rcNE onUpdate onDelete) ]+                   _ -> []+         pure ( [ Db.SomeDatabasePredicate (Db.TableExistsPredicate tblName) ]-             ++ pkPred ++ columnPreds ++ idxPreds )+             ++ pkPred ++ columnPreds ++ idxPreds ++ fkPreds )      pure tblPreds++parseForeignKeyAction :: T.Text -> Db.ForeignKeyAction+parseForeignKeyAction t = case T.toUpper t of+  "CASCADE"    -> Db.ForeignKeyActionCascade+  "SET NULL"   -> Db.ForeignKeyActionSetNull+  "SET DEFAULT"-> Db.ForeignKeyActionSetDefault+  "RESTRICT"   -> Db.ForeignKeyActionRestrict+  "NO ACTION"  -> Db.ForeignKeyNoAction+  _            -> error $+    "parseForeignKeyAction: unrecognised foreign key action: " ++ T.unpack t  sqliteText :: Db.DataType Sqlite T.Text sqliteText = Db.DataType sqliteTextType
Database/Beam/Sqlite/Syntax.hs view
@@ -65,7 +65,7 @@ import qualified Data.DList as DL import           Data.Hashable import           Data.Int-import qualified Data.List.NonEmpty as NE (toList)+import qualified Data.List.NonEmpty as NE (NonEmpty(..), toList) import           Data.Maybe import           Data.Scientific import           Data.String@@ -282,7 +282,7 @@ data SqliteTableConstraintSyntax   = SqliteTableConstraintSyntax   { fromSqliteTableConstraint :: SqliteSyntax-  , sqliteTableConstraintPrimaryKey :: Maybe [ T.Text ] }+  , sqliteTableConstraintPrimaryKey :: Maybe (NE.NonEmpty T.Text) } data SqliteMatchTypeSyntax     = SqliteMatchTypeSyntax     { fromSqliteMatchType :: SqliteSyntax@@ -476,11 +476,27 @@   referentialActionSetDefaultSyntax = SqliteReferentialActionSyntax (emit "SET DEFAULT") referentialActionSetDefaultSyntax   referentialActionNoActionSyntax = SqliteReferentialActionSyntax (emit "NO ACTION") referentialActionNoActionSyntax +sqliteForeignKeyAction :: ForeignKeyAction -> SqliteSyntax+sqliteForeignKeyAction ForeignKeyActionCascade    = emit "CASCADE"+sqliteForeignKeyAction ForeignKeyActionSetNull    = emit "SET NULL"+sqliteForeignKeyAction ForeignKeyActionSetDefault = emit "SET DEFAULT"+sqliteForeignKeyAction ForeignKeyActionRestrict   = emit "RESTRICT"+sqliteForeignKeyAction ForeignKeyNoAction         = emit "NO ACTION"+ instance IsSql92TableConstraintSyntax SqliteTableConstraintSyntax where   primaryKeyConstraintSyntax fields =     SqliteTableConstraintSyntax-      (emit "PRIMARY KEY" <> parens (commas (map quotedIdentifier fields)))+      (emit "PRIMARY KEY" <> parens (commas (map quotedIdentifier $ NE.toList fields)))       (Just fields)+  foreignKeyConstraintSyntax localCols refTbl refCols onUpdate onDelete =+    SqliteTableConstraintSyntax syntax Nothing+    where+      syntax =+        emit "FOREIGN KEY" <> parens (commas (map quotedIdentifier $ NE.toList localCols)) <>+        emit " REFERENCES " <> quotedIdentifier refTbl <>+        parens (commas (map quotedIdentifier $ NE.toList refCols)) <>+        emit " ON UPDATE " <> sqliteForeignKeyAction onUpdate <>+        emit " ON DELETE " <> sqliteForeignKeyAction onDelete  instance IsSql92CreateTableSyntax SqliteCreateTableSyntax where   type Sql92CreateTableColumnSchemaSyntax SqliteCreateTableSyntax = SqliteColumnSchemaSyntax@@ -511,7 +527,7 @@          [field] ->            case constraintPks of              [] -> error "A column claims to have a primary key, but there is no key on this table"-             [[fieldPk]]+             [fieldPk NE.:| _]                | field /= fieldPk -> error "Two columns claim to be a primary key on this table"                | otherwise -> createTableNoPkConstraint              _ -> error "There are multiple primary key constraints on this table"
+ Database/Beam/Sqlite/TempTable.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE TypeApplications #-}++{-# OPTIONS_GHC -fno-warn-orphans #-}++module Database.Beam.Sqlite.TempTable+  ( -- * Temporary tables++    -- ** @CREATE TEMPORARY TABLE@+    runCreateTempTable++    -- ** Options+  , TempTableCreateMode(..)+  , TempTableOptions(..)+  , defaultTempTableOptions++    -- ** Schema constraint+  , HasDefaultSqliteTempTableSchema++  ) where++import           Database.Beam.Migrate.TempTable+  ( BeamHasTempTables(..)+  , HasDefaultTempTableSchema+  , TempTableCreateMode(..)+  , TempTableOptions(..)+  , defaultTempTableOptions+  , runCreateTempTable+  )+import           Database.Beam.Sqlite.Connection (Sqlite)+import           Database.Beam.Sqlite.Syntax+  ( SqliteCommandSyntax(..)+  , fromSqliteColumnSchema, sqliteIsSerialColumn+  , emit, quotedIdentifier, parens, commas+  )++--------------------------------------------------------------------------------++-- | Tables for which @CREATE TEMPORARY TABLE@ statements can be emitted+-- automatically.+type HasDefaultSqliteTempTableSchema tbl = HasDefaultTempTableSchema Sqlite tbl++instance BeamHasTempTables Sqlite where+  createTempTableCmd nm colDefs pkCols ifNotExists =+    SqliteCommandSyntax $+      emit "CREATE TEMPORARY TABLE "+        <> (if ifNotExists then emit "IF NOT EXISTS " else mempty)+        <> quotedIdentifier nm+        <> parens (commas (colDefPieces ++ pkPiece))+    where+      colDefPieces =+        map (\(n, s) -> quotedIdentifier n <> emit " " <> fromSqliteColumnSchema s)+            colDefs++      -- SQLite's INTEGER PRIMARY KEY AUTOINCREMENT already encodes the+      -- primary key constraint, so a separate PRIMARY KEY constraint would+      -- constitute a syntax error.+      hasSerial = any (sqliteIsSerialColumn . snd) colDefs+      pkPiece+        | hasSerial || null pkCols = []+        | otherwise =+            [ emit "PRIMARY KEY "+                <> parens (commas (map quotedIdentifier pkCols)) ]++  dropTempTableIfExistsCmd nm =+    SqliteCommandSyntax $+      emit "DROP TABLE IF EXISTS " <> quotedIdentifier nm
README.md view
@@ -10,3 +10,36 @@ Due to SQLite's embedded nature, there are currently no plans to get rid of these. However, proposals and PRs to fix these corner cases are welcome, where appropriate.++## Migration support++`beam-sqlite` supports schema introspection and verification via+`Database.Beam.Sqlite.Migrate`, including:++- Tables, columns, and their types,+- `NOT NULL` constraints,+- Primary keys,+- User-created secondary indices (via `PRAGMA index_list` / `PRAGMA index_info`),+- Foreign key constraints (via `PRAGMA foreign_key_list`), including+  `ON DELETE` / `ON UPDATE` actions.++### Declaring foreign keys++Use `addTableForeignKey` from `beam-migrate` to declare a foreign key on a+checked table entity:++```haskell+let db = defaultMigratableDbSettings `withDbModification`+          (dbModification @_ @Sqlite)+            { _orders =+                addTableForeignKey (_customers db)+                  (\t -> selectorColumnName _order_customer_id t NE.:| [])+                  primaryKeyColumns+                  ForeignKeyNoAction      -- ON UPDATE NO ACTION (default)+                  ForeignKeyActionCascade -- ON DELETE CASCADE+            }+```++> **Note:** SQLite disables foreign key enforcement by default. Issue+> `PRAGMA foreign_keys = ON` on each connection (or set it in your open hook)+> to enable it at runtime.
beam-sqlite.cabal view
@@ -1,5 +1,5 @@ name:                beam-sqlite-version:             0.5.7.0+version:             0.6.0.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>@@ -22,6 +22,7 @@                       Database.Beam.Sqlite.Syntax                       Database.Beam.Sqlite.Connection                       Database.Beam.Sqlite.Migrate+                      Database.Beam.Sqlite.TempTable   other-modules:      Database.Beam.Sqlite.SqliteSpecific   build-depends:      base          >=4.11 && <5, @@ -79,6 +80,7 @@     Database.Beam.Sqlite.Test.Migrate     Database.Beam.Sqlite.Test.Returning     Database.Beam.Sqlite.Test.Select+    Database.Beam.Sqlite.Test.TempTable   default-language: Haskell2010   default-extensions:     DeriveAnyClass,
test/Database/Beam/Sqlite/Test/Migrate.hs view
@@ -1,5 +1,7 @@ module Database.Beam.Sqlite.Test.Migrate (tests) where +import Control.Exception (try, IOException)+import Data.List (isInfixOf) import Database.SQLite.Simple import Test.Tasty import Test.Tasty.HUnit@@ -7,6 +9,7 @@ import qualified Data.List.NonEmpty as NE import Data.Int (Int32) import Database.Beam+import Database.Beam.Backend.SQL.BeamExtensions (SqlSerial(..)) import Database.Beam.Sqlite import Database.Beam.Sqlite.Migrate import Database.Beam.Migrate@@ -21,6 +24,12 @@   , verifiesNoPrimaryKey   , verifiesIndex   , verifiesUniqueIndex+  , migrateNonUniqueToUniqueIndex+  , verifiesForeignKey+  , addingForeignKeyFails+  , verifiesForeignKeyActions+  , foreignKeyActionsWork+  , idempotentMigration   ]  newtype WithPkT f = WithPkT@@ -121,3 +130,251 @@                     addTableIndex "idx_tbl_value_uniq" idxOpts                       (\t -> selectorColumnName _idx_value t NE.:| []) }     testVerifySchema conn db++-- | Check that we can change the uniqueness of an index in a migration+migrateNonUniqueToUniqueIndex :: TestTree+migrateNonUniqueToUniqueIndex =+  testCase "autoMigrate can change a non-unique index to a unique index" $+  withTestDb $ \conn -> do+    let nonUnique :: CheckedDatabaseSettings Sqlite IdxDb+        nonUnique =+          defaultMigratableDbSettings `withDbModification`+            (dbModification @_ @Sqlite)+              { _idx_tbl =+                  addTableIndex "idx_tbl_value"+                    (defaultIndexOptions @SqliteCommandSyntax)+                    (\t -> selectorColumnName _idx_value t NE.:| []) }+        unique :: CheckedDatabaseSettings Sqlite IdxDb+        unique =+          defaultMigratableDbSettings `withDbModification`+            (dbModification @_ @Sqlite)+              { _idx_tbl =+                  addTableIndex "idx_tbl_value"+                    (setUniqueIndexOptions @SqliteCommandSyntax True $+                     defaultIndexOptions @SqliteCommandSyntax)+                    (\t -> selectorColumnName _idx_value t NE.:| []) }+    runBeamSqlite conn $ autoMigrate migrationBackend nonUnique+    runBeamSqlite conn $ autoMigrate migrationBackend unique++-- Foreign key tests++data ParentT f = ParentT+  { _parent_id :: C f Int32+  } deriving (Generic, Beamable)++instance Table ParentT where+  newtype PrimaryKey ParentT f = ParentPk (C f Int32)+    deriving (Generic, Beamable)+  primaryKey = ParentPk . _parent_id++data ChildT f = ChildT+  { _child_id        :: C f Int32+  , _child_parent_id :: PrimaryKey ParentT f+  } deriving (Generic, Beamable)++instance Table ChildT where+  newtype PrimaryKey ChildT f = ChildPk (C f Int32)+    deriving (Generic, Beamable)+  primaryKey = ChildPk . _child_id++data FkDb entity = FkDb+  { _fk_parent :: entity (TableEntity ParentT)+  , _fk_child  :: entity (TableEntity ChildT)+  } deriving (Generic, Database Sqlite)++verifiesForeignKey :: TestTree+verifiesForeignKey = testCase "verifySchema detects a plain foreign key" $+  withTestDb $ \conn -> do+    execute_ conn "create table fk_parent (parent_id int not null primary key)"+    execute_ conn "create table fk_child  (child_id int not null primary key, \+                  \child_parent_id int not null, \+                  \foreign key (child_parent_id) references fk_parent (parent_id))"+    let db :: CheckedDatabaseSettings Sqlite FkDb+        db = defaultMigratableDbSettings `withDbModification`+              (dbModification @_ @Sqlite)+                { _fk_child =+                    addTableForeignKey (_fk_parent db)+                      (foreignKeyColumns _child_parent_id)+                      primaryKeyColumns+                      ForeignKeyNoAction+                      ForeignKeyNoAction+                    <> modifyCheckedTable id+                         (ChildT { _child_id        = "child_id"+                                 , _child_parent_id = ParentPk "child_parent_id" }) }+    testVerifySchema conn db++-- Schema used to test solver behaviour when a foreign key migration is impossible.+-- Needs to be somewhat large to trigger the exponential blowup in search space.++data WideParentT f = WideParentT+  { _wp_id :: C f Int32+  , _wp_a  :: C f Int32+  , _wp_b  :: C f Int32+  , _wp_c  :: C f Int32+  , _wp_d  :: C f Int32+  } deriving (Generic, Beamable)++instance Table WideParentT where+  newtype PrimaryKey WideParentT f = WideParentPk (C f Int32)+    deriving (Generic, Beamable)+  primaryKey = WideParentPk . _wp_id++data WideChildT f = WideChildT+  { _wc_id        :: C f Int32+  , _wc_parent_id :: PrimaryKey WideParentT f+  , _wc_a         :: C f Int32+  , _wc_b         :: C f Int32+  , _wc_c         :: C f Int32+  , _wc_d         :: C f Int32+  } deriving (Generic, Beamable)++instance Table WideChildT where+  newtype PrimaryKey WideChildT f = WideChildPk (C f Int32)+    deriving (Generic, Beamable)+  primaryKey = WideChildPk . _wc_id++data WideFkDb entity = WideFkDb+  { _wide_parent :: entity (TableEntity WideParentT)+  , _wide_child  :: entity (TableEntity WideChildT)+  } deriving (Generic, Database Sqlite)++-- | Check that the solver doesn't grow an enormous search space trying to find+-- a way to add a foreign key constraint (which SQLite cannot do to an existing+-- table).+addingForeignKeyFails :: TestTree+addingForeignKeyFails =+  testCase "autoMigrate fails promptly when asked to add a foreign key" $+  withTestDb $ \conn -> do+    let noFk :: CheckedDatabaseSettings Sqlite WideFkDb+        noFk = defaultMigratableDbSettings+        withFk :: CheckedDatabaseSettings Sqlite WideFkDb+        withFk = noFk `withDbModification`+                   (dbModification @_ @Sqlite)+                     { _wide_child =+                         addTableForeignKey (_wide_parent withFk)+                           (foreignKeyColumns _wc_parent_id)+                           primaryKeyColumns+                           ForeignKeyNoAction+                           ForeignKeyNoAction }+    runBeamSqlite conn $ autoMigrate migrationBackend noFk+    result <- try @IOException (runBeamSqlite conn $ autoMigrate migrationBackend withFk)+    case result of+      Left e+        | "Could not determine migration" `isInfixOf` show e+        -> return ()+      Left e  -> assertFailure $ unlines+        [ "unexpected exception from autoMigrate:"+        , "  - " ++ show e+        ]+      Right _ -> assertFailure $ unlines+        [ "expected autoMigrate to fail:"+        , "  SQLite cannot add foreign key constraints to existing tables"+        ]++verifiesForeignKeyActions :: TestTree+verifiesForeignKeyActions =+  testCase "verifySchema detects a foreign key with ON DELETE & ON UPDATE actions" $+  withTestDb $ \conn -> do+    execute_ conn "create table fk_parent (parent_id int not null primary key)"+    execute_ conn "create table fk_child  (child_id int not null primary key, \+                  \child_parent_id int not null, \+                  \foreign key (child_parent_id) references fk_parent (parent_id) \+                  \on delete cascade on update restrict)"+    let db :: CheckedDatabaseSettings Sqlite FkDb+        db = defaultMigratableDbSettings `withDbModification`+              (dbModification @_ @Sqlite)+                { _fk_child =+                    addTableForeignKey (_fk_parent db)+                      (foreignKeyColumns _child_parent_id)+                      primaryKeyColumns+                      ForeignKeyActionRestrict+                      ForeignKeyActionCascade+                    <> modifyCheckedTable id+                         (ChildT { _child_id        = "child_id"+                                 , _child_parent_id = ParentPk "child_parent_id" }) }+    testVerifySchema conn db++-- | Verifies that foreign key actions are enforced at runtime.+foreignKeyActionsWork :: TestTree+foreignKeyActionsWork =+  testCase "foreign key actions" $+  withTestDb $ \conn -> do+    -- Enable foreign key support (required for SQLite)+    execute_ conn "PRAGMA foreign_keys = ON"+    let db :: CheckedDatabaseSettings Sqlite FkDb+        db = defaultMigratableDbSettings `withDbModification`+              (dbModification @_ @Sqlite)+                { _fk_child =+                    addTableForeignKey (_fk_parent db)+                      (foreignKeyColumns _child_parent_id)+                      primaryKeyColumns+                      ForeignKeyActionCascade+                      ForeignKeyActionCascade+                }+        unc = unCheckDatabase db+    runBeamSqlite conn $ autoMigrate migrationBackend db++    -- Insert two parents and three children (two for parent 1, one for parent 2).+    runBeamSqlite conn $ do+      runInsert $ insert (_fk_parent unc) $ insertValues+        [ ParentT 1, ParentT 2 ]+      runInsert $ insert (_fk_child unc) $ insertValues+        [ ChildT 1 (ParentPk 1), ChildT 2 (ParentPk 1), ChildT 3 (ParentPk 2) ]++    -- ON UPDATE CASCADE: changing parent_id 1 → 10 should cascade to child rows.+    runBeamSqlite conn $+      runUpdate $ update (_fk_parent unc)+        (\p -> _parent_id p <-. val_ 10)+        (\p -> _parent_id p ==. val_ 1)+    childrenOf10 <- runBeamSqlite conn $ runSelectReturningList $ select $+      filter_ (\c -> let ParentPk pid = _child_parent_id c in pid ==. val_ 10) $ all_ (_fk_child unc)+    assertEqual "two children should now reference updated parent id 10"+      2 (length childrenOf10)+    childrenOf1 <- runBeamSqlite conn $ runSelectReturningList $ select $+      filter_ (\c -> let ParentPk pid = _child_parent_id c in pid ==. val_ 1) $ all_ (_fk_child unc)+    assertEqual "no children should still reference old parent id 1"+      0 (length childrenOf1)++    -- ON DELETE CASCADE: deleting parent 2 should remove its child row.+    runBeamSqlite conn $+      runDelete $ delete (_fk_parent unc) (\p -> _parent_id p ==. val_ 2)+    childrenOf2 <- runBeamSqlite conn $ runSelectReturningList $ select $+      filter_ (\c -> let ParentPk pid = _child_parent_id c in pid ==. val_ 2) $ all_ (_fk_child unc)+    assertEqual "child of deleted parent 2 should be removed"+      0 (length childrenOf2)+    allChildren <- runBeamSqlite conn $ runSelectReturningList $ select $+      all_ (_fk_child unc)+    assertEqual "only the two children of parent 10 should remain"+      2 (length allChildren)++--------------------------------------------------------------------------------+-- Ensure migration is idempotent (no data loss due to internal tables)++data SimpleT f = SimpleT+  { _auto_incr_id :: C f (SqlSerial Int32)+  } deriving (Generic, Beamable)++instance Table SimpleT where+  newtype PrimaryKey SimpleT f = AutoIncrPk (C f (SqlSerial Int32))+    deriving (Generic, Beamable)+  primaryKey = AutoIncrPk . _auto_incr_id++data SimpleDb entity = SimpleDb+  { _simple_tbl :: entity (TableEntity SimpleT)+  } deriving (Generic, Database Sqlite)++simpleCheckedDb :: CheckedDatabaseSettings Sqlite SimpleDb+simpleCheckedDb = defaultMigratableDbSettings++idempotentMigration :: TestTree+idempotentMigration =+  testCase "autoMigrate is idempotent" $+  withTestDb $ \conn -> do+    runBeamSqlite conn $ autoMigrate migrationBackend simpleCheckedDb+    -- Insert a row so that sqlite_sequence is populated in sqlite_master.+    execute_ conn "INSERT INTO simple_tbl DEFAULT VALUES"+    -- Make sure we don't think a migration would lose data due to the+    -- presence of the sqlite_sequence table.+    runBeamSqlite conn $ autoMigrate migrationBackend simpleCheckedDb++--------------------------------------------------------------------------------
test/Database/Beam/Sqlite/Test/Returning.hs view
@@ -15,7 +15,7 @@  import Database.Beam import Database.Beam.Backend.SQL.BeamExtensions-  (conflictingFields, onConflictUpdateSet, onConflictUpdateSetWhere)+  (conflictingFields, onConflictUpdateSet, onConflictUpdateSetWhere, runUpdateReturningList) import Database.Beam.Migrate (defaultMigratableDbSettings) import Database.Beam.Migrate.Simple (CheckedDatabaseSettings, autoMigrate) import Database.Beam.Sqlite (Sqlite, runBeamSqlite, insertOnConflictReturning)@@ -26,7 +26,7 @@     , insertReturning     , runDeleteReturningList     , runInsertReturningList-    , runUpdateReturningList+    , runSqliteUpdateReturningList     , updateReturning     ) @@ -38,6 +38,7 @@     testGroup         "SQLite RETURNING statement tests"         [ testInsertOnConflictReturning+        , testSqliteUpdateReturning         , testUpdateReturning         , testDeleteReturning         ]@@ -124,9 +125,9 @@       , (3, "upserted_user3",  33)       ] --- | Test @UPDATE ... RETURNING@-testUpdateReturning :: TestTree-testUpdateReturning = testCase "UPDATE .. RETURNING" $+-- | Test @UPDATE ... RETURNING@ using the Sqlite-specific runSqliteUpdateReturningList+testSqliteUpdateReturning :: TestTree+testSqliteUpdateReturning = testCase "UPDATE .. RETURNING" $   withTestDb $ \conn -> do     updatedUsers <-       runBeamSqlite conn $ do@@ -142,7 +143,7 @@               ]          -- Update user 2 and return projected columns from the updated row-        runUpdateReturningList $+        runSqliteUpdateReturningList $           updateReturning             (usersTable testDb)             (\u -> userName u <-. val_ "updated_user2")@@ -150,6 +151,33 @@             (\u -> (userId u, userName u, userInfo2 u))      updatedUsers @?= [(2, "updated_user2", 22)]++-- | Test @UPDATE ... RETURNING@ using MonadBeamUpdateReturning's runUpdateReturningList+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 $+          update+            (usersTable testDb)+            (\u -> userName u <-. val_ "updated_user2")+            (\u -> userId u ==. val_ 2)+++    fmap (\u -> (userId u, userName u, userInfo2 u)) updatedUsers @?= [(2, "updated_user2", 22)]  -- | Test @DELETE .. RETURNING@ testDeleteReturning :: TestTree
+ test/Database/Beam/Sqlite/Test/TempTable.hs view
@@ -0,0 +1,85 @@+{-# LANGUAGE OverloadedStrings #-}++module Database.Beam.Sqlite.Test.TempTable (tests) where++import Data.Int (Int32)+import Data.Kind (Type)+import GHC.Exts (Any)++import Database.Beam+import Database.Beam.Sqlite+import Database.SQLite.Simple (execute_)++import Test.Tasty+import Test.Tasty.HUnit++import Database.Beam.Sqlite.Test++tests :: TestTree+tests = testGroup "Temporary table tests"+  [ testStageFilteredRows+  , testDropAndCreate+  ]++-- | A permanent table of items.+data ItemT f = Item+  { itemId    :: C f Int32+  , itemValue :: C f Int32+  } deriving (Generic, Beamable)++deriving instance Show (ItemT Identity)+deriving instance Eq   (ItemT Identity)++instance Table ItemT where+  data PrimaryKey ItemT f = ItemKey (C f Int32)+    deriving (Generic, Beamable)+  primaryKey = ItemKey . itemId++data ItemDb entity = ItemDb+  { dbItems :: entity (TableEntity ItemT)+  } deriving (Generic, Database Sqlite)++itemDb :: DatabaseSettings be ItemDb+itemDb = defaultDbSettings++testStageFilteredRows :: TestTree+testStageFilteredRows = testCase "sanity test" $+  withTestDb $ \conn -> do+    execute_ conn "CREATE TABLE items (id INT PRIMARY KEY, value INT NOT NULL)"+    result <- runBeamSqlite conn $ do+      runInsert $ insert (dbItems itemDb) $ insertValues+        [ Item 1 5, Item 2 10, Item 3 15, Item 4 20, Item 5 25 ]+      scratch <- runCreateTempTable @_ @Any @ItemT+                   defaultTempTableOptions "high_value_items"+      runInsert $ insert scratch $ insertFrom $+        filter_ (\r -> itemValue r >. val_ 15) $+        all_ (dbItems itemDb)+      runSelectReturningList $ select $+        orderBy_ (asc_ . itemId) $ all_ scratch+    assertEqual "high-value items staged" [Item 4 20, Item 5 25] result++-- | 'DropAndCreate' resets the temp table so a second pass populates a+-- clean table, with no leftovers from the first pass.+testDropAndCreate :: TestTree+testDropAndCreate = testCase "drop-and-create" $+  withTestDb $ \conn -> do+    execute_ conn "CREATE TABLE items (id INT PRIMARY KEY, value INT NOT NULL)"+    result <- runBeamSqlite conn $ do+      runInsert $ insert (dbItems itemDb) $ insertValues+        [ Item 1 5, Item 2 10, Item 3 15, Item 4 20, Item 5 25 ]+      -- First pass: stage low-value items.+      scratch <- runCreateTempTable @_ @Any @ItemT+                   defaultTempTableOptions "scratch"+      runInsert $ insert scratch $ insertFrom $+        filter_ (\r -> itemValue r <=. val_ 15) $+        all_ (dbItems itemDb)+      -- Second pass: drop and recreate, then stage high-value items only.+      scratch' <- runCreateTempTable @_ @Any @ItemT+                    (defaultTempTableOptions { tempTableCreateMode = DropAndCreate })+                    "scratch"+      runInsert $ insert scratch' $ insertFrom $+        filter_ (\r -> itemValue r >. val_ 15) $+        all_ (dbItems itemDb)+      runSelectReturningList $ select $+        orderBy_ (asc_ . itemId) $ all_ scratch'+    assertEqual "only high-value items after reset" [Item 4 20, Item 5 25] result
test/Main.hs view
@@ -7,6 +7,7 @@ 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+import qualified Database.Beam.Sqlite.Test.TempTable as TempTable  main :: IO () main = defaultMain $ testGroup "beam-sqlite tests"@@ -15,4 +16,5 @@       , Insert.tests       , InsertOnConflictReturning.tests       , Returning.tests+      , TempTable.tests       ]