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}