core: use strict tables (#6535)

* core: use strict tables

* fix field types

* change encodings to match schema types; migrate sqlite tables to strict mode

* stabilize postgres client tests, remove slow handshake tests

* update simplexmq

* fix test

* change call_state type to text

* fix directory service queries

* update local_alias for existing schemas

* change types before strict
This commit is contained in:
Evgeny
2026-01-05 08:53:26 +00:00
committed by GitHub
parent f0467aee00
commit 87e8a10f1e
19 changed files with 220 additions and 121 deletions

View File

@@ -665,7 +665,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
where
rStatus = groupRolesStatus contactRole serviceRole
groupRef = groupReference g
ctRole = "*" <> strEncodeTxt contactRole <> "*"
ctRole = "*" <> textEncode contactRole <> "*"
suCtRole = "(user role is set to " <> ctRole <> ")."
deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO ()
@@ -691,7 +691,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
_ -> pure ()
where
groupRef = groupReference g
srvRole = "*" <> strEncodeTxt serviceRole <> "*"
srvRole = "*" <> textEncode serviceRole <> "*"
suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")."
whenContactIsOwner gr action =
getOwnerGroupMember groupId gr
@@ -801,7 +801,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let anotherRole = case acceptMemberRole of GRObserver -> GRMember; _ -> GRObserver
sendReply $
initialRole n acceptMemberRole
<> ("Send /'role " <> tshow gId <> " " <> strEncodeTxt anotherRole <> "' to change it.\n\n")
<> ("Send /'role " <> tshow gId <> " " <> textEncode anotherRole <> "' to change it.\n\n")
<> onlyViaLink gLink
Left _ -> sendReply $ "Error: failed reading the initial member role for the group " <> n
Just mRole -> do
@@ -809,7 +809,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
Just gLink -> sendReply $ initialRole n mRole <> "\n" <> onlyViaLink gLink
Nothing -> sendReply $ "Error: the initial member role for the group " <> n <> " was NOT upgated."
where
initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> strEncodeTxt mRole <> "*\n"
initialRole n mRole = "The initial member role for the group " <> n <> " is set to *" <> textEncode mRole <> "*\n"
onlyViaLink gLink = "*Please note*: it applies only to members joining via this link: " <> groupLinkText gLink
DCGroupFilter gId gName_ acceptance_ ->
(if isAdmin then withGroupAndReg_ sendReply else withUserGroupReg_) gId gName_ $ \g _gr -> do
@@ -852,7 +852,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
T.unlines $
[ "The link to join the group " <> groupRef <> ":",
groupLinkText gLink,
"New member role: " <> strEncodeTxt acceptMemberRole
"New member role: " <> textEncode acceptMemberRole
]
<> ["The link is being upgraded..." | shouldBeUpgraded]
when shouldBeUpgraded $ do

View File

@@ -124,6 +124,7 @@ library
Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade
Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector
Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations
Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables
else
exposed-modules:
Simplex.Chat.Archive
@@ -271,6 +272,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade
Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector
Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations
Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables
other-modules:
Paths_simplex_chat
hs-source-dirs:

View File

@@ -76,6 +76,7 @@ import Simplex.Messaging.Agent.Lock
import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.Common (DBStore, withTransaction, withTransactionPriority)
import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation, UpMigration)
import Simplex.Messaging.Agent.Store.DB (SQLError)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SMPWebPortServers (..), SocksMode (..))
import qualified Simplex.Messaging.Crypto as C
@@ -96,13 +97,7 @@ import Simplex.RemoteControl.Types
import System.IO (Handle)
import System.Mem.Weak (Weak)
import UnliftIO.STM
#if defined(dbPostgres)
import qualified Database.PostgreSQL.Simple as PSQL
type SQLError = PSQL.SqlError
#else
import Database.SQLite.Simple (SQLError)
#if !defined(dbPostgres)
import qualified Database.SQLite.Simple as SQL
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
#endif

View File

@@ -22,7 +22,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Lazy as LB
import Data.Int (Int64)
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
import Data.Text.Encoding (encodeUtf8)
import Data.Type.Equality
import Data.Word (Word32)
import Simplex.Chat.Messages.CIContent.Events
@@ -323,7 +323,7 @@ e2eInfoPQText =
ciGroupInvitationToText :: CIGroupInvitation -> GroupMemberRole -> Text
ciGroupInvitationToText CIGroupInvitation {groupProfile = GroupProfile {displayName, fullName}} role =
"invitation to join group " <> displayName <> optionalFullName displayName fullName Nothing <> " as " <> (decodeLatin1 . strEncode $ role)
"invitation to join group " <> displayName <> optionalFullName displayName fullName Nothing <> " as " <> textEncode role
rcvDirectEventToText :: RcvDirectEvent -> Text
rcvDirectEventToText = \case
@@ -338,9 +338,9 @@ rcvGroupEventToText = \case
RGEMemberAccepted _ p -> "accepted " <> profileToText p
RGEUserAccepted -> "accepted you"
RGEMemberLeft -> "left"
RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r)
RGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> textEncode r
RGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p
RGEUserRole r -> "changed your role to " <> safeDecodeUtf8 (strEncode r)
RGEUserRole r -> "changed your role to " <> textEncode r
RGEMemberDeleted _ p -> "removed " <> profileToText p
RGEUserDeleted -> "removed you"
RGEGroupDeleted -> "deleted group"
@@ -352,9 +352,9 @@ rcvGroupEventToText = \case
sndGroupEventToText :: SndGroupEvent -> Text
sndGroupEventToText = \case
SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> safeDecodeUtf8 (strEncode r)
SGEMemberRole _ p r -> "changed role of " <> profileToText p <> " to " <> textEncode r
SGEMemberBlocked _ p blocked -> (if blocked then "blocked" else "unblocked") <> " " <> profileToText p
SGEUserRole r -> "changed role for yourself to " <> safeDecodeUtf8 (strEncode r)
SGEUserRole r -> "changed role for yourself to " <> textEncode r
SGEMemberDeleted _ p -> "removed " <> profileToText p
SGEUserLeft -> "left"
SGEGroupUpdated _ -> "group profile updated"

View File

@@ -5,11 +5,12 @@ module Simplex.Chat.Store.AppSettings where
import Control.Monad (join)
import Control.Monad.IO.Class (liftIO)
import qualified Data.Aeson as J
import Data.Maybe (fromMaybe)
import Simplex.Chat.AppSettings (AppSettings (..), combineAppSettings, defaultAppSettings, defaultParseAppSettings)
import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Util (decodeJSON, encodeJSON)
#if defined(dbPostgres)
import Database.PostgreSQL.Simple (Only (..))
#else
@@ -19,9 +20,9 @@ import Database.SQLite.Simple (Only (..))
saveAppSettings :: DB.Connection -> AppSettings -> IO ()
saveAppSettings db appSettings = do
DB.execute_ db "DELETE FROM app_settings"
DB.execute db "INSERT INTO app_settings (app_settings) VALUES (?)" (Only $ J.encode appSettings)
DB.execute db "INSERT INTO app_settings (app_settings) VALUES (?)" (Only $ encodeJSON appSettings)
getAppSettings :: DB.Connection -> Maybe AppSettings -> IO AppSettings
getAppSettings db platformDefaults = do
stored_ <- join <$> liftIO (maybeFirstRow (J.decodeStrict . fromOnly) $ DB.query_ db "SELECT app_settings FROM app_settings")
stored_ <- join <$> liftIO (maybeFirstRow (decodeJSON . fromOnly) $ DB.query_ db "SELECT app_settings FROM app_settings")
pure $ combineAppSettings (fromMaybe defaultAppSettings platformDefaults) (fromMaybe defaultParseAppSettings stored_)

View File

@@ -23,6 +23,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20251007_connections_sync
import Simplex.Chat.Store.Postgres.Migrations.M20251017_chat_tags_cascade
import Simplex.Chat.Store.Postgres.Migrations.M20251117_member_relations_vector
import Simplex.Chat.Store.Postgres.Migrations.M20251128_migrate_member_relations
import Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -45,7 +46,8 @@ schemaMigrations =
("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync),
("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade),
("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector),
("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations)
("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations),
("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables)
]
-- | The list of migrations in ascending order by date

View File

@@ -0,0 +1,26 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20251230_strict_tables where
import Data.Text (Text)
import Text.RawString.QQ (r)
import Simplex.Messaging.Agent.Store.Postgres.Migrations.M20251230_strict_tables (isValidText)
m20251230_strict_tables :: Text
m20251230_strict_tables =
isValidText
<> [r|
DELETE FROM calls
WHERE NOT simplex_is_valid_text(call_state);
ALTER TABLE calls ALTER COLUMN call_state TYPE TEXT USING call_state::TEXT;
DROP FUNCTION simplex_is_valid_text(BYTEA);
|]
down_m20251230_strict_tables :: Text
down_m20251230_strict_tables =
[r|
ALTER TABLE calls ALTER COLUMN call_state TYPE BYTEA USING call_state::BYTEA;
|]

View File

@@ -173,7 +173,7 @@ CREATE TABLE test_chat_schema.calls (
contact_id bigint NOT NULL,
shared_call_id bytea NOT NULL,
chat_item_id bigint NOT NULL,
call_state bytea NOT NULL,
call_state text NOT NULL,
call_ts timestamp with time zone NOT NULL,
user_id bigint NOT NULL,
created_at timestamp with time zone DEFAULT now() NOT NULL,

View File

@@ -146,6 +146,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20251007_connections_sync
import Simplex.Chat.Store.SQLite.Migrations.M20251017_chat_tags_cascade
import Simplex.Chat.Store.SQLite.Migrations.M20251117_member_relations_vector
import Simplex.Chat.Store.SQLite.Migrations.M20251128_migrate_member_relations
import Simplex.Chat.Store.SQLite.Migrations.M20251230_strict_tables
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -291,7 +292,8 @@ schemaMigrations =
("20251007_connections_sync", m20251007_connections_sync, Just down_m20251007_connections_sync),
("20251017_chat_tags_cascade", m20251017_chat_tags_cascade, Just down_m20251017_chat_tags_cascade),
("20251117_member_relations_vector", m20251117_member_relations_vector, Just down_m20251117_member_relations_vector),
("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations)
("20251128_migrate_member_relations", m20251128_migrate_member_relations, Just down_m20251128_migrate_member_relations),
("20251230_strict_tables", m20251230_strict_tables, Just down_m20251230_strict_tables)
]
-- | The list of migrations in ascending order by date

View File

@@ -0,0 +1,70 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.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 group_members
SET member_role = CAST(member_role as TEXT),
member_restriction = CAST(member_restriction as TEXT);
UPDATE user_contact_links SET group_link_member_role = CAST(group_link_member_role as TEXT);
UPDATE app_settings SET app_settings = CAST(app_settings as TEXT);
PRAGMA writable_schema=1;
UPDATE sqlite_master
SET sql = replace(sql, 'call_state BLOB NOT NULL', 'call_state TEXT NOT NULL')
WHERE type = 'table' AND name = 'calls';
UPDATE sqlite_master
SET sql = replace(sql, 'local_alias DEFAULT', 'local_alias TEXT DEFAULT')
WHERE type = 'table' AND name = 'connections';
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';
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, 'call_state TEXT NOT NULL', 'call_state BLOB NOT NULL')
WHERE type = 'table' AND name = 'calls';
UPDATE sqlite_master
SET sql = replace(sql, 'local_alias TEXT DEFAULT', 'local_alias DEFAULT')
WHERE type = 'table' AND name = 'connections';
PRAGMA writable_schema=0;
UPDATE group_members
SET member_role = CAST(member_role as BLOB),
member_restriction = CAST(member_restriction as BLOB);
UPDATE user_contact_links SET group_link_member_role = CAST(group_link_member_role as BLOB);
UPDATE app_settings SET app_settings = CAST(app_settings as BLOB);
|]

View File

@@ -1255,10 +1255,6 @@ Query: UPDATE snd_file_chunk_replicas SET replica_status = ?, updated_at = ? WHE
Plan:
SEARCH snd_file_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE snd_files SET deleted = 1, updated_at = ? WHERE snd_file_id = ?
Plan:
SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE snd_files SET prefix_path = NULL, status = ?, updated_at = ? WHERE snd_file_id = ?
Plan:
SEARCH snd_files USING INTEGER PRIMARY KEY (rowid=?)

View File

@@ -2,7 +2,7 @@ CREATE TABLE migrations(
name TEXT NOT NULL PRIMARY KEY,
ts TEXT NOT NULL,
down TEXT
);
) STRICT;
CREATE TABLE contact_profiles(
-- remote user profile
contact_profile_id INTEGER PRIMARY KEY,
@@ -20,7 +20,7 @@ CREATE TABLE contact_profiles(
contact_link BLOB,
short_descr TEXT,
chat_peer_type TEXT
);
) STRICT;
CREATE TABLE users(
user_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE RESTRICT
@@ -44,7 +44,7 @@ CREATE TABLE users(
ON DELETE RESTRICT
ON UPDATE CASCADE
DEFERRABLE INITIALLY DEFERRED
);
) STRICT;
CREATE TABLE display_names(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
local_display_name TEXT NOT NULL,
@@ -54,7 +54,7 @@ CREATE TABLE display_names(
updated_at TEXT CHECK(updated_at NOT NULL),
PRIMARY KEY(user_id, local_display_name) ON CONFLICT FAIL,
UNIQUE(user_id, ldn_base, ldn_suffix) ON CONFLICT FAIL
) WITHOUT ROWID;
) WITHOUT ROWID, STRICT;
CREATE TABLE contacts(
contact_id INTEGER PRIMARY KEY,
contact_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL,
@@ -96,7 +96,7 @@ CREATE TABLE contacts(
ON UPDATE CASCADE,
UNIQUE(user_id, local_display_name),
UNIQUE(user_id, contact_profile_id)
);
) STRICT;
CREATE TABLE known_servers(
server_id INTEGER PRIMARY KEY,
host TEXT NOT NULL,
@@ -106,7 +106,7 @@ CREATE TABLE known_servers(
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
UNIQUE(user_id, host, port)
) WITHOUT ROWID;
) WITHOUT ROWID, STRICT;
CREATE TABLE group_profiles(
-- shared group profiles
group_profile_id INTEGER PRIMARY KEY,
@@ -122,7 +122,7 @@ CREATE TABLE group_profiles(
description TEXT NULL,
member_admission TEXT,
short_descr TEXT
);
) STRICT;
CREATE TABLE groups(
group_id INTEGER PRIMARY KEY, -- local group ID
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
@@ -163,7 +163,7 @@ CREATE TABLE groups(
ON UPDATE CASCADE,
UNIQUE(user_id, local_display_name),
UNIQUE(user_id, group_profile_id)
);
) STRICT;
CREATE TABLE group_members(
-- group members, excluding the local user
group_member_id INTEGER PRIMARY KEY,
@@ -203,7 +203,7 @@ CREATE TABLE group_members(
ON DELETE CASCADE
ON UPDATE CASCADE,
UNIQUE(group_id, member_id)
);
) STRICT;
CREATE TABLE group_member_intros(
group_member_intro_id INTEGER PRIMARY KEY,
re_group_member_id INTEGER NOT NULL REFERENCES group_members(group_member_id) ON DELETE CASCADE,
@@ -215,7 +215,7 @@ CREATE TABLE group_member_intros(
updated_at TEXT CHECK(updated_at NOT NULL),
intro_chat_protocol_version INTEGER NOT NULL DEFAULT 3, -- see GroupMemberIntroStatus
UNIQUE(re_group_member_id, to_group_member_id)
);
) STRICT;
CREATE TABLE files(
file_id INTEGER PRIMARY KEY,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
@@ -240,7 +240,7 @@ CREATE TABLE files(
file_crypto_nonce BLOB,
note_folder_id INTEGER DEFAULT NULL REFERENCES note_folders ON DELETE CASCADE,
redirect_file_id INTEGER REFERENCES files ON DELETE CASCADE
);
) STRICT;
CREATE TABLE snd_files(
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
connection_id INTEGER NOT NULL REFERENCES connections ON DELETE CASCADE,
@@ -253,7 +253,7 @@ CREATE TABLE snd_files(
file_descr_id INTEGER NULL
REFERENCES xftp_file_descriptions ON DELETE SET NULL,
PRIMARY KEY(file_id, connection_id)
) WITHOUT ROWID;
) WITHOUT ROWID, STRICT;
CREATE TABLE rcv_files(
file_id INTEGER PRIMARY KEY REFERENCES files ON DELETE CASCADE,
file_status TEXT NOT NULL, -- new, accepted, connected, completed
@@ -270,7 +270,7 @@ CREATE TABLE rcv_files(
agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL),
to_receive INTEGER,
user_approved_relays INTEGER NOT NULL DEFAULT 0
);
) STRICT;
CREATE TABLE rcv_file_chunks(
file_id INTEGER NOT NULL REFERENCES rcv_files ON DELETE CASCADE,
chunk_number INTEGER NOT NULL,
@@ -279,7 +279,7 @@ CREATE TABLE rcv_file_chunks(
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL), -- 0(received), 1(appended to file)
PRIMARY KEY(file_id, chunk_number)
) WITHOUT ROWID;
) WITHOUT ROWID, STRICT;
CREATE TABLE connections(
-- all SMP agent connections
connection_id INTEGER PRIMARY KEY,
@@ -302,7 +302,7 @@ CREATE TABLE connections(
REFERENCES user_contact_links(user_contact_link_id) ON DELETE SET NULL,
custom_user_profile_id INTEGER REFERENCES contact_profiles ON DELETE SET NULL,
conn_req_inv BLOB,
local_alias DEFAULT '' CHECK(local_alias NOT NULL),
local_alias TEXT DEFAULT '' CHECK(local_alias NOT NULL),
via_group_link INTEGER DEFAULT 0 CHECK(via_group_link NOT NULL),
group_link_id BLOB,
security_code TEXT NULL,
@@ -325,7 +325,7 @@ CREATE TABLE connections(
REFERENCES snd_files(file_id, connection_id)
ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED
);
) STRICT;
CREATE TABLE user_contact_links(
user_contact_link_id INTEGER PRIMARY KEY,
conn_req_contact BLOB NOT NULL,
@@ -344,7 +344,7 @@ CREATE TABLE user_contact_links(
short_link_data_set INTEGER NOT NULL DEFAULT 0,
short_link_large_data_set INTEGER NOT NULL DEFAULT 0,
UNIQUE(user_id, local_display_name)
);
) STRICT;
CREATE TABLE contact_requests(
contact_request_id INTEGER PRIMARY KEY,
user_contact_link_id INTEGER REFERENCES user_contact_links
@@ -372,7 +372,7 @@ CREATE TABLE contact_requests(
DEFERRABLE INITIALLY DEFERRED,
UNIQUE(user_id, local_display_name),
UNIQUE(user_id, contact_profile_id)
);
) STRICT;
CREATE TABLE messages(
message_id INTEGER PRIMARY KEY,
msg_sent INTEGER NOT NULL, -- 0 for received, 1 for sent
@@ -388,14 +388,14 @@ CREATE TABLE messages(
author_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL,
forwarded_by_group_member_id INTEGER REFERENCES group_members ON DELETE SET NULL,
broker_ts TEXT
);
) STRICT;
CREATE TABLE pending_group_messages(
pending_group_message_id INTEGER PRIMARY KEY,
group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE,
message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE chat_items(
chat_item_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
@@ -441,7 +441,7 @@ CREATE TABLE chat_items(
group_scope_tag TEXT,
group_scope_group_member_id INTEGER REFERENCES group_members(group_member_id) ON DELETE CASCADE,
show_group_as_sender INTEGER NOT NULL DEFAULT 0
);
) STRICT;
CREATE TABLE sqlite_sequence(name,seq);
CREATE TABLE chat_item_messages(
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
@@ -449,21 +449,21 @@ CREATE TABLE chat_item_messages(
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
UNIQUE(chat_item_id, message_id)
);
) STRICT;
CREATE TABLE calls(
-- stores call invitations state for communicating state between NSE and app when call notification comes
call_id INTEGER PRIMARY KEY,
contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE,
shared_call_id BLOB NOT NULL,
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
call_state BLOB NOT NULL,
call_state TEXT NOT NULL,
call_ts TEXT NOT NULL,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
,
call_uuid TEXT NOT NULL DEFAULT ""
);
) STRICT;
CREATE TABLE commands(
command_id INTEGER PRIMARY KEY AUTOINCREMENT, -- used as ACorrId
connection_id INTEGER REFERENCES connections ON DELETE CASCADE,
@@ -472,14 +472,14 @@ CREATE TABLE commands(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE settings(
settings_id INTEGER PRIMARY KEY,
chat_item_ttl INTEGER,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE IF NOT EXISTS "protocol_servers"(
smp_server_id INTEGER PRIMARY KEY,
host TEXT NOT NULL,
@@ -494,7 +494,7 @@ CREATE TABLE IF NOT EXISTS "protocol_servers"(
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
protocol TEXT NOT NULL DEFAULT 'smp',
UNIQUE(user_id, host, port)
);
) STRICT;
CREATE TABLE xftp_file_descriptions(
file_descr_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
@@ -503,7 +503,7 @@ CREATE TABLE xftp_file_descriptions(
file_descr_complete INTEGER NOT NULL DEFAULT(0),
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE extra_xftp_file_descriptions(
extra_file_descr_id INTEGER PRIMARY KEY,
file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE,
@@ -511,7 +511,7 @@ CREATE TABLE extra_xftp_file_descriptions(
file_descr_text TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE chat_item_versions(
-- contains versions only for edited chat items, including current version
chat_item_version_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -520,7 +520,7 @@ CREATE TABLE chat_item_versions(
item_version_ts TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE chat_item_reactions(
chat_item_reaction_id INTEGER PRIMARY KEY AUTOINCREMENT,
item_member_id BLOB, -- member that created item, NULL for items in direct chats
@@ -534,7 +534,7 @@ CREATE TABLE chat_item_reactions(
reaction_ts TEXT NOT NULL, -- broker_ts of creating message for received, created_at for sent
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE chat_item_moderations(
chat_item_moderation_id INTEGER PRIMARY KEY,
group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE,
@@ -545,7 +545,7 @@ CREATE TABLE chat_item_moderations(
moderated_at TEXT NOT NULL, -- broker_ts of creating message
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE group_snd_item_statuses(
group_snd_item_status_id INTEGER PRIMARY KEY,
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
@@ -555,7 +555,7 @@ CREATE TABLE group_snd_item_statuses(
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
,
via_proxy INTEGER
);
) STRICT;
CREATE TABLE IF NOT EXISTS "sent_probes"(
sent_probe_id INTEGER PRIMARY KEY,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
@@ -565,7 +565,7 @@ CREATE TABLE IF NOT EXISTS "sent_probes"(
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
UNIQUE(user_id, probe)
);
) STRICT;
CREATE TABLE IF NOT EXISTS "sent_probe_hashes"(
sent_probe_hash_id INTEGER PRIMARY KEY,
sent_probe_id INTEGER NOT NULL REFERENCES "sent_probes" ON DELETE CASCADE,
@@ -574,7 +574,7 @@ CREATE TABLE IF NOT EXISTS "sent_probe_hashes"(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL)
);
) STRICT;
CREATE TABLE IF NOT EXISTS "received_probes"(
received_probe_id INTEGER PRIMARY KEY,
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
@@ -584,7 +584,7 @@ CREATE TABLE IF NOT EXISTS "received_probes"(
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL)
);
) STRICT;
CREATE TABLE remote_hosts(
-- e.g., mobiles known to a desktop app
remote_host_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -599,7 +599,7 @@ CREATE TABLE remote_hosts(
bind_addr TEXT,
bind_iface TEXT,
bind_port INTEGER
);
) STRICT;
CREATE TABLE remote_controllers(
-- e.g., desktops known to a mobile app
remote_ctrl_id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -610,7 +610,7 @@ CREATE TABLE remote_controllers(
id_pub BLOB NOT NULL, -- remote controller long-term/identity key to verify signatures
dh_priv_key BLOB NOT NULL, -- last session DH key
prev_dh_priv_key BLOB -- previous session DH key
);
) STRICT;
CREATE TABLE IF NOT EXISTS "msg_deliveries"(
msg_delivery_id INTEGER PRIMARY KEY,
message_id INTEGER NOT NULL REFERENCES messages ON DELETE CASCADE, -- non UNIQUE for group messages and for batched messages
@@ -621,7 +621,7 @@ CREATE TABLE IF NOT EXISTS "msg_deliveries"(
created_at TEXT CHECK(created_at NOT NULL),
updated_at TEXT CHECK(updated_at NOT NULL),
delivery_status TEXT -- MsgDeliveryStatus
);
) STRICT;
CREATE TABLE note_folders(
note_folder_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
@@ -630,8 +630,8 @@ CREATE TABLE note_folders(
chat_ts TEXT NOT NULL DEFAULT(datetime('now')),
favorite INTEGER NOT NULL DEFAULT 0,
unread_chat INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE app_settings(app_settings TEXT NOT NULL);
) STRICT;
CREATE TABLE app_settings(app_settings TEXT NOT NULL) STRICT;
CREATE TABLE server_operators(
server_operator_id INTEGER PRIMARY KEY AUTOINCREMENT,
server_operator_tag TEXT,
@@ -645,14 +645,14 @@ CREATE TABLE server_operators(
xftp_role_proxy INTEGER NOT NULL DEFAULT 1,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE usage_conditions(
usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
conditions_commit TEXT NOT NULL UNIQUE,
notified_at TEXT,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE operator_usage_conditions(
operator_usage_conditions_id INTEGER PRIMARY KEY AUTOINCREMENT,
server_operator_id INTEGER REFERENCES server_operators(server_operator_id) ON DELETE SET NULL ON UPDATE CASCADE,
@@ -662,26 +662,26 @@ CREATE TABLE operator_usage_conditions(
created_at TEXT NOT NULL DEFAULT(datetime('now'))
,
auto_accepted INTEGER DEFAULT 0
);
) STRICT;
CREATE TABLE chat_tags(
chat_tag_id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users ON DELETE CASCADE,
chat_tag_text TEXT NOT NULL,
chat_tag_emoji TEXT,
tag_order INTEGER NOT NULL
);
) STRICT;
CREATE TABLE chat_tags_chats(
contact_id INTEGER REFERENCES contacts ON DELETE CASCADE,
group_id INTEGER REFERENCES groups ON DELETE CASCADE,
chat_tag_id INTEGER NOT NULL REFERENCES chat_tags ON DELETE CASCADE
);
) STRICT;
CREATE TABLE chat_item_mentions(
chat_item_mention_id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE,
member_id BLOB NOT NULL,
chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE,
display_name TEXT NOT NULL
);
) STRICT;
CREATE TABLE delivery_tasks(
delivery_task_id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE,
@@ -697,7 +697,7 @@ CREATE TABLE delivery_tasks(
failed INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE delivery_jobs(
delivery_job_id INTEGER PRIMARY KEY AUTOINCREMENT,
group_id INTEGER NOT NULL REFERENCES groups ON DELETE CASCADE,
@@ -713,16 +713,16 @@ CREATE TABLE delivery_jobs(
failed INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT(datetime('now')),
updated_at TEXT NOT NULL DEFAULT(datetime('now'))
);
) STRICT;
CREATE TABLE group_member_status_predicates(
member_status TEXT NOT NULL PRIMARY KEY,
current_member INTEGER NOT NULL DEFAULT 0
);
) STRICT;
CREATE TABLE connections_sync(
connections_sync_id INTEGER PRIMARY KEY AUTOINCREMENT,
should_sync INTEGER NOT NULL DEFAULT 0,
last_sync_ts TEXT
);
) STRICT;
CREATE INDEX contact_profiles_index ON contact_profiles(
display_name,
full_name

View File

@@ -58,7 +58,7 @@ import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, sumTypeJSON)
import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>))
import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8)
import Simplex.Messaging.Version
import Simplex.Messaging.Version.Internal
#if defined(dbPostgres)
@@ -871,27 +871,26 @@ data MemberRestrictionStatus
| MRSUnknown Text
deriving (Eq, Show)
instance FromField MemberRestrictionStatus where fromField = blobFieldDecoder strDecode
instance FromField MemberRestrictionStatus where fromField = fromTextField_ textDecode
instance ToField MemberRestrictionStatus where toField = toField . strEncode
instance ToField MemberRestrictionStatus where toField = toField . textEncode
instance StrEncoding MemberRestrictionStatus where
strEncode = \case
instance TextEncoding MemberRestrictionStatus where
textEncode = \case
MRSBlocked -> "blocked"
MRSUnrestricted -> "unrestricted"
MRSUnknown tag -> encodeUtf8 tag
strDecode s = Right $ case s of
MRSUnknown tag -> tag
textDecode s = Just $ case s of
"blocked" -> MRSBlocked
"unrestricted" -> MRSUnrestricted
tag -> MRSUnknown $ safeDecodeUtf8 tag
strP = strDecode <$?> A.takeByteString
tag -> MRSUnknown tag
instance FromJSON MemberRestrictionStatus where
parseJSON = strParseJSON "MemberRestrictionStatus"
parseJSON = textParseJSON "MemberRestrictionStatus"
instance ToJSON MemberRestrictionStatus where
toJSON = strToJSON
toEncoding = strToJEncoding
toJSON = textToJSON
toEncoding = textToEncoding
mrsBlocked :: MemberRestrictionStatus -> Bool
mrsBlocked = \case

View File

@@ -782,7 +782,7 @@ groupPrefStateText :: HasField "enable" p GroupFeatureEnabled => GroupFeature ->
groupPrefStateText feature pref param role =
let enabled = getField @"enable" pref
paramText = if enabled == FEOn then groupParamText_ feature param else ""
roleText = maybe "" (\r -> " for " <> safeDecodeUtf8 (strEncode r) <> "s") role
roleText = maybe "" (\r -> " for " <> textEncode r <> "s") role
in groupFeatureNameText feature <> ": " <> safeDecodeUtf8 (strEncode enabled) <> paramText <> roleText
groupParamText_ :: GroupFeature -> Maybe Int -> Text

View File

@@ -7,7 +7,7 @@ import Data.Aeson (FromJSON (..), ToJSON (..))
import qualified Data.Attoparsec.ByteString.Char8 as A
import qualified Data.ByteString.Char8 as B
import Simplex.Chat.Options.DB (FromField (..), ToField (..))
import Simplex.Messaging.Agent.Store.DB (blobFieldDecoder)
import Simplex.Messaging.Agent.Store.DB (fromTextField_)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Util ((<$?>))
@@ -20,34 +20,33 @@ data GroupMemberRole
| GROwner -- + delete and change group information, add/remove/change roles for Owners
deriving (Eq, Show, Ord)
instance FromField GroupMemberRole where fromField = blobFieldDecoder strDecode
instance FromField GroupMemberRole where fromField = fromTextField_ textDecode
instance ToField GroupMemberRole where toField = toField . strEncode
instance ToField GroupMemberRole where toField = toField . textEncode
instance StrEncoding GroupMemberRole where
strEncode = \case
instance TextEncoding GroupMemberRole where
textEncode = \case
GROwner -> "owner"
GRAdmin -> "admin"
GRModerator -> "moderator"
GRMember -> "member"
GRAuthor -> "author"
GRObserver -> "observer"
strDecode = \case
"owner" -> Right GROwner
"admin" -> Right GRAdmin
"moderator" -> Right GRModerator
"member" -> Right GRMember
"author" -> Right GRAuthor
"observer" -> Right GRObserver
r -> Left $ "bad GroupMemberRole " <> B.unpack r
strP = strDecode <$?> A.takeByteString
textDecode = \case
"owner" -> Just GROwner
"admin" -> Just GRAdmin
"moderator" -> Just GRModerator
"member" -> Just GRMember
"author" -> Just GRAuthor
"observer" -> Just GRObserver
r -> Nothing
instance FromJSON GroupMemberRole where
parseJSON = strParseJSON "GroupMemberRole"
parseJSON = textParseJSON "GroupMemberRole"
instance ToJSON GroupMemberRole where
toJSON = strToJSON
toEncoding = strToJEncoding
toJSON = textToJSON
toEncoding = textToEncoding
data GroupAcceptance = GAAccepted | GAPendingApproval | GAPendingReview deriving (Eq, Show)

View File

@@ -1231,7 +1231,7 @@ viewConnectedToGroupMember g@GroupInfo {groupId} m@GroupMember {groupMemberId, m
viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString]
viewReceivedGroupInvitation g c role =
ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role)
ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> showRole role
: case incognitoMembershipProfile g of
Just mp -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)]
Nothing -> ["use " <> highlight ("/j " <> viewGroupName g) <> " to accept"]
@@ -1270,15 +1270,15 @@ viewMembersBlockedForAllUser g members blocked = case members of
mems' -> [ttyGroup' g <> ": you " <> (if blocked then "blocked" else "unblocked") <> " " <> sShow (length mems') <> " members"]
showRole :: GroupMemberRole -> StyledString
showRole = plain . strEncode
showRole = plain . textEncode
viewGroupMembers :: Group -> [StyledString]
viewGroupMembers (Group GroupInfo {membership} members) = map groupMember . filter (not . removedOrLeft) $ membership : members
where
removedOrLeft m = let s = memberStatus m in s == GSMemRejected || s == GSMemRemoved || s == GSMemLeft
groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (intercalate ", " $ [role m] <> category m <> status m <> muted m)
role :: GroupMember -> String
role GroupMember {memberRole} = B.unpack $ strEncode memberRole
groupMember m = memIncognito m <> ttyFullMember m <> ": " <> plain (T.intercalate ", " $ [role m] <> category m <> status m <> muted m)
role :: GroupMember -> Text
role GroupMember {memberRole} = textEncode memberRole
category m = case memberCategory m of
GCUserMember -> ["you"]
GCInviteeMember -> ["invited"]
@@ -2455,8 +2455,8 @@ viewChatError isCmd logLevel testView = \case
CEGroupUserRole g role ->
(: []) . (ttyGroup' g <>) $ case role of
GRAuthor -> ": you don't have permission to send messages"
_ -> ": you have insufficient permissions for this action, the required role is " <> plain (strEncode role)
CEGroupMemberInitialRole g role -> [ttyGroup' g <> ": initial role for group member cannot be " <> plain (strEncode role) <> ", use member or observer"]
_ -> ": you have insufficient permissions for this action, the required role is " <> showRole role
CEGroupMemberInitialRole g role -> [ttyGroup' g <> ": initial role for group member cannot be " <> showRole role <> ", use member or observer"]
CEContactIncognitoCantInvite -> ["you're using your main profile for this group - prohibited to invite contacts to whom you are connected incognito"]
CEGroupIncognitoCantInvite -> ["you are using an incognito profile for this group - prohibited to invite contacts"]
CEGroupContactRole c -> ["contact " <> ttyContact c <> " has insufficient permissions for this group action"]

View File

@@ -8389,7 +8389,7 @@ testChannelsRelayDeliver =
createChannel5 :: TestCC -> TestCC -> TestCC -> TestCC -> TestCC -> GroupMemberRole -> IO ()
createChannel5 alice bob cath dan eve mRole = do
createGroup2 "team" alice bob
bob ##> ("/create link #team " <> B.unpack (strEncode mRole))
bob ##> ("/create link #team " <> T.unpack (textEncode mRole))
gLink <- getGroupLink bob "team" mRole True
cath ##> ("/c " <> gLink)
cath <## "connection request sent!"

View File

@@ -593,7 +593,7 @@ getGroupLink_ cc gName mRole created = do
cc <## ""
link <- getTermLine cc
cc <## ""
cc <## ("Anybody can connect to you and join group as " <> B.unpack (strEncode mRole) <> " with: /c <group_link_above>")
cc <## ("Anybody can connect to you and join group as " <> T.unpack (textEncode mRole) <> " with: /c <group_link_above>")
cc <## ("to show it again: /show link #" <> gName)
cc <## ("to delete it: /delete link #" <> gName <> " (joined members will remain connected to you)")
pure link
@@ -809,12 +809,12 @@ fullAddMember :: HasCallStack => String -> String -> TestCC -> TestCC -> GroupMe
fullAddMember gName fullName inviting invitee role = do
name1 <- userName inviting
memName <- userName invitee
inviting ##> ("/a " <> gName <> " " <> memName <> " " <> B.unpack (strEncode role))
inviting ##> ("/a " <> gName <> " " <> memName <> " " <> T.unpack (textEncode role))
let fullName' = if null fullName || fullName == gName then "" else " (" <> fullName <> ")"
concurrentlyN_
[ inviting <## ("invitation to join the group #" <> gName <> " sent to " <> memName),
do
invitee <## ("#" <> gName <> fullName' <> ": " <> name1 <> " invites you to join the group as " <> B.unpack (strEncode role))
invitee <## ("#" <> gName <> fullName' <> ": " <> name1 <> " invites you to join the group as " <> T.unpack (textEncode role))
invitee <## ("use /j " <> gName <> " to accept")
]

View File

@@ -17,7 +17,7 @@ import Data.Maybe (fromJust, isJust)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Database.SQLite.Simple (Query (..))
import Database.SQLite.Simple (Only (..), Query (..))
import Simplex.Chat.Options.SQLite (chatDBFunctions)
import Simplex.Chat.Store (createChatStore)
import qualified Simplex.Chat.Store as Store
@@ -59,6 +59,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
testVerifySchemaDump :: IO ()
testVerifySchemaDump = withTmpFiles $ do
@@ -101,6 +102,12 @@ testSchemaMigrations = withTmpFiles $ do
schema''' <- getSchema testDB testSchema
schema''' `shouldBe` schema'
testVerifyStrict :: IO ()
testVerifyStrict = do
Right st <- createDBStore (DBOpts testDB chatDBFunctions "" False True TQOff) Store.migrations (MigrationConfig MCConsole Nothing)
withConnection st (`DB.query_` "SELECT name FROM sqlite_master WHERE type = 'table' AND name != 'sqlite_sequence' AND sql NOT LIKE '% STRICT'")
`shouldReturn` ([] :: [Only Text])
skipComparisonForUpMigrations :: [String]
skipComparisonForUpMigrations =
[ -- schema doesn't change