radixdb_executor/operators/
count_pk_semijoin.rs1use crate::context::ExecutionContext;
11use crate::operator::{ColumnInfo, Operator, RowRef};
12use radixdb_core::{Result, Row, Value};
13use radixdb_storage::traits::Table;
14
15pub const DEFAULT_PK_SEMIJOIN_BATCH_SIZE: usize = 1_024;
19
20pub struct CountPkSemiJoinOperator {
21 child: Box<dyn Operator>,
22 parent: Box<dyn Table>,
23 child_key_index: usize,
24 batch_size: usize,
25 schema: Vec<ColumnInfo>,
26 result: Option<Row>,
27 context: Option<ExecutionContext>,
28 opened: bool,
29}
30
31impl CountPkSemiJoinOperator {
32 pub fn new(child: Box<dyn Operator>, parent: Box<dyn Table>, child_key_index: usize) -> Self {
33 Self::with_batch_size(
34 child,
35 parent,
36 child_key_index,
37 DEFAULT_PK_SEMIJOIN_BATCH_SIZE,
38 )
39 }
40
41 pub fn with_batch_size(
42 child: Box<dyn Operator>,
43 parent: Box<dyn Table>,
44 child_key_index: usize,
45 batch_size: usize,
46 ) -> Self {
47 Self {
48 child,
49 parent,
50 child_key_index,
51 batch_size: batch_size.max(1),
52 schema: vec![ColumnInfo::new("count")],
53 result: None,
54 context: None,
55 opened: false,
56 }
57 }
58
59 pub fn with_context(mut self, context: &ExecutionContext) -> Self {
62 self.context = Some(context.clone());
63 self
64 }
65
66 fn flush_batch(&self, keys: &mut Vec<i64>, matches: &mut Vec<bool>) -> Result<u64> {
67 if keys.is_empty() {
68 return Ok(0);
69 }
70 matches.clear();
71 matches.resize(keys.len(), false);
72 let hits = self.parent.probe_visible_row_ids(keys, matches)?;
73 radixdb_storage::instrumentation::record_join_pk_probe(keys.len() as u64, hits as u64, 0);
74 keys.clear();
75 Ok(hits as u64)
76 }
77}
78
79impl Operator for CountPkSemiJoinOperator {
80 fn open(&mut self) -> Result<()> {
81 if let Err(error) = self.child.open() {
82 let _ = self.child.close();
83 return Err(error);
84 }
85 let mut keys = Vec::with_capacity(self.batch_size);
86 let mut matches = Vec::with_capacity(self.batch_size);
87 let mut child_rows = 0_u64;
88 let mut child_key_rows = 0_u64;
89 let computation = (|| -> Result<u64> {
90 let mut count = 0_u64;
91 if let Some(context) = &self.context {
92 context.check_cancelled()?;
93 }
94
95 while let Some(row) = self.child.next()? {
96 child_rows = child_rows.saturating_add(1);
97 if child_rows.is_multiple_of(256) {
98 if let Some(context) = &self.context {
99 context.check_cancelled()?;
100 }
101 }
102 if let Some(Value::Integer(key)) = row.get(self.child_key_index) {
103 child_key_rows = child_key_rows.saturating_add(1);
104 keys.push(*key);
105 if keys.len() == self.batch_size {
106 count = count.saturating_add(self.flush_batch(&mut keys, &mut matches)?);
107 }
108 }
109 }
110 Ok(count.saturating_add(self.flush_batch(&mut keys, &mut matches)?))
111 })();
112 let count = match computation {
113 Ok(count) => count,
114 Err(error) => {
115 let _ = self.child.close();
119 return Err(error);
120 }
121 };
122 radixdb_storage::instrumentation::record_join_outer_rows(child_rows, child_key_rows);
123
124 self.result = Some(Row::from_values(vec![Value::Integer(
125 i64::try_from(count).unwrap_or(i64::MAX),
126 )]));
127 self.opened = true;
128 Ok(())
129 }
130
131 fn next(&mut self) -> Result<Option<RowRef>> {
132 if !self.opened {
133 return Err(radixdb_core::Error::internal(
134 "CountPkSemiJoinOperator::next called before open",
135 ));
136 }
137 Ok(self.result.take().map(RowRef::Owned))
138 }
139
140 fn close(&mut self) -> Result<()> {
141 self.child.close()
142 }
143
144 fn schema(&self) -> &[ColumnInfo] {
145 &self.schema
146 }
147
148 fn estimated_rows(&self) -> Option<usize> {
149 Some(1)
150 }
151
152 fn name(&self) -> &str {
153 "CountPkSemiJoin"
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::operator::MaterializedOperator;
161 use std::sync::Arc;
162
163 use radixdb_storage::mvcc::engine::MVCCEngine;
164 use radixdb_storage::traits::Engine;
165
166 #[test]
167 fn counts_matching_keys_preserves_child_duplicates_and_skips_nulls() {
168 let engine = Arc::new(MVCCEngine::in_memory());
169 engine.open_engine().unwrap();
170 let executor = crate::Executor::new(Arc::clone(&engine));
171 executor
172 .execute("CREATE TABLE parent (id INTEGER PRIMARY KEY, payload TEXT)")
173 .unwrap();
174 drop(executor);
175
176 let mut write = engine.begin_transaction().unwrap();
177 let mut parent = write.get_table("parent").unwrap();
178 parent
179 .insert(Row::from_values(vec![
180 Value::Integer(10),
181 Value::from("must not be read"),
182 ]))
183 .unwrap();
184 parent
185 .insert(Row::from_values(vec![
186 Value::Integer(20),
187 Value::from("must not be read"),
188 ]))
189 .unwrap();
190 drop(parent);
191 write.commit().unwrap();
192
193 let mut read = engine.begin_transaction().unwrap();
194 let parent = read.get_table("parent").unwrap();
195 let child = MaterializedOperator::new(
196 vec![
197 Row::from_values(vec![Value::Integer(10)]),
198 Row::from_values(vec![Value::Integer(10)]),
199 Row::from_values(vec![Value::Integer(99)]),
200 Row::from_values(vec![Value::null_unknown()]),
201 Row::from_values(vec![Value::Integer(20)]),
202 ],
203 vec![ColumnInfo::new("parent_id")],
204 );
205 let mut op = CountPkSemiJoinOperator::with_batch_size(Box::new(child), parent, 0, 2);
206 op.open().unwrap();
207 let row = op.next().unwrap().unwrap().into_owned();
208 assert_eq!(row.get(0), Some(&Value::Integer(3)));
209 assert!(op.next().unwrap().is_none());
210 op.close().unwrap();
211 read.rollback().unwrap();
212 engine.close_engine().unwrap();
213 }
214
215 #[test]
216 fn checks_cancellation_before_consuming_child_stream() {
217 let engine = Arc::new(MVCCEngine::in_memory());
218 engine.open_engine().unwrap();
219 let executor = crate::Executor::new(Arc::clone(&engine));
220 executor
221 .execute("CREATE TABLE parent (id INTEGER PRIMARY KEY)")
222 .unwrap();
223 drop(executor);
224 let mut tx = engine.begin_transaction().unwrap();
225 let parent = tx.get_table("parent").unwrap();
226 let child = MaterializedOperator::new(
227 vec![Row::from_values(vec![Value::Integer(1)])],
228 vec![ColumnInfo::new("parent_id")],
229 );
230 let context = ExecutionContext::new();
231 context.cancel();
232 let mut op =
233 CountPkSemiJoinOperator::new(Box::new(child), parent, 0).with_context(&context);
234 assert!(op.open().is_err());
235 tx.rollback().unwrap();
236 engine.close_engine().unwrap();
237 }
238}