diff --git a/Database/SQLite3.hs b/Database/SQLite3.hs
--- a/Database/SQLite3.hs
+++ b/Database/SQLite3.hs
@@ -1,10 +1,11 @@
 {-# LANGUAGE BangPatterns #-}
-{-# LANGUAGE CPP #-}
 {-# LANGUAGE DeriveDataTypeable #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE DeriveGeneric #-}
 module Database.SQLite3 (
     -- * Connection management
     open,
+    open2,
     close,
 
     -- * Simple query execution
@@ -111,6 +112,8 @@
     Database,
     Statement,
     SQLData(..),
+    SQLOpenFlag(..),
+    SQLVFS(..),
     SQLError(..),
     ColumnType(..),
     FuncContext,
@@ -188,12 +191,14 @@
 import Control.Monad        (when, zipWithM, zipWithM_)
 import Data.ByteString      (ByteString)
 import Data.Int             (Int64)
+import Data.Bits            ((.|.))
 import Data.Maybe           (fromMaybe)
 import Data.Text            (Text)
 import Data.Text.Encoding   (encodeUtf8, decodeUtf8With)
 import Data.Text.Encoding.Error (UnicodeException(..), lenientDecode)
 import Data.Typeable
 import Foreign.Ptr          (Ptr)
+import GHC.Generics
 
 data SQLData
     = SQLInteger    !Int64
@@ -201,8 +206,45 @@
     | SQLText       !Text
     | SQLBlob       !ByteString
     | SQLNull
-    deriving (Eq, Show, Typeable)
+    deriving (Eq, Show, Typeable, Generic)
 
+-- | These flags are used when using the `open2` function.
+-- <https://www.sqlite.org/c3ref/c_open_autoproxy.html>
+data SQLOpenFlag
+    = SQLOpenReadOnly      -- Ok for sqlite3_open_v2()
+    | SQLOpenReadWrite     -- Ok for sqlite3_open_v2()
+    | SQLOpenCreate        -- Ok for sqlite3_open_v2()
+    | SQLOpenDeleteOnClose -- VFS only
+    | SQLOpenExclusive     -- VFS only
+    | SQLOpenAutoProxy     -- VFS only
+    | SQLOpenURI           -- Ok for sqlite3_open_v2()
+    | SQLOpenMemory        -- Ok for sqlite3_open_v2()
+    | SQLOpenMainDB        -- VFS only
+    | SQLOpenTempDB        -- VFS only
+    | SQLOpenTransientDB   -- VFS only
+    | SQLOpenMainJournal   -- VFS only
+    | SQLOpenTempJournal   -- VFS only
+    | SQLOpenSubJournal    -- VFS only
+    | SQLOpenMasterJournal -- VFS only
+    | SQLOpenNoMutex       -- Ok for sqlite3_open_v2()
+    | SQLOpenFullMutex     -- Ok for sqlite3_open_v2()
+    | SQLOpenSharedCache   -- Ok for sqlite3_open_v2()
+    | SQLOpenPrivateCache  -- Ok for sqlite3_open_v2()
+    | SQLOpenWAL           -- VFS only
+    | SQLOpenNoFollow      -- Ok for sqlite3_open_v2()
+    deriving (Eq, Show)
+
+-- | These VFS names are used when using the `open2` function.
+data SQLVFS
+    = SQLVFSDefault
+    | SQLVFSUnix
+    | SQLVFSUnixDotFile
+    | SQLVFSUnixExcl
+    | SQLVFSUnixNone
+    | SQLVFSUnixNamedSem
+    | SQLVFSCustom Text
+    deriving (Eq, Show)
+
 -- | Exception thrown when SQLite3 reports an error.
 --
 -- direct-sqlite may throw other types of exceptions if you misuse the API.
@@ -215,7 +257,7 @@
         -- ^ Indicates what action produced this error,
         --   e.g. @exec \"SELECT * FROM foo\"@
     }
-    deriving (Eq, Typeable)
+    deriving (Eq, Typeable, Generic)
 
 -- NB: SQLError is lazy in 'sqlErrorDetails' and 'sqlErrorContext',
 -- to defer message construction in the case where a user catches and
@@ -290,6 +332,46 @@
     Direct.open (toUtf8 path)
         >>= checkErrorMsg ("open " `appendShow` path)
 
+-- | <https://www.sqlite.org/c3ref/open.html>
+open2 :: Text -> [SQLOpenFlag] -> SQLVFS -> IO Database
+open2 path flags zvfs =
+    Direct.open2 (toUtf8 path) (makeFlag flags) (toMUtf8 zvfs)
+        >>= checkErrorMsg ("open2 " `appendShow` path)
+    where
+        toMUtf8 = fmap toUtf8 . toMText
+
+        toMText SQLVFSDefault           = Nothing
+        toMText SQLVFSUnix              = Just "unix"
+        toMText SQLVFSUnixDotFile       = Just "unix-dotfile"
+        toMText SQLVFSUnixExcl          = Just "unix-excl"
+        toMText SQLVFSUnixNone          = Just "unix-none"
+        toMText SQLVFSUnixNamedSem      = Just "unix-namedsem"
+        toMText (SQLVFSCustom custom)   = Just custom
+
+        makeFlag = foldr (.|.) 0 . fmap toNum
+
+        toNum SQLOpenReadOnly       = 0x00000001
+        toNum SQLOpenReadWrite      = 0x00000002
+        toNum SQLOpenCreate         = 0x00000004
+        toNum SQLOpenDeleteOnClose  = 0x00000008
+        toNum SQLOpenExclusive      = 0x00000010
+        toNum SQLOpenAutoProxy      = 0x00000020
+        toNum SQLOpenURI            = 0x00000040
+        toNum SQLOpenMemory         = 0x00000080
+        toNum SQLOpenMainDB         = 0x00000100
+        toNum SQLOpenTempDB         = 0x00000200
+        toNum SQLOpenTransientDB    = 0x00000400
+        toNum SQLOpenMainJournal    = 0x00000800
+        toNum SQLOpenTempJournal    = 0x00001000
+        toNum SQLOpenSubJournal     = 0x00002000
+        toNum SQLOpenMasterJournal  = 0x00004000
+        toNum SQLOpenNoMutex        = 0x00008000
+        toNum SQLOpenFullMutex      = 0x00010000
+        toNum SQLOpenSharedCache    = 0x00020000
+        toNum SQLOpenPrivateCache   = 0x00040000
+        toNum SQLOpenWAL            = 0x00080000
+        toNum SQLOpenNoFollow       = 0x01000000
+
 -- | <https://www.sqlite.org/c3ref/close.html>
 close :: Database -> IO ()
 close db =
@@ -302,7 +384,6 @@
 -- It works by running the callback in a forked thread.  If interrupted,
 -- it uses 'interrupt' to try to stop the operation.
 interruptibly :: Database -> IO a -> IO a
-#if MIN_VERSION_base(4,3,0)
 interruptibly db io
   | rtsSupportsBoundThreads =
       mask $ \restore -> do
@@ -335,9 +416,6 @@
   where
     try' :: IO a -> IO (Either SomeException a)
     try' = try
-#else
-interruptibly _db io = io
-#endif
 
 -- | Execute zero or more SQL statements delimited by semicolons.
 exec :: Database -> Text -> IO ()
diff --git a/Database/SQLite3/Bindings.hs b/Database/SQLite3/Bindings.hs
--- a/Database/SQLite3/Bindings.hs
+++ b/Database/SQLite3/Bindings.hs
@@ -4,8 +4,10 @@
 
     -- * Connection management
     c_sqlite3_open,
+    c_sqlite3_open_v2,
     c_sqlite3_close,
     c_sqlite3_errcode,
+    c_sqlite3_extended_errcode,
     c_sqlite3_errmsg,
     c_sqlite3_interrupt,
     c_sqlite3_trace,
@@ -138,6 +140,16 @@
 foreign import ccall "sqlite3_open"
     c_sqlite3_open :: CString -> Ptr (Ptr CDatabase) -> IO CError
 
+-- | <https://www.sqlite.org/c3ref/open.html>
+--
+-- This sets the @'Ptr CDatabase'@ even on failure.
+foreign import ccall "sqlite3_open_v2"
+    c_sqlite3_open_v2 :: CString
+                      -> Ptr (Ptr CDatabase)
+                      -> CInt
+                      -> CString
+                      -> IO CError
+
 -- | <https://www.sqlite.org/c3ref/close.html>
 foreign import ccall "sqlite3_close"
     c_sqlite3_close :: Ptr CDatabase -> IO CError
@@ -145,6 +157,10 @@
 -- | <https://www.sqlite.org/c3ref/errcode.html>
 foreign import ccall unsafe "sqlite3_errcode"
     c_sqlite3_errcode :: Ptr CDatabase -> IO CError
+
+-- | <https://www.sqlite.org/c3ref/errcode.html>
+foreign import ccall unsafe "sqlite3_extended_errcode"
+    c_sqlite3_extended_errcode :: Ptr CDatabase -> IO CError
 
 -- | <https://www.sqlite.org/c3ref/errcode.html>
 foreign import ccall unsafe "sqlite3_errmsg"
diff --git a/Database/SQLite3/Bindings/Types.hsc b/Database/SQLite3/Bindings/Types.hsc
--- a/Database/SQLite3/Bindings/Types.hsc
+++ b/Database/SQLite3/Bindings/Types.hsc
@@ -2,6 +2,7 @@
 {-# LANGUAGE FunctionalDependencies #-}
 {-# LANGUAGE MultiParamTypeClasses #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DeriveGeneric #-}
 module Database.SQLite3.Bindings.Types (
     -- * Objects
     -- | <https://www.sqlite.org/c3ref/objlist.html>
@@ -59,12 +60,14 @@
 #include "cbits/sqlite3.h"
 #endif
 
+import GHC.Generics
 import Foreign.C.Types
 import Foreign.Ptr
 
 -- Result code documentation copied from <https://www.sqlite.org/c3ref/c_abort.html>
 
 -- | <https://www.sqlite.org/c3ref/c_abort.html>
+-- <https://www.sqlite.org/c3ref/c_abort_rollback.html>
 data Error = ErrorOK                     -- ^ Successful result
            | ErrorError                  -- ^ SQL error or missing database
            | ErrorInternal               -- ^ Internal logic error in SQLite
@@ -96,8 +99,83 @@
            | ErrorWarning                -- ^ Warnings from sqlite3_log()
            | ErrorRow                    -- ^ @sqlite3_step()@ has another row ready
            | ErrorDone                   -- ^ @sqlite3_step()@ has finished executing
-             deriving (Eq, Show)
 
+           | ErrorErrorMissingCollatingSquence
+           | ErrorErrorRetry
+           | ErrorErrorSnapshot
+           | ErrorIORead
+           | ErrorIOShortRead
+           | ErrorIOWrite
+           | ErrorIOFsync
+           | ErrorIODirectoryFsync
+           | ErrorIOTruncate
+           | ErrorIOFstat
+           | ErrorIOUnlock
+           | ErrorIOReadLock
+           | ErrorIOBlocked
+           | ErrorIODelete
+           | ErrorIONoMemory
+           | ErrorIOAccess
+           | ErrorIOCheckReservedLock
+           | ErrorIOLock
+           | ErrorIOClose
+           | ErrorIODirectoryClose
+           | ErrorIOShmOpen
+           | ErrorIOShmSize
+           | ErrorIOShmLock
+           | ErrorIOShmMap
+           | ErrorIOSeek
+           | ErrorIODeleteNoEntity
+           | ErrorIOMmap
+           | ErrorIOGetTempPath
+           | ErrorIOConvertedPath
+           | ErrorIOVNode
+           | ErrorIOAuth
+           | ErrorIOBeginAtomic
+           | ErrorIOCommitAtomic
+           | ErrorIORollbackAtomic
+           | ErrorIOData
+           | ErrorIOCorruptFilesystem
+           | ErrorLockedSharedCache
+           | ErrorLockedVirtualTable
+           | ErrorBusyRecovery
+           | ErrorBusySnapshot
+           | ErrorBusyTimeout
+           | ErrorCan'tOpenNotTempDirectory
+           | ErrorCan'tOpenIsDirectory
+           | ErrorCan'tOpenFullPath
+           | ErrorCan'tOpenConvertedPath
+           | ErrorCan'tOpenDirtyWriteAheadLog
+           | ErrorCan'tOpenSymlink
+           | ErrorCorruptVirtualTable
+           | ErrorCorruptSequence
+           | ErrorCorruptIndex
+           | ErrorReadOnlyRecovery
+           | ErrorReadOnlyCan'tLock
+           | ErrorReadOnlyRollback
+           | ErrorReadOnlyDatabaseMoved
+           | ErrorReadOnlyCan'tInit
+           | ErrorReadOnlyDirectory
+           | ErrorAbortRollback
+           | ErrorConstraintCheck
+           | ErrorConstraintCommitHook
+           | ErrorConstraintForeignKey
+           | ErrorConstraintFunction
+           | ErrorConstraintNotNull
+           | ErrorConstraintPrimaryKey
+           | ErrorConstraintTrigger
+           | ErrorConstraintUnique
+           | ErrorConstraintVirtualTable
+           | ErrorConstraintRowId
+           | ErrorConstraintPinned
+           | ErrorConstraintDataType
+           | ErrorNoticeRecoverWriteAheadLog
+           | ErrorNoticeRecoverRollback
+           | ErrorWarningAutoIndex
+           | ErrorAuthUser
+           | ErrorOkLoadPermanently
+             deriving (Eq, Show, Generic)
+
 -- | <https://www.sqlite.org/c3ref/c_blob.html>
 data ColumnType = IntegerColumn
                 | FloatColumn
@@ -293,6 +371,81 @@
     #{const SQLITE_WARNING}    -> ErrorWarning
     #{const SQLITE_ROW}        -> ErrorRow
     #{const SQLITE_DONE}       -> ErrorDone
+    -- extended result codes
+    #{const SQLITE_ERROR_MISSING_COLLSEQ}   -> ErrorErrorMissingCollatingSquence
+    #{const SQLITE_ERROR_RETRY}             -> ErrorErrorRetry
+    #{const SQLITE_ERROR_SNAPSHOT}          -> ErrorErrorSnapshot
+    #{const SQLITE_IOERR_READ}              -> ErrorIORead
+    #{const SQLITE_IOERR_SHORT_READ}        -> ErrorIOShortRead
+    #{const SQLITE_IOERR_WRITE}             -> ErrorIOWrite
+    #{const SQLITE_IOERR_FSYNC}             -> ErrorIOFsync
+    #{const SQLITE_IOERR_DIR_FSYNC}         -> ErrorIODirectoryFsync
+    #{const SQLITE_IOERR_TRUNCATE}          -> ErrorIOTruncate
+    #{const SQLITE_IOERR_FSTAT}             -> ErrorIOFstat
+    #{const SQLITE_IOERR_UNLOCK}            -> ErrorIOUnlock
+    #{const SQLITE_IOERR_RDLOCK}            -> ErrorIOReadLock
+    #{const SQLITE_IOERR_DELETE}            -> ErrorIODelete
+    #{const SQLITE_IOERR_BLOCKED}           -> ErrorIOBlocked
+    #{const SQLITE_IOERR_NOMEM}             -> ErrorIONoMemory
+    #{const SQLITE_IOERR_ACCESS}            -> ErrorIOAccess
+    #{const SQLITE_IOERR_CHECKRESERVEDLOCK} -> ErrorIOCheckReservedLock
+    #{const SQLITE_IOERR_LOCK}              -> ErrorIOLock
+    #{const SQLITE_IOERR_CLOSE}             -> ErrorIOClose
+    #{const SQLITE_IOERR_DIR_CLOSE}         -> ErrorIODirectoryClose
+    #{const SQLITE_IOERR_SHMOPEN}           -> ErrorIOShmOpen
+    #{const SQLITE_IOERR_SHMSIZE}           -> ErrorIOShmSize
+    #{const SQLITE_IOERR_SHMLOCK}           -> ErrorIOShmLock
+    #{const SQLITE_IOERR_SHMMAP}            -> ErrorIOShmMap
+    #{const SQLITE_IOERR_SEEK}              -> ErrorIOSeek
+    #{const SQLITE_IOERR_DELETE_NOENT}      -> ErrorIODeleteNoEntity
+    #{const SQLITE_IOERR_MMAP}              -> ErrorIOMmap
+    #{const SQLITE_IOERR_GETTEMPPATH}       -> ErrorIOGetTempPath
+    #{const SQLITE_IOERR_CONVPATH}          -> ErrorIOConvertedPath
+    #{const SQLITE_IOERR_VNODE}             -> ErrorIOVNode
+    #{const SQLITE_IOERR_AUTH}              -> ErrorIOAuth
+    #{const SQLITE_IOERR_BEGIN_ATOMIC}      -> ErrorIOBeginAtomic
+    #{const SQLITE_IOERR_COMMIT_ATOMIC}     -> ErrorIOCommitAtomic
+    #{const SQLITE_IOERR_ROLLBACK_ATOMIC}   -> ErrorIORollbackAtomic
+    #{const SQLITE_IOERR_DATA}              -> ErrorIOData
+    #{const SQLITE_IOERR_CORRUPTFS}         -> ErrorIOCorruptFilesystem
+    #{const SQLITE_LOCKED_SHAREDCACHE}      -> ErrorLockedSharedCache
+    #{const SQLITE_LOCKED_VTAB}             -> ErrorLockedVirtualTable
+    #{const SQLITE_BUSY_RECOVERY}           -> ErrorBusyRecovery
+    #{const SQLITE_BUSY_SNAPSHOT}           -> ErrorBusySnapshot
+    #{const SQLITE_BUSY_TIMEOUT}            -> ErrorBusyTimeout
+    #{const SQLITE_CANTOPEN_NOTEMPDIR}      -> ErrorCan'tOpenNotTempDirectory
+    #{const SQLITE_CANTOPEN_ISDIR}          -> ErrorCan'tOpenIsDirectory
+    #{const SQLITE_CANTOPEN_FULLPATH}       -> ErrorCan'tOpenFullPath
+    #{const SQLITE_CANTOPEN_CONVPATH}       -> ErrorCan'tOpenConvertedPath
+    #{const SQLITE_CANTOPEN_DIRTYWAL}       -> ErrorCan'tOpenDirtyWriteAheadLog
+    #{const SQLITE_CANTOPEN_SYMLINK}        -> ErrorCan'tOpenSymlink
+    #{const SQLITE_CORRUPT_VTAB}            -> ErrorCorruptVirtualTable
+    #{const SQLITE_CORRUPT_SEQUENCE}        -> ErrorCorruptSequence
+    #{const SQLITE_CORRUPT_INDEX}           -> ErrorCorruptIndex
+    #{const SQLITE_READONLY_RECOVERY}       -> ErrorReadOnlyRecovery
+    #{const SQLITE_READONLY_CANTLOCK}       -> ErrorReadOnlyCan'tLock
+    #{const SQLITE_READONLY_ROLLBACK}       -> ErrorReadOnlyRollback
+    #{const SQLITE_READONLY_DBMOVED}        -> ErrorReadOnlyDatabaseMoved
+    #{const SQLITE_READONLY_CANTINIT}       -> ErrorReadOnlyCan'tInit
+    #{const SQLITE_READONLY_DIRECTORY}      -> ErrorReadOnlyDirectory
+    #{const SQLITE_ABORT_ROLLBACK}          -> ErrorAbortRollback
+    #{const SQLITE_CONSTRAINT_CHECK}        -> ErrorConstraintCheck
+    #{const SQLITE_CONSTRAINT_COMMITHOOK}   -> ErrorConstraintCommitHook
+    #{const SQLITE_CONSTRAINT_FOREIGNKEY}   -> ErrorConstraintForeignKey
+    #{const SQLITE_CONSTRAINT_FUNCTION}     -> ErrorConstraintFunction
+    #{const SQLITE_CONSTRAINT_NOTNULL}      -> ErrorConstraintNotNull
+    #{const SQLITE_CONSTRAINT_PRIMARYKEY}   -> ErrorConstraintPrimaryKey
+    #{const SQLITE_CONSTRAINT_TRIGGER}      -> ErrorConstraintTrigger
+    #{const SQLITE_CONSTRAINT_UNIQUE}       -> ErrorConstraintUnique
+    #{const SQLITE_CONSTRAINT_VTAB}         -> ErrorConstraintVirtualTable
+    #{const SQLITE_CONSTRAINT_ROWID}        -> ErrorConstraintRowId
+    #{const SQLITE_CONSTRAINT_PINNED}       -> ErrorConstraintPinned
+    #{const SQLITE_CONSTRAINT_DATATYPE}     -> ErrorConstraintDataType
+    #{const SQLITE_NOTICE_RECOVER_WAL}      -> ErrorNoticeRecoverWriteAheadLog
+    #{const SQLITE_NOTICE_RECOVER_ROLLBACK} -> ErrorNoticeRecoverRollback
+    #{const SQLITE_WARNING_AUTOINDEX}       -> ErrorWarningAutoIndex
+    #{const SQLITE_AUTH_USER}               -> ErrorAuthUser
+    #{const SQLITE_OK_LOAD_PERMANENTLY}     -> ErrorOkLoadPermanently
     _                          -> error $ "decodeError " ++ show n
 
 encodeError :: Error -> CError
@@ -328,6 +481,81 @@
     ErrorWarning            -> #const SQLITE_WARNING
     ErrorRow                -> #const SQLITE_ROW
     ErrorDone               -> #const SQLITE_DONE
+
+    ErrorErrorMissingCollatingSquence  -> #const SQLITE_ERROR_MISSING_COLLSEQ
+    ErrorErrorRetry                    -> #const SQLITE_ERROR_RETRY
+    ErrorErrorSnapshot                 -> #const SQLITE_ERROR_SNAPSHOT
+    ErrorIORead                        -> #const SQLITE_IOERR_READ
+    ErrorIOShortRead                   -> #const SQLITE_IOERR_SHORT_READ
+    ErrorIOWrite                       -> #const SQLITE_IOERR_WRITE
+    ErrorIOFsync                       -> #const SQLITE_IOERR_FSYNC
+    ErrorIODirectoryFsync              -> #const SQLITE_IOERR_DIR_FSYNC
+    ErrorIOTruncate                    -> #const SQLITE_IOERR_TRUNCATE
+    ErrorIOFstat                       -> #const SQLITE_IOERR_FSTAT
+    ErrorIOUnlock                      -> #const SQLITE_IOERR_UNLOCK
+    ErrorIOReadLock                    -> #const SQLITE_IOERR_RDLOCK
+    ErrorIOBlocked                     -> #const SQLITE_IOERR_BLOCKED
+    ErrorIODelete                      -> #const SQLITE_IOERR_DELETE
+    ErrorIONoMemory                    -> #const SQLITE_IOERR_NOMEM
+    ErrorIOAccess                      -> #const SQLITE_IOERR_ACCESS
+    ErrorIOCheckReservedLock           -> #const SQLITE_IOERR_CHECKRESERVEDLOCK
+    ErrorIOLock                        -> #const SQLITE_IOERR_LOCK
+    ErrorIOClose                       -> #const SQLITE_IOERR_CLOSE
+    ErrorIODirectoryClose              -> #const SQLITE_IOERR_DIR_CLOSE
+    ErrorIOShmOpen                     -> #const SQLITE_IOERR_SHMOPEN
+    ErrorIOShmSize                     -> #const SQLITE_IOERR_SHMSIZE
+    ErrorIOShmLock                     -> #const SQLITE_IOERR_SHMLOCK
+    ErrorIOShmMap                      -> #const SQLITE_IOERR_SHMMAP
+    ErrorIOSeek                        -> #const SQLITE_IOERR_SEEK
+    ErrorIODeleteNoEntity              -> #const SQLITE_IOERR_DELETE_NOENT
+    ErrorIOMmap                        -> #const SQLITE_IOERR_MMAP
+    ErrorIOGetTempPath                 -> #const SQLITE_IOERR_GETTEMPPATH
+    ErrorIOConvertedPath               -> #const SQLITE_IOERR_CONVPATH
+    ErrorIOVNode                       -> #const SQLITE_IOERR_VNODE
+    ErrorIOAuth                        -> #const SQLITE_IOERR_AUTH
+    ErrorIOBeginAtomic                 -> #const SQLITE_IOERR_BEGIN_ATOMIC
+    ErrorIOCommitAtomic                -> #const SQLITE_IOERR_COMMIT_ATOMIC
+    ErrorIORollbackAtomic              -> #const SQLITE_IOERR_ROLLBACK_ATOMIC
+    ErrorIOData                        -> #const SQLITE_IOERR_DATA
+    ErrorIOCorruptFilesystem           -> #const SQLITE_IOERR_CORRUPTFS
+    ErrorLockedSharedCache             -> #const SQLITE_LOCKED_SHAREDCACHE
+    ErrorLockedVirtualTable            -> #const SQLITE_LOCKED_VTAB
+    ErrorBusyRecovery                  -> #const SQLITE_BUSY_RECOVERY
+    ErrorBusySnapshot                  -> #const SQLITE_BUSY_SNAPSHOT
+    ErrorBusyTimeout                   -> #const SQLITE_BUSY_TIMEOUT
+    ErrorCan'tOpenNotTempDirectory     -> #const SQLITE_CANTOPEN_NOTEMPDIR
+    ErrorCan'tOpenIsDirectory          -> #const SQLITE_CANTOPEN_ISDIR
+    ErrorCan'tOpenFullPath             -> #const SQLITE_CANTOPEN_FULLPATH
+    ErrorCan'tOpenConvertedPath        -> #const SQLITE_CANTOPEN_CONVPATH
+    ErrorCan'tOpenDirtyWriteAheadLog   -> #const SQLITE_CANTOPEN_DIRTYWAL
+    ErrorCan'tOpenSymlink              -> #const SQLITE_CANTOPEN_SYMLINK
+    ErrorCorruptVirtualTable           -> #const SQLITE_CORRUPT_VTAB
+    ErrorCorruptSequence               -> #const SQLITE_CORRUPT_SEQUENCE
+    ErrorCorruptIndex                  -> #const SQLITE_CORRUPT_INDEX
+    ErrorReadOnlyRecovery              -> #const SQLITE_READONLY_RECOVERY
+    ErrorReadOnlyCan'tLock             -> #const SQLITE_READONLY_CANTLOCK
+    ErrorReadOnlyRollback              -> #const SQLITE_READONLY_ROLLBACK
+    ErrorReadOnlyDatabaseMoved         -> #const SQLITE_READONLY_DBMOVED
+    ErrorReadOnlyCan'tInit             -> #const SQLITE_READONLY_CANTINIT
+    ErrorReadOnlyDirectory             -> #const SQLITE_READONLY_DIRECTORY
+    ErrorAbortRollback                 -> #const SQLITE_ABORT_ROLLBACK
+    ErrorConstraintCheck               -> #const SQLITE_CONSTRAINT_CHECK
+    ErrorConstraintCommitHook          -> #const SQLITE_CONSTRAINT_COMMITHOOK
+    ErrorConstraintForeignKey          -> #const SQLITE_CONSTRAINT_FOREIGNKEY
+    ErrorConstraintFunction            -> #const SQLITE_CONSTRAINT_FUNCTION
+    ErrorConstraintNotNull             -> #const SQLITE_CONSTRAINT_NOTNULL
+    ErrorConstraintPrimaryKey          -> #const SQLITE_CONSTRAINT_PRIMARYKEY
+    ErrorConstraintTrigger             -> #const SQLITE_CONSTRAINT_TRIGGER
+    ErrorConstraintUnique              -> #const SQLITE_CONSTRAINT_UNIQUE
+    ErrorConstraintVirtualTable        -> #const SQLITE_CONSTRAINT_VTAB
+    ErrorConstraintRowId               -> #const SQLITE_CONSTRAINT_ROWID
+    ErrorConstraintPinned              -> #const SQLITE_CONSTRAINT_PINNED
+    ErrorConstraintDataType            -> #const SQLITE_CONSTRAINT_DATATYPE
+    ErrorNoticeRecoverWriteAheadLog    -> #const SQLITE_NOTICE_RECOVER_WAL
+    ErrorNoticeRecoverRollback         -> #const SQLITE_NOTICE_RECOVER_ROLLBACK
+    ErrorWarningAutoIndex              -> #const SQLITE_WARNING_AUTOINDEX
+    ErrorAuthUser                      -> #const SQLITE_AUTH_USER
+    ErrorOkLoadPermanently             -> #const SQLITE_OK_LOAD_PERMANENTLY
 
 -- | <https://www.sqlite.org/c3ref/c_blob.html>
 newtype CColumnType = CColumnType CInt
diff --git a/Database/SQLite3/Direct.hs b/Database/SQLite3/Direct.hs
--- a/Database/SQLite3/Direct.hs
+++ b/Database/SQLite3/Direct.hs
@@ -1,5 +1,4 @@
 {-# LANGUAGE BangPatterns               #-}
-{-# LANGUAGE CPP                        #-}
 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
 {-# LANGUAGE ScopedTypeVariables        #-}
 -- |
@@ -12,8 +11,10 @@
 module Database.SQLite3.Direct (
     -- * Connection management
     open,
+    open2,
     close,
     errcode,
+    extendedErrcode,
     errmsg,
     setTrace,
     getAutoCommit,
@@ -151,10 +152,6 @@
 import           Foreign.C
 import qualified System.IO.Unsafe as IOU
 
-#if !MIN_VERSION_base(4,11,0)
-import           Data.Semigroup (Semigroup)
-#endif
-
 newtype Database = Database (Ptr CDatabase)
     deriving (Eq, Show)
 
@@ -259,19 +256,36 @@
     BS.useAsCString path $ \path' ->
     alloca $ \database -> do
         rc <- c_sqlite3_open path' database
-        db <- Database <$> peek database
-            -- sqlite3_open returns a sqlite3 even on failure.
-            -- That's where we get a more descriptive error message.
-        case toResult () rc of
-            Left err -> do
-                msg <- errmsg db -- This returns "out of memory" if db is null.
-                _   <- close db  -- This is harmless if db is null.
-                return $ Left (err, msg)
-            Right () ->
-                if db == Database nullPtr
-                    then fail "sqlite3_open unexpectedly returned NULL"
-                    else return $ Right db
+        openHelper rc database
 
+-- | <https://www.sqlite.org/c3ref/open.html>
+open2 :: Utf8 -> Int -> Maybe Utf8 -> IO (Either (Error, Utf8) Database)
+open2 (Utf8 path) flags mzvfs =
+    BS.useAsCString path $ \path' ->
+    useAsMaybeCString mzvfs $ \zvfs' ->
+    alloca $ \database -> do
+        rc <- c_sqlite3_open_v2 path' database (toEnum flags) zvfs'
+        openHelper rc database
+
+    where useAsMaybeCString :: Maybe Utf8 -> (CString -> IO a) -> IO a
+          useAsMaybeCString (Just (Utf8 zvfs)) f = BS.useAsCString zvfs f
+          useAsMaybeCString _ f = f nullPtr
+
+openHelper :: CError -> Ptr (Ptr CDatabase) -> IO (Either (Error, Utf8) Database)
+openHelper rc database = do
+    db <- Database <$> peek database
+        -- sqlite3_open and sqlite3_open_v2 return a sqlite3 even on failure.
+        -- That's where we get a more descriptive error message.
+    case toResult () rc of
+        Left err -> do
+            msg <- errmsg db -- This returns "out of memory" if db is null.
+            _   <- close db  -- This is harmless if db is null.
+            return $ Left (err, msg)
+        Right () ->
+            if db == Database nullPtr
+                then fail "sqlite3_open unexpectedly returned NULL"
+                else return $ Right db
+
 -- | <https://www.sqlite.org/c3ref/close.html>
 close :: Database -> IO (Either Error ())
 close (Database db) =
@@ -294,6 +308,11 @@
 errcode :: Database -> IO Error
 errcode (Database db) =
     decodeError <$> c_sqlite3_errcode db
+
+-- | <https://www.sqlite.org/c3ref/errcode.html>
+extendedErrcode :: Database -> IO Error
+extendedErrcode (Database db) =
+    decodeError <$> c_sqlite3_extended_errcode db
 
 -- | <https://www.sqlite.org/c3ref/errcode.html>
 errmsg :: Database -> IO Utf8
diff --git a/cbits/sqlite3.c b/cbits/sqlite3.c
# file too large to diff: cbits/sqlite3.c
diff --git a/cbits/sqlite3.h b/cbits/sqlite3.h
--- a/cbits/sqlite3.h
+++ b/cbits/sqlite3.h
@@ -146,9 +146,9 @@
 ** [sqlite3_libversion_number()], [sqlite3_sourceid()],
 ** [sqlite_version()] and [sqlite_source_id()].
 */
-#define SQLITE_VERSION        "3.38.5"
-#define SQLITE_VERSION_NUMBER 3038005
-#define SQLITE_SOURCE_ID      "2022-05-06 15:25:27 78d9c993d404cdfaa7fdd2973fa1052e3da9f66215cff9c5540ebe55c407d9fe"
+#define SQLITE_VERSION        "3.41.0"
+#define SQLITE_VERSION_NUMBER 3041000
+#define SQLITE_SOURCE_ID      "2023-02-21 18:09:37 05941c2a04037fc3ed2ffae11f5d2260706f89431f463518740f72ada350866d"
 
 /*
 ** CAPI3REF: Run-Time Library Version Numbers
@@ -563,6 +563,7 @@
 #define SQLITE_CONSTRAINT_DATATYPE     (SQLITE_CONSTRAINT |(12<<8))
 #define SQLITE_NOTICE_RECOVER_WAL      (SQLITE_NOTICE | (1<<8))
 #define SQLITE_NOTICE_RECOVER_ROLLBACK (SQLITE_NOTICE | (2<<8))
+#define SQLITE_NOTICE_RBU              (SQLITE_NOTICE | (3<<8))
 #define SQLITE_WARNING_AUTOINDEX       (SQLITE_WARNING | (1<<8))
 #define SQLITE_AUTH_USER               (SQLITE_AUTH | (1<<8))
 #define SQLITE_OK_LOAD_PERMANENTLY     (SQLITE_OK | (1<<8))
@@ -670,13 +671,17 @@
 **
 ** SQLite uses one of these integer values as the second
 ** argument to calls it makes to the xLock() and xUnlock() methods
-** of an [sqlite3_io_methods] object.
+** of an [sqlite3_io_methods] object.  These values are ordered from
+** lest restrictive to most restrictive.
+**
+** The argument to xLock() is always SHARED or higher.  The argument to
+** xUnlock is either SHARED or NONE.
 */
-#define SQLITE_LOCK_NONE          0
-#define SQLITE_LOCK_SHARED        1
-#define SQLITE_LOCK_RESERVED      2
-#define SQLITE_LOCK_PENDING       3
-#define SQLITE_LOCK_EXCLUSIVE     4
+#define SQLITE_LOCK_NONE          0       /* xUnlock() only */
+#define SQLITE_LOCK_SHARED        1       /* xLock() or xUnlock() */
+#define SQLITE_LOCK_RESERVED      2       /* xLock() only */
+#define SQLITE_LOCK_PENDING       3       /* xLock() only */
+#define SQLITE_LOCK_EXCLUSIVE     4       /* xLock() only */
 
 /*
 ** CAPI3REF: Synchronization Type Flags
@@ -754,7 +759,14 @@
 ** <li> [SQLITE_LOCK_PENDING], or
 ** <li> [SQLITE_LOCK_EXCLUSIVE].
 ** </ul>
-** xLock() increases the lock. xUnlock() decreases the lock.
+** xLock() upgrades the database file lock.  In other words, xLock() moves the
+** database file lock in the direction NONE toward EXCLUSIVE. The argument to
+** xLock() is always on of SHARED, RESERVED, PENDING, or EXCLUSIVE, never
+** SQLITE_LOCK_NONE.  If the database file lock is already at or above the
+** requested lock, then the call to xLock() is a no-op.
+** xUnlock() downgrades the database file lock to either SHARED or NONE.
+*  If the lock is already at or below the requested lock state, then the call
+** to xUnlock() is a no-op.
 ** The xCheckReservedLock() method checks whether any database connection,
 ** either in this process or in some other process, is holding a RESERVED,
 ** PENDING, or EXCLUSIVE lock on the file.  It returns true
@@ -859,9 +871,8 @@
 ** opcode causes the xFileControl method to write the current state of
 ** the lock (one of [SQLITE_LOCK_NONE], [SQLITE_LOCK_SHARED],
 ** [SQLITE_LOCK_RESERVED], [SQLITE_LOCK_PENDING], or [SQLITE_LOCK_EXCLUSIVE])
-** into an integer that the pArg argument points to. This capability
-** is used during testing and is only available when the SQLITE_TEST
-** compile-time option is used.
+** into an integer that the pArg argument points to.
+** This capability is only available if SQLite is compiled with [SQLITE_DEBUG].
 **
 ** <li>[[SQLITE_FCNTL_SIZE_HINT]]
 ** The [SQLITE_FCNTL_SIZE_HINT] opcode is used by SQLite to give the VFS
@@ -1165,7 +1176,6 @@
 ** in wal mode after the client has finished copying pages from the wal
 ** file to the database file, but before the *-shm file is updated to
 ** record the fact that the pages have been checkpointed.
-** </ul>
 **
 ** <li>[[SQLITE_FCNTL_EXTERNAL_READER]]
 ** The EXPERIMENTAL [SQLITE_FCNTL_EXTERNAL_READER] opcode is used to detect
@@ -1178,10 +1188,16 @@
 ** the database is not a wal-mode db, or if there is no such connection in any
 ** other process. This opcode cannot be used to detect transactions opened
 ** by clients within the current process, only within other processes.
-** </ul>
 **
 ** <li>[[SQLITE_FCNTL_CKSM_FILE]]
-** Used by the cksmvfs VFS module only.
+** The [SQLITE_FCNTL_CKSM_FILE] opcode is for use interally by the
+** [checksum VFS shim] only.
+**
+** <li>[[SQLITE_FCNTL_RESET_CACHE]]
+** If there is currently no transaction open on the database, and the
+** database is not a temp db, then the [SQLITE_FCNTL_RESET_CACHE] file-control
+** purges the contents of the in-memory page cache. If there is an open
+** transaction, or if the db is a temp-db, this opcode is a no-op, not an error.
 ** </ul>
 */
 #define SQLITE_FCNTL_LOCKSTATE               1
@@ -1224,6 +1240,7 @@
 #define SQLITE_FCNTL_CKPT_START             39
 #define SQLITE_FCNTL_EXTERNAL_READER        40
 #define SQLITE_FCNTL_CKSM_FILE              41
+#define SQLITE_FCNTL_RESET_CACHE            42
 
 /* deprecated names */
 #define SQLITE_GET_LOCKPROXYFILE      SQLITE_FCNTL_GET_LOCKPROXYFILE
@@ -1254,6 +1271,26 @@
 typedef struct sqlite3_api_routines sqlite3_api_routines;
 
 /*
+** CAPI3REF: File Name
+**
+** Type [sqlite3_filename] is used by SQLite to pass filenames to the
+** xOpen method of a [VFS]. It may be cast to (const char*) and treated
+** as a normal, nul-terminated, UTF-8 buffer containing the filename, but
+** may also be passed to special APIs such as:
+**
+** <ul>
+** <li>  sqlite3_filename_database()
+** <li>  sqlite3_filename_journal()
+** <li>  sqlite3_filename_wal()
+** <li>  sqlite3_uri_parameter()
+** <li>  sqlite3_uri_boolean()
+** <li>  sqlite3_uri_int64()
+** <li>  sqlite3_uri_key()
+** </ul>
+*/
+typedef const char *sqlite3_filename;
+
+/*
 ** CAPI3REF: OS Interface Object
 **
 ** An instance of the sqlite3_vfs object defines the interface between
@@ -1431,7 +1468,7 @@
   sqlite3_vfs *pNext;      /* Next registered VFS */
   const char *zName;       /* Name of this virtual file system */
   void *pAppData;          /* Pointer to application-specific data */
-  int (*xOpen)(sqlite3_vfs*, const char *zName, sqlite3_file*,
+  int (*xOpen)(sqlite3_vfs*, sqlite3_filename zName, sqlite3_file*,
                int flags, int *pOutFlags);
   int (*xDelete)(sqlite3_vfs*, const char *zName, int syncDir);
   int (*xAccess)(sqlite3_vfs*, const char *zName, int flags, int *pResOut);
@@ -2147,7 +2184,7 @@
 ** configuration for a database connection can only be changed when that
 ** connection is not currently using lookaside memory, or in other words
 ** when the "current value" returned by
-** [sqlite3_db_status](D,[SQLITE_CONFIG_LOOKASIDE],...) is zero.
+** [sqlite3_db_status](D,[SQLITE_DBSTATUS_LOOKASIDE_USED],...) is zero.
 ** Any attempt to change the lookaside memory configuration when lookaside
 ** memory is in use leaves the configuration unchanged and returns
 ** [SQLITE_BUSY].)^</dd>
@@ -2297,8 +2334,12 @@
 ** <li> sqlite3_db_config(db, SQLITE_DBCONFIG_RESET_DATABASE, 0, 0);
 ** </ol>
 ** Because resetting a database is destructive and irreversible, the
-** process requires the use of this obscure API and multiple steps to help
-** ensure that it does not happen by accident.
+** process requires the use of this obscure API and multiple steps to
+** help ensure that it does not happen by accident. Because this
+** feature must be capable of resetting corrupt databases, and
+** shutting down virtual tables may require access to that corrupt
+** storage, the library must abandon any installed virtual tables
+** without calling their xDestroy() methods.
 **
 ** [[SQLITE_DBCONFIG_DEFENSIVE]] <dt>SQLITE_DBCONFIG_DEFENSIVE</dt>
 ** <dd>The SQLITE_DBCONFIG_DEFENSIVE option activates or deactivates the
@@ -2309,6 +2350,7 @@
 ** <ul>
 ** <li> The [PRAGMA writable_schema=ON] statement.
 ** <li> The [PRAGMA journal_mode=OFF] statement.
+** <li> The [PRAGMA schema_version=N] statement.
 ** <li> Writes to the [sqlite_dbpage] virtual table.
 ** <li> Direct writes to [shadow tables].
 ** </ul>
@@ -2636,8 +2678,12 @@
 ** ^A call to sqlite3_interrupt(D) that occurs when there are no running
 ** SQL statements is a no-op and has no effect on SQL statements
 ** that are started after the sqlite3_interrupt() call returns.
+**
+** ^The [sqlite3_is_interrupted(D)] interface can be used to determine whether
+** or not an interrupt is currently in effect for [database connection] D.
 */
 SQLITE_API void sqlite3_interrupt(sqlite3*);
+SQLITE_API int sqlite3_is_interrupted(sqlite3*);
 
 /*
 ** CAPI3REF: Determine If An SQL Statement Is Complete
@@ -3255,8 +3301,8 @@
 ** <dd>^An SQLITE_TRACE_PROFILE callback provides approximately the same
 ** information as is provided by the [sqlite3_profile()] callback.
 ** ^The P argument is a pointer to the [prepared statement] and the
-** X argument points to a 64-bit integer which is the estimated of
-** the number of nanosecond that the prepared statement took to run.
+** X argument points to a 64-bit integer which is approximately
+** the number of nanoseconds that the prepared statement took to run.
 ** ^The SQLITE_TRACE_PROFILE callback is invoked when the statement finishes.
 **
 ** [[SQLITE_TRACE_ROW]] <dt>SQLITE_TRACE_ROW</dt>
@@ -3319,7 +3365,7 @@
 **
 ** ^The sqlite3_progress_handler(D,N,X,P) interface causes the callback
 ** function X to be invoked periodically during long running calls to
-** [sqlite3_exec()], [sqlite3_step()] and [sqlite3_get_table()] for
+** [sqlite3_step()] and [sqlite3_prepare()] and similar for
 ** database connection D.  An example use for this
 ** interface is to keep a GUI updated during a large query.
 **
@@ -3344,6 +3390,13 @@
 ** Note that [sqlite3_prepare_v2()] and [sqlite3_step()] both modify their
 ** database connections for the meaning of "modify" in this paragraph.
 **
+** The progress handler callback would originally only be invoked from the
+** bytecode engine.  It still might be invoked during [sqlite3_prepare()]
+** and similar because those routines might force a reparse of the schema
+** which involves running the bytecode engine.  However, beginning with
+** SQLite version 3.41.0, the progress handler callback might also be
+** invoked directly from [sqlite3_prepare()] while analyzing and generating
+** code for complex queries.
 */
 SQLITE_API void sqlite3_progress_handler(sqlite3*, int, int(*)(void*), void*);
 
@@ -3380,13 +3433,18 @@
 **
 ** <dl>
 ** ^(<dt>[SQLITE_OPEN_READONLY]</dt>
-** <dd>The database is opened in read-only mode.  If the database does not
-** already exist, an error is returned.</dd>)^
+** <dd>The database is opened in read-only mode.  If the database does
+** not already exist, an error is returned.</dd>)^
 **
 ** ^(<dt>[SQLITE_OPEN_READWRITE]</dt>
-** <dd>The database is opened for reading and writing if possible, or reading
-** only if the file is write protected by the operating system.  In either
-** case the database must already exist, otherwise an error is returned.</dd>)^
+** <dd>The database is opened for reading and writing if possible, or
+** reading only if the file is write protected by the operating
+** system.  In either case the database must already exist, otherwise
+** an error is returned.  For historical reasons, if opening in
+** read-write mode fails due to OS-level permissions, an attempt is
+** made to open it in read-only mode. [sqlite3_db_readonly()] can be
+** used to determine whether the database is actually
+** read-write.</dd>)^
 **
 ** ^(<dt>[SQLITE_OPEN_READWRITE] | [SQLITE_OPEN_CREATE]</dt>
 ** <dd>The database is opened for reading and writing, and is created if
@@ -3424,6 +3482,9 @@
 ** <dd>The database is opened [shared cache] enabled, overriding
 ** the default shared cache setting provided by
 ** [sqlite3_enable_shared_cache()].)^
+** The [use of shared cache mode is discouraged] and hence shared cache
+** capabilities may be omitted from many builds of SQLite.  In such cases,
+** this option is a no-op.
 **
 ** ^(<dt>[SQLITE_OPEN_PRIVATECACHE]</dt>
 ** <dd>The database is opened [shared cache] disabled, overriding
@@ -3439,7 +3500,7 @@
 ** to return an extended result code.</dd>
 **
 ** [[OPEN_NOFOLLOW]] ^(<dt>[SQLITE_OPEN_NOFOLLOW]</dt>
-** <dd>The database filename is not allowed to be a symbolic link</dd>
+** <dd>The database filename is not allowed to contain a symbolic link</dd>
 ** </dl>)^
 **
 ** If the 3rd parameter to sqlite3_open_v2() is not one of the
@@ -3698,10 +3759,10 @@
 **
 ** See the [URI filename] documentation for additional information.
 */
-SQLITE_API const char *sqlite3_uri_parameter(const char *zFilename, const char *zParam);
-SQLITE_API int sqlite3_uri_boolean(const char *zFile, const char *zParam, int bDefault);
-SQLITE_API sqlite3_int64 sqlite3_uri_int64(const char*, const char*, sqlite3_int64);
-SQLITE_API const char *sqlite3_uri_key(const char *zFilename, int N);
+SQLITE_API const char *sqlite3_uri_parameter(sqlite3_filename z, const char *zParam);
+SQLITE_API int sqlite3_uri_boolean(sqlite3_filename z, const char *zParam, int bDefault);
+SQLITE_API sqlite3_int64 sqlite3_uri_int64(sqlite3_filename, const char*, sqlite3_int64);
+SQLITE_API const char *sqlite3_uri_key(sqlite3_filename z, int N);
 
 /*
 ** CAPI3REF:  Translate filenames
@@ -3730,9 +3791,9 @@
 ** return value from [sqlite3_db_filename()], then the result is
 ** undefined and is likely a memory access violation.
 */
-SQLITE_API const char *sqlite3_filename_database(const char*);
-SQLITE_API const char *sqlite3_filename_journal(const char*);
-SQLITE_API const char *sqlite3_filename_wal(const char*);
+SQLITE_API const char *sqlite3_filename_database(sqlite3_filename);
+SQLITE_API const char *sqlite3_filename_journal(sqlite3_filename);
+SQLITE_API const char *sqlite3_filename_wal(sqlite3_filename);
 
 /*
 ** CAPI3REF:  Database File Corresponding To A Journal
@@ -3798,14 +3859,14 @@
 ** then the corresponding [sqlite3_module.xClose() method should also be
 ** invoked prior to calling sqlite3_free_filename(Y).
 */
-SQLITE_API char *sqlite3_create_filename(
+SQLITE_API sqlite3_filename sqlite3_create_filename(
   const char *zDatabase,
   const char *zJournal,
   const char *zWal,
   int nParam,
   const char **azParam
 );
-SQLITE_API void sqlite3_free_filename(char*);
+SQLITE_API void sqlite3_free_filename(sqlite3_filename);
 
 /*
 ** CAPI3REF: Error Codes And Messages
@@ -5364,10 +5425,21 @@
 ** from top-level SQL, and cannot be used in VIEWs or TRIGGERs nor in
 ** schema structures such as [CHECK constraints], [DEFAULT clauses],
 ** [expression indexes], [partial indexes], or [generated columns].
-** The SQLITE_DIRECTONLY flags is a security feature which is recommended
-** for all [application-defined SQL functions], and especially for functions
-** that have side-effects or that could potentially leak sensitive
-** information.
+** <p>
+** The SQLITE_DIRECTONLY flag is recommended for any
+** [application-defined SQL function]
+** that has side-effects or that could potentially leak sensitive information.
+** This will prevent attacks in which an application is tricked
+** into using a database file that has had its schema surreptiously
+** modified to invoke the application-defined function in ways that are
+** harmful.
+** <p>
+** Some people say it is good practice to set SQLITE_DIRECTONLY on all
+** [application-defined SQL functions], regardless of whether or not they
+** are security sensitive, as doing so prevents those functions from being used
+** inside of the database schema, and thus ensures that the database
+** can be inspected and modified using generic tools (such as the [CLI])
+** that do not have access to the application-defined functions.
 ** </dd>
 **
 ** [[SQLITE_INNOCUOUS]] <dt>SQLITE_INNOCUOUS</dt><dd>
@@ -5574,6 +5646,28 @@
 SQLITE_API int sqlite3_value_frombind(sqlite3_value*);
 
 /*
+** CAPI3REF: Report the internal text encoding state of an sqlite3_value object
+** METHOD: sqlite3_value
+**
+** ^(The sqlite3_value_encoding(X) interface returns one of [SQLITE_UTF8],
+** [SQLITE_UTF16BE], or [SQLITE_UTF16LE] according to the current text encoding
+** of the value X, assuming that X has type TEXT.)^  If sqlite3_value_type(X)
+** returns something other than SQLITE_TEXT, then the return value from
+** sqlite3_value_encoding(X) is meaningless.  ^Calls to
+** [sqlite3_value_text(X)], [sqlite3_value_text16(X)], [sqlite3_value_text16be(X)],
+** [sqlite3_value_text16le(X)], [sqlite3_value_bytes(X)], or
+** [sqlite3_value_bytes16(X)] might change the encoding of the value X and
+** thus change the return from subsequent calls to sqlite3_value_encoding(X).
+**
+** This routine is intended for used by applications that test and validate
+** the SQLite implementation.  This routine is inquiring about the opaque
+** internal state of an [sqlite3_value] object.  Ordinary applications should
+** not need to know what the internal state of an sqlite3_value object is and
+** hence should not need to use this interface.
+*/
+SQLITE_API int sqlite3_value_encoding(sqlite3_value*);
+
+/*
 ** CAPI3REF: Finding The Subtype Of SQL Values
 ** METHOD: sqlite3_value
 **
@@ -5593,7 +5687,8 @@
 ** object D and returns a pointer to that copy.  ^The [sqlite3_value] returned
 ** is a [protected sqlite3_value] object even if the input is not.
 ** ^The sqlite3_value_dup(V) interface returns NULL if V is NULL or if a
-** memory allocation fails.
+** memory allocation fails. ^If V is a [pointer value], then the result
+** of sqlite3_value_dup(V) is a NULL value.
 **
 ** ^The sqlite3_value_free(V) interface frees an [sqlite3_value] object
 ** previously obtained from [sqlite3_value_dup()].  ^If V is a NULL pointer
@@ -5624,7 +5719,7 @@
 **
 ** ^The sqlite3_aggregate_context(C,N) routine returns a NULL pointer
 ** when first called if N is less than or equal to zero or if a memory
-** allocate error occurs.
+** allocation error occurs.
 **
 ** ^(The amount of space allocated by sqlite3_aggregate_context(C,N) is
 ** determined by the N parameter on first successful call.  Changing the
@@ -5829,9 +5924,10 @@
 ** of [SQLITE_UTF8], [SQLITE_UTF16], [SQLITE_UTF16BE], or [SQLITE_UTF16LE].
 ** ^SQLite takes the text result from the application from
 ** the 2nd parameter of the sqlite3_result_text* interfaces.
-** ^If the 3rd parameter to the sqlite3_result_text* interfaces
-** is negative, then SQLite takes result text from the 2nd parameter
-** through the first zero character.
+** ^If the 3rd parameter to any of the sqlite3_result_text* interfaces
+** other than sqlite3_result_text64() is negative, then SQLite computes
+** the string length itself by searching the 2nd parameter for the first
+** zero character.
 ** ^If the 3rd parameter to the sqlite3_result_text* interfaces
 ** is non-negative, then as many bytes (not characters) of the text
 ** pointed to by the 2nd parameter are taken as the application-defined
@@ -6276,6 +6372,28 @@
 SQLITE_API sqlite3 *sqlite3_db_handle(sqlite3_stmt*);
 
 /*
+** CAPI3REF: Return The Schema Name For A Database Connection
+** METHOD: sqlite3
+**
+** ^The sqlite3_db_name(D,N) interface returns a pointer to the schema name
+** for the N-th database on database connection D, or a NULL pointer of N is
+** out of range.  An N value of 0 means the main database file.  An N of 1 is
+** the "temp" schema.  Larger values of N correspond to various ATTACH-ed
+** databases.
+**
+** Space to hold the string that is returned by sqlite3_db_name() is managed
+** by SQLite itself.  The string might be deallocated by any operation that
+** changes the schema, including [ATTACH] or [DETACH] or calls to
+** [sqlite3_serialize()] or [sqlite3_deserialize()], even operations that
+** occur on a different thread.  Applications that need to
+** remember the string long-term should make their own copy.  Applications that
+** are accessing the same database connection simultaneously on multiple
+** threads should mutex-protect calls to this API and should make their own
+** private copy of the result prior to releasing the mutex.
+*/
+SQLITE_API const char *sqlite3_db_name(sqlite3 *db, int N);
+
+/*
 ** CAPI3REF: Return The Filename For A Database Connection
 ** METHOD: sqlite3
 **
@@ -6305,7 +6423,7 @@
 ** <li> [sqlite3_filename_wal()]
 ** </ul>
 */
-SQLITE_API const char *sqlite3_db_filename(sqlite3 *db, const char *zDbName);
+SQLITE_API sqlite3_filename sqlite3_db_filename(sqlite3 *db, const char *zDbName);
 
 /*
 ** CAPI3REF: Determine if a database is read-only
@@ -6442,7 +6560,7 @@
 ** function C that is invoked prior to each autovacuum of the database
 ** file.  ^The callback is passed a copy of the generic data pointer (P),
 ** the schema-name of the attached database that is being autovacuumed,
-** the the size of the database file in pages, the number of free pages,
+** the size of the database file in pages, the number of free pages,
 ** and the number of bytes per page, respectively.  The callback should
 ** return the number of free pages that should be removed by the
 ** autovacuum.  ^If the callback returns zero, then no autovacuum happens.
@@ -6563,6 +6681,11 @@
 ** to the same database. Sharing is enabled if the argument is true
 ** and disabled if the argument is false.)^
 **
+** This interface is omitted if SQLite is compiled with
+** [-DSQLITE_OMIT_SHARED_CACHE].  The [-DSQLITE_OMIT_SHARED_CACHE]
+** compile-time option is recommended because the
+** [use of shared cache mode is discouraged].
+**
 ** ^Cache sharing is enabled and disabled for an entire process.
 ** This is a change as of SQLite [version 3.5.0] ([dateof:3.5.0]).
 ** In prior versions of SQLite,
@@ -6661,7 +6784,7 @@
 ** ^The soft heap limit may not be greater than the hard heap limit.
 ** ^If the hard heap limit is enabled and if sqlite3_soft_heap_limit(N)
 ** is invoked with a value of N that is greater than the hard heap limit,
-** the the soft heap limit is set to the value of the hard heap limit.
+** the soft heap limit is set to the value of the hard heap limit.
 ** ^The soft heap limit is automatically enabled whenever the hard heap
 ** limit is enabled. ^When sqlite3_hard_heap_limit64(N) is invoked and
 ** the soft heap limit is outside the range of 1..N, then the soft heap
@@ -6923,15 +7046,6 @@
 SQLITE_API void sqlite3_reset_auto_extension(void);
 
 /*
-** The interface to the virtual-table mechanism is currently considered
-** to be experimental.  The interface might change in incompatible ways.
-** If this is a problem for you, do not use the interface at this time.
-**
-** When the virtual-table mechanism stabilizes, we will declare the
-** interface fixed, support it indefinitely, and remove this comment.
-*/
-
-/*
 ** Structures used by the virtual table interface
 */
 typedef struct sqlite3_vtab sqlite3_vtab;
@@ -7049,10 +7163,10 @@
 ** when the omit flag is true there is no guarantee that the constraint will
 ** not be checked again using byte code.)^
 **
-** ^The idxNum and idxPtr values are recorded and passed into the
+** ^The idxNum and idxStr values are recorded and passed into the
 ** [xFilter] method.
-** ^[sqlite3_free()] is used to free idxPtr if and only if
-** needToFreeIdxPtr is true.
+** ^[sqlite3_free()] is used to free idxStr if and only if
+** needToFreeIdxStr is true.
 **
 ** ^The orderByConsumed means that output from [xFilter]/[xNext] will occur in
 ** the correct order to satisfy the ORDER BY clause so that no separate
@@ -7172,7 +7286,7 @@
 ** the [sqlite3_vtab_collation()] interface.  For most real-world virtual
 ** tables, the collating sequence of constraints does not matter (for example
 ** because the constraints are numeric) and so the sqlite3_vtab_collation()
-** interface is no commonly needed.
+** interface is not commonly needed.
 */
 #define SQLITE_INDEX_CONSTRAINT_EQ          2
 #define SQLITE_INDEX_CONSTRAINT_GT          4
@@ -7332,16 +7446,6 @@
 SQLITE_API int sqlite3_overload_function(sqlite3*, const char *zFuncName, int nArg);
 
 /*
-** The interface to the virtual-table mechanism defined above (back up
-** to a comment remarkably similar to this one) is currently considered
-** to be experimental.  The interface might change in incompatible ways.
-** If this is a problem for you, do not use the interface at this time.
-**
-** When the virtual-table mechanism stabilizes, we will declare the
-** interface fixed, support it indefinitely, and remove this comment.
-*/
-
-/*
 ** CAPI3REF: A Handle To An Open BLOB
 ** KEYWORDS: {BLOB handle} {BLOB handles}
 **
@@ -8956,7 +9060,7 @@
 ** if the application incorrectly accesses the destination [database connection]
 ** and so no error code is reported, but the operations may malfunction
 ** nevertheless.  Use of the destination database connection while a
-** backup is in progress might also also cause a mutex deadlock.
+** backup is in progress might also cause a mutex deadlock.
 **
 ** If running in [shared cache mode], the application must
 ** guarantee that the shared cache used by the destination database
@@ -9384,7 +9488,7 @@
 */
 #define SQLITE_CHECKPOINT_PASSIVE  0  /* Do as much as possible w/o blocking */
 #define SQLITE_CHECKPOINT_FULL     1  /* Wait for writers, then checkpoint */
-#define SQLITE_CHECKPOINT_RESTART  2  /* Like FULL but wait for for readers */
+#define SQLITE_CHECKPOINT_RESTART  2  /* Like FULL but wait for readers */
 #define SQLITE_CHECKPOINT_TRUNCATE 3  /* Like RESTART but also truncate WAL */
 
 /*
@@ -9544,7 +9648,7 @@
 ** <li><p> Otherwise, "BINARY" is returned.
 ** </ol>
 */
-SQLITE_API SQLITE_EXPERIMENTAL const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
+SQLITE_API const char *sqlite3_vtab_collation(sqlite3_index_info*,int);
 
 /*
 ** CAPI3REF: Determine if a virtual table query is DISTINCT
@@ -9554,8 +9658,8 @@
 ** of a [virtual table] implementation. The result of calling this
 ** interface from outside of xBestIndex() is undefined and probably harmful.
 **
-** ^The sqlite3_vtab_distinct() interface returns an integer that is
-** either 0, 1, or 2.  The integer returned by sqlite3_vtab_distinct()
+** ^The sqlite3_vtab_distinct() interface returns an integer between 0 and
+** 3.  The integer returned by sqlite3_vtab_distinct()
 ** gives the virtual table additional information about how the query
 ** planner wants the output to be ordered. As long as the virtual table
 ** can meet the ordering requirements of the query planner, it may set
@@ -9587,6 +9691,13 @@
 ** that have the same value for all columns identified by "aOrderBy".
 ** ^However omitting the extra rows is optional.
 ** This mode is used for a DISTINCT query.
+** <li value="3"><p>
+** ^(If the sqlite3_vtab_distinct() interface returns 3, that means
+** that the query planner needs only distinct rows but it does need the
+** rows to be sorted.)^ ^The virtual table implementation is free to omit
+** rows that are identical in all aOrderBy columns, if it wants to, but
+** it is not required to omit any rows.  This mode is used for queries
+** that have both DISTINCT and ORDER BY clauses.
 ** </ol>
 **
 ** ^For the purposes of comparing virtual table output values to see if the
@@ -9694,21 +9805,20 @@
 ** is undefined and probably harmful.
 **
 ** The X parameter in a call to sqlite3_vtab_in_first(X,P) or
-** sqlite3_vtab_in_next(X,P) must be one of the parameters to the
+** sqlite3_vtab_in_next(X,P) should be one of the parameters to the
 ** xFilter method which invokes these routines, and specifically
 ** a parameter that was previously selected for all-at-once IN constraint
 ** processing use the [sqlite3_vtab_in()] interface in the
 ** [xBestIndex|xBestIndex method].  ^(If the X parameter is not
 ** an xFilter argument that was selected for all-at-once IN constraint
-** processing, then these routines return [SQLITE_MISUSE])^ or perhaps
-** exhibit some other undefined or harmful behavior.
+** processing, then these routines return [SQLITE_ERROR].)^
 **
 ** ^(Use these routines to access all values on the right-hand side
 ** of the IN constraint using code like the following:
 **
 ** <blockquote><pre>
 ** &nbsp;  for(rc=sqlite3_vtab_in_first(pList, &pVal);
-** &nbsp;      rc==SQLITE_OK && pVal
+** &nbsp;      rc==SQLITE_OK && pVal;
 ** &nbsp;      rc=sqlite3_vtab_in_next(pList, &pVal)
 ** &nbsp;  ){
 ** &nbsp;    // do something with pVal
@@ -9806,6 +9916,10 @@
 ** managed by the prepared statement S and will be automatically freed when
 ** S is finalized.
 **
+** Not all values are available for all query elements. When a value is
+** not available, the output variable is set to -1 if the value is numeric,
+** or to NULL if it is a string (SQLITE_SCANSTAT_NAME).
+**
 ** <dl>
 ** [[SQLITE_SCANSTAT_NLOOP]] <dt>SQLITE_SCANSTAT_NLOOP</dt>
 ** <dd>^The [sqlite3_int64] variable pointed to by the V parameter will be
@@ -9833,12 +9947,24 @@
 ** to a zero-terminated UTF-8 string containing the [EXPLAIN QUERY PLAN]
 ** description for the X-th loop.
 **
-** [[SQLITE_SCANSTAT_SELECTID]] <dt>SQLITE_SCANSTAT_SELECT</dt>
+** [[SQLITE_SCANSTAT_SELECTID]] <dt>SQLITE_SCANSTAT_SELECTID</dt>
 ** <dd>^The "int" variable pointed to by the V parameter will be set to the
-** "select-id" for the X-th loop.  The select-id identifies which query or
-** subquery the loop is part of.  The main query has a select-id of zero.
-** The select-id is the same value as is output in the first column
-** of an [EXPLAIN QUERY PLAN] query.
+** id for the X-th query plan element. The id value is unique within the
+** statement. The select-id is the same value as is output in the first
+** column of an [EXPLAIN QUERY PLAN] query.
+**
+** [[SQLITE_SCANSTAT_PARENTID]] <dt>SQLITE_SCANSTAT_PARENTID</dt>
+** <dd>The "int" variable pointed to by the V parameter will be set to the
+** the id of the parent of the current query element, if applicable, or
+** to zero if the query element has no parent. This is the same value as
+** returned in the second column of an [EXPLAIN QUERY PLAN] query.
+**
+** [[SQLITE_SCANSTAT_NCYCLE]] <dt>SQLITE_SCANSTAT_NCYCLE</dt>
+** <dd>The sqlite3_int64 output value is set to the number of cycles,
+** according to the processor time-stamp counter, that elapsed while the
+** query element was being processed. This value is not available for
+** all query elements - if it is unavailable the output variable is
+** set to -1.
 ** </dl>
 */
 #define SQLITE_SCANSTAT_NLOOP    0
@@ -9847,12 +9973,14 @@
 #define SQLITE_SCANSTAT_NAME     3
 #define SQLITE_SCANSTAT_EXPLAIN  4
 #define SQLITE_SCANSTAT_SELECTID 5
+#define SQLITE_SCANSTAT_PARENTID 6
+#define SQLITE_SCANSTAT_NCYCLE   7
 
 /*
 ** CAPI3REF: Prepared Statement Scan Status
 ** METHOD: sqlite3_stmt
 **
-** This interface returns information about the predicted and measured
+** These interfaces return information about the predicted and measured
 ** performance for pStmt.  Advanced applications can use this
 ** interface to compare the predicted and the measured performance and
 ** issue warnings and/or rerun [ANALYZE] if discrepancies are found.
@@ -9863,20 +9991,26 @@
 **
 ** The "iScanStatusOp" parameter determines which status information to return.
 ** The "iScanStatusOp" must be one of the [scanstatus options] or the behavior
-** of this interface is undefined.
-** ^The requested measurement is written into a variable pointed to by
-** the "pOut" parameter.
-** Parameter "idx" identifies the specific loop to retrieve statistics for.
-** Loops are numbered starting from zero. ^If idx is out of range - less than
-** zero or greater than or equal to the total number of loops used to implement
-** the statement - a non-zero value is returned and the variable that pOut
-** points to is unchanged.
+** of this interface is undefined. ^The requested measurement is written into
+** a variable pointed to by the "pOut" parameter.
 **
-** ^Statistics might not be available for all loops in all statements. ^In cases
-** where there exist loops with no available statistics, this function behaves
-** as if the loop did not exist - it returns non-zero and leave the variable
-** that pOut points to unchanged.
+** The "flags" parameter must be passed a mask of flags. At present only
+** one flag is defined - SQLITE_SCANSTAT_COMPLEX. If SQLITE_SCANSTAT_COMPLEX
+** is specified, then status information is available for all elements
+** of a query plan that are reported by "EXPLAIN QUERY PLAN" output. If
+** SQLITE_SCANSTAT_COMPLEX is not specified, then only query plan elements
+** that correspond to query loops (the "SCAN..." and "SEARCH..." elements of
+** the EXPLAIN QUERY PLAN output) are available. Invoking API
+** sqlite3_stmt_scanstatus() is equivalent to calling
+** sqlite3_stmt_scanstatus_v2() with a zeroed flags parameter.
 **
+** Parameter "idx" identifies the specific query element to retrieve statistics
+** for. Query elements are numbered starting from zero. A value of -1 may be
+** to query for statistics regarding the entire query. ^If idx is out of range
+** - less than -1 or greater than or equal to the total number of query
+** elements used to implement the statement - a non-zero value is returned and
+** the variable that pOut points to is unchanged.
+**
 ** See also: [sqlite3_stmt_scanstatus_reset()]
 */
 SQLITE_API int sqlite3_stmt_scanstatus(
@@ -9885,8 +10019,21 @@
   int iScanStatusOp,        /* Information desired.  SQLITE_SCANSTAT_* */
   void *pOut                /* Result written here */
 );
+SQLITE_API int sqlite3_stmt_scanstatus_v2(
+  sqlite3_stmt *pStmt,      /* Prepared statement for which info desired */
+  int idx,                  /* Index of loop to report on */
+  int iScanStatusOp,        /* Information desired.  SQLITE_SCANSTAT_* */
+  int flags,                /* Mask of flags defined below */
+  void *pOut                /* Result written here */
+);
 
 /*
+** CAPI3REF: Prepared Statement Scan Status
+** KEYWORDS: {scan status flags}
+*/
+#define SQLITE_SCANSTAT_COMPLEX 0x0001
+
+/*
 ** CAPI3REF: Zero Scan-Status Counters
 ** METHOD: sqlite3_stmt
 **
@@ -9975,6 +10122,10 @@
 ** function is not defined for operations on WITHOUT ROWID tables, or for
 ** DELETE operations on rowid tables.
 **
+** ^The sqlite3_preupdate_hook(D,C,P) function returns the P argument from
+** the previous call on the same [database connection] D, or NULL for
+** the first call on D.
+**
 ** The [sqlite3_preupdate_old()], [sqlite3_preupdate_new()],
 ** [sqlite3_preupdate_count()], and [sqlite3_preupdate_depth()] interfaces
 ** provide additional information about a preupdate event. These routines
@@ -10378,6 +10529,19 @@
 */
 #ifdef SQLITE_OMIT_FLOATING_POINT
 # undef double
+#endif
+
+#if defined(__wasi__)
+# undef SQLITE_WASI
+# define SQLITE_WASI 1
+# undef SQLITE_OMIT_WAL
+# define SQLITE_OMIT_WAL 1/* because it requires shared memory APIs */
+# ifndef SQLITE_OMIT_LOAD_EXTENSION
+#  define SQLITE_OMIT_LOAD_EXTENSION
+# endif
+# ifndef SQLITE_THREADSAFE
+#  define SQLITE_THREADSAFE 0
+# endif
 #endif
 
 #ifdef __cplusplus
diff --git a/cbits/sqlite3ext.h b/cbits/sqlite3ext.h
--- a/cbits/sqlite3ext.h
+++ b/cbits/sqlite3ext.h
@@ -331,9 +331,9 @@
   const char *(*filename_journal)(const char*);
   const char *(*filename_wal)(const char*);
   /* Version 3.32.0 and later */
-  char *(*create_filename)(const char*,const char*,const char*,
+  const char *(*create_filename)(const char*,const char*,const char*,
                            int,const char**);
-  void (*free_filename)(char*);
+  void (*free_filename)(const char*);
   sqlite3_file *(*database_file_object)(const char*);
   /* Version 3.34.0 and later */
   int (*txn_state)(sqlite3*,const char*);
@@ -351,6 +351,16 @@
   int (*vtab_in)(sqlite3_index_info*,int,int);
   int (*vtab_in_first)(sqlite3_value*,sqlite3_value**);
   int (*vtab_in_next)(sqlite3_value*,sqlite3_value**);
+  /* Version 3.39.0 and later */
+  int (*deserialize)(sqlite3*,const char*,unsigned char*,
+                     sqlite3_int64,sqlite3_int64,unsigned);
+  unsigned char *(*serialize)(sqlite3*,const char *,sqlite3_int64*,
+                              unsigned int);
+  const char *(*db_name)(sqlite3*,int);
+  /* Version 3.40.0 and later */
+  int (*value_encoding)(sqlite3_value*);
+  /* Version 3.41.0 and later */
+  int (*is_interrupted)(sqlite3*);
 };
 
 /*
@@ -669,6 +679,16 @@
 #define sqlite3_vtab_in                sqlite3_api->vtab_in
 #define sqlite3_vtab_in_first          sqlite3_api->vtab_in_first
 #define sqlite3_vtab_in_next           sqlite3_api->vtab_in_next
+/* Version 3.39.0 and later */
+#ifndef SQLITE_OMIT_DESERIALIZE
+#define sqlite3_deserialize            sqlite3_api->deserialize
+#define sqlite3_serialize              sqlite3_api->serialize
+#endif
+#define sqlite3_db_name                sqlite3_api->db_name
+/* Version 3.40.0 and later */
+#define sqlite3_value_encoding         sqlite3_api->value_encoding
+/* Version 3.41.0 and later */
+#define sqlite3_is_interrupted         sqlite3_api->is_interrupted
 #endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
 
 #if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
diff --git a/changelog b/changelog
--- a/changelog
+++ b/changelog
@@ -1,3 +1,6 @@
+v2.3.28:
+	* Upgrade embedded sqlite library to 3.41.0.
+
 v2.3.27:
     * Add support for up to GHC 9.2
 	* Upgrade embedded sqlite library to 3.38.5.
diff --git a/direct-sqlite.cabal b/direct-sqlite.cabal
--- a/direct-sqlite.cabal
+++ b/direct-sqlite.cabal
@@ -1,5 +1,5 @@
 name:               direct-sqlite
-version:            2.3.27
+version:            2.3.28
 synopsis:           Low-level binding to SQLite3.  Includes UTF8 and BLOB support.
 description:        This package is not very different from the other SQLite3 bindings out
                     there, but it fixes a few deficiencies I was finding.  As compared to
@@ -54,15 +54,12 @@
                     Database.SQLite3.Bindings
                     Database.SQLite3.Bindings.Types
                     Database.SQLite3.Direct
-  build-depends:    base       >= 4.1 && < 5
+  build-depends:    base       >= 4.11 && < 5
                   , bytestring >= 0.9.2.1
                   , text       >= 0.11
   default-language: Haskell2010
   include-dirs:     .
   ghc-options:      -Wall -fwarn-tabs
-
-  if impl(ghc < 8.0)
-    build-depends: semigroups >= 0.18 && < 0.20
 
   if flag(systemlib)
     extra-libraries: sqlite3
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -3,13 +3,14 @@
 import Database.SQLite3
 import qualified Database.SQLite3.Direct as Direct
 
+import Control.Applicative
 import Control.Concurrent
 import Control.Exception
-import Control.Monad        (forM_, liftM3, when)
+import Control.Monad        (forM_, liftM3, unless)
+import Data.Functor.Identity
 import Data.Text            (Text)
 import Data.Text.Encoding.Error (UnicodeException(..))
 import Data.Typeable
-import Data.Monoid
 import System.Directory     ()
 import System.Exit          (exitFailure)
 import System.IO
@@ -23,7 +24,7 @@
 import qualified Data.Text              as T
 import qualified Data.Text.Encoding     as T
 
-data TestEnv =
+data TestEnv f =
   TestEnv {
     conn :: Database
     -- ^ Database shared by all the tests
@@ -32,10 +33,14 @@
     --   This connection will be isolated from others.
   , withConnShared :: forall a. (Database -> IO a) -> IO a
     -- ^ Like 'withConn', but every invocation shares the same database.
+  , withConnReadOnly :: forall a. f ((Database -> IO a) -> IO a)
+    -- ^ Like 'withConn', but every invocation shares the same read-only database.
   }
 
-regressionTests :: [TestEnv -> Test]
-regressionTests =
+type WithRoTestEnv = TestEnv Identity
+
+regressionTests1 :: forall f. [TestEnv f -> Test]
+regressionTests1 =
     [ TestLabel "Exec"          . testExec
     , TestLabel "ExecCallback"  . testExecCallback
     , TestLabel "Simple"        . testSimplest
@@ -61,12 +66,15 @@
     , TestLabel "CustomAggr"    . testCustomAggragate
     , TestLabel "CustomColl"    . testCustomCollation
     , TestLabel "IncrBlobIO"    . testIncrementalBlobIO
-    ] ++
-    (if rtsSupportsBoundThreads then
-    [ TestLabel "Interrupt"     . testInterrupt
-    ] else [])
+    ] <>
+    [ TestLabel "Interrupt"     . testInterrupt | rtsSupportsBoundThreads ]
 
-featureTests :: [TestEnv -> Test]
+
+regressionTests2 :: [WithRoTestEnv -> Test]
+regressionTests2 = regressionTests1 <> [TestLabel "ReadOnly" . testReadOnly]
+
+
+featureTests :: forall f. [TestEnv f -> Test]
 featureTests =
     [ TestLabel "MultiRowInsert" . testMultiRowInsert
     ]
@@ -87,7 +95,7 @@
 withStmt :: Database -> Text -> (Statement -> IO a) -> IO a
 withStmt conn sql = bracket (prepare conn sql) finalize
 
-testExec :: TestEnv -> Test
+testExec :: forall f. TestEnv f -> Test
 testExec TestEnv{..} = TestCase $ do
   exec conn ""
   exec conn "     "
@@ -107,7 +115,7 @@
               \INSERT INTO foo VALUES (null, ''); \
               \INSERT INTO foo VALUES (null, 'null'); \
               \INSERT INTO foo VALUES (null, null)"
-    withStmt conn ("SELECT * FROM foo") $ \stmt -> do
+    withStmt conn "SELECT * FROM foo" $ \stmt -> do
       Row <- step stmt
       [SQLFloat 3.5, SQLNull]       <- columns stmt
       Row <- step stmt
@@ -121,12 +129,19 @@
       Done <- step stmt
       return ()
 
+testReadOnly :: WithRoTestEnv -> Test
+testReadOnly TestEnv{..} = TestCase $ do
+  let reqCreate = "CREATE TABLE readonly (n FLOAT, t TEXT);"
+  runIdentity withConnReadOnly $ \conn -> do
+    Left SQLError{sqlError = ErrorReadOnly} <- try $ exec conn reqCreate
+    return ()
+
 data Ex = Ex
     deriving (Show, Typeable)
 
 instance Exception Ex
 
-testExecCallback :: TestEnv -> Test
+testExecCallback :: forall f. TestEnv f -> Test
 testExecCallback TestEnv{..} = TestCase $
   withConn $ \conn -> do
     chan <- newChan
@@ -163,11 +178,11 @@
     return ()
 
 
-testTracing :: TestEnv -> Test
+testTracing :: forall f. TestEnv f -> Test
 testTracing TestEnv{..} = TestCase $
   withConn $ \conn -> do
     chan <- newChan
-    let logger m = writeChan chan m
+    let logger = writeChan chan
     Direct.setTrace conn (Just logger)
     withStmt conn "SELECT null" $ \stmt -> do
       Row <- step stmt
@@ -200,7 +215,7 @@
 
 
 -- Simplest SELECT
-testSimplest :: TestEnv -> Test
+testSimplest :: forall f. TestEnv f -> Test
 testSimplest TestEnv{..} = TestCase $ do
   stmt <- prepare conn "SELECT 1+1"
   Row <- step stmt
@@ -209,7 +224,7 @@
   finalize stmt
   assertEqual "1+1" (SQLInteger 2) res
 
-testPrepare :: TestEnv -> Test
+testPrepare :: forall f. TestEnv f -> Test
 testPrepare TestEnv{..} = TestCase $ do
   True <- shouldFail $ prepare conn ""
   True <- shouldFail $ prepare conn ";"
@@ -237,15 +252,15 @@
     exec conn "COMMIT"
   return ()
 
-testCloseBusy :: TestEnv -> Test
+testCloseBusy :: forall f. TestEnv f -> Test
 testCloseBusy _ = TestCase $ do
-  conn <- open ":memory:"
+  conn <- open2 ":memory:" [SQLOpenReadWrite, SQLOpenCreate] SQLVFSDefault
   stmt <- prepare conn "SELECT 1"
   Left SQLError{sqlError = ErrorBusy} <- try $ close conn
   finalize stmt
   close conn
 
-testBind :: TestEnv -> Test
+testBind :: forall f. TestEnv f -> Test
 testBind TestEnv{..} = TestCase $ do
   bracket (prepare conn "SELECT ?") finalize testBind1
   bracket (prepare conn "SELECT ?+?") finalize testBind2
@@ -278,7 +293,7 @@
       assertEqual "blob vs. zeroblob" [SQLBlob bs, SQLBlob bs] res
 
 -- Test bindParameterCount
-testBindParamCounts :: TestEnv -> Test
+testBindParamCounts :: forall f. TestEnv f -> Test
 testBindParamCounts TestEnv{..} = TestCase $ do
   testCase "single $a"                  "SELECT $a"                     1
   testCase "3 unique ?NNNs"             "SELECT (?1+?1+?1+?2+?3)"       3
@@ -295,7 +310,7 @@
             >>= assertEqual label expected
 
 -- Test bindParameterName
-testBindParamName :: TestEnv -> Test
+testBindParamName :: forall f. TestEnv f -> Test
 testBindParamName TestEnv{..} = TestCase $ do
   bracket (prepare conn "SELECT :v + :v2") finalize (testNames [Just ":v", Just ":v2"])
   bracket (prepare conn "SELECT ?1 + ?1") finalize (testNames [Just "?1"])
@@ -310,7 +325,7 @@
                 name <- bindParameterName stmt ndx
                 assertEqual "name match" expecting name) $ zip [1..] names
 
-testBindErrorValidation :: TestEnv -> Test
+testBindErrorValidation :: forall f. TestEnv f -> Test
 testBindErrorValidation TestEnv{..} = TestCase $ do
   bracket (prepare conn "SELECT ?") finalize (assertFail . testException1)
   bracket (prepare conn "SELECT ?") finalize (assertFail . testException2)
@@ -320,7 +335,7 @@
     -- Invalid use, one param in q string, 2 given
     testException2 stmt = bind stmt [SQLInteger 1, SQLInteger 2]
 
-testNamedBindParams :: TestEnv -> Test
+testNamedBindParams :: forall f. TestEnv f -> Test
 testNamedBindParams TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     withStmt conn "SELECT :foo / :bar" $ \stmt -> do
@@ -353,7 +368,7 @@
       Done <- step stmt
       return ()
 
-testColumns :: TestEnv -> Test
+testColumns :: forall f. TestEnv f -> Test
 testColumns TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     withStmt conn "CREATE TABLE foo (a INT)" command
@@ -409,7 +424,7 @@
       0 <- columnCount stmt
       return ()
 
-testTypedColumns :: TestEnv -> Test
+testTypedColumns :: forall f. TestEnv f -> Test
 testTypedColumns TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     withStmt conn "CREATE TABLE foo (a INT, b INT)" command
@@ -442,7 +457,7 @@
       0 <- columnCount stmt
       return ()
 
-testColumnName :: TestEnv -> Test
+testColumnName :: forall f. TestEnv f -> Test
 testColumnName TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     exec conn "CREATE TABLE foo (id INTEGER PRIMARY KEY, abc TEXT, \"123\" REAL, über INT)"
@@ -491,12 +506,12 @@
 --  * ErrorLocked
 
 --  * ErrorBusy
-testErrors :: TestEnv -> Test
+testErrors :: forall f. TestEnv f -> Test
 testErrors TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     exec conn "CREATE TABLE foo (n INT UNIQUE)"
     exec conn "INSERT INTO foo VALUES (3)"
-    expectError ErrorConstraint $
+    expectError conn ErrorConstraint ErrorConstraintUnique $
       exec conn "INSERT INTO foo VALUES (3)"
 
     -- Multiple NULLs are allowed when there's a UNIQUE constraint
@@ -504,13 +519,13 @@
     exec conn "INSERT INTO foo VALUES (null)"
 
     exec conn "CREATE TABLE bar (n INT NOT NULL)"
-    expectError ErrorConstraint $
+    expectError conn ErrorConstraint ErrorConstraintNotNull$
       exec conn "INSERT INTO bar VALUES (null)"
 
     withStmt conn "SELECT ?" $ \stmt -> do
       forM_ [-1, 0, 2] $ \i -> do
-        expectError ErrorRange $ bindSQLData stmt i $ SQLInteger 42
-        expectError ErrorRange $ bindSQLData stmt i SQLNull
+        expectError conn ErrorRange ErrorRange $ bindSQLData stmt i $ SQLInteger 42
+        expectError conn ErrorRange ErrorRange $ bindSQLData stmt i SQLNull
       bindSQLData stmt 1 $ SQLInteger 42
       Row <- step stmt
 
@@ -524,8 +539,8 @@
 
     withStmt conn "SELECT 1" $ \stmt -> do
       forM_ [-1, 0, 1, 2] $ \i -> do
-        expectError ErrorRange $ bindSQLData stmt i $ SQLInteger 42
-        expectError ErrorRange $ bindSQLData stmt i SQLNull
+        expectError conn ErrorRange ErrorRange $ bindSQLData stmt i $ SQLInteger 42
+        expectError conn ErrorRange ErrorRange $ bindSQLData stmt i SQLNull
       bind stmt []  -- This should succeed.  Don't whine that there aren't any
                     -- parameters to bind!
       Row <- step stmt
@@ -541,8 +556,8 @@
 
     withStmt conn "SELECT ?5" $ \stmt -> do
       forM_ [-1, 0, 6, 7] $ \i -> do
-        expectError ErrorRange $ bindSQLData stmt i $ SQLInteger 42
-        expectError ErrorRange $ bindSQLData stmt i SQLNull
+        expectError conn ErrorRange ErrorRange $ bindSQLData stmt i $ SQLInteger 42
+        expectError conn ErrorRange ErrorRange $ bindSQLData stmt i SQLNull
       bind stmt $ map SQLInteger [1..5]
         -- This succeeds, even though 1..4 aren't used.
       Row <- step stmt
@@ -565,14 +580,14 @@
       [SQLInteger 1, SQLInteger 2] <- columns stmt
 
       -- "DROP TABLE foo" should fail, now that the statement is running.
-      expectError ErrorLocked $ exec conn "DROP TABLE foo"
+      expectError conn ErrorLocked ErrorLocked $ exec conn "DROP TABLE foo"
       withConnShared $ \conn -> do
-        expectError ErrorBusy $ exec conn "DROP TABLE foo"
+        expectError conn ErrorBusy ErrorBusy $ exec conn "DROP TABLE foo"
 
         -- Apparently, we can pretend to drop the table, but we get ErrorBusy
         -- if we try to actually COMMIT it.
         exec conn "BEGIN; DROP TABLE foo"
-        expectError ErrorBusy $ exec conn "COMMIT"
+        expectError conn ErrorBusy ErrorBusy $ exec conn "COMMIT"
 
         exec conn "ROLLBACK"
 
@@ -583,9 +598,9 @@
       2 <- columnCount stmt
       [SQLInteger 5, SQLInteger 6] <- columns stmt
 
-      expectError ErrorLocked $ exec conn "DROP TABLE foo"
+      expectError conn ErrorLocked ErrorLocked $ exec conn "DROP TABLE foo"
       withConnShared $ \conn ->
-        expectError ErrorBusy $ exec conn "DROP TABLE foo"
+        expectError conn ErrorBusy ErrorBusy $ exec conn "DROP TABLE foo"
 
       Done <- step stmt
       2 <- columnCount stmt
@@ -603,9 +618,13 @@
                   err == ErrorSchema)   -- SQLite 3.6.22
 
   where
-    expectError err io = do
+    expectError conn err extendedErr io = do
       Left SQLError{sqlError = err'} <- try io
-      assertEqual "testErrors: expectError" err err'
+      assertEqual "testErrors: expectExtError" err err'
+      err' <- Direct.errcode conn
+      assertEqual "testErrors: expectExtError errcode" err err'
+      extendedErr' <- Direct.extendedErrcode conn
+      assertEqual "testErrors: expectExtError extendedErrcode" extendedErr extendedErr'
 
     foo123456 conn =
       exec conn "CREATE TABLE foo (a INT, b INT); \
@@ -614,7 +633,7 @@
                 \INSERT INTO foo VALUES (5, 6)"
 
 -- Make sure data stored in a table comes back as-is.
-testIntegrity :: TestEnv -> Test
+testIntegrity :: forall f. TestEnv f -> Test
 testIntegrity TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     exec conn "CREATE TABLE foo (i INT, f FLOAT, t TEXT, b BLOB, n TEXT)"
@@ -638,7 +657,7 @@
 
         True <- test [SQLInteger 0, SQLFloat 0.0, SQLText T.empty, SQLBlob B.empty, SQLNull]
         True <- test [SQLInteger minBound, SQLFloat (-1/0), SQLText "\0", SQLBlob (B8.pack "\0"), SQLNull]
-        True <- test [SQLInteger maxBound, SQLFloat (1/0), SQLText "\1114111", SQLBlob ("\255"), SQLNull]
+        True <- test [SQLInteger maxBound, SQLFloat (1/0), SQLText "\1114111", SQLBlob "\255", SQLNull]
 
         -- SQLite3 turns NaN into SQLNull.
         True <- testWith (\_old new -> new === [SQLNull, SQLNull, SQLNull, SQLNull, SQLNull])
@@ -646,7 +665,7 @@
 
         return ()
 
-testDecodeError :: TestEnv -> Test
+testDecodeError :: forall f. TestEnv f -> Test
 testDecodeError TestEnv{..} = TestCase $ do
   withStmt conn "SELECT ?" $ \stmt -> do
     Right () <- Direct.bindText stmt 1 invalidUtf8
@@ -677,7 +696,7 @@
   where
     invalidUtf8 = Direct.Utf8 $ B.pack [0x80]
 
-testResultStats :: TestEnv -> Test
+testResultStats :: forall f. TestEnv f -> Test
 testResultStats TestEnv{..} = TestCase $
   withConn $ \conn -> do
     (0, 0, 0) <- stats conn
@@ -705,7 +724,7 @@
                   (changes conn)
                   (Direct.totalChanges conn)
 
-testGetAutoCommit :: TestEnv -> Test
+testGetAutoCommit :: forall f. TestEnv f -> Test
 testGetAutoCommit TestEnv{..} = TestCase $
   withConn $ \conn -> do
     True <- Direct.getAutoCommit conn
@@ -721,14 +740,14 @@
 
     return ()
 
-testStatementSql :: TestEnv -> Test
+testStatementSql :: forall f. TestEnv f -> Test
 testStatementSql TestEnv{..} = TestCase $ do
   let q1 = "SELECT 1+1"
   withStmt conn q1 $ \stmt -> do
     Just (Direct.Utf8 sql1) <- Direct.statementSql stmt
     T.encodeUtf8 q1 @=? sql1
 
-testCustomFunction :: TestEnv -> Test
+testCustomFunction :: forall f. TestEnv f -> Test
 testCustomFunction TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     createFunction conn "repeat" (Just 2) True repeatString
@@ -747,7 +766,7 @@
         s <- funcArgText args 1
         funcResultText ctx $ T.concat $ replicate (fromIntegral n) s
 
-testCustomFunctionError :: TestEnv -> Test
+testCustomFunctionError :: forall f. TestEnv f -> Test
 testCustomFunctionError TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     createFunction conn "fail" (Just 0) True throwError
@@ -760,7 +779,7 @@
   where
     throwError _ _ = error "error message"
 
-testCustomAggragate :: TestEnv -> Test
+testCustomAggragate :: forall f. TestEnv f -> Test
 testCustomAggragate TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     exec conn "CREATE TABLE tbl (n INT)"
@@ -780,7 +799,7 @@
         n <- funcArgInt64 args 0
         return (s + n)
 
-testCustomCollation :: TestEnv -> Test
+testCustomCollation :: forall f. TestEnv f -> Test
 testCustomCollation TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     exec conn "CREATE TABLE tbl (n TEXT)"
@@ -805,7 +824,7 @@
     -- order by length first, then by lexicographical order
     cmpLen s1 s2 = compare (T.length s1) (T.length s2) <> compare s1 s2
 
-testIncrementalBlobIO :: TestEnv -> Test
+testIncrementalBlobIO :: forall f. TestEnv f -> Test
 testIncrementalBlobIO TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     exec conn "CREATE TABLE tbl (n BLOB)"
@@ -822,7 +841,7 @@
       s' <- columnBlob stmt 0
       assertEqual "blobWrite" "aBCdefg" s'
 
-testInterrupt :: TestEnv -> Test
+testInterrupt :: forall f. TestEnv f -> Test
 testInterrupt TestEnv{..} = TestCase $
   withConn $ \conn -> do
     exec conn "CREATE TABLE tbl (n INT)"
@@ -848,7 +867,7 @@
   where
     tripleSum = "SELECT sum(a.n + b.n + c.n) FROM tbl as a, tbl as b, tbl as c"
 
-testMultiRowInsert :: TestEnv -> Test
+testMultiRowInsert :: forall f. TestEnv f -> Test
 testMultiRowInsert TestEnv{..} = TestCase $ do
   withConn $ \conn -> do
     exec conn "CREATE TABLE foo (a INT, b INT)"
@@ -869,14 +888,14 @@
           Done <- step stmt
           return ()
 
-
-withTestEnv :: String -> (TestEnv -> IO a) -> IO a
-withTestEnv tempDbName cb =
+withTestEnv1 :: String -> (forall f. TestEnv f -> IO a) -> IO a
+withTestEnv1 tempDbName cb =
     withConn $ \conn ->
         cb TestEnv
-            { conn           = conn
-            , withConn       = withConn
-            , withConnShared = withConnPath (T.pack tempDbName)
+            { conn             = conn
+            , withConn         = withConn
+            , withConnShared   = withConnPath (T.pack tempDbName)
+            , withConnReadOnly = Const ()
             }
   where
     withConn = withConnPath ":memory:"
@@ -890,20 +909,60 @@
       close conn
       return r
 
-runTestGroup :: String -> [TestEnv -> Test] -> IO Bool
-runTestGroup tempDbName tests = do
+withTestEnv2 :: String -> (WithRoTestEnv -> IO a) -> IO a
+withTestEnv2 tempDbName cb =
+    withConn $ \conn ->
+        cb TestEnv
+            { conn             = conn
+            , withConn         = withConn
+            , withConnShared   = withConnPath (T.pack tempDbName)
+                                              [SQLOpenReadWrite, SQLOpenCreate]
+            , withConnReadOnly = Identity $ withConnPath (T.pack tempDbName)
+                                                         [SQLOpenReadOnly]
+            }
+  where
+    withConn = withConnPath ":memory:" [SQLOpenReadWrite, SQLOpenCreate]
+    withConnPath path flags cb = do
+      conn <- open2 path flags SQLVFSDefault
+      r <- cb conn `onException` Direct.close conn
+            -- If the callback throws an exception, try to close the DB.
+            -- If closing fails (usually due to open 'Statement's),
+            -- throw the original error, not the error produced by 'close'.
+            -- Direct.close returns the error rather than throwing it.
+      close conn
+      return r
+
+runTestGroup1 :: String -> (forall f. [TestEnv f -> Test]) -> IO Bool
+runTestGroup1 tempDbName tests = do
   Counts{cases, tried, errors, failures} <-
-    withTestEnv tempDbName $ \env -> runTestTT $ TestList $ map ($ env) tests
+    withTestEnv1 tempDbName $ \env -> runTestTT $ TestList $ map ($ env) tests
   return (cases == tried && errors == 0 && failures == 0)
 
+runTestGroup2 :: String -> [WithRoTestEnv -> Test] -> IO Bool
+runTestGroup2 tempDbName tests = do
+  Counts{cases, tried, errors, failures} <-
+    withTestEnv2 tempDbName $ \env -> runTestTT $ TestList $ map ($ env) tests
+  return (cases == tried && errors == 0 && failures == 0)
+
 main :: IO ()
 main = do
   mapM_ (`hSetBuffering` LineBuffering) [stdout, stderr]
   withTempFile "." "direct-sqlite-test-database" $ \tempDbName _hFile -> do
     open (T.pack tempDbName) >>= close
-    ok <- runTestGroup tempDbName regressionTests
-    when (not ok) exitFailure
+    ok <- runTestGroup1 tempDbName regressionTests1
+    unless ok exitFailure
     -- Signal failure if feature tests fail.  I'd rather print a noisy warning
     -- instead, but cabal redirects test output to log files by default.
-    ok <- runTestGroup tempDbName featureTests
-    when (not ok) exitFailure
+    ok <- runTestGroup1 tempDbName featureTests
+    unless ok exitFailure
+  withTempFile "." "direct-sqlite-test-database" $ \tempDbName _hFile -> do
+    open2 (T.pack tempDbName)
+          [SQLOpenReadWrite, SQLOpenCreate]
+          SQLVFSDefault
+          >>= close
+    ok <- runTestGroup2 tempDbName regressionTests2
+    unless ok exitFailure
+    -- Signal failure if feature tests fail.  I'd rather print a noisy warning
+    -- instead, but cabal redirects test output to log files by default.
+    ok <- runTestGroup1 tempDbName featureTests
+    unless ok exitFailure
