1use alloc::string::String;
25use alloc::vec::Vec;
26use core::fmt::{Debug, Write};
27use core::hash::Hash;
28
29use crate::builders::operation::Operation;
30use crate::builders::{ChangesetFormat, DiffSetBuilder, PatchsetFormat};
31use crate::encoding::{MaybeValue, Value};
32use crate::schema::NamedColumns;
33
34type ChangesetUpdatePairs<S, B> = [(MaybeValue<S, B>, MaybeValue<S, B>)];
36
37fn quote_identifier(name: &str) -> String {
41 let mut out = String::with_capacity(name.len() + 2);
42 out.push('"');
43 for c in name.chars() {
44 if c == '"' {
45 out.push_str("\"\"");
46 } else {
47 out.push(c);
48 }
49 }
50 out.push('"');
51 out
52}
53
54pub trait ColumnNames: NamedColumns {
58 fn column_name(&self, index: usize) -> Option<&str>;
62
63 fn pk_indices(&self) -> Vec<usize> {
65 let mut pk_cols: Vec<(usize, usize)> = Vec::new();
67 for col_idx in 0..self.number_of_columns() {
68 if let Some(pk_ordinal) = self.primary_key_index(col_idx) {
69 pk_cols.push((pk_ordinal, col_idx));
70 }
71 }
72 pk_cols.sort_by_key(|(ordinal, _)| *ordinal);
73 pk_cols.into_iter().map(|(_, col_idx)| col_idx).collect()
74 }
75}
76
77impl ColumnNames for crate::SimpleTable {
78 fn column_name(&self, index: usize) -> Option<&str> {
79 self.column_name(index)
80 }
81
82 fn pk_indices(&self) -> Vec<usize> {
83 crate::SimpleTable::pk_indices(self)
85 }
86}
87
88fn format_insert<T: ColumnNames, S: AsRef<str>, B: AsRef<[u8]>>(
90 table: &T,
91 values: &[Value<S, B>],
92) -> String {
93 let mut sql = String::new();
94 write!(sql, "INSERT INTO {}", quote_identifier(table.name())).unwrap();
95
96 sql.push_str(" (");
98 for i in 0..table.number_of_columns() {
99 if i > 0 {
100 sql.push_str(", ");
101 }
102 if let Some(name) = table.column_name(i) {
103 sql.push_str("e_identifier(name));
104 } else {
105 write!(sql, "\"col{i}\"").unwrap();
106 }
107 }
108 sql.push_str(") VALUES (");
109
110 for (i, val) in values.iter().enumerate() {
112 if i > 0 {
113 sql.push_str(", ");
114 }
115 write!(sql, "{val}").unwrap();
116 }
117 sql.push(')');
118 sql
119}
120
121fn format_delete_changeset<T: ColumnNames, S: AsRef<str>, B: AsRef<[u8]>>(
123 table: &T,
124 values: &[Value<S, B>],
125) -> String {
126 let mut sql = String::new();
127 write!(sql, "DELETE FROM {}", quote_identifier(table.name())).unwrap();
128 sql.push_str(" WHERE ");
129
130 let mut first = true;
132 for (col_idx, value) in values.iter().enumerate() {
133 if table.primary_key_index(col_idx).is_some() {
134 if !first {
135 sql.push_str(" AND ");
136 }
137 first = false;
138 if let Some(name) = table.column_name(col_idx) {
139 sql.push_str("e_identifier(name));
140 } else {
141 write!(sql, "\"col{col_idx}\"").unwrap();
142 }
143 sql.push_str(" = ");
144 write!(sql, "{value}").unwrap();
145 }
146 }
147 sql
148}
149
150fn format_delete_patchset<T: ColumnNames, S: AsRef<str>, B: AsRef<[u8]>>(
152 table: &T,
153 pk: &[Value<S, B>],
154) -> String {
155 let mut sql = String::new();
156 write!(sql, "DELETE FROM {}", quote_identifier(table.name())).unwrap();
157 sql.push_str(" WHERE ");
158
159 let pk_indices = table.pk_indices();
161
162 let mut first = true;
163 for (pk_ordinal, &col_idx) in pk_indices.iter().enumerate() {
164 if !first {
165 sql.push_str(" AND ");
166 }
167 first = false;
168 if let Some(name) = table.column_name(col_idx) {
169 sql.push_str("e_identifier(name));
170 } else {
171 write!(sql, "\"col{col_idx}\"").unwrap();
172 }
173 sql.push_str(" = ");
174 write!(sql, "{}", pk[pk_ordinal]).unwrap();
175 }
176 sql
177}
178
179fn format_update_changeset<T: ColumnNames, S: AsRef<str>, B: AsRef<[u8]>>(
181 table: &T,
182 pairs: &ChangesetUpdatePairs<S, B>,
183) -> String {
184 let mut sql = String::new();
185 write!(sql, "UPDATE {}", quote_identifier(table.name())).unwrap();
186 sql.push_str(" SET ");
187
188 let mut first_set = true;
190 for (col_idx, (_old, new)) in pairs.iter().enumerate() {
191 if let Some(new_val) = new {
192 if table.primary_key_index(col_idx).is_some() {
194 continue;
195 }
196 if !first_set {
197 sql.push_str(", ");
198 }
199 first_set = false;
200 if let Some(name) = table.column_name(col_idx) {
201 sql.push_str("e_identifier(name));
202 } else {
203 write!(sql, "\"col{col_idx}\"").unwrap();
204 }
205 sql.push_str(" = ");
206 write!(sql, "{new_val}").unwrap();
207 }
208 }
209
210 sql.push_str(" WHERE ");
212 let mut first_where = true;
213 for (col_idx, (old, _new)) in pairs.iter().enumerate() {
214 if table.primary_key_index(col_idx).is_some() {
215 if let Some(old_val) = old {
216 if !first_where {
217 sql.push_str(" AND ");
218 }
219 first_where = false;
220 if let Some(name) = table.column_name(col_idx) {
221 sql.push_str("e_identifier(name));
222 } else {
223 write!(sql, "\"col{col_idx}\"").unwrap();
224 }
225 sql.push_str(" = ");
226 write!(sql, "{old_val}").unwrap();
227 }
228 }
229 }
230 sql
231}
232
233fn format_update_patchset<T: ColumnNames, S: AsRef<str>, B: AsRef<[u8]>>(
235 table: &T,
236 pk: &[Value<S, B>],
237 pairs: &[((), MaybeValue<S, B>)],
238) -> String {
239 let mut sql = String::new();
240 write!(sql, "UPDATE {}", quote_identifier(table.name())).unwrap();
241 sql.push_str(" SET ");
242
243 let mut first_set = true;
245 for (col_idx, ((), new)) in pairs.iter().enumerate() {
246 if let Some(new_val) = new {
247 if table.primary_key_index(col_idx).is_some() {
249 continue;
250 }
251 if !first_set {
252 sql.push_str(", ");
253 }
254 first_set = false;
255 if let Some(name) = table.column_name(col_idx) {
256 sql.push_str("e_identifier(name));
257 } else {
258 write!(sql, "\"col{col_idx}\"").unwrap();
259 }
260 sql.push_str(" = ");
261 write!(sql, "{new_val}").unwrap();
262 }
263 }
264
265 sql.push_str(" WHERE ");
267 let pk_indices = table.pk_indices();
268
269 let mut first_where = true;
270 for (pk_ordinal, &col_idx) in pk_indices.iter().enumerate() {
271 if !first_where {
272 sql.push_str(" AND ");
273 }
274 first_where = false;
275 if let Some(name) = table.column_name(col_idx) {
276 sql.push_str("e_identifier(name));
277 } else {
278 write!(sql, "\"col{col_idx}\"").unwrap();
279 }
280 sql.push_str(" = ");
281 write!(sql, "{}", pk[pk_ordinal]).unwrap();
282 }
283 sql
284}
285
286impl<
291 T: ColumnNames,
292 S: AsRef<str> + Clone + Debug + Hash + Eq,
293 B: AsRef<[u8]> + Clone + Debug + Hash + Eq,
294> DiffSetBuilder<ChangesetFormat, T, S, B>
295{
296 pub fn sql_statements(&self) -> impl Iterator<Item = String> + '_ {
319 self.tables.iter().flat_map(|(table, rows)| {
320 rows.values().map(move |op| match op {
321 Operation::Insert { values, .. } => format_insert(table, values),
322 Operation::Delete { data: values, .. } => format_delete_changeset(table, values),
323 Operation::Update { values, .. } => format_update_changeset(table, values),
324 })
325 })
326 }
327}
328
329impl<T: ColumnNames, S: AsRef<str> + Clone + Hash + Eq, B: AsRef<[u8]> + Clone + Hash + Eq>
330 DiffSetBuilder<PatchsetFormat, T, S, B>
331{
332 pub fn sql_statements(&self) -> impl Iterator<Item = String> + '_ {
355 self.tables.iter().flat_map(|(table, rows)| {
356 rows.iter().map(move |(pk, op)| match op {
357 Operation::Insert { values, .. } => format_insert(table, values),
358 Operation::Delete { data: (), .. } => format_delete_patchset(table, pk),
359 Operation::Update { values, .. } => format_update_patchset(table, pk, values),
360 })
361 })
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::{
369 ChangeDelete, ChangeSet, DiffOps, Insert, PatchDelete, PatchSet, SimpleTable, Update,
370 };
371
372 #[test]
373 fn test_quote_identifier_simple() {
374 assert_eq!(quote_identifier("users"), r#""users""#);
375 }
376
377 #[test]
378 fn test_quote_identifier_with_quotes() {
379 assert_eq!(quote_identifier(r#"user"name"#), r#""user""name""#);
380 }
381
382 #[test]
383 fn test_changeset_insert_sql() {
384 let table = SimpleTable::new("users", &["id", "name"], &[0]);
385 let insert = Insert::from(table.clone())
386 .set(0, 1i64)
387 .unwrap()
388 .set(1, "Alice")
389 .unwrap();
390
391 let cs = ChangeSet::<SimpleTable, String, Vec<u8>>::new().insert(insert);
392 let stmts: Vec<_> = cs.sql_statements().collect();
393
394 assert_eq!(stmts.len(), 1);
395 assert_eq!(
396 stmts[0],
397 r#"INSERT INTO "users" ("id", "name") VALUES (1, 'Alice')"#
398 );
399 }
400
401 #[test]
402 fn test_patchset_insert_sql() {
403 let table = SimpleTable::new("users", &["id", "name"], &[0]);
404 let insert = Insert::from(table.clone())
405 .set(0, 1i64)
406 .unwrap()
407 .set(1, "Alice")
408 .unwrap();
409
410 let ps = PatchSet::<SimpleTable, String, Vec<u8>>::new().insert(insert);
411 let stmts: Vec<_> = ps.sql_statements().collect();
412
413 assert_eq!(stmts.len(), 1);
414 assert_eq!(
415 stmts[0],
416 r#"INSERT INTO "users" ("id", "name") VALUES (1, 'Alice')"#
417 );
418 }
419
420 #[test]
421 fn test_changeset_delete_sql() {
422 let table = SimpleTable::new("users", &["id", "name"], &[0]);
423 let delete = ChangeDelete::from(table.clone())
424 .set(0, 42i64)
425 .unwrap()
426 .set(1, "Bob")
427 .unwrap();
428
429 let cs = ChangeSet::<SimpleTable, String, Vec<u8>>::new().delete(delete);
430 let stmts: Vec<_> = cs.sql_statements().collect();
431
432 assert_eq!(stmts.len(), 1);
433 assert_eq!(stmts[0], r#"DELETE FROM "users" WHERE "id" = 42"#);
434 }
435
436 #[test]
437 fn test_patchset_delete_sql() {
438 let table = SimpleTable::new("users", &["id", "name"], &[0]);
439 let delete = PatchDelete::new(table.clone(), alloc::vec![Value::Integer(42)]);
440
441 let ps = PatchSet::<SimpleTable, String, Vec<u8>>::new().delete(delete);
442 let stmts: Vec<_> = ps.sql_statements().collect();
443
444 assert_eq!(stmts.len(), 1);
445 assert_eq!(stmts[0], r#"DELETE FROM "users" WHERE "id" = 42"#);
446 }
447
448 #[test]
449 fn test_changeset_update_sql() {
450 let table = SimpleTable::new("users", &["id", "name"], &[0]);
451 let update = Update::<SimpleTable, ChangesetFormat, String, Vec<u8>>::from(table.clone())
452 .set(0, 1i64, 1i64)
453 .unwrap()
454 .set(1, "Alice", "Alicia")
455 .unwrap();
456
457 let cs = ChangeSet::<SimpleTable, String, Vec<u8>>::new().update(update);
458 let stmts: Vec<_> = cs.sql_statements().collect();
459
460 assert_eq!(stmts.len(), 1);
461 assert_eq!(
462 stmts[0],
463 r#"UPDATE "users" SET "name" = 'Alicia' WHERE "id" = 1"#
464 );
465 }
466
467 #[test]
468 fn test_patchset_update_sql() {
469 let table = SimpleTable::new("users", &["id", "name"], &[0]);
470 let update = Update::<SimpleTable, PatchsetFormat, String, Vec<u8>>::from(table.clone())
471 .set(0, 1i64)
472 .unwrap()
473 .set(1, "Alicia")
474 .unwrap();
475
476 let ps = PatchSet::<SimpleTable, String, Vec<u8>>::new().update(update);
477 let stmts: Vec<_> = ps.sql_statements().collect();
478
479 assert_eq!(stmts.len(), 1);
480 assert_eq!(
481 stmts[0],
482 r#"UPDATE "users" SET "name" = 'Alicia' WHERE "id" = 1"#
483 );
484 }
485
486 #[test]
487 fn test_sql_escapes_quotes_in_strings() {
488 let table = SimpleTable::new("users", &["id", "name"], &[0]);
489 let insert = Insert::from(table.clone())
490 .set(0, 1i64)
491 .unwrap()
492 .set(1, "O'Brien")
493 .unwrap();
494
495 let cs = ChangeSet::<SimpleTable, String, Vec<u8>>::new().insert(insert);
496 let stmts: Vec<_> = cs.sql_statements().collect();
497
498 assert_eq!(
499 stmts[0],
500 r#"INSERT INTO "users" ("id", "name") VALUES (1, 'O''Brien')"#
501 );
502 }
503
504 #[test]
505 fn test_sql_escapes_quotes_in_identifiers() {
506 let table = SimpleTable::new(r#"user"table"#, &["id", r#"user"name"#], &[0]);
507 let insert = Insert::from(table.clone())
508 .set(0, 1i64)
509 .unwrap()
510 .set(1, "Alice")
511 .unwrap();
512
513 let cs = ChangeSet::<SimpleTable, String, Vec<u8>>::new().insert(insert);
514 let stmts: Vec<_> = cs.sql_statements().collect();
515
516 assert_eq!(
517 stmts[0],
518 r#"INSERT INTO "user""table" ("id", "user""name") VALUES (1, 'Alice')"#
519 );
520 }
521
522 #[test]
523 fn test_multiple_operations() {
524 let table = SimpleTable::new("users", &["id", "name"], &[0]);
525
526 let insert1 = Insert::from(table.clone())
527 .set(0, 1i64)
528 .unwrap()
529 .set(1, "Alice")
530 .unwrap();
531
532 let insert2 = Insert::from(table.clone())
533 .set(0, 2i64)
534 .unwrap()
535 .set(1, "Bob")
536 .unwrap();
537
538 let cs = ChangeSet::<SimpleTable, String, Vec<u8>>::new()
539 .insert(insert1)
540 .insert(insert2);
541
542 let stmts: Vec<_> = cs.sql_statements().collect();
543 assert_eq!(stmts.len(), 2);
544 }
545
546 #[test]
547 fn test_composite_pk_delete() {
548 let table = SimpleTable::new("order_items", &["order_id", "item_id", "qty"], &[0, 1]);
549 let delete = ChangeDelete::from(table.clone())
550 .set(0, 100i64)
551 .unwrap()
552 .set(1, 5i64)
553 .unwrap()
554 .set(2, 10i64)
555 .unwrap();
556
557 let cs = ChangeSet::<SimpleTable, String, Vec<u8>>::new().delete(delete);
558 let stmts: Vec<_> = cs.sql_statements().collect();
559
560 assert_eq!(
561 stmts[0],
562 r#"DELETE FROM "order_items" WHERE "order_id" = 100 AND "item_id" = 5"#
563 );
564 }
565
566 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
570 struct AnonColsTable {
571 name: alloc::string::String,
572 num_columns: usize,
573 pk_column: usize,
574 }
575
576 impl crate::DynTable for AnonColsTable {
577 fn name(&self) -> &str {
578 &self.name
579 }
580 fn number_of_columns(&self) -> usize {
581 self.num_columns
582 }
583 fn write_pk_flags(&self, buf: &mut [u8]) {
584 for (i, b) in buf.iter_mut().enumerate() {
585 *b = u8::from(i == self.pk_column);
586 }
587 }
588 }
589
590 impl crate::SchemaWithPK for AnonColsTable {
591 fn number_of_primary_keys(&self) -> usize {
592 1
593 }
594 fn primary_key_index(&self, col_idx: usize) -> Option<usize> {
595 (col_idx == self.pk_column).then_some(0)
596 }
597 fn extract_pk<S: Clone, B: Clone>(
598 &self,
599 values: &impl crate::IndexableValues<Text = S, Binary = B>,
600 ) -> Vec<Value<S, B>> {
601 alloc::vec![values.get(self.pk_column).unwrap()]
602 }
603 }
604
605 impl crate::schema::NamedColumns for AnonColsTable {
606 fn column_index(&self, _column_name: &str) -> Option<usize> {
607 None
608 }
609 }
610
611 impl crate::ColumnNames for AnonColsTable {
612 fn column_name(&self, _index: usize) -> Option<&str> {
615 None
616 }
617 }
618
619 #[test]
620 fn test_sql_output_uses_col_index_fallback() {
621 let table = AnonColsTable {
622 name: "t".into(),
623 num_columns: 2,
624 pk_column: 0,
625 };
626 let insert = Insert::from(table.clone())
627 .set(0, 42i64)
628 .unwrap()
629 .set(1, "hello")
630 .unwrap();
631 let cs = ChangeSet::<AnonColsTable, String, Vec<u8>>::new().insert(insert);
632 let stmts: Vec<_> = cs.sql_statements().collect();
633 assert_eq!(
634 stmts[0],
635 r#"INSERT INTO "t" ("col0", "col1") VALUES (42, 'hello')"#
636 );
637 }
638
639 #[test]
640 fn test_sql_output_col_index_fallback_in_patchset_delete() {
641 let table = AnonColsTable {
642 name: "t".into(),
643 num_columns: 2,
644 pk_column: 0,
645 };
646 let delete: PatchDelete<_, String, Vec<u8>> =
647 PatchDelete::new(table.clone(), alloc::vec![Value::Integer(7)]);
648 let ps = PatchSet::<AnonColsTable, String, Vec<u8>>::new().delete(delete);
649 let stmts: Vec<_> = ps.sql_statements().collect();
650 assert_eq!(stmts[0], r#"DELETE FROM "t" WHERE "col0" = 7"#);
651 }
652}