use std::sync::Arc;
use super::SqlOptions;
use crate::store::StoreError;
pub trait Dialect: sealed::Driver {}
impl<D: sealed::Driver> Dialect for D {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Int,
Bytes,
Text,
}
#[cfg(feature = "postgres")]
impl Value {
pub(crate) fn into_int(self) -> Option<i64> {
if let Self::Int(value) = self {
Some(value)
} else {
None
}
}
pub(crate) fn into_bytes(self) -> Option<Vec<u8>> {
if let Self::Bytes(value) = self {
Some(value)
} else {
None
}
}
pub(crate) fn into_text(self) -> Option<String> {
if let Self::Text(value) = self {
Some(value)
} else {
None
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Value {
Int(i64),
Bytes(Vec<u8>),
Text(String),
Null(Kind),
}
#[derive(Debug, Clone)]
pub struct Statement {
pub(crate) text: Arc<str>,
pub(crate) columns: &'static [Kind],
}
#[derive(Debug, Clone)]
pub struct Batch {
pub(crate) template: Arc<str>,
pub(crate) kinds: Vec<Kind>,
pub(crate) columns: &'static [Kind],
}
impl Batch {
pub(crate) fn text<D: sealed::Driver>(&self, rows: usize) -> Arc<str> {
Arc::from(
self.template
.replace("{{rows}}", &D::rows_source(&self.kinds, rows)),
)
}
}
#[derive(Debug)]
pub enum Write<'s> {
Row(&'s Statement, Vec<Value>),
Rows(&'s Batch, Vec<Vec<Value>>),
}
#[cfg(feature = "postgres")]
impl<'s> Write<'s> {
pub(crate) fn combine<D: sealed::Driver>(writes: Vec<Self>) -> Option<(String, Vec<Self>)> {
let mut text = String::from("WITH ");
let mut offset = 0usize;
let mut kept = Vec::with_capacity(writes.len());
for write in writes {
let (sql, count) = match &write {
Self::Rows(_, rows) if rows.is_empty() => continue,
Self::Row(statement, args) => (Arc::clone(&statement.text), args.len()),
Self::Rows(batch, _) => (batch.text::<D>(0), batch.kinds.len()),
};
if !kept.is_empty() {
text.push_str(", ");
}
let name = kept.len();
text.push_str(&format!("w{name} AS ({})", renumber(&sql, offset)));
offset = offset.saturating_add(count);
kept.push(write);
}
text.push_str(" SELECT 1");
(!kept.is_empty()).then_some((text, kept))
}
}
#[cfg(feature = "postgres")]
fn renumber(sql: &str, offset: usize) -> String {
let mut out = String::with_capacity(sql.len().saturating_add(8));
let mut chars = sql.chars().peekable();
while let Some(c) = chars.next() {
out.push(c);
if c != '$' {
continue;
}
let mut number = 0usize;
let mut digits = false;
while let Some(digit) = chars.peek().and_then(|c| c.to_digit(10)) {
chars.next();
digits = true;
number = number.saturating_mul(10).saturating_add(digit as usize);
}
if digits {
out.push_str(&number.saturating_add(offset).to_string());
}
}
out
}
#[cfg(feature = "sqlite")]
pub(crate) fn chunks(
rows: Vec<Vec<Value>>,
parameters: usize,
width: usize,
) -> Vec<Vec<Vec<Value>>> {
let per_chunk = parameters.checked_div(width).unwrap_or(1).max(1);
let mut rows = rows.into_iter().peekable();
let mut chunks = Vec::new();
while rows.peek().is_some() {
chunks.push(rows.by_ref().take(per_chunk).collect());
}
chunks
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Write,
Read,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Class {
Busy,
Lost,
Integrity,
MissingTable,
Failed,
Broken,
}
impl Class {
pub(crate) fn of(error: &sqlx::Error, database: fn(&str, &str) -> Self) -> Self {
match error {
sqlx::Error::Database(error) => {
database(error.code().as_deref().unwrap_or_default(), error.message())
}
sqlx::Error::PoolTimedOut => Self::Busy,
sqlx::Error::Io(_) => Self::Lost,
sqlx::Error::ColumnDecode { .. } | sqlx::Error::Decode(_) => Self::Integrity,
_ => Self::Broken,
}
}
}
macro_rules! plumbing {
(methods) => {
async fn close(pool: &Self::Pool) {
pool.close().await;
}
async fn commit(tx: Self::Tx) -> Result<(), sqlx::Error> {
tx.commit().await
}
async fn rollback(tx: Self::Tx) -> Result<(), sqlx::Error> {
tx.rollback().await
}
async fn fetch(
tx: &mut Self::Tx,
statement: &Statement,
args: Vec<Value>,
) -> Result<Vec<Vec<Value>>, sqlx::Error> {
let rows = query(&statement.text, args).fetch_all(&mut **tx).await?;
rows.iter()
.map(|row| decode(row, statement.columns))
.collect()
}
async fn execute(
tx: &mut Self::Tx,
statement: &Statement,
args: Vec<Value>,
) -> Result<u64, sqlx::Error> {
Ok(query(&statement.text, args)
.execute(&mut **tx)
.await?
.rows_affected())
}
async fn script(tx: &mut Self::Tx, script: String) -> Result<(), sqlx::Error> {
sqlx::raw_sql(sqlx::AssertSqlSafe(script))
.execute(&mut **tx)
.await
.map(drop)
}
};
($db:ty) => {
type Query = sqlx::query::Query<'static, $db, <$db as sqlx::Database>::Arguments>;
fn bind(query: Query, value: Value) -> Query {
match value {
Value::Int(value) => query.bind(value),
Value::Bytes(value) => query.bind(value),
Value::Text(value) => query.bind(value),
Value::Null(Kind::Int) => query.bind(None::<i64>),
Value::Null(Kind::Bytes) => query.bind(None::<Vec<u8>>),
Value::Null(Kind::Text) => query.bind(None::<String>),
}
}
fn query(text: &std::sync::Arc<str>, args: Vec<Value>) -> Query {
args.into_iter().fold(
sqlx::query(sqlx::AssertSqlSafe(std::sync::Arc::clone(text))),
bind,
)
}
fn decode(
row: &<$db as sqlx::Database>::Row,
columns: &[Kind],
) -> Result<Vec<Value>, sqlx::Error> {
use sqlx::Row as _;
columns
.iter()
.enumerate()
.map(|(index, kind)| {
Ok(match kind {
Kind::Int => row.try_get::<Option<i64>, _>(index)?.map(Value::Int),
Kind::Bytes => row.try_get::<Option<Vec<u8>>, _>(index)?.map(Value::Bytes),
Kind::Text => row.try_get::<Option<String>, _>(index)?.map(Value::Text),
}
.unwrap_or(Value::Null(*kind)))
})
.collect()
}
};
}
pub(crate) use plumbing;
#[derive(Debug, Clone, Copy)]
pub struct Tokens {
pub(crate) int: &'static str,
pub(crate) bytes: &'static str,
pub(crate) text: &'static str,
pub(crate) len: &'static str,
pub(crate) strict: &'static str,
}
#[derive(Debug, Clone, Copy)]
pub struct Triggers {
pub(crate) append_only: &'static str,
pub(crate) maintained: &'static str,
pub(crate) every_table: &'static str,
pub(crate) no_replace: &'static str,
pub(crate) scoped: &'static str,
}
pub(crate) mod sealed {
use core::future::Future;
use super::{
Batch, Class, Kind, Mode, SqlOptions, Statement, StoreError, Tokens, Triggers, Value, Write,
};
pub trait Driver: Send + Sync + Sized + 'static {
type Pool: Clone + Send + Sync + 'static;
type Tx: Send;
const TOKENS: Tokens;
const TRIGGERS: Triggers;
fn qualifier(schema: &str) -> Result<String, StoreError>;
fn connect(
url: &str,
options: &SqlOptions,
) -> impl Future<Output = Result<Self::Pool, StoreError>> + Send;
fn close(pool: &Self::Pool) -> impl Future<Output = ()> + Send;
fn begin(
pool: &Self::Pool,
mode: Mode,
) -> impl Future<Output = Result<Self::Tx, sqlx::Error>> + Send;
fn lock(
tx: &mut Self::Tx,
key: i64,
) -> impl Future<Output = Result<(), sqlx::Error>> + Send;
fn commit(tx: Self::Tx) -> impl Future<Output = Result<(), sqlx::Error>> + Send;
fn rollback(tx: Self::Tx) -> impl Future<Output = Result<(), sqlx::Error>> + Send;
fn fetch(
tx: &mut Self::Tx,
statement: &Statement,
args: Vec<Value>,
) -> impl Future<Output = Result<Vec<Vec<Value>>, sqlx::Error>> + Send;
fn execute(
tx: &mut Self::Tx,
statement: &Statement,
args: Vec<Value>,
) -> impl Future<Output = Result<u64, sqlx::Error>> + Send;
fn batch(
tx: &mut Self::Tx,
batch: &Batch,
rows: Vec<Vec<Value>>,
) -> impl Future<Output = Result<Vec<Vec<Value>>, sqlx::Error>> + Send;
fn script(
tx: &mut Self::Tx,
script: String,
) -> impl Future<Output = Result<(), sqlx::Error>> + Send;
fn rows_source(kinds: &[Kind], rows: usize) -> String;
fn apply(
tx: &mut Self::Tx,
writes: Vec<Write<'_>>,
) -> impl Future<Output = Result<(), sqlx::Error>> + Send {
async move {
for write in writes {
match write {
Write::Row(statement, args) => {
Self::execute(tx, statement, args).await?;
}
Write::Rows(_, rows) if rows.is_empty() => {}
Write::Rows(batch, rows) => {
Self::batch(tx, batch, rows).await?;
}
}
}
Ok(())
}
}
const META_PROBE: &str;
const PRELUDE: &str;
fn classify(code: &str, message: &str) -> Class;
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "postgres")]
#[test]
fn parameters_are_renumbered_after_the_ones_before() {
assert_eq!(
super::renumber("INSERT INTO t (a, b) VALUES ($1, $12) -- $", 3),
"INSERT INTO t (a, b) VALUES ($4, $15) -- $"
);
assert_eq!(super::renumber("SELECT 1", 7), "SELECT 1");
}
#[cfg(feature = "sqlite")]
#[test]
fn chunks_stay_below_the_parameter_limit() {
use super::{Value, chunks};
let rows = |n: usize| (0..n).map(|_| vec![Value::Int(1); 3]).collect::<Vec<_>>();
let sizes = |chunks: Vec<Vec<Vec<Value>>>| chunks.iter().map(Vec::len).collect::<Vec<_>>();
assert_eq!(sizes(chunks(rows(7), 9, 3)), [3, 3, 1]);
assert_eq!(sizes(chunks(rows(2), 2, 3)), [1, 1]);
assert!(chunks(rows(0), 9, 3).is_empty());
}
}
#[cfg(test)]
pub(crate) mod fake {
use std::borrow::Cow;
use sqlx::error::{DatabaseError, ErrorKind};
#[derive(Debug)]
struct Fake {
code: &'static str,
message: &'static str,
}
impl std::fmt::Display for Fake {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.message)
}
}
impl std::error::Error for Fake {}
impl DatabaseError for Fake {
fn message(&self) -> &str {
self.message
}
fn code(&self) -> Option<Cow<'_, str>> {
Some(Cow::Borrowed(self.code))
}
fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
self
}
fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) {
self
}
fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> {
self
}
fn kind(&self) -> ErrorKind {
ErrorKind::Other
}
}
pub(crate) fn database(code: &'static str, message: &'static str) -> sqlx::Error {
sqlx::Error::Database(Box::new(Fake { code, message }))
}
}