Skip to main content

ruststream_sqlx_dialect/
insert.rs

1//! The insert of a row into a queue table, for each built-in dialect, written by a `const fn`.
2//!
3//! [`postgres`], [`mysql`] and [`sqlite`] turn a [`TableSpec`] into the text of its insert while
4//! the service compiles: every column the database does not fill, in [`TableSpec::columns`]
5//! order, each bound by the next placeholder. The built-in dialects' [`Dialect::insert`], and
6//! with it the insert `#[derive(Inbox)]` generates, take their text from the same writer, so a
7//! table described by hand inserts with the same statement as a derived one.
8//!
9//! The text lands in a [`Sql<N>`]: `N` bytes the caller chooses, a constant like the description
10//! it renders. A capacity too small for the text, a description read with `*` and a name longer
11//! than the database keeps are refused with a panic, which in a `const` is a build error naming
12//! the table.
13//!
14//! # Examples
15//!
16//! ```
17//! # #[cfg(feature = "postgres")]
18//! # fn main() {
19//! use ruststream_sqlx_dialect::insert::{self, Sql};
20//! use ruststream_sqlx_dialect::{Column, Form, TableSpec};
21//!
22//! const EMAILS: TableSpec<'static> = TableSpec::new(
23//!     "email_jobs",
24//!     Column::new("job_id").generated(),
25//!     Form::Lease(Column::new("locked_until")),
26//! )
27//! .group(Column::new("name"))
28//! .payload(Column::new("payload"))
29//! .data(&[Column::new("recipient")]);
30//!
31//! // The binds follow `TableSpec::columns`: the roles, then the data columns.
32//! const INSERT: Sql<128> = insert::postgres(&EMAILS);
33//!
34//! assert_eq!(
35//!     INSERT.as_str(),
36//!     r#"INSERT INTO "email_jobs" ("name", "locked_until", "payload", "recipient") VALUES ($1, $2, $3, $4)"#,
37//! );
38//! # }
39//! # #[cfg(not(feature = "postgres"))]
40//! # fn main() {}
41//! ```
42//!
43//! [`Dialect::insert`]: crate::Dialect::insert
44
45use core::fmt::{self, Debug, Display, Formatter};
46
47use crate::column::Column;
48use crate::spec::{Columns, TableSpec};
49use crate::statement::{NameLimit, Param, Statement, StatementError};
50
51/// How a built-in database spells an insert.
52#[derive(Debug, Clone, Copy)]
53pub(crate) struct Spelling {
54    /// The dialect's name, as [`Dialect::name`](crate::Dialect::name) gives it.
55    pub(crate) dialect: &'static str,
56    /// The character that quotes a name; one inside the name is doubled.
57    pub(crate) quote: u8,
58    /// How a placeholder reads.
59    pub(crate) placeholder: Placeholder,
60    /// What follows the table in an insert that writes no column, so every column takes its
61    /// default.
62    pub(crate) default_row: &'static str,
63    /// The longest name the database keeps; `None` where it keeps a name of any length.
64    pub(crate) name_limit: Option<NameLimit>,
65}
66
67/// How a placeholder reads.
68#[derive(Debug, Clone, Copy)]
69pub(crate) enum Placeholder {
70    /// `?`, bound by position.
71    #[cfg(any(feature = "mysql", feature = "sqlite"))]
72    Question,
73    /// `$1`, `$2`, ..: numbered from one.
74    #[cfg(feature = "postgres")]
75    Dollar,
76}
77
78/// Appends `ident` to `out`, quoted with `quote`, which doubles inside the name.
79pub(crate) fn quote_into(quote: u8, ident: &str, out: &mut String) {
80    let quote = char::from(quote);
81    out.push(quote);
82    for character in ident.chars() {
83        if character == quote {
84            out.push(quote);
85        }
86        out.push(character);
87    }
88    out.push(quote);
89}
90
91/// Whether `name` fits in `limit`, measured the way the database measures it.
92pub(crate) const fn fits(name: &str, limit: NameLimit) -> bool {
93    match limit {
94        NameLimit::Bytes(most) => name.len() <= most as usize,
95        NameLimit::Characters(most) => characters(name) <= most as usize,
96    }
97}
98
99/// The characters of `text`: its bytes that start one.
100const fn characters(text: &str) -> usize {
101    let bytes = text.as_bytes();
102    let mut count = 0;
103    let mut index = 0;
104    while index < bytes.len() {
105        // A UTF-8 continuation byte reads `0b10xx_xxxx`.
106        if bytes[index] & 0b1100_0000 != 0b1000_0000 {
107            count += 1;
108        }
109        index += 1;
110    }
111    count
112}
113
114/// The first name of `spec` longer than `limit` allows: its schema, its table, then its columns
115/// in [`TableSpec::columns`] order.
116pub(crate) const fn too_long<'a>(spec: &TableSpec<'a>, limit: NameLimit) -> Option<&'a str> {
117    if let Some(schema) = spec.schema()
118        && !fits(schema, limit)
119    {
120        return Some(schema);
121    }
122    if !fits(spec.table(), limit) {
123        return Some(spec.table());
124    }
125    let mut columns = Columns::new(spec);
126    while let Some(column) = columns.next_column() {
127        if !fits(column.name(), limit) {
128            return Some(column.name());
129        }
130    }
131    None
132}
133
134/// Why a description has no insert.
135#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136enum Refusal<'a> {
137    /// The description reads the table with `*`, so its columns are not all known.
138    Flattened,
139    /// A name is longer than the database keeps.
140    TooLong(&'a str, NameLimit),
141}
142
143impl Refusal<'_> {
144    /// The error [`Dialect::insert`](crate::Dialect::insert) returns for it.
145    fn into_error(self, dialect: &'static str) -> StatementError {
146        match self {
147            Self::Flattened => StatementError::Flattened {
148                statement: "insert",
149            },
150            Self::TooLong(identifier, limit) => StatementError::IdentifierTooLong {
151                dialect,
152                identifier: identifier.to_owned(),
153                limit,
154            },
155        }
156    }
157}
158
159/// Why `spec` has no insert in `spelling`, or `None` when it has one.
160const fn refusal<'a>(spec: &TableSpec<'a>, spelling: Spelling) -> Option<Refusal<'a>> {
161    if let Some(limit) = spelling.name_limit
162        && let Some(name) = too_long(spec, limit)
163    {
164        return Some(Refusal::TooLong(name, limit));
165    }
166    if spec.selects_all() {
167        return Some(Refusal::Flattened);
168    }
169    None
170}
171
172/// Text written into a buffer by a `const fn`. The length counts every byte written, those past
173/// the buffer's end included, so a pass over an empty buffer measures the text.
174struct Text<'b> {
175    out: &'b mut [u8],
176    len: usize,
177}
178
179impl<'b> Text<'b> {
180    const fn new(out: &'b mut [u8]) -> Self {
181        Self { out, len: 0 }
182    }
183
184    const fn byte(&mut self, byte: u8) {
185        if self.len < self.out.len() {
186            self.out[self.len] = byte;
187        }
188        self.len += 1;
189    }
190
191    const fn push(&mut self, text: &str) {
192        let bytes = text.as_bytes();
193        let mut index = 0;
194        while index < bytes.len() {
195            self.byte(bytes[index]);
196            index += 1;
197        }
198    }
199
200    /// `name`, quoted with `quote`, which doubles inside the name.
201    const fn ident(&mut self, quote: u8, name: &str) {
202        self.byte(quote);
203        let bytes = name.as_bytes();
204        let mut index = 0;
205        while index < bytes.len() {
206            if bytes[index] == quote {
207                self.byte(quote);
208            }
209            self.byte(bytes[index]);
210            index += 1;
211        }
212        self.byte(quote);
213    }
214
215    /// `number` in decimal.
216    const fn number(&mut self, number: usize) {
217        let mut unit = 1;
218        while number / unit >= 10 {
219            unit *= 10;
220        }
221        while unit > 0 {
222            // A decimal digit is below ten, so it fits a byte.
223            #[allow(clippy::cast_possible_truncation)]
224            self.byte(b'0' + (number / unit % 10) as u8);
225            unit /= 10;
226        }
227    }
228
229    /// The text written so far, cut at the last whole character that fits the buffer.
230    const fn as_str(&self) -> &str {
231        let written = if self.len < self.out.len() {
232            self.len
233        } else {
234            self.out.len()
235        };
236        let (text, _) = self.out.split_at(written);
237        prefix(text)
238    }
239}
240
241/// The longest prefix of `bytes` that is UTF-8.
242const fn prefix(bytes: &[u8]) -> &str {
243    match core::str::from_utf8(bytes) {
244        Ok(text) => text,
245        Err(error) => {
246            let (valid, _) = bytes.split_at(error.valid_up_to());
247            match core::str::from_utf8(valid) {
248                Ok(text) => text,
249                Err(_) => panic!("a prefix up to `valid_up_to` is UTF-8"),
250            }
251        }
252    }
253}
254
255/// Whether an insert writes `column`: the database fills a generated one.
256const fn written(column: Column<'_>) -> bool {
257    !column.is_generated()
258}
259
260/// Writes the insert of `spec` in `spelling`: every column the database does not fill.
261const fn write(text: &mut Text<'_>, spec: &TableSpec<'_>, spelling: Spelling) {
262    text.push("INSERT INTO ");
263    if let Some(schema) = spec.schema() {
264        text.ident(spelling.quote, schema);
265        text.push(".");
266    }
267    text.ident(spelling.quote, spec.table());
268    let mut columns = Columns::new(spec);
269    let mut count = 0;
270    while let Some(column) = columns.next_column() {
271        if written(column) {
272            text.push(if count == 0 { " (" } else { ", " });
273            text.ident(spelling.quote, column.name());
274            count += 1;
275        }
276    }
277    if count == 0 {
278        text.push(spelling.default_row);
279        return;
280    }
281    text.push(") VALUES (");
282    let mut index = 0;
283    while index < count {
284        if index > 0 {
285            text.push(", ");
286        }
287        match spelling.placeholder {
288            #[cfg(any(feature = "mysql", feature = "sqlite"))]
289            Placeholder::Question => text.push("?"),
290            #[cfg(feature = "postgres")]
291            Placeholder::Dollar => {
292                text.push("$");
293                text.number(index + 1);
294            }
295        }
296        index += 1;
297    }
298    text.push(")");
299}
300
301/// The insert of `spec` in `spelling` as a statement: its text, and the position in
302/// [`TableSpec::columns`] of each column its placeholders bind, in order.
303pub(crate) fn statement(
304    spec: &TableSpec<'_>,
305    spelling: Spelling,
306) -> Result<Statement, StatementError> {
307    if let Some(refusal) = refusal(spec, spelling) {
308        return Err(refusal.into_error(spelling.dialect));
309    }
310    let mut measure = Text::new(&mut []);
311    write(&mut measure, spec, spelling);
312    let mut bytes = vec![0; measure.len];
313    let mut text = Text::new(&mut bytes);
314    write(&mut text, spec, spelling);
315    let sql = text.as_str().to_owned();
316    let params = spec
317        .columns()
318        .enumerate()
319        .filter(|(_, column)| written(*column))
320        .map(|(position, _)| Param::Column(position));
321    Ok(Statement::new(sql, params))
322}
323
324/// The text of a statement rendered in a `const`: up to `N` bytes.
325///
326/// [`postgres`], [`mysql`] and [`sqlite`] fill one from a [`TableSpec`]. `N` is the caller's
327/// choice; a text longer than `N` is refused with a panic that names `N`, the table and the length
328/// the text needs, so in a `const` it is a build error that says what to raise `N` to.
329///
330/// # Examples
331///
332/// ```
333/// # #[cfg(feature = "sqlite")]
334/// # fn main() {
335/// use ruststream_sqlx_dialect::insert::{self, Sql};
336/// use ruststream_sqlx_dialect::{Column, Form, TableSpec};
337///
338/// const MAIL: TableSpec<'static> = TableSpec::new(
339///     "mail",
340///     Column::new("id").generated(),
341///     Form::Lease(Column::new("locked_until")),
342/// )
343/// .data(&[Column::new("recipient")]);
344///
345/// const INSERT: Sql<96> = insert::sqlite(&MAIL);
346///
347/// // A constant's text lives as long as the program, as a driver's prepared statement needs.
348/// let sql: &'static str = INSERT.as_str();
349/// assert_eq!(sql, "INSERT INTO `mail` (`locked_until`, `recipient`) VALUES (?, ?)");
350/// # }
351/// # #[cfg(not(feature = "sqlite"))]
352/// # fn main() {}
353/// ```
354///
355/// A capacity the text outgrows does not build:
356///
357/// ```compile_fail,E0080
358/// # #[cfg(feature = "sqlite")]
359/// # mod demo {
360/// use ruststream_sqlx_dialect::insert::{self, Sql};
361/// use ruststream_sqlx_dialect::{Column, Form, TableSpec};
362///
363/// const MAIL: TableSpec<'static> = TableSpec::new("mail", Column::new("id"), Form::RowLock)
364///     .data(&[Column::new("recipient")]);
365///
366/// // The insert into `mail` takes 52 bytes, more than `Sql<16>` holds.
367/// pub const INSERT: Sql<16> = insert::sqlite(&MAIL);
368/// # }
369/// # fn main() {}
370/// # #[cfg(not(feature = "sqlite"))]
371/// # compile_error!("the example needs the `sqlite` feature");
372/// ```
373#[derive(Clone, Copy, PartialEq, Eq, Hash)]
374pub struct Sql<const N: usize> {
375    bytes: [u8; N],
376    len: usize,
377}
378
379impl<const N: usize> Sql<N> {
380    /// The insert of `spec` in `spelling`, refused with a panic where it has none or outgrows `N`.
381    const fn insert(spec: &TableSpec<'_>, spelling: Spelling) -> Self {
382        if let Some(refusal) = refusal(spec, spelling) {
383            refuse(spec, spelling, refusal);
384        }
385        let mut bytes = [0; N];
386        let mut text = Text::new(&mut bytes);
387        write(&mut text, spec, spelling);
388        let len = text.len;
389        if len > N {
390            outgrown(spec, N, len);
391        }
392        Self { bytes, len }
393    }
394
395    /// The text.
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// # #[cfg(feature = "mysql")]
401    /// # fn main() {
402    /// use ruststream_sqlx_dialect::insert::{self, Sql};
403    /// use ruststream_sqlx_dialect::{Column, Form, TableSpec};
404    ///
405    /// const JOBS: TableSpec<'static> =
406    ///     TableSpec::new("jobs", Column::new("id"), Form::RowLock).payload(Column::new("payload"));
407    /// const INSERT: Sql<64> = insert::mysql(&JOBS);
408    ///
409    /// assert_eq!(INSERT.as_str(), "INSERT INTO `jobs` (`id`, `payload`) VALUES (?, ?)");
410    /// # }
411    /// # #[cfg(not(feature = "mysql"))]
412    /// # fn main() {}
413    /// ```
414    #[must_use]
415    pub const fn as_str(&self) -> &str {
416        // The text is whole names and ASCII, so the prefix is all of it.
417        let (text, _) = self.bytes.split_at(self.len);
418        prefix(text)
419    }
420}
421
422impl<const N: usize> Debug for Sql<N> {
423    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
424        f.debug_tuple("Sql").field(&self.as_str()).finish()
425    }
426}
427
428impl<const N: usize> Display for Sql<N> {
429    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
430        f.write_str(self.as_str())
431    }
432}
433
434impl<const N: usize> AsRef<str> for Sql<N> {
435    fn as_ref(&self) -> &str {
436        self.as_str()
437    }
438}
439
440/// The capacity of a refusal's message; a longer one is cut at a whole character.
441const MESSAGE: usize = 512;
442
443/// Panics with `refusal`'s message.
444const fn refuse(spec: &TableSpec<'_>, spelling: Spelling, refusal: Refusal<'_>) -> ! {
445    let mut bytes = [0; MESSAGE];
446    let mut text = Text::new(&mut bytes);
447    match refusal {
448        Refusal::Flattened => {
449            text.push("the insert into `");
450            text.push(spec.table());
451            text.push(
452                "` needs every column, and the description reads the table with `*` \
453                 (`selecting_all`): write the insert in the service",
454            );
455        }
456        Refusal::TooLong(name, limit) => {
457            text.push("`");
458            text.push(name);
459            text.push("` is longer than the ");
460            match limit {
461                NameLimit::Bytes(most) => {
462                    text.number(most as usize);
463                    text.push(" bytes");
464                }
465                NameLimit::Characters(most) => {
466                    text.number(most as usize);
467                    text.push(" characters");
468                }
469            }
470            text.push(" the ");
471            text.push(spelling.dialect);
472            text.push(" dialect allows in a name");
473        }
474    }
475    panic!("{}", text.as_str())
476}
477
478/// Panics with the message of a text of `len` bytes that outgrows `Sql<capacity>`.
479const fn outgrown(spec: &TableSpec<'_>, capacity: usize, len: usize) -> ! {
480    let mut bytes = [0; MESSAGE];
481    let mut text = Text::new(&mut bytes);
482    text.push("the insert into `");
483    text.push(spec.table());
484    text.push("` takes ");
485    text.number(len);
486    text.push(" bytes, more than `Sql<");
487    text.number(capacity);
488    text.push(">` holds: raise `N` to ");
489    text.number(len);
490    panic!("{}", text.as_str())
491}
492
493/// The Postgres insert of `spec`: every column the database does not fill, in
494/// [`TableSpec::columns`] order, bound as `$1`, `$2`, ..; `DEFAULT VALUES` when there is none.
495///
496/// # Panics
497///
498/// When the text outgrows `N`, when the description reads the table with `*`
499/// ([`TableSpec::selecting_all`]), or when a name is longer than the 63 bytes Postgres keeps. In
500/// a `const` the panic is a build error.
501///
502/// # Examples
503///
504/// ```
505/// use ruststream_sqlx_dialect::insert::{self, Sql};
506/// use ruststream_sqlx_dialect::{Column, Form, TableSpec};
507///
508/// const ORDERS: TableSpec<'static> =
509///     TableSpec::new("order_jobs", Column::new("job_id").generated(), Form::RowLock)
510///         .within("app")
511///         .payload(Column::new("payload"));
512/// const INSERT: Sql<64> = insert::postgres(&ORDERS);
513///
514/// assert_eq!(INSERT.as_str(), r#"INSERT INTO "app"."order_jobs" ("payload") VALUES ($1)"#);
515/// ```
516#[cfg(feature = "postgres")]
517#[must_use]
518pub const fn postgres<const N: usize>(spec: &TableSpec<'_>) -> Sql<N> {
519    Sql::insert(spec, crate::postgres::SPELLING)
520}
521
522/// The MySQL and MariaDB insert of `spec`: every column the database does not fill, in
523/// [`TableSpec::columns`] order, bound as `?`; `() VALUES ()` when there is none.
524///
525/// # Panics
526///
527/// When the text outgrows `N`, when the description reads the table with `*`
528/// ([`TableSpec::selecting_all`]), or when a name is longer than the 64 characters MySQL keeps.
529/// In a `const` the panic is a build error.
530///
531/// # Examples
532///
533/// ```
534/// use ruststream_sqlx_dialect::insert::{self, Sql};
535/// use ruststream_sqlx_dialect::{Column, Form, TableSpec};
536///
537/// const ORDERS: TableSpec<'static> =
538///     TableSpec::new("order_jobs", Column::new("job_id").generated(), Form::RowLock)
539///         .payload(Column::new("payload"));
540/// const INSERT: Sql<64> = insert::mysql(&ORDERS);
541///
542/// assert_eq!(INSERT.as_str(), "INSERT INTO `order_jobs` (`payload`) VALUES (?)");
543/// ```
544#[cfg(feature = "mysql")]
545#[must_use]
546pub const fn mysql<const N: usize>(spec: &TableSpec<'_>) -> Sql<N> {
547    Sql::insert(spec, crate::mysql::SPELLING)
548}
549
550/// The SQLite insert of `spec`: every column the database does not fill, in
551/// [`TableSpec::columns`] order, bound as `?`; `DEFAULT VALUES` when there is none.
552///
553/// # Panics
554///
555/// When the text outgrows `N`, or when the description reads the table with `*`
556/// ([`TableSpec::selecting_all`]). In a `const` the panic is a build error.
557///
558/// # Examples
559///
560/// ```
561/// use ruststream_sqlx_dialect::insert::{self, Sql};
562/// use ruststream_sqlx_dialect::{Column, Form, TableSpec};
563///
564/// const ORDERS: TableSpec<'static> =
565///     TableSpec::new("order_jobs", Column::new("job_id").generated(), Form::RowLock)
566///         .payload(Column::new("payload"));
567/// const INSERT: Sql<64> = insert::sqlite(&ORDERS);
568///
569/// assert_eq!(INSERT.as_str(), "INSERT INTO `order_jobs` (`payload`) VALUES (?)");
570/// ```
571#[cfg(feature = "sqlite")]
572#[must_use]
573pub const fn sqlite<const N: usize>(spec: &TableSpec<'_>) -> Sql<N> {
574    Sql::insert(spec, crate::sqlite::SPELLING)
575}