1use crate::error::FaucetError;
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6use std::collections::HashMap;
7
8#[derive(
15 Debug, Clone, Copy, Default, Serialize, Deserialize, schemars::JsonSchema, PartialEq, Eq,
16)]
17#[serde(rename_all = "snake_case")]
18#[non_exhaustive]
19pub enum WriteMode {
20 #[default]
22 Append,
23 Upsert,
25 Delete,
27 Overwrite,
32}
33
34impl WriteMode {
35 pub fn as_str(&self) -> &'static str {
37 match self {
38 WriteMode::Append => "append",
39 WriteMode::Upsert => "upsert",
40 WriteMode::Delete => "delete",
41 WriteMode::Overwrite => "overwrite",
42 }
43 }
44}
45
46#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema, PartialEq, Eq)]
49pub struct DeleteMarker {
50 pub field: String,
52 pub values: Vec<String>,
54}
55
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
62#[serde(rename_all = "snake_case")]
63#[non_exhaustive]
64pub enum OverwriteScope {
65 Window {
67 column: String,
69 from: Value,
71 to: Value,
73 },
74}
75
76impl OverwriteScope {
77 pub fn column(&self) -> &str {
79 match self {
80 OverwriteScope::Window { column, .. } => column,
81 }
82 }
83
84 pub fn validate(&self) -> Result<(), FaucetError> {
86 match self {
87 OverwriteScope::Window { column, from, to } => {
88 if column.trim().is_empty() {
89 return Err(FaucetError::Config(
90 "overwrite scope: window `column` must not be empty".into(),
91 ));
92 }
93 if from.is_null() || to.is_null() {
94 return Err(FaucetError::Config(
95 "overwrite scope: window `from`/`to` must not be null".into(),
96 ));
97 }
98 Ok(())
99 }
100 }
101 }
102
103 pub fn render_where_literal(&self, quoted_col: &str) -> String {
110 self.render_where_with(quoted_col, sql_literal)
111 }
112
113 pub fn render_where_with(
117 &self,
118 quoted_col: &str,
119 literal: impl Fn(&Value) -> String,
120 ) -> String {
121 match self {
122 OverwriteScope::Window { from, to, .. } => format!(
123 "{quoted_col} >= {} AND {quoted_col} < {}",
124 literal(from),
125 literal(to)
126 ),
127 }
128 }
129}
130
131pub fn sql_literal(v: &Value) -> String {
151 match v {
152 Value::String(s) => format!("'{}'", s.replace('\'', "''")),
153 Value::Number(n) => n.to_string(),
154 Value::Bool(b) => b.to_string(),
155 _ => "NULL".to_owned(),
158 }
159}
160
161#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
165pub struct WriteSpec {
166 #[serde(default)]
168 pub write_mode: WriteMode,
169 #[serde(default)]
171 pub key: Vec<String>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub delete_marker: Option<DeleteMarker>,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub rollback: Option<crate::rollback::RollbackWriteSpec>,
183}
184
185impl WriteSpec {
186 pub fn rollback_run_id(&self) -> Option<&str> {
188 self.rollback.as_ref().map(|r| r.run_id.as_str())
189 }
190
191 pub fn journals(&self) -> bool {
193 self.rollback.as_ref().is_some_and(|r| r.journal)
194 }
195
196 pub fn keeps_previous(&self) -> bool {
198 self.rollback.as_ref().is_some_and(|r| r.keep_previous)
199 }
200
201 pub fn validate(&self) -> Result<(), FaucetError> {
203 if matches!(self.write_mode, WriteMode::Upsert | WriteMode::Delete) && self.key.is_empty() {
204 return Err(FaucetError::Config(format!(
205 "write_mode: {} requires a non-empty `key`",
206 self.write_mode.as_str()
207 )));
208 }
209 Ok(())
210 }
211
212 pub fn dedups_by_key(&self) -> bool {
217 matches!(self.write_mode, WriteMode::Upsert | WriteMode::Delete) && !self.key.is_empty()
218 }
219
220 pub fn is_overwrite(&self) -> bool {
225 matches!(self.write_mode, WriteMode::Overwrite)
226 }
227}
228
229#[derive(Debug, Clone, PartialEq)]
231pub struct KeyTuple(pub Vec<(String, Value)>);
232
233#[derive(Debug, Default)]
237pub struct WritePlan {
238 pub upserts: Vec<Value>,
240 pub deletes: Vec<KeyTuple>,
242 pub failed: Vec<(usize, String)>,
244}
245
246#[derive(Clone)]
247enum Action {
248 Upsert(Value),
249 Delete(KeyTuple),
250}
251
252pub fn plan_writes(page: &[Value], spec: &WriteSpec) -> WritePlan {
260 plan_writes_by(page, spec, canonical)
261}
262
263pub fn plan_writes_exact(page: &[Value], spec: &WriteSpec) -> WritePlan {
267 plan_writes_by(page, spec, |k| {
268 let values: Vec<Value> = k.0.iter().map(|(_, v)| numeric_value(v)).collect();
269 crate::util::canonical_json(&Value::Array(values))
270 })
271}
272
273fn numeric_value(v: &Value) -> Value {
276 match v {
277 Value::Number(n) if n.is_f64() => {
278 let f = n.as_f64().unwrap_or(f64::NAN);
279 let whole = f.is_finite() && f.fract() == 0.0;
280 if whole && f >= i64::MIN as f64 && f < i64::MAX as f64 {
281 Value::from(f as i64)
282 } else if whole && f >= 0.0 && f < u64::MAX as f64 {
283 Value::from(f as u64)
284 } else {
285 v.clone()
286 }
287 }
288 Value::Array(items) => Value::Array(items.iter().map(numeric_value).collect()),
289 Value::Object(map) => Value::Object(
290 map.iter()
291 .map(|(k, v)| (k.clone(), numeric_value(v)))
292 .collect(),
293 ),
294 _ => v.clone(),
295 }
296}
297
298fn plan_writes_by(
299 page: &[Value],
300 spec: &WriteSpec,
301 canonical: impl Fn(&KeyTuple) -> String,
302) -> WritePlan {
303 debug_assert!(
304 matches!(spec.write_mode, WriteMode::Upsert | WriteMode::Delete),
305 "plan_writes is only for Upsert/Delete — Append and Overwrite are routed separately"
306 );
307 let mut plan = WritePlan::default();
308 let mut index: HashMap<String, usize> = HashMap::new();
309 let mut order: Vec<Action> = Vec::new();
310
311 for (i, rec) in page.iter().enumerate() {
312 let key_tuple = match extract_key(rec, &spec.key) {
313 Ok(k) => k,
314 Err(msg) => {
315 plan.failed.push((i, msg));
316 continue;
317 }
318 };
319 let canon = canonical(&key_tuple);
320
321 let is_delete = match spec.write_mode {
322 WriteMode::Delete => true,
323 WriteMode::Upsert => is_delete_marked(rec, spec.delete_marker.as_ref()),
324 WriteMode::Append | WriteMode::Overwrite => false,
325 };
326
327 let action = if is_delete {
328 Action::Delete(key_tuple)
329 } else {
330 Action::Upsert(strip_marker(rec.clone(), spec.delete_marker.as_ref()))
331 };
332
333 match index.get(&canon) {
334 Some(&slot) => order[slot] = action,
335 None => {
336 index.insert(canon, order.len());
337 order.push(action);
338 }
339 }
340 }
341
342 for action in order {
343 match action {
344 Action::Upsert(v) => plan.upserts.push(v),
345 Action::Delete(k) => plan.deletes.push(k),
346 }
347 }
348 plan
349}
350
351pub fn plan_writes_ordered(page: &[Value], spec: &WriteSpec) -> Vec<WritePlan> {
359 let mut plans = Vec::new();
360 let mut run: Vec<Value> = Vec::new();
361 let mut run_is_delete = None;
362 for rec in page {
363 let single = plan_writes(std::slice::from_ref(rec), spec);
364 if !single.failed.is_empty() {
365 continue;
366 }
367 let is_delete = !single.deletes.is_empty();
368 if run_is_delete.is_some_and(|d| d != is_delete) {
369 plans.push(plan_writes(&std::mem::take(&mut run), spec));
370 }
371 run_is_delete = Some(is_delete);
372 run.push(rec.clone());
373 }
374 if !run.is_empty() {
375 plans.push(plan_writes(&run, spec));
376 }
377 plans
378}
379
380pub fn record_key(rec: &Value, key: &[String]) -> Option<KeyTuple> {
383 extract_key(rec, key).ok()
384}
385
386fn extract_key(rec: &Value, key: &[String]) -> Result<KeyTuple, String> {
389 let obj = rec
390 .as_object()
391 .ok_or_else(|| "record is not a JSON object".to_string())?;
392 let mut out = Vec::with_capacity(key.len());
393 for col in key {
394 match obj.get(col) {
395 None => return Err(format!("missing key column '{col}'")),
396 Some(Value::Null) => return Err(format!("null value for key column '{col}'")),
397 Some(v) => out.push((col.clone(), v.clone())),
398 }
399 }
400 Ok(KeyTuple(out))
401}
402
403fn is_delete_marked(rec: &Value, marker: Option<&DeleteMarker>) -> bool {
404 let Some(dm) = marker else { return false };
405 let Some(v) = rec.get(&dm.field) else {
406 return false;
407 };
408 let Some(s) = v.as_str() else { return false };
409 dm.values.iter().any(|m| m == s)
410}
411
412fn strip_marker(mut rec: Value, marker: Option<&DeleteMarker>) -> Value {
413 if let (Some(dm), Value::Object(map)) = (marker, &mut rec) {
414 map.remove(&dm.field);
415 }
416 rec
417}
418
419fn canonical(k: &KeyTuple) -> String {
423 let arr: Vec<String> = k.0.iter().map(|(_, v)| key_scalar_text(v)).collect();
424 serde_json::to_string(&arr).expect("a Vec<String> always serializes")
425}
426
427pub(crate) fn key_scalar_text(v: &Value) -> String {
428 match v {
429 Value::String(s) => s.clone(),
430 Value::Number(n) => match n.as_f64() {
431 Some(f)
432 if n.as_i64().is_none()
433 && n.as_u64().is_none()
434 && f.fract() == 0.0
435 && f.abs() < 9_007_199_254_740_992.0 =>
436 {
437 (f as i64).to_string()
438 }
439 _ => n.to_string(),
440 },
441 Value::Bool(b) => b.to_string(),
442 other => crate::util::canonical_json(other),
443 }
444}
445
446pub fn key_to_doc_id(k: &KeyTuple, separator: &str) -> String {
460 let _ = separator; if k.0.len() == 1 {
462 return match &k.0[0].1 {
463 Value::String(s) => s.clone(),
464 other => crate::util::canonical_json(other),
465 };
466 }
467 let values: Vec<Value> = k.0.iter().map(|(_, v)| v.clone()).collect();
468 crate::util::canonical_json(&Value::Array(values))
469}
470
471pub fn key_to_filter(k: &KeyTuple) -> Map<String, Value> {
473 k.0.iter().map(|(c, v)| (c.clone(), v.clone())).collect()
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479 use serde_json::json;
480
481 #[test]
482 fn exact_planning_keeps_keys_of_different_json_types_apart() {
483 let spec = WriteSpec {
484 write_mode: WriteMode::Upsert,
485 key: vec!["id".into()],
486 ..Default::default()
487 };
488 let page = vec![
489 json!({"id": 7, "v": "a"}),
490 json!({"id": "7", "v": "b"}),
491 json!({"id": 7, "v": "c"}),
492 ];
493 let exact = plan_writes_exact(&page, &spec);
494 assert_eq!(
495 exact.upserts,
496 vec![json!({"id": 7, "v": "c"}), json!({"id": "7", "v": "b"})]
497 );
498 assert_eq!(
499 plan_writes(&page, &spec).upserts,
500 vec![json!({"id": 7, "v": "c"})]
501 );
502 }
503
504 #[test]
505 fn exact_planning_compares_numbers_by_value() {
506 let spec = WriteSpec {
507 write_mode: WriteMode::Upsert,
508 key: vec!["id".into()],
509 ..Default::default()
510 };
511 let page = vec![
512 json!({"id": 7, "v": 1}),
513 json!({"id": 7.0, "v": 2}),
514 json!({"id": 7.5, "v": 3}),
515 json!({"id": {"a": [1.0]}, "v": 4}),
516 json!({"id": {"a": [1]}, "v": 5}),
517 json!({"id": 9_007_199_254_740_993_u64, "v": 6}),
518 json!({"id": 9_007_199_254_740_992.0, "v": 7}),
519 json!({"id": 1.8e19, "v": 8}),
520 json!({"id": 1e300, "v": 9}),
521 ];
522 let v: Vec<Value> = plan_writes_exact(&page, &spec)
523 .upserts
524 .iter()
525 .map(|r| r["v"].clone())
526 .collect();
527 assert_eq!(
528 v,
529 vec![
530 json!(2),
531 json!(3),
532 json!(5),
533 json!(6),
534 json!(7),
535 json!(8),
536 json!(9)
537 ]
538 );
539 }
540
541 fn login_then_id() -> Value {
543 let mut m = serde_json::Map::new();
544 m.insert("login".into(), json!("o"));
545 m.insert("id".into(), json!(2));
546 Value::Object(m)
547 }
548
549 #[test]
550 fn object_key_values_render_with_sorted_keys() {
551 assert_eq!(key_scalar_text(&login_then_id()), r#"{"id":2,"login":"o"}"#);
552 let k = KeyTuple(vec![("k".into(), login_then_id())]);
553 assert_eq!(key_to_doc_id(&k, "_"), r#"{"id":2,"login":"o"}"#);
554 }
555
556 fn upsert_spec(keys: &[&str]) -> WriteSpec {
557 WriteSpec {
558 write_mode: WriteMode::Upsert,
559 key: keys.iter().map(|s| s.to_string()).collect(),
560 delete_marker: None,
561 rollback: None,
562 }
563 }
564
565 #[test]
566 fn ordered_plans_keep_page_order_across_upsert_delete_runs() {
567 let spec = WriteSpec {
568 write_mode: WriteMode::Upsert,
569 key: vec!["name".into()],
570 delete_marker: Some(DeleteMarker {
571 field: "op".into(),
572 values: vec!["d".into()],
573 }),
574 rollback: None,
575 };
576 let page = vec![
577 json!({"name": "Bob", "op": "d"}),
578 json!({"name": "bob", "v": 1}),
579 json!({"name": "carl", "v": 2}),
580 json!({"v": 3}),
581 json!({"name": "dee", "op": "d"}),
582 ];
583 let plans = plan_writes_ordered(&page, &spec);
584 let shape: Vec<(usize, usize)> = plans
585 .iter()
586 .map(|p| (p.upserts.len(), p.deletes.len()))
587 .collect();
588 assert_eq!(shape, vec![(0, 1), (2, 0), (0, 1)]);
589 assert!(plans.iter().all(|p| p.failed.is_empty()));
590 assert!(plan_writes_ordered(&[], &spec).is_empty());
591 }
592
593 #[test]
594 fn upsert_extracts_key_and_keeps_row() {
595 let plan = plan_writes(&[json!({"id": 1, "name": "a"})], &upsert_spec(&["id"]));
596 assert_eq!(plan.upserts, vec![json!({"id": 1, "name": "a"})]);
597 assert!(plan.deletes.is_empty());
598 assert!(plan.failed.is_empty());
599 }
600
601 #[test]
602 fn key_to_doc_id_single_key_is_plain() {
603 let k = KeyTuple(vec![("id".into(), json!(7))]);
604 assert_eq!(key_to_doc_id(&k, "_"), "7");
605 let k = KeyTuple(vec![("name".into(), json!("alice"))]);
606 assert_eq!(key_to_doc_id(&k, "_"), "alice");
607 }
608
609 #[test]
610 fn key_to_doc_id_composite_is_injective() {
611 let k1 = KeyTuple(vec![("x".into(), json!("a_")), ("y".into(), json!("b"))]);
613 let k2 = KeyTuple(vec![("x".into(), json!("a")), ("y".into(), json!("_b"))]);
614 let id1 = key_to_doc_id(&k1, "_");
615 let id2 = key_to_doc_id(&k2, "_");
616 assert_ne!(id1, id2, "distinct composite keys must map to distinct ids");
617 let k3 = KeyTuple(vec![("x".into(), json!(1)), ("y".into(), json!("2"))]);
619 let k4 = KeyTuple(vec![("x".into(), json!("1")), ("y".into(), json!(2))]);
620 assert_ne!(key_to_doc_id(&k3, "_"), key_to_doc_id(&k4, "_"));
621 }
622
623 #[test]
624 fn missing_key_goes_to_failed_with_original_index() {
625 let plan = plan_writes(
626 &[json!({"id": 1}), json!({"name": "no-key"})],
627 &upsert_spec(&["id"]),
628 );
629 assert_eq!(plan.upserts.len(), 1);
630 assert_eq!(plan.failed.len(), 1);
631 assert_eq!(plan.failed[0].0, 1, "failed row keeps its page index");
632 }
633
634 #[test]
635 fn null_key_value_is_a_failure() {
636 let plan = plan_writes(&[json!({"id": null})], &upsert_spec(&["id"]));
637 assert!(plan.upserts.is_empty());
638 assert_eq!(plan.failed.len(), 1);
639 }
640
641 #[test]
642 fn delete_marker_routes_to_deletes_and_strips_marker() {
643 let spec = WriteSpec {
644 write_mode: WriteMode::Upsert,
645 key: vec!["id".into()],
646 delete_marker: Some(DeleteMarker {
647 field: "__op".into(),
648 values: vec!["d".into()],
649 }),
650 rollback: None,
651 };
652 let plan = plan_writes(
653 &[
654 json!({"id": 1, "name": "a", "__op": "u"}),
655 json!({"id": 2, "__op": "d"}),
656 ],
657 &spec,
658 );
659 assert_eq!(plan.upserts, vec![json!({"id": 1, "name": "a"})]);
660 assert_eq!(plan.deletes.len(), 1);
661 assert_eq!(plan.deletes[0].0, vec![("id".to_string(), json!(2))]);
662 }
663
664 #[test]
665 fn last_write_wins_dedup_keeps_final_upsert() {
666 let plan = plan_writes(
667 &[json!({"id": 1, "v": "old"}), json!({"id": 1, "v": "new"})],
668 &upsert_spec(&["id"]),
669 );
670 assert_eq!(plan.upserts, vec![json!({"id": 1, "v": "new"})]);
671 }
672
673 #[test]
674 fn mixed_json_types_of_one_key_dedup_as_one_key() {
675 let plan = plan_writes(
676 &[
677 json!({"id": 7, "v": "a"}),
678 json!({"id": 7.0, "v": "b"}),
679 json!({"id": "7", "v": "c"}),
680 json!({"id": 7.5, "v": "d"}),
681 ],
682 &upsert_spec(&["id"]),
683 );
684 assert_eq!(
685 plan.upserts,
686 vec![json!({"id": "7", "v": "c"}), json!({"id": 7.5, "v": "d"})]
687 );
688 }
689
690 #[test]
691 fn last_write_wins_delete_after_upsert_is_a_delete() {
692 let spec = WriteSpec {
693 write_mode: WriteMode::Upsert,
694 key: vec!["id".into()],
695 delete_marker: Some(DeleteMarker {
696 field: "__op".into(),
697 values: vec!["d".into()],
698 }),
699 rollback: None,
700 };
701 let plan = plan_writes(
702 &[json!({"id": 1, "__op": "u"}), json!({"id": 1, "__op": "d"})],
703 &spec,
704 );
705 assert!(plan.upserts.is_empty());
706 assert_eq!(plan.deletes.len(), 1);
707 }
708
709 #[test]
710 fn delete_mode_routes_every_row_to_deletes() {
711 let spec = WriteSpec {
712 write_mode: WriteMode::Delete,
713 key: vec!["id".into()],
714 delete_marker: None,
715 rollback: None,
716 };
717 let plan = plan_writes(&[json!({"id": 1}), json!({"id": 2})], &spec);
718 assert!(plan.upserts.is_empty());
719 assert_eq!(plan.deletes.len(), 2);
720 }
721
722 #[test]
723 fn composite_key_tuple_is_ordered() {
724 let plan = plan_writes(
725 &[json!({"a": 1, "b": 2, "v": 9})],
726 &upsert_spec(&["a", "b"]),
727 );
728 assert_eq!(plan.upserts.len(), 1);
729 let plan2 = plan_writes(
730 &[
731 json!({"a": 1, "b": 2, "v": "x"}),
732 json!({"a": 1, "b": 3, "v": "y"}),
733 ],
734 &upsert_spec(&["a", "b"]),
735 );
736 assert_eq!(plan2.upserts.len(), 2, "(1,2) and (1,3) are distinct keys");
737 }
738
739 #[test]
740 fn validate_rejects_upsert_without_key() {
741 let spec = WriteSpec {
742 write_mode: WriteMode::Upsert,
743 key: vec![],
744 delete_marker: None,
745 rollback: None,
746 };
747 assert!(spec.validate().is_err());
748 }
749
750 #[test]
751 fn validate_allows_append_without_key() {
752 assert!(WriteSpec::default().validate().is_ok());
753 }
754
755 #[test]
756 fn dedups_by_key_requires_keyed_upsert_or_delete() {
757 assert!(!WriteSpec::default().dedups_by_key());
758 let upsert = WriteSpec {
759 write_mode: WriteMode::Upsert,
760 key: vec!["id".into()],
761 delete_marker: None,
762 rollback: None,
763 };
764 assert!(upsert.dedups_by_key());
765 let delete = WriteSpec {
766 write_mode: WriteMode::Delete,
767 key: vec!["id".into()],
768 delete_marker: None,
769 rollback: None,
770 };
771 assert!(delete.dedups_by_key());
772 let keyless = WriteSpec {
774 write_mode: WriteMode::Upsert,
775 key: vec![],
776 delete_marker: None,
777 rollback: None,
778 };
779 assert!(!keyless.dedups_by_key());
780 }
781
782 #[test]
783 fn last_write_wins_upsert_after_delete_is_an_upsert() {
784 let spec = WriteSpec {
786 write_mode: WriteMode::Upsert,
787 key: vec!["id".into()],
788 delete_marker: Some(DeleteMarker {
789 field: "__op".into(),
790 values: vec!["d".into()],
791 }),
792 rollback: None,
793 };
794 let plan = plan_writes(
795 &[
796 json!({"id": 1, "__op": "d"}),
797 json!({"id": 1, "v": 9, "__op": "u"}),
798 ],
799 &spec,
800 );
801 assert!(plan.deletes.is_empty());
802 assert_eq!(plan.upserts, vec![json!({"id": 1, "v": 9})]);
803 }
804
805 #[test]
806 fn overwrite_mode_flags_and_needs_no_key() {
807 let spec = WriteSpec {
808 write_mode: WriteMode::Overwrite,
809 ..Default::default()
810 };
811 assert!(spec.is_overwrite());
812 assert!(!spec.dedups_by_key());
813 assert!(spec.validate().is_ok());
815 assert_eq!(WriteMode::Overwrite.as_str(), "overwrite");
816 assert!(!WriteSpec::default().is_overwrite());
818 assert!(!upsert_spec(&["id"]).is_overwrite());
819 }
820
821 #[test]
822 fn overwrite_scope_window_validates_and_renders() {
823 let scope = OverwriteScope::Window {
824 column: "posting_date".into(),
825 from: json!("2024-06-01"),
826 to: json!("2024-07-01"),
827 };
828 assert_eq!(scope.column(), "posting_date");
829 assert!(scope.validate().is_ok());
830 let whr = scope.render_where_literal("\"posting_date\"");
831 assert_eq!(
832 whr,
833 "\"posting_date\" >= '2024-06-01' AND \"posting_date\" < '2024-07-01'"
834 );
835
836 assert!(
838 OverwriteScope::Window {
839 column: " ".into(),
840 from: json!(1),
841 to: json!(2)
842 }
843 .validate()
844 .is_err()
845 );
846 assert!(
847 OverwriteScope::Window {
848 column: "c".into(),
849 from: json!(null),
850 to: json!(2)
851 }
852 .validate()
853 .is_err()
854 );
855 }
856
857 #[test]
858 fn scope_window_number_bounds() {
859 let scope = OverwriteScope::Window {
860 column: "seq".into(),
861 from: json!(100),
862 to: json!(200),
863 };
864 assert!(scope.validate().is_ok());
865 assert_eq!(
866 scope.render_where_literal("`seq`"),
867 "`seq` >= 100 AND `seq` < 200"
868 );
869 }
870
871 #[test]
872 fn sql_literal_renders_each_scalar_kind_by_the_ansi_rule() {
873 assert_eq!(sql_literal(&json!("plain")), "'plain'");
876 assert_eq!(sql_literal(&json!("O'Brien")), "'O''Brien'");
877 assert_eq!(sql_literal(&json!("a\\b")), "'a\\b'");
878 assert_eq!(sql_literal(&json!("'' ' ''")), "''''' '' '''''");
880 assert_eq!(sql_literal(&json!("")), "''");
882 assert_eq!(sql_literal(&json!(42)), "42");
884 assert_eq!(sql_literal(&json!(-1.5)), "-1.5");
885 assert_eq!(sql_literal(&json!(true)), "true");
886 assert_eq!(sql_literal(&json!(false)), "false");
887 assert_eq!(sql_literal(&Value::Null), "NULL");
889 assert_eq!(sql_literal(&json!({"a": 1})), "NULL");
890 assert_eq!(sql_literal(&json!([1, 2])), "NULL");
891 }
892
893 #[test]
894 fn sql_literal_neutralises_a_quote_breakout_attempt() {
895 let out = sql_literal(&json!("x' OR 1=1 --"));
897 assert_eq!(out, "'x'' OR 1=1 --'");
898 let interior = &out[1..out.len() - 1];
901 assert!(
902 interior.matches('\'').count().is_multiple_of(2),
903 "unpaired quote inside literal: {out}"
904 );
905 }
906
907 #[test]
908 fn scope_literal_escapes_quotes() {
909 let scope = OverwriteScope::Window {
911 column: "c".into(),
912 from: json!("x' OR '1'='1"),
913 to: json!("z"),
914 };
915 let whr = scope.render_where_literal("\"c\"");
916 assert!(whr.contains("'x'' OR ''1''=''1'"), "{whr}");
917 }
918
919 #[test]
920 fn render_where_with_uses_the_dialect_renderer() {
921 let scope = OverwriteScope::Window {
922 column: "c".into(),
923 from: json!("a"),
924 to: json!(5),
925 };
926 let whr = scope.render_where_with("`c`", |v| format!("<{v}>"));
927 assert_eq!(whr, "`c` >= <\"a\"> AND `c` < <5>");
928 }
929
930 #[test]
931 fn overwrite_deserializes_from_wire() {
932 let spec: WriteSpec = serde_json::from_value(json!({"write_mode": "overwrite"})).unwrap();
933 assert_eq!(spec.write_mode, WriteMode::Overwrite);
934 assert!(spec.is_overwrite());
935 }
936
937 #[test]
938 fn empty_page_produces_empty_plan() {
939 let plan = plan_writes(&[], &upsert_spec(&["id"]));
940 assert!(plan.upserts.is_empty());
941 assert!(plan.deletes.is_empty());
942 assert!(plan.failed.is_empty());
943 }
944
945 #[test]
946 fn delete_mode_dedups_repeated_key() {
947 let spec = WriteSpec {
949 write_mode: WriteMode::Delete,
950 key: vec!["id".into()],
951 delete_marker: None,
952 rollback: None,
953 };
954 let plan = plan_writes(&[json!({"id": 1}), json!({"id": 1})], &spec);
955 assert_eq!(plan.deletes.len(), 1);
956 }
957}