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}