spg_engine/eval/compiled.rs
1//! Compiled expressions — PG's ExprState idea (cut 30, extracted
2//! from `eval.rs`; v7.32 perf knife D / architecture v2 P1).
3//!
4//! Walk the tree ONCE per query, pre-resolve column positions and
5//! collation-fold decisions (both row-invariant), emit a flat
6//! post-order step program; per-row evaluation is a linear loop —
7//! no tree dispatch, no name resolution, no collation lookups.
8//! Anything the compiler doesn't model becomes a `Step::Subtree`
9//! that calls the interpreter for that node, so values AND error
10//! behaviour stay bit-for-bit with `eval_expr` (invariant I3).
11
12use alloc::format;
13use alloc::vec::Vec;
14
15use spg_sql::ast::{BinOp, ColumnName, Expr, Literal, UnOp};
16use spg_storage::{Row, Value};
17
18use super::{
19 EvalContext, EvalError, apply_binary, apply_unary, column_collation, composite_eq, eval_expr,
20 like_match_str, literal_to_value,
21};
22
23pub(crate) enum Step {
24 /// Pre-resolved column read (position into the row).
25 Column(usize),
26 /// Pre-converted literal.
27 Lit(Value<'static>),
28 /// Pops rhs then lhs, pushes the op result. Eager both-sides
29 /// evaluation — same as the interpreter for every op EXCEPT the two
30 /// boolean connectives, which take `Connective` below.
31 Binary(BinOp),
32 /// v7.39 (round 621) — COALESCE and NULLIF as steps on the borrowed
33 /// stack, because they are control flow wearing a function's name.
34 ///
35 /// Through `Step::Function` each had to return `Value<'static>`, which
36 /// forces a clone of a borrowed text argument; and the coalesce arm also
37 /// built a `Vec<DataType>` EVERY row for the numeric widening that
38 /// `COALESCE(1, 2.5)` needs. Measured: `count(coalesce(s,'z'))` at 3.00
39 /// allocations a row, `count(nullif(s,'row1'))` at 2.00, their chain at
40 /// 5.00 — all of it for values that end up borrowed from the row anyway.
41 ///
42 /// On the stack, the chosen argument is handed back AS IS. The widening
43 /// survives by inspection: only when the non-null arguments carry MIXED
44 /// numeric-family types does the step fall to the owned function arm,
45 /// which still does what it always did — same answers, paid only by the
46 /// mixed shapes that need it.
47 Coalesce {
48 n_args: usize,
49 },
50 NullIf,
51 /// v7.39 (round 717) — GREATEST / LEAST. Through `Step::Function`
52 /// every row re-ran `apply_function_lower`'s name dispatch, and
53 /// "least" lives in the crowded five-letter probe chain — measured
54 /// +6 ms over "greatest" on the same 500k scan REGARDLESS of which
55 /// argument wins (the take-always and take-never shapes cost the
56 /// same, so the branch was never the tax; the name was). Uniform
57 /// same-type arguments compare in place off the stack; the mixed /
58 /// coercing / xid / MySQL-NULL shapes fall to the function arm,
59 /// which still does what it always did.
60 Extremum {
61 n_args: usize,
62 max: bool,
63 },
64 /// v7.39 (round 621) — `AND` / `OR`, short-circuiting.
65 ///
66 /// The VM is a stack machine, so both operands were pushed before the
67 /// `Binary` step could look at either: `WHERE x <> 0 AND 1/x > 0` divided
68 /// by zero on exactly the rows the guard exists to exclude. The
69 /// interpreter's arm was fixed first and this path still failed, which is
70 /// the second time a connective has been fixed in one evaluator and not
71 /// the other (round 346's MySQL reading was the first — its comment is
72 /// three screens down).
73 ///
74 /// Rather than turn the hottest loop in the engine into an indexed one
75 /// with jumps, the right operand is its OWN program, run only when the
76 /// left does not decide. Nesting depth is the AND-nesting depth of the
77 /// predicate.
78 Connective {
79 op: BinOp,
80 rhs: Vec<Step>,
81 },
82 /// Comparison whose operands referenced a CaseInsensitive
83 /// column: ASCII-fold Text operands first (decided at compile
84 /// time; the interpreter re-decides per row).
85 BinaryCi(BinOp),
86 Unary(UnOp),
87 IsNull {
88 negated: bool,
89 },
90 /// v7.39 (round 488) — the verdict of an all-`%` LIKE pattern:
91 /// matches every non-NULL operand, and is NULL for a NULL one.
92 ///
93 /// v7.36 collapsed this shape into `IsNull { negated: !negated }`,
94 /// which answers a three-valued question two-valued. `NULL NOT LIKE
95 /// '%'` came out TRUE where PG18 says NULL, so `WHERE s NOT LIKE '%'`
96 /// SELECTED the NULL row (PG selects nothing), and `SELECT s LIKE '%'`
97 /// printed `false` where PG prints NULL. Same collapse, three-valued.
98 AnyTextMatch {
99 negated: bool,
100 },
101 /// v7.32 (architecture v2, P1) — `needle [NOT] IN (literals…)`.
102 /// The membership SET is a COMPILE PRODUCT, not a runtime cache:
103 /// it lives in the step, so there is no "forgot to pass the
104 /// memo" failure mode (the round-25 18.7 s accident is now
105 /// unconstructable — see v7.32-executor-architecture-design.md
106 /// invariant I2). The needle is the preceding sub-program; this
107 /// step pops it. `fallback` is the whole InList node, used only
108 /// when the runtime needle family doesn't match the set
109 /// (e.g. Float needle vs Int set) — same escape the interpreter
110 /// takes, evaluated cold.
111 InSet {
112 set: crate::memoize::InListSet,
113 has_null: bool,
114 negated: bool,
115 fallback: Expr,
116 },
117 /// v7.32 (P1) — `text [NOT] [I]LIKE '<literal pattern>'`. The
118 /// pattern (and its lowercased form for ILIKE) is compiled once;
119 /// the step pops the text operand.
120 Like {
121 pattern: alloc::vec::Vec<char>,
122 negated: bool,
123 case_insensitive: bool,
124 },
125 /// v7.39 (perf — like_filter tied 1.04×) — unanchored substring
126 /// LIKE: `%[k×_]literal[m×_]%`. Instead of the generic matcher's
127 /// try-every-suffix backtracking (per-position `_`+literal walk),
128 /// scan with `str::find` (two-way, sublinear) over the literal and
129 /// verify the `k` leading / `m` trailing wildcard chars have room.
130 /// v7.39 (round 594) — `text ~ '<literal pattern>'` and its `~*` /
131 /// `regexp_like(...)` spellings. `regexp_like` parsed the pattern into a
132 /// tree for EVERY row: 500k rows cost 350 ms against PG18's 34.5, the
133 /// same 10x whichever way the match was spelled. The pattern is a
134 /// compile product here, exactly as `Step::Like`'s is — PG solves the
135 /// same problem with a cache; a compile product cannot be forgotten.
136 /// v7.39 (round 597) — `<expr> <op> ANY/ALL (<constant array>)`. The
137 /// array is a compile PRODUCT: it used to be rebuilt for every row, and
138 /// `WHERE id = ANY (ARRAY[1..10])` cost 268 ms over 500k rows against
139 /// PG18's 8.3 — 494 at twenty elements — where the equivalent
140 /// `id IN (1..10)` took 2.3. A non-constant right-hand side keeps the
141 /// interpreter, which has to rebuild it: there it really can differ.
142 AnyAll {
143 op: spg_sql::ast::BinOp,
144 is_any: bool,
145 arr: Value<'static>,
146 },
147 /// v7.39 (round 595) — `EXTRACT(<field> FROM <expr>)`. The field is a
148 /// keyword, not a value, so it rides in the step; the source is the
149 /// preceding sub-program and this pops it. `fallback` carries the whole
150 /// node because the extraction's error wording names the source's
151 /// declared type, which only the node knows.
152 Extract {
153 field: spg_sql::ast::ExtractField,
154 fallback: Expr,
155 },
156 Regex {
157 re: crate::eval::CompiledRe,
158 /// The whole call, for an operand that is not text: the interpreter
159 /// owns whatever coercion or error that is, and this step must not
160 /// invent one. Same escape `Step::InSet` takes, evaluated cold.
161 fallback: Expr,
162 },
163 LikeSubstring {
164 needle: alloc::string::String,
165 k_before: usize,
166 m_after: usize,
167 negated: bool,
168 case_insensitive: bool,
169 },
170 /// v7.36 (perf — mailrs Ask 1) — pure scalar function call
171 /// (LENGTH, COALESCE, UPPER, etc.) on already-pushed args.
172 /// Pops `n_args` values, calls `apply_function(name, args, ctx)`,
173 /// pushes the result. Replaces the Subtree fallback for the
174 /// "function over bound columns" shape that aggregate arg paths
175 /// like `SUM(LENGTH(text_body))` and `MAX(COALESCE(col, ''))`
176 /// otherwise force the row-materialise eval path. Only the
177 /// `fully_compilable` whitelist (PURE scalars — no NOW / RANDOM
178 /// / sequence accessors) is emitted; everything else stays on
179 /// `Step::Subtree`.
180 /// `name_lower` is pre-lowercased at compile time so the per-
181 /// row dispatch in `apply_function` skips an allocation on
182 /// every input row.
183 Function {
184 name_lower: alloc::string::String,
185 n_args: usize,
186 },
187 /// v7.36 (perf — mailrs Ask 1 SUM(LENGTH(text_body)) zero-copy)
188 /// — `LENGTH(<column>)` / `CHAR_LENGTH(<column>)` /
189 /// `CHARACTER_LENGTH(<column>)` over a bound column. Reads the
190 /// cell by reference, computes the char length WITHOUT cloning
191 /// the underlying `String` — the 1 KB text bodies in
192 /// `user_storage_usage` otherwise pay 25 k × 1 KB heap allocs
193 /// per query just to push a `Value::Text` onto the stack so the
194 /// next Step pops it and asks `s.len()`.
195 ColumnLength {
196 pos: usize,
197 },
198 /// v7.36 — `OCTET_LENGTH(<column>)` — byte count, regardless of
199 /// encoding. Even simpler than `ColumnLength` (no ASCII probe).
200 ColumnOctetLength {
201 pos: usize,
202 },
203 /// v7.36 — `CAST(<expr> AS <ty>)` over an already-pushed value.
204 /// Pure / context-free conversion goes through the same
205 /// `cast_value` dispatcher the interpreter uses.
206 Cast {
207 target: spg_sql::ast::CastTarget,
208 },
209 /// v7.39 (round 722) — a NAMED cast whose name resolved at COMPILE
210 /// time (`::NUMERIC`, `::REAL`, `numeric(10,2)` — the
211 /// `plain_named_target` table). The blanket Named -> Subtree rule
212 /// sent these to the interpreter — worse, it made the whole
213 /// aggregate argument non-compilable, so `count(id::NUMERIC)` fell
214 /// off the round-716 fused parallel lane entirely. The name rides
215 /// along for error wording only.
216 CastPlain {
217 dt: spg_storage::DataType,
218 name: alloc::string::String,
219 },
220 /// v7.37.5-A2b — `CASE [operand] WHEN x THEN y … ELSE z END`.
221 /// Each `(when, then)` branch and the optional `else` is a
222 /// pre-compiled sub-program; the executor short-circuits on the
223 /// first matching WHEN. Compiles only when **every** sub-program
224 /// is itself `fully_compilable` (so the Case never falls back to
225 /// a Subtree that would force a row materialise — profile-guided
226 /// fix for Track A `COUNT(DISTINCT CASE WHEN ...)` aggregates).
227 /// Searched form has `operand=None` and treats each WHEN as a
228 /// Bool predicate; simple form has `operand=Some(prog)` and
229 /// compares the operand value with each WHEN via `BinOp::Eq`.
230 Case {
231 operand: Option<CompiledExpr>,
232 branches: alloc::vec::Vec<(CompiledExpr, CompiledExpr)>,
233 else_branch: Option<CompiledExpr>,
234 },
235 /// v7.38 (read01) — widen the top-of-stack value to a statically
236 /// resolved PG common type (e.g. a `CASE` whose branches mix integer and
237 /// numeric resolves to numeric). Resolved once at compile time from the
238 /// branch expressions' types, so the per-row cost is a single
239 /// scale-preserving coercion, not a describe. See
240 /// [`crate::eval::widen_value_to`].
241 CoerceCommon(spg_storage::DataType),
242 /// Fallback: interpret this subtree with eval_expr.
243 Subtree(Expr),
244}
245
246pub(crate) struct CompiledExpr {
247 steps: Vec<Step>,
248 /// Which fast predicate shape this program is — settled once, here,
249 /// instead of re-derived per row. See [`PredShape`].
250 pred_shape: PredShape,
251}
252
253/// v7.39 (round 486) — the shape of a compiled predicate, decided at
254/// compile time.
255///
256/// Round 482 added a `<column> <cmp> <literal>` fast path and this round
257/// added `<column> [NOT] IN (<literals>)`. Both were slice pattern-matches
258/// run PER ROW, so a program that is neither paid for every probe in the
259/// list: adding the second one cost `like_filter` — a shape with no `IN`
260/// anywhere in it — 4.5 %, measured against the previous commit on the same
261/// machine minutes apart. A program's shape does not change between its
262/// rows, so it is settled once and the row loop reads one discriminant.
263#[derive(Clone, Copy, PartialEq, Eq, Debug)]
264pub(crate) enum PredShape {
265 Other,
266 /// r1021 — an integer-only arithmetic predicate, run without building a
267 /// single `Value`. See [`CompiledExpr::is_int_arith_pred`].
268 IntArith,
269 ColumnCmpLit,
270 ColumnInSet,
271 ColumnLike,
272}
273
274impl CompiledExpr {
275 /// v7.36 (perf — mailrs Phase 1, user_storage_usage hot loop) —
276 /// shape inspector for the aggregate's tight inner. Returns
277 /// `Some(pos)` iff this compiled expression is exactly the
278 /// single step `ColumnLength { pos }` — i.e. `LENGTH(<column>)`
279 /// on a bound text column with no surrounding work.
280 /// r1021 — the deepest an integer lane will go. A predicate needing
281 /// more stack than this falls back; measured shapes use two or three.
282 const INT_LANE_DEPTH: usize = 8;
283
284 /// r1021 — is this predicate built only from integer columns, integer
285 /// literals and integer arithmetic, ending in one comparison?
286 ///
287 /// Round 482 traced the per-row predicate cost to `Value` churn and
288 /// answered it with ONE hard-coded shape, `<column> <cmp> <literal>`.
289 /// Anything with arithmetic in it — `id % 3 = 0`, the bucketing and
290 /// parity predicates real schemas are full of — still builds and
291 /// destroys a `Value` per step. Profiled (2026-08-14, see
292 /// `docs/PERF_FILTERED_THEN_ORDER_2026-08-14.md`):
293 /// `drop_glue<Value>` is the LARGEST leaf on `WHERE id % 3 = 0`, ahead
294 /// of the modulo it carries, and 22x heavier per rep than on the shape
295 /// that skips the step machine.
296 ///
297 /// So this recognises a CLASS rather than a shape. Structural only —
298 /// no column types are consulted here — because every value the lane
299 /// cannot handle makes it fall back at run time instead of guessing.
300 fn is_int_arith_pred(&self) -> bool {
301 let mut depth = 0usize;
302 let mut comparisons = 0usize;
303 for (i, step) in self.steps.iter().enumerate() {
304 match step {
305 Step::Column(_) => depth += 1,
306 Step::Lit(v) => {
307 if !matches!(v, Value::Int(_) | Value::BigInt(_)) {
308 return false;
309 }
310 depth += 1;
311 }
312 Step::Binary(op) => {
313 if depth < 2 {
314 return false;
315 }
316 depth -= 1;
317 if is_int_comparison(*op) {
318 comparisons += 1;
319 // The comparison is the answer, so it ends the
320 // program; a later step would consume a bool the
321 // lane does not carry.
322 if i + 1 != self.steps.len() {
323 return false;
324 }
325 } else if !is_int_arithmetic(*op) {
326 return false;
327 }
328 }
329 _ => return false,
330 }
331 if depth > Self::INT_LANE_DEPTH {
332 return false;
333 }
334 }
335 depth == 1 && comparisons == 1
336 }
337
338 /// r1021 — run an [`Self::is_int_arith_pred`] program over `i64`s.
339 ///
340 /// `None` means "this row is not for the lane" and the caller runs the
341 /// ordinary machine. Every case that could answer differently from the
342 /// interpreter takes that exit rather than deciding for itself: a NULL
343 /// or non-integer cell, a division by zero, an overflow, and a result
344 /// that would not fit the width the operands imply. The lane therefore
345 /// cannot change a single answer — it can only reach the same one
346 /// without a heap type in the middle.
347 ///
348 /// Width follows PG: `int4 op int4` stays `int4` and overflowing it is
349 /// an error, so a 32-bit result that leaves 32-bit range hands the row
350 /// back and the interpreter raises exactly as before. Mixed widths
351 /// widen to 64-bit, and `smallint` is simply not admitted.
352 fn eval_int_arith_pred(&self, row: &Row<'static>) -> Option<bool> {
353 let mut vals = [0i64; Self::INT_LANE_DEPTH];
354 let mut narrow = [false; Self::INT_LANE_DEPTH];
355 let mut n = 0usize;
356 for step in &self.steps {
357 match step {
358 Step::Column(pos) => {
359 let (v, is32) = int_operand(row.values.get(*pos)?)?;
360 vals[n] = v;
361 narrow[n] = is32;
362 n += 1;
363 }
364 Step::Lit(lit) => {
365 let (v, is32) = int_operand(lit)?;
366 vals[n] = v;
367 narrow[n] = is32;
368 n += 1;
369 }
370 Step::Binary(op) => {
371 let (rhs, rhs32) = (vals[n - 1], narrow[n - 1]);
372 let (lhs, lhs32) = (vals[n - 2], narrow[n - 2]);
373 n -= 2;
374 if is_int_comparison(*op) {
375 return Some(match op {
376 BinOp::Eq => lhs == rhs,
377 BinOp::NotEq => lhs != rhs,
378 BinOp::Lt => lhs < rhs,
379 BinOp::LtEq => lhs <= rhs,
380 BinOp::Gt => lhs > rhs,
381 _ => lhs >= rhs,
382 });
383 }
384 let out = match op {
385 BinOp::Add => lhs.checked_add(rhs)?,
386 BinOp::Sub => lhs.checked_sub(rhs)?,
387 BinOp::Mul => lhs.checked_mul(rhs)?,
388 BinOp::Div => lhs.checked_div(rhs)?,
389 _ => lhs.checked_rem(rhs)?,
390 };
391 let out32 = lhs32 && rhs32;
392 if out32 && i32::try_from(out).is_err() {
393 return None;
394 }
395 vals[n] = out;
396 narrow[n] = out32;
397 n += 1;
398 }
399 _ => return None,
400 }
401 }
402 None
403 }
404
405 /// v7.39 (round 482) — is this exactly `<column> <cmp> <literal>`?
406 ///
407 /// Rounds 478-481 traced the per-row predicate cost to `Value` churn:
408 /// three steps a row (Column, Lit, Binary) means three `Value`s built
409 /// and destroyed, and `drop_glue<Value>` is an out-of-line call that
410 /// switches on the discriminant even when the value carries no heap.
411 /// Round 481's counter ruled out leftovers on the stack — the churn is
412 /// the VM's ordinary operands.
413 ///
414 /// This shape needs none of them: both operands can be read by
415 /// reference. It covers `g = 5` and `s = '…'`; `LIKE` is its own AST
416 /// node rather than a `BinOp`, so it compiles to a different step and
417 /// is NOT covered here — measured, not assumed.
418 ///
419 /// `BinaryCi` is deliberately not matched: it folds its operands
420 /// first, which is a different comparison. Nor is the mirrored
421 /// `<literal> <cmp> <column>` — flipping the operator is a separate
422 /// judgement and this returns None so it takes the general path.
423 pub(crate) fn as_column_cmp_literal(&self) -> Option<(usize, BinOp, &Value<'static>)> {
424 let [Step::Column(pos), Step::Lit(lit), Step::Binary(op)] = &self.steps[..] else {
425 return None;
426 };
427 if !matches!(
428 op,
429 BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
430 ) {
431 return None;
432 }
433 Some((*pos, *op, lit))
434 }
435
436 /// v7.39 (round 486) — the sibling shape `<column> [NOT] IN (<literals>)`.
437 ///
438 /// `big_in` is the read panel's worst shape and compiles to exactly two
439 /// steps, `Column` then `InSet`. The round-482 fast path does not cover
440 /// it (three steps, a `Binary`), so it runs the general VM: a `Value`
441 /// built from the cell, popped, and a `Value::Bool` built and popped
442 /// again. Its profile put `drop_glue<Value>` at 20 % and the VM loop at
443 /// 27 %. The set lookup itself wants nothing but a reference to the
444 /// cell.
445 pub(crate) fn as_column_in_set(
446 &self,
447 ) -> Option<(usize, &crate::memoize::InListSet, bool, bool)> {
448 let [
449 Step::Column(pos),
450 Step::InSet {
451 set,
452 has_null,
453 negated,
454 ..
455 },
456 ] = &self.steps[..]
457 else {
458 return None;
459 };
460 Some((*pos, set, *has_null, *negated))
461 }
462
463 /// v7.39 (round 488) — the third two-step shape: `<column> [NOT]
464 /// [I]LIKE '<literal>'`, in either the general matcher's form or the
465 /// unanchored-substring form round 484 added.
466 ///
467 /// `like_filter` is the read panel's worst shape. Rounds 482 and 486
468 /// covered its two siblings; this one still ran the general VM, which
469 /// pushes the cell as a `Value` and pops it again for a matcher that
470 /// only ever wanted a `&str`.
471 pub(crate) fn as_column_like(&self) -> Option<(usize, &Step)> {
472 let [
473 Step::Column(pos),
474 step @ (Step::Like { .. } | Step::LikeSubstring { .. }),
475 ] = &self.steps[..]
476 else {
477 return None;
478 };
479 Some((*pos, step))
480 }
481
482 pub(crate) fn as_single_column_length(&self) -> Option<usize> {
483 if self.steps.len() == 1
484 && let Step::ColumnLength { pos } = &self.steps[0]
485 {
486 Some(*pos)
487 } else {
488 None
489 }
490 }
491}
492
493/// Column-position resolution at compile time. Mirrors the happy
494/// layers of `resolve_column`; ANY case that would reach an error
495/// path, an ambiguity, or a miss returns None so the node falls
496/// back to the interpreter (identical runtime error / NULL
497/// semantics).
498///
499/// v7.37.16 — pub(crate): the aggregate bind-once fast path
500/// (aggregate.rs `col_pos`) uses this as its resolver so bare-name
501/// group/arg columns bind exactly like compiled-WHERE columns do.
502/// v7.39 (round 693) — does this comparison operand carry a collation the
503/// VM cannot perform?
504///
505/// Deliberately a COMPILE-time question. The answer is the same for every
506/// row of the scan, and the alternative — asking per row inside `compare` —
507/// puts a lookup on the hottest path in the engine.
508/// v7.39 (round 704) — does this comparison pair an unknown string literal
509/// with a numeric-family operand whose type the literal will not parse as?
510/// Compile-time twin of the eval Binary arm's error rewrite; see the bail
511/// site for why the shape cannot stay on the VM.
512fn unparseable_numeric_literal_cmp(lhs: &Expr, rhs: &Expr, ctx: &EvalContext<'_>) -> bool {
513 let check = |lit: &Expr, other: &Expr| -> bool {
514 let Expr::Literal(spg_sql::ast::Literal::String(text)) = lit else {
515 return false;
516 };
517 let Some(desc) = crate::describe::describe_expr(other, ctx.columns) else {
518 return false;
519 };
520 if !matches!(
521 desc.ty,
522 spg_storage::DataType::SmallInt
523 | spg_storage::DataType::Int
524 | spg_storage::DataType::BigInt
525 | spg_storage::DataType::Float
526 | spg_storage::DataType::Real
527 | spg_storage::DataType::Numeric { .. }
528 ) {
529 return false;
530 }
531 crate::conversions::coerce_value(spg_storage::Value::text(text.as_str()), desc.ty, "", 0)
532 .is_err()
533 };
534 check(lhs, rhs) || check(rhs, lhs)
535}
536
537fn operand_declares_a_collation(e: &Expr, ctx: &EvalContext<'_>) -> bool {
538 let derived = crate::collate_derive::derive(e, &|c: &ColumnName| {
539 let pos = crate::eval::find_column_pos(c, ctx)?;
540 ctx.columns.get(pos)?.collation_name.clone()
541 });
542 // A conflict has to leave the VM too — the tree evaluator is where the
543 // error is raised, with PG's own sentence.
544 derived.conflict().is_some()
545 || derived
546 .name()
547 .is_some_and(|n| crate::collate::is_supported(n))
548}
549
550pub(crate) fn compile_column_pos(c: &ColumnName, ctx: &EvalContext<'_>) -> Option<usize> {
551 if let Some(q) = &c.qualifier {
552 if let Some(pos) = ctx
553 .columns
554 .iter()
555 .position(|s| composite_eq(&s.name, q, &c.name))
556 {
557 return Some(pos);
558 }
559 // resolve_column's error layers live behind this point:
560 // composites under the qualifier exist (ColumnNotFound) or
561 // the qualifier is unknown (UnknownQualifier) — interpret.
562 let prefix_exists = ctx.columns.iter().any(|s| {
563 s.name.starts_with(q.as_str()) && s.name.as_bytes().get(q.len()) == Some(&b'.')
564 });
565 if prefix_exists {
566 return None;
567 }
568 match ctx.table_alias {
569 // Alias-accepted single-table reference: fall through
570 // to the bare layers (the inner-subquery hot shape).
571 Some(a) if a == q => {}
572 _ => return None,
573 }
574 }
575 if let Some(pos) = ctx.columns.iter().position(|s| s.name == c.name) {
576 return Some(pos);
577 }
578 let mut matches = ctx.columns.iter().enumerate().filter(|(_, s)| {
579 s.name.len() > c.name.len()
580 && s.name.ends_with(c.name.as_str())
581 && s.name.as_bytes()[s.name.len() - c.name.len() - 1] == b'.'
582 });
583 let first = matches.next();
584 if matches.next().is_some() {
585 return None; // ambiguous — interpreter owns the error text
586 }
587 first.map(|(i, _)| i)
588}
589
590/// v7.39 (round 621) — can evaluating this raise at RUN time?
591///
592/// The errors a short circuit spares are the run-time ones: a division, an
593/// overflow, a cast that will not parse, a function that refuses its input.
594/// A type mismatch is not among them — PG raises those while ANALYSING, so it
595/// raises them whether or not the operand would have been evaluated, and so
596/// does SPG. That is why a predicate built only from columns, literals,
597/// comparisons and the boolean shapes over them needs no short circuit: there
598/// is nothing for it to spare.
599///
600/// Unrecognised shapes answer `true`, so a new kind of expression short
601/// circuits (correct, slightly slower) rather than silently not.
602fn can_raise_at_run_time(e: &Expr) -> bool {
603 match e {
604 Expr::Literal(_) | Expr::Column(_) => false,
605 Expr::Binary { op, lhs, rhs } => {
606 !matches!(
607 op,
608 BinOp::Eq
609 | BinOp::NotEq
610 | BinOp::Lt
611 | BinOp::LtEq
612 | BinOp::Gt
613 | BinOp::GtEq
614 | BinOp::And
615 | BinOp::Or
616 ) || can_raise_at_run_time(lhs)
617 || can_raise_at_run_time(rhs)
618 }
619 Expr::Unary { op, expr } => !matches!(op, UnOp::Not) || can_raise_at_run_time(expr),
620 Expr::IsNull { expr, .. } | Expr::BoolTest { expr, .. } => can_raise_at_run_time(expr),
621 Expr::Like { expr, pattern, .. } => {
622 can_raise_at_run_time(expr) || can_raise_at_run_time(pattern)
623 }
624 Expr::InList { expr, list, .. } => {
625 can_raise_at_run_time(expr) || list.iter().any(can_raise_at_run_time)
626 }
627 _ => true,
628 }
629}
630
631fn compile_into(e: &Expr, ctx: &EvalContext<'_>, steps: &mut Vec<Step>) {
632 match e {
633 Expr::Literal(l) => steps.push(Step::Lit(literal_to_value(l))),
634 Expr::Column(c) => match compile_column_pos(c, ctx) {
635 // v7.39 (read01 round 56) — a COMPOSITE column must not compile to
636 // a raw `Step::Column`: that loads the stored JSON straight off the
637 // row and skips the rehydration into `Value::Composite` that
638 // `resolve_column` does. `p = ROW(2,'b')::pt` in a WHERE then
639 // compared Json against Composite and errored, while the same
640 // predicate in a projection worked. Route it through eval instead.
641 // The check is COMPILE-time, so the hot column path pays nothing.
642 Some(pos)
643 if ctx
644 .columns
645 .get(pos)
646 .is_some_and(|sc| sc.user_composite_type.is_some()) =>
647 {
648 steps.push(Step::Subtree(e.clone()));
649 }
650 Some(pos) => steps.push(Step::Column(pos)),
651 None => steps.push(Step::Subtree(e.clone())),
652 },
653 Expr::Binary { lhs, op, rhs } => {
654 // v7.39 (round 383) — the MySQL bitwise operators are UNSIGNED
655 // 64-bit (`~ & | ^ << >>`); the VM's Step::Binary calls the
656 // dialect-blind apply_binary, so route them to the interpreter,
657 // which has the dialect (eval.rs `mysql_bitwise`). `<< >>` share
658 // the inet-containment BinOps — the interpreter still keeps the
659 // inet meaning for non-numeric operands.
660 if ctx.mysql_dialect
661 && matches!(
662 op,
663 BinOp::BitAnd
664 | BinOp::BitOr
665 | BinOp::BitXor
666 | BinOp::InetContainedBy
667 | BinOp::InetContains
668 )
669 {
670 steps.push(Step::Subtree(e.clone()));
671 return;
672 }
673 // v7.39 (round 407) — MySQL's logical `XOR` reads both sides as
674 // truth values, which the VM's dialect-blind apply_binary (no
675 // LogicalXor arm) cannot do. Route to the interpreter, whose
676 // eval_expr arm handles the connective (eval.rs
677 // `eval_mysql_connective`).
678 if ctx.mysql_dialect && matches!(op, BinOp::LogicalXor) {
679 steps.push(Step::Subtree(e.clone()));
680 return;
681 }
682 // v7.39 (round 402) — an arithmetic op on a SET / inline-ENUM
683 // column reads the column numerically (bitmask / 1-based
684 // ordinal), which the VM's value-level Add cannot see (it has the
685 // text). Route to the interpreter, which folds it (eval.rs
686 // resolve `collation_fold_for_compare`).
687 if ctx.mysql_dialect
688 && matches!(
689 op,
690 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod
691 )
692 && (crate::eval::expr_set_variants(lhs, ctx.columns).is_some()
693 || crate::eval::expr_set_variants(rhs, ctx.columns).is_some()
694 || crate::eval::expr_inline_enum_variants(lhs, ctx.columns).is_some()
695 || crate::eval::expr_inline_enum_variants(rhs, ctx.columns).is_some())
696 {
697 steps.push(Step::Subtree(e.clone()));
698 return;
699 }
700 // v7.39 (round 621) — the boolean connectives short-circuit, so
701 // the right operand compiles to its own program. The shapes whose
702 // right operand is a literal go to the interpreter instead: those
703 // carry PG's analysis-time half (a non-boolean literal is refused
704 // even when the short circuit would not reach it, and an unknown
705 // string literal is resolved), which is decided there and is not
706 // worth a second implementation for how rare they are in a
707 // compiled predicate.
708 if matches!(op, BinOp::And | BinOp::Or) {
709 if matches!(rhs.as_ref(), Expr::Literal(_)) {
710 steps.push(Step::Subtree(e.clone()));
711 return;
712 }
713 // A right operand that cannot fail has nothing to be spared,
714 // so it keeps the eager step and its inline cost. `WHERE g
715 // BETWEEN 10 AND 20` is `g >= 10 AND g <= 20`, the commonest
716 // conjunctive predicate there is, and paying a nested program
717 // per row for it measured +42% to +60% on the panel — a real
718 // regression, reproduced, for a short circuit that can never
719 // change an answer.
720 if !can_raise_at_run_time(rhs) {
721 compile_into(lhs, ctx, steps);
722 compile_into(rhs, ctx, steps);
723 steps.push(Step::Binary(*op));
724 return;
725 }
726 compile_into(lhs, ctx, steps);
727 let mut rhs_steps = Vec::new();
728 compile_into(rhs, ctx, &mut rhs_steps);
729 steps.push(Step::Connective {
730 op: *op,
731 rhs: rhs_steps,
732 });
733 return;
734 }
735 let cmp = matches!(
736 op,
737 BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
738 );
739 // v7.39 (enum order knife) — an enum-witnessed comparison must
740 // order by catalog member order; the VM's value-level compare
741 // cannot. Fall back to the tree evaluator for this subtree
742 // (compile-time check, zero cost when the catalog has no
743 // enum types).
744 if cmp
745 && ctx.catalog.is_some_and(|cat| !cat.enum_types().is_empty())
746 && (crate::eval::expr_enum_labels(lhs, ctx.columns, ctx.catalog).is_some()
747 || crate::eval::expr_enum_labels(rhs, ctx.columns, ctx.catalog).is_some())
748 {
749 steps.push(Step::Subtree(e.clone()));
750 return;
751 }
752 // v7.39 (round 693) — and the same move for a declared
753 // collation, which is the shape F36 had left: `loc BETWEEN 'a'
754 // AND 'd'` returns a different ROW SET under en_US.utf8 than
755 // under byte order.
756 //
757 // Compile-time, like its enum neighbour, and for the better of
758 // the two reasons. `binop::compare` is the dominant cost of a
759 // scan — its own comment measures 35.6 % of self time on
760 // `g = 5` — so a per-row collation lookup there would have to
761 // earn its place against a bench. Deciding once, while the
762 // predicate compiles, costs the scan nothing at all: a column
763 // that declares nothing never leaves the VM.
764 //
765 // Only the ORDERING operators. Measured on PG18, `=`, `<>`,
766 // LIKE, IN and count(DISTINCT …) all give byte-equality's
767 // answer under a deterministic collation.
768 if matches!(op, BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq)
769 && operand_declares_a_collation(lhs, ctx) | operand_declares_a_collation(rhs, ctx)
770 {
771 steps.push(Step::Subtree(e.clone()));
772 return;
773 }
774 // v7.39 (round 704) — an UNKNOWN string literal against a
775 // numeric-family operand that will NOT parse as its type. PG's
776 // error for `WHERE i = 'abc'` is the input function's
777 // (`invalid input syntax for type integer: "abc"`); the VM's
778 // value-level compare can only say "operator does not exist",
779 // so this shape leaves for the tree evaluator, whose Binary
780 // arm has the Exprs and rewrites the error. Compile-time and
781 // failure-only: a literal that parses stays on the VM path
782 // and costs nothing.
783 if cmp && unparseable_numeric_literal_cmp(lhs, rhs, ctx) {
784 steps.push(Step::Subtree(e.clone()));
785 return;
786 }
787 compile_into(lhs, ctx, steps);
788 compile_into(rhs, ctx, steps);
789 let ci = cmp
790 && (matches!(
791 column_collation(lhs, ctx),
792 Some(spg_storage::Collation::CaseInsensitive)
793 ) || matches!(
794 column_collation(rhs, ctx),
795 Some(spg_storage::Collation::CaseInsensitive)
796 ));
797 // v7.39 (round 364, M4 P2) — a MySQL session folds every text
798 // comparison, so it needs the CI step too (the step chooses
799 // the accent-aware fold at run time).
800 let ci = ci || (cmp && super::resolve::mysql_text_fold_applies(lhs, rhs, ctx));
801 steps.push(if ci {
802 Step::BinaryCi(*op)
803 } else {
804 Step::Binary(*op)
805 });
806 }
807 Expr::Unary { op, expr } => {
808 // v7.39 (round 383) — MySQL `~x` is the UNSIGNED 64-bit
809 // complement; route to the interpreter (eval.rs `mysql_bit_not`)
810 // since Step::Unary calls the dialect-blind apply_unary.
811 if ctx.mysql_dialect && matches!(op, UnOp::BitNot) {
812 steps.push(Step::Subtree(e.clone()));
813 return;
814 }
815 compile_into(expr, ctx, steps);
816 steps.push(Step::Unary(*op));
817 }
818 Expr::IsNull { expr, negated } => {
819 compile_into(expr, ctx, steps);
820 steps.push(Step::IsNull { negated: *negated });
821 }
822 Expr::InList {
823 expr,
824 list,
825 negated,
826 } => {
827 // v7.39 (round 364, M4 P2) — a MySQL session folds text before
828 // the membership test; the set-based compiled path compares
829 // raw. Route it to the interpreter, which folds (eval.rs
830 // `eval_in_list_arm`). The perf-critical InSet path is PG-only.
831 if ctx.mysql_dialect {
832 steps.push(Step::Subtree(e.clone()));
833 return;
834 }
835 // I2: the set is built at compile time. The gate
836 // (`fully_compilable`) guarantees we only reach here
837 // when the list builds a set and the needle compiles —
838 // but keep the Subtree fallback for defence in depth.
839 match crate::build_in_list_set(list) {
840 Some(entry) if fully_compilable(expr) => {
841 compile_into(expr, ctx, steps);
842 steps.push(Step::InSet {
843 set: entry.set,
844 has_null: entry.has_null,
845 negated: *negated,
846 fallback: e.clone(),
847 });
848 }
849 _ => steps.push(Step::Subtree(e.clone())),
850 }
851 }
852 Expr::Like {
853 expr,
854 pattern,
855 negated,
856 case_insensitive,
857 } => {
858 // v7.39 (round 364, M4 P2) — LIKE folds accents + case on a
859 // MySQL session (eval.rs `eval_like_arm`); the compiled
860 // pattern walk does not. Route to the interpreter.
861 if ctx.mysql_dialect {
862 steps.push(Step::Subtree(e.clone()));
863 return;
864 }
865 match literal_text_pattern(pattern) {
866 Some(pat) if fully_compilable(expr) => {
867 // v7.36 (perf — mailrs Phase 1, get_contacts hot
868 // inner) — trivial all-`%` pattern (`%`, `%%`, …)
869 // matches every non-NULL text. Collapse the LIKE
870 // into a `lhs IS NOT NULL` check: emit the operand
871 // then `IsNull { negated: !*negated }`. For ILIKE
872 // `%%` on 25 k rows the per-row `like_match_inner`
873 // → 2-char walk (~30 ns each) becomes a tag check
874 // (~3 ns); the operand still gets evaluated for the
875 // NULL semantics that SQL `LIKE` requires.
876 if !pat.is_empty() && pat.chars().all(|c| c == '%') {
877 compile_into(expr, ctx, steps);
878 steps.push(Step::AnyTextMatch { negated: *negated });
879 return;
880 }
881 compile_into(expr, ctx, steps);
882 let chars: alloc::vec::Vec<char> = if *case_insensitive {
883 pat.to_lowercase().chars().collect()
884 } else {
885 pat.chars().collect()
886 };
887 // v7.39 — `%[k×_]lit[m×_]%` runs on the substring fast
888 // path (see Step::LikeSubstring).
889 if let Some((k, needle, m)) = like_substring_shape(&chars) {
890 steps.push(Step::LikeSubstring {
891 needle,
892 k_before: k,
893 m_after: m,
894 negated: *negated,
895 case_insensitive: *case_insensitive,
896 });
897 return;
898 }
899 steps.push(Step::Like {
900 pattern: chars,
901 negated: *negated,
902 case_insensitive: *case_insensitive,
903 });
904 }
905 _ => steps.push(Step::Subtree(e.clone())),
906 }
907 }
908 // v7.39 (round 594) — a literal-pattern regex compiles here instead
909 // of once per row. `s ~ 'p'` and `s ~* 'p'` both lower to
910 // `regexp_like`, so this one shape covers the operators too. A
911 // pattern that is not a literal (or flags that are not) stays on the
912 // interpreter, which still has to compile per row: the pattern can
913 // differ row to row.
914 Expr::FunctionCall { name, args }
915 if name.eq_ignore_ascii_case("regexp_like")
916 && matches!(args.len(), 2 | 3)
917 && regex_literal_parts(args.as_slice()).is_some()
918 && fully_compilable(&args[0]) =>
919 {
920 let (pat, ci) = regex_literal_parts(args.as_slice()).expect("checked above");
921 match crate::eval::compile_re(pat, ci) {
922 Ok(re) => {
923 compile_into(&args[0], ctx, steps);
924 steps.push(Step::Regex {
925 re,
926 fallback: e.clone(),
927 });
928 }
929 // An invalid pattern is an error the interpreter words; let
930 // it keep raising it, in its own wording.
931 Err(_) => steps.push(Step::Subtree(e.clone())),
932 }
933 }
934 // v7.36 — PURE scalar function call: emit args then a
935 // single Function step that pops them. `fully_compilable`
936 // gates the whitelist + recurses into args, so this branch
937 // only fires when the entire subtree is compilable.
938 Expr::FunctionCall { name, args } if is_pure_scalar_function(name) => {
939 // v7.36 — specialise `LENGTH(<column>)` /
940 // `OCTET_LENGTH(<column>)` so the column's `Value::Text`
941 // isn't cloned just to read its length. The general
942 // `Step::Function` path goes through `apply_function`,
943 // which can't borrow off the stack — it copies.
944 let lower = name.to_ascii_lowercase();
945 if args.len() == 1 {
946 if let Expr::Column(c) = &args[0]
947 && let Some(pos) = compile_column_pos(c, ctx)
948 {
949 match lower.as_str() {
950 "length" | "char_length" | "character_length" => {
951 steps.push(Step::ColumnLength { pos });
952 return;
953 }
954 "octet_length" => {
955 steps.push(Step::ColumnOctetLength { pos });
956 return;
957 }
958 _ => {}
959 }
960 }
961 }
962 for a in args {
963 compile_into(a, ctx, steps);
964 }
965 // v7.39 (round 621) — COALESCE / NULLIF compile to their own
966 // steps (see the variants) so the chosen argument stays borrowed.
967 // The arguments are already on the stack from the loop above — a
968 // first cut recompiled them here and doubled them.
969 if name.eq_ignore_ascii_case("coalesce") && !args.is_empty() {
970 steps.push(Step::Coalesce { n_args: args.len() });
971 return;
972 }
973 if name.eq_ignore_ascii_case("nullif") && args.len() == 2 {
974 steps.push(Step::NullIf);
975 return;
976 }
977 // v7.39 (round 717) — GREATEST / LEAST get their own step;
978 // see the variant.
979 if (lower == "greatest" || lower == "least") && !args.is_empty() {
980 steps.push(Step::Extremum {
981 n_args: args.len(),
982 max: lower == "greatest",
983 });
984 return;
985 }
986 steps.push(Step::Function {
987 name_lower: lower,
988 n_args: args.len(),
989 });
990 }
991 // v7.39 (round 605) — a CONSTANT subexpression is evaluated once here
992 // rather than for every row. `WHERE id < ('500')::INT` cost two
993 // allocations a row against none for `WHERE id < 500`, and the same
994 // gap is much wider in a projection. A literal is already a `Lit`
995 // step, so this is only about the shapes built OUT of literals.
996 //
997 // An error stays where it was: if the fold does not evaluate, the
998 // expression compiles as before and raises per row, in the
999 // interpreter's own wording.
1000 e if !matches!(e, Expr::Literal(_)) && constant_expr(e) => {
1001 match eval_expr(e, &Row::new(alloc::vec::Vec::new()), ctx) {
1002 Ok(v) => steps.push(Step::Lit(v)),
1003 Err(_) => steps.push(Step::Subtree(e.clone())),
1004 }
1005 }
1006 // v7.39 (round 597) — `x = ANY (ARRAY[literals])` is `x IN (…)` and
1007 // `x <> ALL (…)` is `x NOT IN (…)`, down to the three-valued
1008 // treatment of a NULL element, so they take the membership set the
1009 // IN list already builds at compile time: 40.9 ms for a ten-element
1010 // array against 2.1 for the IN spelling of the same question. Folding
1011 // the array (below) alone left the per-row cost growing with the
1012 // array's length; a set does not.
1013 Expr::AnyAll {
1014 expr,
1015 op,
1016 array,
1017 is_any,
1018 } if !ctx.mysql_dialect
1019 && ((matches!(op, spg_sql::ast::BinOp::Eq) && *is_any)
1020 || (matches!(op, spg_sql::ast::BinOp::NotEq) && !*is_any))
1021 && array_literal_items(array)
1022 .is_some_and(|it| !it.is_empty() && crate::build_in_list_set(it).is_some())
1023 && fully_compilable(expr) =>
1024 {
1025 let items = array_literal_items(array).expect("checked above");
1026 let entry = crate::build_in_list_set(items).expect("checked above");
1027 compile_into(expr, ctx, steps);
1028 steps.push(Step::InSet {
1029 set: entry.set,
1030 has_null: entry.has_null,
1031 negated: !*is_any,
1032 fallback: e.clone(),
1033 });
1034 }
1035 // v7.39 (round 597) — any other ANY/ALL whose right-hand array is
1036 // constant: build it once here rather than per row.
1037 Expr::AnyAll {
1038 expr,
1039 op,
1040 array,
1041 is_any,
1042 } if constant_expr(array) => {
1043 match eval_expr(array, &Row::new(alloc::vec::Vec::new()), ctx) {
1044 Ok(arr) => {
1045 compile_into(expr, ctx, steps);
1046 // v7.39 (round 604) — with the array in hand, an equality
1047 // ANY / inequality ALL is a membership test whatever the
1048 // spelling: `'{1,2,3}'::int[]` keeps its elements inside a
1049 // string, so round 597's literal-list route could not see
1050 // them, but they are values now.
1051 if !ctx.mysql_dialect
1052 && ((matches!(op, spg_sql::ast::BinOp::Eq) && *is_any)
1053 || (matches!(op, spg_sql::ast::BinOp::NotEq) && !*is_any))
1054 && let Some(entry) = value_array_in_list_set(&arr)
1055 {
1056 steps.push(Step::InSet {
1057 set: entry.set,
1058 has_null: entry.has_null,
1059 negated: !*is_any,
1060 fallback: e.clone(),
1061 });
1062 return;
1063 }
1064 steps.push(Step::AnyAll {
1065 op: *op,
1066 is_any: *is_any,
1067 arr,
1068 });
1069 }
1070 // A constant that does not evaluate is the interpreter's
1071 // error to raise, per row, in its own wording.
1072 Err(_) => steps.push(Step::Subtree(e.clone())),
1073 }
1074 }
1075 // v7.39 (round 595) — EXTRACT over a compilable source. One
1076 // non-compilable node used to disqualify the WHOLE predicate, so
1077 // `WHERE extract(year FROM t) = 2020` interpreted the column read
1078 // and the comparison as well: 81.7 ms on 500k rows against PG18's
1079 // 14.5, where a compiled comparison on the same column is 13.1.
1080 Expr::Extract { field, source } => {
1081 compile_into(source, ctx, steps);
1082 steps.push(Step::Extract {
1083 field: field.clone(),
1084 fallback: e.clone(),
1085 });
1086 }
1087 Expr::Cast { expr, target } => {
1088 // v7.39 (read01 ruleutils.c) — catalog-dependent casts run
1089 // through eval's pre-hook (regclass dual-shape, domain/enum/
1090 // composite named types).
1091 // v7.39 (round 621) — the varchar/char FAMILY is catalog-free, so
1092 // it stays on the compiled path; the blanket Named -> Subtree rule
1093 // sent `s::VARCHAR(20)` to the interpreter, which pays two
1094 // allocations a row. Everything else Named (domains, enums,
1095 // composites, regtypes) still needs the interpreter's catalog.
1096 let named_text_family = match target {
1097 spg_sql::ast::CastTarget::Named(n) => named_varchar_family(n),
1098 _ => false,
1099 };
1100 // v7.39 (round 722) — a plain scalar spelling resolves NOW, not
1101 // per row; see `Step::CastPlain`. The text family keeps its
1102 // dedicated route (the timestamptz::text Subtree guard below
1103 // must still see it).
1104 if let spg_sql::ast::CastTarget::Named(n) = target
1105 && !named_text_family
1106 && let Some(dt) = super::cast::plain_named_target(n)
1107 {
1108 compile_into(expr, ctx, steps);
1109 steps.push(Step::CastPlain {
1110 dt,
1111 name: n.clone(),
1112 });
1113 return;
1114 }
1115 if matches!(target, spg_sql::ast::CastTarget::RegClass)
1116 || (matches!(target, spg_sql::ast::CastTarget::Named(_)) && !named_text_family)
1117 {
1118 steps.push(Step::Subtree(e.clone()));
1119 return;
1120 }
1121 // v7.39 (read01 round 76) — `<timestamptz>::text` renders the
1122 // `+00` offset, and tz-ness lives in the *static* type, not in
1123 // the runtime `Value::Timestamp`. `Step::Cast` calls the pure
1124 // `cast_value(value, target)`, which cannot see the expression
1125 // it came from — so a cast the interpreter renders with an
1126 // offset came out without one whenever the compiled VM drove
1127 // it (every cast inside an aggregate argument, and every cast
1128 // over an aggregate result: `string_agg(x::text, ',')`,
1129 // `min(x)::text`). Keep this one shape on Subtree.
1130 if (matches!(target, spg_sql::ast::CastTarget::Text) || named_text_family)
1131 && crate::describe::describe_expr(expr, ctx.columns)
1132 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
1133 {
1134 steps.push(Step::Subtree(e.clone()));
1135 return;
1136 }
1137 compile_into(expr, ctx, steps);
1138 steps.push(Step::Cast {
1139 target: target.clone(),
1140 });
1141 }
1142 Expr::Case {
1143 operand,
1144 branches,
1145 else_branch,
1146 } => {
1147 // Gate by `fully_compilable` at the leaf: if any sub-expr
1148 // can't compile natively, the whole Case stays Subtree so
1149 // a single Case never escapes to a row-materialise eval.
1150 let all_ok = operand.as_deref().is_none_or(fully_compilable)
1151 && branches
1152 .iter()
1153 .all(|(w, t)| fully_compilable(w) && fully_compilable(t))
1154 && else_branch.as_deref().is_none_or(fully_compilable);
1155 if !all_ok {
1156 steps.push(Step::Subtree(e.clone()));
1157 return;
1158 }
1159 let op_c = operand.as_deref().map(|o| compile_expr(o, ctx));
1160 let branches_c: alloc::vec::Vec<(CompiledExpr, CompiledExpr)> = branches
1161 .iter()
1162 .map(|(w, t)| (compile_expr(w, ctx), compile_expr(t, ctx)))
1163 .collect();
1164 let else_c = else_branch.as_deref().map(|el| compile_expr(el, ctx));
1165 steps.push(Step::Case {
1166 operand: op_c,
1167 branches: branches_c,
1168 else_branch: else_c,
1169 });
1170 // v7.38 (read01) — resolve the CASE result to PG's common type of
1171 // every THEN/ELSE branch once, here, and append a scale-preserving
1172 // coercion so a taken integer branch is widened to numeric (and
1173 // `pg_typeof` / downstream division match PG). Costs nothing when
1174 // the branches already share a type (common_type → None).
1175 let branch_types: Vec<spg_storage::DataType> = branches
1176 .iter()
1177 .map(|(_, t)| t)
1178 .chain(else_branch.iter().map(|b| b.as_ref()))
1179 .filter_map(|e| crate::describe::describe_expr(e, ctx.columns).map(|s| s.ty))
1180 .collect();
1181 if let Some(common) = crate::describe::common_type(&branch_types) {
1182 steps.push(Step::CoerceCommon(common));
1183 }
1184 }
1185 other => steps.push(Step::Subtree(other.clone())),
1186 }
1187}
1188
1189/// Literal text pattern behind a LIKE/ILIKE, if any.
1190/// v7.39 — recognise `%[k×_]literal[m×_]%` (any number of leading /
1191/// trailing `%`; literal free of `%` / `_` / `\`). Returns
1192/// `(k, literal, m)` when the pattern fits the substring fast path.
1193fn like_substring_shape(pat: &[char]) -> Option<(usize, alloc::string::String, usize)> {
1194 let mut lo = 0;
1195 while lo < pat.len() && pat[lo] == '%' {
1196 lo += 1;
1197 }
1198 if lo == 0 {
1199 return None; // not %-anchored at the front
1200 }
1201 let mut hi = pat.len();
1202 while hi > lo && pat[hi - 1] == '%' {
1203 hi -= 1;
1204 }
1205 if hi == pat.len() {
1206 return None; // not %-anchored at the back
1207 }
1208 let inner = &pat[lo..hi];
1209 let mut i = 0;
1210 while i < inner.len() && inner[i] == '_' {
1211 i += 1;
1212 }
1213 let mut j = inner.len();
1214 while j > i && inner[j - 1] == '_' {
1215 j -= 1;
1216 }
1217 let lit = &inner[i..j];
1218 if lit.is_empty() || lit.iter().any(|&c| c == '%' || c == '_' || c == '\\') {
1219 return None;
1220 }
1221 Some((i, lit.iter().collect(), inner.len() - j))
1222}
1223
1224/// v7.39 — `%[k×_]needle[m×_]%` matcher: walk `str::find` hits of the
1225/// literal and accept one with ≥k chars before it and ≥m chars after.
1226/// v7.39 (round 484) — find `needle` in `hay` at or after `start`.
1227///
1228/// `str::find(&str)` runs the two-way algorithm, and its SETUP is the cost:
1229/// round 484's profile of `s LIKE '%_05%'` put `StrSearcher::new` at 14.6 %
1230/// of self time — rebuilt for every row against a needle that is a compile
1231/// -time constant, and only two bytes long here.
1232///
1233/// An ASCII needle can be scanned as bytes instead: a UTF-8 continuation
1234/// byte is always >= 0x80, so an ASCII byte match can never land inside a
1235/// multi-byte character and every hit is on a char boundary. A non-ASCII
1236/// needle keeps `find`, where that reasoning does not hold.
1237fn like_find_from(hay: &str, needle: &str, start: usize) -> Option<usize> {
1238 if needle.is_empty() {
1239 return Some(start);
1240 }
1241 if !needle.is_ascii() {
1242 return hay[start..].find(needle).map(|rel| start + rel);
1243 }
1244 let h = hay.as_bytes();
1245 let n = needle.as_bytes();
1246 if h.len() < n.len() {
1247 return None;
1248 }
1249 let last = h.len() - n.len();
1250 let mut i = start;
1251 while i <= last {
1252 let off = h[i..=last].iter().position(|&b| b == n[0])?;
1253 let at = i + off;
1254 if &h[at..at + n.len()] == n {
1255 return Some(at);
1256 }
1257 i = at + 1;
1258 }
1259 None
1260}
1261
1262fn like_substring_match(hay: &str, needle: &str, k: usize, m: usize) -> bool {
1263 let mut start = 0;
1264 while let Some(off) = like_find_from(hay, needle, start) {
1265 let before_ok = k == 0 || hay[..off].chars().take(k).count() == k;
1266 let after_ok = m == 0 || hay[off + needle.len()..].chars().take(m).count() == m;
1267 if before_ok && after_ok {
1268 return true;
1269 }
1270 // Advance one char past this hit's start and retry.
1271 match hay[off..].chars().next() {
1272 Some(c) => start = off + c.len_utf8(),
1273 None => return false,
1274 }
1275 }
1276 false
1277}
1278
1279fn literal_text_pattern(pattern: &Expr) -> Option<&str> {
1280 match pattern {
1281 Expr::Literal(Literal::String(s)) => Some(s.as_str()),
1282 _ => None,
1283 }
1284}
1285
1286/// True when the whole tree consists of nodes the compiler models
1287/// natively. Mixed trees stay on the interpreted path: a Subtree
1288/// fallback would run WITHOUT the per-query MemoizeCache, and
1289/// memo-dependent nodes (InList set fast path — round-25) rebuild
1290/// per row there. Measured: compiling a search WHERE with an
1291/// InList subtree regressed 634 ms → 18.7 s.
1292/// v7.39 (round 621) — is this cast an identity on THIS value?
1293///
1294/// `s::TEXT` over a text cell changes nothing, and neither does an unbounded
1295/// `::VARCHAR`; the compiled path used to clone the cell anyway. Only the
1296/// pairs that provably change nothing are listed — a bounded VARCHAR(n) must
1297/// still check its length, numerics their range — so an unlisted pair merely
1298/// keeps the owned path, never a wrong answer.
1299/// The catalog-free varchar/char family, in the canonical `name(p)` spelling
1300/// the parser produces. Only these Named targets stay on the compiled path.
1301fn named_varchar_family(n: &str) -> bool {
1302 let base = n.split('(').next().unwrap_or(n);
1303 base.eq_ignore_ascii_case("varchar")
1304 || base.eq_ignore_ascii_case("text")
1305 || base.eq_ignore_ascii_case("char")
1306 || base.eq_ignore_ascii_case("bpchar")
1307 || base.eq_ignore_ascii_case("character")
1308}
1309
1310/// `varchar(k)`'s k, when the name carries one.
1311fn varchar_limit(n: &str) -> Option<usize> {
1312 let base = n.split('(').next().unwrap_or(n);
1313 if !base.eq_ignore_ascii_case("varchar") {
1314 return None;
1315 }
1316 let inner = n.split('(').nth(1)?.strip_suffix(')')?;
1317 inner.trim().parse().ok()
1318}
1319
1320fn cast_is_identity_for(v: &Value<'_>, target: &spg_sql::ast::CastTarget) -> bool {
1321 match (v, target) {
1322 (Value::Text(_), spg_sql::ast::CastTarget::Text) => true,
1323 (Value::Text(t), spg_sql::ast::CastTarget::Named(n)) => {
1324 // Unbounded text and varchar change nothing. A BOUNDED varchar is
1325 // an identity exactly when the text is within its limit — VARCHAR
1326 // truncates and never pads. CHAR(n) pads, so it is never one.
1327 n.eq_ignore_ascii_case("text")
1328 || n.eq_ignore_ascii_case("varchar")
1329 || varchar_limit(n).is_some_and(|k| t.chars().take(k + 1).count() <= k)
1330 }
1331 (Value::Int(_), spg_sql::ast::CastTarget::Int) => true,
1332 (Value::BigInt(_), spg_sql::ast::CastTarget::BigInt) => true,
1333 (Value::Float(_), spg_sql::ast::CastTarget::Float) => true,
1334 (Value::Bool(_), spg_sql::ast::CastTarget::Bool) => true,
1335 _ => false,
1336 }
1337}
1338
1339pub(crate) fn fully_compilable(e: &Expr) -> bool {
1340 match e {
1341 Expr::Literal(_) | Expr::Column(_) => true,
1342 Expr::Binary { lhs, rhs, .. } => fully_compilable(lhs) && fully_compilable(rhs),
1343 Expr::Unary { expr, .. } | Expr::IsNull { expr, .. } => fully_compilable(expr),
1344 // I2: an InList is compilable ONLY when it becomes a real
1345 // InSet (all-literal list + compilable needle). A
1346 // non-set-able InList must keep the whole tree off the
1347 // compiled path so it never degrades to a memo-less,
1348 // O(list) per-row Subtree (the round-25 18.7 s trap).
1349 Expr::InList { expr, list, .. } => {
1350 fully_compilable(expr) && crate::build_in_list_set(list).is_some()
1351 }
1352 Expr::Like { expr, pattern, .. } => {
1353 fully_compilable(expr) && literal_text_pattern(pattern).is_some()
1354 }
1355 // v7.36 (perf — mailrs Ask 1) — PURE scalar functions over
1356 // compilable args go to `Step::Function`. The whitelist
1357 // covers the high-traffic / non-volatile cases; anything
1358 // outside (NOW, RANDOM, sequence accessors, EXTRACT-with-
1359 // context-dependent fields, etc.) stays on Subtree where
1360 // the interpreter has the full ctx.
1361 // v7.39 (round 594) — a `regexp_like` with a LITERAL pattern is
1362 // compilable even though the function is not on the pure list: the
1363 // pattern becomes a compile product (`Step::Regex`) rather than an
1364 // argument the step would have to re-parse per row. A non-literal
1365 // pattern stays off, because then it really can differ row to row.
1366 Expr::FunctionCall { name, args }
1367 if name.eq_ignore_ascii_case("regexp_like")
1368 && matches!(args.len(), 2 | 3)
1369 && regex_literal_parts(args.as_slice()).is_some() =>
1370 {
1371 fully_compilable(&args[0])
1372 }
1373 Expr::FunctionCall { name, args } => {
1374 is_pure_scalar_function(name) && args.iter().all(fully_compilable)
1375 }
1376 // v7.36 — CAST over a compilable expression. `cast_value`
1377 // is pure / context-free for the scalar targets we care
1378 // about (text, ints, floats, bool, dates).
1379 // v7.39 (read01 ruleutils.c) — regclass / user-named casts
1380 // need the catalog (dual-shape resolve, domain/enum/composite
1381 // hooks); they stay Subtree so eval's pre-hook runs.
1382 Expr::AnyAll { expr, array, .. } if constant_expr(array) => fully_compilable(expr),
1383 Expr::Extract { source, .. } => fully_compilable(source),
1384 Expr::Cast { expr, target } => {
1385 // v7.39 (round 621) — the varchar/char family is catalog-free and
1386 // compiles (the compile arm gates it the same way); other Named
1387 // targets still need eval's catalog pre-hooks.
1388 let target_ok = match target {
1389 spg_sql::ast::CastTarget::RegClass => false,
1390 // v7.39 (round 722) — a compile-time-resolvable plain name
1391 // is as compilable as the dedicated variants; see
1392 // `Step::CastPlain`.
1393 spg_sql::ast::CastTarget::Named(n) => {
1394 named_varchar_family(n) || super::cast::plain_named_target(n).is_some()
1395 }
1396 _ => true,
1397 };
1398 target_ok && fully_compilable(expr)
1399 }
1400 // v7.37.5-A2b — `CASE [operand] WHEN x THEN y … ELSE z END`
1401 // when every sub-expression is itself fully-compilable. Hot
1402 // shape: Track A's 14 aggregates over
1403 // `COUNT(DISTINCT CASE WHEN m.message_id != '' THEN
1404 // m.message_id
1405 // ELSE CAST(m.id AS TEXT) END)` — without
1406 // this, every Case fell to `arg_compiled = None`, forced
1407 // `needs_mat = true` per-row, and triggered a full combined-
1408 // row `Vec<Value>` clone for the eval path.
1409 Expr::Case {
1410 operand,
1411 branches,
1412 else_branch,
1413 } => {
1414 operand.as_deref().is_none_or(fully_compilable)
1415 && branches
1416 .iter()
1417 .all(|(w, t)| fully_compilable(w) && fully_compilable(t))
1418 && else_branch.as_deref().is_none_or(fully_compilable)
1419 }
1420 _ => false,
1421 }
1422}
1423
1424/// v7.39 (round 595) — functions that are NOT context-free but ARE fixed for
1425/// the whole statement: they read the session's time zone, DateStyle or
1426/// lc_time out of the `EvalContext`, and `Step::Function` hands that context
1427/// to `apply_function_lower` exactly as the interpreter would.
1428///
1429/// Keeping them off the compiled path cost the whole predicate, not just the
1430/// call: one non-compilable node disqualifies the entire WHERE, so
1431/// `WHERE date_trunc('day', t) = TIMESTAMP '…'` interpreted the column read
1432/// and the comparison too — 153.8 ms over 500k rows against PG18's 9.7,
1433/// where a compiled comparison on the same column is 13.1.
1434///
1435/// `now` / `random` / sequence accessors stay off: they are not fixed for
1436/// the statement in the way these are.
1437fn is_session_deterministic_function(name: &str) -> bool {
1438 matches!(
1439 name.to_ascii_lowercase().as_str(),
1440 // v7.39 (round 717) — `format` belongs here, not on the pure
1441 // list: it renders arguments through the SESSION's RenderStyle
1442 // (datestyle / extra_float_digits / bytea_output), exactly the
1443 // dependency class to_char carries. Its absence from BOTH lists
1444 // was the round-716 panel's 4.89× cell — the only remaining
1445 // text-shape loss that was pure fallback tax.
1446 "date_trunc" | "date_part" | "to_char" | "age" | "format"
1447 )
1448}
1449
1450/// v7.36 — PURE scalar function whitelist for `Step::Function`.
1451/// "Pure" means: deterministic, context-independent, no side
1452/// effects. Aggregate names (sum / count / max / …) are filtered
1453/// upstream by the caller — they never reach the compiler. NOW /
1454/// RANDOM / sequence accessors are excluded because they need the
1455/// `EvalContext`'s clock / sequence resolver and aren't
1456/// deterministic. EXTRACT is excluded because the field kind is
1457/// parsed off the Expr tree, not an arg.
1458fn is_pure_scalar_function(name: &str) -> bool {
1459 is_session_deterministic_function(name)
1460 || matches!(
1461 name.to_ascii_lowercase().as_str(),
1462 // string length + slicing
1463 "length"
1464 | "char_length"
1465 | "character_length"
1466 | "octet_length"
1467 | "upper"
1468 | "lower"
1469 | "trim"
1470 | "ltrim"
1471 | "rtrim"
1472 | "btrim"
1473 | "left"
1474 | "right"
1475 | "substring"
1476 | "substr"
1477 | "replace"
1478 | "position"
1479 | "strpos"
1480 | "concat"
1481 | "concat_ws"
1482 | "reverse"
1483 | "repeat"
1484 | "lpad"
1485 | "rpad"
1486 | "split_part"
1487 // v7.39 (round 728) — the JSON constructors: pure over
1488 // their arguments (JSON's number/text rendering is fixed
1489 // by the format, not the session's RenderStyle — probed
1490 // against the ::JSONB cast lane, already whitelisted).
1491 // v7.39 (round 730) — the digest family: pure bytes-in,
1492 // hex/bytea-out. count(md5(s)) was the panel's last
1493 // serial-lane text cell (2.37×): the hash itself is
1494 // ~40% faster than PG's per call here, and ALL of the
1495 // loss was the missing parallel lane.
1496 | "md5"
1497 | "sha224"
1498 | "sha256"
1499 | "sha384"
1500 | "sha512"
1501 | "to_json"
1502 | "to_jsonb"
1503 | "jsonb_build_object"
1504 | "json_build_object"
1505 | "jsonb_build_array"
1506 | "json_build_array"
1507 // null/conditional
1508 | "coalesce"
1509 | "nullif"
1510 | "greatest"
1511 | "least"
1512 | "ifnull"
1513 | "isnull"
1514 | "nvl"
1515 // numeric
1516 | "abs"
1517 | "ceil"
1518 | "ceiling"
1519 | "floor"
1520 | "round"
1521 | "trunc"
1522 | "sqrt"
1523 | "power"
1524 | "pow"
1525 | "mod"
1526 | "sign"
1527 | "log"
1528 | "log10"
1529 | "exp"
1530 | "ln"
1531 // boolean / cast helpers
1532 | "cast"
1533 )
1534}
1535
1536/// r1021 — the arithmetic the integer lane runs. `Div` and `Mod` are in
1537/// because their zero divisor is handled by falling back, not by guessing.
1538const fn is_int_arithmetic(op: BinOp) -> bool {
1539 matches!(
1540 op,
1541 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod
1542 )
1543}
1544
1545/// r1021 — the comparison that ends an integer-lane program.
1546const fn is_int_comparison(op: BinOp) -> bool {
1547 matches!(
1548 op,
1549 BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
1550 )
1551}
1552
1553/// r1021 — `(value, is_32_bit)` for the two widths the lane admits. NULL,
1554/// `smallint` and every non-integer answer `None`, which sends the row to
1555/// the ordinary machine.
1556const fn int_operand(v: &Value<'_>) -> Option<(i64, bool)> {
1557 match v {
1558 Value::Int(i) => Some((*i as i64, true)),
1559 Value::BigInt(i) => Some((*i, false)),
1560 _ => None,
1561 }
1562}
1563
1564pub(crate) fn compile_expr(e: &Expr, ctx: &EvalContext<'_>) -> CompiledExpr {
1565 let mut steps = Vec::new();
1566 compile_into(e, ctx, &mut steps);
1567 let mut c = CompiledExpr {
1568 steps,
1569 pred_shape: PredShape::Other,
1570 };
1571 // Classified through the very matchers the row loop will use, so the
1572 // label and the destructuring cannot disagree.
1573 c.pred_shape = if c.as_column_cmp_literal().is_some() {
1574 PredShape::ColumnCmpLit
1575 } else if c.as_column_in_set().is_some() {
1576 PredShape::ColumnInSet
1577 } else if c.as_column_like().is_some() {
1578 PredShape::ColumnLike
1579 } else if c.is_int_arith_pred() {
1580 PredShape::IntArith
1581 } else {
1582 PredShape::Other
1583 };
1584 c
1585}
1586
1587/// Run a compiled program. `stack` is caller-owned scratch
1588/// (cleared here) so tight row loops never touch the allocator
1589/// for the machine itself.
1590pub(crate) fn eval_compiled(
1591 c: &CompiledExpr,
1592 row: &Row<'static>,
1593 ctx: &EvalContext<'_>,
1594 stack: &mut Vec<Value<'static>>,
1595) -> Result<Value<'static>, EvalError> {
1596 // v7.37.16 — reuse the caller's stack allocation across rows.
1597 // v7.37.9 T3 S2 had severed this: `eval_compiled_ref` pushes
1598 // `Value<'val>` where `'val` is the per-call RowRef borrow, and
1599 // `Vec<Value<'val>>` is invariant in `'val`, so the caller's
1600 // `Vec<Value<'static>>` could not be lent in-place and every call
1601 // allocated a fresh local Vec. That was sized for the ~50×/query
1602 // post-group projection path, but the aggregate/scan WHERE filter
1603 // loops (select.rs) call this once PER ROW — 50 k allocs/query on
1604 // a 50 k-row filter (the heavy.rs filter_agg 1.5×-vs-PG18 loss).
1605 // Instead: MOVE the caller's Vec in (covariant shrink 'static →
1606 // 'val, safe), run, then hand the emptied allocation back via
1607 // `recycle_stack`. Zero per-row alloc; the borrowed-push (S2/S3)
1608 // zero-clone Text path is untouched.
1609 let rowref = crate::join::RowRef::Owned(row);
1610 let mut local_stack: Vec<Value<'_>> = core::mem::take(stack);
1611 let result = eval_compiled_ref(c, rowref, ctx, &mut local_stack);
1612 let owned = result.map(Value::into_owned);
1613 *stack = recycle_stack(local_stack);
1614 owned
1615}
1616
1617/// v7.39 (round 479) — evaluate a compiled WHERE and answer the bool,
1618/// without ever materialising an owned `Value`.
1619///
1620/// `eval_compiled` ends in `result.map(Value::into_owned)` because its
1621/// contract is to hand back a `Value<'static>`. A predicate does not want
1622/// a value at all — it wants one bool — and round 478's profile put
1623/// `Value::into_owned` at 5.8 % of self time and `drop_glue<Value>` at
1624/// 15.1 %, against 5.5 % for the comparison the predicate exists to
1625/// perform. The `into_owned` and the owned value's drop are both pure
1626/// overhead on this path.
1627///
1628/// Everything else is `eval_compiled`'s bridge unchanged: the caller's
1629/// stack is moved in (covariant shrink), run, and handed back emptied.
1630pub(crate) fn eval_compiled_pred(
1631 c: &CompiledExpr,
1632 row: &Row<'static>,
1633 ctx: &EvalContext<'_>,
1634 stack: &mut Vec<Value<'static>>,
1635 mysql: bool,
1636) -> Result<bool, EvalError> {
1637 // The shape was settled at compile time; the row loop reads one
1638 // discriminant instead of re-matching the step list per row.
1639 match c.pred_shape {
1640 // r1021 — integer arithmetic runs on an i64 stack, building no
1641 // `Value` at all. `None` means the row carried something the lane
1642 // does not decide (NULL, a non-integer, a zero divisor, an
1643 // overflow) and the ordinary machine below answers it instead.
1644 PredShape::IntArith => {
1645 if let Some(verdict) = c.eval_int_arith_pred(row) {
1646 crate::bump_counter!(STEP_VM_INTLANE_FIRE);
1647 return Ok(verdict);
1648 }
1649 crate::bump_counter!(STEP_VM_INTLANE_FALLBACK);
1650 }
1651 // v7.39 (round 482) — `<column> <cmp> <literal>` compares in place.
1652 //
1653 // The general path builds three `Value`s a row and drops them;
1654 // this one reads both operands by reference and builds only the
1655 // comparison result. `apply_binary_by_ref` is the SAME function
1656 // `Step::Binary` reaches for first, so the answer is identical by
1657 // construction rather than by a second reading of the semantics.
1658 PredShape::ColumnCmpLit => {
1659 if let Some((pos, op, lit)) = c.as_column_cmp_literal() {
1660 crate::bump_counter!(STEP_VM_FASTPRED_FIRE);
1661 let cell = row.values.get(pos).unwrap_or(&Value::Null);
1662 if let Some(res) = super::apply_binary_by_ref(op, cell, lit)? {
1663 return crate::eval::predicate_is_true(&res, "WHERE", mysql);
1664 }
1665 // The by-ref form declined (an op that builds an owned
1666 // result); fall through rather than answer differently
1667 // from the VM.
1668 }
1669 }
1670 // v7.39 (round 486) — `<column> [NOT] IN (<literals>)` looks the
1671 // cell up in place. Same `in_set_verdict` the `InSet` step calls,
1672 // so the answer is identical by construction; a family mismatch
1673 // returns None and falls through to the general path, which takes
1674 // the step's interpreter fallback.
1675 PredShape::ColumnInSet => {
1676 if let Some((pos, set, has_null, negated)) = c.as_column_in_set() {
1677 let cell = row.values.get(pos).unwrap_or(&Value::Null);
1678 if let Some(v) = in_set_verdict(cell, set, has_null, negated) {
1679 crate::bump_counter!(STEP_VM_FASTPRED_FIRE);
1680 return crate::eval::predicate_is_true(&v, "WHERE", mysql);
1681 }
1682 }
1683 }
1684 // v7.39 (round 488) — `<column> [NOT] [I]LIKE '<literal>'` matches
1685 // straight off the cell. The matcher wanted a `&str` all along;
1686 // the VM was pushing a `Value` and popping it for no other reason.
1687 PredShape::ColumnLike => {
1688 if let Some((pos, step)) = c.as_column_like() {
1689 let cell = row.values.get(pos).unwrap_or(&Value::Null);
1690 if let Some(v) = like_verdict(cell, step) {
1691 crate::bump_counter!(STEP_VM_FASTPRED_FIRE);
1692 return crate::eval::predicate_is_true(&v?, "WHERE", mysql);
1693 }
1694 }
1695 }
1696 PredShape::Other => {}
1697 }
1698 let rowref = crate::join::RowRef::Owned(row);
1699 let mut local_stack: Vec<Value<'_>> = core::mem::take(stack);
1700 let verdict = eval_compiled_ref(c, rowref, ctx, &mut local_stack)
1701 .and_then(|v| crate::eval::predicate_is_true(&v, "WHERE", mysql));
1702 *stack = recycle_stack(local_stack);
1703 verdict
1704}
1705
1706/// v7.39 (round 486) — the membership decision, shared by `Step::InSet`
1707/// and by the fast predicate below so the two cannot drift. `None` means
1708/// the needle's family does not match the set's, which is the caller's
1709/// cue to take the interpreter's coercion path on the whole node.
1710///
1711/// v7.39 (round 489) — `#[inline(always)]` is load-bearing, and the
1712/// measurement behind it is worth stating because round 486 got it wrong.
1713/// Round 486 saw the shared-helper form cost `like_filter` 4.5 % and
1714/// concluded "editing this loop is expensive"; it then duplicated the
1715/// body into the arm to avoid touching it. Re-measured with the shape
1716/// ISOLATED (round 488 found the panel's shapes contaminate each other),
1717/// `like_filter` shows no such cost — that reading was its neighbours.
1718/// What IS real is `big_in`: +4.6 % with a plain call, separated spreads,
1719/// on a shape that takes the fast path and never executes this arm.
1720/// `#[inline(always)]` returns it to parity (-0.1 %, overlapping), so the
1721/// duplicate bought nothing and is gone.
1722///
1723/// `e2e_in_set_fast_path_round486` still runs every needle × set ×
1724/// negated × has-NULL combination down BOTH entry points.
1725#[allow(clippy::inline_always)] // measured: see the note above
1726#[inline(always)]
1727fn in_set_verdict(
1728 needle: &Value<'_>,
1729 set: &crate::memoize::InListSet,
1730 has_null: bool,
1731 negated: bool,
1732) -> Option<Value<'static>> {
1733 let contained = match (needle, set) {
1734 // Non-empty list + NULL needle → NULL (NOT NULL is still NULL) —
1735 // matches the interpreter and eval_with_in_sets.
1736 (Value::Null, _) => return Some(Value::Null),
1737 (Value::SmallInt(n), crate::memoize::InListSet::Int(s)) => s.contains(&i64::from(*n)),
1738 (Value::Int(n), crate::memoize::InListSet::Int(s)) => s.contains(&i64::from(*n)),
1739 (Value::BigInt(n), crate::memoize::InListSet::Int(s)) => s.contains(n),
1740 (Value::Text(t), crate::memoize::InListSet::Text(s)) => s.contains(t.as_ref()),
1741 _ => return None,
1742 };
1743 let inner = if contained {
1744 Value::Bool(true)
1745 } else if has_null {
1746 Value::Null
1747 } else {
1748 Value::Bool(false)
1749 };
1750 Some(match (negated, inner) {
1751 (true, Value::Bool(b)) => Value::Bool(!b),
1752 (_, v) => v,
1753 })
1754}
1755
1756/// v7.39 (round 604) — the membership set of an ALREADY-EVALUATED constant
1757/// array.
1758///
1759/// Round 597 gave `x = ANY (ARRAY[1,2,3])` the same set an IN list builds,
1760/// which took it from 268 ms over 500k rows to 1.93. It could not do the
1761/// same for `x = ANY ('{1,2,3}'::int[])`, because it built the set from AST
1762/// literals and that spelling keeps its elements inside a string: the array
1763/// was folded once but every row still walked it, and the shape stayed at
1764/// 43.49 ms against PG18's 9.37. The array has been evaluated by the time
1765/// this is asked, so the elements are right there.
1766///
1767/// The families are the ones `build_in_list_set` accepts, for the same
1768/// reason: an integer set answers `Int = BigInt` correctly across widths,
1769/// and a text set compares verbatim. Anything else — a mixed array, floats,
1770/// NUMERIC, dates — returns `None` and keeps the folded-array walk.
1771fn value_array_in_list_set(arr: &Value<'_>) -> Option<crate::memoize::InListSetEntry> {
1772 let len = crate::eval::values::array_len(arr)?;
1773 if len == 0 {
1774 return None;
1775 }
1776 let mut ints: hashbrown::HashSet<i64> = hashbrown::HashSet::with_capacity(len);
1777 let mut texts: hashbrown::HashSet<alloc::string::String> =
1778 hashbrown::HashSet::with_capacity(len);
1779 let mut has_null = false;
1780 for i in 0..len {
1781 match crate::eval::values::array_element_at(arr, i) {
1782 None | Some(Value::Null) => has_null = true,
1783 Some(Value::SmallInt(n)) => {
1784 ints.insert(i64::from(n));
1785 }
1786 Some(Value::Int(n)) => {
1787 ints.insert(i64::from(n));
1788 }
1789 Some(Value::BigInt(n)) => {
1790 ints.insert(n);
1791 }
1792 Some(Value::Text(s) | Value::BpChar(s)) => {
1793 texts.insert(s.into_owned());
1794 }
1795 _ => return None,
1796 }
1797 if !ints.is_empty() && !texts.is_empty() {
1798 return None;
1799 }
1800 }
1801 let set = if !ints.is_empty() {
1802 crate::memoize::InListSet::Int(ints)
1803 } else if !texts.is_empty() {
1804 crate::memoize::InListSet::Text(texts)
1805 } else {
1806 return None;
1807 };
1808 Some(crate::memoize::InListSetEntry { set, has_null })
1809}
1810
1811/// v7.39 (round 597) — the literal elements of an `ARRAY[…]` constructor.
1812/// `None` for any other right-hand side, including the `'{1,2}'::int[]`
1813/// spelling, whose elements live inside a string rather than the tree.
1814fn array_literal_items(e: &Expr) -> Option<&[Expr]> {
1815 match e {
1816 Expr::Array(items) if items.iter().all(constant_expr) => Some(items.as_slice()),
1817 _ => None,
1818 }
1819}
1820
1821/// v7.39 (round 605) — the value of a projection item that cannot depend on
1822/// the row, evaluated once. `None` for anything that depends on a row, or
1823/// that fails to evaluate — the latter so its error still comes from the row
1824/// loop, in the interpreter's own wording, rather than from planning.
1825pub(crate) fn constant_projection_value(e: &Expr, ctx: &EvalContext<'_>) -> Option<Value<'static>> {
1826 if matches!(e, Expr::Literal(_)) || !constant_expr(e) {
1827 return None;
1828 }
1829 eval_expr(e, &Row::new(alloc::vec::Vec::new()), ctx).ok()
1830}
1831
1832/// v7.39 (round 597) — an expression whose value cannot depend on the row.
1833/// An allowlist of node kinds, for the reason rounds 590 and 596 recorded:
1834/// asking "does it mention a column" would admit a node the walk did not
1835/// know about, and a function whose volatility SPG cannot look up.
1836fn constant_expr(e: &Expr) -> bool {
1837 match e {
1838 Expr::Literal(_) => true,
1839 Expr::Array(items) => items.iter().all(constant_expr),
1840 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => constant_expr(expr),
1841 Expr::Binary { lhs, rhs, .. } => constant_expr(lhs) && constant_expr(rhs),
1842 _ => false,
1843 }
1844}
1845
1846/// v7.39 (round 595) — the source sub-expression of the EXTRACT node a
1847/// `Step::Extract` was compiled from. Only its declared TYPE is read, for
1848/// the error wording; the value came off the stack.
1849fn source_of_extract(node: &Expr) -> &Expr {
1850 match node {
1851 Expr::Extract { source, .. } => source,
1852 other => other,
1853 }
1854}
1855
1856/// v7.39 (round 594) — the literal pattern and case flag of a `regexp_like`
1857/// call, when both are literals. `None` keeps the call on the interpreter.
1858fn regex_literal_parts(args: &[Expr]) -> Option<(&str, bool)> {
1859 let Expr::Literal(spg_sql::ast::Literal::String(pat)) = &args[1] else {
1860 return None;
1861 };
1862 let ci = match args.get(2) {
1863 None => false,
1864 Some(Expr::Literal(spg_sql::ast::Literal::String(f))) => f.contains('i'),
1865 Some(_) => return None,
1866 };
1867 Some((pat.as_str(), ci))
1868}
1869
1870/// The verdict `Step::Regex` produces. `None` means the operand is not text,
1871/// which is the caller's cue to fall through to the interpreter for its own
1872/// coercion and wording.
1873fn regex_verdict(
1874 cell: &Value<'_>,
1875 re: &crate::eval::CompiledRe,
1876) -> Option<Result<Value<'static>, EvalError>> {
1877 let text = match cell {
1878 Value::Null => return Some(Ok(Value::Null)),
1879 Value::Text(t) | Value::BpChar(t) => t.as_ref(),
1880 _ => return None,
1881 };
1882 Some(crate::eval::compiled_is_match(re, text).map(Value::Bool))
1883}
1884
1885/// v7.39 (round 488) — the verdict `Step::Like` / `Step::LikeSubstring`
1886/// produce, restated for the fast predicate. `None` means the operand is
1887/// not text, which is the caller's cue to fall through to the VM and let
1888/// it raise the type error in its own wording.
1889///
1890/// v7.39 (round 489) — the VM arm calls this too, so there is one body
1891/// rather than two that can drift. Round 488 kept them separate on round
1892/// 486's belief that editing that loop costs unrelated shapes; round 489
1893/// re-measured that belief with the shapes isolated and force-inlined the
1894/// helper, and the cost is gone (see `in_set_verdict`).
1895/// `e2e_like_fast_path_round488` runs both entry points over the same
1896/// matrix.
1897#[allow(clippy::inline_always)] // measured: see `in_set_verdict`
1898#[inline(always)]
1899fn like_verdict(cell: &Value<'_>, step: &Step) -> Option<Result<Value<'static>, EvalError>> {
1900 let (text, negated) = match (cell, step) {
1901 (Value::Null, _) => return Some(Ok(Value::Null)),
1902 (
1903 Value::Text(t) | Value::BpChar(t),
1904 Step::Like { negated, .. } | Step::LikeSubstring { negated, .. },
1905 ) => (t.as_ref(), *negated),
1906 _ => return None,
1907 };
1908 let matched = match step {
1909 Step::Like {
1910 pattern,
1911 case_insensitive,
1912 ..
1913 } => {
1914 let r = if *case_insensitive {
1915 like_match_str(&text.to_lowercase(), pattern, 0)
1916 } else {
1917 like_match_str(text, pattern, 0)
1918 };
1919 match r {
1920 Ok(m) => m,
1921 Err(e) => return Some(Err(e)),
1922 }
1923 }
1924 Step::LikeSubstring {
1925 needle,
1926 k_before,
1927 m_after,
1928 case_insensitive,
1929 ..
1930 } => {
1931 if *case_insensitive {
1932 like_substring_match(&text.to_lowercase(), needle, *k_before, *m_after)
1933 } else {
1934 like_substring_match(text, needle, *k_before, *m_after)
1935 }
1936 }
1937 _ => return None,
1938 };
1939 Some(Ok(Value::Bool(if negated { !matched } else { matched })))
1940}
1941
1942/// Return an emptied stack's allocation with its value lifetime reset.
1943/// This is the standard "recycle" pattern (cf. the `recycle_vec` crate):
1944/// an EMPTY `Vec<Value<'a>>` holds no values, only a raw allocation, so
1945/// re-labelling its element lifetime cannot dangle.
1946#[allow(unsafe_code)] // empty-Vec lifetime relabel; isolated (see SAFETY).
1947fn recycle_stack(mut v: Vec<Value<'_>>) -> Vec<Value<'static>> {
1948 // v7.39 (round 481) — read before the clear: this is exactly the set of
1949 // values the clear is about to drop.
1950 crate::bump_counter!(STEP_VM_STACK_LEFTOVER, v.len() as u64);
1951 #[cfg(feature = "perf-counters")]
1952 {
1953 let heap = v
1954 .iter()
1955 .filter(|x| {
1956 matches!(
1957 x,
1958 Value::Text(_) | Value::Bytes(_) | Value::Json(_) | Value::Vector(_)
1959 )
1960 })
1961 .count();
1962 crate::bump_counter!(STEP_VM_STACK_LEFTOVER_HEAP, heap as u64);
1963 }
1964 v.clear();
1965 debug_assert!(v.is_empty());
1966 // SAFETY: `v` is empty (cleared above) — there are no `Value<'_>`s
1967 // whose lifetime could be unsoundly extended; `Vec<Value<'a>>` and
1968 // `Vec<Value<'static>>` are the same type constructor differing only
1969 // in a lifetime parameter, so they have identical size/align/layout
1970 // (lifetimes are erased before layout is computed).
1971 unsafe { core::mem::transmute::<Vec<Value<'_>>, Vec<Value<'static>>>(v) }
1972}
1973
1974/// v7.32 (P4 borrow channel, increment 2) — the RowRef-borrowing form of
1975/// `eval_compiled`. `Step::Column` borrows its cell straight from the
1976/// RowRef (a join tuple resolves it via `tuple_value`, never
1977/// materialising a combined Row); only the rare Subtree / InSet
1978/// cross-family fallback materialises the row once. Bit-for-bit
1979/// equivalent to the Owned path — `eval_compiled` above is now a thin
1980/// `RowRef::Owned` wrapper, so there is a single interpreter (invariant
1981/// I3); a differential test pins the equivalence.
1982// v7.37.9 T3 S1 — row-lifetime stack plumbing. Two lifetimes:
1983// `'row` = the RowRef's data lifetime; `'val` = stack value lifetime
1984// (must outlive function return). Constraint `'row: 'val` allows the
1985// step body to push `Value::Text(Cow::Borrowed(row_cell))` (S2+) while
1986// the caller's stack stays at whatever lifetime it declared (often
1987// `'static` for Vec<Value<'static>>). S1 keeps every step body forcing
1988// `.into_owned()` so behaviour is bit-identical; later stages
1989// (S2 Column, S3 Lit, S4 Binary, S6 Function, S7 Case) progressively
1990// switch to borrowed push to eliminate per-row String allocs.
1991pub(crate) fn eval_compiled_ref<'row, 'val>(
1992 c: &'val CompiledExpr,
1993 // v7.39 (round 656) — BY VALUE. `RowRef` is `Copy` and its `get`
1994 // borrows the row data, not the wrapper, so taking a reference here
1995 // only served to tie the result's lifetime to a caller local — which
1996 // is what stopped the aggregate loop from holding its `RowRef` by
1997 // value and forced a materialised `Vec<RowRef>` per scan.
1998 row: crate::join::RowRef<'row>,
1999 ctx: &EvalContext<'_>,
2000 stack: &mut Vec<Value<'val>>,
2001) -> Result<Value<'val>, EvalError>
2002where
2003 'row: 'val,
2004{
2005 stack.clear();
2006 run_compiled_steps(&c.steps, row, ctx, stack)?;
2007 Ok(stack.pop().unwrap_or(Value::Null))
2008}
2009
2010/// v7.37.5-A2b — append-mode entry point for nested sub-programs (the
2011/// `Step::Case` executor's per-branch evaluations). Does NOT clear the
2012/// stack; pushes the program's result on top of whatever was already
2013/// there. Caller uses the `mark` to know where to truncate / pop. Kept
2014/// out of public surface — only the Case opcode reaches for it.
2015fn eval_compiled_ref_into<'row, 'val>(
2016 c: &'val CompiledExpr,
2017 // v7.39 (round 656) — BY VALUE. `RowRef` is `Copy` and its `get`
2018 // borrows the row data, not the wrapper, so taking a reference here
2019 // only served to tie the result's lifetime to a caller local — which
2020 // is what stopped the aggregate loop from holding its `RowRef` by
2021 // value and forced a materialised `Vec<RowRef>` per scan.
2022 row: crate::join::RowRef<'row>,
2023 ctx: &EvalContext<'_>,
2024 stack: &mut Vec<Value<'val>>,
2025 _mark: usize,
2026) -> Result<(), EvalError>
2027where
2028 'row: 'val,
2029{
2030 run_compiled_steps(&c.steps, row, ctx, stack)
2031}
2032
2033#[inline]
2034fn run_compiled_steps<'row, 'val>(
2035 steps: &'val [Step],
2036 // v7.39 (round 656) — BY VALUE. `RowRef` is `Copy` and its `get`
2037 // borrows the row data, not the wrapper, so taking a reference here
2038 // only served to tie the result's lifetime to a caller local — which
2039 // is what stopped the aggregate loop from holding its `RowRef` by
2040 // value and forced a materialised `Vec<RowRef>` per scan.
2041 row: crate::join::RowRef<'row>,
2042 ctx: &EvalContext<'_>,
2043 stack: &mut Vec<Value<'val>>,
2044) -> Result<(), EvalError>
2045where
2046 'row: 'val,
2047{
2048 // v7.37.9 Phase 1A-ext-2 T1 — counter per call into the Step VM
2049 // interpreter. Tells us "how many steps does the average compiled
2050 // arg run per row" → narrows the attack target (subtree CSE vs
2051 // column-ref-push vs multi-spec combine). Read-only.
2052 crate::bump_counter!(STEP_VM_CALL_COUNT);
2053 crate::bump_counter!(STEP_VM_STEPS_TOTAL, steps.len() as u64);
2054 for step in steps {
2055 match step {
2056 Step::Column(pos) => {
2057 crate::bump_counter!(STEP_VM_COLUMN_FIRE);
2058 // v7.37.9 T3 S2 — catalog rows hold `Cow::Owned(String)`
2059 // for Text-class variants (per `spg-storage/src/lib.rs:539`
2060 // — "Persistent / catalog Values use Value<'static> with
2061 // Cow::Owned(...)"). Plain `.clone()` would therefore
2062 // still trigger `String::clone()` per cell read. Instead
2063 // manually wrap the existing storage into a borrowed Cow
2064 // pointing at the same bytes — zero-alloc push.
2065 let cell: Value<'val> = match row.get(*pos) {
2066 Some(spg_storage::Value::Text(s)) => {
2067 spg_storage::Value::Text(alloc::borrow::Cow::Borrowed(s.as_ref()))
2068 }
2069 Some(spg_storage::Value::Bytes(b)) => {
2070 spg_storage::Value::Bytes(alloc::borrow::Cow::Borrowed(b.as_ref()))
2071 }
2072 Some(spg_storage::Value::Json(s)) => {
2073 spg_storage::Value::Json(alloc::borrow::Cow::Borrowed(s.as_ref()))
2074 }
2075 Some(spg_storage::Value::Vector(v)) => {
2076 spg_storage::Value::Vector(alloc::borrow::Cow::Borrowed(v.as_ref()))
2077 }
2078 // Copy-light variants: clone is free (just enum copy).
2079 Some(v) => v.clone(),
2080 None => Value::Null,
2081 };
2082 // Classification counter unchanged (still counts cells
2083 // that WERE heap-bearing in the baseline).
2084 if matches!(
2085 &cell,
2086 spg_storage::Value::Text(_)
2087 | spg_storage::Value::Bytes(_)
2088 | spg_storage::Value::Json(_)
2089 | spg_storage::Value::Vector(_)
2090 ) {
2091 crate::bump_counter!(STEP_VM_COLUMN_HEAP_ALLOC);
2092 }
2093 stack.push(cell);
2094 }
2095 Step::Lit(v) => {
2096 crate::bump_counter!(STEP_VM_LIT_FIRE);
2097 if matches!(
2098 v,
2099 spg_storage::Value::Text(_)
2100 | spg_storage::Value::Bytes(_)
2101 | spg_storage::Value::Json(_)
2102 | spg_storage::Value::Vector(_)
2103 ) {
2104 crate::bump_counter!(STEP_VM_LIT_HEAP_ALLOC);
2105 }
2106 // v7.37.9 T3 S3 — borrow literal storage instead of
2107 // String::clone'ing it. Step variants own their
2108 // literal (`Value<'static>` enum payload), so we can
2109 // safely construct a `Cow::Borrowed(&'static …)` view.
2110 // Same pattern as S2's Column path.
2111 let pushed: Value<'val> = match v {
2112 spg_storage::Value::Text(s) => {
2113 spg_storage::Value::Text(alloc::borrow::Cow::Borrowed(s.as_ref()))
2114 }
2115 spg_storage::Value::Bytes(b) => {
2116 spg_storage::Value::Bytes(alloc::borrow::Cow::Borrowed(b.as_ref()))
2117 }
2118 spg_storage::Value::Json(s) => {
2119 spg_storage::Value::Json(alloc::borrow::Cow::Borrowed(s.as_ref()))
2120 }
2121 spg_storage::Value::Vector(vec) => {
2122 spg_storage::Value::Vector(alloc::borrow::Cow::Borrowed(vec.as_ref()))
2123 }
2124 other => other.clone(),
2125 };
2126 stack.push(pushed);
2127 }
2128 Step::Binary(op) => {
2129 crate::bump_counter!(STEP_VM_BINARY_FIRE);
2130 // v7.37.9 T3 S4 — try the by-ref fast path first
2131 // (comparison + 3VL ops). For those, operand bytes are
2132 // read but never stored in the result; we avoid the
2133 // .into_owned() that would clone every Cow::Borrowed
2134 // Text/Bytes/Json/Vector pushed by S2/S3. For ops that
2135 // build owned results (arithmetic, concat, json get,
2136 // etc.) apply_binary_by_ref returns None and we fall
2137 // through to the owning path.
2138 // v7.39 (round 346, M1) — the MySQL reading of AND / OR
2139 // has to be here TOO: a compiled predicate never passes
2140 // through `eval_expr`'s arm, so `WHERE a AND 1` still
2141 // errored on a MySQL session while the interpreted form
2142 // answered. (The pin found this, not the reading.)
2143 if ctx.mysql_dialect && matches!(op, BinOp::And | BinOp::Or) {
2144 let r = super::as_mysql_truth(stack.pop().unwrap_or(Value::Null).into_owned())?;
2145 let l = super::as_mysql_truth(stack.pop().unwrap_or(Value::Null).into_owned())?;
2146 stack.push(apply_binary(*op, l, r)?);
2147 continue;
2148 }
2149 let n = stack.len();
2150 if n >= 2 {
2151 if let Some(result) =
2152 super::apply_binary_by_ref(*op, &stack[n - 2], &stack[n - 1])?
2153 {
2154 stack.truncate(n - 2);
2155 stack.push(result);
2156 continue;
2157 }
2158 }
2159 let r = stack.pop().unwrap_or(Value::Null).into_owned();
2160 let l = stack.pop().unwrap_or(Value::Null).into_owned();
2161 stack.push(apply_binary(*op, l, r)?);
2162 }
2163 Step::Connective { op, rhs } => {
2164 crate::bump_counter!(STEP_VM_BINARY_FIRE);
2165 let l = stack.pop().unwrap_or(Value::Null).into_owned();
2166 // The left decides, or it does not. A NULL decides nothing:
2167 // NULL AND false is false, so the right side is still needed.
2168 match (op, &l) {
2169 (BinOp::And, Value::Bool(false)) => {
2170 stack.push(Value::Bool(false));
2171 continue;
2172 }
2173 (BinOp::Or, Value::Bool(true)) => {
2174 stack.push(Value::Bool(true));
2175 continue;
2176 }
2177 _ => {}
2178 }
2179 run_compiled_steps(rhs, row, ctx, stack)?;
2180 let r = stack.pop().unwrap_or(Value::Null).into_owned();
2181 stack.push(apply_binary(*op, l, r)?);
2182 }
2183 Step::BinaryCi(op) => {
2184 // v7.39 (round 364, M4 P2) — the MySQL session uses the
2185 // accent-aware fold; a PG `case_insensitive` column keeps
2186 // its ASCII-only contract.
2187 let fold = |v: Value<'static>| match v {
2188 Value::Text(s) if ctx.mysql_dialect => {
2189 Value::text(spg_storage::mysql_compare_fold(&s))
2190 }
2191 Value::Text(s) => Value::text(s.to_ascii_lowercase()),
2192 other => other,
2193 };
2194 let r = fold(stack.pop().unwrap_or(Value::Null).into_owned());
2195 let l = fold(stack.pop().unwrap_or(Value::Null).into_owned());
2196 stack.push(apply_binary(*op, l, r)?);
2197 }
2198 Step::Unary(op) => {
2199 let v = stack.pop().unwrap_or(Value::Null).into_owned();
2200 if ctx.mysql_dialect
2201 && matches!(op, UnOp::Not)
2202 && !matches!(v, Value::Bool(_) | Value::Null)
2203 {
2204 stack.push(Value::Bool(!super::predicate_is_true(&v, "NOT", true)?));
2205 continue;
2206 }
2207 stack.push(apply_unary(*op, v)?);
2208 }
2209 Step::IsNull { negated } => {
2210 let v = stack.pop().unwrap_or(Value::Null);
2211 let is_null = matches!(v, Value::Null);
2212 stack.push(Value::Bool(if *negated { !is_null } else { is_null }));
2213 }
2214 Step::AnyTextMatch { negated } => {
2215 let v = stack.pop().unwrap_or(Value::Null);
2216 stack.push(match v {
2217 Value::Null => Value::Null,
2218 _ => Value::Bool(!*negated),
2219 });
2220 }
2221 Step::InSet {
2222 set,
2223 has_null,
2224 negated,
2225 fallback,
2226 } => {
2227 let needle = stack.pop().unwrap_or(Value::Null);
2228 match in_set_verdict(&needle, set, *has_null, *negated) {
2229 Some(v) => stack.push(v),
2230 // Cross-family needle: take the interpreter's
2231 // exact coercion / error path on the whole node.
2232 None => stack.push(eval_expr(fallback, &row.as_row(), ctx)?),
2233 }
2234 }
2235 Step::AnyAll { op, is_any, arr } => {
2236 let lhs = stack.pop().unwrap_or(Value::Null).into_owned();
2237 stack.push(crate::eval::any_all_over(lhs, arr.clone(), op, *is_any)?);
2238 }
2239 Step::Extract { field, fallback } => {
2240 let v = stack.pop().unwrap_or(Value::Null).into_owned();
2241 stack.push(crate::eval::extract_from_value(
2242 field,
2243 v,
2244 source_of_extract(fallback),
2245 ctx,
2246 )?);
2247 }
2248 Step::Regex { re, fallback } => {
2249 let v = stack.pop().unwrap_or(Value::Null);
2250 match regex_verdict(&v, re) {
2251 Some(r) => stack.push(r?),
2252 // Not text: the interpreter's coercion and wording, on
2253 // the whole node, exactly as `Step::InSet` does.
2254 None => stack.push(eval_expr(fallback, &row.as_row(), ctx)?),
2255 }
2256 }
2257 step @ (Step::Like { .. } | Step::LikeSubstring { .. }) => {
2258 // v7.39 (round 489) — one arm for both pattern steps,
2259 // sharing `like_verdict` with the fast predicate.
2260 //
2261 // The matching itself was already out of line: v7.37.16
2262 // borrowed the operand instead of paying `.into_owned()`
2263 // plus a per-row `Vec<char>` collect (~90 ns/row of
2264 // allocator traffic on a LIKE table scan), and round 484
2265 // replaced `str::find`'s two-way searcher — whose SETUP
2266 // was 14.6 % of self time, rebuilt every row for a
2267 // two-byte constant needle — with an ASCII byte scan.
2268 // ILIKE still lowercases; plain LIKE allocates nothing.
2269 let v = stack.pop().unwrap_or(Value::Null);
2270 match like_verdict(&v, step) {
2271 Some(r) => stack.push(r?),
2272 None => {
2273 return Err(EvalError::TypeMismatch {
2274 detail: format!(
2275 "LIKE requires text operands, got {}",
2276 crate::conversions::pg_type_name_for_error_opt(v.data_type())
2277 ),
2278 });
2279 }
2280 }
2281 }
2282 Step::ColumnLength { pos } => {
2283 // v7.36 — zero-copy LENGTH on a column. Read the
2284 // cell by reference; compute char count without
2285 // cloning the underlying `String`. Saves 25 k ×
2286 // ~1 KB heap clones on the user_storage_usage shape.
2287 let v = row.get(*pos).unwrap_or(&Value::Null);
2288 let pushed = match v {
2289 Value::Null => Value::Null,
2290 Value::Text(s) => {
2291 let n = if s.is_ascii() {
2292 i32::try_from(s.len()).unwrap_or(i32::MAX)
2293 } else {
2294 i32::try_from(s.chars().count()).unwrap_or(i32::MAX)
2295 };
2296 Value::Int(n)
2297 }
2298 // v7.39 (bpchar epic) — length(bpchar) counts with the
2299 // trailing blanks stripped (length('ab'::char(5)) = 2).
2300 Value::BpChar(s) => {
2301 let t = s.trim_end_matches(' ');
2302 let n = if t.is_ascii() {
2303 i32::try_from(t.len()).unwrap_or(i32::MAX)
2304 } else {
2305 i32::try_from(t.chars().count()).unwrap_or(i32::MAX)
2306 };
2307 Value::Int(n)
2308 }
2309 Value::Bytes(b) => Value::Int(i32::try_from(b.len()).unwrap_or(i32::MAX)),
2310 other => {
2311 return Err(EvalError::TypeMismatch {
2312 detail: format!(
2313 "length() needs text or bytea, got {}",
2314 crate::conversions::pg_type_name_for_error_opt(other.data_type())
2315 ),
2316 });
2317 }
2318 };
2319 stack.push(pushed);
2320 }
2321 Step::ColumnOctetLength { pos } => {
2322 let v = row.get(*pos).unwrap_or(&Value::Null);
2323 let pushed = match v {
2324 Value::Null => Value::Null,
2325 // v7.39 (bpchar epic) — octet_length(bpchar) counts the
2326 // PADDED stored form.
2327 Value::Text(s) | Value::BpChar(s) => {
2328 Value::Int(i32::try_from(s.len()).unwrap_or(i32::MAX))
2329 }
2330 Value::Bytes(b) => Value::Int(i32::try_from(b.len()).unwrap_or(i32::MAX)),
2331 other => {
2332 return Err(EvalError::TypeMismatch {
2333 detail: format!(
2334 "octet_length() needs text or bytea, got {}",
2335 crate::conversions::pg_type_name_for_error_opt(other.data_type())
2336 ),
2337 });
2338 }
2339 };
2340 stack.push(pushed);
2341 }
2342 Step::Function { name_lower, n_args } => {
2343 crate::bump_counter!(STEP_VM_FUNCTION_FIRE);
2344 let start = stack.len().saturating_sub(*n_args);
2345 // `apply_function` borrows the trailing `n_args`
2346 // values off the stack; we then truncate + push the
2347 // result. `name_lower` is pre-lowercased at compile
2348 // time, so dispatch skips the per-row
2349 // `to_ascii_lowercase()` allocation.
2350 // v7.37.9 T3 S6 — apply_function_lower signature relaxed
2351 // to `&[Value<'_>]`; pass the borrowed stack slice
2352 // directly. Eliminates the Vec materialise + per-arg
2353 // String::clone that S1 introduced as a placeholder.
2354 let result =
2355 super::functions::apply_function_lower(name_lower, &stack[start..], ctx)?;
2356 stack.truncate(start);
2357 stack.push(result);
2358 }
2359 Step::Coalesce { n_args } => {
2360 let start = stack.len().saturating_sub(*n_args);
2361 // The widening `COALESCE(1, 2.5)` needs only exists when the
2362 // non-null arguments carry MIXED types; inspected by ref, and
2363 // the mixed shapes fall to the owned arm that always did it.
2364 let mut mixed = false;
2365 let mut seen: Option<spg_storage::DataType> = None;
2366 for v in &stack[start..] {
2367 if let Some(t) = v.data_type() {
2368 match seen {
2369 None => seen = Some(t),
2370 Some(prev) if prev != t => {
2371 mixed = true;
2372 break;
2373 }
2374 Some(_) => {}
2375 }
2376 }
2377 }
2378 if mixed {
2379 let result =
2380 super::functions::apply_function_lower("coalesce", &stack[start..], ctx)?;
2381 stack.truncate(start);
2382 stack.push(result);
2383 } else {
2384 let chosen = stack[start..]
2385 .iter()
2386 .position(|v| !matches!(v, Value::Null));
2387 match chosen {
2388 Some(k) => {
2389 let v = stack.swap_remove(start + k);
2390 stack.truncate(start);
2391 stack.push(v);
2392 }
2393 None => {
2394 stack.truncate(start);
2395 stack.push(Value::Null);
2396 }
2397 }
2398 }
2399 }
2400 Step::Extremum { n_args, max } => {
2401 let start = stack.len().saturating_sub(*n_args);
2402 // Fast path: every non-NULL argument carries the SAME
2403 // concrete type — the comparison is the type's own and
2404 // the widen-to-common finish is the identity. Everything
2405 // else (mixed types, unknown-type text beside a typed
2406 // sibling, xid's refusal, MySQL's NULL-poisoning) falls
2407 // to the function arm unchanged.
2408 let mut uniform: Option<spg_storage::DataType> = None;
2409 let mut any_null = false;
2410 let mut fall_back = false;
2411 for v in &stack[start..] {
2412 if matches!(v, Value::Null) {
2413 any_null = true;
2414 continue;
2415 }
2416 if matches!(v, Value::Xid(_)) {
2417 fall_back = true;
2418 break;
2419 }
2420 match (v.data_type(), uniform) {
2421 (Some(t), None) => uniform = Some(t),
2422 (Some(t), Some(prev)) if t != prev => {
2423 fall_back = true;
2424 break;
2425 }
2426 (Some(_), Some(_)) => {}
2427 (None, _) => {
2428 fall_back = true;
2429 break;
2430 }
2431 }
2432 }
2433 if fall_back || (ctx.mysql_dialect && any_null) {
2434 let name = if *max { "greatest" } else { "least" };
2435 let result =
2436 super::functions::apply_function_lower(name, &stack[start..], ctx)?;
2437 stack.truncate(start);
2438 stack.push(result);
2439 } else {
2440 let mut best: Option<usize> = None;
2441 for k in start..stack.len() {
2442 if matches!(&stack[k], Value::Null) {
2443 continue;
2444 }
2445 match best {
2446 None => best = Some(k),
2447 Some(b) => {
2448 let ord = super::values::value_cmp_for_min_max(
2449 &stack[b],
2450 &stack[k],
2451 ctx.mysql_dialect,
2452 );
2453 let take = if *max {
2454 ord == core::cmp::Ordering::Less
2455 } else {
2456 ord == core::cmp::Ordering::Greater
2457 };
2458 if take {
2459 best = Some(k);
2460 }
2461 }
2462 }
2463 }
2464 match best {
2465 Some(k) => {
2466 let v = stack.swap_remove(k);
2467 stack.truncate(start);
2468 stack.push(v);
2469 }
2470 None => {
2471 stack.truncate(start);
2472 stack.push(Value::Null);
2473 }
2474 }
2475 }
2476 }
2477 Step::NullIf => {
2478 let n = stack.len();
2479 // NULLIF is `=` under the hood and keeps round 238's refusal
2480 // of incomparable operands; both reads are by reference.
2481 let verdict = match (&stack[n - 2], &stack[n - 1]) {
2482 (Value::Null, _) => Some(true),
2483 (_, Value::Null) => Some(false),
2484 (a, b) => {
2485 super::binop::require_comparable(spg_sql::ast::BinOp::Eq, a, b)?;
2486 match super::apply_binary_by_ref(spg_sql::ast::BinOp::Eq, a, b)? {
2487 Some(Value::Bool(eq)) => Some(eq),
2488 _ => None,
2489 }
2490 }
2491 };
2492 match verdict {
2493 Some(true) => {
2494 stack.truncate(n - 2);
2495 stack.push(Value::Null);
2496 }
2497 Some(false) => {
2498 let a = stack.swap_remove(n - 2);
2499 stack.truncate(n - 2);
2500 stack.push(a);
2501 }
2502 // The by-ref compare could not decide — the owned arm can.
2503 None => {
2504 let result =
2505 super::functions::apply_function_lower("nullif", &stack[n - 2..], ctx)?;
2506 stack.truncate(n - 2);
2507 stack.push(result);
2508 }
2509 }
2510 }
2511 Step::Cast { target } => {
2512 crate::bump_counter!(STEP_VM_CAST_FIRE);
2513 // v7.39 (round 621) — two allocations a row lived on this one
2514 // line: `into_owned()` cloned a borrowed text cell just to
2515 // hand it to the cast, and `target.clone()` re-built the
2516 // target (a String, for the Named form) EVERY row even though
2517 // it is a compile product. `count(s::TEXT)` — a cast that
2518 // changes nothing — measured 2.00 allocs/row and 12 ms where
2519 // `count(s)` measures 0.00 and 2.8 ms.
2520 //
2521 // A cast that is an identity on the value it was given hands
2522 // the borrowed value straight back; everything else takes the
2523 // owned path, with the target passed by reference.
2524 let v = stack.pop().unwrap_or(Value::Null);
2525 if cast_is_identity_for(&v, target) {
2526 stack.push(v);
2527 } else {
2528 stack.push(super::cast::cast_value_ref_in(
2529 v.into_owned(),
2530 target,
2531 ctx.mysql_dialect,
2532 )?);
2533 }
2534 }
2535 Step::CastPlain { dt, name } => {
2536 let v = stack.pop().unwrap_or(Value::Null);
2537 // The name is pre-validated (it came off the plain table),
2538 // so NULL keeps its short-circuit; a same-type value passes
2539 // through untouched, exactly the identity the Cast step
2540 // recognises.
2541 let identity = matches!(
2542 (&v, dt),
2543 (Value::Null, _)
2544 | (Value::Int(_), spg_storage::DataType::Int)
2545 | (Value::BigInt(_), spg_storage::DataType::BigInt)
2546 | (Value::SmallInt(_), spg_storage::DataType::SmallInt)
2547 | (Value::Real(_), spg_storage::DataType::Real)
2548 | (Value::Float(_), spg_storage::DataType::Float)
2549 | (Value::Bool(_), spg_storage::DataType::Bool)
2550 | (Value::Date(_), spg_storage::DataType::Date)
2551 | (Value::Uuid(_), spg_storage::DataType::Uuid)
2552 );
2553 if identity {
2554 stack.push(v);
2555 } else {
2556 stack.push(super::cast::finish_named_cast_plain(
2557 v.into_owned(),
2558 *dt,
2559 name,
2560 ctx.mysql_dialect,
2561 )?);
2562 }
2563 }
2564 Step::Case {
2565 operand,
2566 branches,
2567 else_branch,
2568 } => {
2569 crate::bump_counter!(STEP_VM_CASE_FIRE);
2570 // v7.37.5-A2b — short-circuit Case executor. Mirrors
2571 // `Expr::Case` interpreter semantics bit-for-bit (each
2572 // WHEN evaluates with its own scratch stack; first
2573 // match wins; ELSE = NULL when absent). The outer
2574 // `stack` is reused (truncated back to its pre-Case
2575 // mark after each sub-program); allocator-free per
2576 // branch — the prior version allocated a fresh
2577 // `Vec<Value>` per sub-program which showed up as
2578 // ~3 % `drop_in_place<Vec<Value>>` self time.
2579 let mark = stack.len();
2580 // v7.37.9 T3 S7 — Case sub-program lifetime threads
2581 // through naturally via S1's `'row: 'val`. Operand /
2582 // when / matched / else results are pushed by sub-progs
2583 // into our same stack; we pop them as `Value<'val>` and
2584 // keep them at that lifetime instead of forcing
2585 // into_owned. The simple-form operand match (Eq) uses
2586 // apply_binary_by_ref to avoid the operand clone +
2587 // pop-side into_owned the S1 placeholder was paying.
2588 let operand_value: Option<Value<'val>> = if let Some(op) = operand {
2589 eval_compiled_ref_into(op, row, ctx, stack, mark)?;
2590 Some(stack.pop().unwrap_or(Value::Null))
2591 } else {
2592 None
2593 };
2594 stack.truncate(mark);
2595 let mut matched_value: Option<Value<'val>> = None;
2596 for (when_c, then_c) in branches {
2597 eval_compiled_ref_into(when_c, row, ctx, stack, mark)?;
2598 let when_v = stack.pop().unwrap_or(Value::Null);
2599 stack.truncate(mark);
2600 let matched = match &operand_value {
2601 None => matches!(when_v, Value::Bool(true)),
2602 Some(op_v) => {
2603 // Try the by-ref comparison fast path; fall
2604 // back to owning apply_binary only if the
2605 // by-ref path returns None (non-comparison
2606 // op, which Eq never is).
2607 let eq_result =
2608 match super::apply_binary_by_ref(BinOp::Eq, op_v, &when_v)? {
2609 Some(v) => v,
2610 None => apply_binary(
2611 BinOp::Eq,
2612 op_v.clone().into_owned(),
2613 when_v.clone().into_owned(),
2614 )?,
2615 };
2616 matches!(eq_result, Value::Bool(true))
2617 }
2618 };
2619 if matched {
2620 eval_compiled_ref_into(then_c, row, ctx, stack, mark)?;
2621 matched_value = Some(stack.pop().unwrap_or(Value::Null));
2622 stack.truncate(mark);
2623 break;
2624 }
2625 }
2626 let v: Value<'val> = match matched_value {
2627 Some(v) => v,
2628 None => match else_branch {
2629 Some(el) => {
2630 eval_compiled_ref_into(el, row, ctx, stack, mark)?;
2631 let v = stack.pop().unwrap_or(Value::Null);
2632 stack.truncate(mark);
2633 v
2634 }
2635 None => Value::Null,
2636 },
2637 };
2638 stack.push(v);
2639 }
2640 Step::CoerceCommon(target) => {
2641 let v = stack.pop().unwrap_or(Value::Null).into_owned();
2642 stack.push(super::widen_value_to(v, *target));
2643 }
2644 Step::Subtree(e) => stack.push(eval_expr(e, &row.as_row(), ctx)?),
2645 }
2646 }
2647 Ok(())
2648}
2649
2650/// v7.37.9 Phase 1A-ext-2 T1 — Step VM internal step-type counters.
2651/// Read-only diagnostic; gates no behaviour. Used by counter_dump.rs
2652/// to ground-truth subtree CSE / column-ref-push / multi-spec-combine
2653/// attack ROI estimates.
2654pub static STEP_VM_CALL_COUNT: core::sync::atomic::AtomicU64 =
2655 core::sync::atomic::AtomicU64::new(0);
2656pub static STEP_VM_STEPS_TOTAL: core::sync::atomic::AtomicU64 =
2657 core::sync::atomic::AtomicU64::new(0);
2658pub static STEP_VM_COLUMN_FIRE: core::sync::atomic::AtomicU64 =
2659 core::sync::atomic::AtomicU64::new(0);
2660pub static STEP_VM_LIT_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
2661pub static STEP_VM_BINARY_FIRE: core::sync::atomic::AtomicU64 =
2662 core::sync::atomic::AtomicU64::new(0);
2663pub static STEP_VM_FUNCTION_FIRE: core::sync::atomic::AtomicU64 =
2664 core::sync::atomic::AtomicU64::new(0);
2665pub static STEP_VM_CAST_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
2666pub static STEP_VM_CASE_FIRE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0);
2667
2668/// v7.37.9 Round 3 — heap-alloc counters specifically for the T3
2669/// structural attack's ROI estimate. Step::Column / Step::Lit hits
2670/// pay a String alloc when the cell variant is heap-bearing
2671/// (Text/Bytes/Json/Vector). T3 stack-lifetime push-by-borrow
2672/// would eliminate these for the bulk of per-row work.
2673pub static STEP_VM_COLUMN_HEAP_ALLOC: core::sync::atomic::AtomicU64 =
2674 core::sync::atomic::AtomicU64::new(0);
2675pub static STEP_VM_LIT_HEAP_ALLOC: core::sync::atomic::AtomicU64 =
2676 core::sync::atomic::AtomicU64::new(0);
2677
2678/// v7.39 (round 481) — how many values the stack still holds when a call
2679/// finishes, and how many are heap-bearing.
2680///
2681/// Round 480 left `drop_glue<Value>` at 16 % of self time with the drops
2682/// attributed to the predicate closure, i.e. to the stack rather than to
2683/// the returned value (round 479 removed that one). Whether the ops leave
2684/// operands behind for the next call's `clear()` to drop is a question
2685/// with a number, so this counts it rather than reasoning about it — the
2686/// previous round was spent acting on an inference that turned out to name
2687/// an unreachable branch.
2688/// v7.39 (round 482) — how often the `<column> <cmp> <literal>` fast
2689/// predicate fires, so "is it even reached" is a number and not a guess
2690/// (round 480 was spent on a branch that turned out to be unreachable).
2691pub static STEP_VM_FASTPRED_FIRE: core::sync::atomic::AtomicU64 =
2692 core::sync::atomic::AtomicU64::new(0);
2693
2694/// r1021 — how often the integer lane answers, and how often a row makes it
2695/// hand back. Round 480 was spent on a branch that turned out unreachable,
2696/// so "is it even reached" stays a number here too — and the fallback
2697/// counter is the one that matters for correctness review: it is every row
2698/// the lane declined to decide.
2699pub static STEP_VM_INTLANE_FIRE: core::sync::atomic::AtomicU64 =
2700 core::sync::atomic::AtomicU64::new(0);
2701pub static STEP_VM_INTLANE_FALLBACK: core::sync::atomic::AtomicU64 =
2702 core::sync::atomic::AtomicU64::new(0);
2703
2704pub static STEP_VM_STACK_LEFTOVER: core::sync::atomic::AtomicU64 =
2705 core::sync::atomic::AtomicU64::new(0);
2706pub static STEP_VM_STACK_LEFTOVER_HEAP: core::sync::atomic::AtomicU64 =
2707 core::sync::atomic::AtomicU64::new(0);
2708
2709#[cfg(test)]
2710mod like_substring_tests {
2711 use super::{like_substring_match, like_substring_shape};
2712
2713 fn shape(p: &str) -> Option<(usize, alloc::string::String, usize)> {
2714 let chars: alloc::vec::Vec<char> = p.chars().collect();
2715 like_substring_shape(&chars)
2716 }
2717
2718 #[test]
2719 fn shape_recognition() {
2720 assert_eq!(shape("%_05%"), Some((1, "05".into(), 0)));
2721 assert_eq!(shape("%abc%"), Some((0, "abc".into(), 0)));
2722 assert_eq!(shape("%ab_%"), Some((0, "ab".into(), 1)));
2723 assert_eq!(shape("%%x%%"), Some((0, "x".into(), 0)));
2724 assert_eq!(shape("%__a__%"), Some((2, "a".into(), 2)));
2725 // Not eligible: missing anchors, inner %, escapes, empty literal.
2726 assert_eq!(shape("ab%"), None);
2727 assert_eq!(shape("%ab"), None);
2728 assert_eq!(shape("%a%b%"), None);
2729 assert_eq!(shape("%___%"), None);
2730 assert_eq!(shape("%a\\%b%"), None);
2731 assert_eq!(shape("%"), None);
2732 }
2733
2734 #[test]
2735 fn matcher_semantics() {
2736 // %_05% — needs one char before "05".
2737 assert!(like_substring_match("x05", "05", 1, 0));
2738 assert!(!like_substring_match("05", "05", 1, 0));
2739 assert!(like_substring_match("ab05cd", "05", 1, 0));
2740 // Overlapping / repeated hits: first hit fails the k-check,
2741 // a later one passes.
2742 assert!(like_substring_match("05x05", "05", 1, 0));
2743 // Trailing underscore needs one char after.
2744 assert!(like_substring_match("abz", "ab", 0, 1));
2745 assert!(!like_substring_match("ab", "ab", 0, 1));
2746 // Plain substring.
2747 assert!(like_substring_match("hello", "ell", 0, 0));
2748 assert!(!like_substring_match("hello", "xyz", 0, 0));
2749 // Multi-byte chars count as single wildcard chars.
2750 assert!(like_substring_match("é05", "05", 1, 0));
2751 assert!(!like_substring_match("é5", "05", 1, 0));
2752 }
2753}