use crate::traits::PostgresTable;
use crate::values::PostgresValue;
use drizzle_core::{SQL, ToSQL};
use std::fmt::Debug;
use std::marker::PhantomData;
use super::ExecutableState;
#[derive(Debug, Clone, Copy, Default)]
pub struct InsertInitial;
#[derive(Debug, Clone, Copy, Default)]
pub struct InsertValuesSet;
#[derive(Debug, Clone, Copy, Default)]
pub struct InsertReturningSet;
#[derive(Debug, Clone, Copy, Default)]
pub struct InsertOnConflictSet;
impl InsertInitial {
#[inline]
pub const fn new() -> Self {
Self
}
}
impl InsertValuesSet {
#[inline]
pub const fn new() -> Self {
Self
}
}
impl InsertReturningSet {
#[inline]
pub const fn new() -> Self {
Self
}
}
impl InsertOnConflictSet {
#[inline]
pub const fn new() -> Self {
Self
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum ConflictTarget<'a> {
Columns(SQL<'a, PostgresValue<'a>>),
ColumnsWhere {
columns: SQL<'a, PostgresValue<'a>>,
where_clause: SQL<'a, PostgresValue<'a>>,
},
Constraint(String),
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Conflict<'a> {
DoNothing {
target: Option<ConflictTarget<'a>>,
},
DoUpdate {
target: ConflictTarget<'a>,
set: SQL<'a, PostgresValue<'a>>,
where_clause: Option<SQL<'a, PostgresValue<'a>>>,
},
}
impl<'a> Default for Conflict<'a> {
fn default() -> Self {
Self::DoNothing { target: None }
}
}
impl<'a> Conflict<'a> {
pub fn do_nothing_on_columns<T>(columns: T) -> Self
where
T: ToSQL<'a, PostgresValue<'a>>,
{
Conflict::DoNothing {
target: Some(ConflictTarget::Columns(columns.to_sql())),
}
}
pub fn do_nothing_on_constraint(constraint_name: String) -> Self {
Conflict::DoNothing {
target: Some(ConflictTarget::Constraint(constraint_name)),
}
}
pub fn do_update<S, W>(target: ConflictTarget<'a>, set: S, where_clause: Option<W>) -> Self
where
S: ToSQL<'a, PostgresValue<'a>>,
W: ToSQL<'a, PostgresValue<'a>>,
{
Conflict::DoUpdate {
target,
set: set.to_sql(),
where_clause: where_clause.map(|w| w.to_sql()),
}
}
}
impl<'a> ConflictTarget<'a> {
pub fn columns<T>(columns: T) -> Self
where
T: ToSQL<'a, PostgresValue<'a>>,
{
ConflictTarget::Columns(columns.to_sql())
}
pub fn columns_where<T, W>(columns: T, where_clause: W) -> Self
where
T: ToSQL<'a, PostgresValue<'a>>,
W: ToSQL<'a, PostgresValue<'a>>,
{
ConflictTarget::ColumnsWhere {
columns: columns.to_sql(),
where_clause: where_clause.to_sql(),
}
}
pub fn constraint(constraint_name: String) -> Self {
ConflictTarget::Constraint(constraint_name)
}
}
impl ExecutableState for InsertValuesSet {}
impl ExecutableState for InsertReturningSet {}
impl ExecutableState for InsertOnConflictSet {}
pub type InsertBuilder<'a, Schema, State, Table> = super::QueryBuilder<'a, Schema, State, Table>;
impl<'a, Schema, Table> InsertBuilder<'a, Schema, InsertInitial, Table>
where
Table: PostgresTable<'a>,
{
#[inline]
pub fn values<I, T>(self, values: I) -> InsertBuilder<'a, Schema, InsertValuesSet, Table>
where
I: IntoIterator<Item = Table::Insert<T>>,
{
let sql = crate::helpers::values::<'a, Table, T>(values);
InsertBuilder {
sql: self.sql.append(sql),
schema: PhantomData,
state: PhantomData,
table: PhantomData,
}
}
}
impl<'a, S, T> InsertBuilder<'a, S, InsertValuesSet, T> {
pub fn on_conflict(
self,
conflict: Conflict<'a>,
) -> InsertBuilder<'a, S, InsertOnConflictSet, T> {
let conflict_sql = match conflict {
Conflict::DoNothing { target } => {
let mut sql = SQL::raw("ON CONFLICT");
if let Some(target) = target {
sql = sql.append(Self::build_conflict_target(target));
}
sql.append(SQL::raw(" DO NOTHING"))
}
Conflict::DoUpdate {
target,
set,
where_clause,
} => {
let mut sql = SQL::raw("ON CONFLICT")
.append(Self::build_conflict_target(target))
.append(SQL::raw(" DO UPDATE SET "))
.append(set);
if let Some(where_clause) = where_clause {
sql = sql.append(SQL::raw(" WHERE ")).append(where_clause);
}
sql
}
};
InsertBuilder {
sql: self.sql.append(conflict_sql),
schema: PhantomData,
state: PhantomData,
table: PhantomData,
}
}
fn build_conflict_target(target: ConflictTarget<'a>) -> SQL<'a, PostgresValue<'a>> {
match target {
ConflictTarget::Columns(columns) => {
SQL::raw(" (").append(columns).append(SQL::raw(")"))
}
ConflictTarget::ColumnsWhere {
columns,
where_clause,
} => SQL::raw(" (")
.append(columns)
.append(SQL::raw(") WHERE "))
.append(where_clause),
ConflictTarget::Constraint(constraint_name) => {
SQL::raw(" ON CONSTRAINT ").append(SQL::raw(constraint_name))
}
}
}
pub fn on_conflict_do_nothing(self) -> InsertBuilder<'a, S, InsertOnConflictSet, T> {
self.on_conflict(Conflict::default())
}
pub fn on_conflict_do_nothing_on<C>(
self,
columns: C,
) -> InsertBuilder<'a, S, InsertOnConflictSet, T>
where
C: ToSQL<'a, PostgresValue<'a>>,
{
self.on_conflict(Conflict::do_nothing_on_columns(columns))
}
#[inline]
pub fn returning(
self,
columns: impl ToSQL<'a, PostgresValue<'a>>,
) -> InsertBuilder<'a, S, InsertReturningSet, T> {
let returning_sql = crate::helpers::returning(columns);
InsertBuilder {
sql: self.sql.append(returning_sql),
schema: PhantomData,
state: PhantomData,
table: PhantomData,
}
}
}
impl<'a, S, T> InsertBuilder<'a, S, InsertOnConflictSet, T> {
#[inline]
pub fn returning(
self,
columns: impl ToSQL<'a, PostgresValue<'a>>,
) -> InsertBuilder<'a, S, InsertReturningSet, T> {
let returning_sql = crate::helpers::returning(columns);
InsertBuilder {
sql: self.sql.append(returning_sql),
schema: PhantomData,
state: PhantomData,
table: PhantomData,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use drizzle_core::{SQL, ToSQL};
#[test]
fn test_insert_builder_creation() {
let builder = InsertBuilder::<(), InsertInitial, ()> {
sql: SQL::raw("INSERT INTO test"),
schema: PhantomData,
state: PhantomData,
table: PhantomData,
};
assert_eq!(builder.to_sql().sql(), "INSERT INTO test");
}
#[test]
fn test_conflict_types() {
let do_nothing = Conflict::DoNothing { target: None };
let do_update = Conflict::DoUpdate {
target: ConflictTarget::Columns(SQL::raw("id")),
set: SQL::raw("name = EXCLUDED.name"),
where_clause: None,
};
match do_nothing {
Conflict::DoNothing { .. } => (),
_ => panic!("Expected DoNothing"),
}
match do_update {
Conflict::DoUpdate { .. } => (),
_ => panic!("Expected DoUpdate"),
}
}
}