use std::fmt::Write;
use crate::error::FlozError;
use crate::value::Value;
use super::quote_table;
#[derive(Debug, Clone)]
pub struct InsertQuery {
table: String,
columns: Vec<String>,
values: Vec<Value>,
returning: ReturningClause,
on_conflict: Option<OnConflict>,
}
#[derive(Debug, Clone)]
enum ReturningClause {
None,
All, Cols(Vec<String>), }
#[derive(Debug, Clone)]
struct OnConflict {
column: String,
updates: Vec<(String, Value)>,
}
impl InsertQuery {
pub fn new(table: &str) -> Self {
Self {
table: table.to_string(),
columns: Vec::new(),
values: Vec::new(),
returning: ReturningClause::None,
on_conflict: None,
}
}
pub fn set(mut self, column: &str, value: impl Into<Value>) -> Self {
self.columns.push(column.to_string());
self.values.push(value.into());
self
}
pub fn returning_all(mut self) -> Self {
self.returning = ReturningClause::All;
self
}
pub fn returning_col(mut self, column: &str) -> Self {
self.returning = ReturningClause::Cols(vec![column.to_string()]);
self
}
pub fn returning_cols(mut self, columns: &[&str]) -> Self {
self.returning = ReturningClause::Cols(
columns.iter().map(|c| c.to_string()).collect(),
);
self
}
pub fn on_conflict(mut self, column: &str) -> Self {
self.on_conflict = Some(OnConflict {
column: column.to_string(),
updates: Vec::new(),
});
self
}
pub fn do_update(mut self, column: &str, value: impl Into<Value>) -> Self {
if let Some(ref mut oc) = self.on_conflict {
oc.updates.push((column.to_string(), value.into()));
}
self
}
pub fn to_sql(&self) -> (String, Vec<Value>) {
let mut sql = String::with_capacity(128);
let mut params = Vec::new();
let mut idx = 0usize;
write!(sql, "INSERT INTO {} (", quote_table(&self.table)).unwrap();
for (i, col) in self.columns.iter().enumerate() {
if i > 0 { sql.push_str(", "); }
sql.push_str(col);
}
sql.push_str(") VALUES (");
for (i, val) in self.values.iter().enumerate() {
if i > 0 { sql.push_str(", "); }
idx += 1;
write!(sql, "${}", idx).unwrap();
params.push(val.clone());
}
sql.push(')');
if let Some(ref oc) = self.on_conflict {
write!(sql, " ON CONFLICT ({}) DO UPDATE SET ", oc.column).unwrap();
for (i, (col, val)) in oc.updates.iter().enumerate() {
if i > 0 { sql.push_str(", "); }
idx += 1;
write!(sql, "{} = ${}", col, idx).unwrap();
params.push(val.clone());
}
}
match &self.returning {
ReturningClause::None => {}
ReturningClause::All => sql.push_str(" RETURNING *"),
ReturningClause::Cols(cols) => {
sql.push_str(" RETURNING ");
for (i, col) in cols.iter().enumerate() {
if i > 0 { sql.push_str(", "); }
sql.push_str(col);
}
}
}
(sql, params)
}
}
#[derive(Debug, Clone)]
pub struct InsertManyQuery {
table: String,
rows: Vec<Vec<(String, Value)>>,
returning: ReturningClause,
}
impl InsertManyQuery {
pub fn new(table: &str) -> Self {
Self {
table: table.to_string(),
rows: Vec::new(),
returning: ReturningClause::None,
}
}
pub fn row(mut self, fields: Vec<(String, Value)>) -> Self {
self.rows.push(fields);
self
}
pub fn rows(mut self, rows: Vec<Vec<(String, Value)>>) -> Self {
self.rows = rows;
self
}
pub fn returning_all(mut self) -> Self {
self.returning = ReturningClause::All;
self
}
pub fn to_sql(&self) -> Result<(String, Vec<Value>), FlozError> {
if self.rows.is_empty() {
let sql = format!("INSERT INTO {} DEFAULT VALUES", quote_table(&self.table));
return Ok((sql, Vec::new()));
}
let first_row = &self.rows[0];
let columns: Vec<&str> = first_row.iter().map(|(c, _)| c.as_str()).collect();
let col_count = columns.len();
for (row_idx, row) in self.rows.iter().enumerate().skip(1) {
if row.len() != col_count {
let row_cols: Vec<&str> = row.iter().map(|(c, _)| c.as_str()).collect();
for expected in &columns {
if !row_cols.contains(expected) {
return Err(FlozError::MismatchedBulkInsertColumns {
row: row_idx,
column: expected.to_string(),
});
}
}
return Err(FlozError::MismatchedBulkInsertColumns {
row: row_idx,
column: format!("row has {} columns, expected {}", row.len(), col_count),
});
}
}
let mut sql = String::with_capacity(128);
let mut params = Vec::new();
let mut idx = 0usize;
write!(sql, "INSERT INTO {} (", quote_table(&self.table)).unwrap();
for (i, col) in columns.iter().enumerate() {
if i > 0 { sql.push_str(", "); }
sql.push_str(col);
}
sql.push_str(") VALUES ");
for (row_idx, row) in self.rows.iter().enumerate() {
if row_idx > 0 { sql.push_str(", "); }
sql.push('(');
for (i, (_, val)) in row.iter().enumerate() {
if i > 0 { sql.push_str(", "); }
idx += 1;
write!(sql, "${}", idx).unwrap();
params.push(val.clone());
}
sql.push(')');
}
match &self.returning {
ReturningClause::None => {}
ReturningClause::All => sql.push_str(" RETURNING *"),
ReturningClause::Cols(cols) => {
sql.push_str(" RETURNING ");
for (i, col) in cols.iter().enumerate() {
if i > 0 { sql.push_str(", "); }
sql.push_str(col);
}
}
}
Ok((sql, params))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn insert_single_column() {
let (sql, params) = InsertQuery::new("users")
.set("name", "Alice")
.to_sql();
assert_eq!(sql, "INSERT INTO users (name) VALUES ($1)");
assert_eq!(params, vec![Value::String("Alice".into())]);
}
#[test]
fn insert_multiple_columns() {
let (sql, params) = InsertQuery::new("users")
.set("name", "Alice")
.set("age", 30i16)
.to_sql();
assert_eq!(sql, "INSERT INTO users (name, age) VALUES ($1, $2)");
assert_eq!(params, vec![
Value::String("Alice".into()),
Value::Short(30),
]);
}
#[test]
fn insert_returning_all() {
let (sql, _) = InsertQuery::new("users")
.set("name", "Alice")
.returning_all()
.to_sql();
assert!(sql.ends_with("RETURNING *"));
}
#[test]
fn insert_returning_col() {
let (sql, _) = InsertQuery::new("users")
.set("name", "Alice")
.returning_col("id")
.to_sql();
assert!(sql.ends_with("RETURNING id"));
}
#[test]
fn insert_returning_cols() {
let (sql, _) = InsertQuery::new("users")
.set("name", "Alice")
.returning_cols(&["id", "created_at"])
.to_sql();
assert!(sql.ends_with("RETURNING id, created_at"));
}
#[test]
fn insert_on_conflict() {
let (sql, params) = InsertQuery::new("users")
.set("name", "Alice")
.set("age", 30i16)
.on_conflict("name")
.do_update("age", 31i16)
.to_sql();
assert_eq!(
sql,
"INSERT INTO users (name, age) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET age = $3"
);
assert_eq!(params.len(), 3);
}
#[test]
fn insert_on_conflict_multiple_updates() {
let (sql, params) = InsertQuery::new("users")
.set("name", "Alice")
.set("age", 30i16)
.on_conflict("name")
.do_update("age", 31i16)
.do_update("name", "Alice Updated")
.to_sql();
assert_eq!(
sql,
"INSERT INTO users (name, age) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET age = $3, name = $4"
);
assert_eq!(params.len(), 4);
}
#[test]
fn insert_schema_qualified() {
let (sql, _) = InsertQuery::new("auth.users")
.set("name", "Alice")
.to_sql();
assert!(sql.starts_with("INSERT INTO \"auth\".\"users\""));
}
#[test]
fn insert_many_basic() {
let (sql, params) = InsertManyQuery::new("users")
.row(vec![
("name".into(), Value::String("Alice".into())),
("age".into(), Value::Short(30)),
])
.row(vec![
("name".into(), Value::String("Bob".into())),
("age".into(), Value::Short(25)),
])
.to_sql()
.unwrap();
assert_eq!(
sql,
"INSERT INTO users (name, age) VALUES ($1, $2), ($3, $4)"
);
assert_eq!(params.len(), 4);
}
#[test]
fn insert_many_single_row() {
let (sql, params) = InsertManyQuery::new("users")
.row(vec![
("name".into(), Value::String("Alice".into())),
])
.to_sql()
.unwrap();
assert_eq!(sql, "INSERT INTO users (name) VALUES ($1)");
assert_eq!(params.len(), 1);
}
#[test]
fn insert_many_returning() {
let (sql, _) = InsertManyQuery::new("users")
.row(vec![("name".into(), Value::String("A".into()))])
.row(vec![("name".into(), Value::String("B".into()))])
.returning_all()
.to_sql()
.unwrap();
assert!(sql.ends_with("RETURNING *"));
}
#[test]
fn insert_many_ragged_rows_error() {
let result = InsertManyQuery::new("users")
.row(vec![
("name".into(), Value::String("Alice".into())),
("age".into(), Value::Short(30)),
])
.row(vec![
("name".into(), Value::String("Bob".into())),
])
.to_sql();
assert!(result.is_err());
match result {
Err(FlozError::MismatchedBulkInsertColumns { row, column }) => {
assert_eq!(row, 1);
assert_eq!(column, "age");
}
_ => panic!("Expected MismatchedBulkInsertColumns"),
}
}
#[test]
fn insert_many_empty() {
let (sql, params) = InsertManyQuery::new("users")
.to_sql()
.unwrap();
assert_eq!(sql, "INSERT INTO users DEFAULT VALUES");
assert!(params.is_empty());
}
#[test]
fn insert_many_three_rows() {
let (sql, params) = InsertManyQuery::new("users")
.row(vec![("x".into(), Value::Int(1))])
.row(vec![("x".into(), Value::Int(2))])
.row(vec![("x".into(), Value::Int(3))])
.to_sql()
.unwrap();
assert_eq!(sql, "INSERT INTO users (x) VALUES ($1), ($2), ($3)");
assert_eq!(params.len(), 3);
}
}