Skip to main content

sofie_db/
seam.rs

1use std::fmt;
2use std::num::NonZeroUsize;
3use std::pin::Pin;
4use std::sync::Arc;
5use std::time::Duration;
6
7use sofie_core::{ColumnReader, Error, Result, SqlType, SqlValue};
8
9use crate::{GuardCheck, MessageSql, Migrations};
10
11pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
12
13/// Long enough for a burst to pass, well short of a request's own timeout.
14const DEFAULT_POOL_WAIT: Duration = Duration::from_secs(5);
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub struct PoolSettings {
18    /// Twice the logical CPUs when `None`.
19    pub connections: Option<NonZeroUsize>,
20    pub wait: Duration,
21}
22
23impl Default for PoolSettings {
24    fn default() -> Self {
25        Self {
26            connections: None,
27            wait: DEFAULT_POOL_WAIT,
28        }
29    }
30}
31
32#[derive(Debug)]
33pub enum Step {
34    /// A mismatched `expect_rows` rolls back with its error; a check costs a round trip.
35    Statement {
36        sql: String,
37        params: Vec<SqlValue>,
38        expect_rows: Option<ExpectedRows>,
39    },
40    /// Trusted SQL only (migrations, framework setup), never input.
41    Script(String),
42}
43
44impl Step {
45    /// A mismatched `expect_rows` is a `Conflict`.
46    #[must_use]
47    pub fn statement(
48        sql: impl Into<String>,
49        params: Vec<SqlValue>,
50        expect_rows: Option<u64>,
51    ) -> Self {
52        Self::Statement {
53            sql: sql.into(),
54            params,
55            expect_rows: expect_rows.map(ExpectedRows::or_conflict),
56        }
57    }
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct ExpectedRows {
62    pub rows: u64,
63    pub otherwise: Unmet,
64}
65
66/// What a statement that touched another number of rows than expected fails with.
67#[derive(Clone, Debug, PartialEq, Eq)]
68pub enum Unmet {
69    Conflict,
70    NotFound,
71    Refused(Arc<GuardCheck>),
72}
73
74impl ExpectedRows {
75    #[must_use]
76    pub const fn or_conflict(rows: u64) -> Self {
77        Self {
78            rows,
79            otherwise: Unmet::Conflict,
80        }
81    }
82
83    #[must_use]
84    pub const fn or_not_found(rows: u64) -> Self {
85        Self {
86            rows,
87            otherwise: Unmet::NotFound,
88        }
89    }
90
91    pub fn check(&self, affected: u64) -> Result<()> {
92        let met = affected == self.rows;
93        match &self.otherwise {
94            Unmet::Refused(check) => check.checked(!met),
95            _ if met => Ok(()),
96            Unmet::Conflict => Err(Error::Conflict),
97            Unmet::NotFound => Err(Error::NotFound),
98        }
99    }
100}
101
102pub trait RowSink: Send {
103    /// Checked once before any row is read; `None` takes any type, fewer columns are refused.
104    fn expects(&self) -> &[Option<SqlType>];
105
106    fn row(&mut self, row: &mut dyn ColumnReader) -> Result<()>;
107}
108
109pub trait Driver: Send + Sync + 'static {
110    fn dialect(&self) -> &dyn Dialect;
111
112    fn query<'a>(
113        &'a self,
114        sql: &'a str,
115        params: &'a [SqlValue],
116        rows: &'a mut dyn RowSink,
117    ) -> BoxFuture<'a, Result<()>>;
118
119    /// Read-only, as nothing else keeps raw SQL from writing.
120    #[cfg(feature = "raw-sql")]
121    fn query_read_only<'a>(
122        &'a self,
123        role: Option<&'a str>,
124        sql: &'a str,
125        params: &'a [SqlValue],
126        rows: &'a mut dyn RowSink,
127    ) -> BoxFuture<'a, Result<()>> {
128        let _ = (role, params, rows);
129        crate::raw::refuse_read_only(sql)
130    }
131
132    /// A unique-index refusal fails as `Internal` with a [`UniqueViolation`] source; a check,
133    /// exclusion or foreign key as `Rule` ([`check_refusal`], [`exclusion_refusal`], …).
134    fn transaction(&self, steps: Vec<Step>) -> BoxFuture<'_, Result<()>>;
135
136    /// Held over a whole `Schema::migrate` until dropped; else each migration's transaction locks.
137    fn lock_migrations(&self) -> Option<BoxFuture<'_, Result<Box<dyn MigrationsLock>>>> {
138        None
139    }
140
141    /// Outside a transaction, under `lock_migrations`; other migrations get `record` back.
142    #[cfg(feature = "concurrent-indexes")]
143    fn migrate_alone<'a>(
144        &'a self,
145        schema: &'a str,
146        sql: &'a str,
147        record: Step,
148    ) -> std::result::Result<BoxFuture<'a, Result<()>>, Step> {
149        let _ = (schema, sql);
150        Err(record)
151    }
152
153    fn catalog(&self) -> Option<&dyn crate::Catalog> {
154        None
155    }
156
157    fn wakeups(&self) -> Option<BoxFuture<'_, Result<Box<dyn Wakeups>>>> {
158        None
159    }
160
161    #[cfg(feature = "shield")]
162    fn shield_wakeups(&self) -> Option<BoxFuture<'_, Result<Box<dyn Wakeups>>>> {
163        None
164    }
165
166    /// The payloads sent to the `sofie_cache` channel, such as by `pg_notify`.
167    #[cfg(feature = "cache")]
168    fn cache_notifications(&self) -> Option<BoxFuture<'_, Result<Box<dyn Notifications>>>> {
169        None
170    }
171
172    /// The payloads `Dialect::live_notify` sent.
173    #[cfg(feature = "live")]
174    fn change_notifications(&self) -> Option<BoxFuture<'_, Result<Box<dyn Notifications>>>> {
175        None
176    }
177}
178
179#[derive(Debug)]
180pub struct UniqueViolation {
181    pub index: String,
182}
183
184impl fmt::Display for UniqueViolation {
185    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(formatter, "unique index {} refused the write", self.index)
187    }
188}
189
190impl std::error::Error for UniqueViolation {}
191
192#[must_use]
193pub fn check_refusal(constraint: &str) -> String {
194    format!("a value breaks the rule `{constraint}`")
195}
196
197/// SQLite names no foreign key it refuses, so neither engine's refusal does.
198pub const REFERENCE_REFUSAL: &str =
199    "the write names a record that does not exist, or removes one another record still names";
200
201#[must_use]
202pub fn exclusion_refusal(constraint: &str) -> String {
203    format!("a value overlaps one another record holds, under the rule `{constraint}`")
204}
205
206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
207pub enum Engine {
208    Postgres,
209    Sqlite,
210}
211
212/// Rendered SQL is cached by the dialect's type, so it must not vary per instance.
213pub trait Dialect: Send + Sync + std::any::Any {
214    /// `index` counts from 1.
215    fn push_placeholder(&self, sql: &mut String, index: usize);
216
217    fn push_quoted(&self, sql: &mut String, identifier: &str) {
218        sql.push('"');
219        for character in identifier.chars() {
220            if character == '"' {
221                sql.push('"');
222            }
223            sql.push(character);
224        }
225        sql.push('"');
226    }
227
228    fn placeholder(&self, index: usize) -> String {
229        let mut sql = String::new();
230        self.push_placeholder(&mut sql, index);
231        sql
232    }
233
234    fn quote(&self, identifier: &str) -> String {
235        let mut sql = String::with_capacity(identifier.len() + 2);
236        self.push_quoted(&mut sql, identifier);
237        sql
238    }
239
240    /// Which of a module's migration files apply: SQLite's where it has its own.
241    fn engine(&self) -> Engine {
242        Engine::Postgres
243    }
244
245    /// Locks its one text parameter until the transaction ends; `None` where commits queue anyway.
246    fn key_lock(&self) -> Option<&'static str> {
247        None
248    }
249
250    fn shared_key_lock(&self) -> Option<&'static str> {
251        None
252    }
253
254    /// A module's table: in its schema, or named after it where the database has no schemas.
255    fn qualified(&self, schema: &str, name: &str) -> String {
256        format!("{}.{}", self.quote(schema), self.quote(name))
257    }
258
259    /// A bytes column as the JSON of loaded child rows holds it: `\x` and hex.
260    fn bytes_in_json(&self, column: &str) -> String {
261        column.to_owned()
262    }
263
264    fn nested_rows(&self, columns: &str, from: &str) -> String {
265        panic!("this database cannot load child rows, so `{columns}` of `{from}` cannot be read")
266    }
267
268    fn binds_arrays(&self) -> bool {
269        false
270    }
271
272    fn migrations_setup(&self) -> &'static str;
273
274    fn migrations_lock(&self) -> &'static str;
275
276    fn enter_schema(&self, schema: &str) -> String;
277
278    /// Answers, one per row, the statements `Schema::fresh` drops every table with.
279    fn drop_every_table(&self) -> Option<&'static str> {
280        None
281    }
282
283    /// Without full-text search, the words are found as one piece of text, ignoring case.
284    #[cfg(feature = "search")]
285    fn text_search(&self, column: &str, words: &str) -> String {
286        let (column, words) = (self.lowercase(column), self.lowercase(words));
287        let escaped =
288            format!(r"replace(replace(replace({words}, '\', '\\'), '%', '\%'), '_', '\_')");
289        format!(r"({column} LIKE '%' || {escaped} || '%' ESCAPE '\')")
290    }
291
292    fn lowercase(&self, expression: &str) -> String {
293        format!("lower({expression})")
294    }
295
296    /// After a nullable column's direction: empty values sort as Postgres's do, last ascending.
297    fn empty_order(&self, descending: bool) -> &'static str {
298        let _ = descending;
299        ""
300    }
301
302    /// `None` returns to the app's login.
303    #[cfg(feature = "module-roles")]
304    fn enter_role(&self, module: Option<&str>) -> Option<String> {
305        let _ = module;
306        None
307    }
308
309    fn framework_migrations(&self) -> Migrations;
310
311    fn messages(&self) -> &'static MessageSql;
312
313    #[cfg(feature = "shield")]
314    fn shield(&self) -> Option<&'static crate::ShieldSql> {
315        None
316    }
317
318    #[cfg(feature = "remote-contracts")]
319    fn feed(&self) -> Option<&'static crate::FeedSql> {
320        None
321    }
322
323    #[cfg(feature = "product-reports")]
324    fn product_events(&self) -> Option<&'static crate::ProductSql> {
325        None
326    }
327
328    /// Sends its one text parameter to every instance's `Driver::change_notifications`.
329    #[cfg(feature = "live")]
330    fn live_notify(&self) -> Option<&'static str> {
331        None
332    }
333}
334
335pub trait MigrationsLock: Send {}
336
337pub trait Wakeups: Send {
338    fn next(&mut self) -> BoxFuture<'_, Result<()>>;
339}
340
341#[cfg(any(feature = "cache", feature = "live"))]
342pub trait Notifications: Send {
343    /// Every payload buffered so far, waiting for at least one.
344    fn next(&mut self) -> BoxFuture<'_, Result<Vec<String>>>;
345}