1use std::collections::{BTreeMap, BTreeSet, HashMap};
16use std::fs::{File, OpenOptions};
17use std::hash::{BuildHasher, Hasher};
18use std::io::{ErrorKind, Read, Seek, SeekFrom, Write};
19use std::path::{Path, PathBuf};
20
21use smallvec::{Array, SmallVec};
22use tempfile::{Builder as TempBuilder, TempDir};
23use uqa_core::{DecimalValue, TemporalValue, Value};
24use uqa_sql::ResultRow;
25
26use crate::{
27 Batch, ExecError, ExecResult, PhysicalOperator, PhysicalRow, RowSchema, ScalarExpr,
28 SharedExpressionEvaluator,
29};
30
31pub const DEFAULT_DISTINCT_WORK_MEM_BYTES: usize = 64 * 1024 * 1024;
35
36const DISK_BUCKETS: u64 = 64;
37const COPY_BUFFER_BYTES: usize = 8 * 1024;
38const MICROS_PER_DAY: i128 = 86_400_000_000;
39
40pub(crate) type EncodedKey = SmallVec<[u8; 64]>;
41
42pub fn hash_canonical_row<'a, S: BuildHasher>(
49 build_hasher: &S,
50 values: impl ExactSizeIterator<Item = Option<&'a Value>>,
51) -> ExecResult<u64> {
52 let count = values.len();
53 let mut hasher = build_hasher.build_hasher();
54 {
55 let mut output = HasherOutput(&mut hasher);
56 encode_len(count, &mut output)?;
57 for value in values {
58 if let Some(value) = value {
59 encode_value(value, &mut output)?;
60 } else {
61 output.push_byte(0);
62 }
63 }
64 }
65 Ok(hasher.finish())
66}
67
68pub fn try_pack_compact_text_pair<'a>(
70 values: impl ExactSizeIterator<Item = Option<&'a Value>>,
71) -> Option<u64> {
72 if values.len() != 2 {
73 return None;
74 }
75 let mut values = values;
76 let first = compact_text_component(values.next()?)?;
77 let second = compact_text_component(values.next()?)?;
78 Some(u64::from(first) << 32 | u64::from(second))
79}
80
81fn compact_text_component(value: Option<&Value>) -> Option<u32> {
82 match value {
83 None | Some(Value::Null) => Some(0),
84 Some(Value::Str(value)) if value.len() <= 3 => {
85 let mut packed = [0u8; 4];
86 packed[0] = u8::try_from(value.len()).ok()? + 1;
87 packed[1..][..value.len()].copy_from_slice(value.as_bytes());
88 Some(u32::from_be_bytes(packed))
89 }
90 Some(_) => None,
91 }
92}
93
94pub fn canonical_row_key(values: &[Value]) -> ExecResult<Vec<u8>> {
96 encode_key(values)
97}
98
99pub struct CanonicalRowHashSet {
107 rows: Vec<SmallVec<[Value; 2]>>,
108 index: HashMap<u64, SmallVec<[usize; 1]>, ahash::RandomState>,
109}
110
111impl CanonicalRowHashSet {
112 #[must_use]
113 pub fn new() -> Self {
114 Self {
115 rows: Vec::new(),
116 index: HashMap::with_hasher(ahash::RandomState::new()),
117 }
118 }
119
120 pub fn insert_borrowed(&mut self, values: &[&Value]) -> ExecResult<bool> {
123 let hash = hash_canonical_row(self.index.hasher(), values.iter().copied().map(Some))?;
124 if self.matching_borrowed(hash, values) {
125 return Ok(false);
126 }
127
128 let row = values
129 .iter()
130 .map(|value| (*value).clone())
131 .collect::<SmallVec<[Value; 2]>>();
132 let row_index = self.rows.len();
133 self.rows.push(row);
134 self.index.entry(hash).or_default().push(row_index);
135 Ok(true)
136 }
137
138 pub fn insert_values(&mut self, values: &[Value]) -> ExecResult<bool> {
141 let hash = hash_canonical_row(self.index.hasher(), values.iter().map(Some))?;
142 if self.matching_values(hash, values) {
143 return Ok(false);
144 }
145
146 let row_index = self.rows.len();
147 self.rows.push(values.iter().cloned().collect());
148 self.index.entry(hash).or_default().push(row_index);
149 Ok(true)
150 }
151
152 pub fn contains_borrowed(&self, values: &[&Value]) -> ExecResult<bool> {
155 let hash = hash_canonical_row(self.index.hasher(), values.iter().copied().map(Some))?;
156 Ok(self.matching_borrowed(hash, values))
157 }
158
159 pub fn contains_values(&self, values: &[Value]) -> ExecResult<bool> {
161 let hash = hash_canonical_row(self.index.hasher(), values.iter().map(Some))?;
162 Ok(self.matching_values(hash, values))
163 }
164
165 fn matching_borrowed(&self, hash: u64, values: &[&Value]) -> bool {
166 self.index.get(&hash).is_some_and(|bucket| {
167 bucket.iter().copied().any(|index| {
168 let stored = &self.rows[index];
169 stored.len() == values.len()
170 && stored
171 .iter()
172 .zip(values)
173 .all(|(stored, value)| stored == *value)
174 })
175 })
176 }
177
178 fn matching_values(&self, hash: u64, values: &[Value]) -> bool {
179 self.index.get(&hash).is_some_and(|bucket| {
180 bucket
181 .iter()
182 .copied()
183 .any(|index| self.rows[index].as_slice() == values)
184 })
185 }
186}
187
188impl Default for CanonicalRowHashSet {
189 fn default() -> Self {
190 Self::new()
191 }
192}
193
194pub struct ExactRowSet {
202 seen: SeenKeySet,
203}
204
205impl ExactRowSet {
206 pub fn new(work_mem_bytes: usize) -> Self {
207 Self {
208 seen: SeenKeySet::new(work_mem_bytes, None),
209 }
210 }
211
212 pub fn with_spill_directory(work_mem_bytes: usize, directory: impl Into<PathBuf>) -> Self {
213 Self {
214 seen: SeenKeySet::new(work_mem_bytes, Some(directory.into())),
215 }
216 }
217
218 pub fn insert_row(&mut self, row: &ResultRow, schema: &[String]) -> ExecResult<bool> {
221 self.seen.insert(row_key(row, schema)?)
222 }
223
224 pub fn contains_row(&mut self, row: &ResultRow, schema: &[String]) -> ExecResult<bool> {
225 self.seen.contains(&row_key(row, schema)?)
226 }
227
228 pub fn insert_values(&mut self, values: &[Value]) -> ExecResult<bool> {
232 self.seen.insert(encode_key(values)?)
233 }
234
235 pub fn contains_values(&mut self, values: &[Value]) -> ExecResult<bool> {
238 self.seen.contains(&encode_key(values)?)
239 }
240
241 pub fn insert_physical(&mut self, row: &PhysicalRow, schema: &RowSchema) -> ExecResult<bool> {
243 let view = schema.view(row);
244 self.seen.insert(encode_key_borrowed(
245 (0..schema.len()).map(|position| view.value_at(position)),
246 )?)
247 }
248
249 pub fn contains_physical(&mut self, row: &PhysicalRow, schema: &RowSchema) -> ExecResult<bool> {
251 let view = schema.view(row);
252 self.seen.contains(&encode_key_borrowed(
253 (0..schema.len()).map(|position| view.value_at(position)),
254 )?)
255 }
256
257 pub fn has_spilled(&self) -> bool {
258 self.seen.has_spilled()
259 }
260
261 pub fn in_memory_key_bytes(&self) -> usize {
262 self.seen.in_memory_bytes()
263 }
264}
265
266fn row_key(row: &ResultRow, schema: &[String]) -> ExecResult<Vec<u8>> {
267 encode_key_borrowed(schema.iter().map(|column| row.get(column)))
268}
269
270pub struct Distinct<'a> {
276 child: Box<dyn PhysicalOperator + 'a>,
277 keys: Option<Vec<ScalarExpr>>,
278 evaluator: Option<SharedExpressionEvaluator<'a>>,
279 schema: RowSchema,
280 work_mem_bytes: usize,
281 spill_directory: Option<PathBuf>,
282 seen: SeenKeySet,
283}
284
285impl<'a> Distinct<'a> {
286 pub fn all(child: Box<dyn PhysicalOperator + 'a>) -> Self {
289 Self::all_with_work_mem(child, DEFAULT_DISTINCT_WORK_MEM_BYTES)
290 }
291
292 pub fn all_with_work_mem(child: Box<dyn PhysicalOperator + 'a>, work_mem_bytes: usize) -> Self {
294 let schema = child.row_schema().clone();
295 Self {
296 child,
297 keys: None,
298 evaluator: None,
299 schema,
300 work_mem_bytes,
301 spill_directory: None,
302 seen: SeenKeySet::new(work_mem_bytes, None),
303 }
304 }
305
306 pub fn on(
309 child: Box<dyn PhysicalOperator + 'a>,
310 keys: Vec<ScalarExpr>,
311 evaluator: SharedExpressionEvaluator<'a>,
312 ) -> Self {
313 Self::on_with_work_mem(child, keys, evaluator, DEFAULT_DISTINCT_WORK_MEM_BYTES)
314 }
315
316 pub fn on_with_work_mem(
318 child: Box<dyn PhysicalOperator + 'a>,
319 keys: Vec<ScalarExpr>,
320 evaluator: SharedExpressionEvaluator<'a>,
321 work_mem_bytes: usize,
322 ) -> Self {
323 let schema = child.row_schema().clone();
324 Self {
325 child,
326 keys: Some(keys),
327 evaluator: Some(evaluator),
328 schema,
329 work_mem_bytes,
330 spill_directory: None,
331 seen: SeenKeySet::new(work_mem_bytes, None),
332 }
333 }
334
335 pub fn with_spill_directory(mut self, directory: impl Into<PathBuf>) -> Self {
339 self.spill_directory = Some(directory.into());
340 self.reset_seen();
341 self
342 }
343
344 pub fn has_spilled(&self) -> bool {
346 self.seen.has_spilled()
347 }
348
349 pub fn in_memory_key_bytes(&self) -> usize {
351 self.seen.in_memory_bytes()
352 }
353
354 pub fn spill_path(&self) -> Option<&Path> {
356 self.seen.spill_path()
357 }
358
359 fn reset_seen(&mut self) {
360 self.seen = SeenKeySet::new(self.work_mem_bytes, self.spill_directory.clone());
361 }
362
363 fn key(&self, schema: &RowSchema, row: &crate::PhysicalRow) -> ExecResult<Vec<u8>> {
364 if let Some(keys) = self.keys.as_ref() {
365 let evaluator = self.evaluator.as_ref().ok_or_else(|| {
366 ExecError::Other("DISTINCT ON evaluator is not configured".into())
367 })?;
368 let values = keys
369 .iter()
370 .map(|expression| evaluator.evaluate_physical(expression, schema, row))
371 .collect::<ExecResult<Vec<_>>>()?;
372 return encode_key(&values);
373 }
374 let row = schema.view(row);
375 encode_key_borrowed((0..self.schema.len()).map(|index| row.value_at(index)))
376 }
377}
378
379impl PhysicalOperator for Distinct<'_> {
380 fn row_schema(&self) -> &RowSchema {
381 &self.schema
382 }
383
384 fn open(&mut self) -> ExecResult<()> {
385 self.reset_seen();
386 self.child.open()
387 }
388
389 fn next(&mut self) -> ExecResult<Option<Batch>> {
390 loop {
391 let Some(batch) = self.child.next()? else {
392 return Ok(None);
393 };
394 if batch.schema != self.schema {
395 return Err(ExecError::Other(format!(
396 "DISTINCT input schema mismatch: expected {:?}, got {:?}",
397 self.schema, batch.schema
398 )));
399 }
400 let mut rows = Vec::with_capacity(batch.rows.len());
401 for row in batch.rows {
402 let key = self.key(&batch.schema, &row)?;
403 if self.seen.insert(key)? {
404 rows.push(row.without_lock_origins());
405 }
406 }
407 if !rows.is_empty() {
408 return Ok(Some(Batch::from_physical_rows(self.schema.clone(), rows)));
409 }
410 }
411 }
412
413 fn close(&mut self) -> ExecResult<()> {
414 self.reset_seen();
415 self.child.close()
416 }
417}
418
419pub(crate) struct SeenKeySet {
420 memory: BTreeSet<Vec<u8>>,
421 memory_bytes: usize,
422 budget_bytes: usize,
423 spill_directory: Option<PathBuf>,
424 disk: Option<DiskKeySet>,
425}
426
427impl SeenKeySet {
428 pub(crate) fn new(budget_bytes: usize, spill_directory: Option<PathBuf>) -> Self {
429 Self {
430 memory: BTreeSet::new(),
431 memory_bytes: 0,
432 budget_bytes,
433 spill_directory,
434 disk: None,
435 }
436 }
437
438 pub(crate) fn insert(&mut self, key: Vec<u8>) -> ExecResult<bool> {
439 if let Some(disk) = self.disk.as_mut() {
440 return disk.insert(&key);
441 }
442 if self.memory.contains(&key) {
443 return Ok(false);
444 }
445
446 let fits = self
447 .memory_bytes
448 .checked_add(key.len())
449 .is_some_and(|bytes| bytes <= self.budget_bytes);
450 if fits {
451 self.memory_bytes += key.len();
452 self.memory.insert(key);
453 return Ok(true);
454 }
455
456 let mut disk = DiskKeySet::new(self.spill_directory.as_deref())?;
460 for existing in &self.memory {
461 if !disk.insert(existing)? {
462 return Err(distinct_error(
463 "duplicate key found while migrating DISTINCT state",
464 ));
465 }
466 }
467 let inserted = disk.insert(&key)?;
468 self.memory.clear();
469 self.memory_bytes = 0;
470 self.disk = Some(disk);
471 Ok(inserted)
472 }
473
474 pub(crate) fn contains(&mut self, key: &[u8]) -> ExecResult<bool> {
475 match self.disk.as_mut() {
476 Some(disk) => disk.contains(key),
477 None => Ok(self.memory.contains(key)),
478 }
479 }
480
481 fn has_spilled(&self) -> bool {
482 self.disk.is_some()
483 }
484
485 fn in_memory_bytes(&self) -> usize {
486 self.memory_bytes
487 }
488
489 fn spill_path(&self) -> Option<&Path> {
490 self.disk.as_ref().map(|disk| disk.directory.path())
491 }
492}
493
494struct DiskKeySet {
498 directory: TempDir,
499 buckets: BTreeMap<u8, File>,
500}
501
502impl DiskKeySet {
503 fn new(parent: Option<&Path>) -> ExecResult<Self> {
504 let mut builder = TempBuilder::new();
505 builder.prefix("uqa-distinct-");
506 let directory = parent
507 .map_or_else(|| builder.tempdir(), |parent| builder.tempdir_in(parent))
508 .map_err(|error| {
509 distinct_error(format!(
510 "failed to create DISTINCT spill directory: {error}"
511 ))
512 })?;
513 Ok(Self {
514 directory,
515 buckets: BTreeMap::new(),
516 })
517 }
518
519 fn insert(&mut self, key: &[u8]) -> ExecResult<bool> {
520 let bucket = u8::try_from(stable_hash(key) % DISK_BUCKETS)
521 .map_err(|_| distinct_error("DISTINCT spill bucket exceeds u8"))?;
522 if !self.buckets.contains_key(&bucket) {
523 let path = self.directory.path().join(format!("bucket-{bucket:02x}"));
524 let file = OpenOptions::new()
525 .create_new(true)
526 .read(true)
527 .write(true)
528 .open(&path)
529 .map_err(|error| {
530 distinct_error(format!(
531 "failed to create DISTINCT spill bucket {}: {error}",
532 path.display()
533 ))
534 })?;
535 self.buckets.insert(bucket, file);
536 }
537 let file = self
538 .buckets
539 .get_mut(&bucket)
540 .ok_or_else(|| distinct_error("DISTINCT spill bucket registration failed"))?;
541
542 file.seek(SeekFrom::Start(0)).map_err(|error| {
543 distinct_error(format!("failed to seek DISTINCT spill bucket: {error}"))
544 })?;
545 while let Some(record_len) = read_record_len(file)? {
546 let matches = compare_record(file, record_len, key)?;
547 if matches {
548 return Ok(false);
549 }
550 }
551
552 let original_len = file.seek(SeekFrom::End(0)).map_err(|error| {
553 distinct_error(format!("failed to seek DISTINCT spill bucket: {error}"))
554 })?;
555 let key_len = u64::try_from(key.len())
556 .map_err(|_| distinct_error("DISTINCT key length exceeds the on-disk format"))?;
557 let write_result = (|| {
558 file.write_all(&key_len.to_le_bytes()).map_err(|error| {
559 distinct_error(format!("failed to write DISTINCT key length: {error}"))
560 })?;
561 file.write_all(key).map_err(|error| {
562 distinct_error(format!("failed to write DISTINCT key: {error}"))
563 })?;
564 file.flush().map_err(|error| {
565 distinct_error(format!("failed to flush DISTINCT spill bucket: {error}"))
566 })
567 })();
568 if let Err(error) = write_result {
569 if let Err(rollback_error) = file.set_len(original_len) {
570 return Err(distinct_error(format!(
571 "{error}; failed to roll back partial DISTINCT key: {rollback_error}"
572 )));
573 }
574 return Err(error);
575 }
576 Ok(true)
577 }
578
579 fn contains(&mut self, key: &[u8]) -> ExecResult<bool> {
580 let bucket = u8::try_from(stable_hash(key) % DISK_BUCKETS)
581 .map_err(|_| distinct_error("DISTINCT spill bucket exceeds u8"))?;
582 let Some(file) = self.buckets.get_mut(&bucket) else {
583 return Ok(false);
584 };
585 file.seek(SeekFrom::Start(0)).map_err(|error| {
586 distinct_error(format!("failed to seek DISTINCT spill bucket: {error}"))
587 })?;
588 while let Some(record_len) = read_record_len(file)? {
589 if compare_record(file, record_len, key)? {
590 return Ok(true);
591 }
592 }
593 Ok(false)
594 }
595}
596
597fn read_record_len(file: &mut File) -> ExecResult<Option<u64>> {
598 let mut encoded = [0_u8; 8];
599 match file.read(&mut encoded[..1]) {
600 Ok(0) => return Ok(None),
601 Ok(1) => {}
602 Ok(count) => {
603 return Err(distinct_error(format!(
604 "invalid DISTINCT spill read count: requested 1 byte, received {count}"
605 )));
606 }
607 Err(error) => {
608 return Err(distinct_error(format!(
609 "failed to read DISTINCT spill bucket: {error}"
610 )));
611 }
612 }
613 file.read_exact(&mut encoded[1..]).map_err(|error| {
614 if error.kind() == ErrorKind::UnexpectedEof {
615 distinct_error("truncated DISTINCT spill key length")
616 } else {
617 distinct_error(format!("failed to read DISTINCT key length: {error}"))
618 }
619 })?;
620 Ok(Some(u64::from_le_bytes(encoded)))
621}
622
623fn compare_record(file: &mut File, record_len: u64, key: &[u8]) -> ExecResult<bool> {
625 let key_len = u64::try_from(key.len())
626 .map_err(|_| distinct_error("DISTINCT key length exceeds the on-disk format"))?;
627 let mut remaining = record_len;
628 let mut offset = 0_usize;
629 let mut matches = record_len == key_len;
630 let mut buffer = [0_u8; COPY_BUFFER_BYTES];
631 while remaining > 0 {
632 let copy_buffer_bytes = u64::try_from(COPY_BUFFER_BYTES)
633 .map_err(|_| distinct_error("DISTINCT copy buffer exceeds the on-disk length range"))?;
634 let take = usize::try_from(remaining.min(copy_buffer_bytes)).map_err(|_| {
635 distinct_error("DISTINCT spill key chunk exceeds the addressable memory range")
636 })?;
637 file.read_exact(&mut buffer[..take]).map_err(|error| {
638 if error.kind() == ErrorKind::UnexpectedEof {
639 distinct_error("truncated DISTINCT spill key")
640 } else {
641 distinct_error(format!("failed to read DISTINCT spill key: {error}"))
642 }
643 })?;
644 if matches && buffer[..take] != key[offset..offset + take] {
645 matches = false;
646 }
647 if matches {
648 offset += take;
649 }
650 let consumed = u64::try_from(take)
651 .map_err(|_| distinct_error("DISTINCT spill key chunk exceeds the length range"))?;
652 remaining -= consumed;
653 }
654 Ok(matches)
655}
656
657fn stable_hash(bytes: &[u8]) -> u64 {
658 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
661 for byte in bytes {
662 hash ^= u64::from(*byte);
663 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
664 }
665 hash
666}
667
668fn distinct_error(message: impl Into<String>) -> ExecError {
669 ExecError::Other(message.into())
670}
671
672pub(crate) fn encode_key(values: &[Value]) -> ExecResult<Vec<u8>> {
677 encode_key_borrowed(values.iter().map(Some))
678}
679
680fn encode_key_borrowed<'a>(
681 values: impl ExactSizeIterator<Item = Option<&'a Value>>,
682) -> ExecResult<Vec<u8>> {
683 let estimated_capacity = encoded_key_capacity(values.len())?;
684 let mut output = Vec::with_capacity(estimated_capacity);
685 encode_len(values.len(), &mut output)?;
686 for value in values {
687 match value {
688 Some(value) => encode_value(value, &mut output)?,
689 None => encode_value(&Value::Null, &mut output)?,
690 }
691 }
692 Ok(output)
693}
694
695pub(crate) fn encode_non_null_key<'a>(
699 values: impl ExactSizeIterator<Item = Option<&'a Value>>,
700) -> ExecResult<Option<EncodedKey>> {
701 let count = values.len();
702 let mut output = EncodedKey::with_capacity(encoded_key_capacity(count)?);
703 encode_len(count, &mut output)?;
704 for value in values {
705 let Some(value) = value else {
706 return Ok(None);
707 };
708 if matches!(value, Value::Null) {
709 return Ok(None);
710 }
711 encode_value(value, &mut output)?;
712 }
713 Ok(Some(output))
714}
715
716fn encoded_key_capacity(values: usize) -> ExecResult<usize> {
717 values
718 .checked_mul(22)
719 .and_then(|bytes| bytes.checked_add(8))
720 .ok_or_else(|| distinct_error("DISTINCT key capacity overflow"))
721}
722
723trait KeyOutput {
724 fn push_byte(&mut self, value: u8);
725 fn extend_bytes(&mut self, values: &[u8]);
726}
727
728impl KeyOutput for Vec<u8> {
729 fn push_byte(&mut self, value: u8) {
730 self.push(value);
731 }
732
733 fn extend_bytes(&mut self, values: &[u8]) {
734 self.extend_from_slice(values);
735 }
736}
737
738impl<A: Array<Item = u8>> KeyOutput for SmallVec<A> {
739 fn push_byte(&mut self, value: u8) {
740 self.push(value);
741 }
742
743 fn extend_bytes(&mut self, values: &[u8]) {
744 self.extend_from_slice(values);
745 }
746}
747
748struct HasherOutput<'a, H: Hasher>(&'a mut H);
749
750impl<H: Hasher> KeyOutput for HasherOutput<'_, H> {
751 fn push_byte(&mut self, value: u8) {
752 self.0.write_u8(value);
753 }
754
755 fn extend_bytes(&mut self, values: &[u8]) {
756 self.0.write(values);
757 }
758}
759
760fn encode_value(value: &Value, output: &mut impl KeyOutput) -> ExecResult<()> {
761 match value {
762 Value::Null => output.push_byte(0),
763 Value::Bool(value) => {
764 encode_decimal_numeric(&DecimalValue::from_bool(*value), output)?;
765 }
766 Value::Int(value) => {
767 encode_decimal_numeric(&DecimalValue::from_i64(*value), output)?;
768 }
769 Value::Float(value) => encode_float_numeric(*value, output)?,
770 Value::Decimal(value) => encode_decimal_numeric(value, output)?,
771 Value::Str(value) => {
772 output.push_byte(2);
773 encode_bytes(value.as_bytes(), output)?;
774 }
775 Value::FixedChar(value) => {
776 output.push_byte(7);
777 encode_bytes(value.trim_end_matches(' ').as_bytes(), output)?;
778 }
779 Value::Bytes(value) => {
780 output.push_byte(3);
781 encode_bytes(value, output)?;
782 }
783 Value::Temporal(value) => encode_temporal(value, output),
784 Value::Json(value) => {
785 output.push_byte(8);
786 encode_bytes(value.as_bytes(), output)?;
787 }
788 Value::JsonB(value) => {
789 output.push_byte(9);
790 let canonical = uqa_core::jsonb_equality_key(value)
791 .ok_or_else(|| ExecError::Other("stored JSONB value is not valid JSON".into()))?;
792 encode_bytes(&canonical, output)?;
793 }
794 Value::Array(array) => {
795 output.push_byte(12);
796 encode_len(array.lower_bounds().len(), output)?;
797 for lower_bound in array.lower_bounds() {
798 output.extend_bytes(&lower_bound.to_le_bytes());
799 }
800 encode_len(array.elements().len(), output)?;
801 for value in array.elements() {
802 encode_value(value, output)?;
803 }
804 }
805 Value::List(values) => {
806 output.push_byte(5);
807 encode_len(values.len(), output)?;
808 for value in values {
809 encode_value(value, output)?;
810 }
811 }
812 Value::Row(values) => {
813 output.push_byte(10);
814 encode_len(values.len(), output)?;
815 for value in values {
816 encode_value(value, output)?;
817 }
818 }
819 Value::Record(fields) => {
820 output.push_byte(11);
821 encode_len(fields.len(), output)?;
822 for (_, value) in fields {
823 encode_value(value, output)?;
824 }
825 }
826 Value::Map(values) => {
827 output.push_byte(6);
828 encode_len(values.len(), output)?;
829 for (name, value) in values {
830 encode_bytes(name.as_bytes(), output)?;
831 encode_value(value, output)?;
832 }
833 }
834 }
835 Ok(())
836}
837
838fn encode_decimal_numeric(value: &DecimalValue, output: &mut impl KeyOutput) -> ExecResult<()> {
839 if value.is_nan() {
840 output.extend_bytes(&[1, 1]);
841 } else if value.is_negative_infinity() {
842 output.extend_bytes(&[1, 2]);
843 } else if value.is_positive_infinity() {
844 output.extend_bytes(&[1, 3]);
845 } else {
846 output.extend_bytes(&[1, 0]);
847 encode_bytes(value.to_canonical_string().as_bytes(), output)?;
848 }
849 Ok(())
850}
851
852fn encode_float_numeric(value: f64, output: &mut impl KeyOutput) -> ExecResult<()> {
853 if value.is_nan() {
854 output.extend_bytes(&[1, 1]);
856 } else if value == f64::NEG_INFINITY {
857 output.extend_bytes(&[1, 2]);
858 } else if value == f64::INFINITY {
859 output.extend_bytes(&[1, 3]);
860 } else if let Some(decimal) = DecimalValue::from_f64_lossy(value) {
861 encode_decimal_numeric(&decimal, output)?;
862 } else {
863 output.extend_bytes(&[1, 4]);
866 let normalized = if value == 0.0 { 0.0 } else { value };
867 output.extend_bytes(&normalized.to_bits().to_be_bytes());
868 }
869 Ok(())
870}
871
872fn encode_temporal(value: &TemporalValue, output: &mut impl KeyOutput) {
873 output.push_byte(4);
874 match value {
875 TemporalValue::Date { days } => {
876 output.push_byte(0);
877 output.extend_bytes(&days.to_be_bytes());
878 }
879 TemporalValue::Time { micros } => {
880 output.push_byte(1);
881 let normalized = i128::from(*micros).rem_euclid(MICROS_PER_DAY);
882 output.extend_bytes(&normalized.to_be_bytes());
883 }
884 TemporalValue::TimeTz {
885 micros,
886 offset_minutes,
887 } => {
888 output.push_byte(2);
889 let normalized = (i128::from(*micros) - i128::from(*offset_minutes) * 60_000_000)
890 .rem_euclid(MICROS_PER_DAY);
891 output.extend_bytes(&normalized.to_be_bytes());
892 }
893 TemporalValue::Timestamp { micros } => {
894 output.push_byte(3);
895 output.extend_bytes(µs.to_be_bytes());
896 }
897 TemporalValue::TimestampTz { micros } => {
898 output.push_byte(4);
899 output.extend_bytes(µs.to_be_bytes());
900 }
901 TemporalValue::Interval {
902 months,
903 days,
904 micros,
905 } => {
906 output.push_byte(5);
907 let normalized = (i128::from(*months) * 30 + i128::from(*days)) * MICROS_PER_DAY
908 + i128::from(*micros);
909 output.extend_bytes(&normalized.to_be_bytes());
910 }
911 }
912}
913
914fn encode_bytes(bytes: &[u8], output: &mut impl KeyOutput) -> ExecResult<()> {
915 encode_len(bytes.len(), output)?;
916 output.extend_bytes(bytes);
917 Ok(())
918}
919
920fn encode_len(length: usize, output: &mut impl KeyOutput) -> ExecResult<()> {
921 let length = u64::try_from(length)
922 .map_err(|_| distinct_error("DISTINCT key component exceeds the binary format"))?;
923 output.extend_bytes(&length.to_be_bytes());
924 Ok(())
925}
926
927#[cfg(test)]
928mod tests {
929 use std::sync::Arc;
930
931 use tempfile::NamedTempFile;
932
933 use super::*;
934 use crate::physical::run_to_rows;
935 use crate::scan::TableScan;
936 use crate::{ExpressionEvaluator, PhysicalRow, ScalarEvalContext};
937
938 fn row(a: i64, b: i64) -> ResultRow {
939 [("a".into(), Value::Int(a)), ("b".into(), Value::Int(b))]
940 .into_iter()
941 .collect()
942 }
943
944 fn value_row(value: Value) -> ResultRow {
945 [("v".into(), value)].into_iter().collect()
946 }
947
948 struct Evaluator;
949
950 impl ExpressionEvaluator for Evaluator {
951 fn evaluate(
952 &self,
953 expression: &ScalarExpr,
954 row: &dyn uqa_sql::expr::RowLookup,
955 ) -> ExecResult<Value> {
956 Ok(crate::eval_scalar(
957 expression,
958 &ScalarEvalContext::from_row_lookup(row, &[]),
959 )?)
960 }
961 }
962
963 #[test]
964 fn all_columns_and_distinct_on_preserve_the_first_row() {
965 let rows = vec![row(1, 10), row(1, 10), row(1, 11), row(2, 20)];
966 let scan = TableScan::from_rows(vec!["a".into(), "b".into()], rows.clone());
967 let mut all = Distinct::all_with_work_mem(Box::new(scan), 1);
968 let (_, all_rows) = run_to_rows(&mut all).unwrap();
969 assert_eq!(all_rows, vec![row(1, 10), row(1, 11), row(2, 20)]);
970
971 let scan = TableScan::from_rows(vec!["a".into(), "b".into()], rows);
972 let mut on = Distinct::on_with_work_mem(
973 Box::new(scan),
974 vec![ScalarExpr::Column("a".into())],
975 Arc::new(Evaluator),
976 1,
977 );
978 let (_, on_rows) = run_to_rows(&mut on).unwrap();
979 assert_eq!(on_rows, vec![row(1, 10), row(2, 20)]);
980 }
981
982 #[test]
983 fn distinct_is_a_row_lock_identity_barrier() {
984 let schema = RowSchema::new(vec!["v".into()]);
985 let row = PhysicalRow::from_values(vec![Value::Int(1)])
986 .with_lock_origin(crate::RowLockOrigin::new("source", "public.source", 1));
987 let scan = TableScan::from_physical_rows(schema, vec![row]);
988 let mut distinct = Distinct::all_with_work_mem(Box::new(scan), 1);
989
990 let batches = crate::physical::run_to_batches(&mut distinct).unwrap();
991 assert!(batches[0].rows[0].lock_origins().is_empty());
992 }
993
994 #[test]
995 fn tiny_budget_migrates_to_disk_and_never_retains_key_bytes() {
996 let rows: Vec<_> = (0..50)
997 .flat_map(|value| [value_row(Value::Int(value)), value_row(Value::Int(value))])
998 .collect();
999 let scan = TableScan::from_rows(vec!["v".into()], rows);
1000 let mut distinct = Distinct::all_with_work_mem(Box::new(scan), 1);
1001 distinct.open().unwrap();
1002 let output = distinct.next().unwrap().unwrap();
1003 assert_eq!(output.rows.len(), 50);
1004 assert!(distinct.has_spilled());
1005 assert_eq!(distinct.in_memory_key_bytes(), 0);
1006 assert!(distinct.next().unwrap().is_none());
1007 distinct.close().unwrap();
1008 }
1009
1010 #[test]
1011 fn exact_row_set_persists_disk_backed_state_across_fixpoint_phases() {
1012 let schema = vec!["a".into(), "b".into()];
1013 let mut seen = ExactRowSet::new(1);
1014 for value in 0..100 {
1015 assert!(seen.insert_row(&row(value, value + 1), &schema).unwrap());
1016 }
1017 assert!(seen.has_spilled());
1018 assert_eq!(seen.in_memory_key_bytes(), 0);
1019 for value in 0..100 {
1020 assert!(seen.contains_row(&row(value, value + 1), &schema).unwrap());
1021 assert!(!seen.insert_row(&row(value, value + 1), &schema).unwrap());
1022 }
1023 assert!(!seen.contains_row(&row(101, 102), &schema).unwrap());
1024 }
1025
1026 #[test]
1027 fn binary_keys_cover_every_value_variant_without_structural_collisions() {
1028 let one = DecimalValue::parse("1.000").unwrap();
1029 let mut nested_map = BTreeMap::new();
1030 nested_map.insert("x".into(), Value::Float(1.0));
1031 let values = vec![
1032 Value::Null,
1033 Value::Bool(true),
1034 Value::Int(1),
1035 Value::Float(1.0),
1036 Value::Decimal(one),
1037 Value::Float(f64::NAN),
1038 Value::Float(f64::from_bits(0x7ff8_0000_0000_0001)),
1039 Value::Float(f64::NEG_INFINITY),
1040 Value::Float(f64::INFINITY),
1041 Value::Str("a\0b".into()),
1042 Value::Bytes(vec![b'a', 0, b'b']),
1043 Value::Temporal(TemporalValue::Date { days: 1 }),
1044 Value::Temporal(TemporalValue::Time {
1045 micros: MICROS_PER_DAY as i64 + 7,
1046 }),
1047 Value::Temporal(TemporalValue::Time { micros: 7 }),
1048 Value::Temporal(TemporalValue::TimeTz {
1049 micros: 3_600_000_000,
1050 offset_minutes: 60,
1051 }),
1052 Value::Temporal(TemporalValue::TimeTz {
1053 micros: 0,
1054 offset_minutes: 0,
1055 }),
1056 Value::Temporal(TemporalValue::Timestamp { micros: 9 }),
1057 Value::Temporal(TemporalValue::TimestampTz { micros: 9 }),
1058 Value::Temporal(TemporalValue::Interval {
1059 months: 1,
1060 days: 0,
1061 micros: 0,
1062 }),
1063 Value::Temporal(TemporalValue::Interval {
1064 months: 0,
1065 days: 30,
1066 micros: 0,
1067 }),
1068 Value::List(vec![Value::Int(1), Value::Str("x".into())]),
1069 Value::List(vec![Value::Float(1.0), Value::Str("x".into())]),
1070 Value::Map(nested_map),
1071 ];
1072 let rows: Vec<_> = values
1073 .iter()
1074 .cloned()
1075 .chain(values.iter().cloned())
1076 .map(value_row)
1077 .collect();
1078 let scan = TableScan::from_rows(vec!["v".into()], rows);
1079 let mut distinct = Distinct::all_with_work_mem(Box::new(scan), 0);
1080 let (_, output) = run_to_rows(&mut distinct).unwrap();
1081
1082 assert_eq!(output.len(), 15);
1086 assert_eq!(output[1], value_row(Value::Bool(true)));
1087 assert!(matches!(output[2].get("v"), Some(Value::Float(v)) if v.is_nan()));
1088 assert_eq!(output[5], value_row(Value::Str("a\0b".into())));
1089 assert_eq!(output[6], value_row(Value::Bytes(vec![b'a', 0, b'b'])));
1090 }
1091
1092 #[test]
1093 fn physical_numeric_join_key_stays_inline_and_matches_distinct_encoding() {
1094 let value = Value::Int(42);
1095 let key = encode_non_null_key(std::iter::once(Some(&value)))
1096 .unwrap()
1097 .unwrap();
1098 assert!(!key.spilled());
1099 assert_eq!(
1100 key.as_slice(),
1101 encode_key(std::slice::from_ref(&value)).unwrap()
1102 );
1103
1104 let null = Value::Null;
1105 assert!(encode_non_null_key(std::iter::once(Some(&null)))
1106 .unwrap()
1107 .is_none());
1108 }
1109
1110 #[test]
1111 fn canonical_row_hash_streams_borrowed_composites_with_sql_equality() {
1112 let integer = Value::Int(1);
1113 let decimal = Value::Decimal(DecimalValue::parse("1.000").unwrap());
1114 let text = Value::Str("group".into());
1115 let hash_state = ahash::RandomState::new();
1116 assert_eq!(
1117 hash_canonical_row(&hash_state, [Some(&integer), Some(&text)].into_iter()).unwrap(),
1118 hash_canonical_row(&hash_state, [Some(&decimal), Some(&text)].into_iter()).unwrap()
1119 );
1120
1121 let null = Value::Null;
1122 assert_eq!(
1123 hash_canonical_row(&hash_state, std::iter::once(None)).unwrap(),
1124 hash_canonical_row(&hash_state, std::iter::once(Some(&null))).unwrap()
1125 );
1126 }
1127
1128 #[test]
1129 fn compact_text_pair_key_is_stable_for_borrowed_and_owned_values() {
1130 let first = Value::Str("A".into());
1131 let second = Value::Str("O".into());
1132 let values = [Some(&first), Some(&second)];
1133
1134 let key = try_pack_compact_text_pair(values.into_iter()).unwrap();
1135 let owned = [first.clone(), second.clone()];
1136 assert_eq!(
1137 key,
1138 try_pack_compact_text_pair(owned.iter().map(Some)).unwrap(),
1139 );
1140 let long = Value::Str("x".repeat(64));
1141 assert_eq!(
1142 try_pack_compact_text_pair([Some(&long), Some(&second)].into_iter()),
1143 None,
1144 );
1145
1146 let candidates = [
1147 Value::Null,
1148 Value::Str(String::new()),
1149 Value::Str("A".into()),
1150 Value::Str("AB".into()),
1151 Value::Str("ABC".into()),
1152 Value::Str("é".into()),
1153 Value::Str("한".into()),
1154 ];
1155 let mut keys = std::collections::BTreeSet::new();
1156 for first in &candidates {
1157 for second in &candidates {
1158 assert!(keys.insert(
1159 try_pack_compact_text_pair([Some(first), Some(second)].into_iter()).unwrap()
1160 ));
1161 }
1162 }
1163 }
1164
1165 #[test]
1166 fn canonical_row_hash_set_copies_only_new_keys_and_probes_borrowed() {
1167 let one = Value::Int(1);
1168 let two = Value::Int(2);
1169 let decimal_one = Value::Decimal(DecimalValue::parse("1.000").unwrap());
1170 let mut rows = CanonicalRowHashSet::new();
1171
1172 assert!(rows.insert_borrowed(&[&one, &two]).unwrap());
1173 assert!(!rows.insert_borrowed(&[&decimal_one, &two]).unwrap());
1174 assert!(rows.contains_borrowed(&[&decimal_one, &two]).unwrap());
1175 assert!(!rows.contains_borrowed(&[&two, &one]).unwrap());
1176 assert_eq!(rows.rows.len(), 1);
1177 assert!(!rows.rows[0].spilled());
1178 }
1179
1180 #[test]
1181 fn temporary_directory_is_removed_on_drop() {
1182 let parent = tempfile::tempdir().unwrap();
1183 let scan = TableScan::from_rows(vec!["v".into()], vec![value_row(Value::Int(1))]);
1184 let mut distinct =
1185 Distinct::all_with_work_mem(Box::new(scan), 0).with_spill_directory(parent.path());
1186 distinct.open().unwrap();
1187 distinct.next().unwrap();
1188 let spill_path = distinct.spill_path().unwrap().to_path_buf();
1189 assert!(spill_path.exists());
1190 drop(distinct);
1191 assert!(!spill_path.exists());
1192 }
1193
1194 #[test]
1195 fn spill_creation_failure_is_returned() {
1196 let not_a_directory = NamedTempFile::new().unwrap();
1197 let scan = TableScan::from_rows(vec!["v".into()], vec![value_row(Value::Int(1))]);
1198 let mut distinct = Distinct::all_with_work_mem(Box::new(scan), 0)
1199 .with_spill_directory(not_a_directory.path());
1200 let error = run_to_rows(&mut distinct).unwrap_err();
1201 assert!(error
1202 .to_string()
1203 .contains("failed to create DISTINCT spill directory"));
1204 }
1205
1206 #[test]
1207 fn truncated_disk_record_is_reported() {
1208 let first = encode_key(&[Value::Int(1)]).unwrap();
1209 let bucket = stable_hash(&first) % DISK_BUCKETS;
1210 let second = (2..10_000)
1211 .map(|value| encode_key(&[Value::Int(value)]).unwrap())
1212 .find(|key| stable_hash(key) % DISK_BUCKETS == bucket)
1213 .unwrap();
1214 let mut set = SeenKeySet::new(0, None);
1215 assert!(set.insert(first).unwrap());
1216 let disk = set.disk.as_mut().unwrap();
1217 let file = disk.buckets.get_mut(&(bucket as u8)).unwrap();
1218 file.set_len(4).unwrap();
1219 let error = set.insert(second).unwrap_err();
1220 assert!(error
1221 .to_string()
1222 .contains("truncated DISTINCT spill key length"));
1223 }
1224
1225 struct FailingEvaluator;
1226
1227 struct MismatchedSchemaScan {
1228 declared: RowSchema,
1229 emitted: Option<Batch>,
1230 }
1231
1232 impl PhysicalOperator for MismatchedSchemaScan {
1233 fn row_schema(&self) -> &RowSchema {
1234 &self.declared
1235 }
1236
1237 fn open(&mut self) -> ExecResult<()> {
1238 Ok(())
1239 }
1240
1241 fn next(&mut self) -> ExecResult<Option<Batch>> {
1242 Ok(self.emitted.take())
1243 }
1244
1245 fn close(&mut self) -> ExecResult<()> {
1246 Ok(())
1247 }
1248 }
1249
1250 impl ExpressionEvaluator for FailingEvaluator {
1251 fn evaluate(
1252 &self,
1253 _expression: &ScalarExpr,
1254 _row: &dyn uqa_sql::expr::RowLookup,
1255 ) -> ExecResult<Value> {
1256 Err(ExecError::Other("intentional evaluator failure".into()))
1257 }
1258 }
1259
1260 #[test]
1261 fn evaluator_errors_are_propagated() {
1262 let scan = TableScan::from_rows(vec!["v".into()], vec![value_row(Value::Int(1))]);
1263 let mut distinct = Distinct::on_with_work_mem(
1264 Box::new(scan),
1265 vec![ScalarExpr::Column("v".into())],
1266 Arc::new(FailingEvaluator),
1267 0,
1268 );
1269 let error = run_to_rows(&mut distinct).unwrap_err();
1270 assert!(error.to_string().contains("intentional evaluator failure"));
1271 }
1272
1273 #[test]
1274 fn distinct_rejects_a_child_batch_with_a_different_schema() {
1275 let scan = MismatchedSchemaScan {
1276 declared: RowSchema::new(vec!["declared".into()]),
1277 emitted: Some(Batch::from_physical_rows(
1278 RowSchema::new(vec!["actual".into()]),
1279 vec![PhysicalRow::from_values(vec![Value::Int(1)])],
1280 )),
1281 };
1282 let mut distinct = Distinct::all(Box::new(scan));
1283
1284 let error = run_to_rows(&mut distinct).unwrap_err();
1285
1286 assert!(error.to_string().contains("DISTINCT input schema mismatch"));
1287 }
1288}