use core::fmt::{self, Debug, Display, Formatter};
use crate::column::Column;
use crate::spec::{Columns, TableSpec};
use crate::statement::{NameLimit, Param, Statement, StatementError};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Spelling {
pub(crate) dialect: &'static str,
pub(crate) quote: u8,
pub(crate) placeholder: Placeholder,
pub(crate) default_row: &'static str,
pub(crate) name_limit: Option<NameLimit>,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Placeholder {
#[cfg(any(feature = "mysql", feature = "sqlite"))]
Question,
#[cfg(feature = "postgres")]
Dollar,
}
pub(crate) fn quote_into(quote: u8, ident: &str, out: &mut String) {
let quote = char::from(quote);
out.push(quote);
for character in ident.chars() {
if character == quote {
out.push(quote);
}
out.push(character);
}
out.push(quote);
}
pub(crate) const fn fits(name: &str, limit: NameLimit) -> bool {
match limit {
NameLimit::Bytes(most) => name.len() <= most as usize,
NameLimit::Characters(most) => characters(name) <= most as usize,
}
}
const fn characters(text: &str) -> usize {
let bytes = text.as_bytes();
let mut count = 0;
let mut index = 0;
while index < bytes.len() {
if bytes[index] & 0b1100_0000 != 0b1000_0000 {
count += 1;
}
index += 1;
}
count
}
pub(crate) const fn too_long<'a>(spec: &TableSpec<'a>, limit: NameLimit) -> Option<&'a str> {
if let Some(schema) = spec.schema()
&& !fits(schema, limit)
{
return Some(schema);
}
if !fits(spec.table(), limit) {
return Some(spec.table());
}
let mut columns = Columns::new(spec);
while let Some(column) = columns.next_column() {
if !fits(column.name(), limit) {
return Some(column.name());
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Refusal<'a> {
Flattened,
TooLong(&'a str, NameLimit),
}
impl Refusal<'_> {
fn into_error(self, dialect: &'static str) -> StatementError {
match self {
Self::Flattened => StatementError::Flattened {
statement: "insert",
},
Self::TooLong(identifier, limit) => StatementError::IdentifierTooLong {
dialect,
identifier: identifier.to_owned(),
limit,
},
}
}
}
const fn refusal<'a>(spec: &TableSpec<'a>, spelling: Spelling) -> Option<Refusal<'a>> {
if let Some(limit) = spelling.name_limit
&& let Some(name) = too_long(spec, limit)
{
return Some(Refusal::TooLong(name, limit));
}
if spec.selects_all() {
return Some(Refusal::Flattened);
}
None
}
struct Text<'b> {
out: &'b mut [u8],
len: usize,
}
impl<'b> Text<'b> {
const fn new(out: &'b mut [u8]) -> Self {
Self { out, len: 0 }
}
const fn byte(&mut self, byte: u8) {
if self.len < self.out.len() {
self.out[self.len] = byte;
}
self.len += 1;
}
const fn push(&mut self, text: &str) {
let bytes = text.as_bytes();
let mut index = 0;
while index < bytes.len() {
self.byte(bytes[index]);
index += 1;
}
}
const fn ident(&mut self, quote: u8, name: &str) {
self.byte(quote);
let bytes = name.as_bytes();
let mut index = 0;
while index < bytes.len() {
if bytes[index] == quote {
self.byte(quote);
}
self.byte(bytes[index]);
index += 1;
}
self.byte(quote);
}
const fn number(&mut self, number: usize) {
let mut unit = 1;
while number / unit >= 10 {
unit *= 10;
}
while unit > 0 {
#[allow(clippy::cast_possible_truncation)]
self.byte(b'0' + (number / unit % 10) as u8);
unit /= 10;
}
}
const fn as_str(&self) -> &str {
let written = if self.len < self.out.len() {
self.len
} else {
self.out.len()
};
let (text, _) = self.out.split_at(written);
prefix(text)
}
}
const fn prefix(bytes: &[u8]) -> &str {
match core::str::from_utf8(bytes) {
Ok(text) => text,
Err(error) => {
let (valid, _) = bytes.split_at(error.valid_up_to());
match core::str::from_utf8(valid) {
Ok(text) => text,
Err(_) => panic!("a prefix up to `valid_up_to` is UTF-8"),
}
}
}
}
const fn written(column: Column<'_>) -> bool {
!column.is_generated()
}
const fn write(text: &mut Text<'_>, spec: &TableSpec<'_>, spelling: Spelling) {
text.push("INSERT INTO ");
if let Some(schema) = spec.schema() {
text.ident(spelling.quote, schema);
text.push(".");
}
text.ident(spelling.quote, spec.table());
let mut columns = Columns::new(spec);
let mut count = 0;
while let Some(column) = columns.next_column() {
if written(column) {
text.push(if count == 0 { " (" } else { ", " });
text.ident(spelling.quote, column.name());
count += 1;
}
}
if count == 0 {
text.push(spelling.default_row);
return;
}
text.push(") VALUES (");
let mut index = 0;
while index < count {
if index > 0 {
text.push(", ");
}
match spelling.placeholder {
#[cfg(any(feature = "mysql", feature = "sqlite"))]
Placeholder::Question => text.push("?"),
#[cfg(feature = "postgres")]
Placeholder::Dollar => {
text.push("$");
text.number(index + 1);
}
}
index += 1;
}
text.push(")");
}
pub(crate) fn statement(
spec: &TableSpec<'_>,
spelling: Spelling,
) -> Result<Statement, StatementError> {
if let Some(refusal) = refusal(spec, spelling) {
return Err(refusal.into_error(spelling.dialect));
}
let mut measure = Text::new(&mut []);
write(&mut measure, spec, spelling);
let mut bytes = vec![0; measure.len];
let mut text = Text::new(&mut bytes);
write(&mut text, spec, spelling);
let sql = text.as_str().to_owned();
let params = spec
.columns()
.enumerate()
.filter(|(_, column)| written(*column))
.map(|(position, _)| Param::Column(position));
Ok(Statement::new(sql, params))
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Sql<const N: usize> {
bytes: [u8; N],
len: usize,
}
impl<const N: usize> Sql<N> {
const fn insert(spec: &TableSpec<'_>, spelling: Spelling) -> Self {
if let Some(refusal) = refusal(spec, spelling) {
refuse(spec, spelling, refusal);
}
let mut bytes = [0; N];
let mut text = Text::new(&mut bytes);
write(&mut text, spec, spelling);
let len = text.len;
if len > N {
outgrown(spec, N, len);
}
Self { bytes, len }
}
#[must_use]
pub const fn as_str(&self) -> &str {
let (text, _) = self.bytes.split_at(self.len);
prefix(text)
}
}
impl<const N: usize> Debug for Sql<N> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_tuple("Sql").field(&self.as_str()).finish()
}
}
impl<const N: usize> Display for Sql<N> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl<const N: usize> AsRef<str> for Sql<N> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
const MESSAGE: usize = 512;
const fn refuse(spec: &TableSpec<'_>, spelling: Spelling, refusal: Refusal<'_>) -> ! {
let mut bytes = [0; MESSAGE];
let mut text = Text::new(&mut bytes);
match refusal {
Refusal::Flattened => {
text.push("the insert into `");
text.push(spec.table());
text.push(
"` needs every column, and the description reads the table with `*` \
(`selecting_all`): write the insert in the service",
);
}
Refusal::TooLong(name, limit) => {
text.push("`");
text.push(name);
text.push("` is longer than the ");
match limit {
NameLimit::Bytes(most) => {
text.number(most as usize);
text.push(" bytes");
}
NameLimit::Characters(most) => {
text.number(most as usize);
text.push(" characters");
}
}
text.push(" the ");
text.push(spelling.dialect);
text.push(" dialect allows in a name");
}
}
panic!("{}", text.as_str())
}
const fn outgrown(spec: &TableSpec<'_>, capacity: usize, len: usize) -> ! {
let mut bytes = [0; MESSAGE];
let mut text = Text::new(&mut bytes);
text.push("the insert into `");
text.push(spec.table());
text.push("` takes ");
text.number(len);
text.push(" bytes, more than `Sql<");
text.number(capacity);
text.push(">` holds: raise `N` to ");
text.number(len);
panic!("{}", text.as_str())
}
#[cfg(feature = "postgres")]
#[must_use]
pub const fn postgres<const N: usize>(spec: &TableSpec<'_>) -> Sql<N> {
Sql::insert(spec, crate::postgres::SPELLING)
}
#[cfg(feature = "mysql")]
#[must_use]
pub const fn mysql<const N: usize>(spec: &TableSpec<'_>) -> Sql<N> {
Sql::insert(spec, crate::mysql::SPELLING)
}
#[cfg(feature = "sqlite")]
#[must_use]
pub const fn sqlite<const N: usize>(spec: &TableSpec<'_>) -> Sql<N> {
Sql::insert(spec, crate::sqlite::SPELLING)
}