Skip to main content

toolu_orm_query/
insert.rs

1//! INSERT query builder with conflict handling (ON CONFLICT / OR REPLACE).
2
3use toolu_orm_core::dialect::Dialect;
4use toolu_orm_core::query_column::Column;
5use toolu_orm_core::value::Value;
6
7use crate::where_clause::cfg_single_backend;
8
9cfg_single_backend! {
10  use crate::exec_helpers::impl_execute;
11}
12
13// ── ConflictMode ──────────────────────────────────────────────────────────────
14
15enum ConflictMode {
16  None,
17  Replace,
18  Ignore,
19}
20
21// ── InsertBuilder ─────────────────────────────────────────────────────────────
22
23pub struct InsertBuilder {
24  table: String,
25  columns: Vec<String>,
26  values: Vec<Value>,
27  conflict_mode: ConflictMode,
28  conflict_cols: Vec<String>,
29}
30
31impl InsertBuilder {
32  pub fn new(table: &str) -> Self {
33    Self {
34      table: table.to_owned(),
35      columns: Vec::new(),
36      values: Vec::new(),
37      conflict_mode: ConflictMode::None,
38      conflict_cols: Vec::new(),
39    }
40  }
41
42  pub fn set<T>(mut self, col: &Column<T>, val: impl Into<Value>) -> Self {
43    self.columns.push(col.name.to_owned());
44    self.values.push(val.into());
45    self
46  }
47
48  pub fn set_null<T>(mut self, col: &Column<T>) -> Self {
49    self.columns.push(col.name.to_owned());
50    self.values.push(Value::Null);
51    self
52  }
53
54  pub fn or_replace(mut self) -> Self {
55    self.conflict_mode = ConflictMode::Replace;
56    self
57  }
58
59  pub fn or_ignore(mut self) -> Self {
60    self.conflict_mode = ConflictMode::Ignore;
61    self
62  }
63
64  /// Columns that form the `ON CONFLICT (...)` target for Postgres.
65  ///
66  /// Ignored for SQLite (`INSERT OR REPLACE` / `INSERT OR IGNORE`).
67  /// If unset, the first inserted column is used as the conflict target.
68  pub fn conflict_columns(mut self, cols: &[&str]) -> Self {
69    self.conflict_cols = cols.iter().map(|c| (*c).to_owned()).collect();
70    self
71  }
72
73  pub fn to_sql_for(&self, dialect: Dialect) -> (String, Vec<Value>) {
74    match dialect {
75      Dialect::Sqlite => self.to_sql_sqlite(),
76      Dialect::Postgres => self.to_sql_postgres(),
77    }
78  }
79
80  pub fn to_sql(&self) -> (String, Vec<Value>) {
81    self.to_sql_for(Dialect::CURRENT)
82  }
83
84  fn push_columns_and_values(&self, sql: &mut String, dialect: Dialect) {
85    let col_list: Vec<String> = self.columns.iter().map(|c| format!(r#""{c}""#)).collect();
86    sql.push_str(&format!(" ({}) VALUES (", col_list.join(", ")));
87    let placeholders: Vec<String> = (1..=self.columns.len()).map(|i| dialect.param(i)).collect();
88    sql.push_str(&placeholders.join(", "));
89    sql.push(')');
90  }
91
92  fn to_sql_sqlite(&self) -> (String, Vec<Value>) {
93    let mut sql = String::new();
94
95    let keyword = match self.conflict_mode {
96      ConflictMode::None => "INSERT INTO",
97      ConflictMode::Replace => "INSERT OR REPLACE INTO",
98      ConflictMode::Ignore => "INSERT OR IGNORE INTO",
99    };
100
101    sql.push_str(keyword);
102    sql.push_str(&format!(r#" "{}""#, self.table));
103    self.push_columns_and_values(&mut sql, Dialect::Sqlite);
104
105    (sql, self.values.clone())
106  }
107
108  fn to_sql_postgres(&self) -> (String, Vec<Value>) {
109    let mut sql = String::new();
110
111    sql.push_str(&format!(r#"INSERT INTO "{}""#, self.table));
112    self.push_columns_and_values(&mut sql, Dialect::Postgres);
113
114    match self.conflict_mode {
115      ConflictMode::None => {},
116      ConflictMode::Ignore => {
117        sql.push_str(" ON CONFLICT DO NOTHING");
118      },
119      ConflictMode::Replace => {
120        let conflict_target = if !self.conflict_cols.is_empty() {
121          self.conflict_cols.clone()
122        } else if let Some(first) = self.columns.first() {
123          vec![first.clone()]
124        } else {
125          Vec::new()
126        };
127
128        let conflict_cols_sql: Vec<String> = conflict_target
129          .iter()
130          .map(|c| format!(r#""{c}""#))
131          .collect();
132
133        let update_cols: Vec<String> = self
134          .columns
135          .iter()
136          .filter(|c| !conflict_target.iter().any(|t| t == *c))
137          .map(|c| format!(r#""{c}" = EXCLUDED."{c}""#))
138          .collect();
139
140        sql.push_str(&format!(
141          " ON CONFLICT ({}) DO ",
142          conflict_cols_sql.join(", ")
143        ));
144
145        if update_cols.is_empty() {
146          sql.push_str("NOTHING");
147        } else {
148          sql.push_str(&format!("UPDATE SET {}", update_cols.join(", ")));
149        }
150      },
151    }
152
153    (sql, self.values.clone())
154  }
155}
156
157cfg_single_backend! {
158  impl_execute!(InsertBuilder, "INSERT");
159}