1use super::constraints::Constraint;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[non_exhaustive]
12#[derive(Debug, Clone, PartialEq)]
13pub enum CompositePkError {
14 EmptyFields,
16 MissingField(String),
18 InvalidFieldType {
20 field: String,
22 expected: String,
24 },
25 DuplicateField(String),
27}
28
29impl std::fmt::Display for CompositePkError {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 CompositePkError::EmptyFields => {
33 write!(f, "Composite primary key must have at least one field")
34 }
35 CompositePkError::MissingField(field) => write!(f, "Missing required field: {}", field),
36 CompositePkError::InvalidFieldType { field, expected } => {
37 write!(
38 f,
39 "Invalid type for field '{}': expected {}",
40 field, expected
41 )
42 }
43 CompositePkError::DuplicateField(field) => write!(f, "Duplicate field name: {}", field),
44 }
45 }
46}
47
48impl std::error::Error for CompositePkError {}
49
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52#[serde(untagged)]
53pub enum PkValue {
54 String(String),
56 Int(i64),
58 Uint(u64),
60 Bool(bool),
62}
63
64impl PkValue {
65 pub fn to_sql_string(&self) -> String {
79 match self {
80 PkValue::String(s) => format!("'{}'", s.replace('\'', "''")),
81 PkValue::Int(i) => i.to_string(),
82 PkValue::Uint(u) => u.to_string(),
83 PkValue::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
84 }
85 }
86}
87
88impl From<String> for PkValue {
90 fn from(value: String) -> Self {
91 PkValue::String(value)
92 }
93}
94
95impl From<&str> for PkValue {
96 fn from(value: &str) -> Self {
97 PkValue::String(value.to_string())
98 }
99}
100
101impl From<i32> for PkValue {
102 fn from(value: i32) -> Self {
103 PkValue::Int(value as i64)
104 }
105}
106
107impl From<i64> for PkValue {
108 fn from(value: i64) -> Self {
109 PkValue::Int(value)
110 }
111}
112
113impl From<u32> for PkValue {
114 fn from(value: u32) -> Self {
115 PkValue::Uint(value as u64)
116 }
117}
118
119impl From<u64> for PkValue {
120 fn from(value: u64) -> Self {
121 PkValue::Uint(value)
122 }
123}
124
125impl From<bool> for PkValue {
126 fn from(value: bool) -> Self {
127 PkValue::Bool(value)
128 }
129}
130
131impl From<&i32> for PkValue {
132 fn from(value: &i32) -> Self {
133 PkValue::Int(*value as i64)
134 }
135}
136
137impl From<&i64> for PkValue {
138 fn from(value: &i64) -> Self {
139 PkValue::Int(*value)
140 }
141}
142
143impl From<&u32> for PkValue {
144 fn from(value: &u32) -> Self {
145 PkValue::Uint(*value as u64)
146 }
147}
148
149impl From<&u64> for PkValue {
150 fn from(value: &u64) -> Self {
151 PkValue::Uint(*value)
152 }
153}
154
155impl From<&bool> for PkValue {
156 fn from(value: &bool) -> Self {
157 PkValue::Bool(*value)
158 }
159}
160
161impl From<&String> for PkValue {
162 fn from(value: &String) -> Self {
163 PkValue::String(value.clone())
164 }
165}
166
167fn quote_identifier(ident: &str) -> String {
171 format!("\"{}\"", ident.replace('"', "\"\""))
172}
173
174#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
179pub struct CompositePrimaryKey {
180 fields: Vec<String>,
182 name: Option<String>,
184}
185
186impl CompositePrimaryKey {
187 pub fn new(fields: Vec<String>) -> Result<Self, CompositePkError> {
199 if fields.is_empty() {
200 return Err(CompositePkError::EmptyFields);
201 }
202
203 let mut seen = HashMap::new();
204 for field in &fields {
205 if seen.contains_key(field) {
206 return Err(CompositePkError::DuplicateField(field.clone()));
207 }
208 seen.insert(field.clone(), true);
209 }
210
211 Ok(Self { fields, name: None })
212 }
213
214 pub fn with_name(
228 fields: Vec<String>,
229 name: impl Into<String>,
230 ) -> Result<Self, CompositePkError> {
231 let mut pk = Self::new(fields)?;
232 pk.name = Some(name.into());
233 Ok(pk)
234 }
235
236 pub fn fields(&self) -> &[String] {
247 &self.fields
248 }
249
250 pub fn name(&self) -> Option<&str> {
252 self.name.as_deref()
253 }
254
255 pub fn to_sql(&self) -> String {
269 let fields: Vec<String> = self.fields.iter().map(|f| quote_identifier(f)).collect();
270 let fields = fields.join(", ");
271 if let Some(ref name) = self.name {
272 format!(
273 "CONSTRAINT {} PRIMARY KEY ({})",
274 quote_identifier(name),
275 fields
276 )
277 } else {
278 format!("PRIMARY KEY ({})", fields)
279 }
280 }
281
282 pub fn validate(&self, values: &HashMap<String, PkValue>) -> Result<(), CompositePkError> {
298 for field in &self.fields {
299 if !values.contains_key(field) {
300 return Err(CompositePkError::MissingField(field.clone()));
301 }
302 }
303 Ok(())
304 }
305
306 pub fn to_where_clause(
324 &self,
325 values: &HashMap<String, PkValue>,
326 ) -> Result<String, CompositePkError> {
327 self.validate(values)?;
328
329 let conditions: Vec<String> = self
330 .fields
331 .iter()
332 .map(|field| {
333 let value = values.get(field).unwrap();
334 format!("{} = {}", quote_identifier(field), value.to_sql_string())
335 })
336 .collect();
337
338 Ok(conditions.join(" AND "))
339 }
340
341 pub fn contains_field(&self, field: &str) -> bool {
354 self.fields.iter().any(|f| f == field)
355 }
356
357 pub fn field_count(&self) -> usize {
368 self.fields.len()
369 }
370}
371
372impl Constraint for CompositePrimaryKey {
373 fn to_sql(&self) -> String {
374 CompositePrimaryKey::to_sql(self)
375 }
376
377 fn name(&self) -> &str {
378 self.name.as_deref().unwrap_or("composite_pk")
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use rstest::rstest;
386
387 #[test]
388 fn test_composite_pk_new_valid() {
389 let pk = CompositePrimaryKey::new(vec!["user_id".to_string(), "role_id".to_string()]);
390 let pk = pk.unwrap();
391 assert_eq!(pk.fields().len(), 2);
392 assert_eq!(pk.fields()[0], "user_id");
393 assert_eq!(pk.fields()[1], "role_id");
394 }
395
396 #[test]
397 fn test_composite_pk_new_empty_fields() {
398 let pk = CompositePrimaryKey::new(vec![]);
399 assert!(pk.is_err());
400 assert_eq!(pk.unwrap_err(), CompositePkError::EmptyFields);
401 }
402
403 #[test]
404 fn test_composite_pk_duplicate_fields() {
405 let pk =
406 CompositePrimaryKey::new(vec!["id".to_string(), "type".to_string(), "id".to_string()]);
407 assert!(pk.is_err());
408 match pk.unwrap_err() {
409 CompositePkError::DuplicateField(field) => assert_eq!(field, "id"),
410 _ => panic!("Expected DuplicateField error"),
411 }
412 }
413
414 #[test]
415 fn test_composite_pk_with_name() {
416 let pk =
417 CompositePrimaryKey::with_name(vec!["a".to_string(), "b".to_string()], "custom_pk");
418 let pk = pk.unwrap();
419 assert_eq!(pk.name(), Some("custom_pk"));
420 }
421
422 #[test]
423 fn test_composite_pk_to_sql_without_name() {
424 let pk =
425 CompositePrimaryKey::new(vec!["user_id".to_string(), "order_id".to_string()]).unwrap();
426 let sql = pk.to_sql();
427 assert_eq!(sql, "PRIMARY KEY (\"user_id\", \"order_id\")");
428 }
429
430 #[test]
431 fn test_composite_pk_to_sql_with_name() {
432 let pk = CompositePrimaryKey::with_name(
433 vec!["user_id".to_string(), "order_id".to_string()],
434 "user_order_pk",
435 )
436 .unwrap();
437 let sql = pk.to_sql();
438 assert_eq!(
439 sql,
440 "CONSTRAINT \"user_order_pk\" PRIMARY KEY (\"user_id\", \"order_id\")"
441 );
442 }
443
444 #[test]
445 fn test_pk_value_to_sql_string() {
446 assert_eq!(PkValue::Int(42).to_sql_string(), "42");
447 assert_eq!(PkValue::Uint(100).to_sql_string(), "100");
448 assert_eq!(PkValue::Bool(true).to_sql_string(), "TRUE");
449 assert_eq!(PkValue::Bool(false).to_sql_string(), "FALSE");
450 assert_eq!(
451 PkValue::String("test".to_string()).to_sql_string(),
452 "'test'"
453 );
454 }
455
456 #[test]
457 fn test_pk_value_sql_string_escapes_quotes() {
458 let value = PkValue::String("O'Brien".to_string());
459 assert_eq!(value.to_sql_string(), "'O''Brien'");
460 }
461
462 #[test]
463 fn test_validate_success() {
464 let pk = CompositePrimaryKey::new(vec!["id".to_string(), "type".to_string()]).unwrap();
465 let mut values = HashMap::new();
466 values.insert("id".to_string(), PkValue::Int(1));
467 values.insert("type".to_string(), PkValue::String("admin".to_string()));
468
469 assert!(pk.validate(&values).is_ok());
470 }
471
472 #[test]
473 fn test_validate_missing_field() {
474 let pk = CompositePrimaryKey::new(vec!["id".to_string(), "type".to_string()]).unwrap();
475 let mut values = HashMap::new();
476 values.insert("id".to_string(), PkValue::Int(1));
477
478 let result = pk.validate(&values);
479 assert!(result.is_err());
480 match result.unwrap_err() {
481 CompositePkError::MissingField(field) => assert_eq!(field, "type"),
482 _ => panic!("Expected MissingField error"),
483 }
484 }
485
486 #[test]
487 fn test_to_where_clause_success() {
488 let pk =
489 CompositePrimaryKey::new(vec!["user_id".to_string(), "role_id".to_string()]).unwrap();
490 let mut values = HashMap::new();
491 values.insert("user_id".to_string(), PkValue::Int(100));
492 values.insert("role_id".to_string(), PkValue::Int(5));
493
494 let where_clause = pk.to_where_clause(&values);
495 let clause = where_clause.unwrap();
496 assert!(clause.contains("\"user_id\" = 100"));
497 assert!(clause.contains("\"role_id\" = 5"));
498 assert!(clause.contains(" AND "));
499 }
500
501 #[test]
502 fn test_to_where_clause_missing_field() {
503 let pk =
504 CompositePrimaryKey::new(vec!["user_id".to_string(), "role_id".to_string()]).unwrap();
505 let mut values = HashMap::new();
506 values.insert("user_id".to_string(), PkValue::Int(100));
507
508 let result = pk.to_where_clause(&values);
509 assert!(result.is_err());
510 }
511
512 #[test]
513 fn test_contains_field() {
514 let pk = CompositePrimaryKey::new(vec!["a".to_string(), "b".to_string()]).unwrap();
515 assert!(pk.contains_field("a"));
516 assert!(pk.contains_field("b"));
517 assert!(!pk.contains_field("c"));
518 }
519
520 #[test]
521 fn test_field_count() {
522 let pk = CompositePrimaryKey::new(vec!["a".to_string(), "b".to_string(), "c".to_string()])
523 .unwrap();
524 assert_eq!(pk.field_count(), 3);
525 }
526
527 #[test]
528 fn test_constraint_trait_implementation() {
529 let pk = CompositePrimaryKey::with_name(vec!["id".to_string()], "test_pk").unwrap();
530 assert_eq!(pk.name(), Some("test_pk"));
531 assert!(pk.to_sql().contains("PRIMARY KEY"));
532 }
533
534 #[test]
535 fn test_composite_pk_three_fields() {
536 let pk = CompositePrimaryKey::new(vec![
537 "org_id".to_string(),
538 "user_id".to_string(),
539 "project_id".to_string(),
540 ])
541 .unwrap();
542 assert_eq!(pk.field_count(), 3);
543 let sql = pk.to_sql();
544 assert!(sql.contains("org_id"));
545 assert!(sql.contains("user_id"));
546 assert!(sql.contains("project_id"));
547 }
548
549 #[test]
550 fn test_where_clause_with_string_values() {
551 let pk = CompositePrimaryKey::new(vec!["country".to_string(), "city".to_string()]).unwrap();
552 let mut values = HashMap::new();
553 values.insert("country".to_string(), PkValue::String("USA".to_string()));
554 values.insert("city".to_string(), PkValue::String("New York".to_string()));
555
556 let where_clause = pk.to_where_clause(&values).unwrap();
557 assert!(where_clause.contains("\"country\" = 'USA'"));
558 assert!(where_clause.contains("\"city\" = 'New York'"));
559 }
560
561 #[test]
562 fn test_where_clause_with_mixed_types() {
563 let pk = CompositePrimaryKey::new(vec!["id".to_string(), "active".to_string()]).unwrap();
564 let mut values = HashMap::new();
565 values.insert("id".to_string(), PkValue::Uint(42));
566 values.insert("active".to_string(), PkValue::Bool(true));
567
568 let where_clause = pk.to_where_clause(&values).unwrap();
569 assert!(where_clause.contains("\"id\" = 42"));
570 assert!(where_clause.contains("\"active\" = TRUE"));
571 }
572
573 #[test]
574 fn test_error_display() {
575 let err = CompositePkError::EmptyFields;
576 assert_eq!(
577 err.to_string(),
578 "Composite primary key must have at least one field"
579 );
580
581 let err = CompositePkError::MissingField("user_id".to_string());
582 assert_eq!(err.to_string(), "Missing required field: user_id");
583
584 let err = CompositePkError::DuplicateField("id".to_string());
585 assert_eq!(err.to_string(), "Duplicate field name: id");
586 }
587
588 #[test]
589 fn test_pk_value_serialization() {
590 let value = PkValue::Int(42);
591 let serialized = serde_json::to_string(&value).unwrap();
592 assert_eq!(serialized, "42");
593
594 let value = PkValue::String("test".to_string());
595 let serialized = serde_json::to_string(&value).unwrap();
596 assert_eq!(serialized, "\"test\"");
597 }
598
599 #[rstest]
600 fn test_to_sql_quotes_normal_field_names() {
601 let pk =
603 CompositePrimaryKey::new(vec!["user_id".to_string(), "role_id".to_string()]).unwrap();
604
605 let sql = pk.to_sql();
607
608 assert_eq!(sql, "PRIMARY KEY (\"user_id\", \"role_id\")");
610 }
611
612 #[rstest]
613 fn test_to_sql_escapes_special_characters_in_field_names() {
614 let pk =
616 CompositePrimaryKey::new(vec!["field\"; DROP TABLE users; --".to_string()]).unwrap();
617
618 let sql = pk.to_sql();
620
621 assert_eq!(sql, "PRIMARY KEY (\"field\"\"; DROP TABLE users; --\")");
624 }
625
626 #[rstest]
627 fn test_to_sql_escapes_special_characters_in_constraint_name() {
628 let pk =
630 CompositePrimaryKey::with_name(vec!["id".to_string()], "pk\"; DROP TABLE users; --")
631 .unwrap();
632
633 let sql = pk.to_sql();
635
636 assert_eq!(
638 sql,
639 "CONSTRAINT \"pk\"\"; DROP TABLE users; --\" PRIMARY KEY (\"id\")"
640 );
641 }
642
643 #[rstest]
644 fn test_to_sql_escapes_embedded_double_quotes_in_field() {
645 let pk = CompositePrimaryKey::new(vec!["field\"name".to_string()]).unwrap();
647
648 let sql = pk.to_sql();
650
651 assert_eq!(sql, "PRIMARY KEY (\"field\"\"name\")");
653 }
654
655 #[rstest]
656 fn test_where_clause_quotes_field_names_with_special_characters() {
657 let malicious_field = "id\"; DROP TABLE users; --".to_string();
659 let pk = CompositePrimaryKey::new(vec![malicious_field.clone()]).unwrap();
660 let mut values = HashMap::new();
661 values.insert(malicious_field, PkValue::Int(1));
662
663 let clause = pk.to_where_clause(&values).unwrap();
665
666 assert_eq!(clause, "\"id\"\"; DROP TABLE users; --\" = 1");
668 }
669
670 #[rstest]
671 fn test_where_clause_quotes_normal_field_names() {
672 let pk = CompositePrimaryKey::new(vec!["user_id".to_string()]).unwrap();
674 let mut values = HashMap::new();
675 values.insert("user_id".to_string(), PkValue::Int(42));
676
677 let clause = pk.to_where_clause(&values).unwrap();
679
680 assert_eq!(clause, "\"user_id\" = 42");
682 }
683}