parity_db/
options.rs

1// Copyright 2021-2022 Parity Technologies (UK) Ltd.
2// This file is dual-licensed as Apache-2.0 or MIT.
3
4use crate::{
5	column::{ColId, Salt},
6	compress::CompressionType,
7	error::{try_io, Error, Result},
8};
9use rand::Rng;
10use std::{collections::HashMap, path::Path};
11
12pub const CURRENT_VERSION: u32 = 9;
13// TODO on last supported 5, remove MULTIHEAD_V4 and MULTIPART_V4
14// TODO on last supported 8, remove XOR with salt in column::hash
15// TODO on last supported 9, remove MULTIHEAD_V8
16const LAST_SUPPORTED_VERSION: u32 = 4;
17
18pub const DEFAULT_COMPRESSION_THRESHOLD: u32 = 4096;
19
20/// Database configuration.
21#[derive(Clone, Debug)]
22pub struct Options {
23	/// Database path.
24	pub path: std::path::PathBuf,
25	/// Column settings
26	pub columns: Vec<ColumnOptions>,
27	/// fsync WAL to disk before committing any changes. Provides extra consistency
28	/// guarantees. On by default.
29	pub sync_wal: bool,
30	/// fsync/msync data to disk before removing logs. Provides crash resistance guarantee.
31	/// On by default.
32	pub sync_data: bool,
33	/// Collect database statistics. May have effect on performance.
34	pub stats: bool,
35	/// Override salt value. If `None` is specified salt is loaded from metadata
36	/// or randomly generated when creating a new database.
37	pub salt: Option<Salt>,
38	/// Minimal value size threshold to attempt compressing a value per column.
39	///
40	/// Optional. A sensible default is used if nothing is set for a given column.
41	pub compression_threshold: HashMap<ColId, u32>,
42	#[cfg(any(test, feature = "instrumentation"))]
43	/// Always starts background threads.
44	pub with_background_thread: bool,
45	#[cfg(any(test, feature = "instrumentation"))]
46	/// Always flushes data from the log to the on-disk data structures.
47	pub always_flush: bool,
48}
49
50/// Database column configuration.
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct ColumnOptions {
53	/// Indicates that the column value is the preimage of the key.
54	/// This implies that a given value always has the same key.
55	/// Enables some optimizations.
56	pub preimage: bool,
57	/// Indicates that the keys are at least 32 bytes and
58	/// the first 32 bytes have uniform distribution.
59	/// Allows for skipping additional key hashing.
60	pub uniform: bool,
61	/// Use reference counting for values. Reference operations are allowed for a column. The value
62	/// is deleted when the counter reaches zero.
63	pub ref_counted: bool,
64	/// Compression to use for this column.
65	pub compression: CompressionType,
66	/// Column is configured to use Btree storage. Btree columns allow for ordered key iteration
67	/// and key retrieval, but are significantly less performant and require more disk space.
68	pub btree_index: bool,
69	/// Column supports Multitree operations. This allows committing and querying of tree
70	/// structures.
71	pub multitree: bool,
72	/// Column is append-only. Delete operations are ignored.
73	pub append_only: bool,
74	/// Allow Multitree root and child nodes to be accessed directly without using TreeReader.
75	/// Client code must ensure this is safe.
76	pub allow_direct_node_access: bool,
77}
78
79/// Database metadata.
80#[derive(Clone, Debug)]
81pub struct Metadata {
82	/// Salt value.
83	pub salt: Salt,
84	/// Database version.
85	pub version: u32,
86	/// Column metadata.
87	pub columns: Vec<ColumnOptions>,
88}
89
90impl ColumnOptions {
91	fn as_string(&self) -> String {
92		format!(
93			"preimage: {}, uniform: {}, refc: {}, compression: {}, ordered: {}, multitree: {}, append_only: {}, allow_direct_node_access: {}",
94			self.preimage,
95			self.uniform,
96			self.ref_counted,
97			self.compression as u8,
98			self.btree_index,
99			self.multitree,
100			self.append_only,
101			self.allow_direct_node_access,
102		)
103	}
104
105	pub fn is_valid(&self) -> bool {
106		if self.ref_counted && !self.preimage {
107			log::error!(target: "parity-db", "Using `ref_counted` option without `preimage` enabled is not supported");
108			return false
109		}
110		if self.ref_counted && self.append_only {
111			log::error!(target: "parity-db", "`ref_counted` option is redundant when `append_only` is enabled");
112			return false
113		}
114		if self.multitree && self.compression != CompressionType::NoCompression {
115			log::error!(target: "parity-db", "Compression is not currently supported with multitree columns");
116			return false
117		}
118		true
119	}
120
121	fn from_string(s: &str) -> Option<Self> {
122		let mut split = s.split("sizes: ");
123		let vals = split.next()?;
124
125		let vals: HashMap<&str, &str> = vals
126			.split(", ")
127			.filter_map(|s| {
128				let mut pair = s.split(": ");
129				Some((pair.next()?, pair.next()?))
130			})
131			.collect();
132
133		let preimage = vals.get("preimage")?.parse().ok()?;
134		let uniform = vals.get("uniform")?.parse().ok()?;
135		let ref_counted = vals.get("refc")?.parse().ok()?;
136		let compression: u8 = vals.get("compression").and_then(|c| c.parse().ok()).unwrap_or(0);
137		let btree_index = vals.get("ordered").and_then(|c| c.parse().ok()).unwrap_or(false);
138		let multitree = vals.get("multitree").and_then(|c| c.parse().ok()).unwrap_or(false);
139		let append_only = vals.get("append_only").and_then(|c| c.parse().ok()).unwrap_or(false);
140		let allow_direct_node_access = vals
141			.get("allow_direct_node_access")
142			.and_then(|c| c.parse().ok())
143			.unwrap_or(false);
144
145		Some(ColumnOptions {
146			preimage,
147			uniform,
148			ref_counted,
149			compression: compression.into(),
150			btree_index,
151			multitree,
152			append_only,
153			allow_direct_node_access,
154		})
155	}
156}
157
158impl Default for ColumnOptions {
159	fn default() -> ColumnOptions {
160		ColumnOptions {
161			preimage: false,
162			uniform: false,
163			ref_counted: false,
164			compression: CompressionType::NoCompression,
165			btree_index: false,
166			multitree: false,
167			append_only: false,
168			allow_direct_node_access: false,
169		}
170	}
171}
172
173impl Options {
174	pub fn with_columns(path: &Path, num_columns: u8) -> Options {
175		Options {
176			path: path.into(),
177			sync_wal: true,
178			sync_data: true,
179			stats: true,
180			salt: None,
181			columns: (0..num_columns).map(|_| Default::default()).collect(),
182			compression_threshold: HashMap::new(),
183			#[cfg(any(test, feature = "instrumentation"))]
184			with_background_thread: true,
185			#[cfg(any(test, feature = "instrumentation"))]
186			always_flush: false,
187		}
188	}
189
190	// TODO on next major version remove in favor of write_metadata_with_version
191	pub fn write_metadata(&self, path: &Path, salt: &Salt) -> Result<()> {
192		self.write_metadata_with_version(path, salt, None)
193	}
194
195	// TODO on next major version remove in favor of write_metadata_with_version
196	pub fn write_metadata_file(&self, path: &Path, salt: &Salt) -> Result<()> {
197		self.write_metadata_file_with_version(path, salt, None)
198	}
199
200	pub fn write_metadata_with_version(
201		&self,
202		path: &Path,
203		salt: &Salt,
204		version: Option<u32>,
205	) -> Result<()> {
206		let mut path = path.to_path_buf();
207		path.push("metadata");
208		self.write_metadata_file_with_version(&path, salt, version)
209	}
210
211	pub fn write_metadata_file_with_version(
212		&self,
213		path: &Path,
214		salt: &Salt,
215		version: Option<u32>,
216	) -> Result<()> {
217		let mut metadata = vec![
218			format!("version={}", version.unwrap_or(CURRENT_VERSION)),
219			format!("salt={}", hex::encode(salt)),
220		];
221		for i in 0..self.columns.len() {
222			metadata.push(format!("col{}={}", i, self.columns[i].as_string()));
223		}
224		try_io!(std::fs::write(path, metadata.join("\n")));
225		Ok(())
226	}
227
228	pub fn load_and_validate_metadata(&self, create: bool) -> Result<Metadata> {
229		let meta = Self::load_metadata(&self.path)?;
230
231		if let Some(meta) = meta {
232			if meta.columns.len() != self.columns.len() {
233				return Err(Error::InvalidConfiguration(format!(
234					"Column config mismatch. Expected {} columns, got {}",
235					self.columns.len(),
236					meta.columns.len()
237				)))
238			}
239
240			for c in 0..meta.columns.len() {
241				if meta.columns[c] != self.columns[c] {
242					return Err(Error::IncompatibleColumnConfig {
243						id: c as ColId,
244						reason: format!(
245							"Column config mismatch. Expected \"{}\", got \"{}\"",
246							self.columns[c].as_string(),
247							meta.columns[c].as_string(),
248						),
249					})
250				}
251			}
252			Ok(meta)
253		} else if create {
254			let s: Salt = self.salt.unwrap_or_else(|| rand::rng().random());
255			self.write_metadata(&self.path, &s)?;
256			Ok(Metadata { version: CURRENT_VERSION, columns: self.columns.clone(), salt: s })
257		} else {
258			Err(Error::DatabaseNotFound)
259		}
260	}
261
262	pub fn load_metadata(path: &Path) -> Result<Option<Metadata>> {
263		let mut path = path.to_path_buf();
264		path.push("metadata");
265		Self::load_metadata_file(&path)
266	}
267
268	pub fn load_metadata_file(path: &Path) -> Result<Option<Metadata>> {
269		use std::{io::BufRead, str::FromStr};
270
271		if !path.exists() {
272			return Ok(None)
273		}
274		let file = std::io::BufReader::new(try_io!(std::fs::File::open(path)));
275		let mut salt = None;
276		let mut columns = Vec::new();
277		let mut version = 0;
278		for l in file.lines() {
279			let l = try_io!(l);
280			let mut vals = l.split('=');
281			let k = vals.next().ok_or_else(|| Error::Corruption("Bad metadata".into()))?;
282			let v = vals.next().ok_or_else(|| Error::Corruption("Bad metadata".into()))?;
283			if k == "version" {
284				version =
285					u32::from_str(v).map_err(|_| Error::Corruption("Bad version string".into()))?;
286			} else if k == "salt" {
287				let salt_slice =
288					hex::decode(v).map_err(|_| Error::Corruption("Bad salt string".into()))?;
289				let mut s = Salt::default();
290				s.copy_from_slice(&salt_slice);
291				salt = Some(s);
292			} else if k.starts_with("col") {
293				let col = ColumnOptions::from_string(v)
294					.ok_or_else(|| Error::Corruption("Bad column metadata".into()))?;
295				columns.push(col);
296			}
297		}
298		if version < LAST_SUPPORTED_VERSION {
299			return Err(Error::InvalidConfiguration(format!(
300				"Unsupported database version {version}. Expected {CURRENT_VERSION}"
301			)))
302		}
303		let salt = salt.ok_or_else(|| Error::InvalidConfiguration("Missing salt value".into()))?;
304		Ok(Some(Metadata { version, columns, salt }))
305	}
306
307	pub fn is_valid(&self) -> bool {
308		for option in self.columns.iter() {
309			if !option.is_valid() {
310				return false
311			}
312		}
313		true
314	}
315}
316
317impl Metadata {
318	pub fn columns_to_migrate(&self) -> std::collections::BTreeSet<u8> {
319		std::collections::BTreeSet::new()
320	}
321}