Skip to main content

uqa_execution/distinct/
mod.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Byte-bounded streaming physical `DISTINCT` operator.
8//!
9//! The operator keeps exact encoded keys in memory until their combined byte
10//! size reaches `work_mem`. It then migrates every key to a temporary,
11//! bucketed on-disk set. Disk probes compare the complete encoded key, so a
12//! hash collision can never turn a new row into a duplicate. Output remains
13
14mod encoding;
15mod memory;
16mod spill;
17
18use std::path::{Path, PathBuf};
19
20use crate::{
21    Batch, ExecError, ExecResult, PhysicalOperator, RowSchema, ScalarExpr,
22    SharedExpressionEvaluator,
23};
24
25use encoding::encode_key_borrowed;
26pub use encoding::{canonical_row_key, hash_canonical_row, try_pack_compact_text_pair};
27pub(crate) use encoding::{encode_key, encode_non_null_key, EncodedKey};
28pub use memory::{CanonicalRowHashSet, ExactRowSet};
29pub(crate) use spill::SeenKeySet;
30#[cfg(test)]
31use spill::{stable_hash, DISK_BUCKETS};
32
33/// Default used by compatibility constructors. Engine callers should pass the
34/// current session's `work_mem` through [`Distinct::all_with_work_mem`] or
35/// [`Distinct::on_with_work_mem`].
36pub const DEFAULT_DISTINCT_WORK_MEM_BYTES: usize = 64 * 1024 * 1024;
37
38/// Stable SQL duplicate elimination.
39///
40/// With no key expressions, the complete positional output row is the key.
41/// With expressions, the operator implements `DISTINCT ON`: it preserves the
42/// first row for each evaluated key in child order.
43pub struct Distinct<'a> {
44    child: Box<dyn PhysicalOperator + 'a>,
45    keys: Option<Vec<ScalarExpr>>,
46    evaluator: Option<SharedExpressionEvaluator<'a>>,
47    schema: RowSchema,
48    work_mem_bytes: usize,
49    spill_directory: Option<PathBuf>,
50    seen: SeenKeySet,
51}
52
53impl<'a> Distinct<'a> {
54    /// Construct a bounded full-row `DISTINCT` with the compatibility default
55    /// work-memory budget.
56    pub fn all(child: Box<dyn PhysicalOperator + 'a>) -> Self {
57        Self::all_with_work_mem(child, DEFAULT_DISTINCT_WORK_MEM_BYTES)
58    }
59
60    /// Construct a bounded full-row `DISTINCT` with an explicit byte budget.
61    pub fn all_with_work_mem(child: Box<dyn PhysicalOperator + 'a>, work_mem_bytes: usize) -> Self {
62        let schema = child.row_schema().clone();
63        Self {
64            child,
65            keys: None,
66            evaluator: None,
67            schema,
68            work_mem_bytes,
69            spill_directory: None,
70            seen: SeenKeySet::new(work_mem_bytes, None),
71        }
72    }
73
74    /// Construct a bounded `DISTINCT ON` with the compatibility default
75    /// work-memory budget.
76    pub fn on(
77        child: Box<dyn PhysicalOperator + 'a>,
78        keys: Vec<ScalarExpr>,
79        evaluator: SharedExpressionEvaluator<'a>,
80    ) -> Self {
81        Self::on_with_work_mem(child, keys, evaluator, DEFAULT_DISTINCT_WORK_MEM_BYTES)
82    }
83
84    /// Construct a bounded `DISTINCT ON` with an explicit byte budget.
85    pub fn on_with_work_mem(
86        child: Box<dyn PhysicalOperator + 'a>,
87        keys: Vec<ScalarExpr>,
88        evaluator: SharedExpressionEvaluator<'a>,
89        work_mem_bytes: usize,
90    ) -> Self {
91        let schema = child.row_schema().clone();
92        Self {
93            child,
94            keys: Some(keys),
95            evaluator: Some(evaluator),
96            schema,
97            work_mem_bytes,
98            spill_directory: None,
99            seen: SeenKeySet::new(work_mem_bytes, None),
100        }
101    }
102
103    /// Place the exact-set files in a caller-selected temporary-data
104    /// directory. The directory must already exist; a private child directory
105    /// is created lazily on the first spill and removed through RAII.
106    pub fn with_spill_directory(mut self, directory: impl Into<PathBuf>) -> Self {
107        self.spill_directory = Some(directory.into());
108        self.reset_seen();
109        self
110    }
111
112    /// Whether this invocation has migrated its key set to disk.
113    pub fn has_spilled(&self) -> bool {
114        self.seen.has_spilled()
115    }
116
117    /// Exact encoded key bytes retained by the in-memory set.
118    pub fn in_memory_key_bytes(&self) -> usize {
119        self.seen.in_memory_bytes()
120    }
121
122    /// Live private spill directory, for diagnostics and cleanup tests.
123    pub fn spill_path(&self) -> Option<&Path> {
124        self.seen.spill_path()
125    }
126
127    fn reset_seen(&mut self) {
128        self.seen = SeenKeySet::new(self.work_mem_bytes, self.spill_directory.clone());
129    }
130
131    fn key(&self, schema: &RowSchema, row: &crate::PhysicalRow) -> ExecResult<Vec<u8>> {
132        if let Some(keys) = self.keys.as_ref() {
133            let evaluator = self.evaluator.as_ref().ok_or_else(|| {
134                ExecError::Other("DISTINCT ON evaluator is not configured".into())
135            })?;
136            let values = keys
137                .iter()
138                .map(|expression| evaluator.evaluate_physical(expression, schema, row))
139                .collect::<ExecResult<Vec<_>>>()?;
140            return encode_key(&values);
141        }
142        let row = schema.view(row);
143        encode_key_borrowed((0..self.schema.len()).map(|index| row.value_at(index)))
144    }
145}
146
147impl PhysicalOperator for Distinct<'_> {
148    fn row_schema(&self) -> &RowSchema {
149        &self.schema
150    }
151
152    fn open(&mut self) -> ExecResult<()> {
153        self.reset_seen();
154        self.child.open()
155    }
156
157    fn next(&mut self) -> ExecResult<Option<Batch>> {
158        loop {
159            let Some(batch) = self.child.next()? else {
160                return Ok(None);
161            };
162            if batch.schema != self.schema {
163                return Err(ExecError::Other(format!(
164                    "DISTINCT input schema mismatch: expected {:?}, got {:?}",
165                    self.schema, batch.schema
166                )));
167            }
168            let mut rows = Vec::with_capacity(batch.rows.len());
169            for row in batch.rows {
170                let key = self.key(&batch.schema, &row)?;
171                if self.seen.insert(key)? {
172                    rows.push(row.without_lock_origins());
173                }
174            }
175            if !rows.is_empty() {
176                return Ok(Some(Batch::from_physical_rows(self.schema.clone(), rows)));
177            }
178        }
179    }
180
181    fn close(&mut self) -> ExecResult<()> {
182        self.reset_seen();
183        self.child.close()
184    }
185}
186
187#[cfg(test)]
188mod tests;