1use crate::db::{
7 codec::cursor::encode_cursor,
8 query::plan::AccessPlannedQuery,
9 query::{
10 explain::ExplainPlan,
11 fingerprint::{finalize_sha256_digest, hash_parts, new_plan_fingerprint_hasher_v2},
12 },
13};
14
15#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
22pub struct PlanFingerprint([u8; 32]);
23
24impl PlanFingerprint {
25 #[must_use]
26 pub fn as_hex(&self) -> String {
27 encode_cursor(&self.0)
28 }
29}
30
31impl std::fmt::Display for PlanFingerprint {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 f.write_str(&self.as_hex())
34 }
35}
36
37impl AccessPlannedQuery {
38 #[must_use]
40 pub(in crate::db) fn fingerprint(&self) -> PlanFingerprint {
41 let projection = self.projection_spec_for_identity();
42 let mut hasher = new_plan_fingerprint_hasher_v2();
43 hash_parts::hash_planned_query_profile_with_projection(
44 &mut hasher,
45 self,
46 hash_parts::ExplainHashProfile::FingerprintV2,
47 &projection,
48 );
49
50 PlanFingerprint(finalize_sha256_digest(hasher))
51 }
52}
53
54impl ExplainPlan {
55 #[must_use]
57 pub fn fingerprint(&self) -> PlanFingerprint {
58 let mut hasher = new_plan_fingerprint_hasher_v2();
60 hash_parts::hash_explain_plan_profile(
61 &mut hasher,
62 self,
63 hash_parts::ExplainHashProfile::FingerprintV2,
64 );
65
66 PlanFingerprint(finalize_sha256_digest(hasher))
68 }
69}
70
71#[cfg(test)]
76mod tests {
77 use std::ops::Bound;
78
79 use crate::db::access::AccessPath;
80 use crate::db::predicate::{
81 CoercionId, CompareOp, ComparePredicate, MissingRowPolicy, Predicate,
82 };
83 use crate::db::query::explain::{ExplainGroupedStrategy, ExplainGrouping};
84 use crate::db::query::fingerprint::hash_parts;
85 use crate::db::query::intent::{KeyAccess, build_access_plan_from_keys};
86 use crate::db::query::plan::expr::{
87 Alias, BinaryOp, Expr, FieldId, ProjectionField, ProjectionSpec,
88 };
89 use crate::db::query::plan::{
90 AccessPlannedQuery, AggregateKind, DeleteLimitSpec, DeleteSpec, FieldSlot,
91 GroupAggregateSpec, GroupSpec, GroupedExecutionConfig, LoadSpec, LogicalPlan, PageSpec,
92 QueryMode,
93 };
94 use crate::db::query::{builder::field::FieldRef, builder::sum};
95 use crate::model::index::IndexModel;
96 use crate::types::{Decimal, Ulid};
97 use crate::value::Value;
98
99 fn fingerprint_with_projection(
100 plan: &AccessPlannedQuery,
101 projection: &ProjectionSpec,
102 ) -> super::PlanFingerprint {
103 let explain = plan.explain();
104 let mut hasher = super::super::new_plan_fingerprint_hasher_v2();
105 hash_parts::hash_explain_plan_profile_with_projection(
106 &mut hasher,
107 &explain,
108 hash_parts::ExplainHashProfile::FingerprintV2,
109 projection,
110 );
111
112 super::PlanFingerprint(super::super::finalize_sha256_digest(hasher))
113 }
114
115 fn full_scan_query() -> AccessPlannedQuery {
116 AccessPlannedQuery::new(AccessPath::<Value>::FullScan, MissingRowPolicy::Ignore)
117 }
118
119 fn index_prefix_query(index: IndexModel, values: Vec<Value>) -> AccessPlannedQuery {
120 AccessPlannedQuery::new(
121 AccessPath::IndexPrefix { index, values },
122 MissingRowPolicy::Ignore,
123 )
124 }
125
126 fn index_range_query(
127 index: IndexModel,
128 prefix: Vec<Value>,
129 lower: Bound<Value>,
130 upper: Bound<Value>,
131 ) -> AccessPlannedQuery {
132 AccessPlannedQuery::new(
133 AccessPath::index_range(index, prefix, lower, upper),
134 MissingRowPolicy::Ignore,
135 )
136 }
137
138 fn grouped_explain_with_fixed_shape() -> crate::db::query::explain::ExplainPlan {
139 AccessPlannedQuery::new(AccessPath::<Value>::FullScan, MissingRowPolicy::Ignore)
140 .into_grouped(GroupSpec {
141 group_fields: vec![FieldSlot::from_parts_for_test(1, "rank")],
142 aggregates: vec![GroupAggregateSpec {
143 kind: AggregateKind::Count,
144 target_field: None,
145 distinct: false,
146 }],
147 execution: GroupedExecutionConfig::with_hard_limits(64, 4096),
148 })
149 .explain()
150 }
151
152 #[test]
153 fn fingerprint_is_deterministic_for_equivalent_predicates() {
154 let id = Ulid::default();
155
156 let predicate_a = Predicate::And(vec![
157 FieldRef::new("id").eq(id),
158 FieldRef::new("other").eq(Value::Text("x".to_string())),
159 ]);
160 let predicate_b = Predicate::And(vec![
161 FieldRef::new("other").eq(Value::Text("x".to_string())),
162 FieldRef::new("id").eq(id),
163 ]);
164
165 let mut plan_a: AccessPlannedQuery = full_scan_query();
166 plan_a.scalar_plan_mut().predicate = Some(predicate_a);
167
168 let mut plan_b: AccessPlannedQuery = full_scan_query();
169 plan_b.scalar_plan_mut().predicate = Some(predicate_b);
170
171 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
172 }
173
174 #[test]
175 fn fingerprint_and_signature_are_stable_for_reordered_and_non_canonical_map_predicates() {
176 let map_a = Value::Map(vec![
177 (Value::Text("z".to_string()), Value::Int(9)),
178 (Value::Text("a".to_string()), Value::Int(1)),
179 ]);
180 let map_b = Value::Map(vec![
181 (Value::Text("a".to_string()), Value::Int(1)),
182 (Value::Text("z".to_string()), Value::Int(9)),
183 ]);
184
185 let predicate_a = Predicate::And(vec![
186 FieldRef::new("other").eq(Value::Text("x".to_string())),
187 Predicate::Compare(ComparePredicate::eq("meta".to_string(), map_a)),
188 ]);
189 let predicate_b = Predicate::And(vec![
190 Predicate::Compare(ComparePredicate::eq("meta".to_string(), map_b)),
191 FieldRef::new("other").eq(Value::Text("x".to_string())),
192 ]);
193
194 let mut plan_a: AccessPlannedQuery = full_scan_query();
195 plan_a.scalar_plan_mut().predicate = Some(predicate_a);
196
197 let mut plan_b: AccessPlannedQuery = full_scan_query();
198 plan_b.scalar_plan_mut().predicate = Some(predicate_b);
199
200 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
201 assert_eq!(
202 plan_a.continuation_signature("tests::Entity"),
203 plan_b.continuation_signature("tests::Entity")
204 );
205 }
206
207 #[test]
208 fn fingerprint_and_signature_treat_equivalent_decimal_predicate_literals_as_identical() {
209 let predicate_a = Predicate::Compare(ComparePredicate::eq(
210 "rank".to_string(),
211 Value::Decimal(Decimal::new(10, 1)),
212 ));
213 let predicate_b = Predicate::Compare(ComparePredicate::eq(
214 "rank".to_string(),
215 Value::Decimal(Decimal::new(100, 2)),
216 ));
217
218 let mut plan_a: AccessPlannedQuery = full_scan_query();
219 plan_a.scalar_plan_mut().predicate = Some(predicate_a);
220
221 let mut plan_b: AccessPlannedQuery = full_scan_query();
222 plan_b.scalar_plan_mut().predicate = Some(predicate_b);
223
224 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
225 assert_eq!(
226 plan_a.continuation_signature("tests::Entity"),
227 plan_b.continuation_signature("tests::Entity")
228 );
229 }
230
231 #[test]
232 fn fingerprint_and_signature_treat_equivalent_in_list_predicates_as_identical() {
233 let predicate_a = Predicate::Compare(ComparePredicate::in_(
234 "rank".to_string(),
235 vec![Value::Uint(3), Value::Uint(1), Value::Uint(2)],
236 ));
237 let predicate_b = Predicate::Compare(ComparePredicate::in_(
238 "rank".to_string(),
239 vec![Value::Uint(1), Value::Uint(2), Value::Uint(3)],
240 ));
241
242 let mut plan_a: AccessPlannedQuery = full_scan_query();
243 plan_a.scalar_plan_mut().predicate = Some(predicate_a);
244
245 let mut plan_b: AccessPlannedQuery = full_scan_query();
246 plan_b.scalar_plan_mut().predicate = Some(predicate_b);
247
248 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
249 assert_eq!(
250 plan_a.continuation_signature("tests::Entity"),
251 plan_b.continuation_signature("tests::Entity")
252 );
253 }
254
255 #[test]
256 fn fingerprint_and_signature_treat_same_field_or_eq_and_in_as_identical() {
257 let predicate_or_eq = Predicate::Or(vec![
258 Predicate::Compare(ComparePredicate::with_coercion(
259 "rank",
260 CompareOp::Eq,
261 Value::Uint(3),
262 CoercionId::Strict,
263 )),
264 Predicate::Compare(ComparePredicate::with_coercion(
265 "rank",
266 CompareOp::Eq,
267 Value::Uint(1),
268 CoercionId::Strict,
269 )),
270 Predicate::Compare(ComparePredicate::with_coercion(
271 "rank",
272 CompareOp::Eq,
273 Value::Uint(3),
274 CoercionId::Strict,
275 )),
276 ]);
277 let predicate_in = Predicate::Compare(ComparePredicate::with_coercion(
278 "rank",
279 CompareOp::In,
280 Value::List(vec![Value::Uint(1), Value::Uint(3)]),
281 CoercionId::Strict,
282 ));
283
284 let mut plan_or_eq: AccessPlannedQuery = full_scan_query();
285 plan_or_eq.scalar_plan_mut().predicate = Some(predicate_or_eq);
286
287 let mut plan_in: AccessPlannedQuery = full_scan_query();
288 plan_in.scalar_plan_mut().predicate = Some(predicate_in);
289
290 assert_eq!(plan_or_eq.fingerprint(), plan_in.fingerprint());
291 assert_eq!(
292 plan_or_eq.continuation_signature("tests::Entity"),
293 plan_in.continuation_signature("tests::Entity")
294 );
295 }
296
297 #[test]
298 fn fingerprint_and_signature_treat_equivalent_in_list_duplicate_literals_as_identical() {
299 let predicate_a = Predicate::Compare(ComparePredicate::in_(
300 "rank".to_string(),
301 vec![
302 Value::Uint(3),
303 Value::Uint(1),
304 Value::Uint(3),
305 Value::Uint(2),
306 ],
307 ));
308 let predicate_b = Predicate::Compare(ComparePredicate::in_(
309 "rank".to_string(),
310 vec![Value::Uint(1), Value::Uint(2), Value::Uint(3)],
311 ));
312
313 let mut plan_a: AccessPlannedQuery = full_scan_query();
314 plan_a.scalar_plan_mut().predicate = Some(predicate_a);
315
316 let mut plan_b: AccessPlannedQuery = full_scan_query();
317 plan_b.scalar_plan_mut().predicate = Some(predicate_b);
318
319 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
320 assert_eq!(
321 plan_a.continuation_signature("tests::Entity"),
322 plan_b.continuation_signature("tests::Entity")
323 );
324 }
325
326 #[test]
327 fn fingerprint_and_signature_treat_implicit_and_explicit_strict_coercion_as_identical() {
328 let predicate_a =
329 Predicate::Compare(ComparePredicate::eq("rank".to_string(), Value::Int(7)));
330 let predicate_b = Predicate::Compare(ComparePredicate::with_coercion(
331 "rank",
332 CompareOp::Eq,
333 Value::Int(7),
334 CoercionId::Strict,
335 ));
336
337 let mut plan_a: AccessPlannedQuery = full_scan_query();
338 plan_a.scalar_plan_mut().predicate = Some(predicate_a);
339
340 let mut plan_b: AccessPlannedQuery = full_scan_query();
341 plan_b.scalar_plan_mut().predicate = Some(predicate_b);
342
343 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
344 assert_eq!(
345 plan_a.continuation_signature("tests::Entity"),
346 plan_b.continuation_signature("tests::Entity")
347 );
348 }
349
350 #[test]
351 fn fingerprint_and_signature_distinguish_different_coercion_ids() {
352 let predicate_strict = Predicate::Compare(ComparePredicate::with_coercion(
353 "rank",
354 CompareOp::Eq,
355 Value::Int(7),
356 CoercionId::Strict,
357 ));
358 let predicate_numeric_widen = Predicate::Compare(ComparePredicate::with_coercion(
359 "rank",
360 CompareOp::Eq,
361 Value::Int(7),
362 CoercionId::NumericWiden,
363 ));
364
365 let mut strict_plan: AccessPlannedQuery = full_scan_query();
366 strict_plan.scalar_plan_mut().predicate = Some(predicate_strict);
367
368 let mut numeric_widen_plan: AccessPlannedQuery = full_scan_query();
369 numeric_widen_plan.scalar_plan_mut().predicate = Some(predicate_numeric_widen);
370
371 assert_ne!(strict_plan.fingerprint(), numeric_widen_plan.fingerprint());
372 assert_ne!(
373 strict_plan.continuation_signature("tests::Entity"),
374 numeric_widen_plan.continuation_signature("tests::Entity")
375 );
376 }
377
378 #[test]
379 fn fingerprint_and_signature_treat_numeric_widen_equivalent_literal_subtypes_as_identical() {
380 let predicate_int = Predicate::Compare(ComparePredicate::with_coercion(
381 "rank",
382 CompareOp::Eq,
383 Value::Int(1),
384 CoercionId::NumericWiden,
385 ));
386 let predicate_decimal = Predicate::Compare(ComparePredicate::with_coercion(
387 "rank",
388 CompareOp::Eq,
389 Value::Decimal(Decimal::new(10, 1)),
390 CoercionId::NumericWiden,
391 ));
392
393 let mut int_plan: AccessPlannedQuery = full_scan_query();
394 int_plan.scalar_plan_mut().predicate = Some(predicate_int);
395
396 let mut decimal_plan: AccessPlannedQuery = full_scan_query();
397 decimal_plan.scalar_plan_mut().predicate = Some(predicate_decimal);
398
399 assert_eq!(int_plan.fingerprint(), decimal_plan.fingerprint());
400 assert_eq!(
401 int_plan.continuation_signature("tests::Entity"),
402 decimal_plan.continuation_signature("tests::Entity")
403 );
404 }
405
406 #[test]
407 fn fingerprint_and_signature_treat_text_casefold_case_only_literals_as_identical() {
408 let predicate_lower = Predicate::Compare(ComparePredicate::with_coercion(
409 "name",
410 CompareOp::Eq,
411 Value::Text("ada".to_string()),
412 CoercionId::TextCasefold,
413 ));
414 let predicate_upper = Predicate::Compare(ComparePredicate::with_coercion(
415 "name",
416 CompareOp::Eq,
417 Value::Text("ADA".to_string()),
418 CoercionId::TextCasefold,
419 ));
420
421 let mut lower_plan: AccessPlannedQuery = full_scan_query();
422 lower_plan.scalar_plan_mut().predicate = Some(predicate_lower);
423
424 let mut upper_plan: AccessPlannedQuery = full_scan_query();
425 upper_plan.scalar_plan_mut().predicate = Some(predicate_upper);
426
427 assert_eq!(lower_plan.fingerprint(), upper_plan.fingerprint());
428 assert_eq!(
429 lower_plan.continuation_signature("tests::Entity"),
430 upper_plan.continuation_signature("tests::Entity")
431 );
432 }
433
434 #[test]
435 fn fingerprint_and_signature_keep_strict_text_case_variants_distinct() {
436 let predicate_lower = Predicate::Compare(ComparePredicate::with_coercion(
437 "name",
438 CompareOp::Eq,
439 Value::Text("ada".to_string()),
440 CoercionId::Strict,
441 ));
442 let predicate_upper = Predicate::Compare(ComparePredicate::with_coercion(
443 "name",
444 CompareOp::Eq,
445 Value::Text("ADA".to_string()),
446 CoercionId::Strict,
447 ));
448
449 let mut lower_plan: AccessPlannedQuery = full_scan_query();
450 lower_plan.scalar_plan_mut().predicate = Some(predicate_lower);
451
452 let mut upper_plan: AccessPlannedQuery = full_scan_query();
453 upper_plan.scalar_plan_mut().predicate = Some(predicate_upper);
454
455 assert_ne!(lower_plan.fingerprint(), upper_plan.fingerprint());
456 assert_ne!(
457 lower_plan.continuation_signature("tests::Entity"),
458 upper_plan.continuation_signature("tests::Entity")
459 );
460 }
461
462 #[test]
463 fn fingerprint_and_signature_treat_text_casefold_in_list_case_variants_as_identical() {
464 let predicate_mixed = Predicate::Compare(ComparePredicate::with_coercion(
465 "name",
466 CompareOp::In,
467 Value::List(vec![
468 Value::Text("ADA".to_string()),
469 Value::Text("ada".to_string()),
470 Value::Text("Bob".to_string()),
471 ]),
472 CoercionId::TextCasefold,
473 ));
474 let predicate_canonical = Predicate::Compare(ComparePredicate::with_coercion(
475 "name",
476 CompareOp::In,
477 Value::List(vec![
478 Value::Text("ada".to_string()),
479 Value::Text("bob".to_string()),
480 ]),
481 CoercionId::TextCasefold,
482 ));
483
484 let mut mixed_plan: AccessPlannedQuery = full_scan_query();
485 mixed_plan.scalar_plan_mut().predicate = Some(predicate_mixed);
486
487 let mut canonical_plan: AccessPlannedQuery = full_scan_query();
488 canonical_plan.scalar_plan_mut().predicate = Some(predicate_canonical);
489
490 assert_eq!(mixed_plan.fingerprint(), canonical_plan.fingerprint());
491 assert_eq!(
492 mixed_plan.continuation_signature("tests::Entity"),
493 canonical_plan.continuation_signature("tests::Entity")
494 );
495 }
496
497 #[test]
498 fn fingerprint_and_signature_distinguish_strict_from_text_casefold_coercion() {
499 let predicate_strict = Predicate::Compare(ComparePredicate::with_coercion(
500 "name",
501 CompareOp::Eq,
502 Value::Text("ada".to_string()),
503 CoercionId::Strict,
504 ));
505 let predicate_casefold = Predicate::Compare(ComparePredicate::with_coercion(
506 "name",
507 CompareOp::Eq,
508 Value::Text("ada".to_string()),
509 CoercionId::TextCasefold,
510 ));
511
512 let mut strict_plan: AccessPlannedQuery = full_scan_query();
513 strict_plan.scalar_plan_mut().predicate = Some(predicate_strict);
514
515 let mut casefold_plan: AccessPlannedQuery = full_scan_query();
516 casefold_plan.scalar_plan_mut().predicate = Some(predicate_casefold);
517
518 assert_ne!(strict_plan.fingerprint(), casefold_plan.fingerprint());
519 assert_ne!(
520 strict_plan.continuation_signature("tests::Entity"),
521 casefold_plan.continuation_signature("tests::Entity")
522 );
523 }
524
525 #[test]
526 fn fingerprint_and_signature_distinguish_strict_from_collection_element_coercion() {
527 let predicate_strict = Predicate::Compare(ComparePredicate::with_coercion(
528 "rank",
529 CompareOp::Eq,
530 Value::Int(7),
531 CoercionId::Strict,
532 ));
533 let predicate_collection_element = Predicate::Compare(ComparePredicate::with_coercion(
534 "rank",
535 CompareOp::Eq,
536 Value::Int(7),
537 CoercionId::CollectionElement,
538 ));
539
540 let mut strict_plan: AccessPlannedQuery = full_scan_query();
541 strict_plan.scalar_plan_mut().predicate = Some(predicate_strict);
542
543 let mut collection_plan: AccessPlannedQuery = full_scan_query();
544 collection_plan.scalar_plan_mut().predicate = Some(predicate_collection_element);
545
546 assert_ne!(strict_plan.fingerprint(), collection_plan.fingerprint());
547 assert_ne!(
548 strict_plan.continuation_signature("tests::Entity"),
549 collection_plan.continuation_signature("tests::Entity")
550 );
551 }
552
553 #[test]
554 fn fingerprint_is_deterministic_for_by_keys() {
555 let a = Ulid::from_u128(1);
556 let b = Ulid::from_u128(2);
557
558 let access_a = build_access_plan_from_keys(&KeyAccess::Many(vec![a, b, a]));
559 let access_b = build_access_plan_from_keys(&KeyAccess::Many(vec![b, a]));
560
561 let plan_a: AccessPlannedQuery = AccessPlannedQuery {
562 logical: LogicalPlan::Scalar(crate::db::query::plan::ScalarPlan {
563 mode: QueryMode::Load(LoadSpec::new()),
564 predicate: None,
565 order: None,
566 distinct: false,
567 delete_limit: None,
568 page: None,
569 consistency: MissingRowPolicy::Ignore,
570 }),
571 access: access_a,
572 projection_selection: crate::db::query::plan::expr::ProjectionSelection::All,
573 };
574 let plan_b: AccessPlannedQuery = AccessPlannedQuery {
575 logical: LogicalPlan::Scalar(crate::db::query::plan::ScalarPlan {
576 mode: QueryMode::Load(LoadSpec::new()),
577 predicate: None,
578 order: None,
579 distinct: false,
580 delete_limit: None,
581 page: None,
582 consistency: MissingRowPolicy::Ignore,
583 }),
584 access: access_b,
585 projection_selection: crate::db::query::plan::expr::ProjectionSelection::All,
586 };
587
588 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
589 }
590
591 #[test]
592 fn fingerprint_changes_with_index_choice() {
593 const INDEX_FIELDS: [&str; 1] = ["idx_a"];
594 const INDEX_A: IndexModel = IndexModel::new(
595 "fingerprint::idx_a",
596 "fingerprint::store",
597 &INDEX_FIELDS,
598 false,
599 );
600 const INDEX_B: IndexModel = IndexModel::new(
601 "fingerprint::idx_b",
602 "fingerprint::store",
603 &INDEX_FIELDS,
604 false,
605 );
606
607 let plan_a: AccessPlannedQuery =
608 index_prefix_query(INDEX_A, vec![Value::Text("alpha".to_string())]);
609 let plan_b: AccessPlannedQuery =
610 index_prefix_query(INDEX_B, vec![Value::Text("alpha".to_string())]);
611
612 assert_ne!(plan_a.fingerprint(), plan_b.fingerprint());
613 }
614
615 #[test]
616 fn fingerprint_changes_with_pagination() {
617 let mut plan_a: AccessPlannedQuery = full_scan_query();
618 let mut plan_b: AccessPlannedQuery = full_scan_query();
619 plan_a.scalar_plan_mut().page = Some(PageSpec {
620 limit: Some(10),
621 offset: 0,
622 });
623 plan_b.scalar_plan_mut().page = Some(PageSpec {
624 limit: Some(10),
625 offset: 1,
626 });
627
628 assert_ne!(plan_a.fingerprint(), plan_b.fingerprint());
629 }
630
631 #[test]
632 fn fingerprint_changes_with_delete_limit() {
633 let mut plan_a: AccessPlannedQuery = full_scan_query();
634 let mut plan_b: AccessPlannedQuery = full_scan_query();
635 plan_a.scalar_plan_mut().mode = QueryMode::Delete(DeleteSpec::new());
636 plan_b.scalar_plan_mut().mode = QueryMode::Delete(DeleteSpec::new());
637 plan_a.scalar_plan_mut().delete_limit = Some(DeleteLimitSpec { max_rows: 2 });
638 plan_b.scalar_plan_mut().delete_limit = Some(DeleteLimitSpec { max_rows: 3 });
639
640 assert_ne!(plan_a.fingerprint(), plan_b.fingerprint());
641 }
642
643 #[test]
644 fn fingerprint_changes_with_distinct_flag() {
645 let plan_a: AccessPlannedQuery = full_scan_query();
646 let mut plan_b: AccessPlannedQuery = full_scan_query();
647 plan_b.scalar_plan_mut().distinct = true;
648
649 assert_ne!(plan_a.fingerprint(), plan_b.fingerprint());
650 }
651
652 #[test]
653 fn fingerprint_numeric_projection_alias_only_change_does_not_invalidate() {
654 let plan: AccessPlannedQuery = full_scan_query();
655 let numeric_projection =
656 ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
657 expr: Expr::Binary {
658 op: crate::db::query::plan::expr::BinaryOp::Add,
659 left: Box::new(Expr::Field(FieldId::new("rank"))),
660 right: Box::new(Expr::Literal(Value::Int(1))),
661 },
662 alias: None,
663 }]);
664 let alias_only_numeric_projection =
665 ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
666 expr: Expr::Alias {
667 expr: Box::new(Expr::Binary {
668 op: crate::db::query::plan::expr::BinaryOp::Add,
669 left: Box::new(Expr::Field(FieldId::new("rank"))),
670 right: Box::new(Expr::Literal(Value::Int(1))),
671 }),
672 name: Alias::new("rank_plus_one_expr"),
673 },
674 alias: Some(Alias::new("rank_plus_one")),
675 }]);
676
677 let semantic_fingerprint = fingerprint_with_projection(&plan, &numeric_projection);
678 let alias_fingerprint = fingerprint_with_projection(&plan, &alias_only_numeric_projection);
679
680 assert_eq!(
681 semantic_fingerprint, alias_fingerprint,
682 "numeric projection alias wrappers must not affect fingerprint identity",
683 );
684 }
685
686 #[test]
687 fn fingerprint_numeric_projection_semantic_change_invalidates() {
688 let plan: AccessPlannedQuery = full_scan_query();
689 let projection_add_one =
690 ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
691 expr: Expr::Binary {
692 op: crate::db::query::plan::expr::BinaryOp::Add,
693 left: Box::new(Expr::Field(FieldId::new("rank"))),
694 right: Box::new(Expr::Literal(Value::Int(1))),
695 },
696 alias: None,
697 }]);
698 let projection_mul_one =
699 ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
700 expr: Expr::Binary {
701 op: crate::db::query::plan::expr::BinaryOp::Mul,
702 left: Box::new(Expr::Field(FieldId::new("rank"))),
703 right: Box::new(Expr::Literal(Value::Int(1))),
704 },
705 alias: None,
706 }]);
707
708 let add_fingerprint = fingerprint_with_projection(&plan, &projection_add_one);
709 let mul_fingerprint = fingerprint_with_projection(&plan, &projection_mul_one);
710
711 assert_ne!(
712 add_fingerprint, mul_fingerprint,
713 "numeric projection semantic changes must invalidate fingerprint identity",
714 );
715 }
716
717 #[test]
718 fn fingerprint_numeric_literal_decimal_scale_is_canonicalized() {
719 let plan: AccessPlannedQuery = full_scan_query();
720 let decimal_one_scale_1 =
721 ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
722 expr: Expr::Literal(Value::Decimal(Decimal::new(10, 1))),
723 alias: None,
724 }]);
725 let decimal_one_scale_2 =
726 ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
727 expr: Expr::Literal(Value::Decimal(Decimal::new(100, 2))),
728 alias: None,
729 }]);
730
731 assert_eq!(
732 fingerprint_with_projection(&plan, &decimal_one_scale_1),
733 fingerprint_with_projection(&plan, &decimal_one_scale_2),
734 "decimal scale-only literal changes must not fragment fingerprint identity",
735 );
736 }
737
738 #[test]
739 fn fingerprint_literal_numeric_subtype_remains_significant_when_observable() {
740 let plan: AccessPlannedQuery = full_scan_query();
741 let int_literal = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
742 expr: Expr::Literal(Value::Int(1)),
743 alias: None,
744 }]);
745 let decimal_literal = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
746 expr: Expr::Literal(Value::Decimal(Decimal::new(10, 1))),
747 alias: None,
748 }]);
749
750 assert_ne!(
751 fingerprint_with_projection(&plan, &int_literal),
752 fingerprint_with_projection(&plan, &decimal_literal),
753 "top-level literal subtype remains observable and identity-significant",
754 );
755 }
756
757 #[test]
758 fn fingerprint_numeric_promotion_paths_do_not_fragment() {
759 let plan: AccessPlannedQuery = full_scan_query();
760 let int_plus_int = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
761 expr: Expr::Binary {
762 op: BinaryOp::Add,
763 left: Box::new(Expr::Literal(Value::Int(1))),
764 right: Box::new(Expr::Literal(Value::Int(2))),
765 },
766 alias: None,
767 }]);
768 let int_plus_decimal =
769 ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
770 expr: Expr::Binary {
771 op: BinaryOp::Add,
772 left: Box::new(Expr::Literal(Value::Int(1))),
773 right: Box::new(Expr::Literal(Value::Decimal(Decimal::new(20, 1)))),
774 },
775 alias: None,
776 }]);
777 let decimal_plus_int =
778 ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
779 expr: Expr::Binary {
780 op: BinaryOp::Add,
781 left: Box::new(Expr::Literal(Value::Decimal(Decimal::new(10, 1)))),
782 right: Box::new(Expr::Literal(Value::Int(2))),
783 },
784 alias: None,
785 }]);
786
787 let fingerprint_int_plus_int = fingerprint_with_projection(&plan, &int_plus_int);
788 let fingerprint_int_plus_decimal = fingerprint_with_projection(&plan, &int_plus_decimal);
789 let fingerprint_decimal_plus_int = fingerprint_with_projection(&plan, &decimal_plus_int);
790
791 assert_eq!(fingerprint_int_plus_int, fingerprint_int_plus_decimal);
792 assert_eq!(fingerprint_int_plus_int, fingerprint_decimal_plus_int);
793 }
794
795 #[test]
796 fn fingerprint_commutative_operand_order_remains_significant_without_ast_normalization() {
797 let plan: AccessPlannedQuery = full_scan_query();
798 let rank_plus_score = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
799 expr: Expr::Binary {
800 op: BinaryOp::Add,
801 left: Box::new(Expr::Field(FieldId::new("rank"))),
802 right: Box::new(Expr::Field(FieldId::new("score"))),
803 },
804 alias: None,
805 }]);
806 let score_plus_rank = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
807 expr: Expr::Binary {
808 op: BinaryOp::Add,
809 left: Box::new(Expr::Field(FieldId::new("score"))),
810 right: Box::new(Expr::Field(FieldId::new("rank"))),
811 },
812 alias: None,
813 }]);
814
815 assert_ne!(
816 fingerprint_with_projection(&plan, &rank_plus_score),
817 fingerprint_with_projection(&plan, &score_plus_rank),
818 "fingerprint preserves AST operand order for commutative operators in v2",
819 );
820 }
821
822 #[test]
823 fn fingerprint_aggregate_numeric_target_field_remains_significant() {
824 let plan: AccessPlannedQuery = full_scan_query();
825 let sum_rank = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
826 expr: Expr::Aggregate(sum("rank")),
827 alias: None,
828 }]);
829 let sum_score = ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
830 expr: Expr::Aggregate(sum("score")),
831 alias: None,
832 }]);
833
834 assert_ne!(
835 fingerprint_with_projection(&plan, &sum_rank),
836 fingerprint_with_projection(&plan, &sum_score),
837 "aggregate target field changes must invalidate fingerprint identity",
838 );
839 }
840
841 #[test]
842 fn fingerprint_distinct_numeric_noop_paths_stay_stable() {
843 let plan: AccessPlannedQuery = full_scan_query();
844 let sum_distinct_plus_int_zero =
845 ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
846 expr: Expr::Binary {
847 op: BinaryOp::Add,
848 left: Box::new(Expr::Aggregate(sum("rank").distinct())),
849 right: Box::new(Expr::Literal(Value::Int(0))),
850 },
851 alias: None,
852 }]);
853 let sum_distinct_plus_decimal_zero =
854 ProjectionSpec::from_fields_for_test(vec![ProjectionField::Scalar {
855 expr: Expr::Binary {
856 op: BinaryOp::Add,
857 left: Box::new(Expr::Aggregate(sum("rank").distinct())),
858 right: Box::new(Expr::Literal(Value::Decimal(Decimal::new(0, 1)))),
859 },
860 alias: None,
861 }]);
862
863 assert_eq!(
864 fingerprint_with_projection(&plan, &sum_distinct_plus_int_zero),
865 fingerprint_with_projection(&plan, &sum_distinct_plus_decimal_zero),
866 "distinct numeric no-op literal subtype differences must not fragment fingerprint identity",
867 );
868 }
869
870 #[test]
871 fn fingerprint_is_stable_for_full_scan() {
872 let plan: AccessPlannedQuery = full_scan_query();
873 let fingerprint_a = plan.fingerprint();
874 let fingerprint_b = plan.fingerprint();
875 assert_eq!(fingerprint_a, fingerprint_b);
876 }
877
878 #[test]
879 fn fingerprint_is_stable_for_equivalent_index_range_bounds() {
880 const INDEX_FIELDS: [&str; 2] = ["group", "rank"];
881 const INDEX: IndexModel = IndexModel::new(
882 "fingerprint::group_rank",
883 "fingerprint::store",
884 &INDEX_FIELDS,
885 false,
886 );
887
888 let plan_a: AccessPlannedQuery = index_range_query(
889 INDEX,
890 vec![Value::Uint(7)],
891 Bound::Included(Value::Uint(100)),
892 Bound::Excluded(Value::Uint(200)),
893 );
894 let plan_b: AccessPlannedQuery = index_range_query(
895 INDEX,
896 vec![Value::Uint(7)],
897 Bound::Included(Value::Uint(100)),
898 Bound::Excluded(Value::Uint(200)),
899 );
900
901 assert_eq!(plan_a.fingerprint(), plan_b.fingerprint());
902 }
903
904 #[test]
905 fn fingerprint_changes_when_index_range_bound_discriminant_changes() {
906 const INDEX_FIELDS: [&str; 2] = ["group", "rank"];
907 const INDEX: IndexModel = IndexModel::new(
908 "fingerprint::group_rank",
909 "fingerprint::store",
910 &INDEX_FIELDS,
911 false,
912 );
913
914 let plan_included: AccessPlannedQuery = index_range_query(
915 INDEX,
916 vec![Value::Uint(7)],
917 Bound::Included(Value::Uint(100)),
918 Bound::Excluded(Value::Uint(200)),
919 );
920 let plan_excluded: AccessPlannedQuery = index_range_query(
921 INDEX,
922 vec![Value::Uint(7)],
923 Bound::Excluded(Value::Uint(100)),
924 Bound::Excluded(Value::Uint(200)),
925 );
926
927 assert_ne!(plan_included.fingerprint(), plan_excluded.fingerprint());
928 }
929
930 #[test]
931 fn fingerprint_changes_when_index_range_bound_value_changes() {
932 const INDEX_FIELDS: [&str; 2] = ["group", "rank"];
933 const INDEX: IndexModel = IndexModel::new(
934 "fingerprint::group_rank",
935 "fingerprint::store",
936 &INDEX_FIELDS,
937 false,
938 );
939
940 let plan_low_100: AccessPlannedQuery = index_range_query(
941 INDEX,
942 vec![Value::Uint(7)],
943 Bound::Included(Value::Uint(100)),
944 Bound::Excluded(Value::Uint(200)),
945 );
946 let plan_low_101: AccessPlannedQuery = index_range_query(
947 INDEX,
948 vec![Value::Uint(7)],
949 Bound::Included(Value::Uint(101)),
950 Bound::Excluded(Value::Uint(200)),
951 );
952
953 assert_ne!(plan_low_100.fingerprint(), plan_low_101.fingerprint());
954 }
955
956 #[test]
957 fn explain_fingerprint_grouped_strategy_only_change_does_not_invalidate() {
958 let mut hash_strategy = grouped_explain_with_fixed_shape();
959 let mut ordered_strategy = hash_strategy.clone();
960
961 let ExplainGrouping::Grouped {
962 strategy: hash_value,
963 ..
964 } = &mut hash_strategy.grouping
965 else {
966 panic!("grouped explain fixture must produce grouped explain shape");
967 };
968 *hash_value = ExplainGroupedStrategy::HashGroup;
969 let ExplainGrouping::Grouped {
970 strategy: ordered_value,
971 ..
972 } = &mut ordered_strategy.grouping
973 else {
974 panic!("grouped explain fixture must produce grouped explain shape");
975 };
976 *ordered_value = ExplainGroupedStrategy::OrderedGroup;
977
978 assert_eq!(
979 hash_strategy.fingerprint(),
980 ordered_strategy.fingerprint(),
981 "execution strategy hints are explain/runtime metadata and must not affect semantic fingerprint identity",
982 );
983 }
984}