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