Skip to main content

xsd_schema/validation/
identity.rs

1//! Per-constraint state management and key table types for identity-constraint
2//! streaming validation.
3//!
4//! This module bridges parsed constraint definitions (`IdentityConstraintData`)
5//! to runtime validation by providing:
6//!
7//! - [`CompiledIdentityConstraint`] — pre-compiled selector/field Asttrees
8//! - [`KeyFieldValue`] / [`KeySequence`] — extracted field values with XSD equality
9//! - [`KeyTable`] — duplicate detection for key/unique, deferred storage for keyref
10//! - [`ConstraintStruct`] — per-activation state driving `ActiveAxis` matchers
11
12use crate::ids::NameId;
13use crate::namespace::table::NameTable;
14use crate::parser::frames::{IdentityKind, QNameRef};
15use crate::parser::location::SourceLocation;
16use crate::schema::model::XsdVersion;
17use crate::types::value::XmlValue;
18use crate::types::PrimitiveTypeCode;
19
20use super::active_axis::ActiveAxis;
21use super::asttree::{Asttree, IdentityXPathError};
22use super::errors::{error, error_with_path, ValidationError};
23
24use crate::arenas::IdentityConstraintData;
25use crate::ids::IdentityConstraintKey;
26
27/// Extract the single atomic value from any value shape.
28///
29/// Used for IC equality: XSD §3.11.4 says "single atomic values are not
30/// distinguished from lists with single items" — an atomic value and a
31/// singleton list containing the same atomic value are treated as equal.
32fn extract_single_atomic(value: &XmlValue) -> Option<&crate::types::value::XmlAtomicValue> {
33    match &value.value {
34        crate::types::value::XmlValueKind::Atomic(a) => Some(a),
35        crate::types::value::XmlValueKind::List { items, .. } if items.len() == 1 => {
36            Some(&items[0])
37        }
38        crate::types::value::XmlValueKind::Union(inner) => extract_single_atomic(inner),
39        _ => None,
40    }
41}
42
43/// IC value-space equality: compare type_code + value, ignoring schema_type.
44///
45/// XSD identity constraints compare values in value-space, not by schema type
46/// identity. Two identical primitive values from different anonymous type
47/// restrictions must be considered equal.
48///
49/// For date/time/duration types the derived `PartialEq` compares fields
50/// (hour=12+tz=Z differs from hour=13+tz=+01 even though both denote the
51/// same instant), so we fall back to each type's `PartialOrd::partial_cmp`
52/// which uses value-space (instant / total seconds) comparison.
53///
54/// Also handles singleton-list ↔ atomic equivalence per XSD §3.11.4.
55fn xml_value_ic_eq(a: &XmlValue, b: &XmlValue) -> bool {
56    if a.type_code == b.type_code && a.value == b.value {
57        return true;
58    }
59    if a.type_code == b.type_code && xml_value_datetime_eq(a, b) {
60        return true;
61    }
62    // Singleton-list ↔ atomic equality (XSD §3.11.4)
63    match (extract_single_atomic(a), extract_single_atomic(b)) {
64        (Some(va), Some(vb)) => va == vb || xml_atomic_datetime_eq(va, vb),
65        _ => false,
66    }
67}
68
69/// Value-space equality for date/time/duration atomic kinds using their
70/// `PartialOrd` implementations (which compare in value-space).
71///
72/// XSD Part 2 §3.3.8.2 "Order relation on dateTime" treats date/time/dateTime
73/// values with a timezone and those without as incomparable (neither equal
74/// nor ordered) for identity-constraint purposes. Duration kinds always
75/// compare by total magnitude.
76fn xml_atomic_datetime_eq(
77    a: &crate::types::value::XmlAtomicValue,
78    b: &crate::types::value::XmlAtomicValue,
79) -> bool {
80    use crate::types::value::XmlAtomicValue as V;
81    match (a, b) {
82        (V::DateTime(x), V::DateTime(y)) => {
83            x.timezone.is_some() == y.timezone.is_some()
84                && x.partial_cmp(y) == Some(std::cmp::Ordering::Equal)
85        }
86        (V::Date(x), V::Date(y)) => {
87            x.timezone.is_some() == y.timezone.is_some()
88                && x.partial_cmp(y) == Some(std::cmp::Ordering::Equal)
89        }
90        (V::Time(x), V::Time(y)) => {
91            x.timezone.is_some() == y.timezone.is_some()
92                && x.partial_cmp(y) == Some(std::cmp::Ordering::Equal)
93        }
94        (V::Duration(x), V::Duration(y)) => x.partial_cmp(y) == Some(std::cmp::Ordering::Equal),
95        (V::YearMonthDuration(x), V::YearMonthDuration(y)) => {
96            x.partial_cmp(y) == Some(std::cmp::Ordering::Equal)
97        }
98        (V::DayTimeDuration(x), V::DayTimeDuration(y)) => {
99            x.partial_cmp(y) == Some(std::cmp::Ordering::Equal)
100        }
101        _ => false,
102    }
103}
104
105fn xml_value_datetime_eq(a: &XmlValue, b: &XmlValue) -> bool {
106    use crate::types::value::XmlValueKind;
107    match (&a.value, &b.value) {
108        (XmlValueKind::Atomic(va), XmlValueKind::Atomic(vb)) => xml_atomic_datetime_eq(va, vb),
109        _ => false,
110    }
111}
112
113// ---------------------------------------------------------------------------
114// CompiledIdentityConstraint
115// ---------------------------------------------------------------------------
116
117/// Pre-compiled identity constraint with cloneable `Asttree` instances.
118///
119/// Shared across multiple activations — each activation clones the Asttrees
120/// into fresh `ActiveAxis` instances.
121pub(crate) struct CompiledIdentityConstraint {
122    /// Arena key for this constraint.
123    pub key: IdentityConstraintKey,
124    /// Constraint QName (interned).
125    pub name: NameId,
126    /// Constraint kind: Key, Unique, or Keyref.
127    pub kind: IdentityKind,
128    /// Pre-compiled selector expression.
129    pub selector: Asttree,
130    /// Pre-compiled field expressions.
131    pub fields: Vec<Asttree>,
132    /// Keyref target reference (only for `IdentityKind::Keyref`).
133    pub refer: Option<QNameRef>,
134    /// Resolved arena key of the referenced key/unique constraint (only for keyrefs).
135    /// Set by the validator after compilation via `resolve_refer_key()`.
136    pub refer_key: Option<IdentityConstraintKey>,
137    /// Cached `fields.len()`.
138    pub field_count: usize,
139    /// Target namespace of the schema that defines this constraint.
140    pub target_namespace: Option<NameId>,
141}
142
143impl CompiledIdentityConstraint {
144    /// Compile an identity constraint from its parsed data.
145    ///
146    /// The `schema_xpath_default_ns` is the schema-level fallback for the
147    /// three-level `xpathDefaultNamespace` cascade (field > selector > schema).
148    pub fn compile(
149        data: &IdentityConstraintData,
150        key: IdentityConstraintKey,
151        name_table: &NameTable,
152        schema_xpath_default_ns: Option<NameId>,
153        target_namespace: Option<NameId>,
154        xsd_version: XsdVersion,
155    ) -> Result<Self, IdentityXPathError> {
156        // Compile selector
157        let selector = Asttree::compile_selector(
158            &data.selector.xpath,
159            &data.selector.ns_snapshot,
160            name_table,
161            data.selector.xpath_default_namespace.as_deref(),
162            schema_xpath_default_ns,
163            target_namespace,
164            xsd_version,
165        )?;
166
167        // Determine the selector-level xpath_default_namespace for cascading to fields.
168        // If the selector has its own xpathDefaultNamespace, resolve it to a NameId;
169        // otherwise fall back to the schema-level value.
170        let selector_level_ns = match &data.selector.xpath_default_namespace {
171            Some(val) => match val.as_str() {
172                "##targetNamespace" => target_namespace,
173                "##local" => None,
174                uri => Some(name_table.add(uri)),
175            },
176            None => schema_xpath_default_ns,
177        };
178
179        // Compile fields
180        let fields: Vec<Asttree> = data
181            .fields
182            .iter()
183            .map(|f| {
184                Asttree::compile_field(
185                    &f.xpath,
186                    &f.ns_snapshot,
187                    name_table,
188                    f.xpath_default_namespace.as_deref(),
189                    selector_level_ns,
190                    target_namespace,
191                    xsd_version,
192                )
193            })
194            .collect::<Result<Vec<_>, _>>()?;
195
196        let field_count = fields.len();
197
198        Ok(CompiledIdentityConstraint {
199            key,
200            name: data.name,
201            kind: data.kind,
202            selector,
203            fields,
204            refer: data.refer.clone(),
205            refer_key: None,
206            field_count,
207            target_namespace,
208        })
209    }
210}
211
212// ---------------------------------------------------------------------------
213// KeyFieldValue
214// ---------------------------------------------------------------------------
215
216/// Per-field extracted value for key sequence equality.
217///
218/// Equality semantics follow the XSD spec:
219/// - If both have `typed_value` with the same `PrimitiveTypeCode`, compare in
220///   value space.  For the `Decimal` primitive type (which covers `xs:decimal`,
221///   `xs:integer`, and all integer-derived types), comparison is performed
222///   numerically so that, e.g., `xs:decimal "1"` equals `xs:unsignedByte "1"`.
223/// - For all other primitive types with the same `PrimitiveTypeCode`, compare
224///   via `XmlValue::PartialEq` (structural comparison of `XmlValueKind`).
225/// - Different primitive types are *never* equal (no type promotion for IC equality).
226/// - If either value is untyped, compare `string_value` (string equality).
227#[derive(Debug, Clone)]
228pub struct KeyFieldValue {
229    pub string_value: String,
230    pub typed_value: Option<XmlValue>,
231}
232
233impl PartialEq for KeyFieldValue {
234    fn eq(&self, other: &Self) -> bool {
235        match (&self.typed_value, &other.typed_value) {
236            (Some(a), Some(b)) => {
237                let prim_a = a.primitive_type();
238                let prim_b = b.primitive_type();
239                match (prim_a, prim_b) {
240                    (Some(pa), Some(pb)) if pa == pb => {
241                        // XSD value-space equality: for the decimal hierarchy
242                        // (xs:decimal, xs:integer, and all derived integer types)
243                        // different storage variants (Decimal vs Integer) must be
244                        // compared numerically, not structurally.
245                        if pa == PrimitiveTypeCode::Decimal {
246                            match (a.as_decimal(), b.as_decimal()) {
247                                (Some(da), Some(db)) => da == db,
248                                _ => xml_value_ic_eq(a, b),
249                            }
250                        } else {
251                            xml_value_ic_eq(a, b)
252                        }
253                    }
254                    (Some(_), Some(_)) => false, // different primitive types → never equal
255                    _ => self.string_value == other.string_value,
256                }
257            }
258            _ => self.string_value == other.string_value,
259        }
260    }
261}
262
263impl Eq for KeyFieldValue {}
264
265// ---------------------------------------------------------------------------
266// KeySequence
267// ---------------------------------------------------------------------------
268
269/// Complete key sequence from one selector match.
270///
271/// Each slot corresponds to a `<field>` expression. `None` means the field
272/// did not select a node (missing value).
273#[derive(Debug, Clone)]
274pub struct KeySequence {
275    pub fields: Vec<Option<KeyFieldValue>>,
276}
277
278impl KeySequence {
279    /// Returns `true` if all fields have a value.
280    pub fn is_complete(&self) -> bool {
281        self.fields.iter().all(|f| f.is_some())
282    }
283}
284
285impl PartialEq for KeySequence {
286    fn eq(&self, other: &Self) -> bool {
287        if self.fields.len() != other.fields.len() {
288            return false;
289        }
290        for (a, b) in self.fields.iter().zip(other.fields.iter()) {
291            match (a, b) {
292                (Some(va), Some(vb)) => {
293                    if va != vb {
294                        return false;
295                    }
296                }
297                (None, None) => {
298                    // Both absent — equal for that slot
299                }
300                _ => return false,
301            }
302        }
303        true
304    }
305}
306
307impl Eq for KeySequence {}
308
309// ---------------------------------------------------------------------------
310// KeyTable
311// ---------------------------------------------------------------------------
312
313/// Collection of key sequences for one constraint activation with duplicate detection.
314pub struct KeyTable {
315    /// Arena key identifying which identity constraint produced this table.
316    pub ic_key: IdentityConstraintKey,
317    pub constraint_name: NameId,
318    pub kind: IdentityKind,
319    pub sequences: Vec<KeySequence>,
320}
321
322impl KeyTable {
323    /// Create a new empty key table.
324    pub fn new(ic_key: IdentityConstraintKey, constraint_name: NameId, kind: IdentityKind) -> Self {
325        KeyTable {
326            ic_key,
327            constraint_name,
328            kind,
329            sequences: Vec::new(),
330        }
331    }
332
333    /// Add a key sequence, performing duplicate/completeness checks as appropriate.
334    ///
335    /// - **Key**: error if incomplete (`cvc-identity-constraint.4.2.1`), error if
336    ///   duplicate (`cvc-identity-constraint.4.2.2`).
337    /// - **Unique**: check duplicate only if complete (incomplete sequences are skipped
338    ///   per XSD spec).
339    /// - **Keyref**: just store (deferred to `check_keyref_against`).
340    pub fn add_sequence(
341        &mut self,
342        seq: KeySequence,
343        name_table: &NameTable,
344        element_path: &str,
345        location: Option<SourceLocation>,
346    ) -> Vec<ValidationError> {
347        let mut errors = Vec::new();
348        let name = name_table.resolve(self.constraint_name);
349
350        match self.kind {
351            IdentityKind::Key => {
352                if !seq.is_complete() {
353                    errors.push(error_with_path(
354                        "cvc-identity-constraint.4.2.1",
355                        format!("Key constraint '{}': not all fields have values", name),
356                        location.clone(),
357                        element_path,
358                    ));
359                }
360                // Check for duplicate even if incomplete (per spec, key requires
361                // completeness AND uniqueness)
362                if self.find_duplicate(&seq).is_some() {
363                    errors.push(error_with_path(
364                        "cvc-identity-constraint.4.2.2",
365                        format!("Key constraint '{}': duplicate key value detected", name),
366                        location,
367                        element_path,
368                    ));
369                }
370            }
371            IdentityKind::Unique => {
372                // For unique, only check duplicates when the sequence is complete
373                if seq.is_complete() && self.find_duplicate(&seq).is_some() {
374                    errors.push(error_with_path(
375                        "cvc-identity-constraint.4.2.2",
376                        format!("Unique constraint '{}': duplicate key value detected", name),
377                        location,
378                        element_path,
379                    ));
380                }
381            }
382            IdentityKind::Keyref => {
383                // Just store — checked later via check_keyref_against
384            }
385        }
386
387        self.sequences.push(seq);
388        errors
389    }
390
391    /// Check all keyref sequences against a target key/unique table.
392    ///
393    /// Returns `cvc-identity-constraint.4.3` errors for unmatched keyrefs.
394    pub fn check_keyref_against(
395        &self,
396        target: &KeyTable,
397        name_table: &NameTable,
398    ) -> Vec<ValidationError> {
399        let mut errors = Vec::new();
400        let name = name_table.resolve(self.constraint_name);
401
402        for seq in &self.sequences {
403            if !seq.is_complete() {
404                // Incomplete keyref sequences are not checked
405                continue;
406            }
407            let found = target.sequences.iter().any(|ts| ts == seq);
408            if !found {
409                errors.push(error(
410                    "cvc-identity-constraint.4.3",
411                    format!(
412                        "Keyref constraint '{}': no matching key value found in referenced constraint '{}'",
413                        name,
414                        name_table.resolve(target.constraint_name)
415                    ),
416                    None,
417                ));
418            }
419        }
420
421        errors
422    }
423
424    /// Linear scan for a duplicate of `seq` in the existing sequences.
425    fn find_duplicate(&self, seq: &KeySequence) -> Option<usize> {
426        self.sequences.iter().position(|existing| existing == seq)
427    }
428}
429
430// ---------------------------------------------------------------------------
431// FieldCollectionFrame
432// ---------------------------------------------------------------------------
433
434/// One level of field collection for a single selector match.
435///
436/// A stack of these is maintained in `ConstraintStruct` to handle
437/// overlapping (nested) selector matches correctly — e.g. `.//item`
438/// matching both an outer and inner `<item>`.
439struct FieldCollectionFrame {
440    /// Field axis matchers (one per `<field>`), cloned from templates.
441    fields: Vec<ActiveAxis>,
442    /// Current key sequence being collected (one slot per field).
443    current_key_sequence: Vec<Option<KeyFieldValue>>,
444    /// How many times each field slot has been matched so far.
445    /// Used to detect multi-node field violations (§3.11.4 cvc-identity-constraint.4.2.1).
446    field_match_count: Vec<u8>,
447}
448
449// ---------------------------------------------------------------------------
450// ConstraintStruct
451// ---------------------------------------------------------------------------
452
453/// Per-constraint activation state during streaming validation.
454///
455/// Each time an identity constraint's scope element is entered, a fresh
456/// `ConstraintStruct` is created. It drives selector and field `ActiveAxis`
457/// matchers, collects key sequences, and accumulates them in a `KeyTable`.
458///
459/// Nested selector matches (e.g. `.//item` with nested `<item>` elements)
460/// are handled via a stack of [`FieldCollectionFrame`]s. Each selector hit
461/// pushes a new frame; each selector exit pops the top frame and finalizes
462/// its key sequence.
463pub(crate) struct ConstraintStruct {
464    /// Arena key for the compiled constraint (used for deactivation lookup).
465    pub ic_key: IdentityConstraintKey,
466    /// Selector axis matcher.
467    pub selector: ActiveAxis,
468    /// Field Asttree templates for cloning into new collection frames.
469    field_asttrees: Vec<Asttree>,
470    /// Number of fields (cached from compiled constraint).
471    field_count: usize,
472    /// Stack of active field collection frames (one per nested selector match).
473    /// Empty means no selector match is currently active.
474    collection_stack: Vec<FieldCollectionFrame>,
475    /// Key table accumulating complete key sequences.
476    pub key_table: KeyTable,
477}
478
479impl ConstraintStruct {
480    /// Create a new constraint activation state.
481    ///
482    /// Stores Asttree templates for cloning into fresh `ActiveAxis` instances
483    /// on each selector match.
484    pub fn new(compiled: &CompiledIdentityConstraint) -> Self {
485        let selector = ActiveAxis::new(compiled.selector.clone());
486
487        ConstraintStruct {
488            ic_key: compiled.key,
489            selector,
490            field_asttrees: compiled.fields.clone(),
491            field_count: compiled.field_count,
492            collection_stack: Vec::new(),
493            key_table: KeyTable::new(compiled.key, compiled.name, compiled.kind),
494        }
495    }
496
497    /// Whether field collection is active (at least one selector match on the stack).
498    #[cfg(test)]
499    pub fn collecting_fields(&self) -> bool {
500        !self.collection_stack.is_empty()
501    }
502
503    /// Activate the constraint for the current scope element.
504    ///
505    /// Returns `true` if the selector is a bare `.` match (immediate scope match).
506    pub fn activate(&mut self) -> bool {
507        let is_scope_match = self.selector.activate();
508        if is_scope_match {
509            // Bare "." — immediately start collecting fields
510            self.push_field_collection();
511        }
512        is_scope_match
513    }
514
515    /// Handle an element start event.
516    ///
517    /// Advances the selector; if it matches, pushes a new field collection
518    /// frame. Existing frames continue receiving element events for correct
519    /// depth tracking.
520    pub fn start_element(&mut self, local_name: NameId, ns: NameId) {
521        self.selector.move_to_start_element(local_name, ns);
522
523        // Advance ALL existing frames' field axes first (depth tracking for
524        // outer frames that are "paused" by an inner selector match).
525        for frame in &mut self.collection_stack {
526            for field in &mut frame.fields {
527                field.move_to_start_element(local_name, ns);
528            }
529        }
530
531        if self.selector.entered_match() {
532            // New selector match — push a fresh frame (does NOT advance the
533            // new frame's fields because the matched element is the field scope root).
534            self.push_field_collection();
535        }
536    }
537
538    /// Handle an element start event for an element inside a wildcard
539    /// `processContents="skip"` subtree. Bumps depth tracking on selectors and
540    /// fields so the matching state pops correctly when the subtree ends, but
541    /// suppresses any new matches: skipped elements are outside the schema's
542    /// validation scope and must not be visible to identity-constraint
543    /// selectors/fields per XSD 1.1 §3.11.4 (see wild101..103).
544    pub fn start_element_skipped(&mut self, _local_name: NameId, _ns: NameId) {
545        self.selector.move_to_skipped_element();
546        for frame in &mut self.collection_stack {
547            for field in &mut frame.fields {
548                field.move_to_skipped_element();
549            }
550        }
551    }
552
553    /// Return all field indices whose axis matches the given attribute.
554    ///
555    /// Multiple fields can match the same attribute (e.g. repeated or
556    /// overlapping field expressions), so all matching indices are returned.
557    pub fn matching_fields(&self, local_name: NameId, ns: NameId) -> Vec<usize> {
558        let frame = match self.collection_stack.last() {
559            Some(f) => f,
560            None => return Vec::new(),
561        };
562        let mut indices = Vec::new();
563        for (i, field) in frame.fields.iter().enumerate() {
564            if field.matches_attribute(local_name, ns) {
565                indices.push(i);
566            }
567        }
568        indices
569    }
570
571    /// Store a field value at the given index in the topmost collection frame.
572    ///
573    /// Returns `true` if this is a second (or later) match for the same field slot
574    /// in the current selector-bound element — the caller should report
575    /// `cvc-identity-constraint.4.2.1`.
576    pub fn set_field_value(
577        &mut self,
578        field_idx: usize,
579        string_value: String,
580        typed_value: Option<XmlValue>,
581    ) -> bool {
582        if let Some(frame) = self.collection_stack.last_mut() {
583            if field_idx < frame.current_key_sequence.len() {
584                let already_matched = frame.field_match_count[field_idx] > 0;
585                frame.field_match_count[field_idx] =
586                    frame.field_match_count[field_idx].saturating_add(1);
587                frame.current_key_sequence[field_idx] = Some(KeyFieldValue {
588                    string_value,
589                    typed_value,
590                });
591                return already_matched;
592            }
593        }
594        false
595    }
596
597    /// Increment the field match count without storing a value.
598    ///
599    /// Used for skip-processed wildcard attributes: the attribute is selected by
600    /// the field XPath (counts toward multi-node detection) but cannot contribute
601    /// a typed value, so the field slot stays absent.
602    ///
603    /// Returns `true` if this is a second (or later) match for the same field slot.
604    pub fn increment_field_match_count(&mut self, field_idx: usize) -> bool {
605        if let Some(frame) = self.collection_stack.last_mut() {
606            if field_idx < frame.field_match_count.len() {
607                let already_matched = frame.field_match_count[field_idx] > 0;
608                frame.field_match_count[field_idx] =
609                    frame.field_match_count[field_idx].saturating_add(1);
610                return already_matched;
611            }
612        }
613        false
614    }
615
616    /// Handle an element end event with text content for element-field matching.
617    ///
618    /// Advances field axes in ALL frames FIRST (to detect `exited_match` and
619    /// set values from text content), THEN advances the selector.
620    /// On selector exit, pops the topmost frame and finalizes its key sequence.
621    ///
622    /// `is_nil` indicates that the element carries `xsi:nil="true"`.  Nilled
623    /// elements have no typed value but ARE selected by field XPaths; they
624    /// contribute an empty-string field value (with no type information) so
625    /// that two nil sibling elements are detected as duplicates under
626    /// `xs:unique` / `xs:key`.
627    ///
628    /// `is_complex_content` must be `true` when the closing element has element-only
629    /// or mixed content type.  Per XSD §3.11.4 (cvc-identity-constraint.4), a field
630    /// that selects such an element cannot yield a valid typed value; the constraint
631    /// is violated for every identity-constraint kind (key *and* unique).
632    #[allow(clippy::too_many_arguments)]
633    pub fn end_element_with_text(
634        &mut self,
635        text_content: &str,
636        typed_value: Option<&XmlValue>,
637        is_nil: bool,
638        is_complex_content: bool,
639        name_table: &NameTable,
640        element_path: &str,
641        location: Option<SourceLocation>,
642    ) -> Vec<ValidationError> {
643        let mut errors = Vec::new();
644
645        // 1. Advance field axes in ALL frames FIRST (detect exited element-field matches)
646        for frame in &mut self.collection_stack {
647            for (field_idx, field) in frame.fields.iter_mut().enumerate() {
648                field.end_element();
649                if field.exited_match() {
650                    if frame.field_match_count[field_idx] > 0 {
651                        // §3.11.4 cvc-identity-constraint.4.2.1: field evaluates to
652                        // a node-set with more than one member.
653                        let name = name_table.resolve(self.key_table.constraint_name);
654                        errors.push(error_with_path(
655                            "cvc-identity-constraint.4.2.1",
656                            format!(
657                                "Identity constraint '{}': field {} matches more than one node",
658                                name,
659                                field_idx + 1
660                            ),
661                            location.clone(),
662                            element_path,
663                        ));
664                    } else if frame.current_key_sequence[field_idx].is_none() {
665                        // First match — set the field value (or report complex content error).
666                        frame.field_match_count[field_idx] =
667                            frame.field_match_count[field_idx].saturating_add(1);
668                        if is_complex_content {
669                            // XSD §3.11.4 cvc-identity-constraint.4: a field that selects an
670                            // element with element-only or mixed content type cannot contribute
671                            // a typed value.  This is a constraint violation for both xs:key
672                            // and xs:unique (not merely "absent"), because the node IS present
673                            // but its type precludes a meaningful field value.
674                            let constraint_name =
675                                name_table.resolve(self.key_table.constraint_name);
676                            errors.push(error_with_path(
677                                "cvc-identity-constraint.4",
678                                format!(
679                                    "Identity constraint '{}': field matched an element with \
680                                     complex content type (element-only or mixed); \
681                                     typed field values must be simple-typed",
682                                    constraint_name
683                                ),
684                                location.clone(),
685                                element_path,
686                            ));
687                        } else if typed_value.is_some() {
688                            // Normal case: element has a simple typed value.
689                            frame.current_key_sequence[field_idx] = Some(KeyFieldValue {
690                                string_value: text_content.to_string(),
691                                typed_value: typed_value.cloned(),
692                            });
693                        } else if is_nil {
694                            // Nilled element (xsi:nil="true"): the typed value is absent
695                            // but the element node IS selected.  Contribute an empty-string
696                            // binding (no type) so that two nil siblings compare equal and
697                            // trigger a duplicate violation under xs:unique / xs:key.
698                            frame.current_key_sequence[field_idx] = Some(KeyFieldValue {
699                                string_value: String::new(),
700                                typed_value: None,
701                            });
702                        }
703                        // For empty-content elements that are neither typed nor nilled,
704                        // the slot stays None (absent).
705                    }
706                }
707            }
708        }
709
710        // 2. Then advance selector
711        self.selector.end_element();
712
713        // 3. If selector exits match, pop frame and finalize key sequence
714        if self.selector.exited_match() {
715            if let Some(frame) = self.collection_stack.pop() {
716                let seq = KeySequence {
717                    fields: frame.current_key_sequence,
718                };
719                errors.extend(
720                    self.key_table
721                        .add_sequence(seq, name_table, element_path, location),
722                );
723            }
724        }
725
726        errors
727    }
728
729    /// Whether the selector axis is still active.
730    pub fn is_active(&self) -> bool {
731        self.selector.is_active()
732    }
733
734    /// Push a new field collection frame: clone Asttrees into fresh ActiveAxis
735    /// instances and initialize an empty key sequence.
736    fn push_field_collection(&mut self) {
737        let fields: Vec<ActiveAxis> = self
738            .field_asttrees
739            .iter()
740            .map(|ast| {
741                let mut axis = ActiveAxis::new(ast.clone());
742                axis.activate();
743                axis
744            })
745            .collect();
746        self.collection_stack.push(FieldCollectionFrame {
747            fields,
748            current_key_sequence: vec![None; self.field_count],
749            field_match_count: vec![0; self.field_count],
750        });
751    }
752}
753
754// ---------------------------------------------------------------------------
755// Tests
756// ---------------------------------------------------------------------------
757
758#[cfg(test)]
759mod tests {
760    use super::*;
761    use crate::namespace::context::NamespaceContextSnapshot;
762    use crate::namespace::table::NameTable;
763    use crate::schema::model::XsdVersion;
764    use crate::types::value::{XmlAtomicValue, XmlValueKind};
765    use crate::types::XmlTypeCode;
766
767    // -- Helpers --
768
769    fn make_string_field(s: &str) -> KeyFieldValue {
770        KeyFieldValue {
771            string_value: s.to_string(),
772            typed_value: None,
773        }
774    }
775
776    fn make_typed_field(s: &str, type_code: XmlTypeCode, value: XmlValueKind) -> KeyFieldValue {
777        KeyFieldValue {
778            string_value: s.to_string(),
779            typed_value: Some(XmlValue::new(type_code, value)),
780        }
781    }
782
783    fn make_name_table() -> NameTable {
784        NameTable::new()
785    }
786
787    // -----------------------------------------------------------------------
788    // KeyFieldValue equality
789    // -----------------------------------------------------------------------
790
791    #[test]
792    fn key_field_value_untyped_same_string_equal() {
793        let a = make_string_field("hello");
794        let b = make_string_field("hello");
795        assert_eq!(a, b);
796    }
797
798    #[test]
799    fn key_field_value_untyped_different_string_not_equal() {
800        let a = make_string_field("hello");
801        let b = make_string_field("world");
802        assert_ne!(a, b);
803    }
804
805    #[test]
806    fn key_field_value_typed_same_primitive_same_value_equal() {
807        let a = make_typed_field(
808            "42",
809            XmlTypeCode::Integer,
810            XmlValueKind::Atomic(XmlAtomicValue::Integer(42.into())),
811        );
812        let b = make_typed_field(
813            "42",
814            XmlTypeCode::Integer,
815            XmlValueKind::Atomic(XmlAtomicValue::Integer(42.into())),
816        );
817        assert_eq!(a, b);
818    }
819
820    #[test]
821    fn key_field_value_typed_same_primitive_different_value_not_equal() {
822        let a = make_typed_field(
823            "42",
824            XmlTypeCode::Integer,
825            XmlValueKind::Atomic(XmlAtomicValue::Integer(42.into())),
826        );
827        let b = make_typed_field(
828            "99",
829            XmlTypeCode::Integer,
830            XmlValueKind::Atomic(XmlAtomicValue::Integer(99.into())),
831        );
832        assert_ne!(a, b);
833    }
834
835    #[test]
836    fn key_field_value_typed_different_primitive_not_equal() {
837        // xs:integer(5) vs xs:string("5") — different primitive types → never equal
838        let a = make_typed_field(
839            "5",
840            XmlTypeCode::Integer,
841            XmlValueKind::Atomic(XmlAtomicValue::Integer(5.into())),
842        );
843        let b = make_typed_field(
844            "5",
845            XmlTypeCode::String,
846            XmlValueKind::Atomic(XmlAtomicValue::String("5".to_string())),
847        );
848        assert_ne!(a, b);
849    }
850
851    #[test]
852    fn key_field_value_one_typed_one_untyped_fallback_string() {
853        // One typed + one untyped → fall back to string comparison
854        let a = make_typed_field(
855            "hello",
856            XmlTypeCode::String,
857            XmlValueKind::Atomic(XmlAtomicValue::String("hello".to_string())),
858        );
859        let b = make_string_field("hello");
860        assert_eq!(a, b);
861
862        let c = make_typed_field(
863            "42",
864            XmlTypeCode::Integer,
865            XmlValueKind::Atomic(XmlAtomicValue::Integer(42.into())),
866        );
867        let d = make_string_field("99");
868        assert_ne!(c, d);
869    }
870
871    // -----------------------------------------------------------------------
872    // KeySequence
873    // -----------------------------------------------------------------------
874
875    #[test]
876    fn key_sequence_is_complete_all_present() {
877        let seq = KeySequence {
878            fields: vec![Some(make_string_field("a")), Some(make_string_field("b"))],
879        };
880        assert!(seq.is_complete());
881    }
882
883    #[test]
884    fn key_sequence_is_complete_missing_field() {
885        let seq = KeySequence {
886            fields: vec![Some(make_string_field("a")), None],
887        };
888        assert!(!seq.is_complete());
889    }
890
891    #[test]
892    fn key_sequence_equal() {
893        let a = KeySequence {
894            fields: vec![Some(make_string_field("x")), Some(make_string_field("y"))],
895        };
896        let b = KeySequence {
897            fields: vec![Some(make_string_field("x")), Some(make_string_field("y"))],
898        };
899        assert_eq!(a, b);
900    }
901
902    #[test]
903    fn key_sequence_not_equal() {
904        let a = KeySequence {
905            fields: vec![Some(make_string_field("x")), Some(make_string_field("y"))],
906        };
907        let b = KeySequence {
908            fields: vec![Some(make_string_field("x")), Some(make_string_field("z"))],
909        };
910        assert_ne!(a, b);
911    }
912
913    #[test]
914    fn key_sequence_both_none_equal() {
915        let a = KeySequence {
916            fields: vec![Some(make_string_field("x")), None],
917        };
918        let b = KeySequence {
919            fields: vec![Some(make_string_field("x")), None],
920        };
921        assert_eq!(a, b);
922    }
923
924    // -----------------------------------------------------------------------
925    // KeyTable duplicate detection
926    // -----------------------------------------------------------------------
927
928    #[test]
929    fn key_table_key_duplicate_error() {
930        let nt = make_name_table();
931        let name = nt.add("pk");
932        let mut table = KeyTable::new(IdentityConstraintKey::default(), name, IdentityKind::Key);
933
934        let seq1 = KeySequence {
935            fields: vec![Some(make_string_field("1"))],
936        };
937        let errs = table.add_sequence(seq1, &nt, "/root/item[1]", None);
938        assert!(errs.is_empty());
939
940        let seq2 = KeySequence {
941            fields: vec![Some(make_string_field("1"))],
942        };
943        let errs = table.add_sequence(seq2, &nt, "/root/item[2]", None);
944        assert_eq!(errs.len(), 1);
945        assert_eq!(errs[0].constraint, "cvc-identity-constraint.4.2.2");
946    }
947
948    #[test]
949    fn key_table_key_incomplete_error() {
950        let nt = make_name_table();
951        let name = nt.add("pk");
952        let mut table = KeyTable::new(IdentityConstraintKey::default(), name, IdentityKind::Key);
953
954        let seq = KeySequence {
955            fields: vec![Some(make_string_field("a")), None],
956        };
957        let errs = table.add_sequence(seq, &nt, "/root/item[1]", None);
958        assert!(errs
959            .iter()
960            .any(|e| e.constraint == "cvc-identity-constraint.4.2.1"));
961    }
962
963    #[test]
964    fn key_table_unique_duplicate_error() {
965        let nt = make_name_table();
966        let name = nt.add("uq");
967        let mut table = KeyTable::new(IdentityConstraintKey::default(), name, IdentityKind::Unique);
968
969        let seq1 = KeySequence {
970            fields: vec![Some(make_string_field("val"))],
971        };
972        let errs = table.add_sequence(seq1, &nt, "/root/item[1]", None);
973        assert!(errs.is_empty());
974
975        let seq2 = KeySequence {
976            fields: vec![Some(make_string_field("val"))],
977        };
978        let errs = table.add_sequence(seq2, &nt, "/root/item[2]", None);
979        assert_eq!(errs.len(), 1);
980        assert_eq!(errs[0].constraint, "cvc-identity-constraint.4.2.2");
981    }
982
983    #[test]
984    fn key_table_unique_incomplete_no_error() {
985        let nt = make_name_table();
986        let name = nt.add("uq");
987        let mut table = KeyTable::new(IdentityConstraintKey::default(), name, IdentityKind::Unique);
988
989        let seq = KeySequence { fields: vec![None] };
990        let errs = table.add_sequence(seq, &nt, "/root/item[1]", None);
991        assert!(errs.is_empty());
992    }
993
994    #[test]
995    fn key_table_keyref_no_error() {
996        let nt = make_name_table();
997        let name = nt.add("fk");
998        let mut table = KeyTable::new(IdentityConstraintKey::default(), name, IdentityKind::Keyref);
999
1000        let seq = KeySequence {
1001            fields: vec![Some(make_string_field("anything"))],
1002        };
1003        let errs = table.add_sequence(seq, &nt, "/root/item[1]", None);
1004        assert!(errs.is_empty());
1005    }
1006
1007    #[test]
1008    fn check_keyref_against_matching() {
1009        let nt = make_name_table();
1010        let pk_name = nt.add("pk");
1011        let fk_name = nt.add("fk");
1012
1013        let mut key_table =
1014            KeyTable::new(IdentityConstraintKey::default(), pk_name, IdentityKind::Key);
1015        key_table.sequences.push(KeySequence {
1016            fields: vec![Some(make_string_field("1"))],
1017        });
1018
1019        let mut keyref_table = KeyTable::new(
1020            IdentityConstraintKey::default(),
1021            fk_name,
1022            IdentityKind::Keyref,
1023        );
1024        keyref_table.sequences.push(KeySequence {
1025            fields: vec![Some(make_string_field("1"))],
1026        });
1027
1028        let errs = keyref_table.check_keyref_against(&key_table, &nt);
1029        assert!(errs.is_empty());
1030    }
1031
1032    #[test]
1033    fn check_keyref_against_missing() {
1034        let nt = make_name_table();
1035        let pk_name = nt.add("pk");
1036        let fk_name = nt.add("fk");
1037
1038        let key_table = KeyTable::new(IdentityConstraintKey::default(), pk_name, IdentityKind::Key);
1039
1040        let mut keyref_table = KeyTable::new(
1041            IdentityConstraintKey::default(),
1042            fk_name,
1043            IdentityKind::Keyref,
1044        );
1045        keyref_table.sequences.push(KeySequence {
1046            fields: vec![Some(make_string_field("missing"))],
1047        });
1048
1049        let errs = keyref_table.check_keyref_against(&key_table, &nt);
1050        assert_eq!(errs.len(), 1);
1051        assert_eq!(errs[0].constraint, "cvc-identity-constraint.4.3");
1052    }
1053
1054    // -----------------------------------------------------------------------
1055    // CompiledIdentityConstraint::compile
1056    // -----------------------------------------------------------------------
1057
1058    fn make_selector_result(xpath: &str) -> crate::parser::frames::SelectorResult {
1059        crate::parser::frames::SelectorResult {
1060            xpath: xpath.to_string(),
1061            xpath_default_namespace: None,
1062            ns_snapshot: NamespaceContextSnapshot::default(),
1063            id: None,
1064            annotation: None,
1065            source: None,
1066        }
1067    }
1068
1069    fn make_field_result(xpath: &str) -> crate::parser::frames::FieldResult {
1070        crate::parser::frames::FieldResult {
1071            xpath: xpath.to_string(),
1072            xpath_default_namespace: None,
1073            ns_snapshot: NamespaceContextSnapshot::default(),
1074            id: None,
1075            annotation: None,
1076            source: None,
1077        }
1078    }
1079
1080    fn make_identity_data(
1081        kind: IdentityKind,
1082        name: NameId,
1083        selector_xpath: &str,
1084        field_xpaths: &[&str],
1085    ) -> IdentityConstraintData {
1086        IdentityConstraintData {
1087            kind,
1088            name,
1089            ref_name: None,
1090            refer: None,
1091            selector: make_selector_result(selector_xpath),
1092            fields: field_xpaths.iter().map(|x| make_field_result(x)).collect(),
1093            id: None,
1094            annotation: None,
1095            source: None,
1096        }
1097    }
1098
1099    #[test]
1100    fn compile_simple_constraint() {
1101        let nt = make_name_table();
1102        let name = nt.add("testKey");
1103        let key = IdentityConstraintKey::default();
1104
1105        let data = make_identity_data(IdentityKind::Key, name, "./item", &["@id"]);
1106
1107        let compiled =
1108            CompiledIdentityConstraint::compile(&data, key, &nt, None, None, XsdVersion::V1_0);
1109        assert!(compiled.is_ok());
1110        let compiled = compiled.unwrap();
1111        assert_eq!(compiled.field_count, 1);
1112        assert_eq!(compiled.kind, IdentityKind::Key);
1113        assert_eq!(compiled.name, name);
1114    }
1115
1116    #[test]
1117    fn compile_invalid_xpath_propagates_error() {
1118        let nt = make_name_table();
1119        let name = nt.add("badKey");
1120        let key = IdentityConstraintKey::default();
1121
1122        let data = make_identity_data(IdentityKind::Key, name, "///invalid", &["@id"]);
1123
1124        let result =
1125            CompiledIdentityConstraint::compile(&data, key, &nt, None, None, XsdVersion::V1_0);
1126        assert!(result.is_err());
1127    }
1128
1129    // -----------------------------------------------------------------------
1130    // ConstraintStruct lifecycle
1131    // -----------------------------------------------------------------------
1132
1133    #[test]
1134    fn constraint_struct_lifecycle() {
1135        let nt = make_name_table();
1136        let name = nt.add("testKey");
1137        let key = IdentityConstraintKey::default();
1138
1139        let data = make_identity_data(IdentityKind::Key, name, "./item", &["@id"]);
1140        let compiled =
1141            CompiledIdentityConstraint::compile(&data, key, &nt, None, None, XsdVersion::V1_0)
1142                .unwrap();
1143
1144        let mut cs = ConstraintStruct::new(&compiled);
1145
1146        // Activate at scope element
1147        let scope_match = cs.activate();
1148        assert!(!scope_match); // ./item is not a bare "."
1149        assert!(cs.is_active());
1150
1151        // Start element: "item" (the selector target)
1152        let item_name = nt.add("item");
1153        let empty_ns = NameId(0);
1154        cs.start_element(item_name, empty_ns);
1155
1156        // The selector should have matched "item"
1157        assert!(cs.collecting_fields());
1158
1159        // Check attribute "@id"
1160        let id_name = nt.add("id");
1161        let matches = cs.matching_fields(id_name, empty_ns);
1162        assert_eq!(matches, vec![0]);
1163
1164        // Set field value
1165        cs.set_field_value(0, "val1".to_string(), None);
1166
1167        // End element — should finalize key sequence
1168        let errors = cs.end_element_with_text("", None, false, false, &nt, "/root/item[1]", None);
1169        assert!(errors.is_empty());
1170
1171        // Key table should have one sequence
1172        assert_eq!(cs.key_table.sequences.len(), 1);
1173        assert!(cs.key_table.sequences[0].is_complete());
1174        assert_eq!(
1175            cs.key_table.sequences[0].fields[0]
1176                .as_ref()
1177                .unwrap()
1178                .string_value,
1179            "val1"
1180        );
1181    }
1182
1183    /// Nested selector matches: `.//item` with `<item><item/></item>`.
1184    /// Both outer and inner matches should produce independent key sequences.
1185    #[test]
1186    fn constraint_struct_nested_selector() {
1187        let nt = make_name_table();
1188        let name = nt.add("uq");
1189        let key = IdentityConstraintKey::default();
1190
1191        let data = make_identity_data(IdentityKind::Unique, name, ".//item", &["@id"]);
1192        let compiled =
1193            CompiledIdentityConstraint::compile(&data, key, &nt, None, None, XsdVersion::V1_0)
1194                .unwrap();
1195
1196        let mut cs = ConstraintStruct::new(&compiled);
1197        cs.activate();
1198
1199        let item = nt.add("item");
1200        let id = nt.add("id");
1201        let ns = NameId(0);
1202
1203        // Outer <item>
1204        cs.start_element(item, ns);
1205        assert!(cs.collecting_fields());
1206        let m = cs.matching_fields(id, ns);
1207        assert_eq!(m, vec![0]);
1208        cs.set_field_value(0, "outer".to_string(), None);
1209
1210        // Inner <item> (nested match — pushes second frame)
1211        cs.start_element(item, ns);
1212        // Now two frames on the stack
1213        let m = cs.matching_fields(id, ns);
1214        assert_eq!(m, vec![0]); // top frame's field
1215        cs.set_field_value(0, "inner".to_string(), None);
1216
1217        // End inner </item> — finalizes inner sequence
1218        let errors = cs.end_element_with_text("", None, false, false, &nt, "/root/item/item", None);
1219        assert!(errors.is_empty());
1220        assert_eq!(cs.key_table.sequences.len(), 1);
1221        assert_eq!(
1222            cs.key_table.sequences[0].fields[0]
1223                .as_ref()
1224                .unwrap()
1225                .string_value,
1226            "inner"
1227        );
1228
1229        // Outer frame is still active
1230        assert!(cs.collecting_fields());
1231
1232        // End outer </item> — finalizes outer sequence
1233        let errors = cs.end_element_with_text("", None, false, false, &nt, "/root/item", None);
1234        assert!(errors.is_empty());
1235        assert_eq!(cs.key_table.sequences.len(), 2);
1236        assert_eq!(
1237            cs.key_table.sequences[1].fields[0]
1238                .as_ref()
1239                .unwrap()
1240                .string_value,
1241            "outer"
1242        );
1243
1244        // No more active frames
1245        assert!(!cs.collecting_fields());
1246    }
1247
1248    /// Multiple fields matching the same attribute should all be populated.
1249    #[test]
1250    fn constraint_struct_multi_field_same_attr() {
1251        let nt = make_name_table();
1252        let name = nt.add("uq2");
1253        let key = IdentityConstraintKey::default();
1254
1255        // Two fields both matching @id
1256        let data = make_identity_data(IdentityKind::Unique, name, "./item", &["@id", "@id"]);
1257        let compiled =
1258            CompiledIdentityConstraint::compile(&data, key, &nt, None, None, XsdVersion::V1_0)
1259                .unwrap();
1260
1261        let mut cs = ConstraintStruct::new(&compiled);
1262        cs.activate();
1263
1264        let item = nt.add("item");
1265        let id = nt.add("id");
1266        let ns = NameId(0);
1267
1268        cs.start_element(item, ns);
1269        let matches = cs.matching_fields(id, ns);
1270        assert_eq!(matches, vec![0, 1]); // both fields match
1271
1272        cs.set_field_value(0, "v".to_string(), None);
1273        cs.set_field_value(1, "v".to_string(), None);
1274
1275        let errors = cs.end_element_with_text("", None, false, false, &nt, "/root/item", None);
1276        assert!(errors.is_empty());
1277        assert!(cs.key_table.sequences[0].is_complete());
1278    }
1279}