Skip to main content

sqlite_types/
lib.rs

1//! Shared SQLite execution types for local and remote depot client backends.
2
3mod page_bytes;
4
5pub use page_bytes::PageBytes;
6
7pub const HEAD_FENCE_MISMATCH_GROUP: &str = "depot";
8pub const HEAD_FENCE_MISMATCH_CODE: &str = "head_fence_mismatch";
9
10pub fn is_head_fence_mismatch(group: &str, code: &str) -> bool {
11	group == HEAD_FENCE_MISMATCH_GROUP && code == HEAD_FENCE_MISMATCH_CODE
12}
13
14/// The error a remote SQLite request reports when its transaction changed more pages than one
15/// commit may carry.
16pub const TRANSACTION_TOO_LARGE_GROUP: &str = "sqlite";
17pub const TRANSACTION_TOO_LARGE_CODE: &str = "transaction_too_large";
18
19pub fn is_transaction_too_large(group: &str, code: &str) -> bool {
20	group == TRANSACTION_TOO_LARGE_GROUP && code == TRANSACTION_TOO_LARGE_CODE
21}
22
23/// Recognizes a storage refusal that ran nothing and returns its retry delay. The page
24/// protocol carries the hint as `retry_after_ms=<ms>` in its message; older or malformed
25/// overload replies use 100 ms. Both local VFS and remote SQL use this classification.
26pub fn storage_overload_retry_after_ms(group: &str, code: &str, message: &str) -> Option<u64> {
27	if group != "depot" || code != "overloaded" {
28		return None;
29	}
30	let hint = message.split_once("retry_after_ms=").and_then(|(_, rest)| {
31		let end = rest
32			.find(|c: char| !c.is_ascii_digit())
33			.unwrap_or(rest.len());
34		rest[..end].parse::<u64>().ok()
35	});
36	Some(hint.unwrap_or(100))
37}
38
39/// Pages per storage shard. Shared rather than each side keeping its own copy: the client cuts
40/// staged commit segments on these boundaries and the engine rejects segments that are not cut on
41/// them, so two definitions could disagree and make every large commit fail.
42pub const SHARD_SIZE: u32 = 64;
43
44/// Shards one staged commit segment may span.
45pub const COMMIT_SEGMENT_MAX_SHARDS: u32 = 5;
46
47/// Actor-side ceiling: 128 MiB of 4 KiB pages. The server additionally enforces
48/// its configured filesystem transaction byte cap before admitting each segment.
49/// Large commits stream as bounded segments and publish at one commit sequence.
50pub const MAX_COMMIT_DIRTY_PAGES: usize = 32_768;
51
52/// Where a staged commit segment carrying `pgno` must start.
53pub fn segment_start_for_page(pgno: u32) -> u32 {
54	pgno / SHARD_SIZE * SHARD_SIZE
55}
56
57/// Cuts pages into shard-aligned segments, each spanning at most `COMMIT_SEGMENT_MAX_SHARDS`.
58///
59/// Shard alignment remains part of the existing wire protocol; the server now
60/// writes these pages into one ordinary SQLite file through FS transactions.
61///
62/// `pages` must be sorted ascending by page number; the caller sorts once rather than this sorting
63/// per call.
64pub fn cut_page_segments<T>(pages: &[T], pgno_of: impl Fn(&T) -> u32) -> Vec<(u32, &[T])> {
65	let mut segments = Vec::new();
66	let mut start = 0;
67	while start < pages.len() {
68		let first_shard = pgno_of(&pages[start]) / SHARD_SIZE;
69		// Saturating, so pages at the very top of the page space cut one final segment rather than
70		// wrapping into an empty one and looping.
71		let shard_limit = first_shard.saturating_add(COMMIT_SEGMENT_MAX_SHARDS);
72		let end = pages[start..]
73			.iter()
74			.position(|page| pgno_of(page) / SHARD_SIZE >= shard_limit)
75			.map_or(pages.len(), |offset| start + offset);
76		segments.push((first_shard * SHARD_SIZE, &pages[start..end]));
77		start = end;
78	}
79
80	segments
81}
82
83#[derive(Clone, Debug, PartialEq)]
84pub enum BindParam {
85	Null,
86	Integer(i64),
87	Float(f64),
88	Text(String),
89	Blob(Vec<u8>),
90}
91
92#[derive(Clone, Debug, PartialEq)]
93pub struct ExecResult {
94	pub changes: i64,
95}
96
97#[derive(Clone, Debug, PartialEq)]
98pub struct QueryResult {
99	pub columns: Vec<String>,
100	pub rows: Vec<Vec<ColumnValue>>,
101}
102
103#[derive(Clone, Debug, PartialEq)]
104pub struct ExecuteResult {
105	pub columns: Vec<String>,
106	pub rows: Vec<Vec<ColumnValue>>,
107	pub changes: i64,
108	pub last_insert_row_id: Option<i64>,
109}
110
111impl ExecuteResult {
112	pub fn into_query_result(self) -> QueryResult {
113		QueryResult {
114			columns: self.columns,
115			rows: self.rows,
116		}
117	}
118
119	pub fn into_exec_result(self) -> ExecResult {
120		ExecResult {
121			changes: self.changes,
122		}
123	}
124}
125
126#[derive(Clone, Debug, PartialEq)]
127pub enum ColumnValue {
128	Null,
129	Integer(i64),
130	Float(f64),
131	Text(String),
132	Blob(Vec<u8>),
133}
134
135