use super::{CorruptedError, InternalError, SqliteFullDatabase};
use std::path::Path;
pub fn open(config: Config) -> Result<DatabaseOpen, InternalError> {
let flags = rusqlite::OpenFlags::SQLITE_OPEN_READ_WRITE |
rusqlite::OpenFlags::SQLITE_OPEN_CREATE |
rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX;
let database = match config.ty {
ConfigTy::Disk { path, .. } => rusqlite::Connection::open_with_flags(path, flags),
ConfigTy::Memory => rusqlite::Connection::open_in_memory_with_flags(flags),
}
.map_err(InternalError)?;
database.set_prepared_statement_cache_capacity(64);
database
.execute_batch(
r#"
-- See https://sqlite.org/pragma.html and https://www.sqlite.org/wal.html
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA encoding = 'UTF-8';
PRAGMA trusted_schema = false;
PRAGMA foreign_keys = ON;
"#,
)
.map_err(InternalError)?;
database
.execute(
&format!(
"PRAGMA cache_size = {}",
0i64.saturating_sub_unsigned(
u64::try_from((config.cache_size.saturating_sub(1) / 1024).saturating_add(1))
.unwrap_or(u64::MAX),
)
),
(),
)
.map_err(InternalError)?;
if let ConfigTy::Disk {
memory_map_size, ..
} = config.ty
{
database
.execute_batch(&format!("PRAGMA mmap_size = {}", memory_map_size))
.map_err(InternalError)?;
}
let user_version = database
.prepare_cached("PRAGMA user_version")
.map_err(InternalError)?
.query_row((), |row| row.get::<_, i64>(0))
.map_err(InternalError)?;
if user_version <= 0 {
database
.execute_batch(
r#"
-- `auto_vacuum` can switched between `NONE` and non-`NONE` on newly-created database.
PRAGMA auto_vacuum = INCREMENTAL;
/*
Contains all the "global" values in the database.
A value must be present either in `value_blob` or `value_number` depending on the type of data.
Keys in that table:
- `best` (blob): Hash of the best block.
- `finalized` (number): Height of the finalized block, as a 64bits big endian number.
*/
CREATE TABLE meta(
key STRING NOT NULL PRIMARY KEY,
value_blob BLOB,
value_number INTEGER,
-- Either `value_blob` or `value_number` must be NULL but not both.
CHECK((value_blob IS NULL OR value_number IS NULL) AND (value_blob IS NOT NULL OR value_number IS NOT NULL))
);
/*
List of all trie nodes of all blocks whose trie is stored in the database.
*/
CREATE TABLE trie_node(
hash BLOB NOT NULL PRIMARY KEY,
partial_key BLOB NOT NULL -- Each byte is a nibble, in other words all bytes are <16
);
/*
Storage associated to a trie node.
For each entry in `trie_node` there exists either 0 or 1 entry in `trie_node_storage` indicating
the storage value associated to this node.
*/
CREATE TABLE trie_node_storage(
node_hash BLOB NOT NULL PRIMARY KEY,
value BLOB,
trie_root_ref BLOB,
trie_entry_version INTEGER NOT NULL,
FOREIGN KEY (node_hash) REFERENCES trie_node(hash) ON UPDATE CASCADE ON DELETE CASCADE
CHECK((value IS NULL) != (trie_root_ref IS NULL))
);
CREATE INDEX trie_node_storage_by_trie_root_ref ON trie_node_storage(trie_root_ref);
/*
Parent-child relationship between trie nodes.
*/
CREATE TABLE trie_node_child(
hash BLOB NOT NULL,
child_num BLOB NOT NULL, -- Always contains one single byte. We use `BLOB` instead of `INTEGER` because SQLite stupidly doesn't provide any way of converting between integers and blobs
child_hash BLOB NOT NULL,
PRIMARY KEY (hash, child_num),
FOREIGN KEY (hash) REFERENCES trie_node(hash) ON UPDATE CASCADE ON DELETE CASCADE
CHECK(LENGTH(child_num) == 1 AND HEX(child_num) < '10')
);
CREATE INDEX trie_node_child_by_hash ON trie_node_child(hash);
CREATE INDEX trie_node_child_by_child_hash ON trie_node_child(child_hash);
/*
List of all known blocks, indexed by their hash or number.
*/
CREATE TABLE blocks(
hash BLOB NOT NULL PRIMARY KEY,
parent_hash BLOB, -- NULL only for the genesis block
state_trie_root_hash BLOB, -- NULL if and only if the trie is empty or if the trie storage has been pruned from the database
number INTEGER NOT NULL,
header BLOB NOT NULL,
justification BLOB,
is_best_chain BOOLEAN NOT NULL,
UNIQUE(number, hash)
);
CREATE INDEX blocks_by_number ON blocks(number);
CREATE INDEX blocks_by_parent ON blocks(parent_hash);
CREATE INDEX blocks_by_state_trie_root_hash ON blocks(state_trie_root_hash);
CREATE INDEX blocks_by_best ON blocks(number, is_best_chain);
/*
Each block has a body made from 0+ extrinsics (in practice, there's always at least one extrinsic,
but the database supports 0). This table contains these extrinsics.
The `idx` field contains the index between `0` and `num_extrinsics - 1`. The values in `idx` must
be contiguous for each block.
*/
CREATE TABLE blocks_body(
hash BLOB NOT NULL,
idx INTEGER NOT NULL,
extrinsic BLOB NOT NULL,
UNIQUE(hash, idx),
CHECK(length(hash) == 32),
FOREIGN KEY (hash) REFERENCES blocks(hash) ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE INDEX blocks_body_by_block ON blocks_body(hash);
PRAGMA user_version = 1;
"#,
)
.map_err(InternalError)?
}
let is_empty = database
.prepare_cached("SELECT COUNT(*) FROM meta WHERE key = ?")
.map_err(InternalError)?
.query_row(("best",), |row| row.get::<_, i64>(0))
.map_err(InternalError)?
== 0;
Ok(if !is_empty {
DatabaseOpen::Open(SqliteFullDatabase {
database: parking_lot::Mutex::new(database),
block_number_bytes: config.block_number_bytes, })
} else {
DatabaseOpen::Empty(DatabaseEmpty {
database,
block_number_bytes: config.block_number_bytes,
})
})
}
#[derive(Debug)]
pub struct Config<'a> {
pub ty: ConfigTy<'a>,
pub block_number_bytes: usize,
pub cache_size: usize,
}
#[derive(Debug)]
pub enum ConfigTy<'a> {
Disk {
path: &'a Path,
memory_map_size: usize,
},
Memory,
}
pub enum DatabaseOpen {
Open(SqliteFullDatabase),
Empty(DatabaseEmpty),
}
pub struct DatabaseEmpty {
database: rusqlite::Connection,
block_number_bytes: usize,
}
impl DatabaseEmpty {
pub fn initialize<'a>(
self,
finalized_block_header: &[u8],
finalized_block_body: impl ExactSizeIterator<Item = &'a [u8]>,
finalized_block_justification: Option<Vec<u8>>,
) -> Result<SqliteFullDatabase, CorruptedError> {
let database = SqliteFullDatabase {
database: parking_lot::Mutex::new(self.database),
block_number_bytes: self.block_number_bytes,
};
database.reset(
finalized_block_header,
finalized_block_body,
finalized_block_justification,
)?;
Ok(database)
}
}