1use std::collections::HashMap;
5
6use reifydb_value::value::Value;
7
8use crate::{
9 error::{ExportError, RenderError},
10 model::{NameResolver, ObjectRows},
11 render::{
12 layout::{EnumColumn, LayoutColumn},
13 value::render_value,
14 },
15};
16
17pub fn render_inserts(
18 qualified_object: &str,
19 rows: &ObjectRows,
20 batch_size: usize,
21 layout: &[LayoutColumn],
22 resolver: &NameResolver,
23) -> Result<String, ExportError> {
24 if rows.rows.is_empty() {
25 return Ok(String::new());
26 }
27
28 let index: HashMap<&str, usize> = rows.columns.iter().enumerate().map(|(i, name)| (name.as_str(), i)).collect();
29
30 let batch = batch_size.max(1);
31 let mut out = String::new();
32
33 for chunk in rows.rows.chunks(batch) {
34 out.push_str(&format!("INSERT {} [\n", qualified_object));
35 for (i, row) in chunk.iter().enumerate() {
36 out.push_str(" { ");
37 for (j, column) in layout.iter().enumerate() {
38 if j > 0 {
39 out.push_str(", ");
40 }
41 match column {
42 LayoutColumn::Plain(c) => {
43 let rendered = match cell(&index, row, &c.name) {
44 Some(value) => render_value(value).map_err(|e| {
45 map_value_error(e, qualified_object, &c.name)
46 })?,
47 None => "none".to_string(),
48 };
49 out.push_str(&format!("{}: {}", c.name, rendered));
50 }
51 LayoutColumn::Enum(e) => {
52 let rendered =
53 render_enum_value(e, resolver, &index, row, qualified_object)?;
54 out.push_str(&format!("{}: {}", e.logical_name, rendered));
55 }
56 }
57 }
58 out.push_str(" }");
59 if i + 1 < chunk.len() {
60 out.push(',');
61 }
62 out.push('\n');
63 }
64 out.push_str("];\n");
65 }
66
67 Ok(out)
68}
69
70fn cell<'a>(index: &HashMap<&str, usize>, row: &'a [Value], name: &str) -> Option<&'a Value> {
71 index.get(name).and_then(|&i| row.get(i))
72}
73
74fn render_enum_value(
75 column: &EnumColumn,
76 resolver: &NameResolver,
77 index: &HashMap<&str, usize>,
78 row: &[Value],
79 object: &str,
80) -> Result<String, ExportError> {
81 let qualified = &resolver
82 .sumtype(column.sumtype_id)
83 .ok_or_else(|| ExportError::UnresolvedReference {
84 kind: "sumtype",
85 id: column.sumtype_id,
86 object: object.to_string(),
87 })?
88 .qualified_name;
89
90 let tag = match cell(index, row, &column.tag_column) {
91 None
92 | Some(Value::None {
93 ..
94 }) => return Ok("none".to_string()),
95 Some(Value::Uint1(t)) => *t,
96 Some(_) => {
97 return Err(ExportError::UnsupportedValue {
98 object: object.to_string(),
99 column: column.logical_name.clone(),
100 value_type: "enum tag (expected uint1)".to_string(),
101 });
102 }
103 };
104
105 let variant =
106 column.variants.iter().find(|v| v.tag == tag).ok_or_else(|| ExportError::UnresolvedReference {
107 kind: "sumtype variant",
108 id: tag as u64,
109 object: object.to_string(),
110 })?;
111
112 if variant.fields.is_empty() {
113 return Ok(format!("{}::{}", qualified, variant.name));
114 }
115
116 let mut parts = Vec::with_capacity(variant.fields.len());
117 for (field_name, physical) in &variant.fields {
118 let rendered = match cell(index, row, physical) {
119 Some(value) => render_value(value).map_err(|e| map_value_error(e, object, field_name))?,
120 None => "none".to_string(),
121 };
122 parts.push(format!("{}: {}", field_name, rendered));
123 }
124 Ok(format!("{}::{} {{ {} }}", qualified, variant.name, parts.join(", ")))
125}
126
127fn map_value_error(error: RenderError, object: &str, column: &str) -> ExportError {
128 match error {
129 RenderError::UnrepresentableText => ExportError::UnrepresentableText {
130 object: object.to_string(),
131 column: column.to_string(),
132 },
133 RenderError::NonFiniteFloat => ExportError::NonFiniteFloat {
134 object: object.to_string(),
135 column: column.to_string(),
136 },
137 RenderError::Unsupported(value_type) => ExportError::UnsupportedValue {
138 object: object.to_string(),
139 column: column.to_string(),
140 value_type: value_type.to_string(),
141 },
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use reifydb_core::interface::catalog::{
148 column::{Column, ColumnIndex},
149 id::ColumnId,
150 };
151 use reifydb_value::value::{constraint::TypeConstraint, value_type::ValueType};
152
153 use super::*;
154 use crate::{model::ResolvedSumType, render::layout::EnumVariant};
155
156 fn rows() -> ObjectRows {
157 ObjectRows {
158 columns: vec!["id".to_string(), "name".to_string()],
159 rows: vec![
160 vec![Value::Int4(1), Value::Utf8("Alice".to_string())],
161 vec![Value::Int4(2), Value::Utf8("Bob".to_string())],
162 ],
163 }
164 }
165
166 fn plain(names: &[&str]) -> Vec<Column> {
167 names.iter()
168 .enumerate()
169 .map(|(i, n)| Column {
170 id: ColumnId(i as u64),
171 name: n.to_string(),
172 constraint: TypeConstraint::unconstrained(ValueType::Utf8),
173 properties: vec![],
174 index: ColumnIndex(i as u8),
175 auto_increment: false,
176 dictionary_id: None,
177 })
178 .collect()
179 }
180
181 fn layout(columns: &[Column]) -> Vec<LayoutColumn<'_>> {
182 columns.iter().map(LayoutColumn::Plain).collect()
183 }
184
185 #[test]
186 fn renders_batched_record_inserts() {
187 let cols = plain(&["id", "name"]);
188 let out = render_inserts("test::users", &rows(), 500, &layout(&cols), &NameResolver::empty()).unwrap();
189 assert_eq!(out, "INSERT test::users [\n { id: 1, name: 'Alice' },\n { id: 2, name: 'Bob' }\n];\n");
190 }
191
192 #[test]
193 fn splits_into_batches() {
194 let cols = plain(&["id", "name"]);
195 let out = render_inserts("test::users", &rows(), 1, &layout(&cols), &NameResolver::empty()).unwrap();
196 let inserts = out.matches("INSERT test::users").count();
197 assert_eq!(inserts, 2);
198 }
199
200 #[test]
201 fn empty_rows_produce_nothing() {
202 let empty = ObjectRows {
203 columns: vec!["id".to_string()],
204 rows: vec![],
205 };
206 let cols = plain(&["id"]);
207 assert_eq!(
208 render_inserts("test::users", &empty, 500, &layout(&cols), &NameResolver::empty()).unwrap(),
209 ""
210 );
211 }
212
213 #[test]
214 fn unrepresentable_value_fails_loud_with_location() {
215 let bad = ObjectRows {
216 columns: vec!["note".to_string()],
217 rows: vec![vec![Value::Utf8("both ' and \"".to_string())]],
218 };
219 let cols = plain(&["note"]);
220 assert_eq!(
221 render_inserts("test::notes", &bad, 500, &layout(&cols), &NameResolver::empty()),
222 Err(ExportError::UnrepresentableText {
223 object: "test::notes".to_string(),
224 column: "note".to_string()
225 })
226 );
227 }
228
229 fn resolver_with(id: u64, qualified: &str) -> NameResolver {
230 let mut r = NameResolver::empty();
231 r.sumtypes.insert(
232 id,
233 ResolvedSumType {
234 qualified_name: qualified.to_string(),
235 variants: vec![],
236 },
237 );
238 r
239 }
240
241 #[test]
242 fn reconstructs_unit_enum_value_from_tag() {
243 let id_cols = plain(&["id"]);
244 let enum_col = EnumColumn {
245 logical_name: "state".to_string(),
246 sumtype_id: 9,
247 tag_column: "state_tag".to_string(),
248 variants: vec![
249 EnumVariant {
250 tag: 0,
251 name: "active".to_string(),
252 fields: vec![],
253 },
254 EnumVariant {
255 tag: 1,
256 name: "inactive".to_string(),
257 fields: vec![],
258 },
259 ],
260 };
261 let layout = vec![LayoutColumn::Plain(&id_cols[0]), LayoutColumn::Enum(enum_col)];
262 let rows = ObjectRows {
263 columns: vec!["id".to_string(), "state_tag".to_string()],
264 rows: vec![vec![Value::Int4(1), Value::Uint1(0)], vec![Value::Int4(2), Value::Uint1(1)]],
265 };
266 let out =
267 render_inserts("shop::items", &rows, 500, &layout, &resolver_with(9, "shop::status")).unwrap();
268 assert_eq!(
269 out,
270 "INSERT shop::items [\n { id: 1, state: shop::status::active },\n { id: 2, state: shop::status::inactive }\n];\n"
271 );
272 }
273
274 #[test]
275 fn reconstructs_structured_enum_value_from_tag_and_fields() {
276 let id_cols = plain(&["id"]);
277 let enum_col = EnumColumn {
278 logical_name: "shape".to_string(),
279 sumtype_id: 9,
280 tag_column: "shape_tag".to_string(),
281 variants: vec![
282 EnumVariant {
283 tag: 0,
284 name: "circle".to_string(),
285 fields: vec![("radius".to_string(), "shape_circle_radius".to_string())],
286 },
287 EnumVariant {
288 tag: 1,
289 name: "rectangle".to_string(),
290 fields: vec![
291 ("width".to_string(), "shape_rectangle_width".to_string()),
292 ("height".to_string(), "shape_rectangle_height".to_string()),
293 ],
294 },
295 ],
296 };
297 let layout = vec![LayoutColumn::Plain(&id_cols[0]), LayoutColumn::Enum(enum_col)];
298 let rows = ObjectRows {
299 columns: vec![
300 "id".to_string(),
301 "shape_tag".to_string(),
302 "shape_circle_radius".to_string(),
303 "shape_rectangle_width".to_string(),
304 "shape_rectangle_height".to_string(),
305 ],
306 rows: vec![
307 vec![Value::Int4(1), Value::Uint1(0), Value::float8(5.0), Value::none(), Value::none()],
308 vec![
309 Value::Int4(2),
310 Value::Uint1(1),
311 Value::none(),
312 Value::float8(3.0),
313 Value::float8(4.0),
314 ],
315 ],
316 };
317 let out =
318 render_inserts("shop::shapes", &rows, 500, &layout, &resolver_with(9, "shop::shape")).unwrap();
319 assert_eq!(
320 out,
321 "INSERT shop::shapes [\n { id: 1, shape: shop::shape::circle { radius: 5 } },\n { id: 2, shape: shop::shape::rectangle { width: 3, height: 4 } }\n];\n"
322 );
323 }
324}