Skip to main content

sqlmodel_core/
validate.rs

1//! Runtime validation helpers for SQLModel.
2//!
3//! This module provides validation functions that can be called from
4//! generated validation code (via the `#[derive(Validate)]` macro).
5//!
6//! It also provides `model_validate()` functionality for creating and
7//! validating models from various input types (similar to Pydantic).
8
9use std::collections::HashMap;
10use std::sync::OnceLock;
11
12use regex::Regex;
13use serde::de::DeserializeOwned;
14
15use crate::Value;
16use crate::error::{ValidationError, ValidationErrorKind};
17
18/// Thread-safe regex cache for compiled patterns.
19///
20/// This avoids recompiling regex patterns on every validation call.
21/// Patterns are compiled lazily on first use and cached for the lifetime
22/// of the program.
23struct RegexCache {
24    cache: std::sync::RwLock<std::collections::HashMap<String, Regex>>,
25}
26
27impl RegexCache {
28    fn new() -> Self {
29        Self {
30            cache: std::sync::RwLock::new(std::collections::HashMap::new()),
31        }
32    }
33
34    fn get_or_compile(&self, pattern: &str) -> Result<Regex, regex::Error> {
35        // Fast path: check if already cached
36        // Use unwrap_or_else to recover from poisoned lock (another thread panicked)
37        {
38            let cache = self.cache.read().unwrap_or_else(|e| e.into_inner());
39            if let Some(regex) = cache.get(pattern) {
40                return Ok(regex.clone());
41            }
42        }
43
44        // Slow path: compile and cache
45        let regex = Regex::new(pattern)?;
46        {
47            let mut cache = self.cache.write().unwrap_or_else(|e| e.into_inner());
48            cache.insert(pattern.to_string(), regex.clone());
49        }
50        Ok(regex)
51    }
52}
53
54/// Global regex cache singleton.
55fn regex_cache() -> &'static RegexCache {
56    static CACHE: OnceLock<RegexCache> = OnceLock::new();
57    CACHE.get_or_init(RegexCache::new)
58}
59
60/// Check if a string matches a regex pattern.
61///
62/// This function is designed to be called from generated validation code.
63/// It caches compiled regex patterns for efficiency.
64///
65/// # Arguments
66///
67/// * `value` - The string to validate
68/// * `pattern` - The regex pattern to match against
69///
70/// # Returns
71///
72/// `true` if the value matches the pattern, `false` otherwise.
73/// Returns `false` if the pattern is invalid (logs a warning).
74///
75/// # Example
76///
77/// ```ignore
78/// use sqlmodel_core::validate::matches_pattern;
79///
80/// assert!(matches_pattern("test@example.com", r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"));
81/// assert!(!matches_pattern("invalid", r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"));
82/// ```
83pub fn matches_pattern(value: &str, pattern: &str) -> bool {
84    match regex_cache().get_or_compile(pattern) {
85        Ok(regex) => regex.is_match(value),
86        Err(e) => {
87            // Log the error but don't panic - validation should be resilient
88            tracing::warn!(
89                pattern = pattern,
90                error = %e,
91                "Invalid regex pattern in validation, treating as non-match"
92            );
93            false
94        }
95    }
96}
97
98/// Validate a regex pattern at compile time (for use in proc macros).
99///
100/// Returns an error message if the pattern is invalid, None if valid.
101pub fn validate_pattern(pattern: &str) -> Option<String> {
102    match Regex::new(pattern) {
103        Ok(_) => None,
104        Err(e) => Some(format!("invalid regex pattern: {e}")),
105    }
106}
107
108// ============================================================================
109// Built-in Validators
110// ============================================================================
111
112/// Validate a credit card number using the Luhn algorithm.
113///
114/// The Luhn algorithm (also known as the "modulus 10" algorithm) is a simple
115/// checksum formula used to validate identification numbers such as credit card
116/// numbers, IMEI numbers, and others.
117///
118/// # Algorithm
119///
120/// 1. Starting from the rightmost digit (check digit) and moving left,
121///    double the value of every second digit.
122/// 2. If the result of doubling is greater than 9, subtract 9.
123/// 3. Sum all the digits.
124/// 4. The total modulo 10 must equal 0.
125///
126/// # Arguments
127///
128/// * `value` - The credit card number as a string (may contain spaces or hyphens)
129///
130/// # Returns
131///
132/// `true` if the number is valid according to the Luhn algorithm, `false` otherwise.
133///
134/// # Example
135///
136/// ```ignore
137/// use sqlmodel_core::validate::is_valid_credit_card;
138///
139/// assert!(is_valid_credit_card("4539578763621486"));  // Valid Visa
140/// assert!(is_valid_credit_card("4539-5787-6362-1486")); // With dashes
141/// assert!(is_valid_credit_card("4539 5787 6362 1486")); // With spaces
142/// assert!(!is_valid_credit_card("1234567890123456")); // Invalid
143/// ```
144pub fn is_valid_credit_card(value: &str) -> bool {
145    // Remove all non-digit characters (spaces, hyphens, etc.)
146    let digits: Vec<u32> = value
147        .chars()
148        .filter(|c| c.is_ascii_digit())
149        .filter_map(|c| c.to_digit(10))
150        .collect();
151
152    // Credit card numbers are typically 13-19 digits
153    if digits.len() < 13 || digits.len() > 19 {
154        return false;
155    }
156
157    // Luhn algorithm
158    let mut sum = 0u32;
159    let len = digits.len();
160
161    for (i, &digit) in digits.iter().enumerate() {
162        // Count from right: rightmost is position 1 (odd)
163        // We double every second digit starting from the second-to-last
164        let position_from_right = len - i;
165        let is_double_position = position_from_right.is_multiple_of(2);
166
167        let value = if is_double_position {
168            let doubled = digit * 2;
169            if doubled > 9 { doubled - 9 } else { doubled }
170        } else {
171            digit
172        };
173
174        sum += value;
175    }
176
177    sum.is_multiple_of(10)
178}
179
180// ============================================================================
181// Model Validation (model_validate)
182// ============================================================================
183
184/// Input types for model_validate().
185///
186/// Supports creating models from various input formats.
187#[derive(Debug, Clone)]
188pub enum ValidateInput {
189    /// A HashMap of field names to values.
190    Dict(HashMap<String, Value>),
191    /// A JSON string to parse.
192    Json(String),
193    /// A serde_json::Value for direct deserialization.
194    JsonValue(serde_json::Value),
195}
196
197impl From<HashMap<String, Value>> for ValidateInput {
198    fn from(map: HashMap<String, Value>) -> Self {
199        ValidateInput::Dict(map)
200    }
201}
202
203impl From<String> for ValidateInput {
204    fn from(json: String) -> Self {
205        ValidateInput::Json(json)
206    }
207}
208
209impl From<&str> for ValidateInput {
210    fn from(json: &str) -> Self {
211        ValidateInput::Json(json.to_string())
212    }
213}
214
215impl From<serde_json::Value> for ValidateInput {
216    fn from(value: serde_json::Value) -> Self {
217        ValidateInput::JsonValue(value)
218    }
219}
220
221/// Options for model_validate().
222///
223/// Controls the validation behavior.
224#[derive(Debug, Clone, Default)]
225pub struct ValidateOptions {
226    /// If true, use strict type coercion (no implicit conversions).
227    pub strict: bool,
228    /// If true, read from object attributes (ORM mode).
229    /// Currently unused - reserved for future from_attributes support.
230    pub from_attributes: bool,
231    /// Optional context dictionary passed to custom validators.
232    pub context: Option<HashMap<String, serde_json::Value>>,
233    /// Additional values to merge into the result after parsing.
234    pub update: Option<HashMap<String, serde_json::Value>>,
235}
236
237impl ValidateOptions {
238    /// Create new default options.
239    pub fn new() -> Self {
240        Self::default()
241    }
242
243    /// Enable strict mode (no implicit type conversions).
244    pub fn strict(mut self) -> Self {
245        self.strict = true;
246        self
247    }
248
249    /// Enable from_attributes mode (read from object attributes).
250    pub fn from_attributes(mut self) -> Self {
251        self.from_attributes = true;
252        self
253    }
254
255    /// Set context for custom validators.
256    pub fn with_context(mut self, context: HashMap<String, serde_json::Value>) -> Self {
257        self.context = Some(context);
258        self
259    }
260
261    /// Set values to merge into result.
262    pub fn with_update(mut self, update: HashMap<String, serde_json::Value>) -> Self {
263        self.update = Some(update);
264        self
265    }
266}
267
268/// Result type for model_validate operations.
269pub type ValidateResult<T> = std::result::Result<T, ValidationError>;
270
271/// Trait for models that support model_validate().
272///
273/// This is typically implemented via derive macro or blanket impl
274/// for models that implement Deserialize.
275pub trait ModelValidate: Sized {
276    /// Create and validate a model from input.
277    ///
278    /// # Arguments
279    ///
280    /// * `input` - The input to validate (Dict, Json, or JsonValue)
281    /// * `options` - Validation options
282    ///
283    /// # Returns
284    ///
285    /// The validated model or validation errors.
286    ///
287    /// # Example
288    ///
289    /// ```ignore
290    /// use sqlmodel_core::validate::{ModelValidate, ValidateInput, ValidateOptions};
291    ///
292    /// let user = User::model_validate(
293    ///     r#"{"name": "Alice", "age": 30}"#,
294    ///     ValidateOptions::default()
295    /// )?;
296    /// ```
297    fn model_validate(
298        input: impl Into<ValidateInput>,
299        options: ValidateOptions,
300    ) -> ValidateResult<Self>;
301
302    /// Create and validate a model from JSON string with default options.
303    fn model_validate_json(json: &str) -> ValidateResult<Self> {
304        Self::model_validate(json, ValidateOptions::default())
305    }
306
307    /// Create and validate a model from a HashMap with default options.
308    fn model_validate_dict(dict: HashMap<String, Value>) -> ValidateResult<Self> {
309        Self::model_validate(dict, ValidateOptions::default())
310    }
311}
312
313/// Blanket implementation of ModelValidate for types that implement DeserializeOwned.
314///
315/// This provides model_validate() for any model that can be deserialized from JSON.
316impl<T: DeserializeOwned> ModelValidate for T {
317    fn model_validate(
318        input: impl Into<ValidateInput>,
319        options: ValidateOptions,
320    ) -> ValidateResult<Self> {
321        let input = input.into();
322
323        // Convert input to serde_json::Value
324        let mut json_value = match input {
325            ValidateInput::Dict(dict) => {
326                // Convert HashMap<String, Value> to serde_json::Value
327                let map: serde_json::Map<String, serde_json::Value> = dict
328                    .into_iter()
329                    .map(|(k, v)| (k, value_to_json(v)))
330                    .collect();
331                serde_json::Value::Object(map)
332            }
333            ValidateInput::Json(json_str) => serde_json::from_str(&json_str).map_err(|e| {
334                let mut err = ValidationError::new();
335                err.add(
336                    "_json",
337                    ValidationErrorKind::Custom,
338                    format!("Invalid JSON: {e}"),
339                );
340                err
341            })?,
342            ValidateInput::JsonValue(value) => value,
343        };
344
345        // Apply update values if provided
346        if let Some(update) = options.update
347            && let serde_json::Value::Object(ref mut map) = json_value
348        {
349            for (key, value) in update {
350                map.insert(key, value);
351            }
352        }
353
354        // Deserialize with appropriate strictness
355        if options.strict {
356            // In strict mode, we use serde's strict deserialization
357            // (default behavior - no implicit conversions)
358            serde_json::from_value(json_value).map_err(|e| {
359                let mut err = ValidationError::new();
360                err.add(
361                    "_model",
362                    ValidationErrorKind::Custom,
363                    format!("Validation failed: {e}"),
364                );
365                err
366            })
367        } else {
368            // Non-strict mode - same for now, but could add coercion logic
369            serde_json::from_value(json_value).map_err(|e| {
370                let mut err = ValidationError::new();
371                err.add(
372                    "_model",
373                    ValidationErrorKind::Custom,
374                    format!("Validation failed: {e}"),
375                );
376                err
377            })
378        }
379    }
380}
381
382// ============================================================================
383// Model Dump (model_dump)
384// ============================================================================
385
386/// Output mode for model_dump().
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
388pub enum DumpMode {
389    /// JSON-compatible types (strings, numbers, booleans, null)
390    #[default]
391    Json,
392    /// Rust native types (preserves Value variants)
393    Python,
394}
395
396/// Options for model_dump() and model_dump_json().
397///
398/// Controls the serialization behavior.
399#[derive(Debug, Clone, Default)]
400pub struct DumpOptions {
401    /// Output mode: Json or Python (Rust native).
402    ///
403    /// In Python/Pydantic, `mode='json'` produces JSON-compatible values while
404    /// `mode='python'` preserves native Python types (e.g., datetime objects).
405    ///
406    /// In this crate, `model_dump()`/`sql_model_dump()` return `serde_json::Value`,
407    /// so both modes currently produce equivalent JSON-value output.
408    pub mode: DumpMode,
409    /// Only include these fields (if Some)
410    pub include: Option<std::collections::HashSet<String>>,
411    /// Exclude these fields
412    pub exclude: Option<std::collections::HashSet<String>>,
413    /// Use field aliases in output.
414    ///
415    /// When true, `sql_model_dump()` will rename fields to their
416    /// `serialization_alias` (or `alias` as fallback) in the output.
417    pub by_alias: bool,
418    /// Exclude fields that were not explicitly set.
419    ///
420    /// In Pydantic, this depends on tracking which fields were explicitly
421    /// provided at construction time (distinct from `exclude_defaults`).
422    ///
423    /// Rust structs do not retain "field set" metadata by default, so this
424    /// option is rejected at runtime to avoid silently producing incorrect output.
425    ///
426    /// Pydantic semantics: If a field has a default value and the user explicitly
427    /// sets it to that default, it should still be included (it was "set").
428    /// With `exclude_defaults`, it would be excluded regardless of whether
429    /// it was explicitly provided.
430    pub exclude_unset: bool,
431    /// Exclude fields with default values
432    pub exclude_defaults: bool,
433    /// Exclude fields with None/null values
434    pub exclude_none: bool,
435    /// Exclude computed fields (for future computed_field support)
436    pub exclude_computed_fields: bool,
437    /// Enable round-trip mode (preserves types for re-parsing).
438    ///
439    /// Pydantic can alter serialization to ensure a dump can be fed back into
440    /// validation and reproduce the same model. This crate does not yet implement
441    /// tagged round-trip encoding, so this flag is currently accepted as a no-op.
442    pub round_trip: bool,
443    /// Indentation for JSON output (None = compact, Some(n) = n spaces)
444    pub indent: Option<usize>,
445}
446
447impl DumpOptions {
448    /// Create new default options.
449    pub fn new() -> Self {
450        Self::default()
451    }
452
453    /// Set output mode to JSON.
454    pub fn json(mut self) -> Self {
455        self.mode = DumpMode::Json;
456        self
457    }
458
459    /// Set output mode to Python (Rust native).
460    pub fn python(mut self) -> Self {
461        self.mode = DumpMode::Python;
462        self
463    }
464
465    /// Set fields to include.
466    pub fn include(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
467        self.include = Some(fields.into_iter().map(Into::into).collect());
468        self
469    }
470
471    /// Set fields to exclude.
472    pub fn exclude(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
473        self.exclude = Some(fields.into_iter().map(Into::into).collect());
474        self
475    }
476
477    /// Enable by_alias mode.
478    pub fn by_alias(mut self) -> Self {
479        self.by_alias = true;
480        self
481    }
482
483    /// Enable exclude_unset mode.
484    pub fn exclude_unset(mut self) -> Self {
485        self.exclude_unset = true;
486        self
487    }
488
489    /// Enable exclude_defaults mode.
490    pub fn exclude_defaults(mut self) -> Self {
491        self.exclude_defaults = true;
492        self
493    }
494
495    /// Enable exclude_none mode.
496    pub fn exclude_none(mut self) -> Self {
497        self.exclude_none = true;
498        self
499    }
500
501    /// Enable exclude_computed_fields mode.
502    pub fn exclude_computed_fields(mut self) -> Self {
503        self.exclude_computed_fields = true;
504        self
505    }
506
507    /// Enable round_trip mode.
508    pub fn round_trip(mut self) -> Self {
509        self.round_trip = true;
510        self
511    }
512
513    /// Set indentation for JSON output.
514    ///
515    /// When set, JSON output will be pretty-printed with the specified number
516    /// of spaces for indentation. When None (default), JSON is compact.
517    pub fn indent(mut self, spaces: usize) -> Self {
518        self.indent = Some(spaces);
519        self
520    }
521}
522
523/// Result type for model_dump operations.
524pub type DumpResult = std::result::Result<serde_json::Value, serde_json::Error>;
525
526pub(crate) fn dump_options_unsupported(msg: impl Into<String>) -> serde_json::Error {
527    serde_json::Error::io(std::io::Error::new(
528        std::io::ErrorKind::InvalidInput,
529        msg.into(),
530    ))
531}
532
533/// Trait for models that support model_dump().
534///
535/// This is typically implemented via blanket impl for models that implement Serialize.
536pub trait ModelDump {
537    /// Serialize a model to a JSON value.
538    ///
539    /// # Arguments
540    ///
541    /// * `options` - Dump options controlling serialization behavior
542    ///
543    /// # Returns
544    ///
545    /// A serde_json::Value representing the serialized model.
546    ///
547    /// # Example
548    ///
549    /// ```ignore
550    /// use sqlmodel_core::validate::{ModelDump, DumpOptions};
551    ///
552    /// let json = user.model_dump(DumpOptions::default())?;
553    /// ```
554    fn model_dump(&self, options: DumpOptions) -> DumpResult;
555
556    /// Serialize a model to a JSON string with default options.
557    fn model_dump_json(&self) -> std::result::Result<String, serde_json::Error> {
558        let value = self.model_dump(DumpOptions::default())?;
559        serde_json::to_string(&value)
560    }
561
562    /// Serialize a model to a pretty-printed JSON string.
563    fn model_dump_json_pretty(&self) -> std::result::Result<String, serde_json::Error> {
564        let value = self.model_dump(DumpOptions::default())?;
565        serde_json::to_string_pretty(&value)
566    }
567
568    /// Serialize a model to a JSON string with full options support.
569    ///
570    /// This method supports all DumpOptions including the `indent` option:
571    /// - `indent: None` - compact JSON output
572    /// - `indent: Some(n)` - pretty-printed with n spaces indentation
573    ///
574    /// # Example
575    ///
576    /// ```ignore
577    /// use sqlmodel_core::validate::{ModelDump, DumpOptions};
578    ///
579    /// // Compact JSON with exclusions
580    /// let json = user.model_dump_json_with_options(
581    ///     DumpOptions::default().exclude(["password"])
582    /// )?;
583    ///
584    /// // Pretty-printed with 4-space indent
585    /// let json = user.model_dump_json_with_options(
586    ///     DumpOptions::default().indent(4)
587    /// )?;
588    /// ```
589    fn model_dump_json_with_options(
590        &self,
591        options: DumpOptions,
592    ) -> std::result::Result<String, serde_json::Error> {
593        let value = self.model_dump(DumpOptions {
594            indent: None, // Don't pass indent to model_dump (it returns Value, not String)
595            ..options.clone()
596        })?;
597
598        match options.indent {
599            Some(spaces) => {
600                let indent_bytes = " ".repeat(spaces).into_bytes();
601                let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
602                let mut writer = Vec::new();
603                let mut ser = serde_json::Serializer::with_formatter(&mut writer, formatter);
604                serde::Serialize::serialize(&value, &mut ser)?;
605                // serde_json always produces valid UTF-8, but propagate error instead of panicking
606                String::from_utf8(writer).map_err(|e| {
607                    serde_json::Error::io(std::io::Error::new(
608                        std::io::ErrorKind::InvalidData,
609                        format!("UTF-8 encoding error: {e}"),
610                    ))
611                })
612            }
613            None => serde_json::to_string(&value),
614        }
615    }
616}
617
618/// Blanket implementation of ModelDump for types that implement Serialize.
619impl<T: serde::Serialize> ModelDump for T {
620    fn model_dump(&self, options: DumpOptions) -> DumpResult {
621        if options.exclude_unset {
622            return Err(dump_options_unsupported(
623                "DumpOptions.exclude_unset requires fields_set tracking; use SqlModelValidate::sql_model_validate_tracked(...) or the tracked!(Type { .. }) macro",
624            ));
625        }
626        if options.by_alias || options.exclude_defaults || options.exclude_computed_fields {
627            return Err(dump_options_unsupported(
628                "DumpOptions.by_alias/exclude_defaults/exclude_computed_fields require SqlModelDump",
629            ));
630        }
631
632        // First, serialize to JSON value
633        let mut value = serde_json::to_value(self)?;
634
635        // Apply options
636        if let serde_json::Value::Object(ref mut map) = value {
637            // Apply include filter
638            if let Some(ref include) = options.include {
639                map.retain(|k, _| include.contains(k));
640            }
641
642            // Apply exclude filter
643            if let Some(ref exclude) = options.exclude {
644                map.retain(|k, _| !exclude.contains(k));
645            }
646
647            // Apply exclude_none filter
648            if options.exclude_none {
649                map.retain(|_, v| !v.is_null());
650            }
651
652            // Note: This is the generic ModelDump implementation for Serialize types.
653            // Model-aware transforms are implemented by SqlModelDump and rejected above.
654        }
655
656        Ok(value)
657    }
658}
659
660/// Convert a Value to serde_json::Value.
661fn value_to_json(value: Value) -> serde_json::Value {
662    match value {
663        Value::Null => serde_json::Value::Null,
664        Value::Bool(b) => serde_json::Value::Bool(b),
665        Value::TinyInt(i) => serde_json::Value::Number(i.into()),
666        Value::SmallInt(i) => serde_json::Value::Number(i.into()),
667        Value::Int(i) => serde_json::Value::Number(i.into()),
668        Value::BigInt(i) => serde_json::Value::Number(i.into()),
669        Value::Float(f) => serde_json::Number::from_f64(f64::from(f))
670            .map_or(serde_json::Value::Null, serde_json::Value::Number),
671        Value::Double(f) => serde_json::Number::from_f64(f)
672            .map_or(serde_json::Value::Null, serde_json::Value::Number),
673        Value::Decimal(s) => serde_json::Value::String(s),
674        Value::Text(s) => serde_json::Value::String(s),
675        Value::Bytes(b) => {
676            // Encode bytes as hex string
677            use std::fmt::Write;
678            let hex = b
679                .iter()
680                .fold(String::with_capacity(b.len() * 2), |mut acc, byte| {
681                    let _ = write!(acc, "{byte:02x}");
682                    acc
683                });
684            serde_json::Value::String(hex)
685        }
686        // Date is i32 (days since epoch) - convert to number
687        Value::Date(d) => serde_json::Value::Number(d.into()),
688        // Time is i64 (microseconds since midnight)
689        Value::Time(t) => serde_json::Value::Number(t.into()),
690        // Timestamp is i64 (microseconds since epoch)
691        Value::Timestamp(ts) => serde_json::Value::Number(ts.into()),
692        // TimestampTz is i64 (microseconds since epoch, UTC)
693        Value::TimestampTz(ts) => serde_json::Value::Number(ts.into()),
694        // UUID is [u8; 16] - format as UUID string with dashes
695        Value::Uuid(u) => {
696            use std::fmt::Write;
697            let hex = u.iter().fold(String::with_capacity(32), |mut acc, b| {
698                let _ = write!(acc, "{b:02x}");
699                acc
700            });
701            // Format as UUID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
702            let formatted = format!(
703                "{}-{}-{}-{}-{}",
704                &hex[0..8],
705                &hex[8..12],
706                &hex[12..16],
707                &hex[16..20],
708                &hex[20..32]
709            );
710            serde_json::Value::String(formatted)
711        }
712        Value::Json(j) => j,
713        Value::Array(arr) => serde_json::Value::Array(arr.into_iter().map(value_to_json).collect()),
714        Value::Default => serde_json::Value::Null,
715    }
716}
717
718// ============================================================================
719// Alias-Aware Validation and Serialization
720// ============================================================================
721
722use crate::Model;
723
724/// Apply validation aliases to JSON input.
725///
726/// This transforms input keys that match validation_alias or alias to their
727/// corresponding field names, enabling deserialization to work correctly.
728///
729/// # Arguments
730///
731/// * `json` - The JSON value to transform (modified in place)
732/// * `fields` - The field metadata containing alias information
733pub fn apply_validation_aliases(json: &mut serde_json::Value, fields: &[crate::FieldInfo]) {
734    if let serde_json::Value::Object(map) = json {
735        // Build a mapping from alias -> field_name
736        let mut alias_map: HashMap<&str, &str> = HashMap::new();
737        for field in fields {
738            // validation_alias takes precedence for input
739            if let Some(alias) = field.validation_alias {
740                alias_map.insert(alias, field.name);
741            }
742            // Regular alias also works for input
743            if let Some(alias) = field.alias {
744                alias_map.entry(alias).or_insert(field.name);
745            }
746        }
747
748        // Collect keys that need to be renamed
749        let renames: Vec<(String, &str)> = map
750            .keys()
751            .filter_map(|k| alias_map.get(k.as_str()).map(|v| (k.clone(), *v)))
752            .collect();
753
754        // Apply renames
755        for (old_key, new_key) in renames {
756            if let Some(value) = map.remove(&old_key) {
757                // Only insert if the target key doesn't already exist
758                map.entry(new_key.to_string()).or_insert(value);
759            }
760        }
761    }
762}
763
764/// Apply serialization aliases to JSON output.
765///
766/// This transforms output keys from field names to their serialization_alias
767/// or alias, enabling proper JSON output format.
768///
769/// # Arguments
770///
771/// * `json` - The JSON value to transform (modified in place)
772/// * `fields` - The field metadata containing alias information
773pub fn apply_serialization_aliases(json: &mut serde_json::Value, fields: &[crate::FieldInfo]) {
774    if let serde_json::Value::Object(map) = json {
775        // Build a mapping from field_name -> output_alias
776        let mut alias_map: HashMap<&str, &str> = HashMap::new();
777        for field in fields {
778            // serialization_alias takes precedence for output
779            if let Some(alias) = field.serialization_alias {
780                alias_map.insert(field.name, alias);
781            } else if let Some(alias) = field.alias {
782                // Regular alias is fallback for output
783                alias_map.insert(field.name, alias);
784            }
785        }
786
787        // Collect keys that need to be renamed
788        let renames: Vec<(String, &str)> = map
789            .keys()
790            .filter_map(|k| alias_map.get(k.as_str()).map(|v| (k.clone(), *v)))
791            .collect();
792
793        // Apply renames
794        for (old_key, new_key) in renames {
795            if let Some(value) = map.remove(&old_key) {
796                map.insert(new_key.to_string(), value);
797            }
798        }
799    }
800}
801
802/// Model-aware validation that supports field aliases.
803///
804/// Unlike the generic `ModelValidate`, this trait uses the `Model::fields()`
805/// metadata to transform aliased input keys to their actual field names
806/// before deserialization.
807///
808/// # Example
809///
810/// ```ignore
811/// #[derive(Model, Serialize, Deserialize)]
812/// struct User {
813///     #[sqlmodel(validation_alias = "userName")]
814///     name: String,
815/// }
816///
817/// // Input with alias key works
818/// let user = User::sql_model_validate(r#"{"userName": "Alice"}"#)?;
819/// assert_eq!(user.name, "Alice");
820/// ```
821pub trait SqlModelValidate: Model + DeserializeOwned + Sized {
822    /// Create and validate a model from input, applying validation aliases.
823    fn sql_model_validate(
824        input: impl Into<ValidateInput>,
825        options: ValidateOptions,
826    ) -> ValidateResult<Self> {
827        let input = input.into();
828
829        // Convert input to serde_json::Value
830        let mut json_value = match input {
831            ValidateInput::Dict(dict) => {
832                let map: serde_json::Map<String, serde_json::Value> = dict
833                    .into_iter()
834                    .map(|(k, v)| (k, value_to_json(v)))
835                    .collect();
836                serde_json::Value::Object(map)
837            }
838            ValidateInput::Json(json_str) => serde_json::from_str(&json_str).map_err(|e| {
839                let mut err = ValidationError::new();
840                err.add(
841                    "_json",
842                    ValidationErrorKind::Custom,
843                    format!("Invalid JSON: {e}"),
844                );
845                err
846            })?,
847            ValidateInput::JsonValue(value) => value,
848        };
849
850        // Apply validation aliases before deserialization
851        apply_validation_aliases(&mut json_value, Self::fields());
852
853        // Apply update values if provided
854        if let Some(update) = options.update
855            && let serde_json::Value::Object(ref mut map) = json_value
856        {
857            for (key, value) in update {
858                map.insert(key, value);
859            }
860        }
861
862        // Deserialize
863        serde_json::from_value(json_value).map_err(|e| {
864            let mut err = ValidationError::new();
865            err.add(
866                "_model",
867                ValidationErrorKind::Custom,
868                format!("Validation failed: {e}"),
869            );
870            err
871        })
872    }
873
874    /// Create and validate a model from input, also tracking which fields were explicitly set.
875    ///
876    /// This enables Pydantic-compatible `exclude_unset` behavior when dumping via `TrackedModel`.
877    fn sql_model_validate_tracked(
878        input: impl Into<ValidateInput>,
879        options: ValidateOptions,
880    ) -> ValidateResult<crate::TrackedModel<Self>> {
881        let input = input.into();
882
883        let mut json_value = match input {
884            ValidateInput::Dict(dict) => {
885                let map: serde_json::Map<String, serde_json::Value> = dict
886                    .into_iter()
887                    .map(|(k, v)| (k, value_to_json(v)))
888                    .collect();
889                serde_json::Value::Object(map)
890            }
891            ValidateInput::Json(json_str) => serde_json::from_str(&json_str).map_err(|e| {
892                let mut err = ValidationError::new();
893                err.add(
894                    "_json",
895                    ValidationErrorKind::Custom,
896                    format!("Invalid JSON: {e}"),
897                );
898                err
899            })?,
900            ValidateInput::JsonValue(value) => value,
901        };
902
903        apply_validation_aliases(&mut json_value, Self::fields());
904
905        if let Some(update) = options.update
906            && let serde_json::Value::Object(ref mut map) = json_value
907        {
908            for (key, value) in update {
909                map.insert(key, value);
910            }
911        }
912
913        // Compute fields_set from the (post-alias) object keys.
914        let mut fields_set = crate::FieldsSet::empty(Self::fields().len());
915        if let serde_json::Value::Object(ref map) = json_value {
916            for (idx, field) in Self::fields().iter().enumerate() {
917                if map.contains_key(field.name) {
918                    fields_set.set(idx);
919                }
920            }
921        }
922
923        let model = serde_json::from_value(json_value).map_err(|e| {
924            let mut err = ValidationError::new();
925            err.add(
926                "_model",
927                ValidationErrorKind::Custom,
928                format!("Validation failed: {e}"),
929            );
930            err
931        })?;
932
933        Ok(crate::TrackedModel::new(model, fields_set))
934    }
935
936    /// Create and validate a model from JSON string with default options.
937    fn sql_model_validate_json(json: &str) -> ValidateResult<Self> {
938        Self::sql_model_validate(json, ValidateOptions::default())
939    }
940
941    /// Create and validate a model from a HashMap with default options.
942    fn sql_model_validate_dict(dict: HashMap<String, Value>) -> ValidateResult<Self> {
943        Self::sql_model_validate(dict, ValidateOptions::default())
944    }
945}
946
947/// Blanket implementation for all Model types that implement DeserializeOwned.
948impl<T: Model + DeserializeOwned> SqlModelValidate for T {}
949
950/// Model-aware dump that supports field aliases and computed field exclusion.
951///
952/// Unlike the generic `ModelDump`, this trait uses the `Model::fields()`
953/// metadata to transform field names to their serialization aliases
954/// in the output and to handle computed fields properly.
955///
956/// # Example
957///
958/// ```ignore
959/// #[derive(Model, Serialize, Deserialize)]
960/// struct User {
961///     #[sqlmodel(serialization_alias = "userName")]
962///     name: String,
963///     #[sqlmodel(computed)]
964///     full_name: String, // Derived field, not in DB
965/// }
966///
967/// let user = User { name: "Alice".to_string(), full_name: "Alice Smith".to_string() };
968/// let json = user.sql_model_dump(DumpOptions::default().by_alias())?;
969/// assert_eq!(json["userName"], "Alice");
970///
971/// // Exclude computed fields
972/// let json = user.sql_model_dump(DumpOptions::default().exclude_computed_fields())?;
973/// assert!(json.get("full_name").is_none());
974/// ```
975pub trait SqlModelDump: Model + serde::Serialize {
976    /// Serialize a model to a JSON value, optionally applying aliases.
977    fn sql_model_dump(&self, options: DumpOptions) -> DumpResult {
978        if options.exclude_unset {
979            return Err(dump_options_unsupported(
980                "DumpOptions.exclude_unset requires fields_set tracking; use SqlModelValidate::sql_model_validate_tracked(...) or the tracked!(Type { .. }) macro",
981            ));
982        }
983
984        // First, serialize to JSON value
985        let mut value = serde_json::to_value(self)?;
986
987        // Apply options that work on original field names BEFORE alias renaming
988        if let serde_json::Value::Object(ref mut map) = value {
989            // Always honor per-field exclude flag (Pydantic Field(exclude=True) semantics).
990            for field in Self::fields() {
991                if field.exclude {
992                    map.remove(field.name);
993                }
994            }
995
996            // Exclude computed fields if requested (must happen before alias renaming)
997            if options.exclude_computed_fields {
998                let computed_field_names: std::collections::HashSet<&str> = Self::fields()
999                    .iter()
1000                    .filter(|f| f.computed)
1001                    .map(|f| f.name)
1002                    .collect();
1003                map.retain(|k, _| !computed_field_names.contains(k.as_str()));
1004            }
1005
1006            // Exclude fields with default values if requested
1007            if options.exclude_defaults {
1008                for field in Self::fields() {
1009                    if let Some(default_json) = field.default_json
1010                        && let Some(current_value) = map.get(field.name)
1011                    {
1012                        // Parse the default JSON and compare
1013                        if let Ok(default_value) =
1014                            serde_json::from_str::<serde_json::Value>(default_json)
1015                            && current_value == &default_value
1016                        {
1017                            map.remove(field.name);
1018                        }
1019                    }
1020                }
1021            }
1022        }
1023
1024        // Apply serialization aliases if by_alias is set
1025        if options.by_alias {
1026            apply_serialization_aliases(&mut value, Self::fields());
1027        }
1028
1029        // Apply remaining options (include/exclude work on the final key names)
1030        if let serde_json::Value::Object(ref mut map) = value {
1031            // Apply include filter
1032            if let Some(ref include) = options.include {
1033                map.retain(|k, _| include.contains(k));
1034            }
1035
1036            // Apply exclude filter
1037            if let Some(ref exclude) = options.exclude {
1038                map.retain(|k, _| !exclude.contains(k));
1039            }
1040
1041            // Apply exclude_none filter
1042            if options.exclude_none {
1043                map.retain(|_, v| !v.is_null());
1044            }
1045        }
1046
1047        Ok(value)
1048    }
1049
1050    /// Serialize a model to a JSON string with default options.
1051    fn sql_model_dump_json(&self) -> std::result::Result<String, serde_json::Error> {
1052        let value = self.sql_model_dump(DumpOptions::default())?;
1053        serde_json::to_string(&value)
1054    }
1055
1056    /// Serialize a model to a pretty-printed JSON string.
1057    fn sql_model_dump_json_pretty(&self) -> std::result::Result<String, serde_json::Error> {
1058        let value = self.sql_model_dump(DumpOptions::default())?;
1059        serde_json::to_string_pretty(&value)
1060    }
1061
1062    /// Serialize with aliases to a JSON string.
1063    fn sql_model_dump_json_by_alias(&self) -> std::result::Result<String, serde_json::Error> {
1064        let value = self.sql_model_dump(DumpOptions::default().by_alias())?;
1065        serde_json::to_string(&value)
1066    }
1067
1068    /// Serialize a model to a JSON string with full options support.
1069    ///
1070    /// This method supports all DumpOptions including the `indent` option:
1071    /// - `indent: None` - compact JSON output
1072    /// - `indent: Some(n)` - pretty-printed with n spaces indentation
1073    ///
1074    /// Compared to `model_dump_json_with_options`, this method also applies
1075    /// Model-specific transformations like serialization aliases.
1076    ///
1077    /// # Example
1078    ///
1079    /// ```ignore
1080    /// use sqlmodel_core::validate::{SqlModelDump, DumpOptions};
1081    ///
1082    /// // With aliases and 2-space indent
1083    /// let json = user.sql_model_dump_json_with_options(
1084    ///     DumpOptions::default().by_alias().indent(2)
1085    /// )?;
1086    /// ```
1087    fn sql_model_dump_json_with_options(
1088        &self,
1089        options: DumpOptions,
1090    ) -> std::result::Result<String, serde_json::Error> {
1091        let value = self.sql_model_dump(DumpOptions {
1092            indent: None, // Don't pass indent to sql_model_dump (it returns Value, not String)
1093            ..options.clone()
1094        })?;
1095
1096        match options.indent {
1097            Some(spaces) => {
1098                let indent_bytes = " ".repeat(spaces).into_bytes();
1099                let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
1100                let mut writer = Vec::new();
1101                let mut ser = serde_json::Serializer::with_formatter(&mut writer, formatter);
1102                serde::Serialize::serialize(&value, &mut ser)?;
1103                // serde_json always produces valid UTF-8, but propagate error instead of panicking
1104                String::from_utf8(writer).map_err(|e| {
1105                    serde_json::Error::io(std::io::Error::new(
1106                        std::io::ErrorKind::InvalidData,
1107                        format!("UTF-8 encoding error: {e}"),
1108                    ))
1109                })
1110            }
1111            None => serde_json::to_string(&value),
1112        }
1113    }
1114}
1115
1116/// Blanket implementation for all Model types that implement Serialize.
1117impl<T: Model + serde::Serialize> SqlModelDump for T {}
1118
1119// ============================================================================
1120// Model Update (sqlmodel_update)
1121// ============================================================================
1122
1123/// Input types for sqlmodel_update().
1124///
1125/// Supports updating models from various input formats.
1126#[derive(Debug, Clone)]
1127pub enum UpdateInput {
1128    /// A HashMap of field names to JSON values.
1129    Dict(HashMap<String, serde_json::Value>),
1130    /// A serde_json::Value for direct updating.
1131    JsonValue(serde_json::Value),
1132}
1133
1134impl From<HashMap<String, serde_json::Value>> for UpdateInput {
1135    fn from(map: HashMap<String, serde_json::Value>) -> Self {
1136        UpdateInput::Dict(map)
1137    }
1138}
1139
1140impl From<serde_json::Value> for UpdateInput {
1141    fn from(value: serde_json::Value) -> Self {
1142        UpdateInput::JsonValue(value)
1143    }
1144}
1145
1146impl From<HashMap<String, Value>> for UpdateInput {
1147    fn from(map: HashMap<String, Value>) -> Self {
1148        let json_map: HashMap<String, serde_json::Value> = map
1149            .into_iter()
1150            .map(|(k, v)| (k, value_to_json(v)))
1151            .collect();
1152        UpdateInput::Dict(json_map)
1153    }
1154}
1155
1156/// Options for sqlmodel_update().
1157#[derive(Debug, Clone, Default)]
1158pub struct UpdateOptions {
1159    /// Only update these fields (if Some). Other fields in the source are ignored.
1160    pub update_fields: Option<std::collections::HashSet<String>>,
1161}
1162
1163impl UpdateOptions {
1164    /// Create new default options.
1165    pub fn new() -> Self {
1166        Self::default()
1167    }
1168
1169    /// Set fields to update.
1170    pub fn update_fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
1171        self.update_fields = Some(fields.into_iter().map(Into::into).collect());
1172        self
1173    }
1174}
1175
1176/// Trait for models that support sqlmodel_update().
1177///
1178/// This enables updating a model instance from a dictionary or another model's values.
1179///
1180/// # Example
1181///
1182/// ```ignore
1183/// use sqlmodel_core::validate::{SqlModelUpdate, UpdateInput, UpdateOptions};
1184///
1185/// let mut user = User { id: 1, name: "Alice".to_string(), age: 30 };
1186///
1187/// // Update from a HashMap
1188/// user.sqlmodel_update(
1189///     HashMap::from([("name".to_string(), serde_json::json!("Bob"))]),
1190///     UpdateOptions::default()
1191/// )?;
1192/// assert_eq!(user.name, "Bob");
1193///
1194/// // Update only specific fields
1195/// user.sqlmodel_update(
1196///     HashMap::from([
1197///         ("name".to_string(), serde_json::json!("Carol")),
1198///         ("age".to_string(), serde_json::json!(25))
1199///     ]),
1200///     UpdateOptions::default().update_fields(["name"])
1201/// )?;
1202/// assert_eq!(user.name, "Carol");
1203/// assert_eq!(user.age, 30); // age was not updated
1204/// ```
1205pub trait SqlModelUpdate: Model + serde::Serialize + DeserializeOwned {
1206    /// Update a model instance from input.
1207    ///
1208    /// This method merges values from the input into the current model.
1209    /// Only fields present in the input (and allowed by `update_fields` option)
1210    /// are updated.
1211    ///
1212    /// # Arguments
1213    ///
1214    /// * `input` - The source of update values (Dict or JsonValue)
1215    /// * `options` - Update options controlling which fields to update
1216    ///
1217    /// # Returns
1218    ///
1219    /// Ok(()) if the update succeeds, or a validation error if the resulting
1220    /// model fails validation.
1221    fn sqlmodel_update(
1222        &mut self,
1223        input: impl Into<UpdateInput>,
1224        options: UpdateOptions,
1225    ) -> ValidateResult<()> {
1226        let input = input.into();
1227
1228        // Convert input to a map
1229        let update_map = match input {
1230            UpdateInput::Dict(map) => map,
1231            UpdateInput::JsonValue(value) => {
1232                if let serde_json::Value::Object(map) = value {
1233                    map.into_iter().collect()
1234                } else {
1235                    let mut err = ValidationError::new();
1236                    err.add(
1237                        "_update",
1238                        ValidationErrorKind::Custom,
1239                        "Update input must be an object".to_string(),
1240                    );
1241                    return Err(err);
1242                }
1243            }
1244        };
1245
1246        // Serialize current model to JSON
1247        let mut current = serde_json::to_value(&*self).map_err(|e| {
1248            let mut err = ValidationError::new();
1249            err.add(
1250                "_model",
1251                ValidationErrorKind::Custom,
1252                format!("Failed to serialize model: {e}"),
1253            );
1254            err
1255        })?;
1256
1257        // Get valid field names from model metadata
1258        let valid_fields: std::collections::HashSet<&str> =
1259            Self::fields().iter().map(|f| f.name).collect();
1260
1261        // Update the current JSON with new values
1262        if let serde_json::Value::Object(ref mut current_map) = current {
1263            for (key, value) in update_map {
1264                // Check if field is valid
1265                if !valid_fields.contains(key.as_str()) {
1266                    let mut err = ValidationError::new();
1267                    err.add(
1268                        &key,
1269                        ValidationErrorKind::Custom,
1270                        format!("Unknown field: {key}"),
1271                    );
1272                    return Err(err);
1273                }
1274
1275                // Check if field is allowed by update_fields option
1276                if let Some(ref allowed) = options.update_fields
1277                    && !allowed.contains(&key)
1278                {
1279                    continue; // Skip fields not in update_fields
1280                }
1281
1282                // Update the field
1283                current_map.insert(key, value);
1284            }
1285        }
1286
1287        // Deserialize back to model (this also validates)
1288        let updated: Self = serde_json::from_value(current).map_err(|e| {
1289            let mut err = ValidationError::new();
1290            err.add(
1291                "_model",
1292                ValidationErrorKind::Custom,
1293                format!("Update failed validation: {e}"),
1294            );
1295            err
1296        })?;
1297
1298        // Replace self with the updated model
1299        *self = updated;
1300
1301        Ok(())
1302    }
1303
1304    /// Update a model instance from a HashMap with default options.
1305    fn sqlmodel_update_dict(
1306        &mut self,
1307        dict: HashMap<String, serde_json::Value>,
1308    ) -> ValidateResult<()> {
1309        self.sqlmodel_update(dict, UpdateOptions::default())
1310    }
1311
1312    /// Copy non-None/non-null values from another model into this one.
1313    ///
1314    /// This is useful for partial updates where you have a "patch" model
1315    /// with only the fields that should be updated (non-None values).
1316    ///
1317    /// # Arguments
1318    ///
1319    /// * `source` - The source model to copy values from
1320    /// * `options` - Update options controlling which fields to update
1321    ///
1322    /// # Example
1323    ///
1324    /// ```ignore
1325    /// let mut user = User { id: 1, name: "Alice".to_string(), age: Some(30) };
1326    /// let patch = User { id: 0, name: "Bob".to_string(), age: None };
1327    ///
1328    /// // Only update name, skip None age
1329    /// user.sqlmodel_update_from(&patch, UpdateOptions::default())?;
1330    /// assert_eq!(user.name, "Bob");
1331    /// assert_eq!(user.age, Some(30)); // Unchanged because patch.age is None
1332    /// ```
1333    fn sqlmodel_update_from(&mut self, source: &Self, options: UpdateOptions) -> ValidateResult<()>
1334    where
1335        Self: Sized,
1336    {
1337        // Serialize source to JSON
1338        let source_json = serde_json::to_value(source).map_err(|e| {
1339            let mut err = ValidationError::new();
1340            err.add(
1341                "_source",
1342                ValidationErrorKind::Custom,
1343                format!("Failed to serialize source model: {e}"),
1344            );
1345            err
1346        })?;
1347
1348        // Filter out null values (None fields)
1349        let update_map: HashMap<String, serde_json::Value> =
1350            if let serde_json::Value::Object(map) = source_json {
1351                map.into_iter().filter(|(_, v)| !v.is_null()).collect()
1352            } else {
1353                let mut err = ValidationError::new();
1354                err.add(
1355                    "_source",
1356                    ValidationErrorKind::Custom,
1357                    "Source model must serialize to an object".to_string(),
1358                );
1359                return Err(err);
1360            };
1361
1362        self.sqlmodel_update(update_map, options)
1363    }
1364}
1365
1366/// Blanket implementation for all Model types that implement Serialize + DeserializeOwned.
1367impl<T: Model + serde::Serialize + DeserializeOwned> SqlModelUpdate for T {}
1368
1369#[cfg(test)]
1370mod tests {
1371    use super::*;
1372    use serde::{Deserialize, Serialize};
1373
1374    #[test]
1375    fn test_matches_email_pattern() {
1376        let email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
1377
1378        assert!(matches_pattern("test@example.com", email_pattern));
1379        assert!(matches_pattern("user.name+tag@domain.org", email_pattern));
1380        assert!(!matches_pattern("invalid", email_pattern));
1381        assert!(!matches_pattern("@example.com", email_pattern));
1382        assert!(!matches_pattern("test@", email_pattern));
1383    }
1384
1385    #[test]
1386    fn test_matches_url_pattern() {
1387        let url_pattern = r"^https?://[^\s/$.?#].[^\s]*$";
1388
1389        assert!(matches_pattern("https://example.com", url_pattern));
1390        assert!(matches_pattern("http://example.com/path", url_pattern));
1391        assert!(!matches_pattern("ftp://example.com", url_pattern));
1392        assert!(!matches_pattern("not a url", url_pattern));
1393    }
1394
1395    #[test]
1396    fn test_matches_phone_pattern() {
1397        let phone_pattern = r"^\+?[1-9]\d{1,14}$";
1398
1399        assert!(matches_pattern("+12025551234", phone_pattern));
1400        assert!(matches_pattern("12025551234", phone_pattern));
1401        assert!(!matches_pattern("0123456789", phone_pattern)); // Can't start with 0
1402        assert!(!matches_pattern("abc", phone_pattern));
1403    }
1404
1405    #[test]
1406    fn test_matches_uuid_pattern() {
1407        let uuid_pattern =
1408            r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";
1409
1410        assert!(matches_pattern(
1411            "550e8400-e29b-41d4-a716-446655440000",
1412            uuid_pattern
1413        ));
1414        assert!(matches_pattern(
1415            "550E8400-E29B-41D4-A716-446655440000",
1416            uuid_pattern
1417        ));
1418        assert!(!matches_pattern("invalid-uuid", uuid_pattern));
1419        assert!(!matches_pattern(
1420            "550e8400e29b41d4a716446655440000",
1421            uuid_pattern
1422        ));
1423    }
1424
1425    #[test]
1426    fn test_matches_alphanumeric_pattern() {
1427        let alphanumeric_pattern = r"^[a-zA-Z0-9]+$";
1428
1429        assert!(matches_pattern("abc123", alphanumeric_pattern));
1430        assert!(matches_pattern("ABC", alphanumeric_pattern));
1431        assert!(matches_pattern("123", alphanumeric_pattern));
1432        assert!(!matches_pattern("abc-123", alphanumeric_pattern));
1433        assert!(!matches_pattern("hello world", alphanumeric_pattern));
1434    }
1435
1436    #[test]
1437    fn test_invalid_pattern_returns_false() {
1438        // Invalid regex pattern (unclosed bracket)
1439        let invalid_pattern = r"[unclosed";
1440        assert!(!matches_pattern("anything", invalid_pattern));
1441    }
1442
1443    #[test]
1444    fn test_validate_pattern_valid() {
1445        assert!(validate_pattern(r"^[a-z]+$").is_none());
1446        assert!(validate_pattern(r"^\d{4}-\d{2}-\d{2}$").is_none());
1447    }
1448
1449    #[test]
1450    fn test_validate_pattern_invalid() {
1451        let result = validate_pattern(r"[unclosed");
1452        assert!(result.is_some());
1453        assert!(result.unwrap().contains("invalid regex pattern"));
1454    }
1455
1456    #[test]
1457    fn test_regex_caching() {
1458        let pattern = r"^test\d+$";
1459
1460        // First call compiles the regex
1461        assert!(matches_pattern("test123", pattern));
1462
1463        // Second call should use cached regex
1464        assert!(matches_pattern("test456", pattern));
1465        assert!(!matches_pattern("invalid", pattern));
1466    }
1467
1468    #[test]
1469    fn test_empty_string() {
1470        let pattern = r"^.+$"; // At least one character
1471        assert!(!matches_pattern("", pattern));
1472
1473        let empty_allowed = r"^.*$"; // Zero or more characters
1474        assert!(matches_pattern("", empty_allowed));
1475    }
1476
1477    #[test]
1478    fn test_special_characters() {
1479        let pattern = r"^[a-z]+$";
1480        assert!(!matches_pattern("hello<script>", pattern));
1481        assert!(!matches_pattern("test'; DROP TABLE users;--", pattern));
1482    }
1483
1484    // =========================================================================
1485    // model_validate tests
1486    // =========================================================================
1487
1488    #[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
1489    struct TestUser {
1490        name: String,
1491        age: i32,
1492        #[serde(default)]
1493        active: bool,
1494    }
1495
1496    #[test]
1497    fn test_model_validate_from_json() {
1498        let json = r#"{"name": "Alice", "age": 30}"#;
1499        let user: TestUser = TestUser::model_validate_json(json).unwrap();
1500        assert_eq!(user.name, "Alice");
1501        assert_eq!(user.age, 30);
1502        assert!(!user.active); // default
1503    }
1504
1505    #[test]
1506    fn test_model_validate_from_json_value() {
1507        let json_value = serde_json::json!({"name": "Bob", "age": 25, "active": true});
1508        let user: TestUser =
1509            TestUser::model_validate(json_value, ValidateOptions::default()).unwrap();
1510        assert_eq!(user.name, "Bob");
1511        assert_eq!(user.age, 25);
1512        assert!(user.active);
1513    }
1514
1515    #[test]
1516    fn test_model_validate_from_dict() {
1517        let mut dict = HashMap::new();
1518        dict.insert("name".to_string(), Value::Text("Charlie".to_string()));
1519        dict.insert("age".to_string(), Value::Int(35));
1520        dict.insert("active".to_string(), Value::Bool(true));
1521
1522        let user: TestUser = TestUser::model_validate_dict(dict).unwrap();
1523        assert_eq!(user.name, "Charlie");
1524        assert_eq!(user.age, 35);
1525        assert!(user.active);
1526    }
1527
1528    #[test]
1529    fn test_model_validate_invalid_json() {
1530        let json = r#"{"name": "Invalid"}"#; // missing required 'age' field
1531        let result: ValidateResult<TestUser> = TestUser::model_validate_json(json);
1532        assert!(result.is_err());
1533        let err = result.unwrap_err();
1534        assert!(!err.is_empty());
1535    }
1536
1537    #[test]
1538    fn test_model_validate_malformed_json() {
1539        let json = r#"{"name": "Alice", age: 30}"#; // invalid JSON syntax
1540        let result: ValidateResult<TestUser> = TestUser::model_validate_json(json);
1541        assert!(result.is_err());
1542        let err = result.unwrap_err();
1543        assert!(
1544            err.errors
1545                .iter()
1546                .any(|e| e.message.contains("Invalid JSON"))
1547        );
1548    }
1549
1550    #[test]
1551    fn test_model_validate_with_update() {
1552        let json = r#"{"name": "Original", "age": 20}"#;
1553        let mut update = HashMap::new();
1554        update.insert("name".to_string(), serde_json::json!("Updated"));
1555
1556        let options = ValidateOptions::new().with_update(update);
1557        let user: TestUser = TestUser::model_validate(json, options).unwrap();
1558        assert_eq!(user.name, "Updated"); // overridden by update
1559        assert_eq!(user.age, 20);
1560    }
1561
1562    #[test]
1563    fn test_model_validate_strict_mode() {
1564        let json = r#"{"name": "Alice", "age": 30}"#;
1565        let options = ValidateOptions::new().strict();
1566        let user: TestUser = TestUser::model_validate(json, options).unwrap();
1567        assert_eq!(user.name, "Alice");
1568        assert_eq!(user.age, 30);
1569    }
1570
1571    #[test]
1572    fn test_validate_options_builder() {
1573        let mut context = HashMap::new();
1574        context.insert("key".to_string(), serde_json::json!("value"));
1575
1576        let options = ValidateOptions::new()
1577            .strict()
1578            .from_attributes()
1579            .with_context(context.clone());
1580
1581        assert!(options.strict);
1582        assert!(options.from_attributes);
1583        assert!(options.context.is_some());
1584        assert_eq!(
1585            options.context.unwrap().get("key"),
1586            Some(&serde_json::json!("value"))
1587        );
1588    }
1589
1590    #[test]
1591    fn test_validate_input_from_conversions() {
1592        // From String
1593        let input: ValidateInput = "{}".to_string().into();
1594        assert!(matches!(input, ValidateInput::Json(_)));
1595
1596        // From &str
1597        let input: ValidateInput = "{}".into();
1598        assert!(matches!(input, ValidateInput::Json(_)));
1599
1600        // From serde_json::Value
1601        let input: ValidateInput = serde_json::json!({}).into();
1602        assert!(matches!(input, ValidateInput::JsonValue(_)));
1603
1604        // From HashMap
1605        let map: HashMap<String, Value> = HashMap::new();
1606        let input: ValidateInput = map.into();
1607        assert!(matches!(input, ValidateInput::Dict(_)));
1608    }
1609
1610    #[test]
1611    fn test_value_to_json_conversions() {
1612        assert_eq!(value_to_json(Value::Null), serde_json::Value::Null);
1613        assert_eq!(value_to_json(Value::Bool(true)), serde_json::json!(true));
1614        assert_eq!(value_to_json(Value::Int(42)), serde_json::json!(42));
1615        assert_eq!(value_to_json(Value::BigInt(100)), serde_json::json!(100));
1616        assert_eq!(
1617            value_to_json(Value::Text("hello".to_string())),
1618            serde_json::json!("hello")
1619        );
1620        // UUID is [u8; 16]
1621        let uuid_bytes: [u8; 16] = [
1622            0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
1623            0x00, 0x00,
1624        ];
1625        assert_eq!(
1626            value_to_json(Value::Uuid(uuid_bytes)),
1627            serde_json::json!("550e8400-e29b-41d4-a716-446655440000")
1628        );
1629
1630        // Array conversion
1631        let arr = vec![Value::Int(1), Value::Int(2), Value::Int(3)];
1632        assert_eq!(
1633            value_to_json(Value::Array(arr)),
1634            serde_json::json!([1, 2, 3])
1635        );
1636    }
1637
1638    // =========================================================================
1639    // model_dump tests
1640    // =========================================================================
1641
1642    #[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
1643    struct TestProduct {
1644        name: String,
1645        price: f64,
1646        #[serde(skip_serializing_if = "Option::is_none")]
1647        description: Option<String>,
1648    }
1649
1650    #[test]
1651    fn test_model_dump_default() {
1652        let product = TestProduct {
1653            name: "Widget".to_string(),
1654            price: 19.99,
1655            description: Some("A useful widget".to_string()),
1656        };
1657        let json = product.model_dump(DumpOptions::default()).unwrap();
1658        assert_eq!(json["name"], "Widget");
1659        assert_eq!(json["price"], 19.99);
1660        assert_eq!(json["description"], "A useful widget");
1661    }
1662
1663    #[test]
1664    fn test_model_dump_json() {
1665        let product = TestProduct {
1666            name: "Gadget".to_string(),
1667            price: 29.99,
1668            description: None,
1669        };
1670        let json_str = product.model_dump_json().unwrap();
1671        assert!(json_str.contains("Gadget"));
1672        assert!(json_str.contains("29.99"));
1673    }
1674
1675    #[test]
1676    fn test_model_dump_json_pretty() {
1677        let product = TestProduct {
1678            name: "Gadget".to_string(),
1679            price: 29.99,
1680            description: None,
1681        };
1682        let json_str = product.model_dump_json_pretty().unwrap();
1683        // Pretty print should have newlines
1684        assert!(json_str.contains('\n'));
1685        assert!(json_str.contains("Gadget"));
1686    }
1687
1688    #[test]
1689    fn test_model_dump_json_with_options_compact() {
1690        let product = TestProduct {
1691            name: "Widget".to_string(),
1692            price: 19.99,
1693            description: Some("A widget".to_string()),
1694        };
1695
1696        // Compact JSON (no indent)
1697        let json_str = product
1698            .model_dump_json_with_options(DumpOptions::default())
1699            .unwrap();
1700        assert!(!json_str.contains('\n')); // No newlines in compact mode
1701        assert!(json_str.contains("Widget"));
1702        assert!(json_str.contains("19.99"));
1703    }
1704
1705    #[test]
1706    fn test_model_dump_json_with_options_indent() {
1707        let product = TestProduct {
1708            name: "Widget".to_string(),
1709            price: 19.99,
1710            description: Some("A widget".to_string()),
1711        };
1712
1713        // 2-space indentation
1714        let json_str = product
1715            .model_dump_json_with_options(DumpOptions::default().indent(2))
1716            .unwrap();
1717        assert!(json_str.contains('\n')); // Has newlines
1718        assert!(json_str.contains("  \"name\"")); // 2-space indent
1719        assert!(json_str.contains("Widget"));
1720
1721        // 4-space indentation
1722        let json_str = product
1723            .model_dump_json_with_options(DumpOptions::default().indent(4))
1724            .unwrap();
1725        assert!(json_str.contains("    \"name\"")); // 4-space indent
1726    }
1727
1728    #[test]
1729    fn test_model_dump_json_with_options_combined() {
1730        let product = TestProduct {
1731            name: "Widget".to_string(),
1732            price: 19.99,
1733            description: Some("A widget".to_string()),
1734        };
1735
1736        // Combine indent with exclude
1737        let json_str = product
1738            .model_dump_json_with_options(DumpOptions::default().exclude(["price"]).indent(2))
1739            .unwrap();
1740        assert!(json_str.contains('\n')); // Has newlines
1741        assert!(json_str.contains("Widget"));
1742        assert!(!json_str.contains("19.99")); // price is excluded
1743    }
1744
1745    #[test]
1746    fn test_dump_options_indent_builder() {
1747        let options = DumpOptions::new().indent(4);
1748        assert_eq!(options.indent, Some(4));
1749
1750        // Can combine with other options
1751        let options2 = DumpOptions::new()
1752            .indent(2)
1753            .by_alias()
1754            .exclude(["password"]);
1755        assert_eq!(options2.indent, Some(2));
1756        assert!(options2.by_alias);
1757        assert!(options2.exclude.unwrap().contains("password"));
1758    }
1759
1760    #[test]
1761    fn test_model_dump_include() {
1762        let product = TestProduct {
1763            name: "Widget".to_string(),
1764            price: 19.99,
1765            description: Some("A widget".to_string()),
1766        };
1767        let options = DumpOptions::new().include(["name"]);
1768        let json = product.model_dump(options).unwrap();
1769        assert!(json.get("name").is_some());
1770        assert!(json.get("price").is_none());
1771        assert!(json.get("description").is_none());
1772    }
1773
1774    #[test]
1775    fn test_model_dump_exclude() {
1776        let product = TestProduct {
1777            name: "Widget".to_string(),
1778            price: 19.99,
1779            description: Some("A widget".to_string()),
1780        };
1781        let options = DumpOptions::new().exclude(["description"]);
1782        let json = product.model_dump(options).unwrap();
1783        assert!(json.get("name").is_some());
1784        assert!(json.get("price").is_some());
1785        assert!(json.get("description").is_none());
1786    }
1787
1788    #[test]
1789    fn test_model_dump_exclude_none() {
1790        let product = TestProduct {
1791            name: "Widget".to_string(),
1792            price: 19.99,
1793            description: None,
1794        };
1795        // Note: serde skip_serializing_if already handles this
1796        // But we can still test the exclude_none flag
1797        let options = DumpOptions::new().exclude_none();
1798        let json = product.model_dump(options).unwrap();
1799        assert!(json.get("name").is_some());
1800        // description would be None, but serde already skips it
1801    }
1802
1803    #[test]
1804    fn test_dump_options_builder() {
1805        let options = DumpOptions::new()
1806            .json()
1807            .include(["name", "age"])
1808            .exclude(["password"])
1809            .by_alias()
1810            .exclude_none()
1811            .exclude_defaults()
1812            .round_trip();
1813
1814        assert_eq!(options.mode, DumpMode::Json);
1815        assert!(options.include.is_some());
1816        assert!(options.exclude.is_some());
1817        assert!(options.by_alias);
1818        assert!(options.exclude_none);
1819        assert!(options.exclude_defaults);
1820        assert!(options.round_trip);
1821    }
1822
1823    #[test]
1824    fn test_dump_mode_default() {
1825        assert_eq!(DumpMode::default(), DumpMode::Json);
1826    }
1827
1828    #[test]
1829    fn test_model_dump_include_exclude_combined() {
1830        let user = TestUser {
1831            name: "Alice".to_string(),
1832            age: 30,
1833            active: true,
1834        };
1835        // Include name and age, but exclude age
1836        let options = DumpOptions::new().include(["name", "age"]).exclude(["age"]);
1837        let json = user.model_dump(options).unwrap();
1838        // Include is applied first, then exclude
1839        assert!(json.get("name").is_some());
1840        assert!(json.get("age").is_none());
1841        assert!(json.get("active").is_none());
1842    }
1843
1844    #[test]
1845    fn test_model_dump_accepts_python_mode_and_round_trip() {
1846        let product = TestProduct {
1847            name: "Widget".to_string(),
1848            price: 19.99,
1849            description: Some("A useful widget".to_string()),
1850        };
1851        let json = product
1852            .model_dump(DumpOptions::default().python().round_trip())
1853            .unwrap();
1854
1855        assert_eq!(json["name"], "Widget");
1856        assert_eq!(json["price"], 19.99);
1857        assert_eq!(json["description"], "A useful widget");
1858    }
1859
1860    // ========================================================================
1861    // Alias Tests
1862    // ========================================================================
1863
1864    use crate::{FieldInfo, Row, SqlType};
1865
1866    /// Test model with aliases for validation and serialization tests.
1867    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1868    struct TestAliasedUser {
1869        id: i64,
1870        name: String,
1871        email: String,
1872    }
1873
1874    impl Model for TestAliasedUser {
1875        const TABLE_NAME: &'static str = "users";
1876        const PRIMARY_KEY: &'static [&'static str] = &["id"];
1877
1878        fn fields() -> &'static [FieldInfo] {
1879            static FIELDS: &[FieldInfo] = &[
1880                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
1881                FieldInfo::new("name", "name", SqlType::Text)
1882                    .validation_alias("userName")
1883                    .serialization_alias("displayName"),
1884                FieldInfo::new("email", "email", SqlType::Text).alias("emailAddress"), // Both input and output
1885            ];
1886            FIELDS
1887        }
1888
1889        fn to_row(&self) -> Vec<(&'static str, Value)> {
1890            vec![
1891                ("id", Value::BigInt(self.id)),
1892                ("name", Value::Text(self.name.clone())),
1893                ("email", Value::Text(self.email.clone())),
1894            ]
1895        }
1896
1897        fn from_row(row: &Row) -> crate::Result<Self> {
1898            Ok(Self {
1899                id: row.get_named("id")?,
1900                name: row.get_named("name")?,
1901                email: row.get_named("email")?,
1902            })
1903        }
1904
1905        fn primary_key_value(&self) -> Vec<Value> {
1906            vec![Value::BigInt(self.id)]
1907        }
1908
1909        fn is_new(&self) -> bool {
1910            false
1911        }
1912    }
1913
1914    #[test]
1915    fn test_apply_validation_aliases() {
1916        let fields = TestAliasedUser::fields();
1917
1918        // Test with validation_alias
1919        let mut json = serde_json::json!({
1920            "id": 1,
1921            "userName": "Alice",
1922            "email": "alice@example.com"
1923        });
1924        apply_validation_aliases(&mut json, fields);
1925
1926        // userName should be renamed to name
1927        assert_eq!(json["name"], "Alice");
1928        assert!(json.get("userName").is_none());
1929
1930        // Test with regular alias
1931        let mut json2 = serde_json::json!({
1932            "id": 1,
1933            "name": "Bob",
1934            "emailAddress": "bob@example.com"
1935        });
1936        apply_validation_aliases(&mut json2, fields);
1937
1938        // emailAddress should be renamed to email
1939        assert_eq!(json2["email"], "bob@example.com");
1940        assert!(json2.get("emailAddress").is_none());
1941    }
1942
1943    #[test]
1944    fn test_apply_serialization_aliases() {
1945        let fields = TestAliasedUser::fields();
1946
1947        let mut json = serde_json::json!({
1948            "id": 1,
1949            "name": "Alice",
1950            "email": "alice@example.com"
1951        });
1952        apply_serialization_aliases(&mut json, fields);
1953
1954        // name should be renamed to displayName (serialization_alias)
1955        assert_eq!(json["displayName"], "Alice");
1956        assert!(json.get("name").is_none());
1957
1958        // email should be renamed to emailAddress (regular alias)
1959        assert_eq!(json["emailAddress"], "alice@example.com");
1960        assert!(json.get("email").is_none());
1961    }
1962
1963    #[test]
1964    fn test_sql_model_validate_with_validation_alias() {
1965        // Use validation_alias in input
1966        let json = r#"{"id": 1, "userName": "Alice", "email": "alice@example.com"}"#;
1967        let user: TestAliasedUser = TestAliasedUser::sql_model_validate_json(json).unwrap();
1968
1969        assert_eq!(user.id, 1);
1970        assert_eq!(user.name, "Alice");
1971        assert_eq!(user.email, "alice@example.com");
1972    }
1973
1974    #[test]
1975    fn test_sql_model_validate_with_regular_alias() {
1976        // Use regular alias in input
1977        let json = r#"{"id": 1, "name": "Bob", "emailAddress": "bob@example.com"}"#;
1978        let user: TestAliasedUser = TestAliasedUser::sql_model_validate_json(json).unwrap();
1979
1980        assert_eq!(user.id, 1);
1981        assert_eq!(user.name, "Bob");
1982        assert_eq!(user.email, "bob@example.com");
1983    }
1984
1985    #[test]
1986    fn test_sql_model_validate_with_field_name() {
1987        // Use actual field name (should still work)
1988        let json = r#"{"id": 1, "name": "Charlie", "email": "charlie@example.com"}"#;
1989        let user: TestAliasedUser = TestAliasedUser::sql_model_validate_json(json).unwrap();
1990
1991        assert_eq!(user.id, 1);
1992        assert_eq!(user.name, "Charlie");
1993        assert_eq!(user.email, "charlie@example.com");
1994    }
1995
1996    #[test]
1997    fn test_sql_model_dump_by_alias() {
1998        let user = TestAliasedUser {
1999            id: 1,
2000            name: "Alice".to_string(),
2001            email: "alice@example.com".to_string(),
2002        };
2003
2004        let json = user
2005            .sql_model_dump(DumpOptions::default().by_alias())
2006            .unwrap();
2007
2008        // name should be serialized as displayName
2009        assert_eq!(json["displayName"], "Alice");
2010        assert!(json.get("name").is_none());
2011
2012        // email should be serialized as emailAddress
2013        assert_eq!(json["emailAddress"], "alice@example.com");
2014        assert!(json.get("email").is_none());
2015    }
2016
2017    #[test]
2018    fn test_sql_model_dump_without_alias() {
2019        let user = TestAliasedUser {
2020            id: 1,
2021            name: "Alice".to_string(),
2022            email: "alice@example.com".to_string(),
2023        };
2024
2025        // Without by_alias, use original field names
2026        let json = user.sql_model_dump(DumpOptions::default()).unwrap();
2027
2028        assert_eq!(json["name"], "Alice");
2029        assert_eq!(json["email"], "alice@example.com");
2030        assert!(json.get("displayName").is_none());
2031        assert!(json.get("emailAddress").is_none());
2032    }
2033
2034    #[test]
2035    fn test_sql_model_dump_accepts_python_mode_and_round_trip() {
2036        let user = TestAliasedUser {
2037            id: 1,
2038            name: "Alice".to_string(),
2039            email: "alice@example.com".to_string(),
2040        };
2041        let json = user
2042            .sql_model_dump(DumpOptions::default().python().round_trip())
2043            .unwrap();
2044
2045        assert_eq!(json["name"], "Alice");
2046        assert_eq!(json["email"], "alice@example.com");
2047    }
2048
2049    #[test]
2050    fn test_tracked_model_dump_accepts_python_mode_and_round_trip() {
2051        let user = TestAliasedUser {
2052            id: 1,
2053            name: "Alice".to_string(),
2054            email: "alice@example.com".to_string(),
2055        };
2056        let tracked = crate::TrackedModel::all_fields_set(user);
2057        let json = tracked
2058            .sql_model_dump(DumpOptions::default().python().round_trip())
2059            .unwrap();
2060
2061        assert_eq!(json["name"], "Alice");
2062        assert_eq!(json["email"], "alice@example.com");
2063    }
2064
2065    #[test]
2066    fn test_alias_does_not_overwrite_existing() {
2067        let fields = TestAliasedUser::fields();
2068
2069        // If both alias and field name are present, field name wins
2070        let mut json = serde_json::json!({
2071            "id": 1,
2072            "name": "FieldName",
2073            "userName": "AliasName",
2074            "email": "test@example.com"
2075        });
2076        apply_validation_aliases(&mut json, fields);
2077
2078        // Original "name" field should be preserved
2079        assert_eq!(json["name"], "FieldName");
2080        // userName should be removed (but couldn't insert because "name" exists)
2081        assert!(json.get("userName").is_none());
2082    }
2083
2084    // ========================================================================
2085    // Computed Field Tests
2086    // ========================================================================
2087
2088    /// Test model with computed fields.
2089    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2090    struct TestUserWithComputed {
2091        id: i64,
2092        first_name: String,
2093        last_name: String,
2094        #[serde(default)]
2095        full_name: String, // Computed field - derived from first_name + last_name
2096    }
2097
2098    impl Model for TestUserWithComputed {
2099        const TABLE_NAME: &'static str = "users";
2100        const PRIMARY_KEY: &'static [&'static str] = &["id"];
2101
2102        fn fields() -> &'static [FieldInfo] {
2103            static FIELDS: &[FieldInfo] = &[
2104                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2105                FieldInfo::new("first_name", "first_name", SqlType::Text),
2106                FieldInfo::new("last_name", "last_name", SqlType::Text),
2107                FieldInfo::new("full_name", "full_name", SqlType::Text).computed(true),
2108            ];
2109            FIELDS
2110        }
2111
2112        fn to_row(&self) -> Vec<(&'static str, Value)> {
2113            // Computed field is NOT included in DB operations
2114            vec![
2115                ("id", Value::BigInt(self.id)),
2116                ("first_name", Value::Text(self.first_name.clone())),
2117                ("last_name", Value::Text(self.last_name.clone())),
2118            ]
2119        }
2120
2121        fn from_row(row: &Row) -> crate::Result<Self> {
2122            Ok(Self {
2123                id: row.get_named("id")?,
2124                first_name: row.get_named("first_name")?,
2125                last_name: row.get_named("last_name")?,
2126                // Computed field initialized with Default (empty string)
2127                full_name: String::new(),
2128            })
2129        }
2130
2131        fn primary_key_value(&self) -> Vec<Value> {
2132            vec![Value::BigInt(self.id)]
2133        }
2134
2135        fn is_new(&self) -> bool {
2136            false
2137        }
2138    }
2139
2140    #[test]
2141    fn test_computed_field_included_by_default() {
2142        let user = TestUserWithComputed {
2143            id: 1,
2144            first_name: "John".to_string(),
2145            last_name: "Doe".to_string(),
2146            full_name: "John Doe".to_string(),
2147        };
2148
2149        // By default, computed fields ARE included in model_dump
2150        let json = user.sql_model_dump(DumpOptions::default()).unwrap();
2151
2152        assert_eq!(json["id"], 1);
2153        assert_eq!(json["first_name"], "John");
2154        assert_eq!(json["last_name"], "Doe");
2155        assert_eq!(json["full_name"], "John Doe"); // Computed field is present
2156    }
2157
2158    #[test]
2159    fn test_computed_field_excluded_with_option() {
2160        let user = TestUserWithComputed {
2161            id: 1,
2162            first_name: "John".to_string(),
2163            last_name: "Doe".to_string(),
2164            full_name: "John Doe".to_string(),
2165        };
2166
2167        // With exclude_computed_fields, computed fields are excluded
2168        let json = user
2169            .sql_model_dump(DumpOptions::default().exclude_computed_fields())
2170            .unwrap();
2171
2172        assert_eq!(json["id"], 1);
2173        assert_eq!(json["first_name"], "John");
2174        assert_eq!(json["last_name"], "Doe");
2175        assert!(json.get("full_name").is_none()); // Computed field is excluded
2176    }
2177
2178    #[test]
2179    fn test_computed_field_not_in_to_row() {
2180        let user = TestUserWithComputed {
2181            id: 1,
2182            first_name: "Jane".to_string(),
2183            last_name: "Smith".to_string(),
2184            full_name: "Jane Smith".to_string(),
2185        };
2186
2187        // to_row() should not include computed field (for DB INSERT/UPDATE)
2188        let row = user.to_row();
2189
2190        // Should have 3 fields: id, first_name, last_name
2191        assert_eq!(row.len(), 3);
2192        let field_names: Vec<&str> = row.iter().map(|(name, _)| *name).collect();
2193        assert!(field_names.contains(&"id"));
2194        assert!(field_names.contains(&"first_name"));
2195        assert!(field_names.contains(&"last_name"));
2196        assert!(!field_names.contains(&"full_name")); // Computed field NOT in row
2197    }
2198
2199    #[test]
2200    fn test_computed_field_select_fields_excludes() {
2201        let fields = TestUserWithComputed::fields();
2202
2203        // Check that computed field is marked
2204        let computed: Vec<&FieldInfo> = fields.iter().filter(|f| f.computed).collect();
2205        assert_eq!(computed.len(), 1);
2206        assert_eq!(computed[0].name, "full_name");
2207
2208        // Non-computed fields
2209        let non_computed: Vec<&FieldInfo> = fields.iter().filter(|f| !f.computed).collect();
2210        assert_eq!(non_computed.len(), 3);
2211    }
2212
2213    #[test]
2214    fn test_computed_field_with_other_dump_options() {
2215        let user = TestUserWithComputed {
2216            id: 1,
2217            first_name: "John".to_string(),
2218            last_name: "Doe".to_string(),
2219            full_name: "John Doe".to_string(),
2220        };
2221
2222        // Combine exclude_computed_fields with include filter
2223        let json = user
2224            .sql_model_dump(DumpOptions::default().exclude_computed_fields().include([
2225                "id",
2226                "first_name",
2227                "full_name",
2228            ]))
2229            .unwrap();
2230
2231        // full_name is excluded because it's computed, even though in include list
2232        // (exclude_computed_fields is applied before include filter)
2233        assert!(json.get("id").is_some());
2234        assert!(json.get("first_name").is_some());
2235        assert!(json.get("full_name").is_none()); // Excluded as computed
2236        assert!(json.get("last_name").is_none()); // Not in include list
2237    }
2238
2239    #[test]
2240    fn test_dump_options_exclude_computed_fields_builder() {
2241        let options = DumpOptions::new().exclude_computed_fields();
2242        assert!(options.exclude_computed_fields);
2243
2244        // Can combine with other options
2245        let options2 = DumpOptions::new()
2246            .exclude_computed_fields()
2247            .by_alias()
2248            .exclude_none();
2249        assert!(options2.exclude_computed_fields);
2250        assert!(options2.by_alias);
2251        assert!(options2.exclude_none);
2252    }
2253
2254    /// Test model with both computed fields AND serialization aliases.
2255    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2256    struct TestUserWithComputedAndAlias {
2257        id: i64,
2258        first_name: String,
2259        #[serde(default)]
2260        display_name: String, // Computed field that also has an alias
2261    }
2262
2263    impl Model for TestUserWithComputedAndAlias {
2264        const TABLE_NAME: &'static str = "users";
2265        const PRIMARY_KEY: &'static [&'static str] = &["id"];
2266
2267        fn fields() -> &'static [FieldInfo] {
2268            static FIELDS: &[FieldInfo] = &[
2269                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2270                FieldInfo::new("first_name", "first_name", SqlType::Text)
2271                    .serialization_alias("firstName"),
2272                FieldInfo::new("display_name", "display_name", SqlType::Text)
2273                    .computed(true)
2274                    .serialization_alias("displayName"),
2275            ];
2276            FIELDS
2277        }
2278
2279        fn to_row(&self) -> Vec<(&'static str, Value)> {
2280            vec![
2281                ("id", Value::BigInt(self.id)),
2282                ("first_name", Value::Text(self.first_name.clone())),
2283            ]
2284        }
2285
2286        fn from_row(row: &Row) -> crate::Result<Self> {
2287            Ok(Self {
2288                id: row.get_named("id")?,
2289                first_name: row.get_named("first_name")?,
2290                display_name: String::new(),
2291            })
2292        }
2293
2294        fn primary_key_value(&self) -> Vec<Value> {
2295            vec![Value::BigInt(self.id)]
2296        }
2297
2298        fn is_new(&self) -> bool {
2299            false
2300        }
2301    }
2302
2303    #[test]
2304    fn test_exclude_computed_with_by_alias() {
2305        // This test verifies that computed field exclusion works correctly
2306        // even when combined with by_alias (which renames keys)
2307        let user = TestUserWithComputedAndAlias {
2308            id: 1,
2309            first_name: "John".to_string(),
2310            display_name: "John Doe".to_string(),
2311        };
2312
2313        // Test with by_alias only - computed field should still appear (aliased)
2314        let json = user
2315            .sql_model_dump(DumpOptions::default().by_alias())
2316            .unwrap();
2317        assert_eq!(json["firstName"], "John"); // first_name aliased
2318        assert_eq!(json["displayName"], "John Doe"); // display_name aliased (computed but not excluded)
2319        assert!(json.get("first_name").is_none()); // Original name should not exist
2320        assert!(json.get("display_name").is_none()); // Original name should not exist
2321
2322        // Test with exclude_computed_fields only - computed field should be excluded
2323        let json = user
2324            .sql_model_dump(DumpOptions::default().exclude_computed_fields())
2325            .unwrap();
2326        assert_eq!(json["first_name"], "John");
2327        assert!(json.get("display_name").is_none()); // Computed field excluded
2328
2329        // Test with BOTH by_alias AND exclude_computed_fields
2330        // This was buggy before the fix - computed field wasn't excluded
2331        // because exclusion happened after aliasing
2332        let json = user
2333            .sql_model_dump(DumpOptions::default().by_alias().exclude_computed_fields())
2334            .unwrap();
2335        assert_eq!(json["firstName"], "John"); // first_name aliased
2336        assert!(json.get("displayName").is_none()); // Computed field excluded (even though aliased)
2337        assert!(json.get("display_name").is_none()); // Original name doesn't exist either
2338    }
2339
2340    // ========================================================================
2341    // Exclude Defaults Tests
2342    // ========================================================================
2343
2344    /// Test model with default values for exclude_defaults testing.
2345    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2346    struct TestModelWithDefaults {
2347        id: i64,
2348        name: String,
2349        count: i32,    // default: 0
2350        active: bool,  // default: false
2351        score: f64,    // default: 0.0
2352        label: String, // default: "default"
2353    }
2354
2355    impl Model for TestModelWithDefaults {
2356        const TABLE_NAME: &'static str = "test_defaults";
2357        const PRIMARY_KEY: &'static [&'static str] = &["id"];
2358
2359        fn fields() -> &'static [FieldInfo] {
2360            static FIELDS: &[FieldInfo] = &[
2361                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2362                FieldInfo::new("name", "name", SqlType::Text),
2363                FieldInfo::new("count", "count", SqlType::Integer).default_json("0"),
2364                FieldInfo::new("active", "active", SqlType::Boolean).default_json("false"),
2365                FieldInfo::new("score", "score", SqlType::Double).default_json("0.0"),
2366                FieldInfo::new("label", "label", SqlType::Text).default_json("\"default\""),
2367            ];
2368            FIELDS
2369        }
2370
2371        fn to_row(&self) -> Vec<(&'static str, Value)> {
2372            vec![
2373                ("id", Value::BigInt(self.id)),
2374                ("name", Value::Text(self.name.clone())),
2375                ("count", Value::Int(self.count)),
2376                ("active", Value::Bool(self.active)),
2377                ("score", Value::Double(self.score)),
2378                ("label", Value::Text(self.label.clone())),
2379            ]
2380        }
2381
2382        fn from_row(row: &Row) -> crate::Result<Self> {
2383            Ok(Self {
2384                id: row.get_named("id")?,
2385                name: row.get_named("name")?,
2386                count: row.get_named("count")?,
2387                active: row.get_named("active")?,
2388                score: row.get_named("score")?,
2389                label: row.get_named("label")?,
2390            })
2391        }
2392
2393        fn primary_key_value(&self) -> Vec<Value> {
2394            vec![Value::BigInt(self.id)]
2395        }
2396
2397        fn is_new(&self) -> bool {
2398            false
2399        }
2400    }
2401
2402    #[test]
2403    fn test_exclude_defaults_all_at_default() {
2404        let model = TestModelWithDefaults {
2405            id: 1,
2406            name: "Test".to_string(),
2407            count: 0,                     // at default
2408            active: false,                // at default
2409            score: 0.0,                   // at default
2410            label: "default".to_string(), // at default
2411        };
2412
2413        let json = model
2414            .sql_model_dump(DumpOptions::default().exclude_defaults())
2415            .unwrap();
2416
2417        // id and name have no default_json, so they're always included
2418        assert!(json.get("id").is_some());
2419        assert!(json.get("name").is_some());
2420
2421        // Fields at default value should be excluded
2422        assert!(json.get("count").is_none());
2423        assert!(json.get("active").is_none());
2424        assert!(json.get("score").is_none());
2425        assert!(json.get("label").is_none());
2426    }
2427
2428    #[test]
2429    fn test_exclude_defaults_none_at_default() {
2430        let model = TestModelWithDefaults {
2431            id: 1,
2432            name: "Test".to_string(),
2433            count: 42,                   // not at default
2434            active: true,                // not at default
2435            score: 3.5,                  // not at default
2436            label: "custom".to_string(), // not at default
2437        };
2438
2439        let json = model
2440            .sql_model_dump(DumpOptions::default().exclude_defaults())
2441            .unwrap();
2442
2443        // All fields should be present since none are at defaults
2444        assert!(json.get("id").is_some());
2445        assert!(json.get("name").is_some());
2446        assert!(json.get("count").is_some());
2447        assert!(json.get("active").is_some());
2448        assert!(json.get("score").is_some());
2449        assert!(json.get("label").is_some());
2450
2451        // Verify values
2452        assert_eq!(json["count"], 42);
2453        assert_eq!(json["active"], true);
2454        assert_eq!(json["score"], 3.5);
2455        assert_eq!(json["label"], "custom");
2456    }
2457
2458    #[test]
2459    fn test_exclude_defaults_mixed() {
2460        let model = TestModelWithDefaults {
2461            id: 1,
2462            name: "Test".to_string(),
2463            count: 0,                    // at default
2464            active: true,                // not at default
2465            score: 0.0,                  // at default
2466            label: "custom".to_string(), // not at default
2467        };
2468
2469        let json = model
2470            .sql_model_dump(DumpOptions::default().exclude_defaults())
2471            .unwrap();
2472
2473        assert!(json.get("id").is_some());
2474        assert!(json.get("name").is_some());
2475
2476        // At default - excluded
2477        assert!(json.get("count").is_none());
2478        assert!(json.get("score").is_none());
2479
2480        // Not at default - included
2481        assert!(json.get("active").is_some());
2482        assert!(json.get("label").is_some());
2483        assert_eq!(json["active"], true);
2484        assert_eq!(json["label"], "custom");
2485    }
2486
2487    #[test]
2488    fn test_exclude_defaults_without_flag() {
2489        let model = TestModelWithDefaults {
2490            id: 1,
2491            name: "Test".to_string(),
2492            count: 0,                     // at default
2493            active: false,                // at default
2494            score: 0.0,                   // at default
2495            label: "default".to_string(), // at default
2496        };
2497
2498        // Without exclude_defaults, all fields should be included
2499        let json = model.sql_model_dump(DumpOptions::default()).unwrap();
2500
2501        assert!(json.get("id").is_some());
2502        assert!(json.get("name").is_some());
2503        assert!(json.get("count").is_some());
2504        assert!(json.get("active").is_some());
2505        assert!(json.get("score").is_some());
2506        assert!(json.get("label").is_some());
2507    }
2508
2509    #[test]
2510    fn test_exclude_defaults_with_by_alias() {
2511        // Test that exclude_defaults works correctly with by_alias
2512
2513        /// Model with defaults and aliases
2514        #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2515        struct TestAliasWithDefaults {
2516            id: i64,
2517            count: i32,
2518        }
2519
2520        impl Model for TestAliasWithDefaults {
2521            const TABLE_NAME: &'static str = "test";
2522            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2523
2524            fn fields() -> &'static [FieldInfo] {
2525                static FIELDS: &[FieldInfo] = &[
2526                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2527                    FieldInfo::new("count", "count", SqlType::Integer)
2528                        .default_json("0")
2529                        .serialization_alias("itemCount"),
2530                ];
2531                FIELDS
2532            }
2533
2534            fn to_row(&self) -> Vec<(&'static str, Value)> {
2535                vec![
2536                    ("id", Value::BigInt(self.id)),
2537                    ("count", Value::Int(self.count)),
2538                ]
2539            }
2540
2541            fn from_row(row: &Row) -> crate::Result<Self> {
2542                Ok(Self {
2543                    id: row.get_named("id")?,
2544                    count: row.get_named("count")?,
2545                })
2546            }
2547
2548            fn primary_key_value(&self) -> Vec<Value> {
2549                vec![Value::BigInt(self.id)]
2550            }
2551
2552            fn is_new(&self) -> bool {
2553                false
2554            }
2555        }
2556
2557        // At default value
2558        let model_at_default = TestAliasWithDefaults { id: 1, count: 0 };
2559        let json = model_at_default
2560            .sql_model_dump(DumpOptions::default().exclude_defaults().by_alias())
2561            .unwrap();
2562
2563        // count is at default (0), so neither count nor itemCount should appear
2564        assert!(json.get("count").is_none());
2565        assert!(json.get("itemCount").is_none());
2566
2567        // Not at default
2568        let model_not_at_default = TestAliasWithDefaults { id: 1, count: 5 };
2569        let json = model_not_at_default
2570            .sql_model_dump(DumpOptions::default().exclude_defaults().by_alias())
2571            .unwrap();
2572
2573        // count is not at default, should appear with alias
2574        assert!(json.get("count").is_none()); // Original name not present
2575        assert_eq!(json["itemCount"], 5); // Alias is present
2576    }
2577
2578    #[test]
2579    fn test_field_info_default_json() {
2580        // Test the FieldInfo builder methods for default_json
2581        let field1 = FieldInfo::new("count", "count", SqlType::Integer).default_json("0");
2582        assert_eq!(field1.default_json, Some("0"));
2583        assert!(field1.has_default);
2584
2585        let field2 =
2586            FieldInfo::new("name", "name", SqlType::Text).default_json_opt(Some("\"hello\""));
2587        assert_eq!(field2.default_json, Some("\"hello\""));
2588        assert!(field2.has_default);
2589
2590        let field3 = FieldInfo::new("name", "name", SqlType::Text).default_json_opt(None);
2591        assert_eq!(field3.default_json, None);
2592        assert!(!field3.has_default);
2593
2594        let field4 = FieldInfo::new("flag", "flag", SqlType::Boolean).has_default(true);
2595        assert!(field4.has_default);
2596        assert_eq!(field4.default_json, None); // has_default alone doesn't set default_json
2597    }
2598
2599    // ==================== SqlModelUpdate Tests ====================
2600
2601    #[test]
2602    fn test_sqlmodel_update_from_dict() {
2603        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2604        struct TestUser {
2605            id: i64,
2606            name: String,
2607            age: i32,
2608        }
2609
2610        impl Model for TestUser {
2611            const TABLE_NAME: &'static str = "users";
2612            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2613
2614            fn fields() -> &'static [FieldInfo] {
2615                static FIELDS: &[FieldInfo] = &[
2616                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2617                    FieldInfo::new("name", "name", SqlType::Text),
2618                    FieldInfo::new("age", "age", SqlType::Integer),
2619                ];
2620                FIELDS
2621            }
2622
2623            fn to_row(&self) -> Vec<(&'static str, Value)> {
2624                vec![
2625                    ("id", Value::BigInt(self.id)),
2626                    ("name", Value::Text(self.name.clone())),
2627                    ("age", Value::Int(self.age)),
2628                ]
2629            }
2630
2631            fn from_row(row: &Row) -> crate::Result<Self> {
2632                Ok(Self {
2633                    id: row.get_named("id")?,
2634                    name: row.get_named("name")?,
2635                    age: row.get_named("age")?,
2636                })
2637            }
2638
2639            fn primary_key_value(&self) -> Vec<Value> {
2640                vec![Value::BigInt(self.id)]
2641            }
2642
2643            fn is_new(&self) -> bool {
2644                false
2645            }
2646        }
2647
2648        let mut user = TestUser {
2649            id: 1,
2650            name: "Alice".to_string(),
2651            age: 30,
2652        };
2653
2654        // Update name only
2655        let update = HashMap::from([("name".to_string(), serde_json::json!("Bob"))]);
2656        user.sqlmodel_update(update, UpdateOptions::default())
2657            .unwrap();
2658
2659        assert_eq!(user.name, "Bob");
2660        assert_eq!(user.age, 30); // Unchanged
2661    }
2662
2663    #[test]
2664    fn test_sqlmodel_update_with_update_fields_filter() {
2665        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2666        struct TestUser {
2667            id: i64,
2668            name: String,
2669            age: i32,
2670        }
2671
2672        impl Model for TestUser {
2673            const TABLE_NAME: &'static str = "users";
2674            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2675
2676            fn fields() -> &'static [FieldInfo] {
2677                static FIELDS: &[FieldInfo] = &[
2678                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2679                    FieldInfo::new("name", "name", SqlType::Text),
2680                    FieldInfo::new("age", "age", SqlType::Integer),
2681                ];
2682                FIELDS
2683            }
2684
2685            fn to_row(&self) -> Vec<(&'static str, Value)> {
2686                vec![
2687                    ("id", Value::BigInt(self.id)),
2688                    ("name", Value::Text(self.name.clone())),
2689                    ("age", Value::Int(self.age)),
2690                ]
2691            }
2692
2693            fn from_row(row: &Row) -> crate::Result<Self> {
2694                Ok(Self {
2695                    id: row.get_named("id")?,
2696                    name: row.get_named("name")?,
2697                    age: row.get_named("age")?,
2698                })
2699            }
2700
2701            fn primary_key_value(&self) -> Vec<Value> {
2702                vec![Value::BigInt(self.id)]
2703            }
2704
2705            fn is_new(&self) -> bool {
2706                false
2707            }
2708        }
2709
2710        let mut user = TestUser {
2711            id: 1,
2712            name: "Alice".to_string(),
2713            age: 30,
2714        };
2715
2716        // Try to update both name and age, but only allow name
2717        let update = HashMap::from([
2718            ("name".to_string(), serde_json::json!("Bob")),
2719            ("age".to_string(), serde_json::json!(25)),
2720        ]);
2721        user.sqlmodel_update(update, UpdateOptions::default().update_fields(["name"]))
2722            .unwrap();
2723
2724        assert_eq!(user.name, "Bob"); // Updated
2725        assert_eq!(user.age, 30); // Not updated because not in update_fields
2726    }
2727
2728    #[test]
2729    fn test_sqlmodel_update_invalid_field_error() {
2730        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2731        struct TestUser {
2732            id: i64,
2733            name: String,
2734        }
2735
2736        impl Model for TestUser {
2737            const TABLE_NAME: &'static str = "users";
2738            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2739
2740            fn fields() -> &'static [FieldInfo] {
2741                static FIELDS: &[FieldInfo] = &[
2742                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2743                    FieldInfo::new("name", "name", SqlType::Text),
2744                ];
2745                FIELDS
2746            }
2747
2748            fn to_row(&self) -> Vec<(&'static str, Value)> {
2749                vec![
2750                    ("id", Value::BigInt(self.id)),
2751                    ("name", Value::Text(self.name.clone())),
2752                ]
2753            }
2754
2755            fn from_row(row: &Row) -> crate::Result<Self> {
2756                Ok(Self {
2757                    id: row.get_named("id")?,
2758                    name: row.get_named("name")?,
2759                })
2760            }
2761
2762            fn primary_key_value(&self) -> Vec<Value> {
2763                vec![Value::BigInt(self.id)]
2764            }
2765
2766            fn is_new(&self) -> bool {
2767                false
2768            }
2769        }
2770
2771        let mut user = TestUser {
2772            id: 1,
2773            name: "Alice".to_string(),
2774        };
2775
2776        // Try to update an invalid field
2777        let update = HashMap::from([("invalid_field".to_string(), serde_json::json!("value"))]);
2778        let result = user.sqlmodel_update(update, UpdateOptions::default());
2779
2780        assert!(result.is_err());
2781        let err = result.unwrap_err();
2782        assert!(err.errors.iter().any(|e| e.field == "invalid_field"));
2783    }
2784
2785    #[test]
2786    fn test_sqlmodel_update_from_model() {
2787        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2788        struct TestUser {
2789            id: i64,
2790            name: String,
2791            email: Option<String>,
2792        }
2793
2794        impl Model for TestUser {
2795            const TABLE_NAME: &'static str = "users";
2796            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2797
2798            fn fields() -> &'static [FieldInfo] {
2799                static FIELDS: &[FieldInfo] = &[
2800                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2801                    FieldInfo::new("name", "name", SqlType::Text),
2802                    FieldInfo::new("email", "email", SqlType::Text).nullable(true),
2803                ];
2804                FIELDS
2805            }
2806
2807            fn to_row(&self) -> Vec<(&'static str, Value)> {
2808                vec![
2809                    ("id", Value::BigInt(self.id)),
2810                    ("name", Value::Text(self.name.clone())),
2811                    ("email", self.email.clone().map_or(Value::Null, Value::Text)),
2812                ]
2813            }
2814
2815            fn from_row(row: &Row) -> crate::Result<Self> {
2816                Ok(Self {
2817                    id: row.get_named("id")?,
2818                    name: row.get_named("name")?,
2819                    email: row.get_named("email").ok(),
2820                })
2821            }
2822
2823            fn primary_key_value(&self) -> Vec<Value> {
2824                vec![Value::BigInt(self.id)]
2825            }
2826
2827            fn is_new(&self) -> bool {
2828                false
2829            }
2830        }
2831
2832        let mut user = TestUser {
2833            id: 1,
2834            name: "Alice".to_string(),
2835            email: Some("alice@example.com".to_string()),
2836        };
2837
2838        // Patch with only name set (email is None)
2839        let patch = TestUser {
2840            id: 0, // Will be ignored since we're copying non-null values
2841            name: "Bob".to_string(),
2842            email: None, // Should not overwrite existing email
2843        };
2844
2845        user.sqlmodel_update_from(&patch, UpdateOptions::default())
2846            .unwrap();
2847
2848        assert_eq!(user.name, "Bob"); // Updated
2849        assert_eq!(user.email, Some("alice@example.com".to_string())); // Not updated (patch.email was None)
2850    }
2851
2852    #[test]
2853    fn test_sqlmodel_update_dict_convenience() {
2854        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2855        struct TestItem {
2856            id: i64,
2857            count: i32,
2858        }
2859
2860        impl Model for TestItem {
2861            const TABLE_NAME: &'static str = "items";
2862            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2863
2864            fn fields() -> &'static [FieldInfo] {
2865                static FIELDS: &[FieldInfo] = &[
2866                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2867                    FieldInfo::new("count", "count", SqlType::Integer),
2868                ];
2869                FIELDS
2870            }
2871
2872            fn to_row(&self) -> Vec<(&'static str, Value)> {
2873                vec![
2874                    ("id", Value::BigInt(self.id)),
2875                    ("count", Value::Int(self.count)),
2876                ]
2877            }
2878
2879            fn from_row(row: &Row) -> crate::Result<Self> {
2880                Ok(Self {
2881                    id: row.get_named("id")?,
2882                    count: row.get_named("count")?,
2883                })
2884            }
2885
2886            fn primary_key_value(&self) -> Vec<Value> {
2887                vec![Value::BigInt(self.id)]
2888            }
2889
2890            fn is_new(&self) -> bool {
2891                false
2892            }
2893        }
2894
2895        let mut item = TestItem { id: 1, count: 10 };
2896
2897        // Use the convenience method
2898        item.sqlmodel_update_dict(HashMap::from([(
2899            "count".to_string(),
2900            serde_json::json!(20),
2901        )]))
2902        .unwrap();
2903
2904        assert_eq!(item.count, 20);
2905    }
2906
2907    // ========================================================================
2908    // Credit Card Validation Tests (Luhn Algorithm)
2909    // ========================================================================
2910
2911    #[test]
2912    fn test_credit_card_valid_visa() {
2913        // Valid Visa test number
2914        assert!(is_valid_credit_card("4539578763621486"));
2915    }
2916
2917    #[test]
2918    fn test_credit_card_valid_mastercard() {
2919        // Valid Mastercard test number
2920        assert!(is_valid_credit_card("5425233430109903"));
2921    }
2922
2923    #[test]
2924    fn test_credit_card_valid_amex() {
2925        // Valid American Express test number
2926        assert!(is_valid_credit_card("374245455400126"));
2927    }
2928
2929    #[test]
2930    fn test_credit_card_with_spaces() {
2931        // Spaces should be stripped
2932        assert!(is_valid_credit_card("4539 5787 6362 1486"));
2933    }
2934
2935    #[test]
2936    fn test_credit_card_with_dashes() {
2937        // Dashes should be stripped
2938        assert!(is_valid_credit_card("4539-5787-6362-1486"));
2939    }
2940
2941    #[test]
2942    fn test_credit_card_invalid_luhn() {
2943        // Invalid Luhn checksum
2944        assert!(!is_valid_credit_card("1234567890123456"));
2945    }
2946
2947    #[test]
2948    fn test_credit_card_too_short() {
2949        // Less than 13 digits
2950        assert!(!is_valid_credit_card("123456789012"));
2951    }
2952
2953    #[test]
2954    fn test_credit_card_too_long() {
2955        // More than 19 digits
2956        assert!(!is_valid_credit_card("12345678901234567890"));
2957    }
2958
2959    #[test]
2960    fn test_credit_card_empty() {
2961        assert!(!is_valid_credit_card(""));
2962    }
2963
2964    #[test]
2965    fn test_credit_card_non_numeric() {
2966        // Contains letters
2967        assert!(!is_valid_credit_card("453957876362abcd"));
2968    }
2969
2970    #[test]
2971    fn test_credit_card_all_zeros() {
2972        // All zeros - 16 digits, technically passes Luhn (sum=0, 0%10=0)
2973        // but not a realistic card number
2974        assert!(is_valid_credit_card("0000000000000000"));
2975    }
2976
2977    #[test]
2978    fn test_credit_card_valid_discover() {
2979        // Valid Discover test number
2980        assert!(is_valid_credit_card("6011111111111117"));
2981    }
2982
2983    // =========================================================================
2984    // Nested Model Tests
2985    // =========================================================================
2986
2987    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2988    struct Address {
2989        street: String,
2990        city: String,
2991        #[serde(skip_serializing_if = "Option::is_none")]
2992        zip: Option<String>,
2993    }
2994
2995    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2996    struct Person {
2997        name: String,
2998        age: i32,
2999        address: Address,
3000        #[serde(skip_serializing_if = "Option::is_none")]
3001        spouse: Option<Box<Person>>,
3002    }
3003
3004    #[test]
3005    fn test_nested_model_dump_basic() {
3006        let person = Person {
3007            name: "Alice".to_string(),
3008            age: 30,
3009            address: Address {
3010                street: "123 Main St".to_string(),
3011                city: "Springfield".to_string(),
3012                zip: Some("12345".to_string()),
3013            },
3014            spouse: None,
3015        };
3016
3017        let json = person.model_dump(DumpOptions::default()).unwrap();
3018        assert_eq!(json["name"], "Alice");
3019        assert_eq!(json["age"], 30);
3020        assert_eq!(json["address"]["street"], "123 Main St");
3021        assert_eq!(json["address"]["city"], "Springfield");
3022        assert_eq!(json["address"]["zip"], "12345");
3023    }
3024
3025    #[test]
3026    fn test_nested_model_dump_exclude_top_level() {
3027        let person = Person {
3028            name: "Alice".to_string(),
3029            age: 30,
3030            address: Address {
3031                street: "123 Main St".to_string(),
3032                city: "Springfield".to_string(),
3033                zip: Some("12345".to_string()),
3034            },
3035            spouse: None,
3036        };
3037
3038        // Exclude only applies to top-level fields
3039        let json = person
3040            .model_dump(DumpOptions::default().exclude(["age"]))
3041            .unwrap();
3042        assert!(json.get("name").is_some());
3043        assert!(json.get("age").is_none());
3044        assert!(json.get("address").is_some()); // Still present
3045        // Nested fields are NOT affected by top-level exclude
3046        assert_eq!(json["address"]["city"], "Springfield");
3047    }
3048
3049    #[test]
3050    fn test_nested_model_dump_exclude_nested_limitation() {
3051        // NOTE: This test documents a LIMITATION.
3052        // In Pydantic, you can exclude nested fields with dot notation: exclude={"address.zip"}
3053        // Our current implementation only supports top-level field exclusion.
3054        let person = Person {
3055            name: "Alice".to_string(),
3056            age: 30,
3057            address: Address {
3058                street: "123 Main St".to_string(),
3059                city: "Springfield".to_string(),
3060                zip: Some("12345".to_string()),
3061            },
3062            spouse: None,
3063        };
3064
3065        // Trying to exclude "address.zip" won't work - it treats it as a top-level field name
3066        let json = person
3067            .model_dump(DumpOptions::default().exclude(["address.zip"]))
3068            .unwrap();
3069        // address.zip is still present because we don't support nested path exclusion
3070        assert_eq!(json["address"]["zip"], "12345");
3071    }
3072
3073    #[test]
3074    fn test_deeply_nested_model_dump() {
3075        let person = Person {
3076            name: "Alice".to_string(),
3077            age: 30,
3078            address: Address {
3079                street: "123 Main St".to_string(),
3080                city: "Springfield".to_string(),
3081                zip: None,
3082            },
3083            spouse: Some(Box::new(Person {
3084                name: "Bob".to_string(),
3085                age: 32,
3086                address: Address {
3087                    street: "456 Oak Ave".to_string(),
3088                    city: "Springfield".to_string(),
3089                    zip: Some("12346".to_string()),
3090                },
3091                spouse: None,
3092            })),
3093        };
3094
3095        let json = person.model_dump(DumpOptions::default()).unwrap();
3096        assert_eq!(json["name"], "Alice");
3097        assert_eq!(json["spouse"]["name"], "Bob");
3098        assert_eq!(json["spouse"]["address"]["street"], "456 Oak Ave");
3099    }
3100
3101    #[test]
3102    fn test_nested_model_exclude_none() {
3103        let person = Person {
3104            name: "Alice".to_string(),
3105            age: 30,
3106            address: Address {
3107                street: "123 Main St".to_string(),
3108                city: "Springfield".to_string(),
3109                zip: None, // Will be skipped by serde skip_serializing_if
3110            },
3111            spouse: None, // Will be skipped by serde skip_serializing_if
3112        };
3113
3114        let json = person
3115            .model_dump(DumpOptions::default().exclude_none())
3116            .unwrap();
3117        assert!(json.get("name").is_some());
3118        // spouse is None and serde skips it, so it's not in the output
3119        assert!(json.get("spouse").is_none());
3120        // Note: exclude_none only affects top-level nulls in model_dump
3121        // Nested nulls are handled by serde's skip_serializing_if
3122    }
3123}