Skip to main content

depot_client_types/
lib.rs

1//! Shared SQLite execution types for local and remote depot client backends.
2
3pub const HEAD_FENCE_MISMATCH_GROUP: &str = "depot";
4pub const HEAD_FENCE_MISMATCH_CODE: &str = "head_fence_mismatch";
5
6pub fn is_head_fence_mismatch(group: &str, code: &str) -> bool {
7	group == HEAD_FENCE_MISMATCH_GROUP && code == HEAD_FENCE_MISMATCH_CODE
8}
9
10/// Pages per storage shard. Shared rather than each side keeping its own copy: the client cuts
11/// staged commit segments on these boundaries and the engine rejects segments that are not cut on
12/// them, so two definitions could disagree and make every large commit fail.
13pub const SHARD_SIZE: u32 = 64;
14
15/// Shards one staged commit segment may span.
16pub const COMMIT_SEGMENT_MAX_SHARDS: u32 = 5;
17
18/// Dirty pages one commit may carry, staged or not.
19///
20/// A PIDX row is only about 64 bytes of key and value, but that is not what FoundationDB charges
21/// against its 10 MB transaction limit. Each `set` also carries a write conflict range, and the
22/// conflict ranges dominate: measured against a real cluster, the finalize transaction costs roughly
23/// 170 bytes per page, so the hard ceiling is near 61,000 pages. A commit of 60,146 pages publishes
24/// and one of 62,152 fails with a non-retryable `transaction_too_large`.
25///
26/// Half of that measured ceiling is the cap. The remaining headroom is not slack: truncate cleanup
27/// deletes every above-EOF PIDX row in the same transaction and is not bounded by this constant at
28/// all, so a commit that shrinks a large database pays for pages this number never counted.
29///
30/// Shared rather than duplicated so the client can refuse an oversized commit before staging any of
31/// it. The engine still enforces it, since an old client is not bound by a new value here, but a
32/// client that knows the number does not have to send 128 MiB to be told no.
33pub const MAX_COMMIT_DIRTY_PAGES: usize = 32_768;
34
35/// Where a staged commit segment carrying `pgno` must start.
36pub fn segment_start_for_page(pgno: u32) -> u32 {
37	pgno / SHARD_SIZE * SHARD_SIZE
38}
39
40/// Cuts pages into shard-aligned segments, each spanning at most `COMMIT_SEGMENT_MAX_SHARDS`.
41///
42/// The load-bearing property is that no shard's pages are split across two segments. Compaction
43/// folds a segment into a shard image, so a split shard could be folded from one segment and written
44/// as an image missing the other's newer pages.
45///
46/// `pages` must be sorted ascending by page number; the caller sorts once rather than this sorting
47/// per call.
48pub fn cut_page_segments<T>(pages: &[T], pgno_of: impl Fn(&T) -> u32) -> Vec<(u32, &[T])> {
49	let mut segments = Vec::new();
50	let mut start = 0;
51	while start < pages.len() {
52		let first_shard = pgno_of(&pages[start]) / SHARD_SIZE;
53		// Saturating, so pages at the very top of the page space cut one final segment rather than
54		// wrapping into an empty one and looping.
55		let shard_limit = first_shard.saturating_add(COMMIT_SEGMENT_MAX_SHARDS);
56		let end = pages[start..]
57			.iter()
58			.position(|page| pgno_of(page) / SHARD_SIZE >= shard_limit)
59			.map_or(pages.len(), |offset| start + offset);
60		segments.push((first_shard * SHARD_SIZE, &pages[start..end]));
61		start = end;
62	}
63
64	segments
65}
66
67#[derive(Clone, Debug, PartialEq)]
68pub enum BindParam {
69	Null,
70	Integer(i64),
71	Float(f64),
72	Text(String),
73	Blob(Vec<u8>),
74}
75
76#[derive(Clone, Debug, PartialEq)]
77pub struct ExecResult {
78	pub changes: i64,
79}
80
81#[derive(Clone, Debug, PartialEq)]
82pub struct QueryResult {
83	pub columns: Vec<String>,
84	pub rows: Vec<Vec<ColumnValue>>,
85}
86
87#[derive(Clone, Debug, PartialEq)]
88pub struct ExecuteResult {
89	pub columns: Vec<String>,
90	pub rows: Vec<Vec<ColumnValue>>,
91	pub changes: i64,
92	pub last_insert_row_id: Option<i64>,
93}
94
95impl ExecuteResult {
96	pub fn into_query_result(self) -> QueryResult {
97		QueryResult {
98			columns: self.columns,
99			rows: self.rows,
100		}
101	}
102
103	pub fn into_exec_result(self) -> ExecResult {
104		ExecResult {
105			changes: self.changes,
106		}
107	}
108}
109
110#[derive(Clone, Debug, PartialEq)]
111pub enum ColumnValue {
112	Null,
113	Integer(i64),
114	Float(f64),
115	Text(String),
116	Blob(Vec<u8>),
117}
118
119#[cfg(test)]
120mod tests {
121	use super::{ColumnValue, ExecuteResult};
122
123	#[test]
124	fn execute_result_preserves_result_and_route_metadata() {
125		let result = ExecuteResult {
126			columns: vec!["id".to_owned(), "name".to_owned()],
127			rows: vec![vec![
128				ColumnValue::Integer(7),
129				ColumnValue::Text("alpha".to_owned()),
130			]],
131			changes: 3,
132			last_insert_row_id: Some(42),
133		};
134
135		assert_eq!(result.columns, vec!["id", "name"]);
136		assert_eq!(
137			result.rows,
138			vec![vec![
139				ColumnValue::Integer(7),
140				ColumnValue::Text("alpha".to_owned())
141			]]
142		);
143		assert_eq!(result.changes, 3);
144		assert_eq!(result.last_insert_row_id, Some(42));
145	}
146
147	#[test]
148	fn execute_result_projects_query_and_exec_results() {
149		let result = ExecuteResult {
150			columns: vec!["count".to_owned()],
151			rows: vec![vec![ColumnValue::Integer(9)]],
152			changes: 2,
153			last_insert_row_id: Some(10),
154		};
155
156		let query_result = result.clone().into_query_result();
157		assert_eq!(query_result.columns, vec!["count"]);
158		assert_eq!(query_result.rows, vec![vec![ColumnValue::Integer(9)]]);
159
160		let exec_result = result.into_exec_result();
161		assert_eq!(exec_result.changes, 2);
162	}
163}