zcash_client_sqlite: Use `prepare_cached` instead of manual statement caching.

`rusqlite` includes a mechanism for creating prepared statements that
automatically caches them and reuses the caches when possible. This
means that it's unnecessary for us to do our own caching, and also
offers a minor performance improvement in that we don't need to eagerly
prepare statements that we may not execute in the lifetime of a given
`WalletDb` object. It also improves code locality, because the prepared
statements are now adjacent in the code to the parameter assignment
blocks that correspond to those statements.

This also updates a number of `put_x` methods to use sqlite upsert
functionality via the `ON CONFLICT` clause, instead of having to perform
separate inserts and updates.
This commit is contained in:
Kris Nuttycombe 2023-06-06 12:35:39 -06:00
parent d2f105efe9
commit 8d86ffd9c4
2 changed files with 59 additions and 165 deletions

View File

@ -89,14 +89,6 @@ impl<'a> InsertAddress<'a> {
/// [`WalletWrite`]: zcash_client_backend::data_api::WalletWrite
pub struct DataConnStmtCache<'a, P> {
pub(crate) wallet_db: &'a WalletDb<P>,
stmt_insert_block: Statement<'a>,
stmt_insert_tx_meta: Statement<'a>,
stmt_update_tx_meta: Statement<'a>,
stmt_insert_tx_data: Statement<'a>,
stmt_update_tx_data: Statement<'a>,
stmt_select_tx_ref: Statement<'a>,
stmt_mark_sapling_note_spent: Statement<'a>,
#[cfg(feature = "transparent-inputs")]
@ -129,32 +121,6 @@ impl<'a, P> DataConnStmtCache<'a, P> {
Ok(
DataConnStmtCache {
wallet_db,
stmt_insert_block: wallet_db.conn.prepare(
"INSERT INTO blocks (height, hash, time, sapling_tree)
VALUES (?, ?, ?, ?)",
)?,
stmt_insert_tx_meta: wallet_db.conn.prepare(
"INSERT INTO transactions (txid, block, tx_index)
VALUES (?, ?, ?)",
)?,
stmt_update_tx_meta: wallet_db.conn.prepare(
"UPDATE transactions
SET block = ?, tx_index = ? WHERE txid = ?",
)?,
stmt_insert_tx_data: wallet_db.conn.prepare(
"INSERT INTO transactions (txid, created, expiry_height, raw, fee)
VALUES (?, ?, ?, ?, ?)",
)?,
stmt_update_tx_data: wallet_db.conn.prepare(
"UPDATE transactions
SET expiry_height = :expiry_height,
raw = :raw,
fee = IFNULL(:fee, fee)
WHERE txid = :txid",
)?,
stmt_select_tx_ref: wallet_db.conn.prepare(
"SELECT id_tx FROM transactions WHERE txid = ?",
)?,
stmt_mark_sapling_note_spent: wallet_db.conn.prepare(
"UPDATE sapling_received_notes SET spent = ? WHERE nf = ?"
)?,
@ -271,119 +237,6 @@ impl<'a, P> DataConnStmtCache<'a, P> {
)
}
/// Inserts information about a scanned block into the database.
pub fn stmt_insert_block(
&mut self,
block_height: BlockHeight,
block_hash: BlockHash,
block_time: u32,
commitment_tree: &sapling::CommitmentTree,
) -> Result<(), SqliteClientError> {
let mut encoded_tree = Vec::new();
write_commitment_tree(commitment_tree, &mut encoded_tree).unwrap();
self.stmt_insert_block.execute(params![
u32::from(block_height),
&block_hash.0[..],
block_time,
encoded_tree
])?;
Ok(())
}
/// Inserts the given transaction and its block metadata into the wallet.
///
/// Returns the database row for the newly-inserted transaction, or an error if the
/// transaction exists.
pub(crate) fn stmt_insert_tx_meta(
&mut self,
txid: &TxId,
height: BlockHeight,
tx_index: usize,
) -> Result<i64, SqliteClientError> {
self.stmt_insert_tx_meta.execute(params![
&txid.as_ref()[..],
u32::from(height),
(tx_index as i64),
])?;
Ok(self.wallet_db.conn.last_insert_rowid())
}
/// Updates the block metadata for the given transaction.
///
/// Returns `false` if the transaction doesn't exist in the wallet.
pub(crate) fn stmt_update_tx_meta(
&mut self,
height: BlockHeight,
tx_index: usize,
txid: &TxId,
) -> Result<bool, SqliteClientError> {
match self.stmt_update_tx_meta.execute(params![
u32::from(height),
(tx_index as i64),
&txid.as_ref()[..],
])? {
0 => Ok(false),
1 => Ok(true),
_ => unreachable!("txid column is marked as UNIQUE"),
}
}
/// Inserts the given transaction and its data into the wallet.
///
/// Returns the database row for the newly-inserted transaction, or an error if the
/// transaction exists.
pub(crate) fn stmt_insert_tx_data(
&mut self,
txid: &TxId,
created_at: Option<time::OffsetDateTime>,
expiry_height: BlockHeight,
raw_tx: &[u8],
fee: Option<Amount>,
) -> Result<i64, SqliteClientError> {
self.stmt_insert_tx_data.execute(params![
&txid.as_ref()[..],
created_at,
u32::from(expiry_height),
raw_tx,
fee.map(i64::from)
])?;
Ok(self.wallet_db.conn.last_insert_rowid())
}
/// Updates the data for the given transaction.
///
/// Returns `false` if the transaction doesn't exist in the wallet.
pub(crate) fn stmt_update_tx_data(
&mut self,
expiry_height: BlockHeight,
raw_tx: &[u8],
fee: Option<Amount>,
txid: &TxId,
) -> Result<bool, SqliteClientError> {
let sql_args: &[(&str, &dyn ToSql)] = &[
(":expiry_height", &u32::from(expiry_height)),
(":raw", &raw_tx),
(":fee", &fee.map(i64::from)),
(":txid", &&txid.as_ref()[..]),
];
match self.stmt_update_tx_data.execute(sql_args)? {
0 => Ok(false),
1 => Ok(true),
_ => unreachable!("txid column is marked as UNIQUE"),
}
}
/// Finds the database row for the given `txid`, if the transaction is in the wallet.
pub(crate) fn stmt_select_tx_ref(&mut self, txid: &TxId) -> Result<i64, SqliteClientError> {
self.stmt_select_tx_ref
.query_row([&txid.as_ref()[..]], |row| row.get(0))
.map_err(SqliteClientError::from)
}
/// Marks a given nullifier as having been revealed in the construction of the
/// specified transaction.
///

View File

@ -64,7 +64,7 @@
//! wallet.
//! - `memo` the shielded memo associated with the output, if any.
use rusqlite::{named_params, OptionalExtension, ToSql};
use rusqlite::{named_params, params, OptionalExtension, ToSql};
use std::collections::HashMap;
use std::convert::TryFrom;
@ -72,6 +72,7 @@ use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight, BranchId, NetworkUpgrade, Parameters},
memo::{Memo, MemoBytes},
merkle_tree::{write_commitment_tree, write_incremental_witness},
sapling::CommitmentTree,
transaction::{components::Amount, Transaction, TxId},
zip32::{
@ -94,7 +95,7 @@ use crate::{
#[cfg(feature = "transparent-inputs")]
use {
crate::UtxoId,
rusqlite::{params, Connection},
rusqlite::Connection,
std::collections::BTreeSet,
zcash_client_backend::{
address::AddressMetadata, encoding::AddressCodec, wallet::WalletTransparentOutput,
@ -739,7 +740,22 @@ pub(crate) fn insert_block<'a, P>(
block_time: u32,
commitment_tree: &CommitmentTree,
) -> Result<(), SqliteClientError> {
stmts.stmt_insert_block(block_height, block_hash, block_time, commitment_tree)
let mut encoded_tree = Vec::new();
write_commitment_tree(commitment_tree, &mut encoded_tree).unwrap();
let mut stmt_insert_block = stmts.wallet_db.conn.prepare_cached(
"INSERT INTO blocks (height, hash, time, sapling_tree)
VALUES (?, ?, ?, ?)",
)?;
stmt_insert_block.execute(params![
u32::from(block_height),
&block_hash.0[..],
block_time,
encoded_tree
])?;
Ok(())
}
/// Inserts information about a mined transaction that was observed to
@ -749,13 +765,25 @@ pub(crate) fn put_tx_meta<'a, P, N>(
tx: &WalletTx<N>,
height: BlockHeight,
) -> Result<i64, SqliteClientError> {
if !stmts.stmt_update_tx_meta(height, tx.index, &tx.txid)? {
// It isn't there, so insert our transaction into the database.
stmts.stmt_insert_tx_meta(&tx.txid, height, tx.index)
} else {
// It was there, so grab its row number.
stmts.stmt_select_tx_ref(&tx.txid)
}
// It isn't there, so insert our transaction into the database.
let mut stmt_upsert_tx_meta = stmts.wallet_db.conn.prepare_cached(
"INSERT INTO transactions (txid, block, tx_index)
VALUES (:txid, :block, :tx_index)
ON CONFLICT (txid) DO UPDATE
SET block = :block,
tx_index = :tx_index
RETURNING id_tx",
)?;
let tx_params = named_params![
":txid": &tx.txid.as_ref()[..],
":block": u32::from(height),
":tx_index": i64::try_from(tx.index).expect("transaction indices are representable as i64"),
];
stmt_upsert_tx_meta
.query_row(tx_params, |row| row.get::<_, i64>(0))
.map_err(SqliteClientError::from)
}
/// Inserts full transaction data into the database.
@ -765,18 +793,31 @@ pub(crate) fn put_tx_data<'a, P>(
fee: Option<Amount>,
created_at: Option<time::OffsetDateTime>,
) -> Result<i64, SqliteClientError> {
let txid = tx.txid();
let mut stmt_upsert_tx_data = stmts.wallet_db.conn.prepare_cached(
"INSERT INTO transactions (txid, created, expiry_height, raw, fee)
VALUES (:txid, :created_at, :expiry_height, :raw, :fee)
ON CONFLICT (txid) DO UPDATE
SET expiry_height = :expiry_height,
raw = :raw,
fee = IFNULL(:fee, fee)
RETURNING id_tx",
)?;
let txid = tx.txid();
let mut raw_tx = vec![];
tx.write(&mut raw_tx)?;
if !stmts.stmt_update_tx_data(tx.expiry_height(), &raw_tx, fee, &txid)? {
// It isn't there, so insert our transaction into the database.
stmts.stmt_insert_tx_data(&txid, created_at, tx.expiry_height(), &raw_tx, fee)
} else {
// It was there, so grab its row number.
stmts.stmt_select_tx_ref(&txid)
}
let tx_params = named_params![
":txid": &txid.as_ref()[..],
":created_at": created_at,
":expiry_height": u32::from(tx.expiry_height()),
":raw": raw_tx,
":fee": fee.map(i64::from),
];
stmt_upsert_tx_data
.query_row(tx_params, |row| row.get::<_, i64>(0))
.map_err(SqliteClientError::from)
}
/// Marks the given UTXO as having been spent.