Skip to main content

reifydb_codec/encoded/shape/
fingerprint.rs

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