coven_database/sql_functions.rs
1//! The functions an app gives SQLite, registered on every connection to the
2//! store's database (§4, api.md E1): collations, which order text its SQL
3//! names with `COLLATE`, and scalar functions, which compute a value its SQL
4//! calls by name.
5
6use std::cmp::Ordering;
7use std::collections::BTreeSet;
8use std::panic::{catch_unwind, AssertUnwindSafe};
9use std::sync::Arc;
10
11use rusqlite::functions::{Context, FunctionFlags};
12use rusqlite::types::{FromSql, Value, ValueRef};
13
14use crate::DbError;
15
16/// SQLite's own collations. Coven's tables use only these, so an app name is
17/// refused only when it is one of them.
18const SQLITE_COLLATIONS: [&str; 3] = ["BINARY", "NOCASE", "RTRIM"];
19
20type Compare = dyn Fn(&str, &str) -> Ordering + Send + Sync;
21type Compute = dyn Fn(&FunctionArguments<'_>) -> rusqlite::Result<Value> + Send + Sync;
22
23/// The arguments SQLite passes one call of an app function. Only the
24/// arguments: the connection running the statement stays coven's.
25pub struct FunctionArguments<'a> {
26 context: &'a Context<'a>,
27}
28
29impl FunctionArguments<'_> {
30 /// Argument `index`, counted from zero, converted as a row's column is.
31 /// An index past the function's arguments panics.
32 pub fn get<T: FromSql>(&self, index: usize) -> rusqlite::Result<T> {
33 self.context.get(index)
34 }
35
36 /// Argument `index`, counted from zero, as SQLite holds it. An index past
37 /// the function's arguments panics.
38 pub fn get_raw(&self, index: usize) -> ValueRef<'_> {
39 self.context.get_raw(index)
40 }
41}
42
43/// Whether a scalar function's result depends on its arguments alone.
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub enum Determinism {
46 /// The same arguments always give the same result, on every device and
47 /// in every app version that registers this `version`. SQLite allows the
48 /// function where a stored value depends on it: an index on an
49 /// expression, a partial index's condition, a CHECK and a generated
50 /// column. A writable open rebuilds the indexes that call it when
51 /// `version` differs from the one they were built with (§4).
52 Deterministic {
53 /// The version of the function's results; a new one for every
54 /// change to them.
55 version: u32,
56 },
57 /// The result can change between calls with the same arguments, as one
58 /// that reads the clock does. SQLite refuses it wherever a stored value
59 /// would depend on it.
60 Nondeterministic,
61}
62
63#[derive(Clone)]
64struct Scalar {
65 name: String,
66 arguments: u8,
67 determinism: Determinism,
68 compute: Arc<Compute>,
69}
70
71#[derive(Clone)]
72struct Collation {
73 name: String,
74 version: u32,
75 compare: Arc<Compare>,
76}
77
78/// The app's collations and scalar functions. Coven registers every one on
79/// each connection it opens to the store's database, before any app SQL
80/// runs there: the writer, which also migrates, applies other devices'
81/// writes and merges, the readers, and every connection of a read-only
82/// handle.
83///
84/// A collation, or a deterministic function, is part of the store's schema
85/// once the schema uses it. At one version it must give the same answer for
86/// the same arguments on every device and in every app version that
87/// registers that version, and a collation must be a total order:
88/// consistent, antisymmetric and transitive, though distinct strings may
89/// compare equal. An index keeps its entries as the function gave them when
90/// they were written, so a changed answer takes a new version, and a
91/// writable open rebuilds the indexes that use it. A CHECK or a synced
92/// table's unique index decides by it which rows every device keeps (§9.2,
93/// §9.3), and a generated column keeps what it computed, so a version can't
94/// change while one of those uses it (§4).
95#[derive(Clone, Default)]
96pub struct SqlFunctions {
97 collations: Vec<Collation>,
98 scalars: Vec<Scalar>,
99}
100
101impl SqlFunctions {
102 /// Adds the collation `name` at `version`, ordering text by `compare`.
103 /// Every change to the order takes a new version. SQLite compares
104 /// collation names without regard to ASCII case. A name that is SQLite's
105 /// own, `BINARY`, `NOCASE` or `RTRIM`, fails the open with
106 /// `DbError::ReservedCollation`, and one added twice with
107 /// `DbError::DuplicateCollation`. A panic in `compare` ends the process:
108 /// SQLite has no way for a comparison to fail, and any answer coven made
109 /// up could put an index out of order.
110 pub fn collation<F>(mut self, name: impl Into<String>, version: u32, compare: F) -> Self
111 where
112 F: Fn(&str, &str) -> Ordering + Send + Sync + 'static,
113 {
114 self.collations.push(Collation { name: name.into(), version, compare: Arc::new(compare) });
115 self
116 }
117
118 /// Adds the scalar function `name`, taking exactly `arguments`
119 /// arguments, which SQL calls as `name(...)`. `compute` reads them and
120 /// returns the result, or an error that fails the statement calling it.
121 /// SQLite compares function names without regard to ASCII case. A name
122 /// SQLite or coven already has, or one with coven's reserved prefix
123 /// `coven_` or `_coven_`, fails the open with `DbError::ReservedFunction`,
124 /// and one added twice with `DbError::DuplicateFunction`. A
125 /// deterministic function must take an argument: one that takes none is
126 /// a constant, which SQL can write as a literal, and fails the open with
127 /// `DbError::ConstantFunction`. A deterministic function's version
128 /// changes with every change to its results. Every app function is
129 /// marked safe for triggers and views, which only the app's own
130 /// migrations declare. A panic in `compute` ends the process.
131 pub fn scalar<F, T>(
132 mut self,
133 name: impl Into<String>,
134 arguments: u8,
135 determinism: Determinism,
136 compute: F,
137 ) -> Self
138 where
139 F: Fn(&FunctionArguments<'_>) -> rusqlite::Result<T> + Send + Sync + 'static,
140 T: Into<Value>,
141 {
142 self.scalars.push(Scalar {
143 name: name.into(),
144 arguments,
145 determinism,
146 compute: Arc::new(move |arguments| compute(arguments).map(Into::into)),
147 });
148 self
149 }
150
151 /// The version of each collation and deterministic function, which the
152 /// store's indexes are built with (§4).
153 pub(crate) fn versions(&self) -> Vec<crate::sql_versions::SqlVersion> {
154 use crate::sql_versions::{SqlVersion, Versioned};
155 let collations = self.collations.iter().map(|collation| SqlVersion {
156 kind: Versioned::Collation,
157 name: collation.name.to_ascii_lowercase(),
158 version: collation.version,
159 });
160 let functions = self.scalars.iter().filter_map(|scalar| match scalar.determinism {
161 Determinism::Deterministic { version } => Some(SqlVersion {
162 kind: Versioned::Function,
163 name: scalar.name.to_ascii_lowercase(),
164 version,
165 }),
166 Determinism::Nondeterministic => None,
167 });
168 collations.chain(functions).collect()
169 }
170
171 /// Register every collation and scalar function on a connection that
172 /// has run no app statement, after coven's own functions.
173 pub(crate) fn register(&self, connection: &rusqlite::Connection) -> Result<(), DbError> {
174 for (position, collation) in self.collations.iter().enumerate() {
175 let name = &collation.name;
176 if SQLITE_COLLATIONS.iter().any(|own| own.eq_ignore_ascii_case(name)) {
177 return Err(DbError::ReservedCollation { name: name.clone() });
178 }
179 if self.collations[..position]
180 .iter()
181 .any(|earlier| earlier.name.eq_ignore_ascii_case(name))
182 {
183 return Err(DbError::DuplicateCollation { name: name.clone() });
184 }
185 let compare = collation.compare.clone();
186 let collation = name.clone();
187 connection.create_collation(name.as_str(), move |left, right| {
188 unless_panicked(
189 &collation,
190 "the app's collation panicked; no order is safe to return",
191 || compare(left, right),
192 )
193 })?;
194 }
195 if self.scalars.is_empty() {
196 return Ok(());
197 }
198 // SQLite's built-in functions and coven's, read before any of the
199 // app's is registered.
200 let existing = connection
201 .prepare("SELECT DISTINCT lower(name) FROM pragma_function_list")?
202 .query_map([], |row| row.get::<_, String>(0))?
203 .collect::<rusqlite::Result<BTreeSet<String>>>()?;
204 for (position, scalar) in self.scalars.iter().enumerate() {
205 let name = &scalar.name;
206 if existing.contains(&name.to_ascii_lowercase())
207 || crate::internal_schema::reserved_name(name)
208 {
209 return Err(DbError::ReservedFunction { name: name.clone() });
210 }
211 if self.scalars[..position]
212 .iter()
213 .any(|earlier| earlier.name.eq_ignore_ascii_case(name))
214 {
215 return Err(DbError::DuplicateFunction { name: name.clone() });
216 }
217 let flags = FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_INNOCUOUS;
218 let flags = match scalar.determinism {
219 // SQLite calls a deterministic function of no arguments while
220 // it plans a statement (valueFromFunction, for an analyzed
221 // index), with a null argument array. rusqlite's
222 // call_boxed_closure, in 0.40.2 and still in 0.41.0, builds
223 // `slice::from_raw_parts(argv, argc)` from it, which is
224 // undefined behavior and aborts the process in a debug
225 // build. Refuse such a function until rusqlite takes an
226 // empty slice for no arguments.
227 Determinism::Deterministic { .. } if scalar.arguments == 0 => {
228 return Err(DbError::ConstantFunction { name: name.clone() });
229 }
230 Determinism::Deterministic { .. } => flags | FunctionFlags::SQLITE_DETERMINISTIC,
231 Determinism::Nondeterministic => flags,
232 };
233 let compute = scalar.compute.clone();
234 let function = name.clone();
235 connection.create_scalar_function(
236 name.as_str(),
237 i32::from(scalar.arguments),
238 flags,
239 move |context| {
240 unless_panicked(&function, "the app's SQL function panicked", || {
241 compute(&FunctionArguments { context })
242 })
243 },
244 )?;
245 }
246 Ok(())
247 }
248}
249
250/// Run one of the app's functions for SQLite, ending the process if it
251/// panics. A panic is a bug in the app's function, not an answer: no order a
252/// collation could return is safe, and after a scalar function panics its
253/// answers can't be trusted to match the ones its indexes and CHECKs hold.
254fn unless_panicked<T>(name: &str, message: &'static str, call: impl FnOnce() -> T) -> T {
255 match catch_unwind(AssertUnwindSafe(call)) {
256 Ok(result) => result,
257 Err(_panic) => {
258 tracing::error!(function = %name, "{message}");
259 std::process::abort()
260 }
261 }
262}
263
264#[cfg(test)]
265#[path = "sql_functions_tests.rs"]
266mod tests;