Skip to main content

miden_client_sqlite_store/account/
accounts.rs

1//! Account-related database operations.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::rc::Rc;
5use std::string::ToString;
6use std::vec::Vec;
7
8use miden_client::account::{
9    Account,
10    AccountCode,
11    AccountHeader,
12    AccountId,
13    AccountPatch,
14    AccountStorage,
15    Address,
16    PartialAccount,
17    PartialStorage,
18    PartialStorageMap,
19    StorageMapKey,
20    StorageSlotName,
21    StorageSlotType,
22};
23use miden_client::asset::{Asset, AssetVault, AssetWitness};
24use miden_client::store::{
25    AccountRecord,
26    AccountRecordData,
27    AccountStatus,
28    AccountStorageFilter,
29    AccountUpdate,
30    ClientAccountType,
31    StoreError,
32};
33use miden_client::utils::{Deserializable, Serializable};
34use miden_client::{AccountError, Felt, Word};
35use miden_protocol::account::{AccountStorageHeader, StorageMapWitness, StorageSlotHeader};
36use miden_protocol::asset::{AssetId, PartialVault};
37use miden_protocol::crypto::merkle::MerkleError;
38use rusqlite::types::Value;
39use rusqlite::{
40    Connection,
41    OptionalExtension,
42    Transaction,
43    TransactionBehavior,
44    named_params,
45    params,
46};
47
48use crate::account::helpers::{
49    query_account_addresses,
50    query_account_code,
51    query_historical_account_headers,
52    query_latest_account_headers,
53    query_storage_slots,
54    query_storage_values,
55    query_vault_assets,
56};
57use crate::forest::{ScopedAccountForest, SqliteForestBackend, allocate_forest_revision};
58use crate::sql_error::SqlResultExt;
59use crate::{SqliteStore, column_value_as_u64, insert_sql, subst, u64_to_value};
60
61impl SqliteStore {
62    // READER METHODS
63    // --------------------------------------------------------------------------------------------
64
65    pub(crate) fn get_account_ids(conn: &mut Connection) -> Result<Vec<AccountId>, StoreError> {
66        const QUERY: &str = "SELECT id FROM latest_account_headers";
67
68        conn.prepare_cached(QUERY)
69            .into_store_error()?
70            .query_map([], |row| row.get(0))
71            .expect("no binding parameters used in query")
72            .map(|result| {
73                let id: Vec<u8> = result.map_err(|e| StoreError::ParsingError(e.to_string()))?;
74                Ok(AccountId::read_from_bytes(&id).expect("account id is valid"))
75            })
76            .collect::<Result<Vec<AccountId>, StoreError>>()
77    }
78
79    pub(crate) fn get_account_headers(
80        conn: &mut Connection,
81    ) -> Result<Vec<(AccountHeader, AccountStatus)>, StoreError> {
82        Ok(query_latest_account_headers(conn, "1=1 ORDER BY id", params![])?
83            .into_iter()
84            .map(|(header, status, _)| (header, status))
85            .collect())
86    }
87
88    pub(crate) fn get_account_header(
89        conn: &Connection,
90        account_id: AccountId,
91    ) -> Result<Option<(AccountHeader, AccountStatus)>, StoreError> {
92        Ok(query_latest_account_headers(conn, "id = ?", params![account_id.to_bytes()])?
93            .pop()
94            .map(|(header, status, _)| (header, status)))
95    }
96
97    pub(crate) fn get_account_header_by_commitment(
98        conn: &mut Connection,
99        account_commitment: Word,
100    ) -> Result<Option<AccountHeader>, StoreError> {
101        Ok(query_historical_account_headers(
102            conn,
103            "account_commitment = ?",
104            params![account_commitment.to_bytes()],
105        )?
106        .pop()
107        .map(|(header, _)| header))
108    }
109
110    /// Retrieves a complete account record with full vault and storage data.
111    pub(crate) fn get_account(
112        conn: &mut Connection,
113        account_id: AccountId,
114    ) -> Result<Option<AccountRecord>, StoreError> {
115        let Some((header, status, client_account_type)) =
116            query_latest_account_headers(conn, "id = ?", params![account_id.to_bytes()])?.pop()
117        else {
118            return Ok(None);
119        };
120
121        let assets = query_vault_assets(conn, account_id)?;
122        let vault = AssetVault::new(&assets)?;
123
124        let slots = query_storage_slots(conn, account_id, &AccountStorageFilter::All)?
125            .into_values()
126            .collect();
127
128        let storage = AccountStorage::new(slots)?;
129
130        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
131            return Ok(None);
132        };
133
134        let account = Account::new_unchecked(
135            header.id(),
136            vault,
137            storage,
138            account_code,
139            header.nonce(),
140            status.seed().copied(),
141        );
142
143        let account_data = AccountRecordData::Full(account);
144        Ok(Some(AccountRecord::new(account_data, status, client_account_type)))
145    }
146
147    /// Retrieves a minimal partial account record with storage and vault witnesses.
148    pub(crate) fn get_minimal_partial_account(
149        conn: &mut Connection,
150        account_id: AccountId,
151    ) -> Result<Option<AccountRecord>, StoreError> {
152        let Some((header, status, client_account_type)) =
153            query_latest_account_headers(conn, "id = ?", params![account_id.to_bytes()])?.pop()
154        else {
155            return Ok(None);
156        };
157
158        // Partial vault retrieval
159        let partial_vault = PartialVault::new(header.vault_root());
160
161        // Partial storage retrieval
162        let mut storage_header = Vec::new();
163        let mut maps = vec![];
164
165        let storage_values = query_storage_values(conn, account_id)?;
166
167        // Storage maps are always minimal here (just roots, no entries).
168        // New accounts that need full storage data are handled by the DataStore layer,
169        // which fetches the full account via `get_account()` when nonce == 0.
170        for (slot_name, (slot_type, value)) in storage_values {
171            storage_header.push(StorageSlotHeader::new(slot_name.clone(), slot_type, value));
172            if slot_type == StorageSlotType::Map {
173                maps.push(PartialStorageMap::new(value));
174            }
175        }
176        storage_header.sort_by_key(StorageSlotHeader::id);
177        let storage_header =
178            AccountStorageHeader::new(storage_header).map_err(StoreError::AccountError)?;
179        let partial_storage =
180            PartialStorage::new(storage_header, maps).map_err(StoreError::AccountError)?;
181
182        let Some(account_code) = query_account_code(conn, header.code_commitment())? else {
183            return Ok(None);
184        };
185
186        let partial_account = PartialAccount::new(
187            header.id(),
188            header.nonce(),
189            account_code,
190            partial_storage,
191            partial_vault,
192            status.seed().copied(),
193        )?;
194        let account_record_data = AccountRecordData::Partial(partial_account);
195        Ok(Some(AccountRecord::new(account_record_data, status, client_account_type)))
196    }
197
198    pub fn get_foreign_account_code(
199        conn: &mut Connection,
200        account_ids: Vec<AccountId>,
201    ) -> Result<BTreeMap<AccountId, AccountCode>, StoreError> {
202        let params: Vec<Value> =
203            account_ids.into_iter().map(|id| Value::Blob(id.to_bytes())).collect();
204        const QUERY: &str = "
205            SELECT account_id, code
206            FROM foreign_account_code JOIN account_code ON foreign_account_code.code_commitment = account_code.commitment
207            WHERE account_id IN rarray(?)";
208
209        conn.prepare_cached(QUERY)
210            .into_store_error()?
211            .query_map([Rc::new(params)], |row| Ok((row.get(0)?, row.get(1)?)))
212            .expect("no binding parameters used in query")
213            .map(|result| {
214                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
215                    |(id, code): (Vec<u8>, Vec<u8>)| {
216                        Ok((
217                            AccountId::read_from_bytes(&id)
218                                .map_err(StoreError::DataDeserializationError)?,
219                            AccountCode::read_from_bytes(&code)
220                                .map_err(StoreError::DataDeserializationError)?,
221                        ))
222                    },
223                )
224            })
225            .collect::<Result<BTreeMap<AccountId, AccountCode>, _>>()
226    }
227
228    /// Retrieves the full asset vault for a specific account.
229    pub fn get_account_vault(
230        conn: &Connection,
231        account_id: AccountId,
232    ) -> Result<AssetVault, StoreError> {
233        let assets = query_vault_assets(conn, account_id)?;
234        Ok(AssetVault::new(&assets)?)
235    }
236
237    /// Retrieves the full storage for a specific account.
238    pub fn get_account_storage(
239        conn: &Connection,
240        account_id: AccountId,
241        filter: &AccountStorageFilter,
242    ) -> Result<AccountStorage, StoreError> {
243        let slots = query_storage_slots(conn, account_id, filter)?.into_values().collect();
244        Ok(AccountStorage::new(slots)?)
245    }
246
247    /// Fetches a specific asset from the account's vault without the need of loading the entire
248    /// vault. The witness is retrieved from the [`AccountSmtForest`].
249    pub(crate) fn get_account_asset(
250        conn: &mut Connection,
251        account_id: AccountId,
252        asset_id: AssetId,
253    ) -> Result<Option<(Asset, AssetWitness)>, StoreError> {
254        // Begin the transaction first so the header and forest reads share one snapshot.
255        let db_tx = conn.transaction().into_store_error()?;
256        let header = Self::get_account_header(&db_tx, account_id)?
257            .ok_or(StoreError::AccountDataNotFound(account_id))?
258            .0;
259        let smt_forest = ScopedAccountForest::new(SqliteForestBackend::new(&db_tx))?;
260
261        match smt_forest.get_asset_and_witness(account_id, header.vault_root(), asset_id) {
262            Ok((asset, witness)) => Ok(Some((asset, witness))),
263            Err(StoreError::VaultKeyNotTracked(..)) => Ok(None),
264            Err(err) => Err(err),
265        }
266    }
267
268    /// Retrieves a specific item from the account's storage map without loading the entire storage.
269    /// The witness is retrieved from the [`AccountSmtForest`].
270    pub(crate) fn get_account_map_item(
271        conn: &mut Connection,
272        account_id: AccountId,
273        slot_name: StorageSlotName,
274        key: StorageMapKey,
275    ) -> Result<(Word, StorageMapWitness), StoreError> {
276        // Begin the transaction first so the slot root and forest reads share one snapshot.
277        let db_tx = conn.transaction().into_store_error()?;
278        let header = Self::get_account_header(&db_tx, account_id)?
279            .ok_or(StoreError::AccountDataNotFound(account_id))?
280            .0;
281
282        let mut storage_values = query_storage_values(&db_tx, account_id)?;
283        let (slot_type, map_root) = storage_values
284            .remove(&slot_name)
285            .ok_or(StoreError::AccountStorageRootNotFound(header.storage_commitment()))?;
286        if slot_type != StorageSlotType::Map {
287            return Err(StoreError::AccountError(AccountError::StorageSlotNotMap(slot_name)));
288        }
289
290        let smt_forest = ScopedAccountForest::new(SqliteForestBackend::new(&db_tx))?;
291
292        let witness =
293            smt_forest.get_storage_map_item_witness(account_id, &slot_name, map_root, key)?;
294        let item = witness.get(key).unwrap_or(miden_client::EMPTY_WORD);
295
296        Ok((item, witness))
297    }
298
299    /// Retrieves vault asset witnesses for the given vault keys, including emptiness proofs for
300    /// keys absent from the vault (which the executor needs when an asset is being added).
301    ///
302    /// The witnesses are opened against the account's vault tree in the forest, after verifying
303    /// that its root matches `vault_root` — the committed root the caller expects.
304    pub(crate) fn get_vault_asset_witnesses(
305        conn: &mut Connection,
306        account_id: AccountId,
307        vault_root: Word,
308        asset_ids: BTreeSet<AssetId>,
309    ) -> Result<Vec<AssetWitness>, StoreError> {
310        let db_tx = conn.transaction().into_store_error()?;
311        let smt_forest = ScopedAccountForest::new(SqliteForestBackend::new(&db_tx))?;
312        smt_forest.open_vault_asset_witnesses(account_id, vault_root, asset_ids)
313    }
314
315    pub(crate) fn get_account_addresses(
316        conn: &mut Connection,
317        account_id: AccountId,
318    ) -> Result<Vec<Address>, StoreError> {
319        query_account_addresses(conn, account_id)
320    }
321
322    /// Retrieves the account code for a specific account by ID.
323    pub(crate) fn get_account_code_by_id(
324        conn: &mut Connection,
325        account_id: AccountId,
326    ) -> Result<Option<AccountCode>, StoreError> {
327        let Some((header, ..)) =
328            query_latest_account_headers(conn, "id = ?", params![account_id.to_bytes()])?
329                .into_iter()
330                .next()
331        else {
332            return Ok(None);
333        };
334
335        query_account_code(conn, header.code_commitment())
336    }
337
338    // MUTATOR/WRITER METHODS
339    // --------------------------------------------------------------------------------------------
340
341    pub(crate) fn insert_account(
342        conn: &mut Connection,
343        account: &Account,
344        initial_address: &Address,
345        client_account_type: ClientAccountType,
346    ) -> Result<(), StoreError> {
347        let db_tx = conn
348            .transaction_with_behavior(TransactionBehavior::Immediate)
349            .into_store_error()?;
350        {
351            let mut smt_forest = ScopedAccountForest::new(SqliteForestBackend::new(&db_tx))?;
352            Self::insert_account_code(&db_tx, account.code())?;
353
354            let account_id = account.id();
355            Self::insert_storage_slots(&db_tx, account_id, account.storage().slots().iter())?;
356            Self::insert_assets(&db_tx, account_id, account.vault().assets())?;
357            let watched = matches!(client_account_type, ClientAccountType::Watched);
358            Self::insert_new_account_header(&db_tx, &account.into(), account.seed(), watched)?;
359            Self::insert_address(&db_tx, initial_address, account.id())?;
360
361            Self::reconcile_account_forest(
362                &db_tx,
363                &mut smt_forest,
364                account_id,
365                account.vault(),
366                account.storage(),
367            )?;
368        }
369        db_tx.commit().into_store_error()
370    }
371
372    pub(crate) fn update_account(
373        conn: &mut Connection,
374        new_account_state: &Account,
375    ) -> Result<(), StoreError> {
376        const QUERY: &str = "SELECT id FROM latest_account_headers WHERE id = ?";
377        if conn
378            .prepare(QUERY)
379            .into_store_error()?
380            .query_map(params![new_account_state.id().to_bytes()], |row| row.get(0))
381            .into_store_error()?
382            .map(|result| {
383                result.map_err(|err| StoreError::ParsingError(err.to_string())).and_then(
384                    |id: Vec<u8>| {
385                        AccountId::read_from_bytes(&id)
386                            .map_err(StoreError::DataDeserializationError)
387                    },
388                )
389            })
390            .next()
391            .is_none()
392        {
393            return Err(StoreError::AccountDataNotFound(new_account_state.id()));
394        }
395
396        let db_tx = conn
397            .transaction_with_behavior(TransactionBehavior::Immediate)
398            .into_store_error()?;
399        {
400            let mut smt_forest = ScopedAccountForest::new(SqliteForestBackend::new(&db_tx))?;
401            Self::update_account_state(&db_tx, &mut smt_forest, new_account_state)?;
402        }
403        db_tx.commit().into_store_error()
404    }
405
406    pub fn upsert_foreign_account_code(
407        conn: &mut Connection,
408        account_id: AccountId,
409        code: &AccountCode,
410    ) -> Result<(), StoreError> {
411        let tx = conn.transaction().into_store_error()?;
412
413        Self::insert_account_code(&tx, code)?;
414
415        const QUERY: &str =
416            insert_sql!(foreign_account_code { account_id, code_commitment } | REPLACE);
417
418        tx.execute(QUERY, params![account_id.to_bytes(), code.commitment().to_bytes()])
419            .into_store_error()?;
420
421        Self::insert_account_code(&tx, code)?;
422        tx.commit().into_store_error()
423    }
424
425    pub(crate) fn insert_address(
426        tx: &Transaction<'_>,
427        address: &Address,
428        account_id: AccountId,
429    ) -> Result<(), StoreError> {
430        const QUERY: &str = insert_sql!(addresses { address, account_id } | REPLACE);
431        let serialized_address = address.to_bytes();
432        tx.execute(QUERY, params![serialized_address, account_id.to_bytes(),])
433            .into_store_error()?;
434
435        Ok(())
436    }
437
438    /// Returns `true` if a row was deleted, `false` if the address wasn't tracked.
439    pub(crate) fn remove_address(
440        conn: &mut Connection,
441        address: &Address,
442    ) -> Result<bool, StoreError> {
443        let tx = conn.transaction().into_store_error()?;
444        let serialized_address = address.to_bytes();
445        const DELETE_QUERY: &str = "DELETE FROM addresses WHERE address = ?";
446        let count = tx.execute(DELETE_QUERY, params![serialized_address]).into_store_error()?;
447
448        tx.commit().into_store_error()?;
449
450        Ok(count > 0)
451    }
452
453    /// Inserts an [`AccountCode`].
454    pub(crate) fn insert_account_code(
455        tx: &Transaction<'_>,
456        account_code: &AccountCode,
457    ) -> Result<(), StoreError> {
458        const QUERY: &str = insert_sql!(account_code { commitment, code } | IGNORE);
459        tx.execute(QUERY, params![account_code.commitment().to_bytes(), account_code.to_bytes()])
460            .into_store_error()?;
461        Ok(())
462    }
463
464    /// Applies the account patch to the account state, updating the vault and storage maps.
465    ///
466    /// Archives old values from latest to historical and updates latest via INSERT OR REPLACE.
467    pub(crate) fn apply_account_patch(
468        tx: &Transaction<'_>,
469        smt_forest: &mut ScopedAccountForest<'_, '_>,
470        init_account_state: &AccountHeader,
471        final_account_state: &AccountHeader,
472        patch: &AccountPatch,
473    ) -> Result<(), StoreError> {
474        let account_id = final_account_state.id();
475
476        // Reject patches for accounts the store does not track (forest updates for unknown
477        // accounts would silently create partial state from empty trees), and stale or replayed
478        // patches whose initial state does not match the stored latest state (they would
479        // overwrite newer state and archive incorrect history).
480        let stored_header = Self::require_latest_account_header(tx, account_id)?;
481        if stored_header.to_commitment() != init_account_state.to_commitment() {
482            return Err(StoreError::DatabaseError(format!(
483                "apply_account_patch: stored state {} for account {} does not match the patch's \
484                 initial state {}",
485                stored_header.to_commitment(),
486                account_id,
487                init_account_state.to_commitment(),
488            )));
489        }
490
491        // Archive old header and insert the new one
492        Self::replace_account_header(tx, final_account_state, init_account_state)?;
493
494        Self::apply_account_vault_patch(tx, account_id, final_account_state, patch.vault())?;
495
496        // Build one forest update covering the vault and every changed map slot, and apply it at
497        // a freshly allocated revision.
498        let mut update = AccountUpdate::new();
499        update.vault_patch(account_id, patch.vault(), final_account_state.vault_root());
500        update.storage_patch(account_id, patch.storage());
501
502        let revision = allocate_forest_revision(tx).into_store_error()?;
503        smt_forest.apply(revision, update)?;
504
505        Self::write_storage_patch(
506            tx,
507            smt_forest,
508            account_id,
509            final_account_state.nonce().as_canonical_u64(),
510            patch.storage(),
511        )?;
512        Self::verify_storage_commitment(tx, account_id, final_account_state.storage_commitment())?;
513
514        Ok(())
515    }
516
517    /// Reconciles the account's forest lineages to exactly match the provided full state.
518    ///
519    /// Map slots that disappeared from the state are enumerated from the latest storage tables,
520    /// so this must run before those rows are replaced.
521    pub(crate) fn reconcile_account_forest(
522        tx: &Transaction<'_>,
523        smt_forest: &mut ScopedAccountForest<'_, '_>,
524        account_id: AccountId,
525        vault: &AssetVault,
526        storage: &AccountStorage,
527    ) -> Result<(), StoreError> {
528        let mut update = AccountUpdate::new();
529        update.full_state(account_id, vault.assets(), storage.slots().iter());
530
531        // Slots that still have stored rows but are absent from the new state must be emptied
532        // rather than keeping their old entries. Slots the new state does repopulate keep the
533        // entries recorded above; this only marks the lineage exhaustive.
534        for slot_name in Self::query_map_slot_names(tx, account_id)? {
535            update.clear_map(account_id, &slot_name);
536        }
537
538        Self::apply_forest_update(tx, smt_forest, update)
539    }
540
541    /// Reconciles the account's forest lineages to the state currently stored in the latest
542    /// account tables. Used after rows are restored from historical during undo.
543    ///
544    /// `extra_map_slots` lists map slots that may hold forest entries even though they have no
545    /// rows anymore (captured before the tables were rewritten); their lineages are reset to
546    /// the empty tree unless the restored state repopulates them.
547    fn reconcile_account_forest_from_tables(
548        tx: &Transaction<'_>,
549        smt_forest: &mut ScopedAccountForest<'_, '_>,
550        account_id: AccountId,
551        extra_map_slots: &[StorageSlotName],
552    ) -> Result<(), StoreError> {
553        let assets = query_vault_assets(tx, account_id)?;
554        let slots = query_storage_slots(tx, account_id, &AccountStorageFilter::All)?;
555
556        let mut update = AccountUpdate::new();
557        update.full_state(account_id, assets.into_iter(), slots.values());
558        for slot_name in extra_map_slots {
559            update.clear_map(account_id, slot_name);
560        }
561
562        Self::apply_forest_update(tx, smt_forest, update)
563    }
564
565    /// Verifies that the persisted top-level storage slots match the expected commitment.
566    ///
567    /// This runs after the storage patch is written so create, update, and removal semantics have
568    /// a single source of truth. A mismatch rolls back together with the rest of the transaction.
569    fn verify_storage_commitment(
570        tx: &Transaction<'_>,
571        account_id: AccountId,
572        expected: Word,
573    ) -> Result<(), StoreError> {
574        let mut slot_headers: Vec<StorageSlotHeader> = query_storage_values(tx, account_id)?
575            .into_iter()
576            .map(|(slot_name, (slot_type, value))| {
577                StorageSlotHeader::new(slot_name, slot_type, value)
578            })
579            .collect();
580        slot_headers.sort_by_key(StorageSlotHeader::id);
581
582        let actual = AccountStorageHeader::new(slot_headers)
583            .map_err(StoreError::AccountError)?
584            .to_commitment();
585        if actual != expected {
586            return Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
587                expected_root: expected,
588                actual_root: actual,
589            }));
590        }
591
592        Ok(())
593    }
594
595    /// Applies a recorded forest update at a freshly allocated revision.
596    fn apply_forest_update(
597        tx: &Transaction<'_>,
598        smt_forest: &mut ScopedAccountForest<'_, '_>,
599        update: AccountUpdate,
600    ) -> Result<(), StoreError> {
601        let revision = allocate_forest_revision(tx).into_store_error()?;
602        smt_forest.apply(revision, update)
603    }
604
605    /// Returns the stored latest header of an account, or [`StoreError::AccountDataNotFound`] if
606    /// the store does not track it.
607    fn require_latest_account_header(
608        tx: &Transaction<'_>,
609        account_id: AccountId,
610    ) -> Result<AccountHeader, StoreError> {
611        query_latest_account_headers(tx, "id = ?", params![account_id.to_bytes()])?
612            .into_iter()
613            .next()
614            .map(|(header, ..)| header)
615            .ok_or(StoreError::AccountDataNotFound(account_id))
616    }
617
618    /// Returns the names of the map slots that currently have entries stored for an account.
619    fn query_map_slot_names(
620        tx: &Transaction<'_>,
621        account_id: AccountId,
622    ) -> Result<Vec<StorageSlotName>, StoreError> {
623        let mut stmt = tx
624            .prepare(
625                "SELECT DISTINCT slot_name FROM latest_storage_map_entries WHERE account_id = ?",
626            )
627            .into_store_error()?;
628        let rows = stmt
629            .query_map(params![account_id.to_bytes()], |row| row.get::<_, String>(0))
630            .into_store_error()?;
631
632        let mut names = Vec::new();
633        for row in rows {
634            names.push(
635                StorageSlotName::new(row.into_store_error()?)
636                    .map_err(|e| StoreError::ParsingError(e.to_string()))?,
637            );
638        }
639        Ok(names)
640    }
641
642    /// Undoes discarded account states by restoring old values from historical.
643    pub(crate) fn undo_account_state(
644        tx: &Transaction<'_>,
645        smt_forest: &mut ScopedAccountForest<'_, '_>,
646        discarded_states: &[(AccountId, Word)],
647    ) -> Result<(), StoreError> {
648        if discarded_states.is_empty() {
649            return Ok(());
650        }
651
652        let commitment_params = Rc::new(
653            discarded_states
654                .iter()
655                .map(|(_, commitment)| Value::Blob(commitment.to_bytes()))
656                .collect::<Vec<_>>(),
657        );
658
659        // Step 1: Resolve (account_id, nonce) pairs from both latest and historical headers.
660        // The most recent discarded state is in latest, older ones are in historical.
661        let mut id_nonce_pairs: Vec<(Vec<u8>, u64)> = Vec::new();
662        for query in [
663            "SELECT id, nonce FROM latest_account_headers WHERE account_commitment IN rarray(?)",
664            "SELECT id, nonce FROM historical_account_headers WHERE account_commitment IN rarray(?)",
665        ] {
666            id_nonce_pairs.extend(
667                tx.prepare(query)
668                    .into_store_error()?
669                    .query_map(params![commitment_params.clone()], |row| {
670                        let id: Vec<u8> = row.get(0)?;
671                        let nonce: u64 = column_value_as_u64(row, 1)?;
672                        Ok((id, nonce))
673                    })
674                    .into_store_error()?
675                    .filter_map(Result::ok),
676            );
677        }
678
679        // Step 2: Group nonces by account, sort descending (undo most recent first).
680        // Descending order is needed because each nonce's old value is the state before
681        // that nonce — processing most recent first lets earlier nonces overwrite with
682        // the correct final value.
683        let mut nonces_by_account: BTreeMap<Vec<u8>, Vec<u64>> = BTreeMap::new();
684        for (id, nonce) in &id_nonce_pairs {
685            nonces_by_account.entry(id.clone()).or_default().push(*nonce);
686        }
687        for nonces in nonces_by_account.values_mut() {
688            nonces.sort_unstable();
689            nonces.dedup();
690            nonces.reverse();
691        }
692
693        // Capture each account's current map slots before the restore rewrites the latest tables.
694        let mut pre_undo_map_slots: BTreeMap<Vec<u8>, Vec<StorageSlotName>> = BTreeMap::new();
695        for account_id_bytes in nonces_by_account.keys() {
696            let account_id = AccountId::read_from_bytes(account_id_bytes)?;
697            pre_undo_map_slots
698                .insert(account_id_bytes.clone(), Self::query_map_slot_names(tx, account_id)?);
699        }
700
701        // Steps 3-5
702        for (account_id_bytes, nonces) in &nonces_by_account {
703            Self::undo_account_nonces(tx, account_id_bytes, nonces)?;
704        }
705
706        // Step 6: Reconcile the affected accounts' forest lineages to the restored state.
707        for account_id_bytes in nonces_by_account.keys() {
708            let account_id = AccountId::read_from_bytes(account_id_bytes)?;
709            let stale_slots: &[StorageSlotName] =
710                pre_undo_map_slots.get(account_id_bytes).map_or(&[], Vec::as_slice);
711            Self::reconcile_account_forest_from_tables(tx, smt_forest, account_id, stale_slots)?;
712        }
713
714        Ok(())
715    }
716
717    /// Undoes all nonces for a single account: restores old values, restores old header,
718    /// and cleans up consumed historical entries.
719    fn undo_account_nonces(
720        tx: &Transaction<'_>,
721        account_id_bytes: &[u8],
722        nonces: &[u64],
723    ) -> Result<(), StoreError> {
724        // Step 3: Undo each nonce in descending order
725        for &nonce in nonces {
726            let nonce_val = u64_to_value(nonce);
727            Self::restore_old_values_for_nonce(tx, account_id_bytes, &nonce_val)?;
728        }
729
730        // Step 4: Restore old header from the earliest discarded nonce
731        // SAFETY: `nonces` is non-empty because `undo_account_nonces` is only called for
732        // accounts that appear in `nonces_by_account`, which only contains entries built
733        // from at least one nonce being pushed — so the slice is guaranteed non-empty here.
734        let min_nonce = *nonces.last().unwrap();
735        let min_nonce_val = u64_to_value(min_nonce);
736
737        let old_header_exists: bool = tx
738            .query_row(
739                "SELECT COUNT(*) FROM historical_account_headers \
740                 WHERE id = ? AND replaced_at_nonce = ?",
741                params![account_id_bytes, &min_nonce_val],
742                |row| row.get::<_, i64>(0),
743            )
744            .into_store_error()?
745            > 0;
746
747        if old_header_exists {
748            // `watched` is not carried in historical_account_headers, so this restore resets
749            // it to the column default (FALSE). This is safe because undo only fires for discarded
750            // local transactions, and watched accounts have none.
751            tx.execute(
752                "INSERT OR REPLACE INTO latest_account_headers \
753                 (id, account_commitment, code_commitment, storage_commitment, \
754                  vault_root, nonce, account_seed, locked) \
755                 SELECT id, account_commitment, code_commitment, storage_commitment, \
756                        vault_root, nonce, account_seed, locked \
757                 FROM historical_account_headers \
758                 WHERE id = ? AND replaced_at_nonce = ?",
759                params![account_id_bytes, &min_nonce_val],
760            )
761            .into_store_error()?;
762        } else {
763            // No previous state — delete the account entirely
764            for table in [
765                "DELETE FROM latest_account_headers WHERE id = ?",
766                "DELETE FROM latest_account_storage WHERE account_id = ?",
767                "DELETE FROM latest_storage_map_entries WHERE account_id = ?",
768                "DELETE FROM latest_account_assets WHERE account_id = ?",
769            ] {
770                tx.execute(table, params![account_id_bytes]).into_store_error()?;
771            }
772        }
773
774        // Step 5: Delete all consumed historical entries at the discarded nonces
775        let nonce_params = Rc::new(nonces.iter().map(|n| u64_to_value(*n)).collect::<Vec<_>>());
776        for table in [
777            "historical_account_storage",
778            "historical_storage_map_entries",
779            "historical_account_assets",
780        ] {
781            tx.execute(
782                &format!(
783                    "DELETE FROM {table} WHERE account_id = ? AND replaced_at_nonce IN rarray(?)"
784                ),
785                params![account_id_bytes, nonce_params.clone()],
786            )
787            .into_store_error()?;
788        }
789        tx.execute(
790            "DELETE FROM historical_account_headers \
791             WHERE id = ? AND replaced_at_nonce IN rarray(?)",
792            params![account_id_bytes, nonce_params],
793        )
794        .into_store_error()?;
795
796        Ok(())
797    }
798
799    /// Restores old values from historical entries for a given nonce.
800    /// Non-NULL old values overwrite latest, NULL old values (new entries) are deleted.
801    fn restore_old_values_for_nonce(
802        tx: &Transaction<'_>,
803        account_id_bytes: &[u8],
804        nonce_val: &rusqlite::types::Value,
805    ) -> Result<(), StoreError> {
806        // Restore storage slots with non-NULL old values
807        tx.execute(
808            "INSERT OR REPLACE INTO latest_account_storage \
809             (account_id, slot_name, slot_value, slot_type) \
810             SELECT account_id, slot_name, old_slot_value, slot_type \
811             FROM historical_account_storage \
812             WHERE account_id = ? AND replaced_at_nonce = ? AND old_slot_value IS NOT NULL",
813            params![account_id_bytes, nonce_val],
814        )
815        .into_store_error()?;
816
817        // Delete storage slots that were new (NULL old value)
818        tx.execute(
819            "DELETE FROM latest_account_storage \
820             WHERE account_id = ?1 AND slot_name IN (\
821                 SELECT slot_name FROM historical_account_storage \
822                 WHERE account_id = ?1 AND replaced_at_nonce = ?2 AND old_slot_value IS NULL\
823             )",
824            params![account_id_bytes, nonce_val],
825        )
826        .into_store_error()?;
827
828        // Restore map entries with non-NULL old values
829        tx.execute(
830            "INSERT OR REPLACE INTO latest_storage_map_entries \
831             (account_id, slot_name, key, value) \
832             SELECT account_id, slot_name, key, old_value \
833             FROM historical_storage_map_entries \
834             WHERE account_id = ? AND replaced_at_nonce = ? AND old_value IS NOT NULL",
835            params![account_id_bytes, nonce_val],
836        )
837        .into_store_error()?;
838
839        // Delete map entries that were new (NULL old value)
840        tx.execute(
841            "DELETE FROM latest_storage_map_entries \
842             WHERE account_id = ?1 AND EXISTS (\
843                 SELECT 1 FROM historical_storage_map_entries h \
844                 WHERE h.account_id = latest_storage_map_entries.account_id \
845                   AND h.slot_name = latest_storage_map_entries.slot_name \
846                   AND h.key = latest_storage_map_entries.key \
847                   AND h.replaced_at_nonce = ?2 AND h.old_value IS NULL\
848             )",
849            params![account_id_bytes, nonce_val],
850        )
851        .into_store_error()?;
852
853        // Restore assets with non-NULL old values
854        tx.execute(
855            "INSERT OR REPLACE INTO latest_account_assets \
856             (account_id, asset_id, asset) \
857             SELECT account_id, asset_id, old_asset \
858             FROM historical_account_assets \
859             WHERE account_id = ? AND replaced_at_nonce = ? AND old_asset IS NOT NULL",
860            params![account_id_bytes, nonce_val],
861        )
862        .into_store_error()?;
863
864        // Delete assets that were new (NULL old value)
865        tx.execute(
866            "DELETE FROM latest_account_assets \
867             WHERE account_id = ?1 AND asset_id IN (\
868             SELECT asset_id FROM historical_account_assets \
869                 WHERE account_id = ?1 AND replaced_at_nonce = ?2 AND old_asset IS NULL\
870             )",
871            params![account_id_bytes, nonce_val],
872        )
873        .into_store_error()?;
874
875        Ok(())
876    }
877
878    /// Replaces the account state with a completely new one from the network.
879    ///
880    /// Replaces the account state entirely: archives old state to historical, clears latest,
881    /// inserts new state to latest only. Preserves the `watched` flag.
882    pub(crate) fn update_account_state(
883        tx: &Transaction<'_>,
884        smt_forest: &mut ScopedAccountForest<'_, '_>,
885        new_account_state: &Account,
886    ) -> Result<(), StoreError> {
887        let account_id = new_account_state.id();
888        let account_id_bytes = account_id.to_bytes();
889
890        // Read old header before mutating the SMT snapshot or database rows. Sync filters stale
891        // full-account snapshots; if one still reaches storage, reject it before mutating.
892        let old_header = query_latest_account_headers(tx, "id = ?", params![&account_id_bytes])?
893            .into_iter()
894            .next()
895            .map(|(header, ..)| header)
896            .ok_or(StoreError::AccountDataNotFound(account_id))?;
897
898        if new_account_state.nonce().as_canonical_u64() < old_header.nonce().as_canonical_u64() {
899            return Err(StoreError::DatabaseError(format!(
900                "update_account_state: new nonce {} is less than old nonce {} for account {}",
901                new_account_state.nonce().as_canonical_u64(),
902                old_header.nonce().as_canonical_u64(),
903                account_id,
904            )));
905        }
906
907        let nonce_val = u64_to_value(new_account_state.nonce().as_canonical_u64());
908
909        // Reconcile the forest to the new full state before the latest tables are replaced below.
910        Self::reconcile_account_forest(
911            tx,
912            smt_forest,
913            account_id,
914            new_account_state.vault(),
915            new_account_state.storage(),
916        )?;
917
918        // Archive all old entries from latest → historical
919        tx.execute(
920            "INSERT OR REPLACE INTO historical_account_storage \
921             (account_id, replaced_at_nonce, slot_name, old_slot_value, slot_type) \
922             SELECT account_id, ?, slot_name, slot_value, slot_type \
923             FROM latest_account_storage WHERE account_id = ?",
924            params![&nonce_val, &account_id_bytes],
925        )
926        .into_store_error()?;
927        tx.execute(
928            "INSERT OR REPLACE INTO historical_storage_map_entries \
929             (account_id, replaced_at_nonce, slot_name, key, old_value) \
930             SELECT account_id, ?, slot_name, key, value \
931             FROM latest_storage_map_entries WHERE account_id = ?",
932            params![&nonce_val, &account_id_bytes],
933        )
934        .into_store_error()?;
935        tx.execute(
936            "INSERT OR REPLACE INTO historical_account_assets \
937             (account_id, replaced_at_nonce, asset_id, old_asset) \
938             SELECT account_id, ?, asset_id, asset \
939             FROM latest_account_assets WHERE account_id = ?",
940            params![&nonce_val, &account_id_bytes],
941        )
942        .into_store_error()?;
943
944        // Delete all latest entries for this account
945        tx.execute(
946            "DELETE FROM latest_account_storage WHERE account_id = ?",
947            params![&account_id_bytes],
948        )
949        .into_store_error()?;
950        tx.execute(
951            "DELETE FROM latest_storage_map_entries WHERE account_id = ?",
952            params![&account_id_bytes],
953        )
954        .into_store_error()?;
955        tx.execute(
956            "DELETE FROM latest_account_assets WHERE account_id = ?",
957            params![&account_id_bytes],
958        )
959        .into_store_error()?;
960
961        // Insert all new entries into latest only
962        Self::insert_storage_slots(tx, account_id, new_account_state.storage().slots().iter())?;
963        Self::insert_assets(tx, account_id, new_account_state.vault().assets())?;
964
965        // Write NULL historical entries for genuinely new entries that didn't exist
966        // in the old state (INSERT OR IGNORE skips entries already archived above)
967        tx.execute(
968            "INSERT OR IGNORE INTO historical_account_storage \
969             (account_id, replaced_at_nonce, slot_name, old_slot_value, slot_type) \
970             SELECT account_id, ?, slot_name, NULL, slot_type \
971             FROM latest_account_storage WHERE account_id = ?",
972            params![&nonce_val, &account_id_bytes],
973        )
974        .into_store_error()?;
975        tx.execute(
976            "INSERT OR IGNORE INTO historical_storage_map_entries \
977             (account_id, replaced_at_nonce, slot_name, key, old_value) \
978             SELECT account_id, ?, slot_name, key, NULL \
979             FROM latest_storage_map_entries WHERE account_id = ?",
980            params![&nonce_val, &account_id_bytes],
981        )
982        .into_store_error()?;
983        tx.execute(
984            "INSERT OR IGNORE INTO historical_account_assets \
985             (account_id, replaced_at_nonce, asset_id, old_asset) \
986             SELECT account_id, ?, asset_id, NULL \
987             FROM latest_account_assets WHERE account_id = ?",
988            params![&nonce_val, &account_id_bytes],
989        )
990        .into_store_error()?;
991
992        // Archive the old header to historical and write the new one to latest.
993        Self::replace_account_header(tx, &new_account_state.into(), &old_header)?;
994
995        Ok(())
996    }
997
998    /// Applies an incremental patch to a public account's state during sync.
999    pub(crate) fn apply_sync_account_patch(
1000        tx: &Transaction<'_>,
1001        smt_forest: &mut ScopedAccountForest<'_, '_>,
1002        new_header: &AccountHeader,
1003        patch: &AccountPatch,
1004    ) -> Result<(), StoreError> {
1005        let account_id = new_header.id();
1006
1007        // Read current header from the store.
1008        let init_header = Self::require_latest_account_header(tx, account_id)?;
1009
1010        if new_header.nonce().as_canonical_u64() <= init_header.nonce().as_canonical_u64() {
1011            return Err(StoreError::DatabaseError(format!(
1012                "apply_sync_account_patch: new nonce {} is not greater than local nonce {} for account {}",
1013                new_header.nonce().as_canonical_u64(),
1014                init_header.nonce().as_canonical_u64(),
1015                account_id,
1016            )));
1017        }
1018
1019        // Transaction derefs to Connection, so we can pass it where Connection is expected.
1020
1021        Self::apply_account_patch(tx, smt_forest, &init_header, new_header, patch)
1022    }
1023
1024    /// Locks the account if the mismatched digest doesn't belong to a previous account state (stale
1025    /// data).
1026    pub(crate) fn lock_account_on_unexpected_commitment(
1027        tx: &Transaction<'_>,
1028        account_id: &AccountId,
1029        mismatched_digest: &Word,
1030    ) -> Result<(), StoreError> {
1031        // Mismatched digests may be due to stale network data. If the mismatched digest is
1032        // tracked in the db and corresponds to the mismatched account, it means we
1033        // got a past update and shouldn't lock the account.
1034        const LOCK_CONDITION: &str = "WHERE id = :account_id AND NOT EXISTS (SELECT 1 FROM historical_account_headers WHERE id = :account_id AND account_commitment = :digest)";
1035        let account_id_bytes = account_id.to_bytes();
1036        let digest_bytes = mismatched_digest.to_bytes();
1037        let params = named_params! {
1038            ":account_id": account_id_bytes,
1039            ":digest": digest_bytes
1040        };
1041
1042        let query = format!("UPDATE latest_account_headers SET locked = true {LOCK_CONDITION}");
1043        tx.execute(&query, params).into_store_error()?;
1044
1045        // Also lock historical rows so that undo_account_state preserves the lock.
1046        let query = format!("UPDATE historical_account_headers SET locked = true {LOCK_CONDITION}");
1047        tx.execute(&query, params).into_store_error()?;
1048
1049        Ok(())
1050    }
1051
1052    // HELPERS
1053    // --------------------------------------------------------------------------------------------
1054
1055    /// Writes a new row into `latest_account_headers`.
1056    ///
1057    /// Does not archive any previous state, use [`Self::replace_account_header`] when a row
1058    /// for this account already exists. If a row does exist it will be overwritten with the
1059    /// provided `watched` value and no historical row added.
1060    fn insert_new_account_header(
1061        tx: &Transaction<'_>,
1062        new_header: &AccountHeader,
1063        account_seed: Option<Word>,
1064        watched: bool,
1065    ) -> Result<(), StoreError> {
1066        let id = new_header.id().to_bytes();
1067        let code_commitment = new_header.code_commitment().to_bytes();
1068        let storage_commitment = new_header.storage_commitment().to_bytes();
1069        let vault_root = new_header.vault_root().to_bytes();
1070        let nonce = u64_to_value(new_header.nonce().as_canonical_u64());
1071        let commitment = new_header.to_commitment().to_bytes();
1072        let account_seed = account_seed.map(|seed| seed.to_bytes());
1073
1074        const LATEST_QUERY: &str = insert_sql!(
1075            latest_account_headers {
1076                id,
1077                code_commitment,
1078                storage_commitment,
1079                vault_root,
1080                nonce,
1081                account_seed,
1082                account_commitment,
1083                locked,
1084                watched
1085            } | REPLACE
1086        );
1087
1088        tx.execute(
1089            LATEST_QUERY,
1090            params![
1091                id,
1092                code_commitment,
1093                storage_commitment,
1094                vault_root,
1095                nonce,
1096                account_seed,
1097                commitment,
1098                false,
1099                watched,
1100            ],
1101        )
1102        .into_store_error()?;
1103
1104        Ok(())
1105    }
1106
1107    /// Replaces an account's latest header, archiving the previous one to historical.
1108    ///
1109    /// Preserves the `watched` flag from the existing latest row (mode is a per-account
1110    /// property, not per-state). The new latest row is written with `account_seed = NULL`
1111    /// and `locked = false`; the previous seed and lock state move into the historical row.
1112    fn replace_account_header(
1113        tx: &Transaction<'_>,
1114        new_header: &AccountHeader,
1115        old_header: &AccountHeader,
1116    ) -> Result<(), StoreError> {
1117        if new_header.id() != old_header.id() {
1118            return Err(StoreError::DatabaseError(format!(
1119                "replace_account_header: account id mismatch (new: {}, old: {})",
1120                new_header.id(),
1121                old_header.id(),
1122            )));
1123        }
1124        if new_header.nonce().as_canonical_u64() < old_header.nonce().as_canonical_u64() {
1125            return Err(StoreError::DatabaseError(format!(
1126                "replace_account_header: new nonce {} is less than old nonce {} for account {}",
1127                new_header.nonce().as_canonical_u64(),
1128                old_header.nonce().as_canonical_u64(),
1129                new_header.id(),
1130            )));
1131        }
1132
1133        let id_bytes = new_header.id().to_bytes();
1134
1135        // `AccountHeader` doesn't carry the seed or per-account flags, so read them from the row
1136        // we're about to overwrite: `account_seed`/`locked` get archived into the historical row,
1137        // `watched` is carried into the new latest row.
1138        let (old_seed, old_locked, old_watched): (Option<Vec<u8>>, bool, bool) = tx
1139            .query_row(
1140                "SELECT account_seed, locked, watched FROM latest_account_headers WHERE id = ?",
1141                params![&id_bytes],
1142                |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)),
1143            )
1144            .optional()
1145            .into_store_error()?
1146            .unwrap_or((None, false, false));
1147
1148        // Archive the old header to historical.
1149        let old_id = old_header.id().to_bytes();
1150        let old_code_commitment = old_header.code_commitment().to_bytes();
1151        let old_storage_commitment = old_header.storage_commitment().to_bytes();
1152        let old_vault_root = old_header.vault_root().to_bytes();
1153        let old_nonce = u64_to_value(old_header.nonce().as_canonical_u64());
1154        let old_commitment = old_header.to_commitment().to_bytes();
1155        let replaced_at_nonce = u64_to_value(new_header.nonce().as_canonical_u64());
1156
1157        const HISTORICAL_QUERY: &str = insert_sql!(
1158            historical_account_headers {
1159                id,
1160                code_commitment,
1161                storage_commitment,
1162                vault_root,
1163                nonce,
1164                account_seed,
1165                account_commitment,
1166                locked,
1167                replaced_at_nonce
1168            } | REPLACE
1169        );
1170
1171        tx.execute(
1172            HISTORICAL_QUERY,
1173            params![
1174                old_id,
1175                old_code_commitment,
1176                old_storage_commitment,
1177                old_vault_root,
1178                old_nonce,
1179                old_seed,
1180                old_commitment,
1181                old_locked,
1182                replaced_at_nonce,
1183            ],
1184        )
1185        .into_store_error()?;
1186
1187        // Write the new latest row.
1188        Self::insert_new_account_header(tx, new_header, None, old_watched)
1189    }
1190
1191    /// Prunes historical account states for a single account up to the given nonce.
1192    ///
1193    /// Deletes all historical entries with `replaced_at_nonce <= up_to_nonce`
1194    /// (see DESIGN.md for why this threshold is safe), then removes any account
1195    /// code that was only referenced by the deleted headers.
1196    pub fn prune_account_history(
1197        conn: &mut Connection,
1198        account_id: AccountId,
1199        up_to_nonce: Felt,
1200    ) -> Result<usize, StoreError> {
1201        let tx = conn.transaction().into_store_error()?;
1202        let account_id_bytes = account_id.to_bytes();
1203        let boundary_val = u64_to_value(up_to_nonce.as_canonical_u64());
1204        let mut total_deleted: usize = 0;
1205
1206        // Collect code commitments from headers we are about to delete.
1207        let candidate_code_commitments: Vec<Vec<u8>> = {
1208            let mut stmt = tx
1209                .prepare(
1210                    "SELECT DISTINCT code_commitment FROM historical_account_headers \
1211                     WHERE id = ? AND replaced_at_nonce <= ?",
1212                )
1213                .into_store_error()?;
1214            let rows = stmt
1215                .query_map(params![&account_id_bytes, &boundary_val], |row| row.get(0))
1216                .into_store_error()?;
1217            rows.collect::<Result<Vec<Vec<u8>>, _>>().into_store_error()?
1218        };
1219
1220        // Delete historical entries.
1221        total_deleted += tx
1222            .execute(
1223                "DELETE FROM historical_account_headers \
1224                 WHERE id = ? AND replaced_at_nonce <= ?",
1225                params![&account_id_bytes, &boundary_val],
1226            )
1227            .into_store_error()?;
1228
1229        total_deleted += tx
1230            .execute(
1231                "DELETE FROM historical_account_storage \
1232                 WHERE account_id = ? AND replaced_at_nonce <= ?",
1233                params![&account_id_bytes, &boundary_val],
1234            )
1235            .into_store_error()?;
1236
1237        total_deleted += tx
1238            .execute(
1239                "DELETE FROM historical_storage_map_entries \
1240                 WHERE account_id = ? AND replaced_at_nonce <= ?",
1241                params![&account_id_bytes, &boundary_val],
1242            )
1243            .into_store_error()?;
1244
1245        total_deleted += tx
1246            .execute(
1247                "DELETE FROM historical_account_assets \
1248                 WHERE account_id = ? AND replaced_at_nonce <= ?",
1249                params![&account_id_bytes, &boundary_val],
1250            )
1251            .into_store_error()?;
1252
1253        // Delete orphaned code: only check commitments from the deleted headers,
1254        // and only if they are not referenced by any remaining header or foreign code.
1255        for commitment in &candidate_code_commitments {
1256            let still_referenced: bool = tx
1257                .query_row(
1258                    "SELECT EXISTS(
1259                        SELECT 1 FROM latest_account_headers WHERE code_commitment = ?1
1260                        UNION ALL
1261                        SELECT 1 FROM historical_account_headers WHERE code_commitment = ?1
1262                        UNION ALL
1263                        SELECT 1 FROM foreign_account_code WHERE code_commitment = ?1
1264                    )",
1265                    params![commitment],
1266                    |row| row.get(0),
1267                )
1268                .into_store_error()?;
1269
1270            if !still_referenced {
1271                total_deleted += tx
1272                    .execute("DELETE FROM account_code WHERE commitment = ?", params![commitment])
1273                    .into_store_error()?;
1274            }
1275        }
1276
1277        tx.commit().into_store_error()?;
1278        Ok(total_deleted)
1279    }
1280}