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