Skip to content

Commit e6b41f9

Browse files
Merge pull request #12 from atopio/feat/insert-statement
Added: Complete insert statement
2 parents c1f3257 + 321f1f7 commit e6b41f9

File tree

6 files changed

+864
-1
lines changed

6 files changed

+864
-1
lines changed

src/builders/insert.rs

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
use crate::{
2+
enums::ReturnClause,
3+
internal_macros::push_clause,
4+
types::{
5+
create::SetField,
6+
insert::{InsertContent, InsertData},
7+
},
8+
};
9+
use std::fmt::Write;
10+
11+
pub struct InsertBuilder {
12+
pub data: InsertData,
13+
}
14+
15+
impl InsertBuilder {
16+
/// Adds the `RELATION` keyword to the INSERT statement.
17+
///
18+
/// Produces `INSERT RELATION ... INTO @what`.
19+
///
20+
/// # Example
21+
/// ```
22+
/// # use surrealex::QueryBuilder;
23+
/// let sql = QueryBuilder::insert("knows")
24+
/// .relation()
25+
/// .content("{ in: person:tobie, out: person:jaime, since: '2024-01-01' }")
26+
/// .build();
27+
/// assert_eq!(sql, "INSERT RELATION INTO knows { in: person:tobie, out: person:jaime, since: '2024-01-01' }");
28+
/// ```
29+
pub fn relation(mut self) -> Self {
30+
self.data.relation = true;
31+
self
32+
}
33+
34+
/// Adds the `IGNORE` keyword to the INSERT statement.
35+
///
36+
/// Produces `INSERT IGNORE INTO @what` (or `INSERT RELATION IGNORE INTO @what`).
37+
///
38+
/// # Example
39+
/// ```
40+
/// # use surrealex::QueryBuilder;
41+
/// let sql = QueryBuilder::insert("person")
42+
/// .ignore()
43+
/// .content("{ id: 'tobie', name: 'Tobie' }")
44+
/// .build();
45+
/// assert_eq!(sql, "INSERT IGNORE INTO person { id: 'tobie', name: 'Tobie' }");
46+
/// ```
47+
pub fn ignore(mut self) -> Self {
48+
self.data.ignore = true;
49+
self
50+
}
51+
52+
/// Sets the data-providing mode to a raw value expression (`@value`).
53+
///
54+
/// This replaces any previous `content` or `fields_values` clause.
55+
///
56+
/// # Example
57+
/// ```
58+
/// # use surrealex::QueryBuilder;
59+
/// let sql = QueryBuilder::insert("person")
60+
/// .content("{ name: 'Tobie', company: 'SurrealDB' }")
61+
/// .build();
62+
/// assert_eq!(sql, "INSERT INTO person { name: 'Tobie', company: 'SurrealDB' }");
63+
/// ```
64+
pub fn content(mut self, value: &str) -> Self {
65+
self.data.content = Some(InsertContent::Value(value.to_string()));
66+
self
67+
}
68+
69+
/// Sets the fields for the `(@fields) VALUES (@values)` form.
70+
///
71+
/// This replaces any previous content clause. Call `.values()` afterwards
72+
/// to add one or more value tuples.
73+
///
74+
/// # Example
75+
/// ```
76+
/// # use surrealex::QueryBuilder;
77+
/// let sql = QueryBuilder::insert("person")
78+
/// .fields(vec!["name", "age"])
79+
/// .values(vec!["'Tobie'", "42"])
80+
/// .build();
81+
/// assert_eq!(sql, "INSERT INTO person (name, age) VALUES ('Tobie', 42)");
82+
/// ```
83+
pub fn fields<S: Into<String>>(mut self, fields: Vec<S>) -> Self {
84+
let fields: Vec<String> = fields.into_iter().map(|s| s.into()).collect();
85+
match &mut self.data.content {
86+
Some(InsertContent::FieldsValues {
87+
fields: existing_fields,
88+
..
89+
}) => {
90+
*existing_fields = fields;
91+
}
92+
_ => {
93+
self.data.content = Some(InsertContent::FieldsValues {
94+
fields,
95+
values: Vec::new(),
96+
});
97+
}
98+
}
99+
self
100+
}
101+
102+
/// Adds a row of values for the `(@fields) VALUES (@values)` form.
103+
///
104+
/// Multiple calls accumulate additional value tuples. If no `fields` have
105+
/// been set yet, this will initialise a `FieldsValues` content with empty fields.
106+
///
107+
/// # Example
108+
/// ```
109+
/// # use surrealex::QueryBuilder;
110+
/// let sql = QueryBuilder::insert("person")
111+
/// .fields(vec!["name", "age"])
112+
/// .values(vec!["'Tobie'", "42"])
113+
/// .values(vec!["'Jaime'", "35"])
114+
/// .build();
115+
/// assert_eq!(sql, "INSERT INTO person (name, age) VALUES ('Tobie', 42), ('Jaime', 35)");
116+
/// ```
117+
pub fn values<S: Into<String>>(mut self, row: Vec<S>) -> Self {
118+
let row: Vec<String> = row.into_iter().map(|s| s.into()).collect();
119+
match &mut self.data.content {
120+
Some(InsertContent::FieldsValues { values, .. }) => {
121+
values.push(row);
122+
}
123+
_ => {
124+
self.data.content = Some(InsertContent::FieldsValues {
125+
fields: Vec::new(),
126+
values: vec![row],
127+
});
128+
}
129+
}
130+
self
131+
}
132+
133+
/// Adds a `field = value` pair to the `ON DUPLICATE KEY UPDATE` clause.
134+
///
135+
/// Multiple calls accumulate assignments.
136+
///
137+
/// # Example
138+
/// ```
139+
/// # use surrealex::QueryBuilder;
140+
/// let sql = QueryBuilder::insert("person")
141+
/// .fields(vec!["name", "age"])
142+
/// .values(vec!["'Tobie'", "42"])
143+
/// .on_duplicate_key_update("age", "42")
144+
/// .build();
145+
/// assert_eq!(sql, "INSERT INTO person (name, age) VALUES ('Tobie', 42) ON DUPLICATE KEY UPDATE age = 42");
146+
/// ```
147+
pub fn on_duplicate_key_update(mut self, field: &str, value: &str) -> Self {
148+
self.data.on_duplicate_key_update.push(SetField {
149+
field: field.to_string(),
150+
value: value.to_string(),
151+
});
152+
self
153+
}
154+
155+
/// Sets the RETURN clause to `RETURN NONE`.
156+
pub fn return_none(mut self) -> Self {
157+
self.data.return_clause = Some(ReturnClause::None);
158+
self
159+
}
160+
161+
/// Sets the RETURN clause to `RETURN BEFORE`.
162+
pub fn return_before(mut self) -> Self {
163+
self.data.return_clause = Some(ReturnClause::Before);
164+
self
165+
}
166+
167+
/// Sets the RETURN clause to `RETURN AFTER`.
168+
pub fn return_after(mut self) -> Self {
169+
self.data.return_clause = Some(ReturnClause::After);
170+
self
171+
}
172+
173+
/// Sets the RETURN clause to `RETURN DIFF`.
174+
pub fn return_diff(mut self) -> Self {
175+
self.data.return_clause = Some(ReturnClause::Diff);
176+
self
177+
}
178+
179+
/// Sets the RETURN clause to `RETURN <param1>, <param2>, ...`.
180+
///
181+
/// # Example
182+
/// ```
183+
/// # use surrealex::QueryBuilder;
184+
/// let sql = QueryBuilder::insert("person")
185+
/// .content("{ name: 'Tobie' }")
186+
/// .return_params(vec!["name", "id"])
187+
/// .build();
188+
/// assert_eq!(sql, "INSERT INTO person { name: 'Tobie' } RETURN name, id");
189+
/// ```
190+
pub fn return_params<S: Into<String>>(mut self, params: Vec<S>) -> Self {
191+
self.data.return_clause = Some(ReturnClause::Params(
192+
params.into_iter().map(|s| s.into()).collect(),
193+
));
194+
self
195+
}
196+
197+
/// Sets the RETURN clause to `RETURN VALUE <field>`.
198+
///
199+
/// # Example
200+
/// ```
201+
/// # use surrealex::QueryBuilder;
202+
/// let sql = QueryBuilder::insert("person")
203+
/// .content("{ name: 'Tobie' }")
204+
/// .return_value("name")
205+
/// .build();
206+
/// assert_eq!(sql, "INSERT INTO person { name: 'Tobie' } RETURN VALUE name");
207+
/// ```
208+
pub fn return_value(mut self, field: &str) -> Self {
209+
self.data.return_clause = Some(ReturnClause::Value(field.to_string()));
210+
self
211+
}
212+
213+
/// Builds the final INSERT query string.
214+
pub fn build(self) -> String {
215+
let mut query = String::with_capacity(128);
216+
let target = &self.data.target;
217+
218+
// INSERT [ RELATION ] [ IGNORE ] INTO @what
219+
match (self.data.relation, self.data.ignore) {
220+
(true, true) => push_clause!(query, "INSERT RELATION IGNORE INTO {target}"),
221+
(true, false) => push_clause!(query, "INSERT RELATION INTO {target}"),
222+
(false, true) => push_clause!(query, "INSERT IGNORE INTO {target}"),
223+
(false, false) => push_clause!(query, "INSERT INTO {target}"),
224+
}
225+
226+
// [ @value | (@fields) VALUES (@values) ]
227+
if let Some(ref content) = self.data.content {
228+
match content {
229+
InsertContent::Value(value) => {
230+
push_clause!(query, "{value}");
231+
}
232+
InsertContent::FieldsValues { fields, values } => {
233+
if !fields.is_empty() {
234+
let fields_str = fields.join(", ");
235+
push_clause!(query, "({fields_str})");
236+
}
237+
if !values.is_empty() {
238+
let value_tuples: String = values
239+
.iter()
240+
.map(|row| {
241+
let row_str = row.join(", ");
242+
format!("({row_str})")
243+
})
244+
.collect::<Vec<String>>()
245+
.join(", ");
246+
push_clause!(query, "VALUES {value_tuples}");
247+
}
248+
}
249+
}
250+
}
251+
252+
// [ ON DUPLICATE KEY UPDATE @field = @value ... ]
253+
if !self.data.on_duplicate_key_update.is_empty() {
254+
let assignments: String = self
255+
.data
256+
.on_duplicate_key_update
257+
.iter()
258+
.map(|f| format!("{} = {}", f.field, f.value))
259+
.collect::<Vec<String>>()
260+
.join(", ");
261+
push_clause!(query, "ON DUPLICATE KEY UPDATE {assignments}");
262+
}
263+
264+
// [ RETURN ... ]
265+
if let Some(ref rc) = self.data.return_clause {
266+
push_clause!(query, "RETURN {rc}");
267+
}
268+
269+
query
270+
}
271+
}

src/builders/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod create;
22
pub mod delete;
3+
pub mod insert;
34
pub mod select;

src/lib.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@ pub mod versioning;
1212
pub use crate::versioning::{SurrealV1, SurrealV2};
1313

1414
use crate::{
15-
builders::{create::CreateBuilder, delete::DeleteBuilder, select::SelectBuilder},
15+
builders::{
16+
create::CreateBuilder, delete::DeleteBuilder, insert::InsertBuilder, select::SelectBuilder,
17+
},
1618
enums::SelectionFields,
1719
types::{
1820
create::CreateData,
1921
delete::DeleteData,
22+
insert::InsertData,
2023
select::{SelectData, SelectField},
2124
},
2225
versioning::select::VersionedSelect,
@@ -59,6 +62,14 @@ impl QueryBuilder {
5962
CreateBuilder { data }
6063
}
6164

65+
pub fn insert(target: &str) -> InsertBuilder {
66+
let data = InsertData {
67+
target: target.to_string(),
68+
..Default::default()
69+
};
70+
InsertBuilder { data }
71+
}
72+
6273
/// Create a version-aware query builder.
6374
///
6475
/// Use this to target a specific SurrealDB version for query rendering.
@@ -122,4 +133,12 @@ impl<V> VersionedQueryBuilder<V> {
122133
};
123134
CreateBuilder { data }
124135
}
136+
137+
pub fn insert(self, target: &str) -> InsertBuilder {
138+
let data = InsertData {
139+
target: target.to_string(),
140+
..Default::default()
141+
};
142+
InsertBuilder { data }
143+
}
125144
}

src/types/insert.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use crate::enums::ReturnClause;
2+
use crate::types::create::SetField;
3+
4+
/// Represents the data-providing mode for an INSERT statement.
5+
///
6+
/// SurrealQL supports two ways to provide data in an INSERT:
7+
/// - `@value` — a raw JSON/SurrealQL object or array of objects
8+
/// - `(@fields) VALUES (@values), ...` — explicit fields and value tuples
9+
#[derive(Debug, Clone)]
10+
pub enum InsertContent {
11+
/// A raw value expression (e.g., `{ name: 'Tobie', age: 30 }` or
12+
/// `[{ name: 'Tobie' }, { name: 'Jaime' }]`).
13+
Value(String),
14+
/// Explicit `(@fields) VALUES (@values), ...` form.
15+
FieldsValues {
16+
/// The field names (e.g., `["name", "age"]`).
17+
fields: Vec<String>,
18+
/// One or more value tuples. Each inner `Vec` corresponds to one row
19+
/// and must have the same length as `fields`.
20+
values: Vec<Vec<String>>,
21+
},
22+
}
23+
24+
/// Holds all the data needed to build an INSERT statement.
25+
#[derive(Default, Debug, Clone)]
26+
pub struct InsertData {
27+
/// The target table or record id (e.g., `"person"`, `"person:tobie"`).
28+
pub target: String,
29+
/// When `true`, emits `INSERT RELATION` instead of `INSERT`.
30+
pub relation: bool,
31+
/// When `true`, emits `IGNORE` after `INSERT [RELATION]`.
32+
pub ignore: bool,
33+
/// Optional data content (`@value` or `(@fields) VALUES (@values)`).
34+
pub content: Option<InsertContent>,
35+
/// Optional `ON DUPLICATE KEY UPDATE` assignments.
36+
pub on_duplicate_key_update: Vec<SetField>,
37+
/// Optional RETURN clause (`RETURN NONE | BEFORE | AFTER | DIFF | <params> | VALUE <param>`).
38+
pub return_clause: Option<ReturnClause>,
39+
}

src/types/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod create;
22
pub mod delete;
3+
pub mod insert;
34
pub mod select;

0 commit comments

Comments
 (0)