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 % 2 == 0;
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 % 10 == 0
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            if let serde_json::Value::Object(ref mut map) = json_value {
348                for (key, value) in update {
349                    map.insert(key, value);
350                }
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            if let serde_json::Value::Object(ref mut map) = json_value {
856                for (key, value) in update {
857                    map.insert(key, value);
858                }
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            if let serde_json::Value::Object(ref mut map) = json_value {
907                for (key, value) in update {
908                    map.insert(key, value);
909                }
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                        if let Some(current_value) = map.get(field.name) {
1011                            // Parse the default JSON and compare
1012                            if let Ok(default_value) =
1013                                serde_json::from_str::<serde_json::Value>(default_json)
1014                            {
1015                                if current_value == &default_value {
1016                                    map.remove(field.name);
1017                                }
1018                            }
1019                        }
1020                    }
1021                }
1022            }
1023        }
1024
1025        // Apply serialization aliases if by_alias is set
1026        if options.by_alias {
1027            apply_serialization_aliases(&mut value, Self::fields());
1028        }
1029
1030        // Apply remaining options (include/exclude work on the final key names)
1031        if let serde_json::Value::Object(ref mut map) = value {
1032            // Apply include filter
1033            if let Some(ref include) = options.include {
1034                map.retain(|k, _| include.contains(k));
1035            }
1036
1037            // Apply exclude filter
1038            if let Some(ref exclude) = options.exclude {
1039                map.retain(|k, _| !exclude.contains(k));
1040            }
1041
1042            // Apply exclude_none filter
1043            if options.exclude_none {
1044                map.retain(|_, v| !v.is_null());
1045            }
1046        }
1047
1048        Ok(value)
1049    }
1050
1051    /// Serialize a model to a JSON string with default options.
1052    fn sql_model_dump_json(&self) -> std::result::Result<String, serde_json::Error> {
1053        let value = self.sql_model_dump(DumpOptions::default())?;
1054        serde_json::to_string(&value)
1055    }
1056
1057    /// Serialize a model to a pretty-printed JSON string.
1058    fn sql_model_dump_json_pretty(&self) -> std::result::Result<String, serde_json::Error> {
1059        let value = self.sql_model_dump(DumpOptions::default())?;
1060        serde_json::to_string_pretty(&value)
1061    }
1062
1063    /// Serialize with aliases to a JSON string.
1064    fn sql_model_dump_json_by_alias(&self) -> std::result::Result<String, serde_json::Error> {
1065        let value = self.sql_model_dump(DumpOptions::default().by_alias())?;
1066        serde_json::to_string(&value)
1067    }
1068
1069    /// Serialize a model to a JSON string with full options support.
1070    ///
1071    /// This method supports all DumpOptions including the `indent` option:
1072    /// - `indent: None` - compact JSON output
1073    /// - `indent: Some(n)` - pretty-printed with n spaces indentation
1074    ///
1075    /// Compared to `model_dump_json_with_options`, this method also applies
1076    /// Model-specific transformations like serialization aliases.
1077    ///
1078    /// # Example
1079    ///
1080    /// ```ignore
1081    /// use sqlmodel_core::validate::{SqlModelDump, DumpOptions};
1082    ///
1083    /// // With aliases and 2-space indent
1084    /// let json = user.sql_model_dump_json_with_options(
1085    ///     DumpOptions::default().by_alias().indent(2)
1086    /// )?;
1087    /// ```
1088    fn sql_model_dump_json_with_options(
1089        &self,
1090        options: DumpOptions,
1091    ) -> std::result::Result<String, serde_json::Error> {
1092        let value = self.sql_model_dump(DumpOptions {
1093            indent: None, // Don't pass indent to sql_model_dump (it returns Value, not String)
1094            ..options.clone()
1095        })?;
1096
1097        match options.indent {
1098            Some(spaces) => {
1099                let indent_bytes = " ".repeat(spaces).into_bytes();
1100                let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
1101                let mut writer = Vec::new();
1102                let mut ser = serde_json::Serializer::with_formatter(&mut writer, formatter);
1103                serde::Serialize::serialize(&value, &mut ser)?;
1104                // serde_json always produces valid UTF-8, but propagate error instead of panicking
1105                String::from_utf8(writer).map_err(|e| {
1106                    serde_json::Error::io(std::io::Error::new(
1107                        std::io::ErrorKind::InvalidData,
1108                        format!("UTF-8 encoding error: {e}"),
1109                    ))
1110                })
1111            }
1112            None => serde_json::to_string(&value),
1113        }
1114    }
1115}
1116
1117/// Blanket implementation for all Model types that implement Serialize.
1118impl<T: Model + serde::Serialize> SqlModelDump for T {}
1119
1120// ============================================================================
1121// Model Update (sqlmodel_update)
1122// ============================================================================
1123
1124/// Input types for sqlmodel_update().
1125///
1126/// Supports updating models from various input formats.
1127#[derive(Debug, Clone)]
1128pub enum UpdateInput {
1129    /// A HashMap of field names to JSON values.
1130    Dict(HashMap<String, serde_json::Value>),
1131    /// A serde_json::Value for direct updating.
1132    JsonValue(serde_json::Value),
1133}
1134
1135impl From<HashMap<String, serde_json::Value>> for UpdateInput {
1136    fn from(map: HashMap<String, serde_json::Value>) -> Self {
1137        UpdateInput::Dict(map)
1138    }
1139}
1140
1141impl From<serde_json::Value> for UpdateInput {
1142    fn from(value: serde_json::Value) -> Self {
1143        UpdateInput::JsonValue(value)
1144    }
1145}
1146
1147impl From<HashMap<String, Value>> for UpdateInput {
1148    fn from(map: HashMap<String, Value>) -> Self {
1149        let json_map: HashMap<String, serde_json::Value> = map
1150            .into_iter()
1151            .map(|(k, v)| (k, value_to_json(v)))
1152            .collect();
1153        UpdateInput::Dict(json_map)
1154    }
1155}
1156
1157/// Options for sqlmodel_update().
1158#[derive(Debug, Clone, Default)]
1159pub struct UpdateOptions {
1160    /// Only update these fields (if Some). Other fields in the source are ignored.
1161    pub update_fields: Option<std::collections::HashSet<String>>,
1162}
1163
1164impl UpdateOptions {
1165    /// Create new default options.
1166    pub fn new() -> Self {
1167        Self::default()
1168    }
1169
1170    /// Set fields to update.
1171    pub fn update_fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
1172        self.update_fields = Some(fields.into_iter().map(Into::into).collect());
1173        self
1174    }
1175}
1176
1177/// Trait for models that support sqlmodel_update().
1178///
1179/// This enables updating a model instance from a dictionary or another model's values.
1180///
1181/// # Example
1182///
1183/// ```ignore
1184/// use sqlmodel_core::validate::{SqlModelUpdate, UpdateInput, UpdateOptions};
1185///
1186/// let mut user = User { id: 1, name: "Alice".to_string(), age: 30 };
1187///
1188/// // Update from a HashMap
1189/// user.sqlmodel_update(
1190///     HashMap::from([("name".to_string(), serde_json::json!("Bob"))]),
1191///     UpdateOptions::default()
1192/// )?;
1193/// assert_eq!(user.name, "Bob");
1194///
1195/// // Update only specific fields
1196/// user.sqlmodel_update(
1197///     HashMap::from([
1198///         ("name".to_string(), serde_json::json!("Carol")),
1199///         ("age".to_string(), serde_json::json!(25))
1200///     ]),
1201///     UpdateOptions::default().update_fields(["name"])
1202/// )?;
1203/// assert_eq!(user.name, "Carol");
1204/// assert_eq!(user.age, 30); // age was not updated
1205/// ```
1206pub trait SqlModelUpdate: Model + serde::Serialize + DeserializeOwned {
1207    /// Update a model instance from input.
1208    ///
1209    /// This method merges values from the input into the current model.
1210    /// Only fields present in the input (and allowed by `update_fields` option)
1211    /// are updated.
1212    ///
1213    /// # Arguments
1214    ///
1215    /// * `input` - The source of update values (Dict or JsonValue)
1216    /// * `options` - Update options controlling which fields to update
1217    ///
1218    /// # Returns
1219    ///
1220    /// Ok(()) if the update succeeds, or a validation error if the resulting
1221    /// model fails validation.
1222    fn sqlmodel_update(
1223        &mut self,
1224        input: impl Into<UpdateInput>,
1225        options: UpdateOptions,
1226    ) -> ValidateResult<()> {
1227        let input = input.into();
1228
1229        // Convert input to a map
1230        let update_map = match input {
1231            UpdateInput::Dict(map) => map,
1232            UpdateInput::JsonValue(value) => {
1233                if let serde_json::Value::Object(map) = value {
1234                    map.into_iter().collect()
1235                } else {
1236                    let mut err = ValidationError::new();
1237                    err.add(
1238                        "_update",
1239                        ValidationErrorKind::Custom,
1240                        "Update input must be an object".to_string(),
1241                    );
1242                    return Err(err);
1243                }
1244            }
1245        };
1246
1247        // Serialize current model to JSON
1248        let mut current = serde_json::to_value(&*self).map_err(|e| {
1249            let mut err = ValidationError::new();
1250            err.add(
1251                "_model",
1252                ValidationErrorKind::Custom,
1253                format!("Failed to serialize model: {e}"),
1254            );
1255            err
1256        })?;
1257
1258        // Get valid field names from model metadata
1259        let valid_fields: std::collections::HashSet<&str> =
1260            Self::fields().iter().map(|f| f.name).collect();
1261
1262        // Update the current JSON with new values
1263        if let serde_json::Value::Object(ref mut current_map) = current {
1264            for (key, value) in update_map {
1265                // Check if field is valid
1266                if !valid_fields.contains(key.as_str()) {
1267                    let mut err = ValidationError::new();
1268                    err.add(
1269                        &key,
1270                        ValidationErrorKind::Custom,
1271                        format!("Unknown field: {key}"),
1272                    );
1273                    return Err(err);
1274                }
1275
1276                // Check if field is allowed by update_fields option
1277                if let Some(ref allowed) = options.update_fields {
1278                    if !allowed.contains(&key) {
1279                        continue; // Skip fields not in update_fields
1280                    }
1281                }
1282
1283                // Update the field
1284                current_map.insert(key, value);
1285            }
1286        }
1287
1288        // Deserialize back to model (this also validates)
1289        let updated: Self = serde_json::from_value(current).map_err(|e| {
1290            let mut err = ValidationError::new();
1291            err.add(
1292                "_model",
1293                ValidationErrorKind::Custom,
1294                format!("Update failed validation: {e}"),
1295            );
1296            err
1297        })?;
1298
1299        // Replace self with the updated model
1300        *self = updated;
1301
1302        Ok(())
1303    }
1304
1305    /// Update a model instance from a HashMap with default options.
1306    fn sqlmodel_update_dict(
1307        &mut self,
1308        dict: HashMap<String, serde_json::Value>,
1309    ) -> ValidateResult<()> {
1310        self.sqlmodel_update(dict, UpdateOptions::default())
1311    }
1312
1313    /// Copy non-None/non-null values from another model into this one.
1314    ///
1315    /// This is useful for partial updates where you have a "patch" model
1316    /// with only the fields that should be updated (non-None values).
1317    ///
1318    /// # Arguments
1319    ///
1320    /// * `source` - The source model to copy values from
1321    /// * `options` - Update options controlling which fields to update
1322    ///
1323    /// # Example
1324    ///
1325    /// ```ignore
1326    /// let mut user = User { id: 1, name: "Alice".to_string(), age: Some(30) };
1327    /// let patch = User { id: 0, name: "Bob".to_string(), age: None };
1328    ///
1329    /// // Only update name, skip None age
1330    /// user.sqlmodel_update_from(&patch, UpdateOptions::default())?;
1331    /// assert_eq!(user.name, "Bob");
1332    /// assert_eq!(user.age, Some(30)); // Unchanged because patch.age is None
1333    /// ```
1334    fn sqlmodel_update_from(&mut self, source: &Self, options: UpdateOptions) -> ValidateResult<()>
1335    where
1336        Self: Sized,
1337    {
1338        // Serialize source to JSON
1339        let source_json = serde_json::to_value(source).map_err(|e| {
1340            let mut err = ValidationError::new();
1341            err.add(
1342                "_source",
1343                ValidationErrorKind::Custom,
1344                format!("Failed to serialize source model: {e}"),
1345            );
1346            err
1347        })?;
1348
1349        // Filter out null values (None fields)
1350        let update_map: HashMap<String, serde_json::Value> =
1351            if let serde_json::Value::Object(map) = source_json {
1352                map.into_iter().filter(|(_, v)| !v.is_null()).collect()
1353            } else {
1354                let mut err = ValidationError::new();
1355                err.add(
1356                    "_source",
1357                    ValidationErrorKind::Custom,
1358                    "Source model must serialize to an object".to_string(),
1359                );
1360                return Err(err);
1361            };
1362
1363        self.sqlmodel_update(update_map, options)
1364    }
1365}
1366
1367/// Blanket implementation for all Model types that implement Serialize + DeserializeOwned.
1368impl<T: Model + serde::Serialize + DeserializeOwned> SqlModelUpdate for T {}
1369
1370#[cfg(test)]
1371mod tests {
1372    use super::*;
1373    use serde::{Deserialize, Serialize};
1374
1375    #[test]
1376    fn test_matches_email_pattern() {
1377        let email_pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$";
1378
1379        assert!(matches_pattern("test@example.com", email_pattern));
1380        assert!(matches_pattern("user.name+tag@domain.org", email_pattern));
1381        assert!(!matches_pattern("invalid", email_pattern));
1382        assert!(!matches_pattern("@example.com", email_pattern));
1383        assert!(!matches_pattern("test@", email_pattern));
1384    }
1385
1386    #[test]
1387    fn test_matches_url_pattern() {
1388        let url_pattern = r"^https?://[^\s/$.?#].[^\s]*$";
1389
1390        assert!(matches_pattern("https://example.com", url_pattern));
1391        assert!(matches_pattern("http://example.com/path", url_pattern));
1392        assert!(!matches_pattern("ftp://example.com", url_pattern));
1393        assert!(!matches_pattern("not a url", url_pattern));
1394    }
1395
1396    #[test]
1397    fn test_matches_phone_pattern() {
1398        let phone_pattern = r"^\+?[1-9]\d{1,14}$";
1399
1400        assert!(matches_pattern("+12025551234", phone_pattern));
1401        assert!(matches_pattern("12025551234", phone_pattern));
1402        assert!(!matches_pattern("0123456789", phone_pattern)); // Can't start with 0
1403        assert!(!matches_pattern("abc", phone_pattern));
1404    }
1405
1406    #[test]
1407    fn test_matches_uuid_pattern() {
1408        let uuid_pattern =
1409            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}$";
1410
1411        assert!(matches_pattern(
1412            "550e8400-e29b-41d4-a716-446655440000",
1413            uuid_pattern
1414        ));
1415        assert!(matches_pattern(
1416            "550E8400-E29B-41D4-A716-446655440000",
1417            uuid_pattern
1418        ));
1419        assert!(!matches_pattern("invalid-uuid", uuid_pattern));
1420        assert!(!matches_pattern(
1421            "550e8400e29b41d4a716446655440000",
1422            uuid_pattern
1423        ));
1424    }
1425
1426    #[test]
1427    fn test_matches_alphanumeric_pattern() {
1428        let alphanumeric_pattern = r"^[a-zA-Z0-9]+$";
1429
1430        assert!(matches_pattern("abc123", alphanumeric_pattern));
1431        assert!(matches_pattern("ABC", alphanumeric_pattern));
1432        assert!(matches_pattern("123", alphanumeric_pattern));
1433        assert!(!matches_pattern("abc-123", alphanumeric_pattern));
1434        assert!(!matches_pattern("hello world", alphanumeric_pattern));
1435    }
1436
1437    #[test]
1438    fn test_invalid_pattern_returns_false() {
1439        // Invalid regex pattern (unclosed bracket)
1440        let invalid_pattern = r"[unclosed";
1441        assert!(!matches_pattern("anything", invalid_pattern));
1442    }
1443
1444    #[test]
1445    fn test_validate_pattern_valid() {
1446        assert!(validate_pattern(r"^[a-z]+$").is_none());
1447        assert!(validate_pattern(r"^\d{4}-\d{2}-\d{2}$").is_none());
1448    }
1449
1450    #[test]
1451    fn test_validate_pattern_invalid() {
1452        let result = validate_pattern(r"[unclosed");
1453        assert!(result.is_some());
1454        assert!(result.unwrap().contains("invalid regex pattern"));
1455    }
1456
1457    #[test]
1458    fn test_regex_caching() {
1459        let pattern = r"^test\d+$";
1460
1461        // First call compiles the regex
1462        assert!(matches_pattern("test123", pattern));
1463
1464        // Second call should use cached regex
1465        assert!(matches_pattern("test456", pattern));
1466        assert!(!matches_pattern("invalid", pattern));
1467    }
1468
1469    #[test]
1470    fn test_empty_string() {
1471        let pattern = r"^.+$"; // At least one character
1472        assert!(!matches_pattern("", pattern));
1473
1474        let empty_allowed = r"^.*$"; // Zero or more characters
1475        assert!(matches_pattern("", empty_allowed));
1476    }
1477
1478    #[test]
1479    fn test_special_characters() {
1480        let pattern = r"^[a-z]+$";
1481        assert!(!matches_pattern("hello<script>", pattern));
1482        assert!(!matches_pattern("test'; DROP TABLE users;--", pattern));
1483    }
1484
1485    // =========================================================================
1486    // model_validate tests
1487    // =========================================================================
1488
1489    #[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
1490    struct TestUser {
1491        name: String,
1492        age: i32,
1493        #[serde(default)]
1494        active: bool,
1495    }
1496
1497    #[test]
1498    fn test_model_validate_from_json() {
1499        let json = r#"{"name": "Alice", "age": 30}"#;
1500        let user: TestUser = TestUser::model_validate_json(json).unwrap();
1501        assert_eq!(user.name, "Alice");
1502        assert_eq!(user.age, 30);
1503        assert!(!user.active); // default
1504    }
1505
1506    #[test]
1507    fn test_model_validate_from_json_value() {
1508        let json_value = serde_json::json!({"name": "Bob", "age": 25, "active": true});
1509        let user: TestUser =
1510            TestUser::model_validate(json_value, ValidateOptions::default()).unwrap();
1511        assert_eq!(user.name, "Bob");
1512        assert_eq!(user.age, 25);
1513        assert!(user.active);
1514    }
1515
1516    #[test]
1517    fn test_model_validate_from_dict() {
1518        let mut dict = HashMap::new();
1519        dict.insert("name".to_string(), Value::Text("Charlie".to_string()));
1520        dict.insert("age".to_string(), Value::Int(35));
1521        dict.insert("active".to_string(), Value::Bool(true));
1522
1523        let user: TestUser = TestUser::model_validate_dict(dict).unwrap();
1524        assert_eq!(user.name, "Charlie");
1525        assert_eq!(user.age, 35);
1526        assert!(user.active);
1527    }
1528
1529    #[test]
1530    fn test_model_validate_invalid_json() {
1531        let json = r#"{"name": "Invalid"}"#; // missing required 'age' field
1532        let result: ValidateResult<TestUser> = TestUser::model_validate_json(json);
1533        assert!(result.is_err());
1534        let err = result.unwrap_err();
1535        assert!(!err.is_empty());
1536    }
1537
1538    #[test]
1539    fn test_model_validate_malformed_json() {
1540        let json = r#"{"name": "Alice", age: 30}"#; // invalid JSON syntax
1541        let result: ValidateResult<TestUser> = TestUser::model_validate_json(json);
1542        assert!(result.is_err());
1543        let err = result.unwrap_err();
1544        assert!(
1545            err.errors
1546                .iter()
1547                .any(|e| e.message.contains("Invalid JSON"))
1548        );
1549    }
1550
1551    #[test]
1552    fn test_model_validate_with_update() {
1553        let json = r#"{"name": "Original", "age": 20}"#;
1554        let mut update = HashMap::new();
1555        update.insert("name".to_string(), serde_json::json!("Updated"));
1556
1557        let options = ValidateOptions::new().with_update(update);
1558        let user: TestUser = TestUser::model_validate(json, options).unwrap();
1559        assert_eq!(user.name, "Updated"); // overridden by update
1560        assert_eq!(user.age, 20);
1561    }
1562
1563    #[test]
1564    fn test_model_validate_strict_mode() {
1565        let json = r#"{"name": "Alice", "age": 30}"#;
1566        let options = ValidateOptions::new().strict();
1567        let user: TestUser = TestUser::model_validate(json, options).unwrap();
1568        assert_eq!(user.name, "Alice");
1569        assert_eq!(user.age, 30);
1570    }
1571
1572    #[test]
1573    fn test_validate_options_builder() {
1574        let mut context = HashMap::new();
1575        context.insert("key".to_string(), serde_json::json!("value"));
1576
1577        let options = ValidateOptions::new()
1578            .strict()
1579            .from_attributes()
1580            .with_context(context.clone());
1581
1582        assert!(options.strict);
1583        assert!(options.from_attributes);
1584        assert!(options.context.is_some());
1585        assert_eq!(
1586            options.context.unwrap().get("key"),
1587            Some(&serde_json::json!("value"))
1588        );
1589    }
1590
1591    #[test]
1592    fn test_validate_input_from_conversions() {
1593        // From String
1594        let input: ValidateInput = "{}".to_string().into();
1595        assert!(matches!(input, ValidateInput::Json(_)));
1596
1597        // From &str
1598        let input: ValidateInput = "{}".into();
1599        assert!(matches!(input, ValidateInput::Json(_)));
1600
1601        // From serde_json::Value
1602        let input: ValidateInput = serde_json::json!({}).into();
1603        assert!(matches!(input, ValidateInput::JsonValue(_)));
1604
1605        // From HashMap
1606        let map: HashMap<String, Value> = HashMap::new();
1607        let input: ValidateInput = map.into();
1608        assert!(matches!(input, ValidateInput::Dict(_)));
1609    }
1610
1611    #[test]
1612    fn test_value_to_json_conversions() {
1613        assert_eq!(value_to_json(Value::Null), serde_json::Value::Null);
1614        assert_eq!(value_to_json(Value::Bool(true)), serde_json::json!(true));
1615        assert_eq!(value_to_json(Value::Int(42)), serde_json::json!(42));
1616        assert_eq!(value_to_json(Value::BigInt(100)), serde_json::json!(100));
1617        assert_eq!(
1618            value_to_json(Value::Text("hello".to_string())),
1619            serde_json::json!("hello")
1620        );
1621        // UUID is [u8; 16]
1622        let uuid_bytes: [u8; 16] = [
1623            0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, 0xa7, 0x16, 0x44, 0x66, 0x55, 0x44,
1624            0x00, 0x00,
1625        ];
1626        assert_eq!(
1627            value_to_json(Value::Uuid(uuid_bytes)),
1628            serde_json::json!("550e8400-e29b-41d4-a716-446655440000")
1629        );
1630
1631        // Array conversion
1632        let arr = vec![Value::Int(1), Value::Int(2), Value::Int(3)];
1633        assert_eq!(
1634            value_to_json(Value::Array(arr)),
1635            serde_json::json!([1, 2, 3])
1636        );
1637    }
1638
1639    // =========================================================================
1640    // model_dump tests
1641    // =========================================================================
1642
1643    #[derive(Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
1644    struct TestProduct {
1645        name: String,
1646        price: f64,
1647        #[serde(skip_serializing_if = "Option::is_none")]
1648        description: Option<String>,
1649    }
1650
1651    #[test]
1652    fn test_model_dump_default() {
1653        let product = TestProduct {
1654            name: "Widget".to_string(),
1655            price: 19.99,
1656            description: Some("A useful widget".to_string()),
1657        };
1658        let json = product.model_dump(DumpOptions::default()).unwrap();
1659        assert_eq!(json["name"], "Widget");
1660        assert_eq!(json["price"], 19.99);
1661        assert_eq!(json["description"], "A useful widget");
1662    }
1663
1664    #[test]
1665    fn test_model_dump_json() {
1666        let product = TestProduct {
1667            name: "Gadget".to_string(),
1668            price: 29.99,
1669            description: None,
1670        };
1671        let json_str = product.model_dump_json().unwrap();
1672        assert!(json_str.contains("Gadget"));
1673        assert!(json_str.contains("29.99"));
1674    }
1675
1676    #[test]
1677    fn test_model_dump_json_pretty() {
1678        let product = TestProduct {
1679            name: "Gadget".to_string(),
1680            price: 29.99,
1681            description: None,
1682        };
1683        let json_str = product.model_dump_json_pretty().unwrap();
1684        // Pretty print should have newlines
1685        assert!(json_str.contains('\n'));
1686        assert!(json_str.contains("Gadget"));
1687    }
1688
1689    #[test]
1690    fn test_model_dump_json_with_options_compact() {
1691        let product = TestProduct {
1692            name: "Widget".to_string(),
1693            price: 19.99,
1694            description: Some("A widget".to_string()),
1695        };
1696
1697        // Compact JSON (no indent)
1698        let json_str = product
1699            .model_dump_json_with_options(DumpOptions::default())
1700            .unwrap();
1701        assert!(!json_str.contains('\n')); // No newlines in compact mode
1702        assert!(json_str.contains("Widget"));
1703        assert!(json_str.contains("19.99"));
1704    }
1705
1706    #[test]
1707    fn test_model_dump_json_with_options_indent() {
1708        let product = TestProduct {
1709            name: "Widget".to_string(),
1710            price: 19.99,
1711            description: Some("A widget".to_string()),
1712        };
1713
1714        // 2-space indentation
1715        let json_str = product
1716            .model_dump_json_with_options(DumpOptions::default().indent(2))
1717            .unwrap();
1718        assert!(json_str.contains('\n')); // Has newlines
1719        assert!(json_str.contains("  \"name\"")); // 2-space indent
1720        assert!(json_str.contains("Widget"));
1721
1722        // 4-space indentation
1723        let json_str = product
1724            .model_dump_json_with_options(DumpOptions::default().indent(4))
1725            .unwrap();
1726        assert!(json_str.contains("    \"name\"")); // 4-space indent
1727    }
1728
1729    #[test]
1730    fn test_model_dump_json_with_options_combined() {
1731        let product = TestProduct {
1732            name: "Widget".to_string(),
1733            price: 19.99,
1734            description: Some("A widget".to_string()),
1735        };
1736
1737        // Combine indent with exclude
1738        let json_str = product
1739            .model_dump_json_with_options(DumpOptions::default().exclude(["price"]).indent(2))
1740            .unwrap();
1741        assert!(json_str.contains('\n')); // Has newlines
1742        assert!(json_str.contains("Widget"));
1743        assert!(!json_str.contains("19.99")); // price is excluded
1744    }
1745
1746    #[test]
1747    fn test_dump_options_indent_builder() {
1748        let options = DumpOptions::new().indent(4);
1749        assert_eq!(options.indent, Some(4));
1750
1751        // Can combine with other options
1752        let options2 = DumpOptions::new()
1753            .indent(2)
1754            .by_alias()
1755            .exclude(["password"]);
1756        assert_eq!(options2.indent, Some(2));
1757        assert!(options2.by_alias);
1758        assert!(options2.exclude.unwrap().contains("password"));
1759    }
1760
1761    #[test]
1762    fn test_model_dump_include() {
1763        let product = TestProduct {
1764            name: "Widget".to_string(),
1765            price: 19.99,
1766            description: Some("A widget".to_string()),
1767        };
1768        let options = DumpOptions::new().include(["name"]);
1769        let json = product.model_dump(options).unwrap();
1770        assert!(json.get("name").is_some());
1771        assert!(json.get("price").is_none());
1772        assert!(json.get("description").is_none());
1773    }
1774
1775    #[test]
1776    fn test_model_dump_exclude() {
1777        let product = TestProduct {
1778            name: "Widget".to_string(),
1779            price: 19.99,
1780            description: Some("A widget".to_string()),
1781        };
1782        let options = DumpOptions::new().exclude(["description"]);
1783        let json = product.model_dump(options).unwrap();
1784        assert!(json.get("name").is_some());
1785        assert!(json.get("price").is_some());
1786        assert!(json.get("description").is_none());
1787    }
1788
1789    #[test]
1790    fn test_model_dump_exclude_none() {
1791        let product = TestProduct {
1792            name: "Widget".to_string(),
1793            price: 19.99,
1794            description: None,
1795        };
1796        // Note: serde skip_serializing_if already handles this
1797        // But we can still test the exclude_none flag
1798        let options = DumpOptions::new().exclude_none();
1799        let json = product.model_dump(options).unwrap();
1800        assert!(json.get("name").is_some());
1801        // description would be None, but serde already skips it
1802    }
1803
1804    #[test]
1805    fn test_dump_options_builder() {
1806        let options = DumpOptions::new()
1807            .json()
1808            .include(["name", "age"])
1809            .exclude(["password"])
1810            .by_alias()
1811            .exclude_none()
1812            .exclude_defaults()
1813            .round_trip();
1814
1815        assert_eq!(options.mode, DumpMode::Json);
1816        assert!(options.include.is_some());
1817        assert!(options.exclude.is_some());
1818        assert!(options.by_alias);
1819        assert!(options.exclude_none);
1820        assert!(options.exclude_defaults);
1821        assert!(options.round_trip);
1822    }
1823
1824    #[test]
1825    fn test_dump_mode_default() {
1826        assert_eq!(DumpMode::default(), DumpMode::Json);
1827    }
1828
1829    #[test]
1830    fn test_model_dump_include_exclude_combined() {
1831        let user = TestUser {
1832            name: "Alice".to_string(),
1833            age: 30,
1834            active: true,
1835        };
1836        // Include name and age, but exclude age
1837        let options = DumpOptions::new().include(["name", "age"]).exclude(["age"]);
1838        let json = user.model_dump(options).unwrap();
1839        // Include is applied first, then exclude
1840        assert!(json.get("name").is_some());
1841        assert!(json.get("age").is_none());
1842        assert!(json.get("active").is_none());
1843    }
1844
1845    #[test]
1846    fn test_model_dump_accepts_python_mode_and_round_trip() {
1847        let product = TestProduct {
1848            name: "Widget".to_string(),
1849            price: 19.99,
1850            description: Some("A useful widget".to_string()),
1851        };
1852        let json = product
1853            .model_dump(DumpOptions::default().python().round_trip())
1854            .unwrap();
1855
1856        assert_eq!(json["name"], "Widget");
1857        assert_eq!(json["price"], 19.99);
1858        assert_eq!(json["description"], "A useful widget");
1859    }
1860
1861    // ========================================================================
1862    // Alias Tests
1863    // ========================================================================
1864
1865    use crate::{FieldInfo, Row, SqlType};
1866
1867    /// Test model with aliases for validation and serialization tests.
1868    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
1869    struct TestAliasedUser {
1870        id: i64,
1871        name: String,
1872        email: String,
1873    }
1874
1875    impl Model for TestAliasedUser {
1876        const TABLE_NAME: &'static str = "users";
1877        const PRIMARY_KEY: &'static [&'static str] = &["id"];
1878
1879        fn fields() -> &'static [FieldInfo] {
1880            static FIELDS: &[FieldInfo] = &[
1881                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
1882                FieldInfo::new("name", "name", SqlType::Text)
1883                    .validation_alias("userName")
1884                    .serialization_alias("displayName"),
1885                FieldInfo::new("email", "email", SqlType::Text).alias("emailAddress"), // Both input and output
1886            ];
1887            FIELDS
1888        }
1889
1890        fn to_row(&self) -> Vec<(&'static str, Value)> {
1891            vec![
1892                ("id", Value::BigInt(self.id)),
1893                ("name", Value::Text(self.name.clone())),
1894                ("email", Value::Text(self.email.clone())),
1895            ]
1896        }
1897
1898        fn from_row(row: &Row) -> crate::Result<Self> {
1899            Ok(Self {
1900                id: row.get_named("id")?,
1901                name: row.get_named("name")?,
1902                email: row.get_named("email")?,
1903            })
1904        }
1905
1906        fn primary_key_value(&self) -> Vec<Value> {
1907            vec![Value::BigInt(self.id)]
1908        }
1909
1910        fn is_new(&self) -> bool {
1911            false
1912        }
1913    }
1914
1915    #[test]
1916    fn test_apply_validation_aliases() {
1917        let fields = TestAliasedUser::fields();
1918
1919        // Test with validation_alias
1920        let mut json = serde_json::json!({
1921            "id": 1,
1922            "userName": "Alice",
1923            "email": "alice@example.com"
1924        });
1925        apply_validation_aliases(&mut json, fields);
1926
1927        // userName should be renamed to name
1928        assert_eq!(json["name"], "Alice");
1929        assert!(json.get("userName").is_none());
1930
1931        // Test with regular alias
1932        let mut json2 = serde_json::json!({
1933            "id": 1,
1934            "name": "Bob",
1935            "emailAddress": "bob@example.com"
1936        });
1937        apply_validation_aliases(&mut json2, fields);
1938
1939        // emailAddress should be renamed to email
1940        assert_eq!(json2["email"], "bob@example.com");
1941        assert!(json2.get("emailAddress").is_none());
1942    }
1943
1944    #[test]
1945    fn test_apply_serialization_aliases() {
1946        let fields = TestAliasedUser::fields();
1947
1948        let mut json = serde_json::json!({
1949            "id": 1,
1950            "name": "Alice",
1951            "email": "alice@example.com"
1952        });
1953        apply_serialization_aliases(&mut json, fields);
1954
1955        // name should be renamed to displayName (serialization_alias)
1956        assert_eq!(json["displayName"], "Alice");
1957        assert!(json.get("name").is_none());
1958
1959        // email should be renamed to emailAddress (regular alias)
1960        assert_eq!(json["emailAddress"], "alice@example.com");
1961        assert!(json.get("email").is_none());
1962    }
1963
1964    #[test]
1965    fn test_sql_model_validate_with_validation_alias() {
1966        // Use validation_alias in input
1967        let json = r#"{"id": 1, "userName": "Alice", "email": "alice@example.com"}"#;
1968        let user: TestAliasedUser = TestAliasedUser::sql_model_validate_json(json).unwrap();
1969
1970        assert_eq!(user.id, 1);
1971        assert_eq!(user.name, "Alice");
1972        assert_eq!(user.email, "alice@example.com");
1973    }
1974
1975    #[test]
1976    fn test_sql_model_validate_with_regular_alias() {
1977        // Use regular alias in input
1978        let json = r#"{"id": 1, "name": "Bob", "emailAddress": "bob@example.com"}"#;
1979        let user: TestAliasedUser = TestAliasedUser::sql_model_validate_json(json).unwrap();
1980
1981        assert_eq!(user.id, 1);
1982        assert_eq!(user.name, "Bob");
1983        assert_eq!(user.email, "bob@example.com");
1984    }
1985
1986    #[test]
1987    fn test_sql_model_validate_with_field_name() {
1988        // Use actual field name (should still work)
1989        let json = r#"{"id": 1, "name": "Charlie", "email": "charlie@example.com"}"#;
1990        let user: TestAliasedUser = TestAliasedUser::sql_model_validate_json(json).unwrap();
1991
1992        assert_eq!(user.id, 1);
1993        assert_eq!(user.name, "Charlie");
1994        assert_eq!(user.email, "charlie@example.com");
1995    }
1996
1997    #[test]
1998    fn test_sql_model_dump_by_alias() {
1999        let user = TestAliasedUser {
2000            id: 1,
2001            name: "Alice".to_string(),
2002            email: "alice@example.com".to_string(),
2003        };
2004
2005        let json = user
2006            .sql_model_dump(DumpOptions::default().by_alias())
2007            .unwrap();
2008
2009        // name should be serialized as displayName
2010        assert_eq!(json["displayName"], "Alice");
2011        assert!(json.get("name").is_none());
2012
2013        // email should be serialized as emailAddress
2014        assert_eq!(json["emailAddress"], "alice@example.com");
2015        assert!(json.get("email").is_none());
2016    }
2017
2018    #[test]
2019    fn test_sql_model_dump_without_alias() {
2020        let user = TestAliasedUser {
2021            id: 1,
2022            name: "Alice".to_string(),
2023            email: "alice@example.com".to_string(),
2024        };
2025
2026        // Without by_alias, use original field names
2027        let json = user.sql_model_dump(DumpOptions::default()).unwrap();
2028
2029        assert_eq!(json["name"], "Alice");
2030        assert_eq!(json["email"], "alice@example.com");
2031        assert!(json.get("displayName").is_none());
2032        assert!(json.get("emailAddress").is_none());
2033    }
2034
2035    #[test]
2036    fn test_sql_model_dump_accepts_python_mode_and_round_trip() {
2037        let user = TestAliasedUser {
2038            id: 1,
2039            name: "Alice".to_string(),
2040            email: "alice@example.com".to_string(),
2041        };
2042        let json = user
2043            .sql_model_dump(DumpOptions::default().python().round_trip())
2044            .unwrap();
2045
2046        assert_eq!(json["name"], "Alice");
2047        assert_eq!(json["email"], "alice@example.com");
2048    }
2049
2050    #[test]
2051    fn test_tracked_model_dump_accepts_python_mode_and_round_trip() {
2052        let user = TestAliasedUser {
2053            id: 1,
2054            name: "Alice".to_string(),
2055            email: "alice@example.com".to_string(),
2056        };
2057        let tracked = crate::TrackedModel::all_fields_set(user);
2058        let json = tracked
2059            .sql_model_dump(DumpOptions::default().python().round_trip())
2060            .unwrap();
2061
2062        assert_eq!(json["name"], "Alice");
2063        assert_eq!(json["email"], "alice@example.com");
2064    }
2065
2066    #[test]
2067    fn test_alias_does_not_overwrite_existing() {
2068        let fields = TestAliasedUser::fields();
2069
2070        // If both alias and field name are present, field name wins
2071        let mut json = serde_json::json!({
2072            "id": 1,
2073            "name": "FieldName",
2074            "userName": "AliasName",
2075            "email": "test@example.com"
2076        });
2077        apply_validation_aliases(&mut json, fields);
2078
2079        // Original "name" field should be preserved
2080        assert_eq!(json["name"], "FieldName");
2081        // userName should be removed (but couldn't insert because "name" exists)
2082        assert!(json.get("userName").is_none());
2083    }
2084
2085    // ========================================================================
2086    // Computed Field Tests
2087    // ========================================================================
2088
2089    /// Test model with computed fields.
2090    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2091    struct TestUserWithComputed {
2092        id: i64,
2093        first_name: String,
2094        last_name: String,
2095        #[serde(default)]
2096        full_name: String, // Computed field - derived from first_name + last_name
2097    }
2098
2099    impl Model for TestUserWithComputed {
2100        const TABLE_NAME: &'static str = "users";
2101        const PRIMARY_KEY: &'static [&'static str] = &["id"];
2102
2103        fn fields() -> &'static [FieldInfo] {
2104            static FIELDS: &[FieldInfo] = &[
2105                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2106                FieldInfo::new("first_name", "first_name", SqlType::Text),
2107                FieldInfo::new("last_name", "last_name", SqlType::Text),
2108                FieldInfo::new("full_name", "full_name", SqlType::Text).computed(true),
2109            ];
2110            FIELDS
2111        }
2112
2113        fn to_row(&self) -> Vec<(&'static str, Value)> {
2114            // Computed field is NOT included in DB operations
2115            vec![
2116                ("id", Value::BigInt(self.id)),
2117                ("first_name", Value::Text(self.first_name.clone())),
2118                ("last_name", Value::Text(self.last_name.clone())),
2119            ]
2120        }
2121
2122        fn from_row(row: &Row) -> crate::Result<Self> {
2123            Ok(Self {
2124                id: row.get_named("id")?,
2125                first_name: row.get_named("first_name")?,
2126                last_name: row.get_named("last_name")?,
2127                // Computed field initialized with Default (empty string)
2128                full_name: String::new(),
2129            })
2130        }
2131
2132        fn primary_key_value(&self) -> Vec<Value> {
2133            vec![Value::BigInt(self.id)]
2134        }
2135
2136        fn is_new(&self) -> bool {
2137            false
2138        }
2139    }
2140
2141    #[test]
2142    fn test_computed_field_included_by_default() {
2143        let user = TestUserWithComputed {
2144            id: 1,
2145            first_name: "John".to_string(),
2146            last_name: "Doe".to_string(),
2147            full_name: "John Doe".to_string(),
2148        };
2149
2150        // By default, computed fields ARE included in model_dump
2151        let json = user.sql_model_dump(DumpOptions::default()).unwrap();
2152
2153        assert_eq!(json["id"], 1);
2154        assert_eq!(json["first_name"], "John");
2155        assert_eq!(json["last_name"], "Doe");
2156        assert_eq!(json["full_name"], "John Doe"); // Computed field is present
2157    }
2158
2159    #[test]
2160    fn test_computed_field_excluded_with_option() {
2161        let user = TestUserWithComputed {
2162            id: 1,
2163            first_name: "John".to_string(),
2164            last_name: "Doe".to_string(),
2165            full_name: "John Doe".to_string(),
2166        };
2167
2168        // With exclude_computed_fields, computed fields are excluded
2169        let json = user
2170            .sql_model_dump(DumpOptions::default().exclude_computed_fields())
2171            .unwrap();
2172
2173        assert_eq!(json["id"], 1);
2174        assert_eq!(json["first_name"], "John");
2175        assert_eq!(json["last_name"], "Doe");
2176        assert!(json.get("full_name").is_none()); // Computed field is excluded
2177    }
2178
2179    #[test]
2180    fn test_computed_field_not_in_to_row() {
2181        let user = TestUserWithComputed {
2182            id: 1,
2183            first_name: "Jane".to_string(),
2184            last_name: "Smith".to_string(),
2185            full_name: "Jane Smith".to_string(),
2186        };
2187
2188        // to_row() should not include computed field (for DB INSERT/UPDATE)
2189        let row = user.to_row();
2190
2191        // Should have 3 fields: id, first_name, last_name
2192        assert_eq!(row.len(), 3);
2193        let field_names: Vec<&str> = row.iter().map(|(name, _)| *name).collect();
2194        assert!(field_names.contains(&"id"));
2195        assert!(field_names.contains(&"first_name"));
2196        assert!(field_names.contains(&"last_name"));
2197        assert!(!field_names.contains(&"full_name")); // Computed field NOT in row
2198    }
2199
2200    #[test]
2201    fn test_computed_field_select_fields_excludes() {
2202        let fields = TestUserWithComputed::fields();
2203
2204        // Check that computed field is marked
2205        let computed: Vec<&FieldInfo> = fields.iter().filter(|f| f.computed).collect();
2206        assert_eq!(computed.len(), 1);
2207        assert_eq!(computed[0].name, "full_name");
2208
2209        // Non-computed fields
2210        let non_computed: Vec<&FieldInfo> = fields.iter().filter(|f| !f.computed).collect();
2211        assert_eq!(non_computed.len(), 3);
2212    }
2213
2214    #[test]
2215    fn test_computed_field_with_other_dump_options() {
2216        let user = TestUserWithComputed {
2217            id: 1,
2218            first_name: "John".to_string(),
2219            last_name: "Doe".to_string(),
2220            full_name: "John Doe".to_string(),
2221        };
2222
2223        // Combine exclude_computed_fields with include filter
2224        let json = user
2225            .sql_model_dump(DumpOptions::default().exclude_computed_fields().include([
2226                "id",
2227                "first_name",
2228                "full_name",
2229            ]))
2230            .unwrap();
2231
2232        // full_name is excluded because it's computed, even though in include list
2233        // (exclude_computed_fields is applied before include filter)
2234        assert!(json.get("id").is_some());
2235        assert!(json.get("first_name").is_some());
2236        assert!(json.get("full_name").is_none()); // Excluded as computed
2237        assert!(json.get("last_name").is_none()); // Not in include list
2238    }
2239
2240    #[test]
2241    fn test_dump_options_exclude_computed_fields_builder() {
2242        let options = DumpOptions::new().exclude_computed_fields();
2243        assert!(options.exclude_computed_fields);
2244
2245        // Can combine with other options
2246        let options2 = DumpOptions::new()
2247            .exclude_computed_fields()
2248            .by_alias()
2249            .exclude_none();
2250        assert!(options2.exclude_computed_fields);
2251        assert!(options2.by_alias);
2252        assert!(options2.exclude_none);
2253    }
2254
2255    /// Test model with both computed fields AND serialization aliases.
2256    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2257    struct TestUserWithComputedAndAlias {
2258        id: i64,
2259        first_name: String,
2260        #[serde(default)]
2261        display_name: String, // Computed field that also has an alias
2262    }
2263
2264    impl Model for TestUserWithComputedAndAlias {
2265        const TABLE_NAME: &'static str = "users";
2266        const PRIMARY_KEY: &'static [&'static str] = &["id"];
2267
2268        fn fields() -> &'static [FieldInfo] {
2269            static FIELDS: &[FieldInfo] = &[
2270                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2271                FieldInfo::new("first_name", "first_name", SqlType::Text)
2272                    .serialization_alias("firstName"),
2273                FieldInfo::new("display_name", "display_name", SqlType::Text)
2274                    .computed(true)
2275                    .serialization_alias("displayName"),
2276            ];
2277            FIELDS
2278        }
2279
2280        fn to_row(&self) -> Vec<(&'static str, Value)> {
2281            vec![
2282                ("id", Value::BigInt(self.id)),
2283                ("first_name", Value::Text(self.first_name.clone())),
2284            ]
2285        }
2286
2287        fn from_row(row: &Row) -> crate::Result<Self> {
2288            Ok(Self {
2289                id: row.get_named("id")?,
2290                first_name: row.get_named("first_name")?,
2291                display_name: String::new(),
2292            })
2293        }
2294
2295        fn primary_key_value(&self) -> Vec<Value> {
2296            vec![Value::BigInt(self.id)]
2297        }
2298
2299        fn is_new(&self) -> bool {
2300            false
2301        }
2302    }
2303
2304    #[test]
2305    fn test_exclude_computed_with_by_alias() {
2306        // This test verifies that computed field exclusion works correctly
2307        // even when combined with by_alias (which renames keys)
2308        let user = TestUserWithComputedAndAlias {
2309            id: 1,
2310            first_name: "John".to_string(),
2311            display_name: "John Doe".to_string(),
2312        };
2313
2314        // Test with by_alias only - computed field should still appear (aliased)
2315        let json = user
2316            .sql_model_dump(DumpOptions::default().by_alias())
2317            .unwrap();
2318        assert_eq!(json["firstName"], "John"); // first_name aliased
2319        assert_eq!(json["displayName"], "John Doe"); // display_name aliased (computed but not excluded)
2320        assert!(json.get("first_name").is_none()); // Original name should not exist
2321        assert!(json.get("display_name").is_none()); // Original name should not exist
2322
2323        // Test with exclude_computed_fields only - computed field should be excluded
2324        let json = user
2325            .sql_model_dump(DumpOptions::default().exclude_computed_fields())
2326            .unwrap();
2327        assert_eq!(json["first_name"], "John");
2328        assert!(json.get("display_name").is_none()); // Computed field excluded
2329
2330        // Test with BOTH by_alias AND exclude_computed_fields
2331        // This was buggy before the fix - computed field wasn't excluded
2332        // because exclusion happened after aliasing
2333        let json = user
2334            .sql_model_dump(DumpOptions::default().by_alias().exclude_computed_fields())
2335            .unwrap();
2336        assert_eq!(json["firstName"], "John"); // first_name aliased
2337        assert!(json.get("displayName").is_none()); // Computed field excluded (even though aliased)
2338        assert!(json.get("display_name").is_none()); // Original name doesn't exist either
2339    }
2340
2341    // ========================================================================
2342    // Exclude Defaults Tests
2343    // ========================================================================
2344
2345    /// Test model with default values for exclude_defaults testing.
2346    #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2347    struct TestModelWithDefaults {
2348        id: i64,
2349        name: String,
2350        count: i32,    // default: 0
2351        active: bool,  // default: false
2352        score: f64,    // default: 0.0
2353        label: String, // default: "default"
2354    }
2355
2356    impl Model for TestModelWithDefaults {
2357        const TABLE_NAME: &'static str = "test_defaults";
2358        const PRIMARY_KEY: &'static [&'static str] = &["id"];
2359
2360        fn fields() -> &'static [FieldInfo] {
2361            static FIELDS: &[FieldInfo] = &[
2362                FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2363                FieldInfo::new("name", "name", SqlType::Text),
2364                FieldInfo::new("count", "count", SqlType::Integer).default_json("0"),
2365                FieldInfo::new("active", "active", SqlType::Boolean).default_json("false"),
2366                FieldInfo::new("score", "score", SqlType::Double).default_json("0.0"),
2367                FieldInfo::new("label", "label", SqlType::Text).default_json("\"default\""),
2368            ];
2369            FIELDS
2370        }
2371
2372        fn to_row(&self) -> Vec<(&'static str, Value)> {
2373            vec![
2374                ("id", Value::BigInt(self.id)),
2375                ("name", Value::Text(self.name.clone())),
2376                ("count", Value::Int(self.count)),
2377                ("active", Value::Bool(self.active)),
2378                ("score", Value::Double(self.score)),
2379                ("label", Value::Text(self.label.clone())),
2380            ]
2381        }
2382
2383        fn from_row(row: &Row) -> crate::Result<Self> {
2384            Ok(Self {
2385                id: row.get_named("id")?,
2386                name: row.get_named("name")?,
2387                count: row.get_named("count")?,
2388                active: row.get_named("active")?,
2389                score: row.get_named("score")?,
2390                label: row.get_named("label")?,
2391            })
2392        }
2393
2394        fn primary_key_value(&self) -> Vec<Value> {
2395            vec![Value::BigInt(self.id)]
2396        }
2397
2398        fn is_new(&self) -> bool {
2399            false
2400        }
2401    }
2402
2403    #[test]
2404    fn test_exclude_defaults_all_at_default() {
2405        let model = TestModelWithDefaults {
2406            id: 1,
2407            name: "Test".to_string(),
2408            count: 0,                     // at default
2409            active: false,                // at default
2410            score: 0.0,                   // at default
2411            label: "default".to_string(), // at default
2412        };
2413
2414        let json = model
2415            .sql_model_dump(DumpOptions::default().exclude_defaults())
2416            .unwrap();
2417
2418        // id and name have no default_json, so they're always included
2419        assert!(json.get("id").is_some());
2420        assert!(json.get("name").is_some());
2421
2422        // Fields at default value should be excluded
2423        assert!(json.get("count").is_none());
2424        assert!(json.get("active").is_none());
2425        assert!(json.get("score").is_none());
2426        assert!(json.get("label").is_none());
2427    }
2428
2429    #[test]
2430    fn test_exclude_defaults_none_at_default() {
2431        let model = TestModelWithDefaults {
2432            id: 1,
2433            name: "Test".to_string(),
2434            count: 42,                   // not at default
2435            active: true,                // not at default
2436            score: 3.5,                  // not at default
2437            label: "custom".to_string(), // not at default
2438        };
2439
2440        let json = model
2441            .sql_model_dump(DumpOptions::default().exclude_defaults())
2442            .unwrap();
2443
2444        // All fields should be present since none are at defaults
2445        assert!(json.get("id").is_some());
2446        assert!(json.get("name").is_some());
2447        assert!(json.get("count").is_some());
2448        assert!(json.get("active").is_some());
2449        assert!(json.get("score").is_some());
2450        assert!(json.get("label").is_some());
2451
2452        // Verify values
2453        assert_eq!(json["count"], 42);
2454        assert_eq!(json["active"], true);
2455        assert_eq!(json["score"], 3.5);
2456        assert_eq!(json["label"], "custom");
2457    }
2458
2459    #[test]
2460    fn test_exclude_defaults_mixed() {
2461        let model = TestModelWithDefaults {
2462            id: 1,
2463            name: "Test".to_string(),
2464            count: 0,                    // at default
2465            active: true,                // not at default
2466            score: 0.0,                  // at default
2467            label: "custom".to_string(), // not at default
2468        };
2469
2470        let json = model
2471            .sql_model_dump(DumpOptions::default().exclude_defaults())
2472            .unwrap();
2473
2474        assert!(json.get("id").is_some());
2475        assert!(json.get("name").is_some());
2476
2477        // At default - excluded
2478        assert!(json.get("count").is_none());
2479        assert!(json.get("score").is_none());
2480
2481        // Not at default - included
2482        assert!(json.get("active").is_some());
2483        assert!(json.get("label").is_some());
2484        assert_eq!(json["active"], true);
2485        assert_eq!(json["label"], "custom");
2486    }
2487
2488    #[test]
2489    fn test_exclude_defaults_without_flag() {
2490        let model = TestModelWithDefaults {
2491            id: 1,
2492            name: "Test".to_string(),
2493            count: 0,                     // at default
2494            active: false,                // at default
2495            score: 0.0,                   // at default
2496            label: "default".to_string(), // at default
2497        };
2498
2499        // Without exclude_defaults, all fields should be included
2500        let json = model.sql_model_dump(DumpOptions::default()).unwrap();
2501
2502        assert!(json.get("id").is_some());
2503        assert!(json.get("name").is_some());
2504        assert!(json.get("count").is_some());
2505        assert!(json.get("active").is_some());
2506        assert!(json.get("score").is_some());
2507        assert!(json.get("label").is_some());
2508    }
2509
2510    #[test]
2511    fn test_exclude_defaults_with_by_alias() {
2512        // Test that exclude_defaults works correctly with by_alias
2513
2514        /// Model with defaults and aliases
2515        #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
2516        struct TestAliasWithDefaults {
2517            id: i64,
2518            count: i32,
2519        }
2520
2521        impl Model for TestAliasWithDefaults {
2522            const TABLE_NAME: &'static str = "test";
2523            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2524
2525            fn fields() -> &'static [FieldInfo] {
2526                static FIELDS: &[FieldInfo] = &[
2527                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2528                    FieldInfo::new("count", "count", SqlType::Integer)
2529                        .default_json("0")
2530                        .serialization_alias("itemCount"),
2531                ];
2532                FIELDS
2533            }
2534
2535            fn to_row(&self) -> Vec<(&'static str, Value)> {
2536                vec![
2537                    ("id", Value::BigInt(self.id)),
2538                    ("count", Value::Int(self.count)),
2539                ]
2540            }
2541
2542            fn from_row(row: &Row) -> crate::Result<Self> {
2543                Ok(Self {
2544                    id: row.get_named("id")?,
2545                    count: row.get_named("count")?,
2546                })
2547            }
2548
2549            fn primary_key_value(&self) -> Vec<Value> {
2550                vec![Value::BigInt(self.id)]
2551            }
2552
2553            fn is_new(&self) -> bool {
2554                false
2555            }
2556        }
2557
2558        // At default value
2559        let model_at_default = TestAliasWithDefaults { id: 1, count: 0 };
2560        let json = model_at_default
2561            .sql_model_dump(DumpOptions::default().exclude_defaults().by_alias())
2562            .unwrap();
2563
2564        // count is at default (0), so neither count nor itemCount should appear
2565        assert!(json.get("count").is_none());
2566        assert!(json.get("itemCount").is_none());
2567
2568        // Not at default
2569        let model_not_at_default = TestAliasWithDefaults { id: 1, count: 5 };
2570        let json = model_not_at_default
2571            .sql_model_dump(DumpOptions::default().exclude_defaults().by_alias())
2572            .unwrap();
2573
2574        // count is not at default, should appear with alias
2575        assert!(json.get("count").is_none()); // Original name not present
2576        assert_eq!(json["itemCount"], 5); // Alias is present
2577    }
2578
2579    #[test]
2580    fn test_field_info_default_json() {
2581        // Test the FieldInfo builder methods for default_json
2582        let field1 = FieldInfo::new("count", "count", SqlType::Integer).default_json("0");
2583        assert_eq!(field1.default_json, Some("0"));
2584        assert!(field1.has_default);
2585
2586        let field2 =
2587            FieldInfo::new("name", "name", SqlType::Text).default_json_opt(Some("\"hello\""));
2588        assert_eq!(field2.default_json, Some("\"hello\""));
2589        assert!(field2.has_default);
2590
2591        let field3 = FieldInfo::new("name", "name", SqlType::Text).default_json_opt(None);
2592        assert_eq!(field3.default_json, None);
2593        assert!(!field3.has_default);
2594
2595        let field4 = FieldInfo::new("flag", "flag", SqlType::Boolean).has_default(true);
2596        assert!(field4.has_default);
2597        assert_eq!(field4.default_json, None); // has_default alone doesn't set default_json
2598    }
2599
2600    // ==================== SqlModelUpdate Tests ====================
2601
2602    #[test]
2603    fn test_sqlmodel_update_from_dict() {
2604        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2605        struct TestUser {
2606            id: i64,
2607            name: String,
2608            age: i32,
2609        }
2610
2611        impl Model for TestUser {
2612            const TABLE_NAME: &'static str = "users";
2613            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2614
2615            fn fields() -> &'static [FieldInfo] {
2616                static FIELDS: &[FieldInfo] = &[
2617                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2618                    FieldInfo::new("name", "name", SqlType::Text),
2619                    FieldInfo::new("age", "age", SqlType::Integer),
2620                ];
2621                FIELDS
2622            }
2623
2624            fn to_row(&self) -> Vec<(&'static str, Value)> {
2625                vec![
2626                    ("id", Value::BigInt(self.id)),
2627                    ("name", Value::Text(self.name.clone())),
2628                    ("age", Value::Int(self.age)),
2629                ]
2630            }
2631
2632            fn from_row(row: &Row) -> crate::Result<Self> {
2633                Ok(Self {
2634                    id: row.get_named("id")?,
2635                    name: row.get_named("name")?,
2636                    age: row.get_named("age")?,
2637                })
2638            }
2639
2640            fn primary_key_value(&self) -> Vec<Value> {
2641                vec![Value::BigInt(self.id)]
2642            }
2643
2644            fn is_new(&self) -> bool {
2645                false
2646            }
2647        }
2648
2649        let mut user = TestUser {
2650            id: 1,
2651            name: "Alice".to_string(),
2652            age: 30,
2653        };
2654
2655        // Update name only
2656        let update = HashMap::from([("name".to_string(), serde_json::json!("Bob"))]);
2657        user.sqlmodel_update(update, UpdateOptions::default())
2658            .unwrap();
2659
2660        assert_eq!(user.name, "Bob");
2661        assert_eq!(user.age, 30); // Unchanged
2662    }
2663
2664    #[test]
2665    fn test_sqlmodel_update_with_update_fields_filter() {
2666        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2667        struct TestUser {
2668            id: i64,
2669            name: String,
2670            age: i32,
2671        }
2672
2673        impl Model for TestUser {
2674            const TABLE_NAME: &'static str = "users";
2675            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2676
2677            fn fields() -> &'static [FieldInfo] {
2678                static FIELDS: &[FieldInfo] = &[
2679                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2680                    FieldInfo::new("name", "name", SqlType::Text),
2681                    FieldInfo::new("age", "age", SqlType::Integer),
2682                ];
2683                FIELDS
2684            }
2685
2686            fn to_row(&self) -> Vec<(&'static str, Value)> {
2687                vec![
2688                    ("id", Value::BigInt(self.id)),
2689                    ("name", Value::Text(self.name.clone())),
2690                    ("age", Value::Int(self.age)),
2691                ]
2692            }
2693
2694            fn from_row(row: &Row) -> crate::Result<Self> {
2695                Ok(Self {
2696                    id: row.get_named("id")?,
2697                    name: row.get_named("name")?,
2698                    age: row.get_named("age")?,
2699                })
2700            }
2701
2702            fn primary_key_value(&self) -> Vec<Value> {
2703                vec![Value::BigInt(self.id)]
2704            }
2705
2706            fn is_new(&self) -> bool {
2707                false
2708            }
2709        }
2710
2711        let mut user = TestUser {
2712            id: 1,
2713            name: "Alice".to_string(),
2714            age: 30,
2715        };
2716
2717        // Try to update both name and age, but only allow name
2718        let update = HashMap::from([
2719            ("name".to_string(), serde_json::json!("Bob")),
2720            ("age".to_string(), serde_json::json!(25)),
2721        ]);
2722        user.sqlmodel_update(update, UpdateOptions::default().update_fields(["name"]))
2723            .unwrap();
2724
2725        assert_eq!(user.name, "Bob"); // Updated
2726        assert_eq!(user.age, 30); // Not updated because not in update_fields
2727    }
2728
2729    #[test]
2730    fn test_sqlmodel_update_invalid_field_error() {
2731        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2732        struct TestUser {
2733            id: i64,
2734            name: String,
2735        }
2736
2737        impl Model for TestUser {
2738            const TABLE_NAME: &'static str = "users";
2739            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2740
2741            fn fields() -> &'static [FieldInfo] {
2742                static FIELDS: &[FieldInfo] = &[
2743                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2744                    FieldInfo::new("name", "name", SqlType::Text),
2745                ];
2746                FIELDS
2747            }
2748
2749            fn to_row(&self) -> Vec<(&'static str, Value)> {
2750                vec![
2751                    ("id", Value::BigInt(self.id)),
2752                    ("name", Value::Text(self.name.clone())),
2753                ]
2754            }
2755
2756            fn from_row(row: &Row) -> crate::Result<Self> {
2757                Ok(Self {
2758                    id: row.get_named("id")?,
2759                    name: row.get_named("name")?,
2760                })
2761            }
2762
2763            fn primary_key_value(&self) -> Vec<Value> {
2764                vec![Value::BigInt(self.id)]
2765            }
2766
2767            fn is_new(&self) -> bool {
2768                false
2769            }
2770        }
2771
2772        let mut user = TestUser {
2773            id: 1,
2774            name: "Alice".to_string(),
2775        };
2776
2777        // Try to update an invalid field
2778        let update = HashMap::from([("invalid_field".to_string(), serde_json::json!("value"))]);
2779        let result = user.sqlmodel_update(update, UpdateOptions::default());
2780
2781        assert!(result.is_err());
2782        let err = result.unwrap_err();
2783        assert!(err.errors.iter().any(|e| e.field == "invalid_field"));
2784    }
2785
2786    #[test]
2787    fn test_sqlmodel_update_from_model() {
2788        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2789        struct TestUser {
2790            id: i64,
2791            name: String,
2792            email: Option<String>,
2793        }
2794
2795        impl Model for TestUser {
2796            const TABLE_NAME: &'static str = "users";
2797            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2798
2799            fn fields() -> &'static [FieldInfo] {
2800                static FIELDS: &[FieldInfo] = &[
2801                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2802                    FieldInfo::new("name", "name", SqlType::Text),
2803                    FieldInfo::new("email", "email", SqlType::Text).nullable(true),
2804                ];
2805                FIELDS
2806            }
2807
2808            fn to_row(&self) -> Vec<(&'static str, Value)> {
2809                vec![
2810                    ("id", Value::BigInt(self.id)),
2811                    ("name", Value::Text(self.name.clone())),
2812                    ("email", self.email.clone().map_or(Value::Null, Value::Text)),
2813                ]
2814            }
2815
2816            fn from_row(row: &Row) -> crate::Result<Self> {
2817                Ok(Self {
2818                    id: row.get_named("id")?,
2819                    name: row.get_named("name")?,
2820                    email: row.get_named("email").ok(),
2821                })
2822            }
2823
2824            fn primary_key_value(&self) -> Vec<Value> {
2825                vec![Value::BigInt(self.id)]
2826            }
2827
2828            fn is_new(&self) -> bool {
2829                false
2830            }
2831        }
2832
2833        let mut user = TestUser {
2834            id: 1,
2835            name: "Alice".to_string(),
2836            email: Some("alice@example.com".to_string()),
2837        };
2838
2839        // Patch with only name set (email is None)
2840        let patch = TestUser {
2841            id: 0, // Will be ignored since we're copying non-null values
2842            name: "Bob".to_string(),
2843            email: None, // Should not overwrite existing email
2844        };
2845
2846        user.sqlmodel_update_from(&patch, UpdateOptions::default())
2847            .unwrap();
2848
2849        assert_eq!(user.name, "Bob"); // Updated
2850        assert_eq!(user.email, Some("alice@example.com".to_string())); // Not updated (patch.email was None)
2851    }
2852
2853    #[test]
2854    fn test_sqlmodel_update_dict_convenience() {
2855        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2856        struct TestItem {
2857            id: i64,
2858            count: i32,
2859        }
2860
2861        impl Model for TestItem {
2862            const TABLE_NAME: &'static str = "items";
2863            const PRIMARY_KEY: &'static [&'static str] = &["id"];
2864
2865            fn fields() -> &'static [FieldInfo] {
2866                static FIELDS: &[FieldInfo] = &[
2867                    FieldInfo::new("id", "id", SqlType::BigInt).primary_key(true),
2868                    FieldInfo::new("count", "count", SqlType::Integer),
2869                ];
2870                FIELDS
2871            }
2872
2873            fn to_row(&self) -> Vec<(&'static str, Value)> {
2874                vec![
2875                    ("id", Value::BigInt(self.id)),
2876                    ("count", Value::Int(self.count)),
2877                ]
2878            }
2879
2880            fn from_row(row: &Row) -> crate::Result<Self> {
2881                Ok(Self {
2882                    id: row.get_named("id")?,
2883                    count: row.get_named("count")?,
2884                })
2885            }
2886
2887            fn primary_key_value(&self) -> Vec<Value> {
2888                vec![Value::BigInt(self.id)]
2889            }
2890
2891            fn is_new(&self) -> bool {
2892                false
2893            }
2894        }
2895
2896        let mut item = TestItem { id: 1, count: 10 };
2897
2898        // Use the convenience method
2899        item.sqlmodel_update_dict(HashMap::from([(
2900            "count".to_string(),
2901            serde_json::json!(20),
2902        )]))
2903        .unwrap();
2904
2905        assert_eq!(item.count, 20);
2906    }
2907
2908    // ========================================================================
2909    // Credit Card Validation Tests (Luhn Algorithm)
2910    // ========================================================================
2911
2912    #[test]
2913    fn test_credit_card_valid_visa() {
2914        // Valid Visa test number
2915        assert!(is_valid_credit_card("4539578763621486"));
2916    }
2917
2918    #[test]
2919    fn test_credit_card_valid_mastercard() {
2920        // Valid Mastercard test number
2921        assert!(is_valid_credit_card("5425233430109903"));
2922    }
2923
2924    #[test]
2925    fn test_credit_card_valid_amex() {
2926        // Valid American Express test number
2927        assert!(is_valid_credit_card("374245455400126"));
2928    }
2929
2930    #[test]
2931    fn test_credit_card_with_spaces() {
2932        // Spaces should be stripped
2933        assert!(is_valid_credit_card("4539 5787 6362 1486"));
2934    }
2935
2936    #[test]
2937    fn test_credit_card_with_dashes() {
2938        // Dashes should be stripped
2939        assert!(is_valid_credit_card("4539-5787-6362-1486"));
2940    }
2941
2942    #[test]
2943    fn test_credit_card_invalid_luhn() {
2944        // Invalid Luhn checksum
2945        assert!(!is_valid_credit_card("1234567890123456"));
2946    }
2947
2948    #[test]
2949    fn test_credit_card_too_short() {
2950        // Less than 13 digits
2951        assert!(!is_valid_credit_card("123456789012"));
2952    }
2953
2954    #[test]
2955    fn test_credit_card_too_long() {
2956        // More than 19 digits
2957        assert!(!is_valid_credit_card("12345678901234567890"));
2958    }
2959
2960    #[test]
2961    fn test_credit_card_empty() {
2962        assert!(!is_valid_credit_card(""));
2963    }
2964
2965    #[test]
2966    fn test_credit_card_non_numeric() {
2967        // Contains letters
2968        assert!(!is_valid_credit_card("453957876362abcd"));
2969    }
2970
2971    #[test]
2972    fn test_credit_card_all_zeros() {
2973        // All zeros - 16 digits, technically passes Luhn (sum=0, 0%10=0)
2974        // but not a realistic card number
2975        assert!(is_valid_credit_card("0000000000000000"));
2976    }
2977
2978    #[test]
2979    fn test_credit_card_valid_discover() {
2980        // Valid Discover test number
2981        assert!(is_valid_credit_card("6011111111111117"));
2982    }
2983
2984    // =========================================================================
2985    // Nested Model Tests
2986    // =========================================================================
2987
2988    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2989    struct Address {
2990        street: String,
2991        city: String,
2992        #[serde(skip_serializing_if = "Option::is_none")]
2993        zip: Option<String>,
2994    }
2995
2996    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2997    struct Person {
2998        name: String,
2999        age: i32,
3000        address: Address,
3001        #[serde(skip_serializing_if = "Option::is_none")]
3002        spouse: Option<Box<Person>>,
3003    }
3004
3005    #[test]
3006    fn test_nested_model_dump_basic() {
3007        let person = Person {
3008            name: "Alice".to_string(),
3009            age: 30,
3010            address: Address {
3011                street: "123 Main St".to_string(),
3012                city: "Springfield".to_string(),
3013                zip: Some("12345".to_string()),
3014            },
3015            spouse: None,
3016        };
3017
3018        let json = person.model_dump(DumpOptions::default()).unwrap();
3019        assert_eq!(json["name"], "Alice");
3020        assert_eq!(json["age"], 30);
3021        assert_eq!(json["address"]["street"], "123 Main St");
3022        assert_eq!(json["address"]["city"], "Springfield");
3023        assert_eq!(json["address"]["zip"], "12345");
3024    }
3025
3026    #[test]
3027    fn test_nested_model_dump_exclude_top_level() {
3028        let person = Person {
3029            name: "Alice".to_string(),
3030            age: 30,
3031            address: Address {
3032                street: "123 Main St".to_string(),
3033                city: "Springfield".to_string(),
3034                zip: Some("12345".to_string()),
3035            },
3036            spouse: None,
3037        };
3038
3039        // Exclude only applies to top-level fields
3040        let json = person
3041            .model_dump(DumpOptions::default().exclude(["age"]))
3042            .unwrap();
3043        assert!(json.get("name").is_some());
3044        assert!(json.get("age").is_none());
3045        assert!(json.get("address").is_some()); // Still present
3046        // Nested fields are NOT affected by top-level exclude
3047        assert_eq!(json["address"]["city"], "Springfield");
3048    }
3049
3050    #[test]
3051    fn test_nested_model_dump_exclude_nested_limitation() {
3052        // NOTE: This test documents a LIMITATION.
3053        // In Pydantic, you can exclude nested fields with dot notation: exclude={"address.zip"}
3054        // Our current implementation only supports top-level field exclusion.
3055        let person = Person {
3056            name: "Alice".to_string(),
3057            age: 30,
3058            address: Address {
3059                street: "123 Main St".to_string(),
3060                city: "Springfield".to_string(),
3061                zip: Some("12345".to_string()),
3062            },
3063            spouse: None,
3064        };
3065
3066        // Trying to exclude "address.zip" won't work - it treats it as a top-level field name
3067        let json = person
3068            .model_dump(DumpOptions::default().exclude(["address.zip"]))
3069            .unwrap();
3070        // address.zip is still present because we don't support nested path exclusion
3071        assert_eq!(json["address"]["zip"], "12345");
3072    }
3073
3074    #[test]
3075    fn test_deeply_nested_model_dump() {
3076        let person = Person {
3077            name: "Alice".to_string(),
3078            age: 30,
3079            address: Address {
3080                street: "123 Main St".to_string(),
3081                city: "Springfield".to_string(),
3082                zip: None,
3083            },
3084            spouse: Some(Box::new(Person {
3085                name: "Bob".to_string(),
3086                age: 32,
3087                address: Address {
3088                    street: "456 Oak Ave".to_string(),
3089                    city: "Springfield".to_string(),
3090                    zip: Some("12346".to_string()),
3091                },
3092                spouse: None,
3093            })),
3094        };
3095
3096        let json = person.model_dump(DumpOptions::default()).unwrap();
3097        assert_eq!(json["name"], "Alice");
3098        assert_eq!(json["spouse"]["name"], "Bob");
3099        assert_eq!(json["spouse"]["address"]["street"], "456 Oak Ave");
3100    }
3101
3102    #[test]
3103    fn test_nested_model_exclude_none() {
3104        let person = Person {
3105            name: "Alice".to_string(),
3106            age: 30,
3107            address: Address {
3108                street: "123 Main St".to_string(),
3109                city: "Springfield".to_string(),
3110                zip: None, // Will be skipped by serde skip_serializing_if
3111            },
3112            spouse: None, // Will be skipped by serde skip_serializing_if
3113        };
3114
3115        let json = person
3116            .model_dump(DumpOptions::default().exclude_none())
3117            .unwrap();
3118        assert!(json.get("name").is_some());
3119        // spouse is None and serde skips it, so it's not in the output
3120        assert!(json.get("spouse").is_none());
3121        // Note: exclude_none only affects top-level nulls in model_dump
3122        // Nested nulls are handled by serde's skip_serializing_if
3123    }
3124}