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::regexp;
34use crate::scalar;
35use crate::shape::single;
36
37/// A scalar call with whatever does not change from chunk to chunk already worked out.
38///
39/// Build one with [`Recipe::new`] when the pipeline is built, then run it per chunk with
40/// [`call_prepared`](crate::scalar::call_prepared).
41#[derive(Debug)]
42pub struct Recipe {
43 /// The resolved function name, so a caller carries one thing rather than two.
44 name: String,
45 hoisted: Hoisted,
46}
47
48/// What a recipe managed to lift out of the per chunk path.
49#[derive(Debug)]
50pub(crate) enum Hoisted {
51 /// Nothing, either because this function has no prepare step or because the argument that would
52 /// drive it is not a literal. The kernel does what it always did, which for the functions below
53 /// means deciding per chunk and for everything else means there was never anything to decide.
54 Nothing,
55 /// A compiled `LIKE` pattern, already folded to lower case where the spelling folds case.
56 Like(scalar::Like),
57 /// A compiled regular expression, with the replacement taken apart and the options read.
58 ///
59 /// Boxed because it is several times the size of the other variants and one function node in
60 /// four hundred is a regular expression.
61 Regexp(Box<regexp::Call>),
62}
63
64impl Recipe {
65 /// What this call can work out from the arguments that are literals.
66 ///
67 /// `literals` holds one entry per argument, which is the value where the argument is a literal
68 /// and `None` where it is anything else. An argument that is a literal in the plan arrives as a
69 /// constant vector holding that value on every chunk, so what is read here is what the kernel
70 /// would have read per chunk.
71 #[must_use]
72 pub fn new(name: &str, literals: &[Option<Value>]) -> Self {
73 let hoisted = scalar::hoist(name, literals).unwrap_or(Hoisted::Nothing);
74 Self { name: name.to_owned(), hoisted }
75 }
76
77 /// A call with nothing hoisted, for a caller with no plan to read literals out of.
78 #[must_use]
79 pub fn plain(name: &str) -> Self {
80 Self { name: name.to_owned(), hoisted: Hoisted::Nothing }
81 }
82
83 /// The function this calls.
84 #[must_use]
85 pub fn name(&self) -> &str {
86 &self.name
87 }
88
89 /// Whether anything was lifted out of the per chunk path.
90 ///
91 /// The answer a query gives is the same either way, which is the whole point, so this is what a
92 /// test has to look at to say the lifting happened at all.
93 #[must_use]
94 pub fn hoists(&self) -> bool {
95 !matches!(self.hoisted, Hoisted::Nothing)
96 }
97
98 /// What was lifted, for the kernels that look.
99 pub(crate) fn hoisted(&self) -> &Hoisted {
100 &self.hoisted
101 }
102}
103
104/// A literal with the one row column a comparison reads it through already built.
105///
106/// The comparison loops read both sides through a slice, so the constant side is turned into a one
107/// row column and read at position zero. That column is what carries a string's four byte prefix,
108/// which is the thing the string comparison resolves almost every row from, and building it costs a
109/// couple of allocations. Doing that once for the query rather than once per chunk is what this is.
110///
111/// It matters least where a chunk is full and most where it is not. A second conjunct handed the
112/// eleven rows the first one kept pays the same setup as one handed two thousand, so on a selective
113/// filter the setup was a real part of the call rather than a rounding error on it.
114#[derive(Debug)]
115pub struct Held {
116 value: Value,
117 single: Vector,
118}
119
120impl Held {
121 /// The one row column for `value` read as `ty`, or `None` for a type with no column layout.
122 ///
123 /// A nested type answers `None` and the comparison does what it always did, which is decide per
124 /// chunk and fall through to the row at a time path if it has to.
125 #[must_use]
126 pub fn of(ty: &LogicalType, value: &Value) -> Option<Self> {
127 Some(Self { value: value.clone(), single: single(ty, value)? })
128 }
129
130 /// Whether this was built for the side a kernel is about to read.
131 ///
132 /// The type and the value are both checked, which costs one comparison of two literals per
133 /// chunk against the allocations it saves. The caller that builds one of these takes it from the
134 /// step it is going to hand it back with, so the answer is yes, and the check is here so that a
135 /// caller which gets that wrong is slow rather than wrong.
136 pub(crate) fn matches(&self, ty: &LogicalType, value: &Value) -> bool {
137 self.single.logical_type() == ty && self.value == *value
138 }
139
140 /// The one row column.
141 pub(crate) fn single(&self) -> &Vector {
142 &self.single
143 }
144}
145
146impl Hoisted {
147 /// The compiled `LIKE`, or `None` when this call has none and the kernel should compile its own.
148 pub(crate) fn like(&self) -> Option<&scalar::Like> {
149 match self {
150 Self::Like(like) => Some(like),
151 _ => None,
152 }
153 }
154
155 /// The compiled regular expression, or `None` for the same reason.
156 pub(crate) fn regexp(&self) -> Option<®exp::Call> {
157 match self {
158 Self::Regexp(call) => Some(call),
159 _ => None,
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use rudb_common::Value;
167
168 use super::Recipe;
169
170 fn text(spelling: &str) -> Option<Value> {
171 Some(Value::Varchar(spelling.to_owned()))
172 }
173
174 #[test]
175 fn a_like_against_a_literal_pattern_is_compiled_here() {
176 let recipe = Recipe::new("~~", &[None, text("%google%")]);
177 assert!(recipe.hoisted().like().is_some());
178 assert_eq!(recipe.name(), "~~");
179 }
180
181 #[test]
182 fn a_pattern_that_is_not_a_literal_is_left_to_the_chunk() {
183 // Legal SQL and vanishingly rare, and the point is that it still runs. The per chunk path
184 // reads the pattern off the vector, and where the vector is not constant either it falls
185 // all the way through to the row at a time loop and counts itself there.
186 assert!(Recipe::new("~~", &[None, None]).hoisted().like().is_none());
187 }
188
189 #[test]
190 fn a_regular_expression_against_a_literal_pattern_is_compiled_here() {
191 let recipe = Recipe::new("regexp_matches", &[None, text("^a.*z$")]);
192 assert!(recipe.hoisted().regexp().is_some());
193 }
194
195 #[test]
196 fn a_pattern_that_does_not_compile_is_left_to_the_chunk() {
197 // The one rule a prepare step has to keep. Compiling early must not move an error earlier,
198 // because a query that raises while a pipeline is being built raises before the rows it
199 // would have raised on, and in the case of a pattern under a `CASE` arm it raises on rows
200 // that were never going to reach it.
201 let recipe = Recipe::new("regexp_matches", &[None, text("a(")]);
202 assert!(recipe.hoisted().regexp().is_none());
203 }
204
205 #[test]
206 fn a_function_with_nothing_to_lift_lifts_nothing() {
207 let recipe = Recipe::new("upper", &[None]);
208 assert!(recipe.hoisted().like().is_none());
209 assert!(recipe.hoisted().regexp().is_none());
210 }
211
212 #[test]
213 fn a_plain_recipe_is_the_name_and_no_more() {
214 let recipe = Recipe::plain("~~");
215 assert_eq!(recipe.name(), "~~");
216 assert!(recipe.hoisted().like().is_none());
217 }
218}