-
Notifications
You must be signed in to change notification settings - Fork 307
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(sqlite): add bdk_sqlite crate implementing PersistBackend backed…
… by a SQLite database
- Loading branch information
1 parent
b8aa76c
commit 658358b
Showing
13 changed files
with
1,195 additions
and
154 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,3 +7,4 @@ Cargo.lock | |
|
||
# Example persisted files. | ||
*.db | ||
*.sqlite* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
[package] | ||
name = "bdk_sqlite" | ||
version = "0.1.0" | ||
edition = "2021" | ||
license = "MIT OR Apache-2.0" | ||
repository = "https://github.com/bitcoindevkit/bdk" | ||
documentation = "https://docs.rs/bdk_sqlite" | ||
description = "A simple SQLite based implementation of Persist for Bitcoin Dev Kit." | ||
keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"] | ||
authors = ["Bitcoin Dev Kit Developers"] | ||
readme = "README.md" | ||
|
||
[dependencies] | ||
anyhow = { version = "1", default-features = false } | ||
bdk_chain = { path = "../chain", version = "0.14.0", features = ["serde", "miniscript"] } | ||
bdk_persist = { path = "../persist", version = "0.2.0", features = ["serde"] } | ||
rusqlite = { version = "0.31.0", features = ["bundled"] } | ||
serde = { version = "1", features = ["derive"] } | ||
serde_json = "1" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# BDK SQLite | ||
|
||
This is a simple [SQLite] relational database schema backed implementation of [`PersistBackend`](bdk_persist::PersistBackend). | ||
|
||
The main structure is `Store` which persists [`bdk_persist`] `CombinedChangeSet` data into a SQLite database file. | ||
|
||
[`bdk_persist`]:https://docs.rs/bdk_persist/latest/bdk_persist/ | ||
[SQLite]: https://www.sqlite.org/index.html |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
-- schema version control | ||
CREATE TABLE version | ||
( | ||
version INTEGER | ||
) STRICT; | ||
INSERT INTO version | ||
VALUES (1); | ||
|
||
-- network is the valid network for all other table data | ||
CREATE TABLE network | ||
( | ||
name TEXT UNIQUE NOT NULL | ||
) STRICT; | ||
|
||
-- keychain is the json serialized keychain structure as JSONB, | ||
-- descriptor is the complete descriptor string, | ||
-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum, | ||
-- last revealed index is a u32 | ||
CREATE TABLE keychain | ||
( | ||
keychain BLOB PRIMARY KEY NOT NULL, | ||
descriptor TEXT NOT NULL, | ||
descriptor_id BLOB NOT NULL, | ||
last_revealed INTEGER | ||
) STRICT; | ||
|
||
-- hash is block hash hex string, | ||
-- block height is a u32, | ||
CREATE TABLE block | ||
( | ||
hash TEXT PRIMARY KEY NOT NULL, | ||
height INTEGER NOT NULL | ||
) STRICT; | ||
|
||
-- txid is transaction hash hex string (reversed) | ||
-- whole_tx is a consensus encoded transaction, | ||
-- last seen is a u64 unix epoch seconds | ||
CREATE TABLE tx | ||
( | ||
txid TEXT PRIMARY KEY NOT NULL, | ||
whole_tx BLOB, | ||
last_seen INTEGER | ||
) STRICT; | ||
|
||
-- Outpoint txid hash hex string (reversed) | ||
-- Outpoint vout | ||
-- TxOut value as SATs | ||
-- TxOut script consensus encoded | ||
CREATE TABLE txout | ||
( | ||
txid TEXT NOT NULL, | ||
vout INTEGER NOT NULL, | ||
value INTEGER NOT NULL, | ||
script BLOB NOT NULL, | ||
PRIMARY KEY (txid, vout) | ||
) STRICT; | ||
|
||
-- join table between anchor and tx | ||
-- block hash hex string | ||
-- anchor is a json serialized Anchor structure as JSONB, | ||
-- txid is transaction hash hex string (reversed) | ||
CREATE TABLE anchor_tx | ||
( | ||
block_hash TEXT NOT NULL, | ||
anchor BLOB NOT NULL, | ||
txid TEXT NOT NULL REFERENCES tx (txid), | ||
UNIQUE (anchor, txid), | ||
FOREIGN KEY (block_hash) REFERENCES block(hash) | ||
) STRICT; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
#![doc = include_str!("../README.md")] | ||
// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined | ||
#![cfg_attr(docsrs, feature(doc_cfg))] | ||
|
||
mod schema; | ||
mod store; | ||
|
||
use bdk_chain::bitcoin::Network; | ||
pub use rusqlite; | ||
pub use store::Store; | ||
|
||
/// Error that occurs while reading or writing change sets with the SQLite database. | ||
#[derive(Debug)] | ||
pub enum Error { | ||
/// Invalid network, cannot change the one already stored in the database. | ||
Network { expected: Network, given: Network }, | ||
/// SQLite error. | ||
Sqlite(rusqlite::Error), | ||
} | ||
|
||
impl core::fmt::Display for Error { | ||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
match self { | ||
Self::Network { expected, given } => write!( | ||
f, | ||
"network error trying to read or write change set, expected {}, given {}", | ||
expected, given | ||
), | ||
Self::Sqlite(e) => write!(f, "sqlite error reading or writing changeset: {}", e), | ||
} | ||
} | ||
} | ||
|
||
impl std::error::Error for Error {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
use crate::Store; | ||
use rusqlite::{named_params, Connection, Error}; | ||
|
||
const SCHEMA_0: &str = include_str!("../schema/schema_0.sql"); | ||
const MIGRATIONS: &[&str] = &[SCHEMA_0]; | ||
|
||
/// Schema migration related functions. | ||
impl<K, A> Store<K, A> { | ||
/// Migrate sqlite db schema to latest version. | ||
pub(crate) fn migrate(conn: &mut Connection) -> Result<(), Error> { | ||
let stmts = &MIGRATIONS | ||
.iter() | ||
.flat_map(|stmt| { | ||
// remove comment lines | ||
let s = stmt | ||
.split('\n') | ||
.filter(|l| !l.starts_with("--") && !l.is_empty()) | ||
.collect::<Vec<_>>() | ||
.join(" "); | ||
// split into statements | ||
s.split(';') | ||
// remove extra spaces | ||
.map(|s| { | ||
s.trim() | ||
.split(' ') | ||
.filter(|s| !s.is_empty()) | ||
.collect::<Vec<_>>() | ||
.join(" ") | ||
}) | ||
.collect::<Vec<_>>() | ||
}) | ||
// remove empty statements | ||
.filter(|s| !s.is_empty()) | ||
.collect::<Vec<String>>(); | ||
|
||
let version = Self::get_schema_version(conn)?; | ||
let stmts = &stmts[(version as usize)..]; | ||
|
||
// begin transaction, all migration statements and new schema version commit or rollback | ||
let tx = conn.transaction()?; | ||
|
||
// execute every statement and return `Some` new schema version | ||
// if execution fails, return `Error::Rusqlite` | ||
// if no statements executed returns `None` | ||
let new_version = stmts | ||
.iter() | ||
.enumerate() | ||
.map(|version_stmt| { | ||
tx.execute(version_stmt.1.as_str(), []) | ||
// map result value to next migration version | ||
.map(|_| version_stmt.0 as i32 + version + 1) | ||
}) | ||
.last() | ||
.transpose()?; | ||
|
||
// if `Some` new statement version, set new schema version | ||
if let Some(version) = new_version { | ||
Self::set_schema_version(&tx, version)?; | ||
} | ||
|
||
// commit transaction | ||
tx.commit()?; | ||
Ok(()) | ||
} | ||
|
||
fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> { | ||
let statement = conn.prepare_cached("SELECT version FROM version"); | ||
match statement { | ||
Err(Error::SqliteFailure(e, Some(msg))) => { | ||
if msg == "no such table: version" { | ||
Ok(0) | ||
} else { | ||
Err(Error::SqliteFailure(e, Some(msg))) | ||
} | ||
} | ||
Ok(mut stmt) => { | ||
let mut rows = stmt.query([])?; | ||
match rows.next()? { | ||
Some(row) => { | ||
let version: i32 = row.get(0)?; | ||
Ok(version) | ||
} | ||
None => Ok(0), | ||
} | ||
} | ||
_ => Ok(0), | ||
} | ||
} | ||
|
||
fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> { | ||
conn.execute( | ||
"UPDATE version SET version=:version", | ||
named_params! {":version": version}, | ||
) | ||
} | ||
} |
Oops, something went wrong.