rudb_kernels/prepare.rs
1//! The half of a scalar call that depends on the query rather than on the chunk.
2//!
3//! A kernel is handed a vector and a function name and works out everything else from scratch. Most
4//! of what it works out is the same on every chunk, because it comes from a literal the user wrote:
5//! the pattern of a `LIKE`, the pattern and the options of a regular expression, the part a
6//! `date_part` reads. A pipeline over `hits` runs a hundred thousand chunks, so anything decided
7//! per chunk is decided a hundred thousand times for one query, and compiling a regular expression
8//! is not a cheap thing to do a hundred thousand times.
9//!
10//! A [`Recipe`] is that work done once. The caller that builds a pipeline knows which arguments are
11//! literals, because it has the plan in front of it, so it hands them here and gets back a call it
12//! can run per chunk with the deciding already finished.
13//!
14//! # Why this is not a cache
15//!
16//! A cache keyed on the pattern would also compile once, and it would cost a hash and a lock on
17//! every chunk to find out that nothing changed, and it would be wrong the first time somebody runs
18//! two pipelines that use two patterns on two threads with a cache of one entry. The information is
19//! already sitting in the plan. Reading it there is both cheaper and simpler than rediscovering it.
20//!
21//! # Adding one
22//!
23//! The `Hoisted` enum is the list of what has been lifted so far, and it is deliberately short. A
24//! function earns a variant when the work it repeats per chunk is worth more than the branch that
25//! asks whether it was hoisted, which in practice means the function compiles something. The rule a
26//! new variant has to keep is that hoisting changes nothing a query can see: a literal that does not
27//! compile stays unhoisted rather than failing here, so the error still comes out of the chunk that
28//! reaches it and reads exactly as it did before.
29
30use rudb_common::{LogicalType, Value};
31use rudb_vector::Vector;
32
33use crate::peel::{Lookup, Peel};
34use crate::regexp;
35use crate::scalar;
36use crate::shape::single;
37
38/// A scalar call with whatever does not change from chunk to chunk already worked out.
39///
40/// Build one with [`Recipe::new`] when the pipeline is built, then run it per chunk with
41/// [`call_prepared`](crate::scalar::call_prepared).
42#[derive(Debug)]
43pub struct Recipe {
44 /// The resolved function name, so a caller carries one thing rather than two.
45 name: String,
46 hoisted: Hoisted,
47}
48
49/// What a recipe managed to lift out of the per chunk path.
50#[derive(Debug)]
51pub(crate) enum Hoisted {
52 /// Nothing, either because this function has no prepare step or because the argument that would
53 /// drive it is not a literal. The kernel does what it always did, which for the functions below
54 /// means deciding per chunk and for everything else means there was never anything to decide.
55 Nothing,
56 /// A compiled `LIKE` pattern, already folded to lower case where the spelling folds case.
57 Like(scalar::Like),
58 /// A compiled regular expression, with the replacement taken apart and the options read.
59 ///
60 /// Boxed because it is several times the size of the other variants and one function node in
61 /// four hundred is a regular expression.
62 Regexp(Box<regexp::Call>),
63}
64
65impl Recipe {
66 /// What this call can work out from the arguments that are literals.
67 ///
68 /// `literals` holds one entry per argument, which is the value where the argument is a literal
69 /// and `None` where it is anything else. An argument that is a literal in the plan arrives as a
70 /// constant vector holding that value on every chunk, so what is read here is what the kernel
71 /// would have read per chunk.
72 #[must_use]
73 pub fn new(name: &str, literals: &[Option<Value>]) -> Self {
74 let hoisted = scalar::hoist(name, literals).unwrap_or(Hoisted::Nothing);
75 Self { name: name.to_owned(), hoisted }
76 }
77
78 /// A call with nothing hoisted, for a caller with no plan to read literals out of.
79 #[must_use]
80 pub fn plain(name: &str) -> Self {
81 Self { name: name.to_owned(), hoisted: Hoisted::Nothing }
82 }
83
84 /// The function this calls.
85 #[must_use]
86 pub fn name(&self) -> &str {
87 &self.name
88 }
89
90 /// Whether anything was lifted out of the per chunk path.
91 ///
92 /// The answer a query gives is the same either way, which is the whole point, so this is what a
93 /// test has to look at to say the lifting happened at all.
94 #[must_use]
95 pub fn hoists(&self) -> bool {
96 !matches!(self.hoisted, Hoisted::Nothing)
97 }
98
99 /// What was lifted, for the kernels that look.
100 pub(crate) fn hoisted(&self) -> &Hoisted {
101 &self.hoisted
102 }
103}
104
105/// A literal with the one row column a comparison reads it through already built.
106///
107/// The comparison loops read both sides through a slice, so the constant side is turned into a one
108/// row column and read at position zero. That column is what carries a string's four byte prefix,
109/// which is the thing the string comparison resolves almost every row from, and building it costs a
110/// couple of allocations. Doing that once for the query rather than once per chunk is what this is.
111///
112/// It matters least where a chunk is full and most where it is not. A second conjunct handed the
113/// eleven rows the first one kept pays the same setup as one handed two thousand, so on a selective
114/// filter the setup was a real part of the call rather than a rounding error on it.
115///
116/// It is also where the memos live for a comparison against a dictionary, which is the other thing
117/// that only works if it is built once for the query. The `peel` module has what those are and why
118/// they belong to one comparison node rather than to the kernel.
119#[derive(Debug)]
120pub struct Held {
121 value: Value,
122 single: Vector,
123 peel: Peel,
124 lookup: Lookup,
125}
126
127impl Held {
128 /// The one row column for `value` read as `ty`, or `None` for a type with no column layout.
129 ///
130 /// A nested type answers `None` and the comparison does what it always did, which is decide per
131 /// chunk and fall through to the row at a time path if it has to.
132 #[must_use]
133 pub fn of(ty: &LogicalType, value: &Value) -> Option<Self> {
134 Some(Self {
135 value: value.clone(),
136 single: single(ty, value)?,
137 peel: Peel::default(),
138 lookup: Lookup::default(),
139 })
140 }
141
142 /// Whether this was built for the side a kernel is about to read.
143 ///
144 /// The type and the value are both checked, which costs one comparison of two literals per
145 /// chunk against the allocations it saves. The caller that builds one of these takes it from the
146 /// step it is going to hand it back with, so the answer is yes, and the check is here so that a
147 /// caller which gets that wrong is slow rather than wrong.
148 pub(crate) fn matches(&self, ty: &LogicalType, value: &Value) -> bool {
149 self.single.logical_type() == ty && self.value == *value
150 }
151
152 /// The one row column.
153 pub(crate) fn single(&self) -> &Vector {
154 &self.single
155 }
156
157 /// The literal's bytes, when it is text, so a kernel can check it is the one it is comparing
158 /// against without building a value per chunk to check with.
159 pub(crate) fn text(&self) -> Option<&[u8]> {
160 match &self.value {
161 Value::Varchar(text) => Some(text.as_bytes()),
162 _ => None,
163 }
164 }
165
166 /// The per value memo for this comparison node.
167 pub(crate) fn peel(&self) -> &Peel {
168 &self.peel
169 }
170
171 /// Where this node's literal sits in the dictionary it is compared against, when the
172 /// dictionary knows its own order.
173 pub(crate) fn lookup(&self) -> &Lookup {
174 &self.lookup
175 }
176}
177
178impl Hoisted {
179 /// The compiled `LIKE`, or `None` when this call has none and the kernel should compile its own.
180 pub(crate) fn like(&self) -> Option<&scalar::Like> {
181 match self {
182 Self::Like(like) => Some(like),
183 _ => None,
184 }
185 }
186
187 /// The compiled regular expression, or `None` for the same reason.
188 pub(crate) fn regexp(&self) -> Option<®exp::Call> {
189 match self {
190 Self::Regexp(call) => Some(call),
191 _ => None,
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 use rudb_common::Value;
199
200 use super::Recipe;
201
202 fn text(spelling: &str) -> Option<Value> {
203 Some(Value::Varchar(spelling.to_owned()))
204 }
205
206 #[test]
207 fn a_like_against_a_literal_pattern_is_compiled_here() {
208 let recipe = Recipe::new("~~", &[None, text("%google%")]);
209 assert!(recipe.hoisted().like().is_some());
210 assert_eq!(recipe.name(), "~~");
211 }
212
213 #[test]
214 fn a_pattern_that_is_not_a_literal_is_left_to_the_chunk() {
215 // Legal SQL and vanishingly rare, and the point is that it still runs. The per chunk path
216 // reads the pattern off the vector, and where the vector is not constant either it falls
217 // all the way through to the row at a time loop and counts itself there.
218 assert!(Recipe::new("~~", &[None, None]).hoisted().like().is_none());
219 }
220
221 #[test]
222 fn a_regular_expression_against_a_literal_pattern_is_compiled_here() {
223 let recipe = Recipe::new("regexp_matches", &[None, text("^a.*z$")]);
224 assert!(recipe.hoisted().regexp().is_some());
225 }
226
227 #[test]
228 fn a_pattern_that_does_not_compile_is_left_to_the_chunk() {
229 // The one rule a prepare step has to keep. Compiling early must not move an error earlier,
230 // because a query that raises while a pipeline is being built raises before the rows it
231 // would have raised on, and in the case of a pattern under a `CASE` arm it raises on rows
232 // that were never going to reach it.
233 let recipe = Recipe::new("regexp_matches", &[None, text("a(")]);
234 assert!(recipe.hoisted().regexp().is_none());
235 }
236
237 #[test]
238 fn a_function_with_nothing_to_lift_lifts_nothing() {
239 let recipe = Recipe::new("upper", &[None]);
240 assert!(recipe.hoisted().like().is_none());
241 assert!(recipe.hoisted().regexp().is_none());
242 }
243
244 #[test]
245 fn a_plain_recipe_is_the_name_and_no_more() {
246 let recipe = Recipe::plain("~~");
247 assert_eq!(recipe.name(), "~~");
248 assert!(recipe.hoisted().like().is_none());
249 }
250}