diff --git a/simplexmq.cabal b/simplexmq.cabal index fbc46f612..4dd8d9101 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -167,6 +167,7 @@ library Simplex.Messaging.Agent.Store.Postgres.Migrations.M20250702_conn_invitations_remove_cascade_delete Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251009_queue_to_subscribe Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251010_client_notices + Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251230_strict_tables else exposed-modules: Simplex.Messaging.Agent.Store.SQLite @@ -216,6 +217,7 @@ library Simplex.Messaging.Agent.Store.SQLite.Migrations.M20250702_conn_invitations_remove_cascade_delete Simplex.Messaging.Agent.Store.SQLite.Migrations.M20251009_queue_to_subscribe Simplex.Messaging.Agent.Store.SQLite.Migrations.M20251010_client_notices + Simplex.Messaging.Agent.Store.SQLite.Migrations.M20251230_strict_tables Simplex.Messaging.Agent.Store.SQLite.Util if flag(client_postgres) || flag(server_postgres) exposed-modules: diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index 217a1682a..f90c4db4f 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -240,6 +240,7 @@ import Simplex.Messaging.Agent.Stats import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.AgentStore (getClientNotices, updateClientNotices) import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction) +import Simplex.Messaging.Agent.Store.DB (SQLError) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Agent.TSessionSubs (TSessionSubs) @@ -2124,7 +2125,9 @@ withWork_ c doWork getWork action = | otherwise -> notifyErr INTERNAL e where noWork = liftIO $ noWorkToDo doWork - notifyErr err e = atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err $ show e) + notifyErr err e = do + logError $ "withWork_ error: " <> tshow e + atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err $ show e) withWorkItems :: (AnyStoreError e', MonadIO m) => AgentClient -> TMVar () -> ExceptT e m (Either e' [Either e' a]) -> (NonEmpty a -> ExceptT e m ()) -> ExceptT e m () withWorkItems c doWork getWork action = do @@ -2145,7 +2148,9 @@ withWorkItems c doWork getWork action = do | otherwise -> notifyErr INTERNAL e where noWork = liftIO $ noWorkToDo doWork - notifyErr err e = atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err $ show e) + notifyErr err e = do + logError $ "withWorkItems error: " <> tshow e + atomically $ writeTBQueue (subQ c) ("", "", AEvt SAEConn $ ERR $ err $ show e) noWorkToDo :: TMVar () -> IO () noWorkToDo = void . atomically . tryTakeTMVar @@ -2243,24 +2248,19 @@ withStore :: AgentClient -> (DB.Connection -> IO (Either StoreError a)) -> AM a withStore c action = do st <- asks store withExceptT storeError . ExceptT . liftIO . agentOperationBracket c AODatabase (\_ -> pure ()) $ - withTransaction st action `E.catches` handleDBErrors + withTransaction st action `E.catch` handleDBErrors where + handleDBErrors :: E.SomeException -> IO (Either StoreError a) + handleDBErrors e = pure $ Left $ case E.fromException e of + Just (e' :: SQLError) -> #if defined(dbPostgres) - -- TODO [postgres] postgres specific error handling - handleDBErrors :: [E.Handler IO (Either StoreError a)] - handleDBErrors = - [ E.Handler $ \(E.SomeException e) -> pure . Left $ SEInternal $ bshow e - ] + SEInternal $ bshow e' #else - handleDBErrors :: [E.Handler IO (Either StoreError a)] - handleDBErrors = - [ E.Handler $ \(e :: SQL.SQLError) -> - let se = SQL.sqlError e - busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked - in pure . Left . (if busy then SEDatabaseBusy else SEInternal) $ bshow se, - E.Handler $ \(E.SomeException e) -> pure . Left $ SEInternal $ bshow e - ] + let se = SQL.sqlError e' + busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked + in (if busy then SEDatabaseBusy else SEInternal) $ bshow e' #endif + Nothing -> SEInternal $ bshow e unsafeWithStore :: AgentClient -> (DB.Connection -> IO a) -> AM' a unsafeWithStore c action = do diff --git a/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs b/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs index fe852ac64..c89ad3d90 100644 --- a/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs +++ b/src/Simplex/Messaging/Agent/NtfSubSupervisor.hs @@ -36,6 +36,7 @@ import qualified Data.List.NonEmpty as L import qualified Data.Map.Strict as M import Data.Maybe (catMaybes) import qualified Data.Set as S +import qualified Data.Text as T import Data.Time (UTCTime, addUTCTime, getCurrentTime) import Data.Time.Clock (diffUTCTime) import Simplex.Messaging.Agent.Client @@ -46,13 +47,13 @@ import Simplex.Messaging.Agent.Stats import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.AgentStore import qualified Simplex.Messaging.Agent.Store.DB as DB -import Simplex.Messaging.Client (NetworkRequestMode (..)) +import Simplex.Messaging.Client (NetworkRequestMode (..), nonBlockingWriteTBQueue) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Notifications.Protocol import Simplex.Messaging.Notifications.Types import Simplex.Messaging.Protocol (NtfServer, sameSrvAddr) import qualified Simplex.Messaging.Protocol as SMP -import Simplex.Messaging.Util (catchAllErrors, diffToMicroseconds, threadDelay', tryAllErrors, tshow, whenM) +import Simplex.Messaging.Util (catchAllErrors, catchAllErrors', diffToMicroseconds, threadDelay', tryAllErrors, tshow, whenM) import System.Random (randomR) import UnliftIO import UnliftIO.Concurrent (forkIO) @@ -66,19 +67,15 @@ runNtfSupervisor c = do Right _ -> pure () forever $ do cmd <- atomically . readTBQueue $ ntfSubQ ns - handleErr . agentOperationBracket c AONtfNetwork waitUntilActive $ - runExceptT (processNtfCmd c cmd) >>= \case - Left e -> notifyErr e - Right _ -> return () + handleErr $ agentOperationBracket c AONtfNetwork waitUntilActive $ + processNtfCmd c cmd `catchAllErrors'` notifyErr where startTknDelete :: AM () startTknDelete = do pendingDelServers <- withStore' c getPendingDelTknServers lift . forM_ pendingDelServers $ getNtfTknDelWorker True c handleErr :: AM' () -> AM' () - handleErr = E.handle $ \(e :: E.SomeException) -> do - logError $ "runNtfSupervisor error " <> tshow e - notifyErr e + handleErr = E.handle $ \(e :: E.SomeException) -> notifyErr e notifyErr e = notifyInternalError' c $ "runNtfSupervisor error " <> show e partitionErrs :: (a -> ConnId) -> [a] -> [Either AgentErrorType b] -> ([(ConnId, AgentErrorType)], [b]) @@ -505,16 +502,18 @@ workerInternalError c connId internalErrStr = do -- TODO change error notifyInternalError :: MonadIO m => AgentClient -> ConnId -> String -> m () -notifyInternalError AgentClient {subQ} connId internalErrStr = atomically $ writeTBQueue subQ ("", connId, AEvt SAEConn $ ERR $ INTERNAL internalErrStr) -{-# INLINE notifyInternalError #-} +notifyInternalError AgentClient {subQ} connId internalErrStr = do + logError $ T.pack internalErrStr + liftIO $ nonBlockingWriteTBQueue subQ ("", connId, AEvt SAEConn $ ERR $ INTERNAL internalErrStr) notifyInternalError' :: MonadIO m => AgentClient -> String -> m () -notifyInternalError' AgentClient {subQ} internalErrStr = atomically $ writeTBQueue subQ ("", "", AEvt SAEConn $ ERR $ INTERNAL internalErrStr) +notifyInternalError' c = notifyInternalError c "" {-# INLINE notifyInternalError' #-} notifyErrs :: MonadIO m => AgentClient -> [(ConnId, AgentErrorType)] -> m () -notifyErrs c = mapM_ (notifySub c . ERRS) . L.nonEmpty -{-# INLINE notifyErrs #-} +notifyErrs c errs_ = forM_ (L.nonEmpty errs_) $ \errs -> do + logError $ "notifyErrs: " <> tshow errs + notifySub c $ ERRS errs getNtfToken :: AM' (Maybe NtfToken) getNtfToken = do diff --git a/src/Simplex/Messaging/Agent/Protocol.hs b/src/Simplex/Messaging/Agent/Protocol.hs index 05ebc1b27..5b8a3da1d 100644 --- a/src/Simplex/Messaging/Agent/Protocol.hs +++ b/src/Simplex/Messaging/Agent/Protocol.hs @@ -698,9 +698,9 @@ instance ToJSON NotificationsMode where instance FromJSON NotificationsMode where parseJSON = strParseJSON "NotificationsMode" -instance ToField NotificationsMode where toField = toField . strEncode +instance ToField NotificationsMode where toField = toField . decodeLatin1 . strEncode -instance FromField NotificationsMode where fromField = blobFieldDecoder $ parseAll strP +instance FromField NotificationsMode where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8 data NotificationInfo = NotificationInfo { ntfConnId :: ConnId, diff --git a/src/Simplex/Messaging/Agent/Store.hs b/src/Simplex/Messaging/Agent/Store.hs index c054cb267..25ad86729 100644 --- a/src/Simplex/Messaging/Agent/Store.hs +++ b/src/Simplex/Messaging/Agent/Store.hs @@ -16,7 +16,7 @@ module Simplex.Messaging.Agent.Store where -import Control.Exception (Exception) +import Control.Exception (Exception (..)) import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) @@ -31,6 +31,7 @@ import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.RetryInterval (RI2State) import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Agent.Store.Common +import Simplex.Messaging.Agent.Store.DB (SQLError) import Simplex.Messaging.Agent.Store.Interface (createDBStore) import Simplex.Messaging.Agent.Store.Migrations.App (appMigrations) import Simplex.Messaging.Agent.Store.Shared (MigrationConfig (..), MigrationError (..)) @@ -752,7 +753,9 @@ data StoreError deriving (Eq, Show, Exception) instance AnyError StoreError where - fromSomeException = SEInternal . bshow + fromSomeException e = SEInternal $ case fromException e of + Just (e' :: SQLError) -> bshow e' + Nothing -> bshow e class (Show e, AnyError e) => AnyStoreError e where isWorkItemError :: e -> Bool diff --git a/src/Simplex/Messaging/Agent/Store/AgentStore.hs b/src/Simplex/Messaging/Agent/Store/AgentStore.hs index 104767c10..a4c95af87 100644 --- a/src/Simplex/Messaging/Agent/Store/AgentStore.hs +++ b/src/Simplex/Messaging/Agent/Store/AgentStore.hs @@ -285,7 +285,7 @@ import Simplex.Messaging.Agent.Stats import Simplex.Messaging.Agent.Store import Simplex.Messaging.Agent.Store.Common import qualified Simplex.Messaging.Agent.Store.DB as DB -import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..), FromField (..), ToField (..), blobFieldDecoder, fromTextField_) +import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..), FromField (..), ToField (..), SQLError, blobFieldDecoder, fromTextField_) import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Client (SMPTransportSession) import qualified Simplex.Messaging.Crypto as C @@ -308,11 +308,11 @@ import qualified UnliftIO.Exception as E import UnliftIO.STM #if defined(dbPostgres) import Data.List (sortOn) -import Database.PostgreSQL.Simple (In (..), Only (..), Query, SqlError, (:.) (..)) +import Database.PostgreSQL.Simple (In (..), Only (..), Query, (:.) (..)) import Database.PostgreSQL.Simple.Errors (constraintViolation) import Database.PostgreSQL.Simple.SqlQQ (sql) #else -import Database.SQLite.Simple (FromRow (..), Only (..), Query (..), SQLError, ToRow (..), field, (:.) (..)) +import Database.SQLite.Simple (FromRow (..), Only (..), Query (..), ToRow (..), field, (:.) (..)) import qualified Database.SQLite.Simple as SQL import Database.SQLite.Simple.QQ (sql) #endif @@ -320,13 +320,12 @@ import Database.SQLite.Simple.QQ (sql) checkConstraint :: StoreError -> IO (Either StoreError a) -> IO (Either StoreError a) checkConstraint err action = action `E.catch` (pure . Left . handleSQLError err) +handleSQLError :: StoreError -> SQLError -> StoreError #if defined(dbPostgres) -handleSQLError :: StoreError -> SqlError -> StoreError handleSQLError err e = case constraintViolation e of Just _ -> err Nothing -> SEInternal $ bshow e #else -handleSQLError :: StoreError -> SQLError -> StoreError handleSQLError err e | SQL.sqlError e == SQL.ErrorConstraint = err | otherwise = SEInternal $ bshow e @@ -1428,7 +1427,7 @@ createNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer INSERT INTO ntf_tokens (provider, device_token, ntf_host, ntf_port, tkn_id, tkn_pub_key, tkn_priv_key, tkn_pub_dh_key, tkn_priv_dh_key, tkn_dh_secret, tkn_status, tkn_action, ntf_mode) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) |] - ((provider, token, host, port, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhPubKey, ntfDhPrivKey, ntfDhSecret) :. (ntfTknStatus, ntfTknAction, ntfMode)) + ((provider, Binary token, host, port, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhPubKey, ntfDhPrivKey, ntfDhSecret) :. (ntfTknStatus, ntfTknAction, ntfMode)) getSavedNtfToken :: DB.Connection -> IO (Maybe NtfToken) getSavedNtfToken db = do @@ -1443,7 +1442,7 @@ getSavedNtfToken db = do JOIN ntf_servers s USING (ntf_host, ntf_port) |] where - ntfToken ((host, port, keyHash) :. (provider, dt, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhPubKey, ntfDhPrivKey, ntfDhSecret) :. (ntfTknStatus, ntfTknAction, ntfMode_)) = + ntfToken ((host, port, keyHash) :. (provider, Binary dt, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhPubKey, ntfDhPrivKey, ntfDhSecret) :. (ntfTknStatus, ntfTknAction, ntfMode_)) = let ntfServer = NtfServer host port keyHash ntfDhKeys = (ntfDhPubKey, ntfDhPrivKey) ntfMode = fromMaybe NMPeriodic ntfMode_ @@ -1459,7 +1458,7 @@ updateNtfTokenRegistration db NtfToken {deviceToken = DeviceToken provider token SET tkn_id = ?, tkn_dh_secret = ?, tkn_status = ?, tkn_action = ?, updated_at = ? WHERE provider = ? AND device_token = ? AND ntf_host = ? AND ntf_port = ? |] - (tknId, ntfDhSecret, NTRegistered, Nothing :: Maybe NtfTknAction, updatedAt, provider, token, host, port) + (tknId, ntfDhSecret, NTRegistered, Nothing :: Maybe NtfTknAction, updatedAt, provider, Binary token, host, port) updateDeviceToken :: DB.Connection -> NtfToken -> DeviceToken -> IO () updateDeviceToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer = ProtocolServer {host, port}} (DeviceToken toProvider toToken) = do @@ -1471,7 +1470,7 @@ updateDeviceToken db NtfToken {deviceToken = DeviceToken provider token, ntfServ SET provider = ?, device_token = ?, tkn_status = ?, tkn_action = ?, updated_at = ? WHERE provider = ? AND device_token = ? AND ntf_host = ? AND ntf_port = ? |] - (toProvider, toToken, NTRegistered, Nothing :: Maybe NtfTknAction, updatedAt, provider, token, host, port) + (toProvider, Binary toToken, NTRegistered, Nothing :: Maybe NtfTknAction, updatedAt, provider, Binary token, host, port) updateNtfMode :: DB.Connection -> NtfToken -> NotificationsMode -> IO () updateNtfMode db NtfToken {deviceToken = DeviceToken provider token, ntfServer = ProtocolServer {host, port}} ntfMode = do @@ -1483,7 +1482,7 @@ updateNtfMode db NtfToken {deviceToken = DeviceToken provider token, ntfServer = SET ntf_mode = ?, updated_at = ? WHERE provider = ? AND device_token = ? AND ntf_host = ? AND ntf_port = ? |] - (ntfMode, updatedAt, provider, token, host, port) + (ntfMode, updatedAt, provider, Binary token, host, port) updateNtfToken :: DB.Connection -> NtfToken -> NtfTknStatus -> Maybe NtfTknAction -> IO () updateNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer = ProtocolServer {host, port}} tknStatus tknAction = do @@ -1495,7 +1494,7 @@ updateNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer SET tkn_status = ?, tkn_action = ?, updated_at = ? WHERE provider = ? AND device_token = ? AND ntf_host = ? AND ntf_port = ? |] - (tknStatus, tknAction, updatedAt, provider, token, host, port) + (tknStatus, tknAction, updatedAt, provider, Binary token, host, port) removeNtfToken :: DB.Connection -> NtfToken -> IO () removeNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer = ProtocolServer {host, port}} = @@ -1505,7 +1504,7 @@ removeNtfToken db NtfToken {deviceToken = DeviceToken provider token, ntfServer DELETE FROM ntf_tokens WHERE provider = ? AND device_token = ? AND ntf_host = ? AND ntf_port = ? |] - (provider, token, host, port) + (provider, Binary token, host, port) addNtfTokenToDelete :: DB.Connection -> NtfServer -> C.APrivateAuthKey -> NtfTokenId -> IO () addNtfTokenToDelete db ProtocolServer {host, port, keyHash} ntfPrivKey tknId = @@ -1819,7 +1818,7 @@ getActiveNtfToken db = |] (Only NTActive) where - ntfToken ((host, port, keyHash) :. (provider, dt, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhPubKey, ntfDhPrivKey, ntfDhSecret) :. (ntfTknStatus, ntfTknAction, ntfMode_)) = + ntfToken ((host, port, keyHash) :. (provider, Binary dt, ntfTokenId, ntfPubKey, ntfPrivKey, ntfDhPubKey, ntfDhPrivKey, ntfDhSecret) :. (ntfTknStatus, ntfTknAction, ntfMode_)) = let ntfServer = NtfServer host port keyHash ntfDhKeys = (ntfDhPubKey, ntfDhPrivKey) ntfMode = fromMaybe NMPeriodic ntfMode_ diff --git a/src/Simplex/Messaging/Agent/Store/Postgres/DB.hs b/src/Simplex/Messaging/Agent/Store/Postgres/DB.hs index fc2c7cef0..7bd1103e8 100644 --- a/src/Simplex/Messaging/Agent/Store/Postgres/DB.hs +++ b/src/Simplex/Messaging/Agent/Store/Postgres/DB.hs @@ -6,6 +6,7 @@ module Simplex.Messaging.Agent.Store.Postgres.DB PSQL.Connection, FromField (..), ToField (..), + SQLError, PSQL.connect, PSQL.close, execute, @@ -33,6 +34,8 @@ import Database.PostgreSQL.Simple.TypeInfo.Static (textOid, varcharOid) newtype BoolInt = BI {unBI :: Bool} +type SQLError = PSQL.SqlError + instance FromField BoolInt where fromField field dat = BI . (/= (0 :: Int)) <$> fromField field dat {-# INLINE fromField #-} diff --git a/src/Simplex/Messaging/Agent/Store/Postgres/Migrations/App.hs b/src/Simplex/Messaging/Agent/Store/Postgres/Migrations/App.hs index 011d89031..6f1d68bcb 100644 --- a/src/Simplex/Messaging/Agent/Store/Postgres/Migrations/App.hs +++ b/src/Simplex/Messaging/Agent/Store/Postgres/Migrations/App.hs @@ -10,6 +10,7 @@ import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20250322_short_links import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20250702_conn_invitations_remove_cascade_delete import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251009_queue_to_subscribe import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251010_client_notices +import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251230_strict_tables import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Text, Maybe Text)] @@ -19,7 +20,8 @@ schemaMigrations = ("20250322_short_links", m20250322_short_links, Just down_m20250322_short_links), ("20250702_conn_invitations_remove_cascade_delete", m20250702_conn_invitations_remove_cascade_delete, Just down_m20250702_conn_invitations_remove_cascade_delete), ("20251009_queue_to_subscribe", m20251009_queue_to_subscribe, Just down_m20251009_queue_to_subscribe), - ("20251010_client_notices", m20251010_client_notices, Just down_m20251010_client_notices) + ("20251010_client_notices", m20251010_client_notices, Just down_m20251010_client_notices), + ("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Messaging/Agent/Store/Postgres/Migrations/M20251230_strict_tables.hs b/src/Simplex/Messaging/Agent/Store/Postgres/Migrations/M20251230_strict_tables.hs new file mode 100644 index 000000000..a2d11dde9 --- /dev/null +++ b/src/Simplex/Messaging/Agent/Store/Postgres/Migrations/M20251230_strict_tables.hs @@ -0,0 +1,66 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251230_strict_tables where + +import Data.Text (Text) +import Text.RawString.QQ (r) + +m20251230_strict_tables :: Text +m20251230_strict_tables = + isValidText + <> [r| +DELETE FROM ntf_tokens +WHERE NOT simplex_is_valid_text(ntf_mode); + +ALTER TABLE ntf_tokens + ALTER COLUMN device_token TYPE BYTEA USING device_token::BYTEA, + ALTER COLUMN ntf_mode TYPE TEXT USING ntf_mode::TEXT; + +UPDATE ntf_subscriptions +SET ntf_sub_action = NULL +WHERE NOT simplex_is_valid_text(ntf_sub_action); + +UPDATE ntf_subscriptions +SET ntf_sub_smp_action = NULL +WHERE NOT simplex_is_valid_text(ntf_sub_smp_action); + +ALTER TABLE ntf_subscriptions + ALTER COLUMN ntf_sub_action TYPE TEXT USING ntf_sub_action::TEXT, + ALTER COLUMN ntf_sub_smp_action TYPE TEXT USING ntf_sub_smp_action::TEXT; + +DROP FUNCTION simplex_is_valid_text(BYTEA); +|] + +down_m20251230_strict_tables :: Text +down_m20251230_strict_tables = + isValidText + <> [r| +DELETE FROM ntf_tokens +WHERE NOT simplex_is_valid_text(device_token); + +ALTER TABLE ntf_tokens + ALTER COLUMN device_token TYPE TEXT USING device_token::TEXT, + ALTER COLUMN ntf_mode TYPE BYTEA USING ntf_mode::BYTEA; + +ALTER TABLE ntf_subscriptions + ALTER COLUMN ntf_sub_action TYPE BYTEA USING ntf_sub_action::BYTEA, + ALTER COLUMN ntf_sub_smp_action TYPE BYTEA USING ntf_sub_smp_action::BYTEA; + +DROP FUNCTION simplex_is_valid_text(BYTEA); +|] + +isValidText :: Text +isValidText = + [r| +CREATE FUNCTION simplex_is_valid_text(b BYTEA) +RETURNS BOOLEAN +LANGUAGE plpgsql AS $$ +BEGIN + PERFORM b::TEXT; + RETURN TRUE; +EXCEPTION + WHEN OTHERS THEN RETURN FALSE; +END; +$$; +|] diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/DB.hs b/src/Simplex/Messaging/Agent/Store/SQLite/DB.hs index 2620e561b..67cfa6213 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/DB.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/DB.hs @@ -14,6 +14,7 @@ module Simplex.Messaging.Agent.Store.SQLite.DB TrackQueries (..), FromField (..), ToField (..), + SQLError, open, close, execute, @@ -38,7 +39,7 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Time (diffUTCTime, getCurrentTime) import Data.Typeable (Typeable) -import Database.SQLite.Simple (FromRow, ResultError (..), Query, SQLData (..), ToRow) +import Database.SQLite.Simple (FromRow, ResultError (..), Query, SQLData (..), SQLError, ToRow) import qualified Database.SQLite.Simple as SQL import Database.SQLite.Simple.FromField (FieldParser, FromField (..), returnError) import Database.SQLite.Simple.Internal (Field (..)) diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/App.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/App.hs index 7371d9584..26df43bc8 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/App.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/App.hs @@ -46,6 +46,7 @@ import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20250322_short_links import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20250702_conn_invitations_remove_cascade_delete import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20251009_queue_to_subscribe import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20251010_client_notices +import Simplex.Messaging.Agent.Store.SQLite.Migrations.M20251230_strict_tables import Simplex.Messaging.Agent.Store.Shared (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -91,7 +92,8 @@ schemaMigrations = ("m20250322_short_links", m20250322_short_links, Just down_m20250322_short_links), ("m20250702_conn_invitations_remove_cascade_delete", m20250702_conn_invitations_remove_cascade_delete, Just down_m20250702_conn_invitations_remove_cascade_delete), ("m20251009_queue_to_subscribe", m20251009_queue_to_subscribe, Just down_m20251009_queue_to_subscribe), - ("m20251010_client_notices", m20251010_client_notices, Just down_m20251010_client_notices) + ("m20251010_client_notices", m20251010_client_notices, Just down_m20251010_client_notices), + ("m20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20220322_notifications.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20220322_notifications.hs index 3ff01fd87..c5d553322 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20220322_notifications.hs +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20220322_notifications.hs @@ -19,7 +19,7 @@ CREATE TABLE ntf_servers ( CREATE TABLE ntf_tokens ( provider TEXT NOT NULL, -- apns - device_token TEXT NOT NULL, -- ! this field is mislabeled and is actually saved as binary + device_token TEXT NOT NULL, ntf_host TEXT NOT NULL, ntf_port TEXT NOT NULL, tkn_id BLOB, -- token ID assigned by notifications server diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20251230_strict_tables.hs b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20251230_strict_tables.hs new file mode 100644 index 000000000..3d05e054a --- /dev/null +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/M20251230_strict_tables.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Messaging.Agent.Store.SQLite.Migrations.M20251230_strict_tables where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20251230_strict_tables :: Query +m20251230_strict_tables = + [sql| +UPDATE ntf_tokens SET ntf_mode = CAST(ntf_mode as TEXT); + +UPDATE ntf_subscriptions +SET ntf_sub_action = CAST(ntf_sub_action as TEXT), + ntf_sub_smp_action = CAST(ntf_sub_smp_action as TEXT); + +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = CASE + WHEN LOWER(SUBSTR(sql, -15)) = ') without rowid' THEN sql || ', STRICT' + WHEN SUBSTR(sql, -1) = ')' THEN sql || ' STRICT' + ELSE sql +END +WHERE type = 'table' AND name != 'sqlite_sequence'; + +UPDATE sqlite_master +SET sql = replace(sql, 'device_token TEXT NOT NULL', 'device_token BLOB NOT NULL') +WHERE type = 'table' AND name = 'ntf_tokens'; + +PRAGMA writable_schema=0; +|] + +down_m20251230_strict_tables :: Query +down_m20251230_strict_tables = + [sql| +PRAGMA writable_schema=1; + +UPDATE sqlite_master +SET sql = CASE + WHEN LOWER(SUBSTR(sql, -8)) = ', strict' THEN SUBSTR(sql, 1, LENGTH(sql) - 8) + WHEN LOWER(SUBSTR(sql, -7)) = ' strict' THEN SUBSTR(sql, 1, LENGTH(sql) - 7) + ELSE sql +END +WHERE type = 'table' AND name != 'sqlite_sequence'; + +UPDATE sqlite_master +SET sql = replace(sql, 'device_token BLOB NOT NULL', 'device_token TEXT NOT NULL') +WHERE type = 'table' AND name = 'ntf_tokens'; + +PRAGMA writable_schema=0; + +UPDATE ntf_tokens SET ntf_mode = CAST(ntf_mode as BLOB); + +UPDATE ntf_subscriptions +SET ntf_sub_action = CAST(ntf_sub_action as BLOB), + ntf_sub_smp_action = CAST(ntf_sub_smp_action as BLOB); +|] diff --git a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql index d2838a7b0..b98d3dbf4 100644 --- a/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql +++ b/src/Simplex/Messaging/Agent/Store/SQLite/Migrations/agent_schema.sql @@ -2,13 +2,13 @@ CREATE TABLE migrations( name TEXT NOT NULL PRIMARY KEY, ts TEXT NOT NULL, down TEXT -); +) STRICT; CREATE TABLE servers( host TEXT NOT NULL, port TEXT NOT NULL, key_hash BLOB NOT NULL, PRIMARY KEY(host, port) -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE connections( conn_id BLOB NOT NULL PRIMARY KEY, conn_mode TEXT NOT NULL, @@ -28,7 +28,7 @@ CREATE TABLE connections( ratchet_sync_state TEXT NOT NULL DEFAULT 'ok', deleted_at_wait_delivery TEXT, pq_support INTEGER NOT NULL DEFAULT 0 -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE rcv_queues( host TEXT NOT NULL, port TEXT NOT NULL, @@ -67,7 +67,7 @@ CREATE TABLE rcv_queues( FOREIGN KEY(host, port) REFERENCES servers ON DELETE RESTRICT ON UPDATE CASCADE, UNIQUE(host, port, snd_id) -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE snd_queues( host TEXT NOT NULL, port TEXT NOT NULL, @@ -89,7 +89,7 @@ CREATE TABLE snd_queues( PRIMARY KEY(host, port, snd_id), FOREIGN KEY(host, port) REFERENCES servers ON DELETE RESTRICT ON UPDATE CASCADE -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE messages( conn_id BLOB NOT NULL REFERENCES connections(conn_id) ON DELETE CASCADE, @@ -106,7 +106,7 @@ CREATE TABLE messages( ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED, FOREIGN KEY(conn_id, internal_snd_id) REFERENCES snd_messages ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE rcv_messages( conn_id BLOB NOT NULL, internal_rcv_id INTEGER NOT NULL, @@ -122,7 +122,7 @@ CREATE TABLE rcv_messages( PRIMARY KEY(conn_id, internal_rcv_id), FOREIGN KEY(conn_id, internal_id) REFERENCES messages ON DELETE CASCADE -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE snd_messages( conn_id BLOB NOT NULL, internal_snd_id INTEGER NOT NULL, @@ -139,7 +139,7 @@ CREATE TABLE snd_messages( PRIMARY KEY(conn_id, internal_snd_id), FOREIGN KEY(conn_id, internal_id) REFERENCES messages ON DELETE CASCADE -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE conn_confirmations( confirmation_id BLOB NOT NULL PRIMARY KEY, conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE, @@ -153,7 +153,7 @@ CREATE TABLE conn_confirmations( , smp_reply_queues BLOB NULL, smp_client_version INTEGER -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE conn_invitations( invitation_id BLOB NOT NULL PRIMARY KEY, contact_conn_id BLOB REFERENCES connections ON DELETE SET NULL, @@ -162,7 +162,7 @@ CREATE TABLE conn_invitations( accepted INTEGER NOT NULL DEFAULT 0, own_conn_info BLOB, created_at TEXT NOT NULL DEFAULT(datetime('now')) -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE ratchets( conn_id BLOB NOT NULL PRIMARY KEY REFERENCES connections ON DELETE CASCADE, @@ -177,7 +177,7 @@ CREATE TABLE ratchets( x3dh_pub_key_2 BLOB, pq_priv_kem BLOB, pq_pub_kem BLOB -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE skipped_messages( skipped_message_id INTEGER PRIMARY KEY, conn_id BLOB NOT NULL REFERENCES ratchets @@ -185,7 +185,7 @@ CREATE TABLE skipped_messages( header_key BLOB NOT NULL, msg_n INTEGER NOT NULL, msg_key BLOB NOT NULL -); +) STRICT; CREATE TABLE ntf_servers( ntf_host TEXT NOT NULL, ntf_port TEXT NOT NULL, @@ -193,10 +193,10 @@ CREATE TABLE ntf_servers( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')), PRIMARY KEY(ntf_host, ntf_port) -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE ntf_tokens( provider TEXT NOT NULL, -- apns - device_token TEXT NOT NULL, -- ! this field is mislabeled and is actually saved as binary + device_token BLOB NOT NULL, ntf_host TEXT NOT NULL, ntf_port TEXT NOT NULL, tkn_id BLOB, -- token ID assigned by notifications server @@ -213,7 +213,7 @@ tkn_dh_secret BLOB, -- DH secret for e2e encryption of notifications PRIMARY KEY(provider, device_token, ntf_host, ntf_port), FOREIGN KEY(ntf_host, ntf_port) REFERENCES ntf_servers ON DELETE RESTRICT ON UPDATE CASCADE -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE ntf_subscriptions( conn_id BLOB NOT NULL, smp_host TEXT NULL, @@ -237,7 +237,7 @@ CREATE TABLE ntf_subscriptions( ON DELETE SET NULL ON UPDATE CASCADE, FOREIGN KEY(ntf_host, ntf_port) REFERENCES ntf_servers ON DELETE RESTRICT ON UPDATE CASCADE -) WITHOUT ROWID; +) WITHOUT ROWID, STRICT; CREATE TABLE commands( command_id INTEGER PRIMARY KEY, conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE, @@ -252,7 +252,7 @@ CREATE TABLE commands( failed INTEGER DEFAULT 0, FOREIGN KEY(host, port) REFERENCES servers ON DELETE RESTRICT ON UPDATE CASCADE -); +) STRICT; CREATE TABLE snd_message_deliveries( snd_message_delivery_id INTEGER PRIMARY KEY AUTOINCREMENT, conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE, @@ -260,13 +260,13 @@ CREATE TABLE snd_message_deliveries( internal_id INTEGER NOT NULL, failed INTEGER DEFAULT 0, FOREIGN KEY(conn_id, internal_id) REFERENCES messages ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED -); +) STRICT; CREATE TABLE sqlite_sequence(name,seq); CREATE TABLE users( user_id INTEGER PRIMARY KEY AUTOINCREMENT , deleted INTEGER DEFAULT 0 CHECK(deleted NOT NULL) -); +) STRICT; CREATE TABLE xftp_servers( xftp_server_id INTEGER PRIMARY KEY, xftp_host TEXT NOT NULL, @@ -275,7 +275,7 @@ CREATE TABLE xftp_servers( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')), UNIQUE(xftp_host, xftp_port, xftp_key_hash) -); +) STRICT; CREATE TABLE rcv_files( rcv_file_id INTEGER PRIMARY KEY, rcv_file_entity_id BLOB NOT NULL, @@ -302,7 +302,7 @@ CREATE TABLE rcv_files( redirect_digest BLOB, approved_relays INTEGER NOT NULL DEFAULT 0, UNIQUE(rcv_file_entity_id) -); +) STRICT; CREATE TABLE rcv_file_chunks( rcv_file_chunk_id INTEGER PRIMARY KEY, rcv_file_id INTEGER NOT NULL REFERENCES rcv_files ON DELETE CASCADE, @@ -312,7 +312,7 @@ CREATE TABLE rcv_file_chunks( tmp_path TEXT, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE rcv_file_chunk_replicas( rcv_file_chunk_replica_id INTEGER PRIMARY KEY, rcv_file_chunk_id INTEGER NOT NULL REFERENCES rcv_file_chunks ON DELETE CASCADE, @@ -325,7 +325,7 @@ CREATE TABLE rcv_file_chunk_replicas( retries INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE snd_files( snd_file_id INTEGER PRIMARY KEY, snd_file_entity_id BLOB NOT NULL, @@ -347,7 +347,7 @@ CREATE TABLE snd_files( failed INTEGER DEFAULT 0, redirect_size INTEGER, redirect_digest BLOB -); +) STRICT; CREATE TABLE snd_file_chunks( snd_file_chunk_id INTEGER PRIMARY KEY, snd_file_id INTEGER NOT NULL REFERENCES snd_files ON DELETE CASCADE, @@ -357,7 +357,7 @@ CREATE TABLE snd_file_chunks( digest BLOB NOT NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE snd_file_chunk_replicas( snd_file_chunk_replica_id INTEGER PRIMARY KEY, snd_file_chunk_id INTEGER NOT NULL REFERENCES snd_file_chunks ON DELETE CASCADE, @@ -370,7 +370,7 @@ CREATE TABLE snd_file_chunk_replicas( retries INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE snd_file_chunk_replica_recipients( snd_file_chunk_replica_recipient_id INTEGER PRIMARY KEY, snd_file_chunk_replica_id INTEGER NOT NULL REFERENCES snd_file_chunk_replicas ON DELETE CASCADE, @@ -378,7 +378,7 @@ CREATE TABLE snd_file_chunk_replica_recipients( rcv_replica_key BLOB NOT NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE deleted_snd_chunk_replicas( deleted_snd_chunk_replica_id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, @@ -392,28 +392,28 @@ CREATE TABLE deleted_snd_chunk_replicas( updated_at TEXT NOT NULL DEFAULT(datetime('now')) , failed INTEGER DEFAULT 0 -); +) STRICT; CREATE TABLE encrypted_rcv_message_hashes( encrypted_rcv_message_hash_id INTEGER PRIMARY KEY, conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE, hash BLOB NOT NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE processed_ratchet_key_hashes( processed_ratchet_key_hash_id INTEGER PRIMARY KEY, conn_id BLOB NOT NULL REFERENCES connections ON DELETE CASCADE, hash BLOB NOT NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE servers_stats( servers_stats_id INTEGER PRIMARY KEY, servers_stats TEXT, started_at TEXT NOT NULL DEFAULT(datetime('now')), created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE ntf_tokens_to_delete( ntf_token_to_delete_id INTEGER PRIMARY KEY, ntf_host TEXT NOT NULL, @@ -423,11 +423,11 @@ CREATE TABLE ntf_tokens_to_delete( tkn_priv_key BLOB NOT NULL, -- client's private key to sign token commands, del_failed INTEGER DEFAULT 0, created_at TEXT NOT NULL DEFAULT(datetime('now')) -); +) STRICT; CREATE TABLE snd_message_bodies( snd_message_body_id INTEGER PRIMARY KEY, agent_msg BLOB NOT NULL DEFAULT x'' -); +) STRICT; CREATE TABLE inv_short_links( inv_short_link_id INTEGER PRIMARY KEY AUTOINCREMENT, host TEXT NOT NULL, @@ -438,7 +438,7 @@ CREATE TABLE inv_short_links( snd_private_key BLOB NOT NULL, snd_id BLOB, FOREIGN KEY(host, port) REFERENCES servers ON DELETE RESTRICT ON UPDATE CASCADE -); +) STRICT; CREATE TABLE client_notices( client_notice_id INTEGER PRIMARY KEY AUTOINCREMENT, protocol TEXT NOT NULL, @@ -449,7 +449,7 @@ CREATE TABLE client_notices( notice_ttl INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL -); +) STRICT; CREATE UNIQUE INDEX idx_rcv_queues_ntf ON rcv_queues(host, port, ntf_id); CREATE UNIQUE INDEX idx_rcv_queue_id ON rcv_queues(conn_id, rcv_queue_id); CREATE UNIQUE INDEX idx_snd_queue_id ON snd_queues(conn_id, snd_queue_id); diff --git a/src/Simplex/Messaging/Notifications/Types.hs b/src/Simplex/Messaging/Notifications/Types.hs index a7665b5b2..166ff7516 100644 --- a/src/Simplex/Messaging/Notifications/Types.hs +++ b/src/Simplex/Messaging/Notifications/Types.hs @@ -13,8 +13,10 @@ import Simplex.Messaging.Agent.Protocol (ConnId, NotificationsMode (..), UserId) import Simplex.Messaging.Agent.Store.DB (Binary (..), FromField (..), ToField (..), blobFieldDecoder, fromTextField_) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding +import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol import Simplex.Messaging.Protocol (NotifierId, NtfServer, SMPServer) +import Simplex.Messaging.Util (eitherToMaybe) data NtfTknAction = NTARegister @@ -101,42 +103,40 @@ data NtfSubNTFAction | NSARotate -- deprecated deriving (Show) -instance Encoding NtfSubNTFAction where - smpEncode = \case +instance TextEncoding NtfSubNTFAction where + textEncode = \case NSACreate -> "N" NSACheck -> "C" NSADelete -> "D" NSARotate -> "R" - smpP = - A.anyChar >>= \case - 'N' -> pure NSACreate - 'C' -> pure NSACheck - 'D' -> pure NSADelete - 'R' -> pure NSARotate - _ -> fail "bad NtfSubNTFAction" + textDecode = \case + "N" -> Just NSACreate + "C" -> Just NSACheck + "D" -> Just NSADelete + "R" -> Just NSARotate + _ -> Nothing -instance FromField NtfSubNTFAction where fromField = blobFieldDecoder smpDecode +instance FromField NtfSubNTFAction where fromField = fromTextField_ textDecode -instance ToField NtfSubNTFAction where toField = toField . Binary . smpEncode +instance ToField NtfSubNTFAction where toField = toField . textEncode data NtfSubSMPAction = NSASmpKey | NSASmpDelete deriving (Show) -instance Encoding NtfSubSMPAction where - smpEncode = \case +instance TextEncoding NtfSubSMPAction where + textEncode = \case NSASmpKey -> "K" NSASmpDelete -> "D" - smpP = - A.anyChar >>= \case - 'K' -> pure NSASmpKey - 'D' -> pure NSASmpDelete - _ -> fail "bad NtfSubSMPAction" + textDecode = \case + "K" -> Just NSASmpKey + "D" -> Just NSASmpDelete + _ -> Nothing -instance FromField NtfSubSMPAction where fromField = blobFieldDecoder smpDecode +instance FromField NtfSubSMPAction where fromField = fromTextField_ textDecode -instance ToField NtfSubSMPAction where toField = toField . Binary . smpEncode +instance ToField NtfSubSMPAction where toField = toField . textEncode data NtfAgentSubStatus = -- | subscription started @@ -171,7 +171,7 @@ instance Encoding NtfAgentSubStatus where "DELETED" -> pure NASDeleted _ -> fail "bad NtfAgentSubStatus" -instance FromField NtfAgentSubStatus where fromField = fromTextField_ $ either (const Nothing) Just . smpDecode . encodeUtf8 +instance FromField NtfAgentSubStatus where fromField = fromTextField_ $ eitherToMaybe . smpDecode . encodeUtf8 instance ToField NtfAgentSubStatus where toField = toField . decodeLatin1 . smpEncode diff --git a/tests/AgentTests/SQLiteTests.hs b/tests/AgentTests/SQLiteTests.hs index 5785e1b53..b68731ff7 100644 --- a/tests/AgentTests/SQLiteTests.hs +++ b/tests/AgentTests/SQLiteTests.hs @@ -778,14 +778,15 @@ testGetNextSndFileToPrepare st = do withTransaction st $ \db -> do Right Nothing <- getNextSndFileToPrepare db 86400 - Right _ <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing - DB.execute_ db "UPDATE snd_files SET status = 'new', num_recipients = 'bad' WHERE snd_file_id = 1" + -- Can't test it with strict tables + -- Right _ <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing + -- DB.execute_ db "UPDATE snd_files SET status = 'new', num_recipients = 'bad' WHERE snd_file_id = 1" Right fId2 <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing DB.execute_ db "UPDATE snd_files SET status = 'new' WHERE snd_file_id = 2" - Left e <- getNextSndFileToPrepare db 86400 - show e `shouldContain` "ConversionFailed" - DB.query_ db "SELECT snd_file_id FROM snd_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] + -- Left e <- getNextSndFileToPrepare db 86400 + -- show e `shouldContain` "ConversionFailed" + -- DB.query_ db "SELECT snd_file_id FROM snd_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] Right (Just SndFile {sndFileEntityId}) <- getNextSndFileToPrepare db 86400 sndFileEntityId `shouldBe` fId2 @@ -808,16 +809,17 @@ testGetNextSndChunkToUpload st = do -- create file 1 Right _ <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing updateSndFileEncrypted db 1 (FileDigest "abc") [(XFTPChunkSpec "filepath" 1 1, FileDigest "ghi")] - createSndFileReplica_ db 1 newSndChunkReplica1 - DB.execute_ db "UPDATE snd_files SET num_recipients = 'bad' WHERE snd_file_id = 1" + -- Can't test it with strict tables + -- createSndFileReplica_ db 1 newSndChunkReplica1 + -- DB.execute_ db "UPDATE snd_files SET num_recipients = 'bad' WHERE snd_file_id = 1" -- create file 2 Right fId2 <- createSndFile db g 1 (CryptoFile "filepath" Nothing) 1 "filepath" testFileSbKey testFileCbNonce Nothing updateSndFileEncrypted db 2 (FileDigest "abc") [(XFTPChunkSpec "filepath" 1 1, FileDigest "ghi")] createSndFileReplica_ db 2 newSndChunkReplica1 - Left e <- getNextSndChunkToUpload db xftpServer1 86400 - show e `shouldContain` "ConversionFailed" - DB.query_ db "SELECT snd_file_id FROM snd_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] + -- Left e <- getNextSndChunkToUpload db xftpServer1 86400 + -- show e `shouldContain` "ConversionFailed" + -- DB.query_ db "SELECT snd_file_id FROM snd_files WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] Right (Just SndFileChunk {sndFileEntityId}) <- getNextSndChunkToUpload db xftpServer1 86400 sndFileEntityId `shouldBe` fId2 @@ -827,16 +829,17 @@ testGetNextDeletedSndChunkReplica st = do withTransaction st $ \db -> do Right Nothing <- getNextDeletedSndChunkReplica db xftpServer1 86400 - createDeletedSndChunkReplica db 1 (FileChunkReplica xftpServer1 (ChunkReplicaId $ EntityId "abc") testFileReplicaKey) (FileDigest "ghi") - DB.execute_ db "UPDATE deleted_snd_chunk_replicas SET delay = 'bad' WHERE deleted_snd_chunk_replica_id = 1" + -- Can't test it with strict tables + -- createDeletedSndChunkReplica db 1 (FileChunkReplica xftpServer1 (ChunkReplicaId $ EntityId "abc") testFileReplicaKey) (FileDigest "ghi") + -- DB.execute_ db "UPDATE deleted_snd_chunk_replicas SET delay = 'bad' WHERE deleted_snd_chunk_replica_id = 1" createDeletedSndChunkReplica db 1 (FileChunkReplica xftpServer1 (ChunkReplicaId $ EntityId "abc") testFileReplicaKey) (FileDigest "ghi") - Left e <- getNextDeletedSndChunkReplica db xftpServer1 86400 - show e `shouldContain` "ConversionFailed" - DB.query_ db "SELECT deleted_snd_chunk_replica_id FROM deleted_snd_chunk_replicas WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] + -- Left e <- getNextDeletedSndChunkReplica db xftpServer1 86400 + -- show e `shouldContain` "ConversionFailed" + -- DB.query_ db "SELECT deleted_snd_chunk_replica_id FROM deleted_snd_chunk_replicas WHERE failed = 1" `shouldReturn` [Only (1 :: Int)] Right (Just DeletedSndChunkReplica {deletedSndChunkReplicaId}) <- getNextDeletedSndChunkReplica db xftpServer1 86400 - deletedSndChunkReplicaId `shouldBe` 2 + deletedSndChunkReplicaId `shouldBe` 1 testMarkNtfSubActionNtfFailed :: DBStore -> Expectation testMarkNtfSubActionNtfFailed st = do diff --git a/tests/AgentTests/SchemaDump.hs b/tests/AgentTests/SchemaDump.hs index 51aa59528..d9aa79513 100644 --- a/tests/AgentTests/SchemaDump.hs +++ b/tests/AgentTests/SchemaDump.hs @@ -7,6 +7,7 @@ import Control.DeepSeq import Control.Monad (unless, void) import Data.List (dropWhileEnd) import Data.Maybe (fromJust, isJust) +import Data.Text (Text) import Database.SQLite.Simple (Only (..)) import qualified Database.SQLite.Simple as SQL import Simplex.Messaging.Agent.Store.Migrations.App (appMigrations) @@ -44,6 +45,7 @@ schemaDumpTest = do it "verify and overwrite schema dump" testVerifySchemaDump it "verify .lint fkey-indexes" testVerifyLintFKeyIndexes it "verify schema down migrations" testSchemaMigrations + it "verify strict tables" testVerifyStrict it "should NOT create user record for new database" testUsersMigrationNew it "should create user record for old database" testUsersMigrationOld @@ -87,6 +89,12 @@ testSchemaMigrations = do schema''' <- getSchema testDB testSchema schema''' `shouldBe` schema' +testVerifyStrict :: IO () +testVerifyStrict = do + Right st <- createDBStore (DBOpts testDB [] "" False True TQOff) appMigrations (MigrationConfig MCConsole Nothing) + withTransaction' st (`SQL.query_` "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND sql NOT LIKE '% STRICT'") + `shouldReturn` ([] :: [Only Text]) + testUsersMigrationNew :: IO () testUsersMigrationNew = do Right st <- createDBStore (DBOpts testDB [] "" False True TQOff) appMigrations (MigrationConfig MCError Nothing)