use alloc::boxed::Box;
use alloc::string::ToString;
use alloc::vec::Vec;
use diesel::backend::Backend;
use diesel::query_builder::{AstPass, QueryFragment, QueryId};
use diesel::query_dsl::RunQueryDsl;
use diesel::result::{Error as DieselError, QueryResult};
use diesel::serialize::ToSql;
use diesel::sql_types::{BigInt, Binary, Double, HasSqlType, Text};
use super::sql_output::ColumnNames;
use super::view::{PatchsetOp, PatchsetUpdateEntry};
use crate::SchemaWithPK;
use crate::encoding::Value;
#[derive(Debug, Clone, thiserror::Error)]
enum RenderError {
#[error("missing column name for index {column_index}")]
MissingColumnName { column_index: usize },
#[error("patchset UPDATE has an empty SET clause")]
EmptyUpdateSet,
#[error("patchset UPDATE/DELETE targets a table with no primary key")]
EmptyRowPredicate,
#[error("adapter rejected column {column_index} of table {table_name:?}: {message}")]
AdapterBindFailure {
table_name: alloc::string::String,
column_index: usize,
message: alloc::string::String,
},
}
#[inline]
fn render_err(err: RenderError) -> DieselError {
DieselError::QueryBuilderError(Box::new(err))
}
#[inline]
fn column_name<T: ColumnNames>(table: &T, index: usize) -> QueryResult<&str> {
table.column_name(index).ok_or_else(|| {
render_err(RenderError::MissingColumnName {
column_index: index,
})
})
}
fn push_value<'b, S, B, DB>(
out: &mut AstPass<'_, 'b, DB>,
value: &'b Value<S, B>,
) -> QueryResult<()>
where
S: AsRef<str>,
B: AsRef<[u8]>,
DB: Backend + HasSqlType<BigInt> + HasSqlType<Double> + HasSqlType<Text> + HasSqlType<Binary>,
i64: ToSql<BigInt, DB>,
f64: ToSql<Double, DB>,
str: ToSql<Text, DB>,
[u8]: ToSql<Binary, DB>,
{
match value {
Value::Null => {
out.push_sql("NULL");
Ok(())
}
Value::Integer(i) => out.push_bind_param::<BigInt, i64>(i),
Value::Real(f) => out.push_bind_param::<Double, f64>(f),
Value::Text(s) => out.push_bind_param::<Text, str>(s.as_ref()),
Value::Blob(b) => out.push_bind_param::<Binary, [u8]>(b.as_ref()),
}
}
fn walk_insert<'b, T, S, B, DB>(
table: &T,
values: &'b [Value<S, B>],
out: &mut AstPass<'_, 'b, DB>,
) -> QueryResult<()>
where
T: ColumnNames,
S: AsRef<str>,
B: AsRef<[u8]>,
DB: Backend + HasSqlType<BigInt> + HasSqlType<Double> + HasSqlType<Text> + HasSqlType<Binary>,
i64: ToSql<BigInt, DB>,
f64: ToSql<Double, DB>,
str: ToSql<Text, DB>,
[u8]: ToSql<Binary, DB>,
{
out.push_sql("INSERT INTO ");
out.push_identifier(table.name())?;
out.push_sql(" (");
for index in 0..table.number_of_columns() {
if index > 0 {
out.push_sql(", ");
}
out.push_identifier(column_name(table, index)?)?;
}
out.push_sql(") VALUES (");
for (index, value) in values.iter().enumerate() {
if index > 0 {
out.push_sql(", ");
}
push_value(out, value)?;
}
out.push_sql(")");
Ok(())
}
fn walk_update<'b, T, S, B, DB>(
table: &T,
pk: &'b [Value<S, B>],
entries: &'b [PatchsetUpdateEntry<S, B>],
out: &mut AstPass<'_, 'b, DB>,
) -> QueryResult<()>
where
T: ColumnNames,
S: AsRef<str>,
B: AsRef<[u8]>,
DB: Backend + HasSqlType<BigInt> + HasSqlType<Double> + HasSqlType<Text> + HasSqlType<Binary>,
i64: ToSql<BigInt, DB>,
f64: ToSql<Double, DB>,
str: ToSql<Text, DB>,
[u8]: ToSql<Binary, DB>,
{
out.push_sql("UPDATE ");
out.push_identifier(table.name())?;
out.push_sql(" SET ");
let mut first_set = true;
for (col_idx, ((), new)) in entries.iter().enumerate() {
let Some(new_value) = new.as_ref() else {
continue;
};
if table.primary_key_index(col_idx).is_some() {
continue;
}
if !first_set {
out.push_sql(", ");
}
first_set = false;
out.push_identifier(column_name(table, col_idx)?)?;
out.push_sql(" = ");
push_value(out, new_value)?;
}
if first_set {
return Err(render_err(RenderError::EmptyUpdateSet));
}
out.push_sql(" WHERE ");
walk_pk_predicate(table, pk, out)
}
fn walk_delete<'b, T, S, B, DB>(
table: &T,
pk: &'b [Value<S, B>],
out: &mut AstPass<'_, 'b, DB>,
) -> QueryResult<()>
where
T: ColumnNames,
S: AsRef<str>,
B: AsRef<[u8]>,
DB: Backend + HasSqlType<BigInt> + HasSqlType<Double> + HasSqlType<Text> + HasSqlType<Binary>,
i64: ToSql<BigInt, DB>,
f64: ToSql<Double, DB>,
str: ToSql<Text, DB>,
[u8]: ToSql<Binary, DB>,
{
out.push_sql("DELETE FROM ");
out.push_identifier(table.name())?;
out.push_sql(" WHERE ");
walk_pk_predicate(table, pk, out)
}
fn walk_pk_predicate<'b, T, S, B, DB>(
table: &T,
pk: &'b [Value<S, B>],
out: &mut AstPass<'_, 'b, DB>,
) -> QueryResult<()>
where
T: ColumnNames,
S: AsRef<str>,
B: AsRef<[u8]>,
DB: Backend + HasSqlType<BigInt> + HasSqlType<Double> + HasSqlType<Text> + HasSqlType<Binary>,
i64: ToSql<BigInt, DB>,
f64: ToSql<Double, DB>,
str: ToSql<Text, DB>,
[u8]: ToSql<Binary, DB>,
{
let pk_indices = table.pk_indices();
if pk_indices.is_empty() {
return Err(render_err(RenderError::EmptyRowPredicate));
}
for (pk_ordinal, &col_idx) in pk_indices.iter().enumerate() {
if pk_ordinal > 0 {
out.push_sql(" AND ");
}
out.push_identifier(column_name(table, col_idx)?)?;
out.push_sql(" = ");
push_value(out, &pk[pk_ordinal])?;
}
Ok(())
}
impl<T, S, B, DB> QueryFragment<DB> for PatchsetOp<'_, T, S, B>
where
T: ColumnNames,
S: AsRef<str>,
B: AsRef<[u8]>,
DB: Backend + HasSqlType<BigInt> + HasSqlType<Double> + HasSqlType<Text> + HasSqlType<Binary>,
i64: ToSql<BigInt, DB>,
f64: ToSql<Double, DB>,
str: ToSql<Text, DB>,
[u8]: ToSql<Binary, DB>,
{
fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, DB>) -> QueryResult<()> {
out.unsafe_to_cache_prepared();
match *self {
PatchsetOp::Insert { table, values, .. } => walk_insert(table, values, &mut out),
PatchsetOp::Update {
table, pk, entries, ..
} => walk_update(table, pk, entries, &mut out),
PatchsetOp::Delete { table, pk, .. } => walk_delete(table, pk, &mut out),
}
}
}
impl<T, S, B> QueryId for PatchsetOp<'_, T, S, B> {
type QueryId = ();
const HAS_STATIC_QUERY_ID: bool = false;
}
impl<T, S, B, Conn> RunQueryDsl<Conn> for PatchsetOp<'_, T, S, B> {}
pub trait Binder<DB: Backend> {
fn walk<'b>(&'b self, out: &mut AstPass<'_, 'b, DB>) -> QueryResult<()>;
}
pub struct DefaultBinder<'a, S, B> {
value: &'a Value<S, B>,
}
impl<'a, S, B> DefaultBinder<'a, S, B> {
#[must_use]
pub fn new(value: &'a Value<S, B>) -> Self {
Self { value }
}
}
impl<'a, S, B> From<&'a Value<S, B>> for DefaultBinder<'a, S, B> {
fn from(value: &'a Value<S, B>) -> Self {
Self { value }
}
}
impl<S, B, DB> Binder<DB> for DefaultBinder<'_, S, B>
where
S: AsRef<str>,
B: AsRef<[u8]>,
DB: Backend + HasSqlType<BigInt> + HasSqlType<Double> + HasSqlType<Text> + HasSqlType<Binary>,
i64: ToSql<BigInt, DB>,
f64: ToSql<Double, DB>,
str: ToSql<Text, DB>,
[u8]: ToSql<Binary, DB>,
{
fn walk<'b>(&'b self, out: &mut AstPass<'_, 'b, DB>) -> QueryResult<()> {
push_value(out, self.value)
}
}
pub trait Adapter<DB, S, B> {
fn column_name(&self, table_name: &str, column_index: usize) -> &str;
fn bind<'a>(
&self,
table_name: &str,
column_index: usize,
value: &'a Value<S, B>,
) -> QueryResult<Box<dyn Binder<DB> + Send + 'a>>;
}
pub struct BoundPatchsetOp<'a, T, S, B, DB, A>
where
DB: Backend,
{
op: PatchsetOp<'a, T, S, B>,
resolved: Result<Vec<Box<dyn Binder<DB> + Send + 'a>>, RenderError>,
adapter: &'a A,
}
impl<'a, T, S, B, DB, A> BoundPatchsetOp<'a, T, S, B, DB, A>
where
T: SchemaWithPK,
S: AsRef<str>,
B: AsRef<[u8]>,
DB: Backend,
A: Adapter<DB, S, B> + Send + Sync,
{
fn resolve(op: PatchsetOp<'a, T, S, B>, adapter: &'a A) -> Self {
let table = op.table();
let table_name = table.name();
let resolved = (|| -> Result<Vec<Box<dyn Binder<DB> + Send + 'a>>, RenderError> {
let mut binders: Vec<Box<dyn Binder<DB> + Send + 'a>> = Vec::new();
let bind = |col_idx: usize,
value: &'a Value<S, B>|
-> Result<Box<dyn Binder<DB> + Send + 'a>, RenderError> {
adapter.bind(table_name, col_idx, value).map_err(|err| {
RenderError::AdapterBindFailure {
table_name: table_name.into(),
column_index: col_idx,
message: err.to_string(),
}
})
};
match op {
PatchsetOp::Insert { values, .. } => {
for (col_idx, value) in values.iter().enumerate() {
binders.push(bind(col_idx, value)?);
}
}
PatchsetOp::Update { pk, entries, .. } => {
for (col_idx, ((), new)) in entries.iter().enumerate() {
if let Some(new_val) = new
&& table.primary_key_index(col_idx).is_none()
{
binders.push(bind(col_idx, new_val)?);
}
}
for (pk_ordinal, col_idx) in pk_indices(table).into_iter().enumerate() {
binders.push(bind(col_idx, &pk[pk_ordinal])?);
}
}
PatchsetOp::Delete { pk, .. } => {
for (pk_ordinal, col_idx) in pk_indices(table).into_iter().enumerate() {
binders.push(bind(col_idx, &pk[pk_ordinal])?);
}
}
}
Ok(binders)
})();
Self {
op,
resolved,
adapter,
}
}
}
fn pk_indices<T: SchemaWithPK>(table: &T) -> Vec<usize> {
let mut pk_cols: Vec<(usize, usize)> = Vec::new();
for col_idx in 0..table.number_of_columns() {
if let Some(pk_ordinal) = table.primary_key_index(col_idx) {
pk_cols.push((pk_ordinal, col_idx));
}
}
pk_cols.sort_by_key(|(ordinal, _)| *ordinal);
pk_cols.into_iter().map(|(_, col_idx)| col_idx).collect()
}
impl<T, S, B, DB, A> QueryFragment<DB> for BoundPatchsetOp<'_, T, S, B, DB, A>
where
T: SchemaWithPK,
S: AsRef<str>,
B: AsRef<[u8]>,
DB: Backend,
A: Adapter<DB, S, B> + Send + Sync,
{
fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, DB>) -> QueryResult<()> {
out.unsafe_to_cache_prepared();
let binder_vec = self
.resolved
.as_ref()
.map_err(|err| render_err(err.clone()))?;
let mut binders = binder_vec.iter();
let bind_next = |binders: &mut core::slice::Iter<'b, Box<dyn Binder<DB> + Send + '_>>,
out: &mut AstPass<'_, 'b, DB>|
-> QueryResult<()> {
let binder = binders.next().ok_or_else(|| {
DieselError::QueryBuilderError(alloc::boxed::Box::new(BinderResolutionError))
})?;
binder.walk(out)
};
let adapter = self.adapter;
match self.op {
PatchsetOp::Insert { table, .. } => {
let table_name = table.name();
out.push_sql("INSERT INTO ");
out.push_identifier(table_name)?;
out.push_sql(" (");
let column_count = table.number_of_columns();
for index in 0..column_count {
if index > 0 {
out.push_sql(", ");
}
out.push_identifier(adapter.column_name(table_name, index))?;
}
out.push_sql(") VALUES (");
for index in 0..column_count {
if index > 0 {
out.push_sql(", ");
}
bind_next(&mut binders, &mut out)?;
}
out.push_sql(")");
Ok(())
}
PatchsetOp::Update {
table, pk, entries, ..
} => {
let table_name = table.name();
out.push_sql("UPDATE ");
out.push_identifier(table_name)?;
out.push_sql(" SET ");
let mut first_set = true;
for (col_idx, ((), new)) in entries.iter().enumerate() {
if new.is_none() || table.primary_key_index(col_idx).is_some() {
continue;
}
if !first_set {
out.push_sql(", ");
}
first_set = false;
out.push_identifier(adapter.column_name(table_name, col_idx))?;
out.push_sql(" = ");
bind_next(&mut binders, &mut out)?;
}
if first_set {
return Err(render_err(RenderError::EmptyUpdateSet));
}
out.push_sql(" WHERE ");
let pk_col_indices = pk_indices(table);
if pk_col_indices.is_empty() {
return Err(render_err(RenderError::EmptyRowPredicate));
}
for (pk_ordinal, col_idx) in pk_col_indices.iter().copied().enumerate() {
if pk_ordinal > 0 {
out.push_sql(" AND ");
}
out.push_identifier(adapter.column_name(table_name, col_idx))?;
out.push_sql(" = ");
bind_next(&mut binders, &mut out)?;
}
let _ = pk;
Ok(())
}
PatchsetOp::Delete { table, pk, .. } => {
let table_name = table.name();
out.push_sql("DELETE FROM ");
out.push_identifier(table_name)?;
out.push_sql(" WHERE ");
let pk_col_indices = pk_indices(table);
if pk_col_indices.is_empty() {
return Err(render_err(RenderError::EmptyRowPredicate));
}
for (pk_ordinal, col_idx) in pk_col_indices.iter().copied().enumerate() {
if pk_ordinal > 0 {
out.push_sql(" AND ");
}
out.push_identifier(adapter.column_name(table_name, col_idx))?;
out.push_sql(" = ");
bind_next(&mut binders, &mut out)?;
}
let _ = pk;
Ok(())
}
}
}
}
impl<T, S, B, DB, A> QueryId for BoundPatchsetOp<'_, T, S, B, DB, A>
where
DB: Backend,
{
type QueryId = ();
const HAS_STATIC_QUERY_ID: bool = false;
}
impl<T, S, B, DB, A, Conn> RunQueryDsl<Conn> for BoundPatchsetOp<'_, T, S, B, DB, A> where
DB: Backend
{
}
#[derive(Debug, thiserror::Error)]
#[error("internal: binder count mismatch between BoundPatchsetOp resolve and walk_ast")]
struct BinderResolutionError;
impl<'a, T, S, B> PatchsetOp<'a, T, S, B>
where
T: SchemaWithPK,
S: AsRef<str>,
B: AsRef<[u8]>,
{
#[must_use]
pub fn with_adapter<DB, A>(self, adapter: &'a A) -> BoundPatchsetOp<'a, T, S, B, DB, A>
where
A: Adapter<DB, S, B> + Send + Sync,
DB: Backend,
{
BoundPatchsetOp::resolve(self, adapter)
}
}
pub trait ApplyOps: Iterator + Sized {
fn apply<Conn>(self, conn: &mut Conn) -> QueryResult<usize>
where
Conn: diesel::Connection,
Self::Item: QueryFragment<Conn::Backend> + QueryId + RunQueryDsl<Conn>;
fn apply_transactional<Conn>(self, conn: &mut Conn) -> QueryResult<usize>
where
Conn: diesel::Connection,
Self::Item: QueryFragment<Conn::Backend> + QueryId + RunQueryDsl<Conn>;
}
impl<I> ApplyOps for I
where
I: Iterator + Sized,
{
fn apply<Conn>(self, conn: &mut Conn) -> QueryResult<usize>
where
Conn: diesel::Connection,
Self::Item: QueryFragment<Conn::Backend> + QueryId + RunQueryDsl<Conn>,
{
let mut total = 0_usize;
for op in self {
total = total.saturating_add(op.execute(conn)?);
}
Ok(total)
}
fn apply_transactional<Conn>(self, conn: &mut Conn) -> QueryResult<usize>
where
Conn: diesel::Connection,
Self::Item: QueryFragment<Conn::Backend> + QueryId + RunQueryDsl<Conn>,
{
conn.transaction(|conn| self.apply(conn))
}
}