deltachat/sql/
migrations.rs

1//! Migrations module.
2
3use anyhow::{ensure, Context as _, Result};
4use deltachat_contact_tools::EmailAddress;
5use rusqlite::OptionalExtension;
6
7use crate::config::Config;
8use crate::configure::EnteredLoginParam;
9use crate::constants::ShowEmails;
10use crate::context::Context;
11use crate::imap;
12use crate::login_param::ConfiguredLoginParam;
13use crate::message::MsgId;
14use crate::provider::get_provider_by_domain;
15use crate::sql::Sql;
16use crate::tools::inc_and_check;
17
18const DBVERSION: i32 = 68;
19const VERSION_CFG: &str = "dbversion";
20const TABLES: &str = include_str!("./tables.sql");
21
22pub async fn run(context: &Context, sql: &Sql) -> Result<(bool, bool, bool, bool)> {
23    let mut recalc_fingerprints = false;
24    let mut exists_before_update = false;
25    let mut dbversion_before_update = DBVERSION;
26
27    if !sql
28        .table_exists("config")
29        .await
30        .context("failed to check if config table exists")?
31    {
32        sql.transaction(move |transaction| {
33            transaction.execute_batch(TABLES)?;
34
35            // set raw config inside the transaction
36            transaction.execute(
37                "INSERT INTO config (keyname, value) VALUES (?, ?);",
38                (VERSION_CFG, format!("{dbversion_before_update}")),
39            )?;
40            Ok(())
41        })
42        .await
43        .context("Creating tables failed")?;
44
45        let mut lock = context.sql.config_cache.write().await;
46        lock.insert(
47            VERSION_CFG.to_string(),
48            Some(format!("{dbversion_before_update}")),
49        );
50        drop(lock);
51    } else {
52        exists_before_update = true;
53        dbversion_before_update = sql
54            .get_raw_config_int(VERSION_CFG)
55            .await?
56            .unwrap_or_default();
57    }
58
59    let dbversion = dbversion_before_update;
60    let mut update_icons = !exists_before_update;
61    let mut disable_server_delete = false;
62    let mut recode_avatar = false;
63
64    if dbversion < 1 {
65        sql.execute_migration(
66            r#"
67CREATE TABLE leftgrps ( id INTEGER PRIMARY KEY, grpid TEXT DEFAULT '');
68CREATE INDEX leftgrps_index1 ON leftgrps (grpid);"#,
69            1,
70        )
71        .await?;
72    }
73    if dbversion < 2 {
74        sql.execute_migration(
75            "ALTER TABLE contacts ADD COLUMN authname TEXT DEFAULT '';",
76            2,
77        )
78        .await?;
79    }
80    if dbversion < 7 {
81        sql.execute_migration(
82            "CREATE TABLE keypairs (\
83                 id INTEGER PRIMARY KEY, \
84                 addr TEXT DEFAULT '' COLLATE NOCASE, \
85                 is_default INTEGER DEFAULT 0, \
86                 private_key, \
87                 public_key, \
88                 created INTEGER DEFAULT 0);",
89            7,
90        )
91        .await?;
92    }
93    if dbversion < 10 {
94        sql.execute_migration(
95            "CREATE TABLE acpeerstates (\
96                 id INTEGER PRIMARY KEY, \
97                 addr TEXT DEFAULT '' COLLATE NOCASE, \
98                 last_seen INTEGER DEFAULT 0, \
99                 last_seen_autocrypt INTEGER DEFAULT 0, \
100                 public_key, \
101                 prefer_encrypted INTEGER DEFAULT 0); \
102              CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);",
103            10,
104        )
105        .await?;
106    }
107    if dbversion < 12 {
108        sql.execute_migration(
109            r#"
110CREATE TABLE msgs_mdns ( msg_id INTEGER,  contact_id INTEGER);
111CREATE INDEX msgs_mdns_index1 ON msgs_mdns (msg_id);"#,
112            12,
113        )
114        .await?;
115    }
116    if dbversion < 17 {
117        sql.execute_migration(
118            r#"
119ALTER TABLE chats ADD COLUMN archived INTEGER DEFAULT 0;
120CREATE INDEX chats_index2 ON chats (archived);
121ALTER TABLE msgs ADD COLUMN starred INTEGER DEFAULT 0;
122CREATE INDEX msgs_index5 ON msgs (starred);"#,
123            17,
124        )
125        .await?;
126    }
127    if dbversion < 18 {
128        sql.execute_migration(
129            r#"
130ALTER TABLE acpeerstates ADD COLUMN gossip_timestamp INTEGER DEFAULT 0;
131ALTER TABLE acpeerstates ADD COLUMN gossip_key;"#,
132            18,
133        )
134        .await?;
135    }
136    if dbversion < 27 {
137        // chat.id=1 and chat.id=2 are the old deaddrops,
138        // the current ones are defined by chats.blocked=2
139        sql.execute_migration(
140            r#"
141DELETE FROM msgs WHERE chat_id=1 OR chat_id=2;
142CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);
143ALTER TABLE msgs ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
144ALTER TABLE msgs ADD COLUMN timestamp_rcvd INTEGER DEFAULT 0;"#,
145            27,
146        )
147        .await?;
148    }
149    if dbversion < 34 {
150        sql.execute_migration(
151            r#"
152ALTER TABLE msgs ADD COLUMN hidden INTEGER DEFAULT 0;
153ALTER TABLE msgs_mdns ADD COLUMN timestamp_sent INTEGER DEFAULT 0;
154ALTER TABLE acpeerstates ADD COLUMN public_key_fingerprint TEXT DEFAULT '';
155ALTER TABLE acpeerstates ADD COLUMN gossip_key_fingerprint TEXT DEFAULT '';
156CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
157CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);"#,
158            34,
159        )
160        .await?;
161        recalc_fingerprints = true;
162    }
163    if dbversion < 39 {
164        sql.execute_migration(
165            r#"
166CREATE TABLE tokens ( 
167  id INTEGER PRIMARY KEY, 
168  namespc INTEGER DEFAULT 0, 
169  foreign_id INTEGER DEFAULT 0, 
170  token TEXT DEFAULT '', 
171  timestamp INTEGER DEFAULT 0
172);
173ALTER TABLE acpeerstates ADD COLUMN verified_key;
174ALTER TABLE acpeerstates ADD COLUMN verified_key_fingerprint TEXT DEFAULT '';
175CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);"#,
176            39,
177        )
178        .await?;
179    }
180    if dbversion < 40 {
181        sql.execute_migration("ALTER TABLE jobs ADD COLUMN thread INTEGER DEFAULT 0;", 40)
182            .await?;
183    }
184    if dbversion < 44 {
185        sql.execute_migration("ALTER TABLE msgs ADD COLUMN mime_headers TEXT;", 44)
186            .await?;
187    }
188    if dbversion < 46 {
189        sql.execute_migration(
190            r#"
191ALTER TABLE msgs ADD COLUMN mime_in_reply_to TEXT;
192ALTER TABLE msgs ADD COLUMN mime_references TEXT;"#,
193            46,
194        )
195        .await?;
196    }
197    if dbversion < 47 {
198        sql.execute_migration("ALTER TABLE jobs ADD COLUMN tries INTEGER DEFAULT 0;", 47)
199            .await?;
200    }
201    if dbversion < 48 {
202        // NOTE: move_state is not used anymore
203        sql.execute_migration(
204            "ALTER TABLE msgs ADD COLUMN move_state INTEGER DEFAULT 1;",
205            48,
206        )
207        .await?;
208    }
209    if dbversion < 49 {
210        sql.execute_migration(
211            "ALTER TABLE chats ADD COLUMN gossiped_timestamp INTEGER DEFAULT 0;",
212            49,
213        )
214        .await?;
215    }
216    if dbversion < 50 {
217        // installations <= 0.100.1 used DC_SHOW_EMAILS_ALL implicitly;
218        // keep this default and use DC_SHOW_EMAILS_NO
219        // only for new installations
220        if exists_before_update {
221            sql.set_raw_config_int("show_emails", ShowEmails::All as i32)
222                .await?;
223        }
224        sql.set_db_version(50).await?;
225    }
226    if dbversion < 53 {
227        // the messages containing _only_ locations
228        // are also added to the database as _hidden_.
229        sql.execute_migration(
230            r#"
231CREATE TABLE locations ( 
232  id INTEGER PRIMARY KEY AUTOINCREMENT, 
233  latitude REAL DEFAULT 0.0, 
234  longitude REAL DEFAULT 0.0, 
235  accuracy REAL DEFAULT 0.0, 
236  timestamp INTEGER DEFAULT 0, 
237  chat_id INTEGER DEFAULT 0, 
238  from_id INTEGER DEFAULT 0
239);"
240CREATE INDEX locations_index1 ON locations (from_id);
241CREATE INDEX locations_index2 ON locations (timestamp);
242ALTER TABLE chats ADD COLUMN locations_send_begin INTEGER DEFAULT 0;
243ALTER TABLE chats ADD COLUMN locations_send_until INTEGER DEFAULT 0;
244ALTER TABLE chats ADD COLUMN locations_last_sent INTEGER DEFAULT 0;
245CREATE INDEX chats_index3 ON chats (locations_send_until);"#,
246            53,
247        )
248        .await?;
249    }
250    if dbversion < 54 {
251        sql.execute_migration(
252            r#"
253ALTER TABLE msgs ADD COLUMN location_id INTEGER DEFAULT 0;
254CREATE INDEX msgs_index6 ON msgs (location_id);"#,
255            54,
256        )
257        .await?;
258    }
259    if dbversion < 55 {
260        sql.execute_migration(
261            "ALTER TABLE locations ADD COLUMN independent INTEGER DEFAULT 0;",
262            55,
263        )
264        .await?;
265    }
266    if dbversion < 59 {
267        // records in the devmsglabels are kept when the message is deleted.
268        // so, msg_id may or may not exist.
269        sql.execute_migration(
270            r#"
271CREATE TABLE devmsglabels (id INTEGER PRIMARY KEY AUTOINCREMENT, label TEXT, msg_id INTEGER DEFAULT 0);
272CREATE INDEX devmsglabels_index1 ON devmsglabels (label);"#, 59)
273            .await?;
274        if exists_before_update && sql.get_raw_config_int("bcc_self").await?.is_none() {
275            sql.set_raw_config_int("bcc_self", 1).await?;
276        }
277    }
278
279    if dbversion < 60 {
280        sql.execute_migration(
281            "ALTER TABLE chats ADD COLUMN created_timestamp INTEGER DEFAULT 0;",
282            60,
283        )
284        .await?;
285    }
286    if dbversion < 61 {
287        sql.execute_migration(
288            "ALTER TABLE contacts ADD COLUMN selfavatar_sent INTEGER DEFAULT 0;",
289            61,
290        )
291        .await?;
292        update_icons = true;
293    }
294    if dbversion < 62 {
295        sql.execute_migration(
296            "ALTER TABLE chats ADD COLUMN muted_until INTEGER DEFAULT 0;",
297            62,
298        )
299        .await?;
300    }
301    if dbversion < 63 {
302        sql.execute_migration("UPDATE chats SET grpid='' WHERE type=100", 63)
303            .await?;
304    }
305    if dbversion < 64 {
306        sql.execute_migration("ALTER TABLE msgs ADD COLUMN error TEXT DEFAULT '';", 64)
307            .await?;
308    }
309    if dbversion < 65 {
310        sql.execute_migration(
311            r#"
312ALTER TABLE chats ADD COLUMN ephemeral_timer INTEGER;
313ALTER TABLE msgs ADD COLUMN ephemeral_timer INTEGER DEFAULT 0;
314ALTER TABLE msgs ADD COLUMN ephemeral_timestamp INTEGER DEFAULT 0;"#,
315            65,
316        )
317        .await?;
318    }
319    if dbversion < 66 {
320        update_icons = true;
321        sql.set_db_version(66).await?;
322    }
323    if dbversion < 67 {
324        for prefix in &["", "configured_"] {
325            if let Some(server_flags) = sql
326                .get_raw_config_int(&format!("{prefix}server_flags"))
327                .await?
328            {
329                let imap_socket_flags = server_flags & 0x700;
330                let key = &format!("{prefix}mail_security");
331                match imap_socket_flags {
332                    0x100 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
333                    0x200 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
334                    0x400 => sql.set_raw_config_int(key, 3).await?, // Plain
335                    _ => sql.set_raw_config_int(key, 0).await?,
336                }
337                let smtp_socket_flags = server_flags & 0x70000;
338                let key = &format!("{prefix}send_security");
339                match smtp_socket_flags {
340                    0x10000 => sql.set_raw_config_int(key, 2).await?, // STARTTLS
341                    0x20000 => sql.set_raw_config_int(key, 1).await?, // SSL/TLS
342                    0x40000 => sql.set_raw_config_int(key, 3).await?, // Plain
343                    _ => sql.set_raw_config_int(key, 0).await?,
344                }
345            }
346        }
347        sql.set_db_version(67).await?;
348    }
349    if dbversion < 68 {
350        // the index is used to speed up get_fresh_msg_cnt() (see comment there for more details) and marknoticed_chat()
351        sql.execute_migration(
352            "CREATE INDEX IF NOT EXISTS msgs_index7 ON msgs (state, hidden, chat_id);",
353            68,
354        )
355        .await?;
356    }
357    if dbversion < 69 {
358        sql.execute_migration(
359            r#"
360ALTER TABLE chats ADD COLUMN protected INTEGER DEFAULT 0;
361-- 120=group, 130=old verified group
362UPDATE chats SET protected=1, type=120 WHERE type=130;"#,
363            69,
364        )
365        .await?;
366    }
367
368    if dbversion < 71 {
369        if let Ok(addr) = context.get_primary_self_addr().await {
370            if let Ok(domain) = EmailAddress::new(&addr).map(|email| email.domain) {
371                context
372                    .set_config_internal(
373                        Config::ConfiguredProvider,
374                        get_provider_by_domain(&domain).map(|provider| provider.id),
375                    )
376                    .await?;
377            } else {
378                warn!(context, "Can't parse configured address: {:?}", addr);
379            }
380        }
381
382        sql.set_db_version(71).await?;
383    }
384    if dbversion < 72 && !sql.col_exists("msgs", "mime_modified").await? {
385        sql.execute_migration(
386            r#"
387    ALTER TABLE msgs ADD COLUMN mime_modified INTEGER DEFAULT 0;"#,
388            72,
389        )
390        .await?;
391    }
392    if dbversion < 73 {
393        use Config::*;
394        sql.execute(
395            r#"
396CREATE TABLE imap_sync (folder TEXT PRIMARY KEY, uidvalidity INTEGER DEFAULT 0, uid_next INTEGER DEFAULT 0);"#,
397()
398        )
399            .await?;
400        for c in &[
401            ConfiguredInboxFolder,
402            ConfiguredSentboxFolder,
403            ConfiguredMvboxFolder,
404        ] {
405            if let Some(folder) = context.get_config(*c).await? {
406                let (uid_validity, last_seen_uid) =
407                    imap::get_config_last_seen_uid(context, &folder).await?;
408                if last_seen_uid > 0 {
409                    imap::set_uid_next(context, &folder, last_seen_uid + 1).await?;
410                    imap::set_uidvalidity(context, &folder, uid_validity).await?;
411                }
412            }
413        }
414        if exists_before_update {
415            disable_server_delete = true;
416
417            // Don't disable server delete if it was on by default (Nauta):
418            if let Some(provider) = context.get_configured_provider().await? {
419                if let Some(defaults) = &provider.config_defaults {
420                    if defaults.iter().any(|d| d.key == Config::DeleteServerAfter) {
421                        disable_server_delete = false;
422                    }
423                }
424            }
425        }
426        sql.set_db_version(73).await?;
427    }
428    if dbversion < 74 {
429        sql.execute_migration("UPDATE contacts SET name='' WHERE name=authname", 74)
430            .await?;
431    }
432    if dbversion < 75 {
433        sql.execute_migration(
434            "ALTER TABLE contacts ADD COLUMN status TEXT DEFAULT '';",
435            75,
436        )
437        .await?;
438    }
439    if dbversion < 76 {
440        sql.execute_migration("ALTER TABLE msgs ADD COLUMN subject TEXT DEFAULT '';", 76)
441            .await?;
442    }
443    if dbversion < 77 {
444        recode_avatar = true;
445        sql.set_db_version(77).await?;
446    }
447    if dbversion < 78 {
448        // move requests to "Archived Chats",
449        // this way, the app looks familiar after the contact request upgrade.
450        sql.execute_migration("UPDATE chats SET archived=1 WHERE blocked=2;", 78)
451            .await?;
452    }
453    if dbversion < 79 {
454        sql.execute_migration(
455            r#"
456        ALTER TABLE msgs ADD COLUMN download_state INTEGER DEFAULT 0;
457        "#,
458            79,
459        )
460        .await?;
461    }
462    if dbversion < 80 {
463        sql.execute_migration(
464            r#"CREATE TABLE multi_device_sync (
465id INTEGER PRIMARY KEY AUTOINCREMENT,
466item TEXT DEFAULT '');"#,
467            80,
468        )
469        .await?;
470    }
471    if dbversion < 81 {
472        sql.execute_migration("ALTER TABLE msgs ADD COLUMN hop_info TEXT;", 81)
473            .await?;
474    }
475    if dbversion < 82 {
476        sql.execute_migration(
477            r#"CREATE TABLE imap (
478id INTEGER PRIMARY KEY AUTOINCREMENT,
479rfc724_mid TEXT DEFAULT '', -- Message-ID header
480folder TEXT DEFAULT '', -- IMAP folder
481target TEXT DEFAULT '', -- Destination folder, empty to delete.
482uid INTEGER DEFAULT 0, -- UID
483uidvalidity INTEGER DEFAULT 0,
484UNIQUE (folder, uid, uidvalidity)
485);
486CREATE INDEX imap_folder ON imap(folder);
487CREATE INDEX imap_messageid ON imap(rfc724_mid);
488
489INSERT INTO imap
490(rfc724_mid, folder, target, uid, uidvalidity)
491SELECT
492rfc724_mid,
493server_folder AS folder,
494server_folder AS target,
495server_uid AS uid,
496(SELECT uidvalidity FROM imap_sync WHERE folder=server_folder) AS uidvalidity
497FROM msgs
498WHERE server_uid>0
499ON CONFLICT (folder, uid, uidvalidity)
500DO UPDATE SET rfc724_mid=excluded.rfc724_mid,
501              target=excluded.target;
502"#,
503            82,
504        )
505        .await?;
506    }
507    if dbversion < 83 {
508        sql.execute_migration(
509            "ALTER TABLE imap_sync
510             ADD COLUMN modseq -- Highest modification sequence
511             INTEGER DEFAULT 0",
512            83,
513        )
514        .await?;
515    }
516    if dbversion < 84 {
517        sql.execute_migration(
518            r#"CREATE TABLE msgs_status_updates (
519id INTEGER PRIMARY KEY AUTOINCREMENT,
520msg_id INTEGER,
521update_item TEXT DEFAULT '',
522update_item_read INTEGER DEFAULT 0 -- XXX unused
523);
524CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);"#,
525            84,
526        )
527        .await?;
528    }
529    if dbversion < 85 {
530        sql.execute_migration(
531            r#"CREATE TABLE smtp (
532id INTEGER PRIMARY KEY,
533rfc724_mid TEXT NOT NULL,          -- Message-ID
534mime TEXT NOT NULL,                -- SMTP payload
535msg_id INTEGER NOT NULL,           -- ID of the message in `msgs` table
536recipients TEXT NOT NULL,          -- List of recipients separated by space
537retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
538);
539CREATE INDEX smtp_messageid ON imap(rfc724_mid);
540"#,
541            85,
542        )
543        .await?;
544    }
545    if dbversion < 86 {
546        sql.execute_migration(
547            r#"CREATE TABLE bobstate (
548                   id INTEGER PRIMARY KEY AUTOINCREMENT,
549                   invite TEXT NOT NULL,
550                   next_step INTEGER NOT NULL,
551                   chat_id INTEGER NOT NULL
552            );"#,
553            86,
554        )
555        .await?;
556    }
557    if dbversion < 87 {
558        // the index is used to speed up delete_expired_messages()
559        sql.execute_migration(
560            "CREATE INDEX IF NOT EXISTS msgs_index8 ON msgs (ephemeral_timestamp);",
561            87,
562        )
563        .await?;
564    }
565    if dbversion < 88 {
566        sql.execute_migration("DROP TABLE IF EXISTS backup_blobs;", 88)
567            .await?;
568    }
569    if dbversion < 89 {
570        sql.execute_migration(
571            r#"CREATE TABLE imap_markseen (
572              id INTEGER,
573              FOREIGN KEY(id) REFERENCES imap(id) ON DELETE CASCADE
574            );"#,
575            89,
576        )
577        .await?;
578    }
579    if dbversion < 90 {
580        sql.execute_migration(
581            r#"CREATE TABLE smtp_mdns (
582              msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN (DEPRECATED 2024-06-21)
583              from_id INTEGER NOT NULL, -- id of the contact that sent the message, MDN destination
584              rfc724_mid TEXT NOT NULL, -- Message-ID header
585              retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send MDN
586            );"#,
587            90,
588        )
589        .await?;
590    }
591    if dbversion < 91 {
592        sql.execute_migration(
593            r#"CREATE TABLE smtp_status_updates (
594              msg_id INTEGER NOT NULL UNIQUE, -- msg_id of the webxdc instance with pending updates
595              first_serial INTEGER NOT NULL, -- id in msgs_status_updates
596              last_serial INTEGER NOT NULL, -- id in msgs_status_updates
597              descr TEXT NOT NULL -- text to send along with the updates
598            );"#,
599            91,
600        )
601        .await?;
602    }
603    if dbversion < 92 {
604        sql.execute_migration(
605            r#"CREATE TABLE reactions (
606              msg_id INTEGER NOT NULL, -- id of the message reacted to
607              contact_id INTEGER NOT NULL, -- id of the contact reacting to the message
608              reaction TEXT DEFAULT '' NOT NULL, -- a sequence of emojis separated by spaces
609              PRIMARY KEY(msg_id, contact_id),
610              FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE -- delete reactions when message is deleted
611              FOREIGN KEY(contact_id) REFERENCES contacts(id) ON DELETE CASCADE -- delete reactions when contact is deleted
612            )"#,
613            92
614        ).await?;
615    }
616    if dbversion < 93 {
617        // `sending_domains` is now unused, but was not removed for backwards compatibility.
618        sql.execute_migration(
619            "CREATE TABLE sending_domains(domain TEXT PRIMARY KEY, dkim_works INTEGER DEFAULT 0);",
620            93,
621        )
622        .await?;
623    }
624    if dbversion < 94 {
625        sql.execute_migration(
626            // Create new `acpeerstates` table, same as before but with unique constraint.
627            //
628            // This allows to use `UPSERT` to update existing or insert a new peerstate
629            // depending on whether one exists already.
630            "CREATE TABLE new_acpeerstates (
631             id INTEGER PRIMARY KEY,
632             addr TEXT DEFAULT '' COLLATE NOCASE,
633             last_seen INTEGER DEFAULT 0,
634             last_seen_autocrypt INTEGER DEFAULT 0,
635             public_key,
636             prefer_encrypted INTEGER DEFAULT 0,
637             gossip_timestamp INTEGER DEFAULT 0,
638             gossip_key,
639             public_key_fingerprint TEXT DEFAULT '',
640             gossip_key_fingerprint TEXT DEFAULT '',
641             verified_key,
642             verified_key_fingerprint TEXT DEFAULT '',
643             UNIQUE (addr) -- Only one peerstate per address
644             );
645            INSERT OR IGNORE INTO new_acpeerstates SELECT
646                id, addr, last_seen, last_seen_autocrypt, public_key, prefer_encrypted,
647                gossip_timestamp, gossip_key, public_key_fingerprint,
648                gossip_key_fingerprint, verified_key, verified_key_fingerprint
649            FROM acpeerstates;
650            DROP TABLE acpeerstates;
651            ALTER TABLE new_acpeerstates RENAME TO acpeerstates;
652            CREATE INDEX acpeerstates_index1 ON acpeerstates (addr);
653            CREATE INDEX acpeerstates_index3 ON acpeerstates (public_key_fingerprint);
654            CREATE INDEX acpeerstates_index4 ON acpeerstates (gossip_key_fingerprint);
655            CREATE INDEX acpeerstates_index5 ON acpeerstates (verified_key_fingerprint);
656            ",
657            94,
658        )
659        .await?;
660    }
661    if dbversion < 95 {
662        sql.execute_migration(
663            "CREATE TABLE new_chats_contacts (chat_id INTEGER, contact_id INTEGER, UNIQUE(chat_id, contact_id));\
664            INSERT OR IGNORE INTO new_chats_contacts SELECT chat_id, contact_id FROM chats_contacts;\
665            DROP TABLE chats_contacts;\
666            ALTER TABLE new_chats_contacts RENAME TO chats_contacts;\
667            CREATE INDEX chats_contacts_index1 ON chats_contacts (chat_id);\
668            CREATE INDEX chats_contacts_index2 ON chats_contacts (contact_id);",
669            95
670        ).await?;
671    }
672    if dbversion < 96 {
673        sql.execute_migration(
674            "ALTER TABLE acpeerstates ADD COLUMN verifier TEXT DEFAULT '';",
675            96,
676        )
677        .await?;
678    }
679    if dbversion < 97 {
680        sql.execute_migration(
681            "CREATE TABLE dns_cache (
682               hostname TEXT NOT NULL,
683               address TEXT NOT NULL, -- IPv4 or IPv6 address
684               timestamp INTEGER NOT NULL,
685               UNIQUE (hostname, address)
686             )",
687            97,
688        )
689        .await?;
690    }
691    if dbversion < 98 {
692        if exists_before_update && sql.get_raw_config_int("show_emails").await?.is_none() {
693            sql.set_raw_config_int("show_emails", ShowEmails::Off as i32)
694                .await?;
695        }
696        sql.set_db_version(98).await?;
697    }
698    if dbversion < 99 {
699        // sql.execute_migration(
700        //     "ALTER TABLE msgs DROP COLUMN server_folder;
701        //      ALTER TABLE msgs DROP COLUMN server_uid;
702        //      ALTER TABLE msgs DROP COLUMN move_state;
703        //      ALTER TABLE chats DROP COLUMN draft_timestamp;
704        //      ALTER TABLE chats DROP COLUMN draft_txt",
705        //     99,
706        // )
707        // .await?;
708
709        // Reverted above, as it requires to load the whole DB in memory.
710        sql.set_db_version(99).await?;
711    }
712    if dbversion < 100 {
713        sql.execute_migration(
714            "ALTER TABLE msgs ADD COLUMN mime_compressed INTEGER NOT NULL DEFAULT 0",
715            100,
716        )
717        .await?;
718    }
719    if dbversion < 101 {
720        // Recreate `smtp` table with autoincrement.
721        // rfc724_mid index is not recreated, because it is not used.
722        sql.execute_migration(
723            "DROP TABLE smtp;
724             CREATE TABLE smtp (
725             id INTEGER PRIMARY KEY AUTOINCREMENT,
726             rfc724_mid TEXT NOT NULL,          -- Message-ID
727             mime TEXT NOT NULL,                -- SMTP payload
728             msg_id INTEGER NOT NULL,           -- ID of the message in `msgs` table
729             recipients TEXT NOT NULL,          -- List of recipients separated by space
730             retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
731            );
732            ",
733            101,
734        )
735        .await?;
736    }
737
738    if dbversion < 102 {
739        sql.execute_migration(
740            "CREATE TABLE download (
741            msg_id INTEGER NOT NULL -- id of the message stub in msgs table
742            )",
743            102,
744        )
745        .await?;
746    }
747
748    // Add is_bot column to contacts table with default false.
749    if dbversion < 103 {
750        sql.execute_migration(
751            "ALTER TABLE contacts ADD COLUMN is_bot INTEGER NOT NULL DEFAULT 0",
752            103,
753        )
754        .await?;
755    }
756
757    if dbversion < 104 {
758        sql.execute_migration(
759            "ALTER TABLE acpeerstates
760             ADD COLUMN secondary_verified_key;
761             ALTER TABLE acpeerstates
762             ADD COLUMN secondary_verified_key_fingerprint TEXT DEFAULT '';
763             ALTER TABLE acpeerstates
764             ADD COLUMN secondary_verifier TEXT DEFAULT ''",
765            104,
766        )
767        .await?;
768    }
769
770    if dbversion < 105 {
771        // Create UNIQUE uid column and drop unused update_item_read column.
772        sql.execute_migration(
773            r#"CREATE TABLE new_msgs_status_updates (
774id INTEGER PRIMARY KEY AUTOINCREMENT,
775msg_id INTEGER,
776update_item TEXT DEFAULT '',
777uid TEXT UNIQUE
778);
779INSERT OR IGNORE INTO new_msgs_status_updates SELECT
780  id, msg_id, update_item, NULL
781FROM msgs_status_updates;
782DROP TABLE msgs_status_updates;
783ALTER TABLE new_msgs_status_updates RENAME TO msgs_status_updates;
784CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);
785CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
786"#,
787            105,
788        )
789        .await?;
790    }
791
792    if dbversion < 106 {
793        // Recreate `config` table with UNIQUE constraint on `keyname`.
794        sql.execute_migration(
795            "CREATE TABLE new_config (
796               id INTEGER PRIMARY KEY,
797               keyname TEXT UNIQUE,
798               value TEXT NOT NULL
799             );
800             INSERT OR IGNORE INTO new_config SELECT
801               id, keyname, value
802             FROM config;
803             DROP TABLE config;
804             ALTER TABLE new_config RENAME TO config;
805             CREATE INDEX config_index1 ON config (keyname);",
806            106,
807        )
808        .await?;
809    }
810
811    if dbversion < 107 {
812        sql.execute_migration(
813            "CREATE TABLE new_keypairs (
814               id INTEGER PRIMARY KEY AUTOINCREMENT,
815               private_key UNIQUE NOT NULL,
816               public_key UNIQUE NOT NULL
817             );
818             INSERT OR IGNORE INTO new_keypairs SELECT id, private_key, public_key FROM keypairs;
819
820             INSERT OR IGNORE
821             INTO config (keyname, value)
822             VALUES
823             ('key_id', (SELECT id FROM new_keypairs
824                         WHERE private_key=
825                           (SELECT private_key FROM keypairs
826                            WHERE addr=(SELECT value FROM config WHERE keyname='configured_addr')
827                            AND is_default=1)));
828
829             -- We do not drop the old `keypairs` table for now,
830             -- but move it to `old_keypairs`. We can remove it later
831             -- in next migrations. This may be needed for recovery
832             -- in case something is wrong with the migration.
833             ALTER TABLE keypairs RENAME TO old_keypairs;
834             ALTER TABLE new_keypairs RENAME TO keypairs;
835             ",
836            107,
837        )
838        .await?;
839    }
840
841    if dbversion < 108 {
842        let version = 108;
843        let chunk_size = context.get_max_smtp_rcpt_to().await?;
844        sql.transaction(move |trans| {
845            Sql::set_db_version_trans(trans, version)?;
846            let id_max =
847                trans.query_row("SELECT IFNULL((SELECT MAX(id) FROM smtp), 0)", (), |row| {
848                    let id_max: i64 = row.get(0)?;
849                    Ok(id_max)
850                })?;
851            while let Some((id, rfc724_mid, mime, msg_id, recipients, retries)) = trans
852                .query_row(
853                    "SELECT id, rfc724_mid, mime, msg_id, recipients, retries FROM smtp \
854                    WHERE id<=? LIMIT 1",
855                    (id_max,),
856                    |row| {
857                        let id: i64 = row.get(0)?;
858                        let rfc724_mid: String = row.get(1)?;
859                        let mime: String = row.get(2)?;
860                        let msg_id: MsgId = row.get(3)?;
861                        let recipients: String = row.get(4)?;
862                        let retries: i64 = row.get(5)?;
863                        Ok((id, rfc724_mid, mime, msg_id, recipients, retries))
864                    },
865                )
866                .optional()?
867            {
868                trans.execute("DELETE FROM smtp WHERE id=?", (id,))?;
869                let recipients = recipients.split(' ').collect::<Vec<_>>();
870                for recipients in recipients.chunks(chunk_size) {
871                    let recipients = recipients.join(" ");
872                    trans.execute(
873                        "INSERT INTO smtp (rfc724_mid, mime, msg_id, recipients, retries) \
874                        VALUES (?, ?, ?, ?, ?)",
875                        (&rfc724_mid, &mime, msg_id, recipients, retries),
876                    )?;
877                }
878            }
879            Ok(())
880        })
881        .await
882        .with_context(|| format!("migration failed for version {version}"))?;
883
884        sql.set_db_version_in_cache(version).await?;
885    }
886
887    if dbversion < 109 {
888        sql.execute_migration(
889            r#"ALTER TABLE acpeerstates
890               ADD COLUMN backward_verified_key_id -- What we think the contact has as our verified key
891               INTEGER;
892               UPDATE acpeerstates
893               SET backward_verified_key_id=(SELECT value FROM config WHERE keyname='key_id')
894               WHERE verified_key IS NOT NULL
895               "#,
896            109,
897        )
898        .await?;
899    }
900
901    if dbversion < 110 {
902        sql.execute_migration(
903            "ALTER TABLE keypairs ADD COLUMN addr TEXT DEFAULT '' COLLATE NOCASE;
904            ALTER TABLE keypairs ADD COLUMN is_default INTEGER DEFAULT 0;
905            ALTER TABLE keypairs ADD COLUMN created INTEGER DEFAULT 0;
906            UPDATE keypairs SET addr=(SELECT value FROM config WHERE keyname='configured_addr'), is_default=1;",
907            110,
908        )
909        .await?;
910    }
911
912    if dbversion < 111 {
913        sql.execute_migration(
914            "CREATE TABLE iroh_gossip_peers (msg_id TEXT not NULL, topic TEXT NOT NULL, public_key TEXT NOT NULL)",
915            111,
916        )
917        .await?;
918    }
919
920    if dbversion < 112 {
921        sql.execute_migration(
922            "DROP TABLE iroh_gossip_peers; CREATE TABLE iroh_gossip_peers (msg_id INTEGER not NULL, topic BLOB NOT NULL, public_key BLOB NOT NULL, relay_server TEXT, UNIQUE (public_key, topic)) STRICT",
923            112,
924        )
925        .await?;
926    }
927
928    if dbversion < 113 {
929        sql.execute_migration(
930            "DROP TABLE iroh_gossip_peers; CREATE TABLE iroh_gossip_peers (msg_id INTEGER not NULL, topic BLOB NOT NULL, public_key BLOB NOT NULL, relay_server TEXT, UNIQUE (topic, public_key), PRIMARY KEY(topic, public_key)) STRICT",
931            113,
932        )
933        .await?;
934    }
935
936    if dbversion < 114 {
937        sql.execute_migration("CREATE INDEX reactions_index1 ON reactions (msg_id)", 114)
938            .await?;
939    }
940
941    if dbversion < 115 {
942        sql.execute_migration("ALTER TABLE msgs ADD COLUMN txt_normalized TEXT", 115)
943            .await?;
944    }
945    let mut migration_version: i32 = 115;
946
947    inc_and_check(&mut migration_version, 116)?;
948    if dbversion < migration_version {
949        // Whether the message part doesn't need to be stored on the server. If all parts are marked
950        // deleted, a server-side deletion is issued.
951        sql.execute_migration(
952            "ALTER TABLE msgs ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0",
953            migration_version,
954        )
955        .await?;
956    }
957
958    inc_and_check(&mut migration_version, 117)?;
959    if dbversion < migration_version {
960        sql.execute_migration(
961            "CREATE TABLE connection_history (
962                host TEXT NOT NULL, -- server hostname
963                port INTEGER NOT NULL, -- server port
964                alpn TEXT NOT NULL, -- ALPN such as smtp or imap
965                addr TEXT NOT NULL, -- IP address
966                timestamp INTEGER NOT NULL, -- timestamp of the most recent successful connection
967                UNIQUE (host, port, alpn, addr)
968            ) STRICT",
969            migration_version,
970        )
971        .await?;
972    }
973
974    inc_and_check(&mut migration_version, 118)?;
975    if dbversion < migration_version {
976        sql.execute_migration(
977            "CREATE TABLE tokens_new (
978                id INTEGER PRIMARY KEY,
979                namespc INTEGER DEFAULT 0,
980                foreign_key TEXT DEFAULT '',
981                token TEXT DEFAULT '',
982                timestamp INTEGER DEFAULT 0
983            ) STRICT;
984            INSERT INTO tokens_new
985                SELECT t.id, t.namespc, IFNULL(c.grpid, ''), t.token, t.timestamp
986                FROM tokens t LEFT JOIN chats c ON t.foreign_id=c.id;
987            DROP TABLE tokens;
988            ALTER TABLE tokens_new RENAME TO tokens;",
989            migration_version,
990        )
991        .await?;
992    }
993
994    inc_and_check(&mut migration_version, 119)?;
995    if dbversion < migration_version {
996        sql.execute_migration(
997            "CREATE TABLE imap_send (
998                id INTEGER PRIMARY KEY AUTOINCREMENT,
999                mime TEXT NOT NULL, -- Message content
1000                msg_id INTEGER NOT NULL, -- ID of the message in the `msgs` table
1001                attempts INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
1002            )",
1003            migration_version,
1004        )
1005        .await?;
1006    }
1007
1008    inc_and_check(&mut migration_version, 120)?;
1009    if dbversion < migration_version {
1010        // Core 1.143.0 changed the default for `delete_server_after`
1011        // to delete immediately (`1`) for chatmail accounts that don't have multidevice
1012        // and updating to `0` when backup is exported.
1013        //
1014        // Since we don't know if existing configurations
1015        // are multidevice, we set `delete_server_after` for them
1016        // to the old default of `0`, so only new configurations are
1017        // affected by the default change.
1018        //
1019        // `INSERT OR IGNORE` works
1020        // because `keyname` was made UNIQUE in migration 106.
1021        sql.execute_migration(
1022            "INSERT OR IGNORE INTO config (keyname, value)
1023             SELECT 'delete_server_after', '0'
1024             FROM config WHERE keyname='configured'
1025            ",
1026            migration_version,
1027        )
1028        .await?;
1029    }
1030
1031    inc_and_check(&mut migration_version, 121)?;
1032    if dbversion < migration_version {
1033        sql.execute_migration(
1034            "CREATE INDEX chats_index4 ON chats (name)",
1035            migration_version,
1036        )
1037        .await?;
1038    }
1039
1040    inc_and_check(&mut migration_version, 122)?;
1041    if dbversion < migration_version {
1042        sql.execute_migration(
1043            "ALTER TABLE tokens ADD COLUMN foreign_id INTEGER NOT NULL DEFAULT 0",
1044            migration_version,
1045        )
1046        .await?;
1047    }
1048
1049    inc_and_check(&mut migration_version, 123)?;
1050    if dbversion < migration_version {
1051        // Add FOREIGN KEY(msg_id).
1052        sql.execute_migration(
1053            "CREATE TABLE new_msgs_status_updates (
1054                id INTEGER PRIMARY KEY AUTOINCREMENT,
1055                msg_id INTEGER,
1056                update_item TEXT DEFAULT '',
1057                uid TEXT UNIQUE,
1058                FOREIGN KEY(msg_id) REFERENCES msgs(id) ON DELETE CASCADE
1059            );
1060            INSERT OR IGNORE INTO new_msgs_status_updates SELECT
1061                id, msg_id, update_item, uid
1062            FROM msgs_status_updates;
1063            DROP TABLE msgs_status_updates;
1064            ALTER TABLE new_msgs_status_updates RENAME TO msgs_status_updates;
1065            CREATE INDEX msgs_status_updates_index1 ON msgs_status_updates (msg_id);
1066            CREATE INDEX msgs_status_updates_index2 ON msgs_status_updates (uid);
1067            ",
1068            migration_version,
1069        )
1070        .await?;
1071    }
1072
1073    inc_and_check(&mut migration_version, 124)?;
1074    if dbversion < migration_version {
1075        // Mark Saved Messages chat as protected if it already exists.
1076        sql.execute_migration(
1077            "UPDATE chats
1078             SET protected=1 -- ProtectionStatus::Protected
1079             WHERE type==100 -- Chattype::Single
1080             AND EXISTS (
1081                 SELECT 1 FROM chats_contacts cc
1082                 WHERE cc.chat_id==chats.id
1083                 AND cc.contact_id=1
1084             )
1085             ",
1086            migration_version,
1087        )
1088        .await?;
1089    }
1090
1091    inc_and_check(&mut migration_version, 125)?;
1092    if dbversion < migration_version {
1093        sql.execute_migration(
1094            "CREATE TABLE http_cache (
1095                url TEXT PRIMARY KEY,
1096                expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
1097                blobname TEXT NOT NULL,
1098                mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
1099                encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
1100            ) STRICT",
1101            migration_version,
1102        )
1103        .await?;
1104    }
1105
1106    inc_and_check(&mut migration_version, 126)?;
1107    if dbversion < migration_version {
1108        // Recreate http_cache table with new `stale` column.
1109        sql.execute_migration(
1110            "DROP TABLE http_cache;
1111             CREATE TABLE http_cache (
1112                url TEXT PRIMARY KEY,
1113                expires INTEGER NOT NULL, -- When the cache entry is considered expired, timestamp in seconds.
1114                stale INTEGER NOT NULL, -- When the cache entry is considered stale, timestamp in seconds.
1115                blobname TEXT NOT NULL,
1116                mimetype TEXT NOT NULL DEFAULT '', -- MIME type extracted from Content-Type header.
1117                encoding TEXT NOT NULL DEFAULT '' -- Encoding from Content-Type header.
1118            ) STRICT",
1119            migration_version,
1120        )
1121        .await?;
1122    }
1123
1124    inc_and_check(&mut migration_version, 127)?;
1125    if dbversion < migration_version {
1126        // This is buggy: `delete_server_after` > 1 isn't handled. Migration #129 fixes this.
1127        sql.execute_migration(
1128            "INSERT OR IGNORE INTO config (keyname, value)
1129             SELECT 'bcc_self', '1'
1130             FROM config WHERE keyname='delete_server_after' AND value='0'
1131            ",
1132            migration_version,
1133        )
1134        .await?;
1135    }
1136
1137    inc_and_check(&mut migration_version, 128)?;
1138    if dbversion < migration_version {
1139        // Add the timestamps of addition and removal.
1140        //
1141        // If `add_timestamp >= remove_timestamp`,
1142        // then the member is currently a member of the chat.
1143        // Otherwise the member is a past member.
1144        sql.execute_migration(
1145            "ALTER TABLE chats_contacts
1146             ADD COLUMN add_timestamp NOT NULL DEFAULT 0;
1147             ALTER TABLE chats_contacts
1148             ADD COLUMN remove_timestamp NOT NULL DEFAULT 0;
1149            ",
1150            migration_version,
1151        )
1152        .await?;
1153    }
1154
1155    inc_and_check(&mut migration_version, 129)?;
1156    if dbversion < migration_version {
1157        // Existing chatmail configurations having `delete_server_after` != "delete at once" should
1158        // get `bcc_self` enabled, they may be multidevice configurations:
1159        // - Before migration #127, `delete_server_after` was set to 0 upon a backup export, but
1160        //   then `bcc_self` is enabled instead (whose default is changed to 0 for chatmail).
1161        // - The user might set `delete_server_after` to a value other than 0 or 1 when that was
1162        //   possible in UIs.
1163        // We don't check `is_chatmail` for simplicity.
1164        sql.execute_migration(
1165            "INSERT OR IGNORE INTO config (keyname, value)
1166             SELECT 'bcc_self', '1'
1167             FROM config WHERE keyname='delete_server_after' AND value!='1'
1168            ",
1169            migration_version,
1170        )
1171        .await?;
1172    }
1173
1174    inc_and_check(&mut migration_version, 130)?;
1175    if dbversion < migration_version {
1176        sql.execute_migration(
1177            "
1178CREATE TABLE gossip_timestamp (
1179  chat_id INTEGER NOT NULL, 
1180  fingerprint TEXT NOT NULL, -- Upper-case fingerprint of the key.
1181  timestamp INTEGER NOT NULL,
1182  UNIQUE (chat_id, fingerprint)
1183) STRICT;
1184CREATE INDEX gossip_timestamp_index ON gossip_timestamp (chat_id, fingerprint);
1185",
1186            migration_version,
1187        )
1188        .await?;
1189    }
1190
1191    inc_and_check(&mut migration_version, 131)?;
1192    if dbversion < migration_version {
1193        let entered_param = EnteredLoginParam::load(context).await?;
1194        let configured_param = ConfiguredLoginParam::load_legacy(context).await?;
1195
1196        sql.execute_migration_transaction(
1197            |transaction| {
1198                transaction.execute(
1199                    "CREATE TABLE transports (
1200                        id INTEGER PRIMARY KEY AUTOINCREMENT,
1201                        addr TEXT NOT NULL,
1202                        entered_param TEXT NOT NULL,
1203                        configured_param TEXT NOT NULL,
1204                        UNIQUE(addr)
1205                        )",
1206                    (),
1207                )?;
1208                if let Some(configured_param) = configured_param {
1209                    transaction.execute(
1210                        "INSERT INTO transports (addr, entered_param, configured_param)
1211                         VALUES (?, ?, ?)",
1212                        (
1213                            configured_param.addr.clone(),
1214                            serde_json::to_string(&entered_param)?,
1215                            configured_param.into_json()?,
1216                        ),
1217                    )?;
1218                }
1219
1220                Ok(())
1221            },
1222            migration_version,
1223        )
1224        .await?;
1225    }
1226
1227    let new_version = sql
1228        .get_raw_config_int(VERSION_CFG)
1229        .await?
1230        .unwrap_or_default();
1231    if new_version != dbversion || !exists_before_update {
1232        let created_db = if exists_before_update {
1233            ""
1234        } else {
1235            "Created new database. "
1236        };
1237        info!(context, "{}Migration done from v{}.", created_db, dbversion);
1238    }
1239    info!(context, "Database version: v{new_version}.");
1240
1241    Ok((
1242        recalc_fingerprints,
1243        update_icons,
1244        disable_server_delete,
1245        recode_avatar,
1246    ))
1247}
1248
1249impl Sql {
1250    async fn set_db_version(&self, version: i32) -> Result<()> {
1251        self.set_raw_config_int(VERSION_CFG, version).await?;
1252        Ok(())
1253    }
1254
1255    // Sets db `version` in the `transaction`.
1256    fn set_db_version_trans(transaction: &mut rusqlite::Transaction, version: i32) -> Result<()> {
1257        transaction.execute(
1258            "UPDATE config SET value=? WHERE keyname=?;",
1259            (format!("{version}"), VERSION_CFG),
1260        )?;
1261        Ok(())
1262    }
1263
1264    async fn set_db_version_in_cache(&self, version: i32) -> Result<()> {
1265        let mut lock = self.config_cache.write().await;
1266        lock.insert(VERSION_CFG.to_string(), Some(format!("{version}")));
1267        Ok(())
1268    }
1269
1270    async fn execute_migration(&self, query: &str, version: i32) -> Result<()> {
1271        self.execute_migration_transaction(
1272            |transaction| {
1273                transaction.execute_batch(query)?;
1274                Ok(())
1275            },
1276            version,
1277        )
1278        .await
1279    }
1280
1281    async fn execute_migration_transaction(
1282        &self,
1283        migration: impl Send + FnOnce(&mut rusqlite::Transaction) -> Result<()>,
1284        version: i32,
1285    ) -> Result<()> {
1286        self.transaction(move |transaction| {
1287            let curr_version: String = transaction.query_row(
1288                "SELECT IFNULL(value, ?) FROM config WHERE keyname=?;",
1289                ("0", VERSION_CFG),
1290                |row| row.get(0),
1291            )?;
1292            let curr_version: i32 = curr_version.parse()?;
1293            ensure!(curr_version < version, "Db version must be increased");
1294            Self::set_db_version_trans(transaction, version)?;
1295            migration(transaction)?;
1296
1297            Ok(())
1298        })
1299        .await
1300        .with_context(|| format!("execute_migration failed for version {version}"))?;
1301
1302        self.config_cache.write().await.clear();
1303
1304        Ok(())
1305    }
1306}
1307
1308#[cfg(test)]
1309mod tests {
1310    use super::*;
1311    use crate::config::Config;
1312    use crate::test_utils::TestContext;
1313
1314    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1315    async fn test_clear_config_cache() -> anyhow::Result<()> {
1316        // Some migrations change the `config` table in SQL.
1317        // This test checks that the config cache is invalidated in `execute_migration()`.
1318
1319        let t = TestContext::new().await;
1320        assert_eq!(t.get_config_bool(Config::IsChatmail).await?, false);
1321
1322        t.sql
1323            .execute_migration(
1324                "INSERT INTO config (keyname, value) VALUES ('is_chatmail', '1')",
1325                1000,
1326            )
1327            .await?;
1328        assert_eq!(t.get_config_bool(Config::IsChatmail).await?, true);
1329        assert_eq!(t.sql.get_raw_config_int(VERSION_CFG).await?.unwrap(), 1000);
1330
1331        Ok(())
1332    }
1333}