lnmp_core/
record.rs

1//! Record and field structures for LNMP data.
2//!
3//! ## Field Ordering
4//!
5//! `LnmpRecord` stores fields internally in a `Vec`, maintaining **insertion order**.
6//! However, for deterministic behavior and canonical representation, use `sorted_fields()`:
7//!
8//! ```
9//! use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
10//!
11//! let mut record = LnmpRecord::new();
12//! record.add_field(LnmpField { fid: 23, value: LnmpValue::Int(3) });
13//! record.add_field(LnmpField { fid: 7, value: LnmpValue::Int(1) });
14//! record.add_field(LnmpField { fid: 12, value: LnmpValue::Int(2) });
15//!
16//! // Insertion order (non-deterministic across different constructions)
17//! assert_eq!(record.fields()[0].fid, 23);
18//! assert_eq!(record.fields()[1].fid, 7);
19//! assert_eq!(record.fields()[2].fid, 12);
20//!
21//! // Canonical order (deterministic, sorted by FID)
22//! let sorted = record.sorted_fields();
23//! assert_eq!(sorted[0].fid, 7);
24//! assert_eq!(sorted[1].fid, 12);
25//! assert_eq!(sorted[2].fid, 23);
26//! ```
27//!
28//! ## When to Use Each
29//!
30//! - **`fields()`**: When insertion order is semantically important
31//!   - Direct iteration over fields as added
32//!   - Structural equality (order-sensitive)
33//!
34//! - **`sorted_fields()`**: For canonical representation
35//!   - Encoding (text/binary)
36//!   - Checksum computation
37//!   - Semantic comparison (order-independent)
38//!   - Deterministic output
39//!
40//! ## Deterministic Guarantees
41//!
42//! The following components **always use `sorted_fields()`** for determinism:
43//! - `SemanticChecksum::serialize_value()` - Field-order-independent checksums
44//! - All encoders in `lnmp-codec` - Canonical output
45//! - Binary format - Sorted fields for stable round-trips
46//!
47//! This ensures that two records with the same fields but different insertion
48//! orders will produce identical checksums and encodings.
49
50use crate::{FieldId, LnmpValue};
51
52/// A single field assignment (field ID + value pair)
53#[derive(Debug, Clone, PartialEq)]
54#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
55pub struct LnmpField {
56    /// Field identifier
57    pub fid: FieldId,
58    /// Field value
59    pub value: LnmpValue,
60}
61
62/// A complete LNMP record (collection of fields)
63#[derive(Debug, Clone, PartialEq, Default)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub struct LnmpRecord {
66    fields: Vec<LnmpField>,
67}
68
69impl LnmpRecord {
70    /// Creates a new empty record
71    pub fn new() -> Self {
72        Self::default()
73    }
74
75    /// Adds a field to the record
76    pub fn add_field(&mut self, field: LnmpField) {
77        self.fields.push(field);
78    }
79
80    /// Gets a field by field ID (returns the first match if duplicates exist)
81    pub fn get_field(&self, fid: FieldId) -> Option<&LnmpField> {
82        self.fields.iter().find(|f| f.fid == fid)
83    }
84
85    /// Removes all fields with the given field ID
86    pub fn remove_field(&mut self, fid: FieldId) {
87        self.fields.retain(|f| f.fid != fid);
88    }
89
90    /// Returns a slice of all fields in the record
91    pub fn fields(&self) -> &[LnmpField] {
92        &self.fields
93    }
94
95    /// Consumes the record and returns the fields vector
96    pub fn into_fields(self) -> Vec<LnmpField> {
97        self.fields
98    }
99
100    /// Returns fields sorted by field ID (stable sort preserves insertion order for duplicates)
101    pub fn sorted_fields(&self) -> Vec<LnmpField> {
102        let mut sorted = self.fields.clone();
103        sorted.sort_by_key(|f| f.fid);
104        sorted
105    }
106
107    /// Creates a record from a vector of fields (typically already sorted)
108    pub fn from_sorted_fields(fields: Vec<LnmpField>) -> Self {
109        Self { fields }
110    }
111
112    /// Creates a record from fields, automatically sorting by FID
113    ///
114    /// This ensures canonical field ordering regardless of input order.
115    /// Use this constructor when building records from unsorted field collections.
116    ///
117    /// # Example
118    ///
119    /// ```
120    /// use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
121    ///
122    /// let fields = vec![
123    ///     LnmpField { fid: 23, value: LnmpValue::Int(3) },
124    ///     LnmpField { fid: 7, value: LnmpValue::Int(1) },
125    ///     LnmpField { fid: 12, value: LnmpValue::Int(2) },
126    /// ];
127    ///
128    /// let record = LnmpRecord::from_fields(fields);
129    ///
130    /// // Fields are automatically sorted by FID
131    /// assert_eq!(record.fields()[0].fid, 7);
132    /// assert_eq!(record.fields()[1].fid, 12);
133    /// assert_eq!(record.fields()[2].fid, 23);
134    /// ```
135    pub fn from_fields(mut fields: Vec<LnmpField>) -> Self {
136        fields.sort_by_key(|f| f.fid);
137        Self::from_sorted_fields(fields)
138    }
139
140    /// Validates this record against structural limits (depth, field counts, lengths).
141    pub fn validate_with_limits(
142        &self,
143        limits: &crate::limits::StructuralLimits,
144    ) -> Result<(), crate::limits::StructuralError> {
145        limits.validate_record(self)
146    }
147
148    /// Compares two records based on canonical form (field order independent).
149    ///
150    /// This method compares records semantically by comparing their sorted fields.
151    /// Two records are canonically equal if they have the same fields (same FID and value),
152    /// regardless of the order in which fields were added.
153    ///
154    /// # Example
155    ///
156    /// ```
157    /// use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
158    ///
159    /// let mut rec1 = LnmpRecord::new();
160    /// rec1.add_field(LnmpField { fid: 12, value: LnmpValue::Int(1) });
161    /// rec1.add_field(LnmpField { fid: 7, value: LnmpValue::Int(2) });
162    ///
163    /// let mut rec2 = LnmpRecord::new();
164    /// rec2.add_field(LnmpField { fid: 7, value: LnmpValue::Int(2) });
165    /// rec2.add_field(LnmpField { fid: 12, value: LnmpValue::Int(1) });
166    ///
167    /// // Structural equality: order matters
168    /// assert_ne!(rec1, rec2);
169    ///
170    /// // Canonical equality: order doesn't matter
171    /// assert!(rec1.canonical_eq(&rec2));
172    /// ```
173    pub fn canonical_eq(&self, other: &Self) -> bool {
174        self.sorted_fields() == other.sorted_fields()
175    }
176
177    /// Computes a hash based on canonical field ordering.
178    ///
179    /// This method computes a hash using sorted fields, ensuring that
180    /// the hash is independent of field insertion order. This is useful
181    /// for using `LnmpRecord` in `HashMap` or `HashSet` with semantic equality.
182    ///
183    /// # Example
184    ///
185    /// ```
186    /// use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
187    /// use std::hash::{Hash, Hasher};
188    /// use std::collections::hash_map::DefaultHasher;
189    ///
190    /// let mut rec1 = LnmpRecord::new();
191    /// rec1.add_field(LnmpField { fid: 12, value: LnmpValue::Int(1) });
192    /// rec1.add_field(LnmpField { fid: 7, value: LnmpValue::Int(2) });
193    ///
194    /// let mut rec2 = LnmpRecord::new();
195    /// rec2.add_field(LnmpField { fid: 7, value: LnmpValue::Int(2) });
196    /// rec2.add_field(LnmpField { fid: 12, value: LnmpValue::Int(1) });
197    ///
198    /// let mut hasher1 = DefaultHasher::new();
199    /// rec1.canonical_hash(&mut hasher1);
200    /// let hash1 = hasher1.finish();
201    ///
202    /// let mut hasher2 = DefaultHasher::new();
203    /// rec2.canonical_hash(&mut hasher2);
204    /// let hash2 = hasher2.finish();
205    ///
206    /// // Same canonical hash despite different insertion order
207    /// assert_eq!(hash1, hash2);
208    /// ```
209    pub fn canonical_hash<H: std::hash::Hasher>(&self, state: &mut H) {
210        use std::hash::Hash;
211
212        // Hash the sorted fields
213        for field in self.sorted_fields() {
214            field.fid.hash(state);
215
216            // Hash the value based on its type
217            match &field.value {
218                LnmpValue::Int(i) => {
219                    0u8.hash(state); // Discriminant
220                    i.hash(state);
221                }
222                LnmpValue::Float(f) => {
223                    1u8.hash(state); // Discriminant
224                                     // Hash float bits for deterministic hashing
225                    f.to_bits().hash(state);
226                }
227                LnmpValue::Bool(b) => {
228                    2u8.hash(state); // Discriminant
229                    b.hash(state);
230                }
231                LnmpValue::String(s) => {
232                    3u8.hash(state); // Discriminant
233                    s.hash(state);
234                }
235                LnmpValue::StringArray(arr) => {
236                    4u8.hash(state); // Discriminant
237                    arr.len().hash(state);
238                    for s in arr {
239                        s.hash(state);
240                    }
241                }
242                LnmpValue::IntArray(arr) => {
243                    10u8.hash(state); // Discriminant
244                    arr.len().hash(state);
245                    for &i in arr {
246                        i.hash(state);
247                    }
248                }
249                LnmpValue::FloatArray(arr) => {
250                    11u8.hash(state); // Discriminant
251                    arr.len().hash(state);
252                    for &f in arr {
253                        f.to_bits().hash(state);
254                    }
255                }
256                LnmpValue::BoolArray(arr) => {
257                    12u8.hash(state); // Discriminant
258                    arr.len().hash(state);
259                    for &b in arr {
260                        b.hash(state);
261                    }
262                }
263                LnmpValue::NestedRecord(record) => {
264                    5u8.hash(state); // Discriminant
265                                     // Recursively use canonical hash
266                    record.canonical_hash(state);
267                }
268                LnmpValue::NestedArray(records) => {
269                    6u8.hash(state); // Discriminant
270                    records.len().hash(state);
271                    for rec in records {
272                        rec.canonical_hash(state);
273                    }
274                }
275                LnmpValue::Embedding(vec) => {
276                    7u8.hash(state); // Discriminant
277                                     // Hash the embedding data using public fields
278                    vec.dim.hash(state);
279                    format!("{:?}", vec.dtype).hash(state); // Hash enum variant
280                    vec.data.hash(state);
281                }
282                LnmpValue::EmbeddingDelta(delta) => {
283                    8u8.hash(state); // Discriminant
284                                     // Hash delta base_id and changes
285                    delta.base_id.hash(state);
286                    delta.changes.len().hash(state);
287                    for change in &delta.changes {
288                        change.index.hash(state);
289                        change.delta.to_bits().hash(state);
290                    }
291                }
292                #[cfg(feature = "quant")]
293                LnmpValue::QuantizedEmbedding(qv) => {
294                    9u8.hash(state); // Discriminant
295                                     // Hash quantized data
296                    format!("{:?}", qv.scheme).hash(state);
297                    qv.scale.to_bits().hash(state);
298                    qv.zero_point.hash(state);
299                    qv.data.hash(state);
300                }
301            }
302        }
303    }
304
305    /// Validates that fields are in canonical order (sorted by FID).
306    ///
307    /// Returns `Ok(())` if fields are sorted, or an error with details about
308    /// the first out-of-order field.
309    ///
310    /// # Example
311    ///
312    /// ```
313    /// use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
314    ///
315    /// // Sorted record
316    /// let record = LnmpRecord::from_fields(vec![
317    ///     LnmpField { fid: 7, value: LnmpValue::Int(1) },
318    ///     LnmpField { fid: 12, value: LnmpValue::Int(2) },
319    /// ]);
320    /// assert!(record.validate_field_ordering().is_ok());
321    ///
322    /// // Unsorted record
323    /// let mut record = LnmpRecord::new();
324    /// record.add_field(LnmpField { fid: 12, value: LnmpValue::Int(2) });
325    /// record.add_field(LnmpField { fid: 7, value: LnmpValue::Int(1) });
326    /// assert!(record.validate_field_ordering().is_err());
327    /// ```
328    pub fn validate_field_ordering(&self) -> Result<(), FieldOrderingError> {
329        let fields = self.fields();
330
331        for i in 1..fields.len() {
332            if fields[i].fid < fields[i - 1].fid {
333                return Err(FieldOrderingError {
334                    position: i,
335                    current_fid: fields[i].fid,
336                    previous_fid: fields[i - 1].fid,
337                });
338            }
339        }
340
341        Ok(())
342    }
343
344    /// Returns whether fields are in canonical order (sorted by FID).
345    ///
346    /// This is a convenience method that returns a boolean instead of a Result.
347    ///
348    /// # Example
349    ///
350    /// ```
351    /// use lnmp_core::{RecordBuilder, LnmpField, LnmpValue};
352    ///
353    /// let record = RecordBuilder::new()
354    ///     .add_field(LnmpField { fid: 7, value: LnmpValue::Int(1) })
355    ///     .build();
356    ///
357    /// assert!(record.is_canonical_order());
358    /// ```
359    pub fn is_canonical_order(&self) -> bool {
360        self.validate_field_ordering().is_ok()
361    }
362
363    /// Returns the number of out-of-order field pairs.
364    ///
365    /// A count of 0 means the record is in canonical order.
366    /// Higher counts indicate more disorder.
367    ///
368    /// # Example
369    ///
370    /// ```
371    /// use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
372    ///
373    /// let mut record = LnmpRecord::new();
374    /// record.add_field(LnmpField { fid: 23, value: LnmpValue::Int(3) });
375    /// record.add_field(LnmpField { fid: 7, value: LnmpValue::Int(1) });
376    /// record.add_field(LnmpField { fid: 12, value: LnmpValue::Int(2) });
377    ///
378    /// // 23 > 7 (disorder), 7 < 12 (ok), so 1 disorder
379    /// assert_eq!(record.count_ordering_violations(), 1);
380    /// ```
381    pub fn count_ordering_violations(&self) -> usize {
382        let fields = self.fields();
383        let mut count = 0;
384
385        for i in 1..fields.len() {
386            if fields[i].fid < fields[i - 1].fid {
387                count += 1;
388            }
389        }
390
391        count
392    }
393}
394
395/// Error returned when field ordering validation fails
396#[derive(Debug, Clone, PartialEq)]
397pub struct FieldOrderingError {
398    /// Position (index) where the ordering violation was found
399    pub position: usize,
400    /// FID of the field at the violation position
401    pub current_fid: FieldId,
402    /// FID of the previous field (which is greater than current_fid)
403    pub previous_fid: FieldId,
404}
405
406impl std::fmt::Display for FieldOrderingError {
407    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408        write!(
409            f,
410            "Field ordering violation at position {}: F{} appears after F{} (expected ascending FID order)",
411            self.position, self.current_fid, self.previous_fid
412        )
413    }
414}
415
416impl std::error::Error for FieldOrderingError {}
417
418#[cfg(test)]
419#[allow(clippy::approx_constant)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn test_validate_field_ordering_sorted() {
425        let record = LnmpRecord::from_fields(vec![
426            LnmpField {
427                fid: 7,
428                value: LnmpValue::Int(1),
429            },
430            LnmpField {
431                fid: 12,
432                value: LnmpValue::Int(2),
433            },
434            LnmpField {
435                fid: 23,
436                value: LnmpValue::Int(3),
437            },
438        ]);
439
440        assert!(record.validate_field_ordering().is_ok());
441    }
442
443    #[test]
444    fn test_validate_field_ordering_unsorted() {
445        let mut record = LnmpRecord::new();
446        record.add_field(LnmpField {
447            fid: 12,
448            value: LnmpValue::Int(2),
449        });
450        record.add_field(LnmpField {
451            fid: 7,
452            value: LnmpValue::Int(1),
453        });
454
455        let result = record.validate_field_ordering();
456        assert!(result.is_err());
457
458        let err = result.unwrap_err();
459        assert_eq!(err.position, 1);
460        assert_eq!(err.current_fid, 7);
461        assert_eq!(err.previous_fid, 12);
462    }
463
464    #[test]
465    fn test_validate_field_ordering_empty() {
466        let record = LnmpRecord::new();
467        assert!(record.validate_field_ordering().is_ok());
468    }
469
470    #[test]
471    fn test_is_canonical_order() {
472        let sorted = LnmpRecord::from_fields(vec![
473            LnmpField {
474                fid: 1,
475                value: LnmpValue::Int(1),
476            },
477            LnmpField {
478                fid: 2,
479                value: LnmpValue::Int(2),
480            },
481        ]);
482        assert!(sorted.is_canonical_order());
483
484        let mut unsorted = LnmpRecord::new();
485        unsorted.add_field(LnmpField {
486            fid: 2,
487            value: LnmpValue::Int(2),
488        });
489        unsorted.add_field(LnmpField {
490            fid: 1,
491            value: LnmpValue::Int(1),
492        });
493        assert!(!unsorted.is_canonical_order());
494    }
495
496    #[test]
497    fn test_count_ordering_violations_none() {
498        let record = LnmpRecord::from_fields(vec![
499            LnmpField {
500                fid: 1,
501                value: LnmpValue::Int(1),
502            },
503            LnmpField {
504                fid: 2,
505                value: LnmpValue::Int(2),
506            },
507            LnmpField {
508                fid: 3,
509                value: LnmpValue::Int(3),
510            },
511        ]);
512
513        assert_eq!(record.count_ordering_violations(), 0);
514    }
515
516    #[test]
517    fn test_count_ordering_violations_one() {
518        let mut record = LnmpRecord::new();
519        record.add_field(LnmpField {
520            fid: 23,
521            value: LnmpValue::Int(3),
522        });
523        record.add_field(LnmpField {
524            fid: 7,
525            value: LnmpValue::Int(1),
526        });
527        record.add_field(LnmpField {
528            fid: 12,
529            value: LnmpValue::Int(2),
530        });
531
532        // 23 > 7 is a violation, then 7 < 12 is ok
533        assert_eq!(record.count_ordering_violations(), 1);
534    }
535
536    #[test]
537    fn test_count_ordering_violations_multiple() {
538        let mut record = LnmpRecord::new();
539        record.add_field(LnmpField {
540            fid: 23,
541            value: LnmpValue::Int(3),
542        });
543        record.add_field(LnmpField {
544            fid: 7,
545            value: LnmpValue::Int(1),
546        });
547        record.add_field(LnmpField {
548            fid: 3,
549            value: LnmpValue::Int(0),
550        });
551
552        // 23 > 7 (violation), 7 > 3 (violation)
553        assert_eq!(record.count_ordering_violations(), 2);
554    }
555
556    #[test]
557    fn test_field_ordering_error_display() {
558        let error = FieldOrderingError {
559            position: 1,
560            current_fid: 7,
561            previous_fid: 12,
562        };
563
564        let msg = error.to_string();
565        assert!(msg.contains("position 1"));
566        assert!(msg.contains("F7"));
567        assert!(msg.contains("F12"));
568        assert!(msg.contains("ascending FID order"));
569    }
570
571    #[test]
572    fn test_new_record_is_empty() {
573        let record = LnmpRecord::new();
574        assert_eq!(record.fields().len(), 0);
575    }
576
577    #[test]
578    fn test_add_field() {
579        let mut record = LnmpRecord::new();
580        record.add_field(LnmpField {
581            fid: 12,
582            value: LnmpValue::Int(14532),
583        });
584
585        assert_eq!(record.fields().len(), 1);
586        assert_eq!(record.fields()[0].fid, 12);
587    }
588
589    #[test]
590    fn test_get_field() {
591        let mut record = LnmpRecord::new();
592        record.add_field(LnmpField {
593            fid: 12,
594            value: LnmpValue::Int(14532),
595        });
596        record.add_field(LnmpField {
597            fid: 7,
598            value: LnmpValue::Bool(true),
599        });
600
601        let field = record.get_field(12);
602        assert!(field.is_some());
603        assert_eq!(field.unwrap().value, LnmpValue::Int(14532));
604
605        let missing = record.get_field(99);
606        assert!(missing.is_none());
607    }
608
609    #[test]
610    fn test_get_field_with_duplicates() {
611        let mut record = LnmpRecord::new();
612        record.add_field(LnmpField {
613            fid: 5,
614            value: LnmpValue::String("first".to_string()),
615        });
616        record.add_field(LnmpField {
617            fid: 5,
618            value: LnmpValue::String("second".to_string()),
619        });
620
621        // Should return the first match
622        let field = record.get_field(5);
623        assert!(field.is_some());
624        assert_eq!(field.unwrap().value, LnmpValue::String("first".to_string()));
625    }
626
627    #[test]
628    fn test_fields_iteration() {
629        let mut record = LnmpRecord::new();
630        record.add_field(LnmpField {
631            fid: 1,
632            value: LnmpValue::Int(100),
633        });
634        record.add_field(LnmpField {
635            fid: 2,
636            value: LnmpValue::Float(3.14),
637        });
638        record.add_field(LnmpField {
639            fid: 3,
640            value: LnmpValue::Bool(false),
641        });
642
643        let fields = record.fields();
644        assert_eq!(fields.len(), 3);
645        assert_eq!(fields[0].fid, 1);
646        assert_eq!(fields[1].fid, 2);
647        assert_eq!(fields[2].fid, 3);
648    }
649
650    #[test]
651    fn test_into_fields() {
652        let mut record = LnmpRecord::new();
653        record.add_field(LnmpField {
654            fid: 10,
655            value: LnmpValue::String("test".to_string()),
656        });
657
658        let fields = record.into_fields();
659        assert_eq!(fields.len(), 1);
660        assert_eq!(fields[0].fid, 10);
661    }
662
663    #[test]
664    fn test_lnmp_value_equality() {
665        assert_eq!(LnmpValue::Int(42), LnmpValue::Int(42));
666        assert_ne!(LnmpValue::Int(42), LnmpValue::Int(43));
667
668        assert_eq!(LnmpValue::Float(3.14), LnmpValue::Float(3.14));
669
670        assert_eq!(LnmpValue::Bool(true), LnmpValue::Bool(true));
671        assert_ne!(LnmpValue::Bool(true), LnmpValue::Bool(false));
672
673        assert_eq!(
674            LnmpValue::String("hello".to_string()),
675            LnmpValue::String("hello".to_string())
676        );
677
678        assert_eq!(
679            LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()]),
680            LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()])
681        );
682    }
683
684    #[test]
685    fn test_lnmp_value_clone() {
686        let original = LnmpValue::StringArray(vec!["test".to_string()]);
687        let cloned = original.clone();
688        assert_eq!(original, cloned);
689    }
690
691    #[test]
692    fn test_empty_record() {
693        let record = LnmpRecord::new();
694        assert_eq!(record.fields().len(), 0);
695        assert!(record.get_field(1).is_none());
696    }
697
698    #[test]
699    fn test_record_with_all_value_types() {
700        let mut record = LnmpRecord::new();
701
702        record.add_field(LnmpField {
703            fid: 1,
704            value: LnmpValue::Int(-42),
705        });
706        record.add_field(LnmpField {
707            fid: 2,
708            value: LnmpValue::Float(3.14159),
709        });
710        record.add_field(LnmpField {
711            fid: 3,
712            value: LnmpValue::Bool(true),
713        });
714        record.add_field(LnmpField {
715            fid: 4,
716            value: LnmpValue::String("hello world".to_string()),
717        });
718        record.add_field(LnmpField {
719            fid: 5,
720            value: LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()]),
721        });
722
723        assert_eq!(record.fields().len(), 5);
724        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(-42));
725        assert_eq!(
726            record.get_field(2).unwrap().value,
727            LnmpValue::Float(3.14159)
728        );
729        assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Bool(true));
730        assert_eq!(
731            record.get_field(4).unwrap().value,
732            LnmpValue::String("hello world".to_string())
733        );
734        assert_eq!(
735            record.get_field(5).unwrap().value,
736            LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()])
737        );
738    }
739
740    #[test]
741    fn test_sorted_fields_basic() {
742        let mut record = LnmpRecord::new();
743        record.add_field(LnmpField {
744            fid: 23,
745            value: LnmpValue::Int(3),
746        });
747        record.add_field(LnmpField {
748            fid: 7,
749            value: LnmpValue::Int(2),
750        });
751        record.add_field(LnmpField {
752            fid: 12,
753            value: LnmpValue::Int(1),
754        });
755
756        let sorted = record.sorted_fields();
757        assert_eq!(sorted.len(), 3);
758        assert_eq!(sorted[0].fid, 7);
759        assert_eq!(sorted[1].fid, 12);
760        assert_eq!(sorted[2].fid, 23);
761    }
762
763    #[test]
764    fn test_sorted_fields_preserves_duplicate_order() {
765        let mut record = LnmpRecord::new();
766        record.add_field(LnmpField {
767            fid: 5,
768            value: LnmpValue::String("first".to_string()),
769        });
770        record.add_field(LnmpField {
771            fid: 10,
772            value: LnmpValue::Int(100),
773        });
774        record.add_field(LnmpField {
775            fid: 5,
776            value: LnmpValue::String("second".to_string()),
777        });
778
779        let sorted = record.sorted_fields();
780        assert_eq!(sorted.len(), 3);
781        assert_eq!(sorted[0].fid, 5);
782        assert_eq!(sorted[0].value, LnmpValue::String("first".to_string()));
783        assert_eq!(sorted[1].fid, 5);
784        assert_eq!(sorted[1].value, LnmpValue::String("second".to_string()));
785        assert_eq!(sorted[2].fid, 10);
786    }
787
788    #[test]
789    fn test_sorted_fields_already_sorted() {
790        let mut record = LnmpRecord::new();
791        record.add_field(LnmpField {
792            fid: 1,
793            value: LnmpValue::Int(1),
794        });
795        record.add_field(LnmpField {
796            fid: 2,
797            value: LnmpValue::Int(2),
798        });
799        record.add_field(LnmpField {
800            fid: 3,
801            value: LnmpValue::Int(3),
802        });
803
804        let sorted = record.sorted_fields();
805        assert_eq!(sorted.len(), 3);
806        assert_eq!(sorted[0].fid, 1);
807        assert_eq!(sorted[1].fid, 2);
808        assert_eq!(sorted[2].fid, 3);
809    }
810
811    #[test]
812    fn test_sorted_fields_empty_record() {
813        let record = LnmpRecord::new();
814        let sorted = record.sorted_fields();
815        assert_eq!(sorted.len(), 0);
816    }
817
818    #[test]
819    fn test_sorted_fields_does_not_modify_original() {
820        let mut record = LnmpRecord::new();
821        record.add_field(LnmpField {
822            fid: 23,
823            value: LnmpValue::Int(3),
824        });
825        record.add_field(LnmpField {
826            fid: 7,
827            value: LnmpValue::Int(2),
828        });
829
830        let _sorted = record.sorted_fields();
831
832        // Original record should remain unchanged
833        assert_eq!(record.fields()[0].fid, 23);
834        assert_eq!(record.fields()[1].fid, 7);
835    }
836
837    #[test]
838    fn test_canonical_eq_same_order() {
839        let mut rec1 = LnmpRecord::new();
840        rec1.add_field(LnmpField {
841            fid: 7,
842            value: LnmpValue::Int(1),
843        });
844        rec1.add_field(LnmpField {
845            fid: 12,
846            value: LnmpValue::Int(2),
847        });
848
849        let mut rec2 = LnmpRecord::new();
850        rec2.add_field(LnmpField {
851            fid: 7,
852            value: LnmpValue::Int(1),
853        });
854        rec2.add_field(LnmpField {
855            fid: 12,
856            value: LnmpValue::Int(2),
857        });
858
859        assert_eq!(rec1, rec2); // Structural equality
860        assert!(rec1.canonical_eq(&rec2)); // Canonical equality
861    }
862
863    #[test]
864    fn test_canonical_eq_different_order() {
865        let mut rec1 = LnmpRecord::new();
866        rec1.add_field(LnmpField {
867            fid: 12,
868            value: LnmpValue::Int(1),
869        });
870        rec1.add_field(LnmpField {
871            fid: 7,
872            value: LnmpValue::Int(2),
873        });
874
875        let mut rec2 = LnmpRecord::new();
876        rec2.add_field(LnmpField {
877            fid: 7,
878            value: LnmpValue::Int(2),
879        });
880        rec2.add_field(LnmpField {
881            fid: 12,
882            value: LnmpValue::Int(1),
883        });
884
885        assert_ne!(rec1, rec2); // Structural inequality (different order)
886        assert!(rec1.canonical_eq(&rec2)); // Canonical equality (same fields)
887    }
888
889    #[test]
890    fn test_canonical_eq_different_values() {
891        let mut rec1 = LnmpRecord::new();
892        rec1.add_field(LnmpField {
893            fid: 7,
894            value: LnmpValue::Int(1),
895        });
896
897        let mut rec2 = LnmpRecord::new();
898        rec2.add_field(LnmpField {
899            fid: 7,
900            value: LnmpValue::Int(2), // Different value
901        });
902
903        assert!(!rec1.canonical_eq(&rec2));
904    }
905
906    #[test]
907    fn test_canonical_eq_with_nested_records() {
908        // Create two nested records with SAME field order
909        // (Note: canonical_eq compares sorted fields at top level,
910        //  but nested NestedRecord values use PartialEq which is order-sensitive)
911        let mut inner1 = LnmpRecord::new();
912        inner1.add_field(LnmpField {
913            fid: 1,
914            value: LnmpValue::Int(100),
915        });
916        inner1.add_field(LnmpField {
917            fid: 2,
918            value: LnmpValue::Int(200),
919        });
920
921        let mut inner2 = LnmpRecord::new();
922        inner2.add_field(LnmpField {
923            fid: 1,
924            value: LnmpValue::Int(100),
925        });
926        inner2.add_field(LnmpField {
927            fid: 2,
928            value: LnmpValue::Int(200),
929        });
930
931        // Create outer records in different field orders
932        let mut rec1 = LnmpRecord::new();
933        rec1.add_field(LnmpField {
934            fid: 50,
935            value: LnmpValue::NestedRecord(Box::new(inner1)),
936        });
937        rec1.add_field(LnmpField {
938            fid: 30,
939            value: LnmpValue::Int(999),
940        });
941
942        let mut rec2 = LnmpRecord::new();
943        rec2.add_field(LnmpField {
944            fid: 30,
945            value: LnmpValue::Int(999),
946        });
947        rec2.add_field(LnmpField {
948            fid: 50,
949            value: LnmpValue::NestedRecord(Box::new(inner2)),
950        });
951
952        // Different field order at top level
953        assert_ne!(rec1, rec2);
954
955        // But canonical_eq should work
956        assert!(rec1.canonical_eq(&rec2));
957    }
958
959    #[test]
960    fn test_canonical_hash_same_order() {
961        use std::collections::hash_map::DefaultHasher;
962        use std::hash::Hasher;
963
964        let mut rec1 = LnmpRecord::new();
965        rec1.add_field(LnmpField {
966            fid: 7,
967            value: LnmpValue::Int(1),
968        });
969        rec1.add_field(LnmpField {
970            fid: 12,
971            value: LnmpValue::Int(2),
972        });
973
974        let mut rec2 = LnmpRecord::new();
975        rec2.add_field(LnmpField {
976            fid: 7,
977            value: LnmpValue::Int(1),
978        });
979        rec2.add_field(LnmpField {
980            fid: 12,
981            value: LnmpValue::Int(2),
982        });
983
984        let mut hasher1 = DefaultHasher::new();
985        rec1.canonical_hash(&mut hasher1);
986        let hash1 = hasher1.finish();
987
988        let mut hasher2 = DefaultHasher::new();
989        rec2.canonical_hash(&mut hasher2);
990        let hash2 = hasher2.finish();
991
992        assert_eq!(hash1, hash2);
993    }
994
995    #[test]
996    fn test_canonical_hash_different_order() {
997        use std::collections::hash_map::DefaultHasher;
998        use std::hash::Hasher;
999
1000        let mut rec1 = LnmpRecord::new();
1001        rec1.add_field(LnmpField {
1002            fid: 12,
1003            value: LnmpValue::Int(1),
1004        });
1005        rec1.add_field(LnmpField {
1006            fid: 7,
1007            value: LnmpValue::Int(2),
1008        });
1009
1010        let mut rec2 = LnmpRecord::new();
1011        rec2.add_field(LnmpField {
1012            fid: 7,
1013            value: LnmpValue::Int(2),
1014        });
1015        rec2.add_field(LnmpField {
1016            fid: 12,
1017            value: LnmpValue::Int(1),
1018        });
1019
1020        let mut hasher1 = DefaultHasher::new();
1021        rec1.canonical_hash(&mut hasher1);
1022        let hash1 = hasher1.finish();
1023
1024        let mut hasher2 = DefaultHasher::new();
1025        rec2.canonical_hash(&mut hasher2);
1026        let hash2 = hasher2.finish();
1027
1028        // Same hash despite different insertion order
1029        assert_eq!(hash1, hash2);
1030    }
1031
1032    #[test]
1033    fn test_canonical_hash_different_values() {
1034        use std::collections::hash_map::DefaultHasher;
1035        use std::hash::Hasher;
1036
1037        let mut rec1 = LnmpRecord::new();
1038        rec1.add_field(LnmpField {
1039            fid: 7,
1040            value: LnmpValue::Int(1),
1041        });
1042
1043        let mut rec2 = LnmpRecord::new();
1044        rec2.add_field(LnmpField {
1045            fid: 7,
1046            value: LnmpValue::Int(2),
1047        });
1048
1049        let mut hasher1 = DefaultHasher::new();
1050        rec1.canonical_hash(&mut hasher1);
1051        let hash1 = hasher1.finish();
1052
1053        let mut hasher2 = DefaultHasher::new();
1054        rec2.canonical_hash(&mut hasher2);
1055        let hash2 = hasher2.finish();
1056
1057        // Different hash for different values
1058        assert_ne!(hash1, hash2);
1059    }
1060}