Skip to main content

ruststream_sqlx_dialect/
sqlite.rs

1//! The built-in SQLite dialect.
2
3use std::num::NonZeroUsize;
4
5use crate::advisory::Advisory;
6use crate::dialect::Dialect;
7use crate::form::KeyPart;
8use crate::insert::{Placeholder, Spelling, quote_into};
9use crate::lease::Lease;
10use crate::opening::{Mode, Opening, Opens, level};
11use crate::outbox::OutboxDialect;
12use crate::spec::TableSpec;
13use crate::statement::{ClaimShape, Param, Statement, StatementError};
14use crate::table_name::TableName;
15use crate::writer::{BuiltIn, OutboxWriter, Probe, SqlWriter};
16
17/// How SQLite reads the current time: as text, in the layout sqlx writes `chrono` times in, so it
18/// compares with the times a service binds.
19const DATABASE_NOW: &str = "strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now')";
20
21/// How a claim of the service's own opens its transaction: with the database's write lock taken,
22/// so no other writer comes between its select and its stamps.
23const BEGIN_CLAIM: &str = "BEGIN IMMEDIATE";
24
25/// How SQLite spells an insert, for the `const fn` that writes it and for [`Sqlite`]'s own
26/// statements.
27pub(crate) const SPELLING: Spelling = Spelling {
28    dialect: "sqlite",
29    quote: b'`',
30    placeholder: Placeholder::Question,
31    default_row: " DEFAULT VALUES",
32    // SQLite keeps a name of any length.
33    name_limit: None,
34};
35
36/// SQLite: backtick-quoted names, `?` placeholders, rows claimed by lease.
37///
38/// It builds the statements of the lease form ([`Lease`]), of the advisory lock form
39/// ([`Advisory`]), and the insert. A writer locks the whole database, not rows, so it builds no
40/// claim that holds rows for a handler: it does not implement [`RowLock`](crate::RowLock), and a
41/// SQLite table declares `locked_until` or `advisory_lock`. A lease claim is one
42/// update that writes the lease, counts the attempt and returns the rows it took, as they were
43/// before; one writer at a time keeps two claims apart. The rows of one claim come back in no
44/// particular order. A dead letter into a table copies the row, then deletes it, in one
45/// transaction. A claim of the service's own opens its transaction with `BEGIN IMMEDIATE`
46/// ([`begin_lease_claim`](Lease::begin_lease_claim)), so it takes the write lock before it
47/// selects. Every name is quoted, so a name keeps its case and may hold any character, and SQLite
48/// keeps a name of any length. The quotes are backticks, which SQLite always reads as a name: a
49/// statement that names a column the table lacks fails when it is prepared.
50/// [`fetch`](Dialect::fetch) is refused, so a claim of the service's own brings a fetch of its
51/// own.
52///
53/// SQLite keeps times as text, so the statements compare times as text: two times compare right
54/// when their text sorts as the times do.
55///
56/// SQLite has no locks a session holds, so in the advisory lock form the broker keeps the keys in
57/// work in the process: [`lock`](Advisory::lock) and [`unlock`](Advisory::unlock) are `None`, and
58/// the claim selects up to [`Param::Limit`] claimable rows in claim order with their keys, the text
59/// the key's parts render, a column without a value read as empty text. The select cannot leave
60/// out the keys in work, so the broker binds a limit that reaches past them. The take counts the
61/// attempt and returns the row in one statement.
62///
63/// A table with FIFO groups needs no guard ([`fifo_guard`](Dialect::fifo_guard) is `None`): one
64/// writer at a time keeps two claims apart, and a lease claim takes nothing while a row of the
65/// group holds a lease.
66///
67/// SQLite runs every transaction serializable, so a table names no isolation level here: it names
68/// a mode, and its transactions open with `BEGIN DEFERRED`, `BEGIN IMMEDIATE` or
69/// `BEGIN EXCLUSIVE` ([`begin`](Dialect::begin)), or with `BEGIN` where it names none.
70///
71/// # Examples
72///
73/// A subscription asks SQLite for the statements of its table, as `ruststream-sqlx` does when the
74/// subscription starts:
75///
76/// ```
77/// use ruststream_sqlx_dialect::{
78///     Advisory, Column, Form, KeyPart, Sqlite, Statement, StatementError, TableSpec,
79/// };
80///
81/// // The jobs table as `#[derive(Inbox)]` describes it, with `advisory_lock = "jobs-{job_id}"`.
82/// const JOBS: TableSpec<'static> = TableSpec::new(
83///     "jobs",
84///     Column::new("job_id"),
85///     Form::Advisory(&[KeyPart::Literal("jobs-"), KeyPart::Column("job_id")]),
86/// );
87///
88/// // What a subscription to an advisory lock table prepares: the claim of candidates, and the
89/// // lock on a key where the database keeps locks; without one the broker keeps the keys in work
90/// // in the process.
91/// fn advisory_statements(
92///     dialect: &impl Advisory,
93///     spec: &TableSpec<'_>,
94/// ) -> Result<(Statement, Option<Statement>), StatementError> {
95///     Ok((dialect.advisory_claim(spec)?, dialect.lock()))
96/// }
97///
98/// fn main() -> Result<(), StatementError> {
99///     // SQLite keeps no locks a session holds, so the process keeps the keys in work.
100///     let (_claim, lock) = advisory_statements(&Sqlite, &JOBS)?;
101///     assert!(lock.is_none());
102///     Ok(())
103/// }
104/// ```
105///
106/// A table in the row lock form finds no claim here, so a subscription to one does not compile:
107///
108/// ```compile_fail,E0277
109/// use ruststream_sqlx_dialect::{
110///     ClaimShape, Column, Form, RowLock, Sqlite, Statement, StatementError, TableSpec,
111/// };
112///
113/// const JOBS: TableSpec<'static> = TableSpec::new("jobs", Column::new("job_id"), Form::RowLock);
114///
115/// // What a subscription to a row lock table asks of its dialect.
116/// fn claims_its_rows(dialect: &impl RowLock) -> Result<Statement, StatementError> {
117///     dialect.lock_claim(&JOBS, ClaimShape::Rows)
118/// }
119///
120/// let claim = claims_its_rows(&Sqlite);
121/// ```
122#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
123pub struct Sqlite;
124
125impl BuiltIn for Sqlite {
126    const SPELLING: Spelling = SPELLING;
127
128    /// A writer locks the whole database, so no claim can hold rows for a handler.
129    const ROW_LOCKS: bool = false;
130
131    const BACKSLASH_ESCAPES: bool = false;
132
133    const UPDATE_RETURNS: bool = true;
134
135    /// The process keeps the locks, so the database cannot tell a key in work.
136    const PROBE: Probe = Probe::Blind;
137
138    fn database_now(&self) -> &'static str {
139        DATABASE_NOW
140    }
141
142    fn database_later(&self, sql: &mut SqlWriter<'_, Self>) {
143        sql.push("strftime('%Y-%m-%dT%H:%M:%f+00:00', 'now', (")
144            .param(Param::Delay)
145            .push(" / 1000000.0) || ' seconds')");
146    }
147
148    // SQLite takes no locks: the broker keeps the keys in work in the process, so the key alone
149    // is rendered.
150    fn render_lock_key(&self, sql: &mut SqlWriter<'_, Self>, _: Option<&str>, key: &[KeyPart<'_>]) {
151        // `||` with a column without a value gives no text at all, so each column reads as empty
152        // text there; the cast makes a key of one number text.
153        sql.push("CAST(")
154            .key_parts(key, " || ", |sql, column| {
155                sql.push("ifnull(").ident(column).push(", '')");
156            })
157            .push(" AS TEXT)");
158    }
159}
160
161impl Dialect for Sqlite {
162    fn name(&self) -> &'static str {
163        "sqlite"
164    }
165
166    fn quote_into(&self, ident: &str, out: &mut String) {
167        // SQLite reads a double-quoted name that matches no column as a string literal, so a
168        // misnamed column would prepare and read as text; a backtick-quoted name is always a name.
169        quote_into(SPELLING.quote, ident, out);
170    }
171
172    fn placeholder_into(&self, _: NonZeroUsize, out: &mut String) {
173        out.push('?');
174    }
175
176    fn fetch(&self, _: &TableSpec<'_>) -> Result<Statement, StatementError> {
177        // A statement is prepared once with fixed text, and SQLite binds no list as one parameter.
178        Err(StatementError::UnsupportedFetch {
179            dialect: self.name(),
180        })
181    }
182
183    fn ack(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
184        self.finish_statement(spec)
185    }
186
187    fn retry(&self, spec: &TableSpec<'_>) -> Result<Option<Statement>, StatementError> {
188        self.retry_statement(spec)
189    }
190
191    fn retry_after(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
192        self.retry_after_statement(spec)
193    }
194
195    fn discard(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
196        self.finish_statement(spec)
197    }
198
199    fn dead_letter_group(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
200        self.dead_letter_group_statement(spec)
201    }
202
203    fn dead_letter_table(
204        &self,
205        spec: &TableSpec<'_>,
206        target: TableName<'_>,
207    ) -> Result<Vec<Statement>, StatementError> {
208        // SQLite feeds no delete's rows into an insert.
209        self.copy_then_delete(spec, target)
210    }
211
212    fn insert(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
213        self.insert_statement(spec)
214    }
215
216    fn begin(&self, opening: Opening) -> Result<Option<&'static str>, StatementError> {
217        match opening {
218            Opening::Default => Ok(None),
219            Opening::Mode(Mode::Deferred) => Ok(Some("BEGIN DEFERRED")),
220            Opening::Mode(Mode::Immediate) => Ok(Some("BEGIN IMMEDIATE")),
221            Opening::Mode(Mode::Exclusive) => Ok(Some("BEGIN EXCLUSIVE")),
222            Opening::Isolation(_) => Err(opening.refused(self.name())),
223        }
224    }
225}
226
227impl OutboxDialect for Sqlite {
228    fn outbox_fetch(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
229        self.outbox_fetch_statement(spec)
230    }
231
232    fn outbox_mark(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
233        self.outbox_mark_statement(spec)
234    }
235
236    fn outbox_recover(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
237        self.outbox_recover_statement(spec)
238    }
239}
240
241impl Opens<level::Deferred> for Sqlite {}
242
243impl Opens<level::Immediate> for Sqlite {}
244
245impl Opens<level::Exclusive> for Sqlite {}
246
247impl Advisory for Sqlite {
248    fn advisory_claim(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
249        self.advisory_claim_statement(spec)
250    }
251
252    fn lock(&self) -> Option<Statement> {
253        None
254    }
255
256    fn unlock(&self) -> Option<Statement> {
257        None
258    }
259
260    fn take(
261        &self,
262        spec: &TableSpec<'_>,
263        shape: ClaimShape,
264    ) -> Result<Vec<Statement>, StatementError> {
265        self.take_statements(spec, shape)
266    }
267}
268
269impl Lease for Sqlite {
270    fn lease_claim(
271        &self,
272        spec: &TableSpec<'_>,
273        shape: ClaimShape,
274    ) -> Result<Statement, StatementError> {
275        let expiry = self.leased(spec, "lease_claim")?;
276        let mut sql = SqlWriter::new(self);
277        sql.returning_claim(spec, shape, expiry.name());
278        Ok(sql.finish())
279    }
280
281    fn extend(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
282        self.extend_statement(spec)
283    }
284
285    fn stamp(&self, spec: &TableSpec<'_>) -> Result<Statement, StatementError> {
286        self.stamp_statement(spec)
287    }
288
289    fn claim_counts_attempt(&self, spec: &TableSpec<'_>) -> bool {
290        // `*` names no column, so the returned rows carry the attempt the claim wrote.
291        spec.selects_all()
292    }
293
294    fn begin_lease_claim(&self) -> Option<&'static str> {
295        Some(BEGIN_CLAIM)
296    }
297}