Skip to main content

reifydb_core/encoded/shape/
fingerprint.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2025 ReifyDB
3
4//! RowShape fingerprint computation for content-addressable storage.
5//!
6//! The fingerprint is a deterministic hash of the shape's canonical representation,
7//! ensuring that identical shapes always produce the same fingerprint regardless
8//! of when or where they are created.
9
10use std::ops::Deref;
11
12use reifydb_runtime::hash::{Hash64, xxh3_64};
13use serde::{Deserialize, Serialize};
14
15use crate::encoded::shape::RowShapeField;
16
17/// A fingerprint that uniquely identifies a shape layout.
18///
19/// This is an 8-byte hash stored in the header of every encoded row,
20/// allowing the shape to be identified without external metadata.
21#[repr(transparent)]
22#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23pub struct RowShapeFingerprint(pub Hash64);
24
25impl Deref for RowShapeFingerprint {
26	type Target = u64;
27
28	fn deref(&self) -> &Self::Target {
29		&self.0.0
30	}
31}
32
33impl RowShapeFingerprint {
34	/// Create a new shape fingerprint from a u64 value.
35	#[inline]
36	pub const fn new(value: u64) -> Self {
37		Self(Hash64(value))
38	}
39
40	/// Create a zero/empty fingerprint.
41	#[inline]
42	pub const fn zero() -> Self {
43		Self(Hash64(0))
44	}
45
46	/// Get the underlying u64 value.
47	#[inline]
48	pub const fn as_u64(&self) -> u64 {
49		self.0.0
50	}
51
52	/// Convert to little-endian bytes.
53	#[inline]
54	pub const fn to_le_bytes(&self) -> [u8; 8] {
55		self.0.0.to_le_bytes()
56	}
57
58	/// Create from little-endian bytes.
59	#[inline]
60	pub const fn from_le_bytes(bytes: [u8; 8]) -> Self {
61		Self(Hash64(u64::from_le_bytes(bytes)))
62	}
63}
64
65impl From<Hash64> for RowShapeFingerprint {
66	fn from(hash: Hash64) -> Self {
67		Self(hash)
68	}
69}
70
71impl From<RowShapeFingerprint> for Hash64 {
72	fn from(fp: RowShapeFingerprint) -> Self {
73		fp.0
74	}
75}
76
77impl From<u64> for RowShapeFingerprint {
78	fn from(value: u64) -> Self {
79		Self(Hash64(value))
80	}
81}
82
83/// Compute a deterministic fingerprint for a shape based on its fields.
84///
85/// The fingerprint is computed by hashing a canonical binary representation
86/// of the fields. This ensures:
87/// - Same fields → same fingerprint (deterministic)
88/// - Different fields → different fingerprint (collision-resistant)
89///
90/// The canonical representation includes:
91/// - Number of fields (u16)
92/// - For each field:
93///   - Field name length (u16) + name bytes (UTF-8)
94///   - Base type (u8)
95///   - Constraint type (u8)
96///   - Constraint param1 (u32)
97///   - Constraint param2 (u32)
98pub fn compute_fingerprint(fields: &[RowShapeField]) -> RowShapeFingerprint {
99	// Estimate buffer size: 2 bytes for count + ~42 bytes per field average
100	let estimated_size = 2 + fields.len() * 42;
101	let mut buffer = Vec::with_capacity(estimated_size);
102
103	// Write field count as u16 (max 65535 fields)
104	let field_count = fields.len() as u16;
105	buffer.extend_from_slice(&field_count.to_le_bytes());
106
107	// Write each field in canonical order
108	for field in fields {
109		// Write name length and bytes
110		let name_bytes = field.name.as_bytes();
111		let name_len = name_bytes.len() as u16;
112		buffer.extend_from_slice(&name_len.to_le_bytes());
113		buffer.extend_from_slice(name_bytes);
114
115		// Write constraint info (base type + constraint type + params)
116		let ffi = field.constraint.to_ffi();
117		buffer.push(ffi.base_type);
118		buffer.push(ffi.constraint_type);
119		buffer.extend_from_slice(&ffi.constraint_param1.to_le_bytes());
120		buffer.extend_from_slice(&ffi.constraint_param2.to_le_bytes());
121	}
122
123	RowShapeFingerprint(xxh3_64(&buffer))
124}
125
126#[cfg(test)]
127mod tests {
128	use reifydb_type::value::{
129		constraint::{Constraint, TypeConstraint, bytes::MaxBytes, precision::Precision, scale::Scale},
130		r#type::Type,
131	};
132
133	use super::*;
134
135	fn make_field(name: &str, field_type: Type) -> RowShapeField {
136		RowShapeField {
137			name: name.to_string(),
138			constraint: TypeConstraint::unconstrained(field_type),
139			offset: 0,
140			size: 0,
141			align: 0,
142		}
143	}
144
145	fn make_constrained_field(name: &str, constraint: TypeConstraint) -> RowShapeField {
146		RowShapeField {
147			name: name.to_string(),
148			constraint,
149			offset: 0,
150			size: 0,
151			align: 0,
152		}
153	}
154
155	#[test]
156	fn test_fingerprint_deterministic() {
157		let fields1 = vec![make_field("a", Type::Int4), make_field("b", Type::Utf8)];
158
159		let fields2 = vec![make_field("a", Type::Int4), make_field("b", Type::Utf8)];
160
161		assert_eq!(compute_fingerprint(&fields1), compute_fingerprint(&fields2));
162	}
163
164	#[test]
165	fn test_fingerprint_different_names() {
166		let fields1 = vec![make_field("a", Type::Int4)];
167		let fields2 = vec![make_field("b", Type::Int4)];
168
169		assert_ne!(compute_fingerprint(&fields1), compute_fingerprint(&fields2));
170	}
171
172	#[test]
173	fn test_fingerprint_different_types() {
174		let fields1 = vec![make_field("a", Type::Int4)];
175		let fields2 = vec![make_field("a", Type::Int8)];
176
177		assert_ne!(compute_fingerprint(&fields1), compute_fingerprint(&fields2));
178	}
179
180	#[test]
181	fn test_fingerprint_different_order() {
182		let fields1 = vec![make_field("a", Type::Int4), make_field("b", Type::Utf8)];
183
184		let fields2 = vec![make_field("b", Type::Utf8), make_field("a", Type::Int4)];
185
186		assert_ne!(compute_fingerprint(&fields1), compute_fingerprint(&fields2));
187	}
188
189	#[test]
190	fn test_fingerprint_empty_shape() {
191		let fields: Vec<RowShapeField> = vec![];
192		// Should not panic and should produce a valid hash
193		let fp = compute_fingerprint(&fields);
194		assert_ne!(*fp, 0);
195	}
196
197	#[test]
198	fn test_fingerprint_utf8_constrained_vs_unconstrained() {
199		let unconstrained = vec![make_field("text", Type::Utf8)];
200		let constrained = vec![make_constrained_field(
201			"text",
202			TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(255))),
203		)];
204
205		assert_ne!(
206			compute_fingerprint(&unconstrained),
207			compute_fingerprint(&constrained),
208			"Utf8 unconstrained should differ from Utf8(255)"
209		);
210	}
211
212	#[test]
213	fn test_fingerprint_utf8_same_constraint_deterministic() {
214		let fields1 = vec![make_constrained_field(
215			"text",
216			TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(100))),
217		)];
218		let fields2 = vec![make_constrained_field(
219			"text",
220			TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(100))),
221		)];
222
223		assert_eq!(
224			compute_fingerprint(&fields1),
225			compute_fingerprint(&fields2),
226			"Utf8(100) should produce same fingerprint"
227		);
228	}
229
230	#[test]
231	fn test_fingerprint_utf8_different_max_bytes() {
232		let small = vec![make_constrained_field(
233			"text",
234			TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(50))),
235		)];
236		let large = vec![make_constrained_field(
237			"text",
238			TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(500))),
239		)];
240
241		assert_ne!(
242			compute_fingerprint(&small),
243			compute_fingerprint(&large),
244			"Utf8(50) should differ from Utf8(500)"
245		);
246	}
247
248	#[test]
249	fn test_fingerprint_int_constrained_vs_unconstrained() {
250		let unconstrained = vec![make_field("num", Type::Int)];
251		let constrained = vec![make_constrained_field(
252			"num",
253			TypeConstraint::with_constraint(Type::Int, Constraint::MaxBytes(MaxBytes::new(8))),
254		)];
255
256		assert_ne!(
257			compute_fingerprint(&unconstrained),
258			compute_fingerprint(&constrained),
259			"Int unconstrained should differ from Int(8)"
260		);
261	}
262
263	#[test]
264	fn test_fingerprint_int_same_constraint_deterministic() {
265		let fields1 = vec![make_constrained_field(
266			"num",
267			TypeConstraint::with_constraint(Type::Int, Constraint::MaxBytes(MaxBytes::new(16))),
268		)];
269		let fields2 = vec![make_constrained_field(
270			"num",
271			TypeConstraint::with_constraint(Type::Int, Constraint::MaxBytes(MaxBytes::new(16))),
272		)];
273
274		assert_eq!(
275			compute_fingerprint(&fields1),
276			compute_fingerprint(&fields2),
277			"Int(16) should produce same fingerprint"
278		);
279	}
280
281	#[test]
282	fn test_fingerprint_int_different_max_bytes() {
283		let small = vec![make_constrained_field(
284			"num",
285			TypeConstraint::with_constraint(Type::Int, Constraint::MaxBytes(MaxBytes::new(4))),
286		)];
287		let large = vec![make_constrained_field(
288			"num",
289			TypeConstraint::with_constraint(Type::Int, Constraint::MaxBytes(MaxBytes::new(32))),
290		)];
291
292		assert_ne!(
293			compute_fingerprint(&small),
294			compute_fingerprint(&large),
295			"Int(4) should differ from Int(32)"
296		);
297	}
298
299	#[test]
300	fn test_fingerprint_uint_constrained_vs_unconstrained() {
301		let unconstrained = vec![make_field("num", Type::Uint)];
302		let constrained = vec![make_constrained_field(
303			"num",
304			TypeConstraint::with_constraint(Type::Uint, Constraint::MaxBytes(MaxBytes::new(8))),
305		)];
306
307		assert_ne!(
308			compute_fingerprint(&unconstrained),
309			compute_fingerprint(&constrained),
310			"Uint unconstrained should differ from Uint(8)"
311		);
312	}
313
314	#[test]
315	fn test_fingerprint_uint_same_constraint_deterministic() {
316		let fields1 = vec![make_constrained_field(
317			"num",
318			TypeConstraint::with_constraint(Type::Uint, Constraint::MaxBytes(MaxBytes::new(64))),
319		)];
320		let fields2 = vec![make_constrained_field(
321			"num",
322			TypeConstraint::with_constraint(Type::Uint, Constraint::MaxBytes(MaxBytes::new(64))),
323		)];
324
325		assert_eq!(
326			compute_fingerprint(&fields1),
327			compute_fingerprint(&fields2),
328			"Uint(64) should produce same fingerprint"
329		);
330	}
331
332	#[test]
333	fn test_fingerprint_uint_different_max_bytes() {
334		let small = vec![make_constrained_field(
335			"num",
336			TypeConstraint::with_constraint(Type::Uint, Constraint::MaxBytes(MaxBytes::new(2))),
337		)];
338		let large = vec![make_constrained_field(
339			"num",
340			TypeConstraint::with_constraint(Type::Uint, Constraint::MaxBytes(MaxBytes::new(128))),
341		)];
342
343		assert_ne!(
344			compute_fingerprint(&small),
345			compute_fingerprint(&large),
346			"Uint(2) should differ from Uint(128)"
347		);
348	}
349
350	#[test]
351	fn test_fingerprint_blob_constrained_vs_unconstrained() {
352		let unconstrained = vec![make_field("data", Type::Blob)];
353		let constrained = vec![make_constrained_field(
354			"data",
355			TypeConstraint::with_constraint(Type::Blob, Constraint::MaxBytes(MaxBytes::new(1024))),
356		)];
357
358		assert_ne!(
359			compute_fingerprint(&unconstrained),
360			compute_fingerprint(&constrained),
361			"Blob unconstrained should differ from Blob(1024)"
362		);
363	}
364
365	#[test]
366	fn test_fingerprint_blob_same_constraint_deterministic() {
367		let fields1 = vec![make_constrained_field(
368			"data",
369			TypeConstraint::with_constraint(Type::Blob, Constraint::MaxBytes(MaxBytes::new(4096))),
370		)];
371		let fields2 = vec![make_constrained_field(
372			"data",
373			TypeConstraint::with_constraint(Type::Blob, Constraint::MaxBytes(MaxBytes::new(4096))),
374		)];
375
376		assert_eq!(
377			compute_fingerprint(&fields1),
378			compute_fingerprint(&fields2),
379			"Blob(4096) should produce same fingerprint"
380		);
381	}
382
383	#[test]
384	fn test_fingerprint_blob_different_max_bytes() {
385		let small = vec![make_constrained_field(
386			"data",
387			TypeConstraint::with_constraint(Type::Blob, Constraint::MaxBytes(MaxBytes::new(256))),
388		)];
389		let large = vec![make_constrained_field(
390			"data",
391			TypeConstraint::with_constraint(Type::Blob, Constraint::MaxBytes(MaxBytes::new(65536))),
392		)];
393
394		assert_ne!(
395			compute_fingerprint(&small),
396			compute_fingerprint(&large),
397			"Blob(256) should differ from Blob(65536)"
398		);
399	}
400
401	#[test]
402	fn test_fingerprint_decimal_constrained_vs_unconstrained() {
403		let unconstrained = vec![make_field("amount", Type::Decimal)];
404		let constrained = vec![make_constrained_field(
405			"amount",
406			TypeConstraint::with_constraint(
407				Type::Decimal,
408				Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
409			),
410		)];
411
412		assert_ne!(
413			compute_fingerprint(&unconstrained),
414			compute_fingerprint(&constrained),
415			"Decimal unconstrained should differ from Decimal(10,2)"
416		);
417	}
418
419	#[test]
420	fn test_fingerprint_decimal_same_constraint_deterministic() {
421		let fields1 = vec![make_constrained_field(
422			"amount",
423			TypeConstraint::with_constraint(
424				Type::Decimal,
425				Constraint::PrecisionScale(Precision::new(18), Scale::new(6)),
426			),
427		)];
428		let fields2 = vec![make_constrained_field(
429			"amount",
430			TypeConstraint::with_constraint(
431				Type::Decimal,
432				Constraint::PrecisionScale(Precision::new(18), Scale::new(6)),
433			),
434		)];
435
436		assert_eq!(
437			compute_fingerprint(&fields1),
438			compute_fingerprint(&fields2),
439			"Decimal(18,6) should produce same fingerprint"
440		);
441	}
442
443	#[test]
444	fn test_fingerprint_decimal_different_precision() {
445		let low_precision = vec![make_constrained_field(
446			"amount",
447			TypeConstraint::with_constraint(
448				Type::Decimal,
449				Constraint::PrecisionScale(Precision::new(5), Scale::new(2)),
450			),
451		)];
452		let high_precision = vec![make_constrained_field(
453			"amount",
454			TypeConstraint::with_constraint(
455				Type::Decimal,
456				Constraint::PrecisionScale(Precision::new(38), Scale::new(2)),
457			),
458		)];
459
460		assert_ne!(
461			compute_fingerprint(&low_precision),
462			compute_fingerprint(&high_precision),
463			"Decimal(5,2) should differ from Decimal(38,2)"
464		);
465	}
466
467	#[test]
468	fn test_fingerprint_decimal_different_scale() {
469		let low_scale = vec![make_constrained_field(
470			"amount",
471			TypeConstraint::with_constraint(
472				Type::Decimal,
473				Constraint::PrecisionScale(Precision::new(10), Scale::new(0)),
474			),
475		)];
476		let high_scale = vec![make_constrained_field(
477			"amount",
478			TypeConstraint::with_constraint(
479				Type::Decimal,
480				Constraint::PrecisionScale(Precision::new(10), Scale::new(8)),
481			),
482		)];
483
484		assert_ne!(
485			compute_fingerprint(&low_scale),
486			compute_fingerprint(&high_scale),
487			"Decimal(10,0) should differ from Decimal(10,8)"
488		);
489	}
490
491	#[test]
492	fn test_fingerprint_decimal_different_precision_and_scale() {
493		let fields1 = vec![make_constrained_field(
494			"amount",
495			TypeConstraint::with_constraint(
496				Type::Decimal,
497				Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
498			),
499		)];
500		let fields2 = vec![make_constrained_field(
501			"amount",
502			TypeConstraint::with_constraint(
503				Type::Decimal,
504				Constraint::PrecisionScale(Precision::new(15), Scale::new(4)),
505			),
506		)];
507
508		assert_ne!(
509			compute_fingerprint(&fields1),
510			compute_fingerprint(&fields2),
511			"Decimal(10,2) should differ from Decimal(15,4)"
512		);
513	}
514
515	#[test]
516	fn test_fingerprint_different_types_same_max_bytes() {
517		// Same MaxBytes value but different base types should produce different fingerprints
518		let utf8 = vec![make_constrained_field(
519			"field",
520			TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(100))),
521		)];
522		let blob = vec![make_constrained_field(
523			"field",
524			TypeConstraint::with_constraint(Type::Blob, Constraint::MaxBytes(MaxBytes::new(100))),
525		)];
526		let int = vec![make_constrained_field(
527			"field",
528			TypeConstraint::with_constraint(Type::Int, Constraint::MaxBytes(MaxBytes::new(100))),
529		)];
530		let uint = vec![make_constrained_field(
531			"field",
532			TypeConstraint::with_constraint(Type::Uint, Constraint::MaxBytes(MaxBytes::new(100))),
533		)];
534
535		let fp_utf8 = compute_fingerprint(&utf8);
536		let fp_blob = compute_fingerprint(&blob);
537		let fp_int = compute_fingerprint(&int);
538		let fp_uint = compute_fingerprint(&uint);
539
540		assert_ne!(fp_utf8, fp_blob, "Utf8(100) should differ from Blob(100)");
541		assert_ne!(fp_utf8, fp_int, "Utf8(100) should differ from Int(100)");
542		assert_ne!(fp_utf8, fp_uint, "Utf8(100) should differ from Uint(100)");
543		assert_ne!(fp_blob, fp_int, "Blob(100) should differ from Int(100)");
544		assert_ne!(fp_blob, fp_uint, "Blob(100) should differ from Uint(100)");
545		assert_ne!(fp_int, fp_uint, "Int(100) should differ from Uint(100)");
546	}
547
548	#[test]
549	fn test_fingerprint_multiple_constrained_fields() {
550		let fields1 = vec![
551			make_constrained_field(
552				"name",
553				TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(255))),
554			),
555			make_constrained_field(
556				"price",
557				TypeConstraint::with_constraint(
558					Type::Decimal,
559					Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
560				),
561			),
562			make_constrained_field(
563				"data",
564				TypeConstraint::with_constraint(Type::Blob, Constraint::MaxBytes(MaxBytes::new(1024))),
565			),
566		];
567
568		let fields2 = vec![
569			make_constrained_field(
570				"name",
571				TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(255))),
572			),
573			make_constrained_field(
574				"price",
575				TypeConstraint::with_constraint(
576					Type::Decimal,
577					Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
578				),
579			),
580			make_constrained_field(
581				"data",
582				TypeConstraint::with_constraint(Type::Blob, Constraint::MaxBytes(MaxBytes::new(1024))),
583			),
584		];
585
586		assert_eq!(
587			compute_fingerprint(&fields1),
588			compute_fingerprint(&fields2),
589			"Identical multi-field constrained shapes should produce same fingerprint"
590		);
591	}
592
593	#[test]
594	fn test_fingerprint_multiple_fields_one_constraint_differs() {
595		let fields1 = vec![
596			make_constrained_field(
597				"name",
598				TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(255))),
599			),
600			make_constrained_field(
601				"price",
602				TypeConstraint::with_constraint(
603					Type::Decimal,
604					Constraint::PrecisionScale(Precision::new(10), Scale::new(2)),
605				),
606			),
607		];
608
609		let fields2 = vec![
610			make_constrained_field(
611				"name",
612				TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(255))),
613			),
614			make_constrained_field(
615				"price",
616				TypeConstraint::with_constraint(
617					Type::Decimal,
618					Constraint::PrecisionScale(Precision::new(10), Scale::new(4)), /* Different scale */
619				),
620			),
621		];
622
623		assert_ne!(
624			compute_fingerprint(&fields1),
625			compute_fingerprint(&fields2),
626			"Shapes differing only in one constraint should have different fingerprints"
627		);
628	}
629
630	#[test]
631	fn test_fingerprint_mixed_constrained_and_unconstrained() {
632		let fields1 = vec![
633			make_field("id", Type::Int8),
634			make_constrained_field(
635				"name",
636				TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(100))),
637			),
638			make_field("active", Type::Boolean),
639		];
640
641		let fields2 = vec![
642			make_field("id", Type::Int8),
643			make_field("name", Type::Utf8), // Unconstrained
644			make_field("active", Type::Boolean),
645		];
646
647		assert_ne!(
648			compute_fingerprint(&fields1),
649			compute_fingerprint(&fields2),
650			"Mixed constrained/unconstrained should differ from all unconstrained"
651		);
652	}
653
654	#[test]
655	fn test_fingerprint_max_bytes_edge_values() {
656		let min_value = vec![make_constrained_field(
657			"data",
658			TypeConstraint::with_constraint(Type::Blob, Constraint::MaxBytes(MaxBytes::new(1))),
659		)];
660		let max_value = vec![make_constrained_field(
661			"data",
662			TypeConstraint::with_constraint(Type::Blob, Constraint::MaxBytes(MaxBytes::new(u32::MAX))),
663		)];
664
665		assert_ne!(
666			compute_fingerprint(&min_value),
667			compute_fingerprint(&max_value),
668			"Blob(1) should differ from Blob(MAX)"
669		);
670	}
671
672	#[test]
673	fn test_fingerprint_decimal_edge_precision_scale() {
674		let min_precision = vec![make_constrained_field(
675			"amount",
676			TypeConstraint::with_constraint(
677				Type::Decimal,
678				Constraint::PrecisionScale(Precision::new(1), Scale::new(0)),
679			),
680		)];
681		let max_precision = vec![make_constrained_field(
682			"amount",
683			TypeConstraint::with_constraint(
684				Type::Decimal,
685				Constraint::PrecisionScale(Precision::new(255), Scale::new(255)),
686			),
687		)];
688
689		assert_ne!(
690			compute_fingerprint(&min_precision),
691			compute_fingerprint(&max_precision),
692			"Decimal(1,0) should differ from Decimal(255,255)"
693		);
694	}
695
696	#[test]
697	fn test_fingerprint_adjacent_max_bytes_values() {
698		// Test that even adjacent values produce different fingerprints
699		let value_99 = vec![make_constrained_field(
700			"text",
701			TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(99))),
702		)];
703		let value_100 = vec![make_constrained_field(
704			"text",
705			TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(100))),
706		)];
707		let value_101 = vec![make_constrained_field(
708			"text",
709			TypeConstraint::with_constraint(Type::Utf8, Constraint::MaxBytes(MaxBytes::new(101))),
710		)];
711
712		let fp_99 = compute_fingerprint(&value_99);
713		let fp_100 = compute_fingerprint(&value_100);
714		let fp_101 = compute_fingerprint(&value_101);
715
716		assert_ne!(fp_99, fp_100, "Utf8(99) should differ from Utf8(100)");
717		assert_ne!(fp_100, fp_101, "Utf8(100) should differ from Utf8(101)");
718		assert_ne!(fp_99, fp_101, "Utf8(99) should differ from Utf8(101)");
719	}
720}