Skip to main content

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;