spg_engine/eval.rs
1//! Expression evaluator. Given a parsed `Expr`, a `Row`, and the row's column
2//! schema, produce a `Value`. v0.4 implements:
3//!
4//! - literals
5//! - column lookups (bare and qualified `t.col`)
6//! - unary minus / NOT
7//! - binary arithmetic, comparison, AND, OR
8//! - numeric widening (`Int → BigInt → Float`) at evaluation time
9//! - SQL three-valued logic for NULL:
10//! * any arithmetic / comparison op with a NULL operand → NULL
11//! * `TRUE OR NULL` → TRUE, `FALSE OR NULL` → NULL,
12//! * `FALSE AND NULL` → FALSE, `TRUE AND NULL` → NULL,
13//! * `NOT NULL` → NULL
14//!
15//! v0.4 deliberately does *not* implement: function calls, string
16//! concatenation, IS NULL / IS NOT NULL, BETWEEN, IN, etc. Those come later.
17
18use alloc::borrow::Cow;
19use alloc::format;
20use alloc::string::{String, ToString};
21use alloc::vec::Vec;
22
23use spg_sql::ast::{BinOp, CastTarget, ColumnName, Expr, Literal};
24use spg_storage::{ColumnSchema, Row, Value};
25
26pub(crate) mod binop;
27mod cast;
28pub mod compiled;
29mod datetime;
30mod encoding;
31mod encodings;
32mod format;
33pub(crate) mod functions;
34mod inet;
35pub(crate) mod math;
36mod regexp;
37mod resolve;
38mod strings;
39pub(crate) mod textsearch;
40pub(crate) mod values;
41
42pub use crate::conversions::format_money_array;
43pub(crate) use binop::{
44 add_interval_to_micros, and_3vl, apply_binary, apply_binary_by_ref, apply_binary_interval,
45};
46use binop::{apply_binary_in, apply_unary, compare, pow10_i128};
47pub use cast::{cast_to_vector, cast_value, parse_vector_text};
48pub(crate) use compiled::{
49 CompiledExpr, compile_column_pos, compile_expr, eval_compiled, eval_compiled_ref,
50 fully_compilable,
51};
52use datetime::{
53 age, date_format_mysql, date_part, date_trunc, extract_field, from_unixtime, unix_timestamp_of,
54};
55use encoding::{decode_text, encode_text};
56pub use format::{
57 days_from_civil, format_bigint_array, format_bool_array, format_bytea_array, format_bytea_hex,
58 format_date, format_date_array, format_float, format_float_array, format_int_array,
59 format_interval, format_interval_array, format_money, format_numeric, format_numeric_array,
60 format_numeric_kind, format_real, format_smallint_array, format_text_array, format_time,
61 format_timestamp, format_timestamp_array, format_timestamptz, format_timestamptz_at,
62 format_timetz, format_uuid_array, parse_date_literal, parse_timestamp_literal,
63};
64// v7.39 (GUC knife 3) — session render styles + styled formatters.
65pub use format::{
66 DateOrder, DateStyleKind, IntervalStyleKind, RenderStyle, format_date_array_styled,
67 format_date_styled, format_float_array_styled, format_float_styled,
68 format_interval_array_styled, format_interval_styled, format_real_styled,
69 format_timestamp_array_styled, format_timestamp_styled, format_timestamptz_styled,
70 format_timestamptz_tz, parse_date_literal_ordered, parse_timestamp_literal_ordered,
71 parse_timestamp_literal_tz_ordered,
72};
73use functions::apply_function;
74use inet::{inet_host, inet_masklen, inet_network, inet_op_bool_result};
75pub(crate) use math::{f64_ceil, f64_floor, f64_sqrt};
76use math::{
77 f64_exp, f64_ln, f64_powi, f64_round_half_away, f64_trunc, prng_next_f64, prng_next_u64,
78};
79pub(crate) use regexp::{
80 CompiledRe, compile_re, compiled_is_match, regex_is_match, regexp_matches_rows,
81};
82use regexp::{regexp_matches, regexp_replace, regexp_split_to_array};
83use resolve::{
84 collation_fold_for_compare, compare_is_case_insensitive, composite_eq, eval_expr_cow,
85 is_owned_compare_value, resolve_column, resolve_column_borrowed, text_prefix_chars,
86};
87pub(crate) use resolve::{
88 column_at, column_collation, find_column_pos, is_binary_coerced, locate_column,
89};
90use strings::{
91 TrimSide, format_string, pg_quote_ident, pg_quote_literal, pg_typeof_name, string_left_right,
92 string_pad, string_trim, to_char, value_to_format_text,
93};
94pub use textsearch::{
95 decode_tsquery_external, decode_tsvector_external, format_tsquery, format_tsvector,
96};
97use textsearch::{
98 fts_phraseto_tsquery, fts_plainto_tsquery, fts_setweight, fts_to_tsquery, fts_to_tsvector,
99 fts_ts_headline, fts_ts_rank, fts_ts_rank_cd, fts_ts_rewrite, fts_tsquery_bool,
100 fts_websearch_to_tsquery, ts_match, tsvector_concat,
101};
102pub use values::gen_random_uuid_bytes;
103/// v7.39 (tz epic) — fixed-offset / abbreviation resolution, exposed
104/// for `SET timezone` validation (named zones go through the host tzdb).
105pub(crate) fn datetime_resolve_zone_offset(z: &str) -> Option<i64> {
106 datetime::resolve_zone_offset(z)
107}
108
109pub use values::value_to_text;
110pub use values::value_to_text_styled;
111pub use values::value_to_text_typed;
112pub use values::value_to_text_typed_styled;
113pub use values::value_to_text_with_fsp;
114use values::{
115 array_2d_dims, array_element_at, array_len, array_rebuild, value_cmp_for_min_max, value_to_f64,
116 values_equal_for_nullif,
117};
118
119/// Resolution context for evaluating a single row. `table_alias` is the alias
120/// (or table name) callers should accept as the qualifier on a column ref —
121/// e.g. `FROM users AS u` makes `u.name` valid and rejects `other.name`.
122#[derive(Clone)]
123#[allow(missing_debug_implementations)] // sequence_resolver is a dyn Fn — no Debug
124pub struct EvalContext<'a> {
125 pub columns: &'a [ColumnSchema],
126 pub table_alias: Option<&'a str>,
127 /// v6.1.1 — bound parameters for `$N` placeholders inside the
128 /// expression tree. Empty for simple queries; populated by the
129 /// prepared-statement Execute path with Bind values converted
130 /// to `Value`. Index N (1-based per PG) hits `params[N-1]`.
131 pub params: &'a [Value<'static>],
132 /// v7.12.1 — session text-search config (from `SET
133 /// default_text_search_config = '<name>'`). Resolved when the
134 /// engine builds an `EvalContext` and consumed by the FTS
135 /// function dispatcher when `to_tsvector(text)` /
136 /// `plainto_tsquery(text)` etc are called without an explicit
137 /// config arg. `None` falls through to `simple`.
138 pub default_text_search_config: Option<&'a str>,
139 /// v7.17.0 Phase 1.1 — `nextval` / `currval` / `setval`
140 /// resolver. The engine builds this around a `&mut Catalog`
141 /// so apply_function can mutate sequence state without
142 /// eval owning a catalog reference. When `None`, sequence
143 /// functions return an error (read-only contexts).
144 pub sequence_resolver: Option<&'a SequenceResolver<'a>>,
145 /// v7.37.16 (16.12) — read-only catalog reference for
146 /// builtins that need catalog walks (e.g. `pg_partition_root`,
147 /// `pg_partition_ancestors`). `None` falls through to the
148 /// "no catalog available" branch which returns NULL — same
149 /// shape PG returns for a non-existent OID. Most evaluation
150 /// sites don't need catalog access (row scans, projections);
151 /// they construct contexts with `catalog: None` and the
152 /// engine populates `Some(&self.catalog)` only at the engine's
153 /// top-level entry points where the borrow is unambiguous.
154 pub catalog: Option<&'a spg_storage::Catalog>,
155 /// v7.39 (round 346, M1) — is this a MySQL-dialect session? The two
156 /// dialects disagree about what counts as a truth value: MariaDB
157 /// takes any non-zero number (and a string's leading number) as
158 /// true, PG refuses anything that is not boolean. Set from the
159 /// engine by [`EvalContext::with_engine`]; a context built without
160 /// one keeps PG's stricter reading.
161 pub mysql_dialect: bool,
162 /// Session GUCs set via `SET name = value` / `set_config`, keyed by
163 /// lowercased name. `current_setting('app.foo')` reads custom
164 /// (namespaced) settings from here — the mechanism apps use for
165 /// request context / RLS. `None` in read-only contexts that have no
166 /// session; unknown names then fall through to PG defaults.
167 pub session_gucs: Option<&'a alloc::collections::BTreeMap<String, String>>,
168 /// v7.39 (read01 round 58) — the engine's role store, so
169 /// `has_table_privilege('bob', …)` can expand bob's role MEMBERSHIPS (a
170 /// grant to a group role answers `true` for its inheriting members). `None`
171 /// in a context with no engine behind it — the role then stands alone.
172 pub users: Option<&'a crate::users::UserStore>,
173 /// v7.39 (read01 round 61) — how deep we are inside USER-DEFINED function
174 /// bodies. A function's body is evaluated with a child context, and a body
175 /// may call another function, so this bounds the recursion (a function that
176 /// calls itself would otherwise blow the stack, which an embed host cannot
177 /// catch).
178 pub fn_depth: u16,
179 /// v7.39 (read01 round 63) — the ENGINE, for a user-function body that has
180 /// its own FROM (`SELECT v FROM t WHERE id = k`). Such a body has to run
181 /// through the real executor: reading `catalog`'s rows straight from eval
182 /// would bypass the row-header visibility filter, so under in-place MVCC a
183 /// function would happily read DEAD rows. `None` in a context with no
184 /// engine behind it — a body with a FROM then errors, saying so.
185 pub engine: Option<&'a crate::Engine>,
186 /// v7.38 (read01 U15) — per-scan deterministic sampler state for
187 /// `TABLESAMPLE … REPEATABLE(seed)`. A fresh cell is created before a
188 /// scan whose predicate may draw `__tsm_fract(seed)`; the cell holds
189 /// `None` until the first draw seeds it from that literal, then a
190 /// scan-local xorshift sequence (isolated from the process-global
191 /// `random()` PRNG, so it's deterministic and rescan-stable). `None`
192 /// here means no sampler is attached.
193 pub sample_rng: Option<&'a core::cell::Cell<Option<u64>>>,
194 /// v7.38 (read01 P3.25) — native-stack-overflow guard. Lazily seeded
195 /// with the stack pointer of the outermost `eval_expr` call; deeper
196 /// calls compare their own pointer against it and bail with
197 /// [`EvalError::StackDepthExceeded`] once usage crosses a safe margin,
198 /// so a pathologically nested expression errors instead of aborting the
199 /// process. Owned (not a borrowed cell) so it stays stack-local and
200 /// never touches `Engine`'s `Sync` bound.
201 pub recursion_base: core::cell::Cell<usize>,
202 /// v7.39 (GUC knife 3) — session render style (DateStyle /
203 /// IntervalStyle / extra_float_digits) for text output produced
204 /// inside expression evaluation (`::text` casts). Contexts built
205 /// away from the session (per-shard scan filters, index probes)
206 /// keep the default — they don't render text output.
207 pub render_style: crate::eval::format::RenderStyle,
208 /// v7.39 (tz epic) — host IANA timezone lookups for named zones
209 /// (session rendering, AT TIME ZONE, literal zone suffixes).
210 pub tz_offset_fn: Option<crate::TzOffsetFn>,
211 pub tz_localize_fn: Option<crate::TzLocalizeFn>,
212 pub tz_abbrev_fn: Option<crate::TzAbbrevFn>,
213 /// v7.38 (read01 P5.24) — host-provided CSPRNG (the server injects
214 /// `/dev/urandom`). Cryptographic builtins (`gen_random_bytes`,
215 /// `gen_salt`) draw from this instead of the process-static xorshift
216 /// PRNG, so their output isn't predictable. `None` (no host CSPRNG)
217 /// falls back to the PRNG — fine for the non-cryptographic `random()`.
218 pub salt_fn: Option<crate::SaltFn>,
219 /// v7.39 (read01 pgstatfuncs.c) — calling-connection identity for
220 /// pg_backend_pid(); `None` (embedded / detached contexts) → 1.
221 pub backend_pid_fn: Option<crate::BackendPidFn>,
222 /// v7.39 (round 476) — the WAL byte position, for the LSN functions.
223 pub wal_lsn_fn: Option<crate::WalLsnFn>,
224 /// v7.39 (round 318, V51) — host connection-control hook for
225 /// `pg_cancel_backend` / `pg_terminate_backend`. `None` (embedded /
226 /// detached contexts) ⇒ there is nothing to signal, so they answer
227 /// false rather than pretending the signal landed.
228 pub backend_signal_fn: Option<crate::BackendSignalFn>,
229 /// v7.38 (read01 P6.08) — host wall clock (µs since Unix epoch). `uuidv7`
230 /// uses it for the real time-ordered 48-bit millisecond prefix; `None`
231 /// (no host clock) falls back to the deterministic anchor.
232 pub clock: Option<crate::ClockFn>,
233 /// v7.38 (T24) — read-only view of the engine's transaction-version state,
234 /// so the `txid_*` / `pg_*_xact_id` / `pg_xact_status` builtins report the
235 /// real transaction ids instead of a constant stub. `None` on the scan /
236 /// join / aggregate contexts that never evaluate them.
237 pub xact: Option<XactView<'a>>,
238 /// v7.38 (T24) — PG's `txid_current()` ASSIGNS an id to a transaction that
239 /// has none. In autocommit a read-only statement has no writer version, so
240 /// the first call allocates one here and later calls in the same statement
241 /// reuse it — `SELECT txid_current(), txid_current()` must agree, as in PG.
242 pub assigned_xid: core::cell::Cell<Option<u64>>,
243}
244
245/// v7.38 (T24) — the transaction-id surface PG's `txid_*` family exposes.
246/// SPG's writer versions ARE its transaction ids (`row_header::next_version`),
247/// so no separate xid counter is needed — this is the bridge U22 was waiting
248/// on.
249#[derive(Clone, Copy, Debug)]
250pub struct XactView<'a> {
251 /// The id assigned to the current transaction (allocated at BEGIN) or, in
252 /// autocommit, to the current statement once it has written. `None` when
253 /// nothing has been assigned — `*_if_assigned` returns NULL there, as PG does.
254 pub current: Option<u64>,
255 /// Ids allocated by transactions that have neither committed nor aborted.
256 pub active: &'a alloc::collections::BTreeSet<u64>,
257 /// Ids of rolled-back transactions.
258 pub aborted: &'a alloc::collections::BTreeSet<u64>,
259}
260
261/// v7.17.0 — sequence-mutating callback used by `apply_function`
262/// for `nextval` / `currval` / `setval`. Implemented by the
263/// engine to thread `&mut Catalog` access through an immutable
264/// `&EvalContext`.
265pub type SequenceResolver<'a> = dyn Fn(SequenceOp) -> Result<i64, EvalError> + 'a;
266
267/// v7.17.0 — sequence operation requested by an Expr eval.
268#[derive(Debug, Clone)]
269pub enum SequenceOp {
270 Next(String),
271 Curr(String),
272 Set {
273 name: String,
274 value: i64,
275 is_called: bool,
276 },
277}
278
279impl<'a> EvalContext<'a> {
280 pub const fn new(columns: &'a [ColumnSchema], table_alias: Option<&'a str>) -> Self {
281 Self {
282 columns,
283 table_alias,
284 params: &[],
285 default_text_search_config: None,
286 sequence_resolver: None,
287 catalog: None,
288 mysql_dialect: false,
289 session_gucs: None,
290 users: None,
291 fn_depth: 0,
292 engine: None,
293 sample_rng: None,
294 recursion_base: core::cell::Cell::new(0),
295 render_style: crate::eval::format::RenderStyle {
296 date_style: crate::eval::format::DateStyleKind::Iso,
297 date_order: crate::eval::format::DateOrder::Mdy,
298 interval_style: crate::eval::format::IntervalStyleKind::Postgres,
299 extra_float_digits: 1,
300 bytea_escape: false,
301 mysql: false,
302 },
303 tz_offset_fn: None,
304 tz_localize_fn: None,
305 tz_abbrev_fn: None,
306 salt_fn: None,
307 backend_pid_fn: None,
308 wal_lsn_fn: None,
309 backend_signal_fn: None,
310 clock: None,
311 xact: None,
312 assigned_xid: core::cell::Cell::new(None),
313 }
314 }
315
316 /// v7.39 (GUC knife 3) — attach the session render style.
317 #[must_use]
318 pub const fn with_render_style(mut self, style: crate::eval::format::RenderStyle) -> Self {
319 self.render_style = style;
320 self
321 }
322
323 /// v7.39 (round 318, V51) — attach the host connection-control hook.
324 #[must_use]
325 pub const fn with_backend_signal_fn(mut self, f: Option<crate::BackendSignalFn>) -> Self {
326 self.backend_signal_fn = f;
327 self
328 }
329
330 /// v7.39 (round 476) — attach the WAL byte-position provider.
331 #[must_use]
332 pub const fn with_wal_lsn_fn(mut self, f: Option<crate::WalLsnFn>) -> Self {
333 self.wal_lsn_fn = f;
334 self
335 }
336
337 /// v7.39 (read01 pgstatfuncs.c) — attach the calling-connection id.
338 #[must_use]
339 pub const fn with_backend_pid_fn(mut self, f: Option<crate::BackendPidFn>) -> Self {
340 self.backend_pid_fn = f;
341 self
342 }
343
344 /// v7.39 (tz epic) — attach the host timezone lookups.
345 #[must_use]
346 pub const fn with_tz_fns(
347 mut self,
348 offset: Option<crate::TzOffsetFn>,
349 localize: Option<crate::TzLocalizeFn>,
350 abbrev: Option<crate::TzAbbrevFn>,
351 ) -> Self {
352 self.tz_offset_fn = offset;
353 self.tz_localize_fn = localize;
354 self.tz_abbrev_fn = abbrev;
355 self
356 }
357
358 /// v7.39 (tz epic) — offset (µs east) of an arbitrary zone spec at
359 /// a UTC instant: fixed forms resolve statically, named zones
360 /// through the host tzdb. None = unknown zone.
361 #[must_use]
362 pub fn zone_offset_at(&self, zone: &str, utc_micros: i64) -> Option<i64> {
363 if let Some(off) = datetime::resolve_zone_offset(zone) {
364 return Some(off);
365 }
366 self.tz_offset_fn.and_then(|f| f(zone, utc_micros))
367 }
368
369 /// v7.39 (tz epic) — the SESSION zone's offset at a UTC instant
370 /// (per-value: DST zones vary within one statement).
371 #[must_use]
372 pub fn session_tz_offset_at(&self, utc_micros: i64) -> i64 {
373 let Some(zone) = self.session_gucs.and_then(|g| g.get("timezone")) else {
374 return 0;
375 };
376 self.zone_offset_at(zone, utc_micros).unwrap_or(0)
377 }
378
379 /// v7.39 (tz epic) — the session zone's designation at an instant
380 /// (named zones only; None lets renderers spell UTC / +HH).
381 #[must_use]
382 pub fn session_tz_abbrev_at(&self, utc_micros: i64) -> Option<alloc::string::String> {
383 let zone = self.session_gucs.and_then(|g| g.get("timezone"))?;
384 if datetime::resolve_zone_offset(zone).is_some()
385 || zone.eq_ignore_ascii_case("utc")
386 || zone.eq_ignore_ascii_case("gmt")
387 {
388 return None;
389 }
390 self.tz_abbrev_fn.and_then(|f| f(zone, utc_micros))
391 }
392
393 /// v7.39 (tz epic) — local wall micros in `zone` -> UTC micros
394 /// (PG's DST disambiguation for named zones).
395 #[must_use]
396 pub fn zone_local_to_utc(&self, zone: &str, local_micros: i64) -> Option<i64> {
397 zone_local_to_utc_with(zone, local_micros, self.tz_localize_fn)
398 }
399
400 /// v7.38 (read01 P5.24) — attach the host CSPRNG so cryptographic
401 /// builtins don't fall back to the predictable PRNG.
402 #[must_use]
403 pub const fn with_salt_fn(mut self, f: Option<crate::SaltFn>) -> Self {
404 self.salt_fn = f;
405 self
406 }
407
408 /// v7.38 (read01 P6.08) — attach the host wall clock so `uuidv7` gets a
409 /// real time-ordered prefix instead of the deterministic anchor.
410 #[must_use]
411 pub const fn with_clock(mut self, f: Option<crate::ClockFn>) -> Self {
412 self.clock = f;
413 self
414 }
415
416 /// v7.38 (read01 U15) — attach a per-scan `TABLESAMPLE REPEATABLE`
417 /// sampler cell. The cell (seeded lazily on first `__tsm_fract` draw)
418 /// must outlive the context and be created fresh per scan so a rescan
419 /// re-seeds and reproduces the same sample.
420 #[must_use]
421 pub const fn with_sample_rng(mut self, cell: &'a core::cell::Cell<Option<u64>>) -> Self {
422 self.sample_rng = Some(cell);
423 self
424 }
425
426 /// Attach the session's GUC map so `current_setting` can resolve
427 /// custom (namespaced) settings written with `SET` / `set_config`.
428 #[must_use]
429 /// v7.39 (read01 round 63) — thread the engine (see `engine`).
430 pub const fn with_engine(mut self, engine: &'a crate::Engine) -> Self {
431 self.mysql_dialect = engine.backslash_escapes;
432 // v7.39 (round 368, M20 P3) — the dialect also decides how a binary
433 // string renders in a string context (latin-1 bytes vs PG `\x…`).
434 self.render_style.mysql = engine.backslash_escapes;
435 self.engine = Some(engine);
436 self
437 }
438
439 /// v7.39 (read01 round 58) — thread the role store (see `users`).
440 pub const fn with_users(mut self, users: &'a crate::users::UserStore) -> Self {
441 self.users = Some(users);
442 self
443 }
444
445 /// v7.39 (round 524) — attach a whole session bag at once. Every
446 /// write path needs the same four, and taking them one at a time is
447 /// how three of them ended up with none.
448 #[must_use]
449 pub(crate) fn with_session<'b: 'a>(mut self, s: &'b DmlSession) -> Self {
450 self.session_gucs = Some(&s.gucs);
451 self.users = Some(&s.users);
452 self.render_style = s.render_style;
453 self.tz_offset_fn = s.tz_offset_fn;
454 self.tz_localize_fn = s.tz_localize_fn;
455 self.tz_abbrev_fn = s.tz_abbrev_fn;
456 self
457 }
458
459 pub const fn with_session_gucs(
460 mut self,
461 gucs: &'a alloc::collections::BTreeMap<String, String>,
462 ) -> Self {
463 self.session_gucs = Some(gucs);
464 self
465 }
466
467 /// v7.37.16 (16.12) — attach a read-only catalog reference
468 /// so builtins like `pg_partition_root` can walk partition
469 /// roles. Defaults to None (NULL semantics).
470 #[must_use]
471 pub const fn with_catalog(mut self, catalog: &'a spg_storage::Catalog) -> Self {
472 self.catalog = Some(catalog);
473 self
474 }
475
476 /// v7.38 (T-tstz Phase 2) — the micro-offset of the session `TimeZone` GUC
477 /// (`SET TimeZone = '+09'` → +9h). A fixed offset / abbreviation resolves;
478 /// UTC and an unset GUC give 0; a named IANA zone (no tzdata) also gives 0
479 /// so a timestamptz still renders — as `+00` — rather than erroring on
480 /// every display. Timestamptz rendering / cast is the only consumer.
481 #[must_use]
482 pub fn session_tz_offset(&self) -> i64 {
483 self.session_gucs
484 .and_then(|g| g.get("timezone"))
485 .and_then(|z| datetime::resolve_zone_offset(z))
486 .unwrap_or(0)
487 }
488
489 /// v7.38 (T24) — attach the transaction-version view the `txid_*` builtins
490 /// read. Defaults to None, where they fall back to the process-wide cursor.
491 #[must_use]
492 pub const fn with_xact(mut self, xact: XactView<'a>) -> Self {
493 self.xact = Some(xact);
494 self
495 }
496
497 /// v7.17.0 — attach a sequence resolver. The engine wraps a
498 /// `&mut Catalog` in a closure that performs the requested
499 /// SequenceOp.
500 #[must_use]
501 pub const fn with_sequence_resolver(mut self, resolver: &'a SequenceResolver<'a>) -> Self {
502 self.sequence_resolver = Some(resolver);
503 self
504 }
505
506 /// v6.1.1 — attach a parameter buffer for `$N` placeholder
507 /// resolution. The slice must outlive the context; callers
508 /// construct it from the prepared statement's Bind values.
509 #[must_use]
510 pub const fn with_params(mut self, params: &'a [Value<'static>]) -> Self {
511 self.params = params;
512 self
513 }
514
515 /// v7.12.1 — attach the session's
516 /// `default_text_search_config`. Used by the FTS function
517 /// dispatcher when no explicit config arg is given.
518 #[must_use]
519 pub const fn with_default_text_search_config(mut self, cfg: Option<&'a str>) -> Self {
520 self.default_text_search_config = cfg;
521 self
522 }
523}
524
525/// v7.39 (round 523) — read a timestamp literal, reporting whether it
526/// carried an offset. Re-exported for the INSERT path, which decides
527/// there whether a value already names an instant.
528pub(crate) fn parse_timestamp_literal_tz_ordered_pub(
529 s: &str,
530 order: DateOrder,
531) -> Option<(i64, bool)> {
532 format::parse_timestamp_literal_tz_ordered(s, order)
533}
534
535/// v7.39 (round 523) — a FIXED zone's offset, when the name is one
536/// (`+09`, `UTC-5`). Named zones go through the host's tzdb instead.
537#[must_use]
538pub(crate) fn resolve_zone_offset_pub(zone: &str) -> Option<i64> {
539 datetime::resolve_zone_offset(zone)
540}
541
542/// v7.39 (round 523) — a wall-clock reading in `zone` as a UTC instant.
543///
544/// A free function because the INSERT path needs it too, and that path
545/// carries no `EvalContext`: it evaluates VALUES through a context-free
546/// literal walker. `EvalContext::zone_local_to_utc` delegates here so the
547/// two cannot drift.
548#[must_use]
549pub(crate) fn zone_local_to_utc_with(
550 zone: &str,
551 local_micros: i64,
552 localize: Option<crate::TzLocalizeFn>,
553) -> Option<i64> {
554 if let Some(off) = datetime::resolve_zone_offset(zone) {
555 return Some(local_micros - off);
556 }
557 localize.and_then(|f| f(zone, local_micros))
558}
559
560/// v7.39 (round 523) — the session zone an assignment to a timestamptz
561/// column is read in, or `None` when the session is on UTC and no shift
562/// applies.
563#[derive(Debug, Clone)]
564pub(crate) struct SessionCoercion {
565 /// The session zone, when it is not UTC. `None` leaves an instant
566 /// where it was.
567 pub zone: Option<alloc::string::String>,
568 pub localize: Option<crate::TzLocalizeFn>,
569 /// The session's date order. A written date is ambiguous
570 /// (`01/02/2020`), and this is what resolves it.
571 pub order: DateOrder,
572}
573
574impl SessionCoercion {
575 /// The UTC instant a naive wall-clock reading names in the session
576 /// zone, or `None` when the session is on UTC.
577 #[must_use]
578 pub(crate) fn wall_to_utc(&self, wall: i64) -> Option<i64> {
579 let zone = self.zone.as_ref()?;
580 zone_local_to_utc_with(zone, wall, self.localize)
581 }
582
583 /// v7.39 (round 524) — the session facts an ASSIGNMENT is read
584 /// under, from an evaluation context. `None` when both are the
585 /// defaults and nothing needs re-reading.
586 #[must_use]
587 pub(crate) fn from_ctx(ctx: &EvalContext<'_>) -> Option<Self> {
588 let zone = ctx
589 .session_gucs
590 .and_then(|g| g.get("timezone"))
591 .filter(|z| !z.eq_ignore_ascii_case("utc") && !z.eq_ignore_ascii_case("gmt"))
592 .cloned();
593 let order = ctx.render_style.date_order;
594 if zone.is_none() && order == DateOrder::Mdy {
595 return None;
596 }
597 Some(Self {
598 zone,
599 localize: ctx.tz_localize_fn,
600 order,
601 })
602 }
603}
604
605/// v7.39 (round 524) — the session facts a DML evaluation context needs,
606/// cloned so the row loop can still borrow the engine mutably.
607///
608/// Every write path built a BARE `EvalContext`, so an expression in an
609/// UPDATE's SET or a DELETE's WHERE was evaluated by an engine that knew
610/// nothing about the connection. One value, built once per statement,
611/// and a grep for `dml_session` finds every path that has it.
612pub(crate) struct DmlSession {
613 pub gucs: alloc::collections::BTreeMap<String, String>,
614 pub users: crate::users::UserStore,
615 pub render_style: RenderStyle,
616 pub tz_offset_fn: Option<crate::TzOffsetFn>,
617 pub tz_localize_fn: Option<crate::TzLocalizeFn>,
618 pub tz_abbrev_fn: Option<crate::TzAbbrevFn>,
619}
620
621/// v7.39 (round 524) — read a TEXT value bound for a temporal column
622/// under the session's date order.
623///
624/// `01/02/2020` is February 1st in a DMY session and January 2nd in an
625/// MDY one, and the write path was reading every one of them as MDY: a
626/// `SELECT '01/02/2020'::date` answered PG's value while the same
627/// literal INSERTed stored the day and month swapped. Nothing errors,
628/// and once stored the two readings are indistinguishable.
629#[must_use]
630pub(crate) fn session_read_temporal_text(
631 v: Value<'static>,
632 target: spg_storage::DataType,
633 coercion: Option<&SessionCoercion>,
634) -> Value<'static> {
635 use spg_storage::DataType as D;
636 let Some(c) = coercion else { return v };
637 if c.order == DateOrder::Mdy {
638 return v;
639 }
640 let Value::Text(s) = &v else { return v };
641 match target {
642 D::Date => format::parse_date_literal_ordered(s, c.order).map_or(v, Value::Date),
643 D::Timestamp | D::Timestamptz => format::parse_timestamp_literal_tz_ordered(s, c.order)
644 .map_or(v, |(t, _)| Value::Timestamp(t)),
645 _ => v,
646 }
647}
648
649#[derive(Debug, Clone, PartialEq)]
650pub enum EvalError {
651 ColumnNotFound {
652 name: String,
653 },
654 UnknownQualifier {
655 qualifier: String,
656 },
657 DivisionByZero,
658 TypeMismatch {
659 detail: String,
660 },
661 /// v6.1.1 — `$N` reference past the number of bound parameters.
662 /// Either the client sent too few in Bind, or the SQL has a
663 /// placeholder the prepared statement didn't account for.
664 PlaceholderOutOfRange {
665 n: u16,
666 bound: u16,
667 },
668 /// v7.38 (read01 P3.25) — the expression tree recursed deep enough to
669 /// threaten a native stack overflow; we bail out with an error the way
670 /// PG's `check_stack_depth()` does instead of aborting the process.
671 StackDepthExceeded,
672}
673
674impl core::fmt::Display for EvalError {
675 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
676 match self {
677 // v7.39 (read01 round 81) — PG's wording (and SQLSTATE trigger):
678 // `column "x" does not exist`, 42703. The old "column not found: x"
679 // matched none of the wire layer's `does not exist` patterns, so a
680 // missing column reached the client as the generic error class.
681 Self::ColumnNotFound { name } => write!(f, "column \"{name}\" does not exist"),
682 // v7.39 (round 241) — PG's wording (and 42P01 trigger): a
683 // qualifier that names no table in scope is "missing
684 // FROM-clause entry for table \"x\"". The old "unknown table
685 // qualifier" matched nothing a driver branches on.
686 Self::UnknownQualifier { qualifier } => {
687 write!(f, "missing FROM-clause entry for table \"{qualifier}\"")
688 }
689 Self::DivisionByZero => f.write_str("division by zero"),
690 Self::TypeMismatch { detail } => write!(f, "type mismatch: {detail}"),
691 Self::PlaceholderOutOfRange { n, bound } => write!(
692 f,
693 "parameter ${n} referenced but only {bound} bound by client"
694 ),
695 Self::StackDepthExceeded => {
696 f.write_str("stack depth limit exceeded (expression nested too deeply)")
697 }
698 }
699 }
700}
701
702/// v7.38 (read01 P3.25) — native-stack budget below the outermost
703/// `eval_expr` frame. Native stacks are typically 2–8 MB; 768 KiB leaves
704/// generous headroom while still permitting PG-class nesting depth (in a
705/// release build ~hundreds-to-thousands of frames fit under this).
706const MAX_EVAL_STACK_BYTES: usize = 768 * 1024;
707
708/// Address of a local in the current frame — a portable stand-in for the
709/// stack pointer (stacks grow downward on all supported targets).
710#[inline(never)]
711fn eval_stack_ptr() -> usize {
712 let probe = 0u8;
713 core::ptr::addr_of!(probe) as usize
714}
715
716/// v7.38 (read01 P6.40) — enforce a user DOMAIN's NOT NULL + CHECK constraints
717/// on a value being cast to it (`x::domain`). NULL fails a NOT NULL domain;
718/// otherwise every CHECK (which references the pseudo-column `VALUE`) must not
719/// evaluate to false. Returns the value unchanged when all constraints pass.
720fn apply_domain_constraints<'a>(
721 v: Value<'a>,
722 dom: &spg_storage::DomainDef,
723 name: &str,
724 cat: &spg_storage::Catalog,
725) -> Result<Value<'a>, EvalError> {
726 if matches!(v, Value::Null) {
727 // A NOT NULL anywhere in the chain rejects a NULL.
728 let mut cur = Some(dom);
729 while let Some(d) = cur {
730 if !d.nullable {
731 return Err(EvalError::TypeMismatch {
732 detail: alloc::format!("domain {name} does not allow null values"),
733 });
734 }
735 cur = d
736 .base_domain
737 .as_ref()
738 .and_then(|p| cat.domain_types().get(p.as_str()));
739 }
740 return Ok(v);
741 }
742 // v7.39 (round 259) — walk the domain chain BASE-FIRST (probed: a
743 // value violating both a parent's and the child's constraint reports
744 // the PARENT's). The message names the domain being cast TO, but the
745 // constraint that actually failed — `value for domain pchild violates
746 // check constraint "pbase_check"`.
747 let mut chain: alloc::vec::Vec<&spg_storage::DomainDef> = alloc::vec![dom];
748 let mut cur = dom;
749 while let Some(parent) = cur
750 .base_domain
751 .as_ref()
752 .and_then(|p| cat.domain_types().get(p.as_str()))
753 {
754 // A cycle cannot be created through CREATE DOMAIN (the parent must
755 // already exist), but stop defensively rather than loop forever.
756 if chain.iter().any(|d| core::ptr::eq(*d, parent)) {
757 break;
758 }
759 chain.push(parent);
760 cur = parent;
761 }
762 chain.reverse();
763 for owner in chain {
764 apply_domain_checks_of(&v, owner, name)?;
765 }
766 Ok(v)
767}
768
769/// v7.39 (round 259) — run ONE domain's own CHECK list against `v`. The
770/// error names `target` (the domain the value is being cast to) and
771/// `owner` (whose constraint failed); for a single-level domain they are
772/// the same, which is the pre-259 wording.
773fn apply_domain_checks_of(
774 v: &Value<'_>,
775 dom: &spg_storage::DomainDef,
776 target: &str,
777) -> Result<(), EvalError> {
778 let name = target;
779 for chk in &dom.checks {
780 let src = &chk.expr;
781 // v7.39 (round 260) — report the constraint that failed by NAME.
782 let owner = chk.name.as_str();
783 let expr = spg_sql::parser::parse_expression(src).map_err(|e| EvalError::TypeMismatch {
784 detail: alloc::format!("domain {name} CHECK ({src:?}) failed to re-parse: {e:?}"),
785 })?;
786 let synth_cols = alloc::vec![spg_storage::ColumnSchema::new(
787 "value",
788 dom.base_type,
789 dom.nullable,
790 )];
791 let synth_ctx = EvalContext::new(&synth_cols, None);
792 // Owned copy so the temporary row doesn't borrow `v`'s lifetime.
793 let synth_row = spg_storage::Row {
794 values: alloc::vec![v.clone().into_owned()],
795 };
796 let r = eval_expr(&expr, &synth_row, &synth_ctx)?;
797 if matches!(r, Value::Bool(false)) {
798 return Err(EvalError::TypeMismatch {
799 detail: alloc::format!(
800 "value for domain {name} violates check constraint \"{owner}\""
801 ),
802 });
803 }
804 }
805 Ok(())
806}
807
808/// v7.38 (read01 P6.67) — validate a value cast to a user ENUM: a text label
809/// must be one of the enum's members (else error, as PG does); a NULL is a
810/// valid typed null. The stored representation stays the text label.
811/// v7.39 (read01 rowtypes.c) — cast into a user composite type: parse the
812/// `(v1,"v 2",)` record text (double-quote wrapping with doubled quotes,
813/// empty field = NULL) and coerce each field to the declared type; a ROW
814/// value re-labels positionally.
815/// v7.39 (round 350/351, M7 + M11) — how MySQL reads a TEXT operand of
816/// an arithmetic or comparison operator. The identity in the PG dialect,
817/// and out-of-line so it costs the recursive `eval_expr` frame nothing.
818///
819/// Measured on MariaDB 11: `'2024-01-15' + INTERVAL 1 DAY` shifts the
820/// date; `'1abc'+0` is 1, `'abc'+0` is 0, `'2024-01-15'+0` is 2024; two
821/// strings compare as STRINGS (`'10' > '9'` is 0) while a mixed pair
822/// compares numerically (`'10' > 9` is 1).
823#[inline(never)]
824pub(crate) fn mysql_operand_reading_pair(
825 op: BinOp,
826 l: Value<'static>,
827 r: Value<'static>,
828) -> (Value<'static>, Value<'static>) {
829 if !mysql_coerces(op) {
830 return (l, r);
831 }
832 match (&l, &r) {
833 (Value::Text(t), Value::Interval { .. }) => (text_as_temporal(t).unwrap_or(l.clone()), r),
834 (Value::Interval { .. }, Value::Text(t)) => {
835 let rr = text_as_temporal(t).unwrap_or(r.clone());
836 (l, rr)
837 }
838 // v7.39 (round 353, M10) — a boolean IS an integer in MySQL, so
839 // `!1 + 1` is 1 (measured). It was `operator does not exist:
840 // boolean + integer`.
841 (Value::Bool(b), other)
842 if mysql_arith(op) && other.data_type().is_some_and(is_numeric_type) =>
843 {
844 (Value::BigInt(i64::from(*b)), r)
845 }
846 (other, Value::Bool(b))
847 if mysql_arith(op) && other.data_type().is_some_and(is_numeric_type) =>
848 {
849 let rr = Value::BigInt(i64::from(*b));
850 (l, rr)
851 }
852 (Value::Text(t), other) if other.data_type().is_some_and(is_numeric_type) => {
853 (mysql_number_of(t), r)
854 }
855 (other, Value::Text(t)) if other.data_type().is_some_and(is_numeric_type) => {
856 let rr = mysql_number_of(t);
857 (l, rr)
858 }
859 // v7.39 (round 367, M20 P2) — a binary string beside a number
860 // reads as its big-endian integer value (`0x10 + 0` = 16,
861 // `0x10 = 16` is true). Beside a Text operand it stays bytes so
862 // the byte-wise string compare (`0x61 = 'a'`) still fires.
863 (Value::Bytes(b), other) if other.data_type().is_some_and(is_numeric_type) => {
864 (mysql_bytes_as_number(b), r)
865 }
866 (other, Value::Bytes(b)) if other.data_type().is_some_and(is_numeric_type) => {
867 let rr = mysql_bytes_as_number(b);
868 (l, rr)
869 }
870 // Arithmetic between two strings is numeric; comparison is not.
871 (Value::Text(a), Value::Text(b)) if mysql_arith(op) => {
872 (mysql_number_of(a), mysql_number_of(b))
873 }
874 _ => (l, r),
875 }
876}
877
878/// Is this a mixed string/number pair, which MySQL compares numerically?
879fn mysql_mixed_pair(l: &Value<'_>, r: &Value<'_>) -> bool {
880 matches!((l, r), (Value::Text(_), o) | (o, Value::Text(_))
881 if o.data_type().is_some_and(is_numeric_type))
882}
883
884const fn mysql_arith(op: BinOp) -> bool {
885 matches!(
886 op,
887 BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod
888 )
889}
890
891/// Does this comparison need the owned path — because a value's type
892/// wants it, because a CI collation folds it, or (MySQL) because a mixed
893/// string/number pair compares NUMERICALLY there while two strings
894/// compare as strings.
895#[inline(never)]
896fn needs_owned_compare(
897 lc: &Value<'_>,
898 rc: &Value<'_>,
899 lhs: &Expr,
900 rhs: &Expr,
901 ctx: &EvalContext<'_>,
902) -> bool {
903 is_owned_compare_value(lc)
904 || is_owned_compare_value(rc)
905 || compare_is_case_insensitive(lhs, rhs, ctx)
906 || (ctx.mysql_dialect && mysql_mixed_pair(lc, rc))
907}
908
909/// Which operators take MySQL's string→number reading.
910const fn mysql_coerces(op: BinOp) -> bool {
911 matches!(
912 op,
913 BinOp::Add
914 | BinOp::Sub
915 | BinOp::Mul
916 | BinOp::Div
917 | BinOp::Mod
918 | BinOp::Eq
919 | BinOp::NotEq
920 | BinOp::Lt
921 | BinOp::LtEq
922 | BinOp::Gt
923 | BinOp::GtEq
924 )
925}
926
927/// Does this type take part in MySQL's numeric coercion?
928fn is_numeric_type(t: spg_storage::DataType) -> bool {
929 use spg_storage::DataType as D;
930 matches!(
931 t,
932 D::SmallInt | D::Int | D::BigInt | D::Float | D::Real | D::Numeric { .. }
933 )
934}
935
936/// v7.39 (round 364, M4 P2) — a value as it participates in a MySQL
937/// session's default-collation comparison: text folds (accent- and
938/// case-insensitive), everything else is itself. Used by IN and LIKE,
939/// whose comparisons do not pass through `collation_fold_for_compare`.
940fn mysql_collation_key(v: Value<'static>, mysql: bool) -> Value<'static> {
941 match v {
942 // v7.38.16 — BpChar too. `mysql_compare_fold` trims trailing
943 // spaces before folding, which is the PAD SPACE half of the same
944 // comparison, so a CHAR cell needs exactly this call and was not
945 // getting it: `s IN ('ALPHA','BETA')` on CHAR(8) answered 1 where
946 // MySQL 9.7.1 answers 1,2. `eval/values.rs` had the pair right
947 // and these two sites did not.
948 // v7.38.17 — CHAR's padding is not data, TEXT's trailing
949 // spaces are. Two calls because they are two questions.
950 Value::BpChar(s) if mysql => Value::text(spg_storage::mysql_compare_fold_char(&s)),
951 Value::Text(s) if mysql => Value::text(spg_storage::mysql_compare_fold(&s)),
952 other => other,
953 }
954}
955
956/// A string as MySQL reads it in numeric position: an exact integer when
957/// the leading number is one, otherwise a double.
958#[inline(never)]
959fn mysql_number_of(s: &str) -> Value<'static> {
960 let n = mysql_leading_number(s);
961 if n.fract() == 0.0 && n.abs() < 9.007_199_254_740_992e15 {
962 #[allow(clippy::cast_possible_truncation)]
963 Value::BigInt(n as i64)
964 } else {
965 Value::Float(n)
966 }
967}
968
969/// v7.39 (round 367, M20 P2) — a MySQL binary string (a `0x…` / `X'…'` /
970/// `b'…'` literal, backed by `Value::Bytes`) reads as its bytes'
971/// BIG-ENDIAN unsigned integer in a numeric context: `0x4142 + 0` is
972/// 16706, `0x10 = 16` is true (measured on MariaDB 11). Only the low 16
973/// bytes participate — a hex literal used in arithmetic is at most an
974/// 8-byte BIGINT in practice — and a value past `i64::MAX` becomes a
975/// NUMERIC so nothing wraps negative.
976fn mysql_bytes_as_number(b: &[u8]) -> Value<'static> {
977 let start = b.len().saturating_sub(16);
978 let acc = b[start..]
979 .iter()
980 .fold(0u128, |a, &x| (a << 8) | u128::from(x));
981 if acc <= i64::MAX as u128 {
982 #[allow(clippy::cast_possible_truncation)]
983 Value::BigInt(acc as i64)
984 } else {
985 crate::conversions::big_literal_to_value(&alloc::format!("{acc}"))
986 }
987}
988
989/// MySQL's `/`: a real division, and NULL on a zero divisor. `None`
990/// when this pairing is not the integer/integer case PG and MySQL
991/// disagree about.
992#[inline(never)]
993pub(crate) fn mysql_true_division(
994 op: BinOp,
995 l: &Value<'_>,
996 r: &Value<'_>,
997 text_operand: bool,
998) -> Option<Value<'static>> {
999 // v7.39 (round 372) — MySQL's `x % 0` / `x MOD 0` is NULL, not the PG
1000 // "division by zero" error (measured on MariaDB 11: `10%0`, `10 MOD
1001 // 0`, `10.5%0` are all NULL, matching `1/0`). A non-zero divisor takes
1002 // the normal modulo path.
1003 if matches!(op, BinOp::Mod) {
1004 return if value_is_zero(r) {
1005 Some(Value::Null)
1006 } else {
1007 None
1008 };
1009 }
1010 if !matches!(op, BinOp::Div) {
1011 return None;
1012 }
1013 // v7.39 (round 393) — MariaDB `/` on exact (int / decimal) operands is a
1014 // DECIMAL whose scale is the LEFT operand's scale + 4 (`7/2` is 3.5000,
1015 // `10.0/3` is 3.33333, `7.00/2` is 3.500000), NOT a float. A float /
1016 // double operand — or a STRING one, `'10'/'4'` is 2.5 (double) — makes
1017 // the result a float; a zero divisor is NULL.
1018 if text_operand
1019 || matches!(l, Value::Float(_) | Value::Real(_))
1020 || matches!(r, Value::Float(_) | Value::Real(_))
1021 {
1022 let f = |v: &Value<'_>| -> Option<f64> {
1023 match v {
1024 Value::Float(x) => Some(*x),
1025 Value::Real(x) => Some(f64::from(*x)),
1026 Value::SmallInt(n) => Some(f64::from(*n)),
1027 Value::Int(n) => Some(f64::from(*n)),
1028 #[allow(clippy::cast_precision_loss)]
1029 Value::BigInt(n) => Some(*n as f64),
1030 _ => None,
1031 }
1032 };
1033 let (a, b) = (f(l)?, f(r)?);
1034 return Some(if b == 0.0 {
1035 Value::Null
1036 } else {
1037 Value::Float(a / b)
1038 });
1039 }
1040 let (ls, lsc) = exact_decimal_parts(l)?;
1041 let (rs, rsc) = exact_decimal_parts(r)?;
1042 if rs == 0 {
1043 return Some(Value::Null);
1044 }
1045 let result_scale = u32::from(lsc) + 4;
1046 // result_scaled = round( ls * 10^(rsc + 4) / rs ), half away from zero.
1047 let pow = 10i128.checked_pow(u32::from(rsc) + 4)?;
1048 let num = ls.checked_mul(pow)?;
1049 let q = num / rs;
1050 let rem = num % rs;
1051 let bump = if rem.unsigned_abs() * 2 >= rs.unsigned_abs() {
1052 if (num < 0) == (rs < 0) { 1 } else { -1 }
1053 } else {
1054 0
1055 };
1056 Some(Value::numeric(q + bump, u16::try_from(result_scale).ok()?))
1057}
1058
1059/// The `(scaled, scale)` of an exact integer / NUMERIC value: an integer
1060/// has scale 0. None for a float / non-numeric (they take the float path).
1061fn exact_decimal_parts(v: &Value<'_>) -> Option<(i128, u16)> {
1062 match v {
1063 Value::SmallInt(n) => Some((i128::from(*n), 0)),
1064 Value::Int(n) => Some((i128::from(*n), 0)),
1065 Value::BigInt(n) => Some((i128::from(*n), 0)),
1066 Value::Numeric {
1067 scaled,
1068 scale,
1069 kind: spg_storage::NumericKind::Finite,
1070 } => Some((*scaled, *scale)),
1071 _ => None,
1072 }
1073}
1074
1075/// v7.39 (round 383) — the UNSIGNED 64-bit value a MySQL bitwise operand
1076/// reads as. MySQL's `& | ^ ~ << >>` all work on `BIGINT UNSIGNED`, so an
1077/// operand is its 64-bit two's-complement pattern (a negative integer:
1078/// `-5` is `0xFFFF…FB`), rounded to the nearest integer (a float / numeric:
1079/// `2.9` is 3), its big-endian value (a `0x…` binary string), or its
1080/// leading number (a string). Anything else (an inet, a range, a
1081/// bit-string) returns None so the operator keeps its own meaning.
1082#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1083fn mysql_bit_u64(v: &Value<'_>) -> Option<u64> {
1084 match v {
1085 Value::SmallInt(n) => Some(i64::from(*n) as u64),
1086 Value::Int(n) => Some(i64::from(*n) as u64),
1087 Value::BigInt(n) => Some(*n as u64),
1088 Value::Bool(b) => Some(u64::from(*b)),
1089 Value::Float(x) => Some(x.round() as i64 as u64),
1090 Value::Real(x) => Some(f64::from(*x).round() as i64 as u64),
1091 Value::Numeric {
1092 scaled,
1093 scale,
1094 kind: spg_storage::NumericKind::Finite,
1095 } => {
1096 // Round half away from zero to an integer, then take the low
1097 // 64 bits (two's complement) — `~2.9` is `~3`.
1098 if *scale > 38 {
1099 return None;
1100 }
1101 let div = 10i128.pow(u32::from(*scale));
1102 let q = scaled / div;
1103 let rem = scaled % div;
1104 let rounded = if rem.unsigned_abs() * 2 >= div.unsigned_abs() {
1105 q + scaled.signum()
1106 } else {
1107 q
1108 };
1109 Some(rounded as u64)
1110 }
1111 Value::Bytes(b) => mysql_bit_u64(&mysql_bytes_as_number(b)),
1112 Value::Text(s) => mysql_bit_u64(&mysql_number_of(s)),
1113 _ => None,
1114 }
1115}
1116
1117/// A MySQL bitwise result — a `BIGINT UNSIGNED`. It stays a signed
1118/// `BigInt` while it fits (so an integer-context consumer — MAKE_SET
1119/// bits, ELT / SUBSTRING / REPEAT counts — still takes it); a value past
1120/// `i64::MAX` (a set bit 63, e.g. `~5`) has no signed integer type, so it
1121/// becomes a scale-0 NUMERIC, which holds the whole `0..=2^64-1` range and
1122/// renders as the plain integer MySQL prints.
1123fn u64_as_value(n: u64) -> Value<'static> {
1124 match i64::try_from(n) {
1125 Ok(v) => Value::BigInt(v),
1126 Err(_) => Value::numeric(i128::from(n), 0),
1127 }
1128}
1129
1130/// v7.39 (round 383) — the MySQL bitwise operators on UNSIGNED 64-bit
1131/// integers. Returns None when either operand is not number-like (so an
1132/// `inet << int` / `bit(n) & bit(n)` / geometric `#` keeps its own path)
1133/// or the operator is not bitwise. A shift of 64 or more is 0 (MySQL does
1134/// not mask the shift count).
1135pub(crate) fn mysql_bitwise(op: BinOp, l: &Value<'_>, r: &Value<'_>) -> Option<Value<'static>> {
1136 let out = match op {
1137 BinOp::BitAnd => mysql_bit_u64(l)? & mysql_bit_u64(r)?,
1138 BinOp::BitOr => mysql_bit_u64(l)? | mysql_bit_u64(r)?,
1139 BinOp::BitXor => mysql_bit_u64(l)? ^ mysql_bit_u64(r)?,
1140 // `<<` / `>>` share the inet-containment BinOps; a numeric pair is a
1141 // shift, anything else stays inet / range / bit-string.
1142 BinOp::InetContainedBy => {
1143 let (a, n) = (mysql_bit_u64(l)?, mysql_bit_u64(r)?);
1144 if n >= 64 { 0 } else { a << n }
1145 }
1146 BinOp::InetContains => {
1147 let (a, n) = (mysql_bit_u64(l)?, mysql_bit_u64(r)?);
1148 if n >= 64 { 0 } else { a >> n }
1149 }
1150 _ => return None,
1151 };
1152 Some(u64_as_value(out))
1153}
1154
1155/// v7.39 (round 383) — MySQL unary `~x`: the UNSIGNED 64-bit complement
1156/// (`~5` is 18446744073709551610). None for a non-number operand (a
1157/// bit-string / inet / macaddr keeps PG's typed complement).
1158pub(crate) fn mysql_bit_not(v: &Value<'_>) -> Option<Value<'static>> {
1159 Some(u64_as_value(!mysql_bit_u64(v)?))
1160}
1161
1162/// v7.39 (round 390, type-fidelity epic P5) — the inline `SET('a','b',…)`
1163/// variant list an expression's column is declared with, or None. Mirrors
1164/// `expr_enum_type_name` — a bare `Expr::Column` looked up by name.
1165pub(crate) fn expr_set_variants<'e>(
1166 e: &'e Expr,
1167 columns: &'e [ColumnSchema],
1168) -> Option<&'e [String]> {
1169 match e {
1170 Expr::Column(c) => columns
1171 .iter()
1172 .find(|col| col.name == c.name)
1173 .and_then(|col| col.inline_set_variants.as_deref()),
1174 _ => None,
1175 }
1176}
1177
1178/// v7.39 (round 402) — the inline `ENUM('a','b',…)` variant list an
1179/// expression's column is declared with, or None. Like `expr_set_variants`.
1180pub(crate) fn expr_inline_enum_variants<'e>(
1181 e: &'e Expr,
1182 columns: &'e [ColumnSchema],
1183) -> Option<&'e [String]> {
1184 match e {
1185 Expr::Column(c) => columns
1186 .iter()
1187 .find(|col| col.name == c.name)
1188 .and_then(|col| col.inline_enum_variants.as_deref()),
1189 _ => None,
1190 }
1191}
1192
1193/// The 1-based ordinal a stored inline-ENUM text carries in a numeric
1194/// context (`e + 0` is 1 for the first member); the empty string / an
1195/// unknown member is 0 (MySQL's implicit `''` enum error value).
1196pub(crate) fn enum_text_to_ordinal(text: &str, variants: &[String]) -> i64 {
1197 variants
1198 .iter()
1199 .position(|v| v == text)
1200 .map_or(0, |p| p as i64 + 1)
1201}
1202
1203/// The bitmask a stored SET text carries in a numeric context: each
1204/// comma-separated member contributes `1 << its position` in the declared
1205/// variant list (`'a,c'` over `('a','b','c','d')` is 1 | 4 = 5). An empty
1206/// string is 0; an unknown member (should not occur — the write path
1207/// validates) contributes nothing.
1208pub(crate) fn set_text_to_bitmask(text: &str, variants: &[String]) -> i64 {
1209 if text.is_empty() {
1210 return 0;
1211 }
1212 let mut bits = 0i64;
1213 for member in text.split(',') {
1214 if let Some(pos) = variants.iter().position(|v| v == member) {
1215 bits |= 1i64 << pos;
1216 }
1217 }
1218 bits
1219}
1220
1221/// Is this an arithmetic / bitwise operator MySQL evaluates a SET column
1222/// numerically under? (`s + 0`, `s & flag`, …). The comparison operators
1223/// are NOT here — `s = 'a,c'` stays a text compare.
1224pub(crate) const fn is_mysql_numeric_binop(op: BinOp) -> bool {
1225 matches!(
1226 op,
1227 BinOp::Add
1228 | BinOp::Sub
1229 | BinOp::Mul
1230 | BinOp::Div
1231 | BinOp::Mod
1232 | BinOp::BitAnd
1233 | BinOp::BitOr
1234 | BinOp::BitXor
1235 | BinOp::InetContainedBy
1236 | BinOp::InetContains
1237 )
1238}
1239
1240/// v7.39 (round 372) — is `v` a numeric zero (any width / kind)? Used to
1241/// route `x % 0` / `MOD(x, 0)` to NULL under the MySQL dialect.
1242pub(crate) fn value_is_zero(v: &Value<'_>) -> bool {
1243 match v {
1244 Value::SmallInt(n) => *n == 0,
1245 Value::Int(n) => *n == 0,
1246 Value::BigInt(n) => *n == 0,
1247 Value::Float(x) => *x == 0.0,
1248 Value::Real(x) => *x == 0.0,
1249 Value::Numeric { scaled, .. } => *scaled == 0,
1250 _ => false,
1251 }
1252}
1253
1254/// `-'5'` in the MySQL dialect. Out-of-line for the same frame reason.
1255#[inline(never)]
1256fn mysql_negate_text(
1257 op: spg_sql::ast::UnOp,
1258 v: &Value<'static>,
1259) -> Option<Result<Value<'static>, EvalError>> {
1260 match v {
1261 Value::Text(t) => Some(apply_unary(op, mysql_number_of(t))),
1262 _ => None,
1263 }
1264}
1265
1266/// The MySQL reading of a unary operator, or None to let `apply_unary` (the
1267/// PG path) run. Kept out of `eval_expr`'s recursive frame — see the
1268/// round-383 frame cliff. Covers `NOT` on any truth value (round 346),
1269/// `-'str'` numeric negation (round 351), and the unsigned `~` complement
1270/// (round 383).
1271#[inline(never)]
1272fn mysql_unary_arm(
1273 op: spg_sql::ast::UnOp,
1274 v: &Value<'static>,
1275) -> Option<Result<Value<'static>, EvalError>> {
1276 use spg_sql::ast::UnOp;
1277 match op {
1278 // `NOT 5` is 0 — read any non-bool as a truth value; a bool / NULL
1279 // keeps the PG path (still refused there for non-bool).
1280 UnOp::Not if !matches!(v, Value::Bool(_) | Value::Null) => Some(mysql_not(v)),
1281 // `-'5'` is -5, `-'abc'` is 0.
1282 UnOp::Neg => mysql_negate_text(op, v),
1283 // `+ anything` is that thing: measured on MariaDB 11, `+'x'` is
1284 // 'x', `+TRUE` is 1, `+NULL` is NULL. No type check at all, unlike
1285 // PG, which refuses every non-numeric operand.
1286 UnOp::Plus => Some(Ok(v.clone())),
1287 // `~5` is the unsigned 64-bit complement; NULL stays NULL (PG path).
1288 UnOp::BitNot if !matches!(v, Value::Null) => mysql_bit_not(v).map(Ok),
1289 _ => None,
1290 }
1291}
1292
1293/// A date / timestamp string as its temporal value, or `None` when it is
1294/// not one (in which case the operand is left exactly as it was).
1295#[inline(never)]
1296fn text_as_temporal(t: &str) -> Option<Value<'static>> {
1297 parse_timestamp_literal(t)
1298 .map(Value::Timestamp)
1299 .or_else(|| parse_date_literal(t).map(Value::Date))
1300}
1301
1302/// v7.39 (round 620) — an unadorned string literal, which is what PG calls
1303/// `unknown`: a value whose type the context gets to choose. `''::TEXT` is
1304/// not one, and neither is a text column.
1305fn is_unknown_string_literal(e: &Expr) -> bool {
1306 matches!(e, Expr::Literal(spg_sql::ast::Literal::String(_)))
1307}
1308
1309/// v7.39 (round 620) — resolve such a literal to boolean, which is what a
1310/// boolean connective asks of it. An unparseable one is PG's input-syntax
1311/// error (22P02), not a type complaint: `'a' AND true` says
1312/// `invalid input syntax for type boolean: "a"`, exactly as `'a'::BOOLEAN`
1313/// does — same failure, same words, because it is the same coercion.
1314#[inline(never)]
1315fn coerce_unknown_literal_to_bool(e: &Expr) -> Result<Value<'static>, EvalError> {
1316 let Expr::Literal(spg_sql::ast::Literal::String(s)) = e else {
1317 unreachable!("guarded by is_unknown_string_literal")
1318 };
1319 cast::cast_value_in(
1320 Value::Text(s.clone().into()),
1321 spg_sql::ast::CastTarget::Bool,
1322 false,
1323 )
1324}
1325
1326/// v7.39 (round 621) — a literal that is plainly not a boolean, and the PG
1327/// type name for it. `NULL` and a bare string literal are deliberately absent:
1328/// neither carries a type of its own, and a boolean connective is a context
1329/// that gives them one.
1330fn non_boolean_literal_type(e: &Expr) -> Option<&'static str> {
1331 use spg_sql::ast::Literal as L;
1332 match e {
1333 Expr::Literal(L::Integer(_)) => Some("integer"),
1334 Expr::Literal(L::Float(_)) => Some("double precision"),
1335 Expr::Literal(L::Numeric { .. } | L::NumericBig(_)) => Some("numeric"),
1336 _ => None,
1337 }
1338}
1339
1340/// v7.39 (round 621) — `AND` / `OR`, evaluated the way PG evaluates them.
1341///
1342/// Round 620 handled the unknown literal here; round 621 adds the part that
1343/// makes `WHERE x <> 0 AND 1/x > 0` work at all. SPG evaluated both sides
1344/// always, so the guard idiom — the whole reason that predicate is written
1345/// that way — raised on the very rows the guard exists to exclude. Measured
1346/// against PG: `false AND (1/0 = 0)` answers `f`, `true OR (1/0 = 0)` answers
1347/// `t`, and a filter guarded that way returns its rows.
1348///
1349/// PG affords that AND still refuses `false AND 1`, because the two happen at
1350/// different times: the operand types are checked during ANALYSIS, before any
1351/// evaluation, and the short circuit is a RUN-TIME decision. Both parts are
1352/// here — the right-hand operand's type is read statically (it is the side
1353/// that may go unevaluated), and only a type that is definitively known and
1354/// definitively not boolean is refused. An unknown type is left alone, so a
1355/// shape the describer cannot type keeps the old behaviour rather than
1356/// earning a spurious error.
1357///
1358/// Order is PG's too, and it is strictly left-first: `(1/0 = 0) AND false`
1359/// raises on both, because the left is evaluated before anything can decide
1360/// that it did not need to be.
1361///
1362/// Out-of-line so it costs `eval_expr` no frame (the round-305 frame cliff).
1363#[inline(never)]
1364fn eval_connective(
1365 lhs: &Expr,
1366 op: BinOp,
1367 rhs: &Expr,
1368 row: &Row<'static>,
1369 ctx: &EvalContext<'_>,
1370) -> Result<Value<'static>, EvalError> {
1371 let side = |e: &Expr| -> Result<Value<'static>, EvalError> {
1372 if is_unknown_string_literal(e) {
1373 coerce_unknown_literal_to_bool(e)
1374 } else {
1375 eval_expr(e, row, ctx)
1376 }
1377 };
1378 let l = side(lhs)?;
1379 // The analysis-time half: refuse a right-hand operand that is plainly not
1380 // boolean, whether or not the short circuit would reach it.
1381 //
1382 // Only a LITERAL is read this way. The first cut asked
1383 // `describe_expr_type` for any expression's type, and it answers
1384 // confidently and wrongly for shapes that matter here — `NULL` comes back
1385 // as text, so `true AND NULL` earned a type error; and a MATCH … AGAINST
1386 // folds internally into an OR over tsvector operands, so full-text search
1387 // stopped working. Three existing pins caught all of it. A literal cannot
1388 // be misread, and it is what PG's own refusals in this area are about.
1389 if let Some(ty) = non_boolean_literal_type(rhs) {
1390 return Err(EvalError::TypeMismatch {
1391 detail: alloc::format!(
1392 "argument of {} must be type boolean, not type {ty}",
1393 if matches!(op, BinOp::And) {
1394 "AND"
1395 } else {
1396 "OR"
1397 },
1398 ),
1399 });
1400 }
1401 // Resolving an unknown literal belongs to the same half — it is a
1402 // coercion PG performs while analysing, so `false AND 'a'` says
1403 // `invalid input syntax for type boolean: "a"` rather than answering `f`.
1404 let rhs_resolved = if is_unknown_string_literal(rhs) {
1405 Some(coerce_unknown_literal_to_bool(rhs)?)
1406 } else {
1407 None
1408 };
1409 // The run-time half.
1410 match (op, &l) {
1411 (BinOp::And, Value::Bool(false)) => return Ok(Value::Bool(false)),
1412 (BinOp::Or, Value::Bool(true)) => return Ok(Value::Bool(true)),
1413 _ => {}
1414 }
1415 let r = match rhs_resolved {
1416 Some(v) => v,
1417 None => side(rhs)?,
1418 };
1419 if matches!(op, BinOp::And) {
1420 and_3vl(l, r)
1421 } else {
1422 apply_binary(op, l, r)
1423 }
1424}
1425
1426/// v7.39 (round 346, M1) — the MySQL reading of `AND` / `OR`, out-of-line
1427/// so it costs `eval_expr` no frame (see the round-305 frame cliff).
1428#[inline(never)]
1429fn eval_mysql_connective(
1430 lhs: &Expr,
1431 op: BinOp,
1432 rhs: &Expr,
1433 row: &Row<'static>,
1434 ctx: &EvalContext<'_>,
1435) -> Result<Value<'static>, EvalError> {
1436 let l = as_mysql_truth(eval_expr(lhs, row, ctx)?)?;
1437 let r = as_mysql_truth(eval_expr(rhs, row, ctx)?)?;
1438 apply_mysql_connective(op, l, r)
1439}
1440
1441/// v7.39 (round 407) — apply a MySQL logical connective (`AND` / `OR` /
1442/// `XOR`) to two operands already reduced to truth values (`Bool` or
1443/// `Null`). AND / OR reuse the dialect-blind `apply_binary`; `XOR` is
1444/// MySQL-only (no `apply_binary` arm) and computed here: NULL on either
1445/// side yields NULL, otherwise the exclusive-or of the two truth values.
1446pub(crate) fn apply_mysql_connective(
1447 op: BinOp,
1448 l: Value<'static>,
1449 r: Value<'static>,
1450) -> Result<Value<'static>, EvalError> {
1451 if op == BinOp::LogicalXor {
1452 return Ok(match (&l, &r) {
1453 (Value::Bool(a), Value::Bool(b)) => Value::Bool(a != b),
1454 _ => Value::Null,
1455 });
1456 }
1457 apply_binary(op, l, r)
1458}
1459
1460#[inline(never)]
1461pub(crate) fn as_mysql_truth(v: Value<'static>) -> Result<Value<'static>, EvalError> {
1462 Ok(match v {
1463 Value::Null => Value::Null,
1464 other => Value::Bool(predicate_is_true(&other, "AND", true)?),
1465 })
1466}
1467
1468/// The MySQL reading of `NOT`, likewise out-of-line.
1469#[inline(never)]
1470fn mysql_not(v: &Value<'_>) -> Result<Value<'static>, EvalError> {
1471 Ok(Value::Bool(!predicate_is_true(v, "NOT", true)?))
1472}
1473
1474/// v7.39 (round 346, M1) — is this value TRUE, in a position that wants a
1475/// truth value (WHERE / CASE WHEN / NOT / AND / OR / HAVING / ON)?
1476///
1477/// The engine used to write `matches!(v, Value::Bool(true))` at every such
1478/// position, so anything that was not already a boolean silently read as
1479/// FALSE. `SELECT CASE WHEN 1 THEN 'a' END` answered NULL and — far worse —
1480/// `SELECT … WHERE 1` returned **no rows at all**. Neither dialect does
1481/// that: MariaDB 11 takes any non-zero number as true, and PG 18.4 raises
1482/// `argument of WHERE must be type boolean, not type integer`.
1483///
1484/// NULL is not true (three-valued logic) and is not an error in either.
1485pub(crate) fn predicate_is_true(v: &Value<'_>, kw: &str, mysql: bool) -> Result<bool, EvalError> {
1486 match v {
1487 Value::Bool(b) => Ok(*b),
1488 Value::Null => Ok(false),
1489 _ if mysql => Ok(mysql_truthy(v)),
1490 // PG resolves a bare literal in this position through boolean
1491 // INPUT, so `CASE WHEN 'true'` is legal and `'abc'` is not.
1492 Value::Text(t) => match crate::eval::cast::cast_value(
1493 Value::text(t.to_string()),
1494 spg_sql::ast::CastTarget::Bool,
1495 )? {
1496 Value::Bool(b) => Ok(b),
1497 _ => Ok(false),
1498 },
1499 other => Err(EvalError::TypeMismatch {
1500 detail: alloc::format!(
1501 "argument of {kw} must be type boolean, not type {}",
1502 crate::eval::strings::pg_typeof_name(other)
1503 ),
1504 }),
1505 }
1506}
1507
1508/// MariaDB 11's reading, measured: a number is true when it is not zero
1509/// (`-1` and `0.5` are both true); a string contributes its LEADING
1510/// number, so `'1abc'` is true while `'abc'` and `''` are false.
1511fn mysql_truthy(v: &Value<'_>) -> bool {
1512 match v {
1513 Value::Bool(b) => *b,
1514 Value::Null => false,
1515 Value::SmallInt(n) => *n != 0,
1516 Value::Int(n) => *n != 0,
1517 Value::BigInt(n) => *n != 0,
1518 Value::Float(f) => *f != 0.0,
1519 Value::Real(f) => *f != 0.0,
1520 Value::Numeric { scaled, .. } => *scaled != 0,
1521 Value::Text(t) => mysql_leading_number(t) != 0.0,
1522 Value::BpChar(t) => mysql_leading_number(t) != 0.0,
1523 // Everything else converts to a non-zero number in MariaDB (a
1524 // DATE reads as its YYYYMMDD digits, for one).
1525 _ => true,
1526 }
1527}
1528
1529/// The leading numeric prefix of a string, MySQL-style: `'1abc'` is 1,
1530/// `'abc'` and `''` are 0.
1531#[inline(never)]
1532pub(crate) fn mysql_leading_number(s: &str) -> f64 {
1533 let t = s.trim_start();
1534 let mut end = 0usize;
1535 let mut seen_dot = false;
1536 let mut seen_digit = false;
1537 // v7.39 (round 351, M11) — the exponent form counts: MariaDB reads
1538 // `'1e3'` as 1000 and `'1.5e2'` as 150 (measured). A trailing `e`
1539 // with no digits after it is not part of the number (`'1e'` is 1).
1540 let mut seen_exp = false;
1541 let mut exp_at = 0usize;
1542 for (i, c) in t.char_indices() {
1543 match c {
1544 '-' | '+' if i == 0 => {}
1545 '-' | '+' if seen_exp && i == exp_at + 1 => {}
1546 '0'..='9' => seen_digit = true,
1547 '.' if !seen_dot && !seen_exp => seen_dot = true,
1548 'e' | 'E' if seen_digit && !seen_exp => {
1549 seen_exp = true;
1550 exp_at = i;
1551 }
1552 _ => break,
1553 }
1554 end = i + c.len_utf8();
1555 }
1556 if !seen_digit {
1557 return 0.0;
1558 }
1559 // Trim an exponent that never got its digits.
1560 let mut text = &t[..end];
1561 while !text.is_empty() && text.parse::<f64>().is_err() {
1562 text = &text[..text.len() - 1];
1563 }
1564 text.parse::<f64>().unwrap_or(0.0)
1565}
1566
1567/// v7.39 (read01 ruleutils.c) — resolve a relation name to its synthetic
1568/// oid: user tables in the 16384+ band (table_names order), views at
1569/// 32768+, and the synthesised system catalogs at their REAL PG oids.
1570/// `None` when the name is unknown (the caller keeps the legacy text
1571/// behaviour so `'anything'::regclass::text` still round-trips).
1572pub(crate) fn regclass_name_to_oid(cat: &spg_storage::Catalog, bare: &str) -> Option<i64> {
1573 // v7.39 (round 337, V62) — an INDEX and a SEQUENCE are relations too:
1574 // both have a `pg_class` row, so both answer to `::regclass` in PG.
1575 // v7.39 (round 338, V64) — and the bands live in ONE allocator now,
1576 // shared with the catalog synths, so `pg_class.oid = 'x'::regclass`
1577 // holds for every kind rather than only for tables.
1578 if let Some(oid) = crate::system_catalog::relation_oid(cat, bare) {
1579 return Some(oid);
1580 }
1581 Some(match bare {
1582 "pg_type" => 1247,
1583 "pg_attribute" => 1249,
1584 "pg_proc" => 1255,
1585 "pg_class" => 1259,
1586 "pg_database" => 1262,
1587 "pg_constraint" => 2606,
1588 "pg_index" => 2610,
1589 "pg_namespace" => 2615,
1590 // v7.39 (round 650) — the text-search catalogs. This list is a
1591 // hand-kept subset of `CATALOG_RELATIONS`, which is why adding a
1592 // catalog there was not enough for `'pg_ts_config'::regclass`.
1593 "pg_ts_config" => 3602,
1594 "pg_ts_config_map" => 3603,
1595 "pg_ts_dict" => 3600,
1596 "pg_ts_parser" => 3601,
1597 "pg_ts_template" => 3764,
1598 // 7.38.1 S5.1 — stop hand-copying: anything CATALOG_RELATIONS
1599 // publishes resolves here too (pg_dump's dependency pass casts
1600 // 'pg_extension' / 'pg_amop' / 'pg_opfamily'::regclass).
1601 other => {
1602 return crate::system_catalog::CATALOG_RELATIONS
1603 .iter()
1604 .find(|(n, _)| other.eq_ignore_ascii_case(n))
1605 .map(|(_, oid)| *oid);
1606 }
1607 })
1608}
1609
1610/// v7.39 (round 263) — crate-visible wrapper so the write path can
1611/// relabel + coerce a value into a composite column's declared type.
1612pub(crate) fn apply_composite_cast_pub(
1613 v: Value<'static>,
1614 comp: &spg_storage::CompositeDef,
1615 cat: Option<&spg_storage::Catalog>,
1616) -> Result<Value<'static>, EvalError> {
1617 apply_composite_cast_in(v, comp, cat)
1618}
1619
1620/// v7.39 (round 264) — resolve one field's value, recursing when the
1621/// field is itself a COMPOSITE. Without this a nested field kept the
1622/// inner record's TEXT rendering, so `(x).inner.street` errored and
1623/// `row_to_json` nested a string rather than an object.
1624fn coerce_composite_field(
1625 val: Value<'static>,
1626 fname: &str,
1627 fty: spg_storage::DataType,
1628 user_ty: Option<&str>,
1629 cat: Option<&spg_storage::Catalog>,
1630) -> Result<Value<'static>, EvalError> {
1631 if matches!(val, Value::Null) {
1632 return Ok(val);
1633 }
1634 if let Some(tn) = user_ty
1635 && let Some(inner) = cat.and_then(|c| c.composite_types().get(tn))
1636 {
1637 return apply_composite_cast_in(val, inner, cat);
1638 }
1639 crate::conversions::coerce_value(val, fty, fname, 0).map_err(|e| EvalError::TypeMismatch {
1640 detail: alloc::format!("{e}"),
1641 })
1642}
1643
1644fn apply_composite_cast(
1645 v: Value<'static>,
1646 comp: &spg_storage::CompositeDef,
1647) -> Result<Value<'static>, EvalError> {
1648 apply_composite_cast_in(v, comp, None)
1649}
1650
1651fn apply_composite_cast_in(
1652 v: Value<'static>,
1653 comp: &spg_storage::CompositeDef,
1654 cat: Option<&spg_storage::Catalog>,
1655) -> Result<Value<'static>, EvalError> {
1656 match v {
1657 Value::Null => Ok(Value::Null),
1658 Value::Composite(fields) => {
1659 if fields.len() != comp.fields.len() {
1660 // PG reports the SHAPE mismatch as a plain cast refusal.
1661 return Err(EvalError::TypeMismatch {
1662 detail: alloc::format!("cannot cast type record to {}", comp.name),
1663 });
1664 }
1665 // v7.39 (round 263) — relabel AND coerce: this branch only
1666 // renamed the fields, so `ROW('x','notanint')::addr` kept the
1667 // text in an int field and PG's input error never fired.
1668 let mut out: alloc::vec::Vec<(alloc::string::String, Value<'static>)> =
1669 alloc::vec::Vec::with_capacity(comp.fields.len());
1670 for (i, ((name, fty), (_, val))) in comp.fields.iter().zip(fields).enumerate() {
1671 let ut = comp.field_user_types.get(i).and_then(Option::as_deref);
1672 let coerced = coerce_composite_field(val, name, *fty, ut, cat)?;
1673 out.push((name.clone(), coerced));
1674 }
1675 Ok(Value::Composite(out))
1676 }
1677 Value::Text(s) => {
1678 let raw = parse_record_text(s.as_ref()).ok_or_else(|| EvalError::TypeMismatch {
1679 detail: alloc::format!("malformed record literal: \"{s}\""),
1680 })?;
1681 if raw.len() != comp.fields.len() {
1682 return Err(EvalError::TypeMismatch {
1683 detail: alloc::format!("malformed record literal: \"{s}\""),
1684 });
1685 }
1686 let mut out: alloc::vec::Vec<(alloc::string::String, Value<'static>)> =
1687 alloc::vec::Vec::with_capacity(raw.len());
1688 for (i, ((fname, fty), field_text)) in comp.fields.iter().zip(raw).enumerate() {
1689 let ut = comp.field_user_types.get(i).and_then(Option::as_deref);
1690 let val = match field_text {
1691 None => Value::Null,
1692 Some(t) => coerce_composite_field(Value::text(t), fname, *fty, ut, cat)?,
1693 };
1694 out.push((fname.clone(), val));
1695 }
1696 Ok(Value::Composite(out))
1697 }
1698 other => Err(EvalError::TypeMismatch {
1699 detail: alloc::format!(
1700 "cannot cast {} to composite type \"{}\"",
1701 crate::conversions::pg_type_name_for_error_opt(other.data_type()),
1702 comp.name
1703 ),
1704 }),
1705 }
1706}
1707
1708/// Split PG's record text `(f1,f2,...)` into per-field raw strings
1709/// (None = empty field = NULL). Double quotes wrap fields containing
1710/// metacharacters; `""` inside is a literal quote; a backslash escapes
1711/// the next character.
1712fn parse_record_text(s: &str) -> Option<alloc::vec::Vec<Option<alloc::string::String>>> {
1713 let t = s.trim();
1714 let inner = t.strip_prefix('(')?.strip_suffix(')')?;
1715 let mut out: alloc::vec::Vec<Option<alloc::string::String>> = alloc::vec::Vec::new();
1716 let chars: alloc::vec::Vec<char> = inner.chars().collect();
1717 let mut field = alloc::string::String::new();
1718 let mut quoted_seen = false;
1719 let mut i = 0usize;
1720 let mut in_quotes = false;
1721 loop {
1722 if i >= chars.len() {
1723 if in_quotes {
1724 return None;
1725 }
1726 out.push(if field.is_empty() && !quoted_seen {
1727 None
1728 } else {
1729 Some(field.clone())
1730 });
1731 break;
1732 }
1733 let c = chars[i];
1734 if in_quotes {
1735 match c {
1736 '"' if chars.get(i + 1) == Some(&'"') => {
1737 field.push('"');
1738 i += 2;
1739 }
1740 '"' => {
1741 in_quotes = false;
1742 i += 1;
1743 }
1744 '\\' => {
1745 field.push(*chars.get(i + 1)?);
1746 i += 2;
1747 }
1748 _ => {
1749 field.push(c);
1750 i += 1;
1751 }
1752 }
1753 } else {
1754 match c {
1755 '"' => {
1756 in_quotes = true;
1757 quoted_seen = true;
1758 i += 1;
1759 }
1760 ',' => {
1761 out.push(if field.is_empty() && !quoted_seen {
1762 None
1763 } else {
1764 Some(core::mem::take(&mut field))
1765 });
1766 quoted_seen = false;
1767 i += 1;
1768 }
1769 '\\' => {
1770 field.push(*chars.get(i + 1)?);
1771 i += 2;
1772 }
1773 _ => {
1774 field.push(c);
1775 i += 1;
1776 }
1777 }
1778 }
1779 }
1780 Some(out)
1781}
1782
1783fn apply_enum_cast<'a>(
1784 v: Value<'a>,
1785 en: &spg_storage::EnumDef,
1786 name: &str,
1787) -> Result<Value<'a>, EvalError> {
1788 match &v {
1789 Value::Null => Ok(v),
1790 Value::Text(s) => {
1791 if en.labels.iter().any(|l| l.as_str() == s.as_ref()) {
1792 Ok(v)
1793 } else {
1794 Err(EvalError::TypeMismatch {
1795 detail: alloc::format!("invalid input value for enum {name}: {s:?}"),
1796 })
1797 }
1798 }
1799 other => Err(EvalError::TypeMismatch {
1800 detail: alloc::format!(
1801 "cannot cast {} to enum {name}",
1802 crate::conversions::pg_type_name_for_error_opt(other.data_type())
1803 ),
1804 }),
1805 }
1806}
1807
1808/// v7.39 (read01 utils/adt, enum.c) — enum_first / enum_last /
1809/// enum_range resolved from the argument's STATIC enum type (an explicit
1810/// `::enumtype` cast or a column's `ColumnSchema.user_enum_type`) over the
1811/// catalog's member order. Returns None when no argument names a known
1812/// enum, letting the generic function path produce its usual error.
1813/// Out-of-line (`inline(never)`) so the sizable locals don't land in
1814/// `eval_expr`'s recursion frame.
1815/// Enum-ness lives outside the DataType lattice: the witness for "this
1816/// expression is enum-typed" is an explicit `::enumtype` cast or a column
1817/// whose `ColumnSchema.user_enum_type` is set.
1818/// v7.39 (round 258) — crate-visible wrapper so the projection builder
1819/// can keep an expression's enum identity (see `select.rs`).
1820pub(crate) fn expr_enum_type_name_pub<'e>(
1821 e: &'e Expr,
1822 columns: &'e [ColumnSchema],
1823) -> Option<&'e str> {
1824 expr_enum_type_name(e, columns)
1825}
1826
1827/// v7.39 (round 425) — the fractional-seconds precision a projected
1828/// expression should RENDER with: the widest declared precision among the
1829/// MySQL temporal columns it reads. `MAX(d3)` and `d3 + INTERVAL 1 SECOND`
1830/// both keep `d3`'s three digits, as MariaDB does. `None` when the
1831/// expression touches no such column, which leaves PG rendering untouched.
1832///
1833/// Residual (recorded, not modelled): MariaDB also WIDENS the precision from
1834/// some operands — `DATE_ADD(d3, INTERVAL 1 MICROSECOND)` prints six digits
1835/// there. Taking the max over referenced columns covers the common shapes
1836/// and never narrows below the source column.
1837pub(crate) fn expr_mysql_fsp(e: &Expr, columns: &[ColumnSchema]) -> Option<u8> {
1838 fn walk(e: &Expr, columns: &[ColumnSchema], best: &mut Option<u8>) {
1839 match e {
1840 Expr::Column(c) => {
1841 if let Some(f) = columns
1842 .iter()
1843 .find(|col| col.name == c.name)
1844 .and_then(|col| col.mysql_fsp)
1845 {
1846 *best = Some(best.map_or(f, |b: u8| b.max(f)));
1847 }
1848 }
1849 Expr::Binary { lhs, rhs, .. } => {
1850 walk(lhs, columns, best);
1851 walk(rhs, columns, best);
1852 }
1853 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, columns, best),
1854 Expr::FunctionCall { args, .. } => {
1855 for a in args {
1856 walk(a, columns, best);
1857 }
1858 }
1859 Expr::Case {
1860 operand,
1861 branches,
1862 else_branch,
1863 } => {
1864 if let Some(o) = operand.as_deref() {
1865 walk(o, columns, best);
1866 }
1867 for (w, t) in branches {
1868 walk(w, columns, best);
1869 walk(t, columns, best);
1870 }
1871 if let Some(el) = else_branch.as_deref() {
1872 walk(el, columns, best);
1873 }
1874 }
1875 _ => {}
1876 }
1877 }
1878 let mut best = None;
1879 walk(e, columns, &mut best);
1880 best
1881}
1882
1883/// v7.39 (round 467) — is this expression MySQL-UNSIGNED?
1884///
1885/// MySQL decides unsignedness statically, from the expression's type, not
1886/// from the value it happens to produce. Measured on MariaDB 11: `SUM(a) -
1887/// 100` answers -99 even though `a` is `INT UNSIGNED`, because SUM's result
1888/// type is not unsigned; `a - 5` on the same column raises 1690. So this
1889/// walks the expression the way MySQL's type resolution does.
1890///
1891/// A cast names its target `unsigned` (the parser lowercases MySQL's
1892/// `CAST(x AS UNSIGNED)` into `CastTarget::Named`). Arithmetic is unsigned
1893/// when EITHER operand is — that is MySQL's rule, and it is why `1 - b`
1894/// raises while `5 - a` does not: both are unsigned expressions, but only
1895/// the first has a negative result.
1896///
1897/// Deliberately NOT unsigned: unary minus (MariaDB answers -1 for
1898/// `-CAST(1 AS UNSIGNED)`), and every function result including the
1899/// aggregates. Both measured.
1900pub(crate) fn expr_is_mysql_unsigned(e: &Expr, columns: &[ColumnSchema]) -> bool {
1901 match e {
1902 Expr::Column(c) => columns
1903 .iter()
1904 .find(|col| col.name == c.name)
1905 .is_some_and(|col| col.is_unsigned),
1906 Expr::Cast {
1907 target: CastTarget::Named(n),
1908 ..
1909 } => n.eq_ignore_ascii_case("unsigned"),
1910 Expr::Binary {
1911 lhs,
1912 op: BinOp::Add | BinOp::Sub | BinOp::Mul,
1913 rhs,
1914 } => expr_is_mysql_unsigned(lhs, columns) || expr_is_mysql_unsigned(rhs, columns),
1915 _ => false,
1916 }
1917}
1918
1919/// v7.39 (round 467) — MySQL arithmetic over an UNSIGNED operand, with
1920/// MySQL's range check.
1921///
1922/// `INT UNSIGNED` columns holding 1 and 5 made `a - b` answer **-4** in a
1923/// MySQL session. MariaDB raises `ERROR 1690 (22003): BIGINT UNSIGNED value
1924/// is out of range`. A negative answer where the server promises a
1925/// non-negative one is the kind of thing an application stores back into
1926/// the same column, so it was silent and wrong in the worst direction.
1927///
1928/// The check runs in i128 so the subtraction that underflows is observed
1929/// rather than wrapped, and it only fires when the expression is unsigned
1930/// AND both operands are integers — a NUMERIC or float operand takes the
1931/// ordinary path, as it does in MySQL.
1932///
1933/// `#[inline(never)]`: this is called from the recursive evaluator's
1934/// hottest frame, which already sits against the stack guard.
1935#[inline(never)]
1936fn apply_binary_mysql_unsigned(
1937 op: BinOp,
1938 lhs: &Expr,
1939 rhs: &Expr,
1940 l: Value<'static>,
1941 r: Value<'static>,
1942 ctx: &EvalContext,
1943) -> Result<Value<'static>, EvalError> {
1944 if ctx.mysql_dialect
1945 && matches!(op, BinOp::Add | BinOp::Sub | BinOp::Mul)
1946 && let Some(a) = mysql_int_operand(&l)
1947 && let Some(b) = mysql_int_operand(&r)
1948 && (expr_is_mysql_unsigned(lhs, ctx.columns) || expr_is_mysql_unsigned(rhs, ctx.columns))
1949 {
1950 let out = match op {
1951 BinOp::Add => a.checked_add(b),
1952 BinOp::Sub => a.checked_sub(b),
1953 _ => a.checked_mul(b),
1954 };
1955 let in_range = out.is_some_and(|v| (0..=i128::from(u64::MAX)).contains(&v));
1956 if !in_range {
1957 // MariaDB names the offending expression in the message, with
1958 // minimal parentheses — `a * 0 - 1`, not `((a * 0) - 1)`.
1959 // `pretty_expr` is the deparser that already produces that
1960 // shape. Residual, recorded rather than faked: MariaDB writes
1961 // its columns fully qualified in backticks
1962 // (`db`.`tbl`.`col`), and the database name is not something
1963 // the evaluation context carries.
1964 return Err(EvalError::TypeMismatch {
1965 detail: alloc::format!(
1966 "BIGINT UNSIGNED value is out of range in '{}'",
1967 spg_sql::ast::pretty_expr_mysql(&Expr::Binary {
1968 lhs: alloc::boxed::Box::new(lhs.clone()),
1969 op,
1970 rhs: alloc::boxed::Box::new(rhs.clone()),
1971 })
1972 ),
1973 });
1974 }
1975 }
1976 let out = apply_binary_in(op, l, r, ctx.mysql_dialect);
1977 // v7.39 (round 503) — MariaDB answers NULL for division / modulo by
1978 // zero; SPG raised.
1979 //
1980 // Measured against MariaDB 11: `SELECT 1/0`, `SELECT 5 DIV 0` and
1981 // `SELECT 5 % 0` are all NULL — and they are NULL under the DEFAULT
1982 // sql_mode too, which contains `ERROR_FOR_DIVISION_BY_ZERO`. That flag
1983 // governs WRITES, not the expression: the division evaluates to NULL,
1984 // and a strict-mode INSERT of that result is what raises 1365.
1985 //
1986 // The rule is therefore the DIALECT's, not the mode's: in a MySQL
1987 // session the expression is NULL. It is deliberately not gated on
1988 // `mysql_strict` — an earlier cut of this was, and the gate fired only
1989 // because the probe's context happens to carry no engine, which a
1990 // later round attaching one would have silently reversed.
1991 //
1992 // RESIDUAL, recorded rather than faked: MariaDB's strict-mode INSERT
1993 // of a division by zero raises 1365 and its non-strict INSERT stores
1994 // NULL. SPG's INSERT path evaluates elsewhere and still raises, so it
1995 // matches strict and diverges from non-strict. Closing that needs the
1996 // expression to know it is in a write, which nothing here carries.
1997 if ctx.mysql_dialect && matches!(out, Err(EvalError::DivisionByZero)) {
1998 return Ok(Value::Null);
1999 }
2000 out
2001}
2002
2003/// The integer an operand contributes to the unsigned range check, or
2004/// `None` when it is not an integer at all (NULL, text, NUMERIC, float).
2005fn mysql_int_operand(v: &Value<'_>) -> Option<i128> {
2006 match v {
2007 Value::SmallInt(n) => Some(i128::from(*n)),
2008 Value::Int(n) => Some(i128::from(*n)),
2009 Value::BigInt(n) => Some(i128::from(*n)),
2010 // v7.39 (round 471) — a BIGINT UNSIGNED cell is stored as Numeric
2011 // with scale 0, so the range check has to see it as the integer it
2012 // is. Without this arm the column's own type moved it out of reach
2013 // of round 467's guard and `c - 5` went back to answering -4.
2014 Value::Numeric { scaled, scale, .. } if *scale == 0 => Some(*scaled),
2015 _ => None,
2016 }
2017}
2018
2019fn expr_enum_type_name<'e>(e: &'e Expr, columns: &'e [ColumnSchema]) -> Option<&'e str> {
2020 match e {
2021 Expr::Cast {
2022 target: CastTarget::Named(n),
2023 ..
2024 } => Some(n.as_str()),
2025 Expr::Column(c) => columns
2026 .iter()
2027 .find(|col| col.name == c.name)
2028 // v7.39 (round 259) — a DOMAIN column carries its name in its
2029 // own field; both are "the user type this column is declared
2030 // as", which is what the callers (enum-order comparison,
2031 // pg_typeof) want. Callers gate on the catalog, so a name that
2032 // is one kind never resolves as the other.
2033 .and_then(|col| {
2034 col.user_enum_type
2035 .as_deref()
2036 .or(col.user_domain_type.as_deref())
2037 }),
2038 _ => None,
2039 }
2040}
2041
2042/// v7.39 (enum order knife) — the member-label list for an enum-typed
2043/// expression, or None when the expression carries no enum witness or the
2044/// name is not a known enum. The returned slice borrows the catalog.
2045pub(crate) fn expr_enum_labels<'c>(
2046 e: &Expr,
2047 columns: &[ColumnSchema],
2048 catalog: Option<&'c spg_storage::Catalog>,
2049) -> Option<&'c [String]> {
2050 let name = expr_enum_type_name(e, columns)?;
2051 catalog
2052 .and_then(|cat| cat.enum_types().get(name))
2053 .map(|en| en.labels.as_slice())
2054}
2055
2056/// v7.39 (enum order knife) — compare two enum labels by member order.
2057/// None when either side is not Text or not a member (caller falls back to
2058/// the generic comparison, so a stray value never panics or misorders
2059/// silently differently from before).
2060pub(crate) fn enum_ord_cmp(
2061 labels: &[String],
2062 a: &Value<'_>,
2063 b: &Value<'_>,
2064) -> Option<core::cmp::Ordering> {
2065 let pos = |v: &Value<'_>| -> Option<usize> {
2066 match v {
2067 Value::Text(s) => labels.iter().position(|l| l.as_str() == s.as_ref()),
2068 _ => None,
2069 }
2070 };
2071 Some(pos(a)?.cmp(&pos(b)?))
2072}
2073
2074/// v7.39 (enum order knife) — Binary-comparison hook: when either side's
2075/// static type witnesses an enum and both runtime values are member labels,
2076/// compare by member order (PG's enumsortorder semantics). Out-of-line to
2077/// keep `eval_expr`'s recursion frame small.
2078#[inline(never)]
2079fn enum_compare_hook(
2080 op: BinOp,
2081 lhs: &Expr,
2082 rhs: &Expr,
2083 l: &Value<'_>,
2084 r: &Value<'_>,
2085 ctx: &EvalContext<'_>,
2086) -> Option<Result<Value<'static>, EvalError>> {
2087 let cat = ctx.catalog?;
2088 if cat.enum_types().is_empty() {
2089 return None;
2090 }
2091 let labels = expr_enum_labels(lhs, ctx.columns, ctx.catalog)
2092 .or_else(|| expr_enum_labels(rhs, ctx.columns, ctx.catalog))?;
2093 let ord = enum_ord_cmp(labels, l, r)?;
2094 let b = match op {
2095 BinOp::Eq => ord == core::cmp::Ordering::Equal,
2096 BinOp::NotEq => ord != core::cmp::Ordering::Equal,
2097 BinOp::Lt => ord == core::cmp::Ordering::Less,
2098 BinOp::LtEq => ord != core::cmp::Ordering::Greater,
2099 BinOp::Gt => ord == core::cmp::Ordering::Greater,
2100 BinOp::GtEq => ord != core::cmp::Ordering::Less,
2101 _ => return None,
2102 };
2103 Some(Ok(Value::Bool(b)))
2104}
2105
2106/// v7.39 (round 693) — Binary-comparison hook for a declared collation, the
2107/// last shape F36 left open: `loc BETWEEN 'a' AND 'd'` returns a different
2108/// ROW SET under `en_US.utf8` than under byte order, not merely a different
2109/// order.
2110///
2111/// It sits beside [`enum_compare_hook`] because it is the same kind of fact
2112/// — something about the operand COLUMNS that `compare` cannot look up from
2113/// two values — and takes the same two protections: `#[inline(never)]`, so
2114/// `eval_expr`'s recursion frame does not grow (the comment at the call site
2115/// records a fourth `||` there tipping the 768 KiB guard on its own), and
2116/// the caller's Text/Text gate, so no integer comparison reaches it.
2117///
2118/// EQUALITY is deliberately not handled. PG18's `en_US.utf8` is
2119/// deterministic, so `=`, `<>`, `LIKE`, `IN` and `count(DISTINCT …)` give
2120/// byte-equality's answer — measured, all five. Only the ordering operators
2121/// change, and `least`/`greatest` follow them through their own comparator.
2122#[inline(never)]
2123fn collate_compare_hook(
2124 op: BinOp,
2125 lhs: &Expr,
2126 rhs: &Expr,
2127 l: &Value<'_>,
2128 r: &Value<'_>,
2129 ctx: &EvalContext<'_>,
2130) -> Option<Result<Value<'static>, EvalError>> {
2131 if !matches!(op, BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq) {
2132 return None;
2133 }
2134 let (Value::Text(a), Value::Text(b)) = (l, r) else {
2135 return None;
2136 };
2137 let resolve = |c: &spg_sql::ast::ColumnName| -> Option<alloc::string::String> {
2138 let pos = find_column_pos(c, ctx)?;
2139 ctx.columns.get(pos)?.collation_name.clone()
2140 };
2141 let derived = crate::collate_derive::derive(lhs, &resolve)
2142 .combine_pub(crate::collate_derive::derive(rhs, &resolve));
2143 if let Some((x, y)) = derived.conflict() {
2144 return Some(Err(EvalError::TypeMismatch {
2145 detail: alloc::format!(
2146 "collation mismatch between implicit collations \"{x}\" and \"{y}\""
2147 ),
2148 }));
2149 }
2150 let ord = crate::collate::compare(derived.name()?, a, b)?;
2151 let b = match op {
2152 BinOp::Lt => ord == core::cmp::Ordering::Less,
2153 BinOp::LtEq => ord != core::cmp::Ordering::Greater,
2154 BinOp::Gt => ord == core::cmp::Ordering::Greater,
2155 BinOp::GtEq => ord != core::cmp::Ordering::Less,
2156 _ => return None,
2157 };
2158 Some(Ok(Value::Bool(b)))
2159}
2160
2161/// v7.39 (round 693) — the collation `least`/`greatest` should compare by,
2162/// derived across every argument the same way a comparison's two operands
2163/// are. `None` keeps byte order, which is right for arguments that declare
2164/// nothing.
2165#[inline(never)]
2166fn greatest_least_collation(args: &[Expr], ctx: &EvalContext<'_>) -> Option<alloc::string::String> {
2167 let resolve = |c: &spg_sql::ast::ColumnName| -> Option<alloc::string::String> {
2168 let pos = find_column_pos(c, ctx)?;
2169 ctx.columns.get(pos)?.collation_name.clone()
2170 };
2171 let derived = args
2172 .iter()
2173 .fold(crate::collate_derive::Derived::None, |acc, a| {
2174 acc.combine_pub(crate::collate_derive::derive(a, &resolve))
2175 });
2176 derived
2177 .name()
2178 .filter(|n| crate::collate::is_supported(n))
2179 .map(alloc::string::ToString::to_string)
2180}
2181
2182/// v7.39 (round 704) — rewrite a comparison's operator-not-found error when
2183/// the operand at fault is an UNKNOWN string literal against a numeric-family
2184/// value. PG commits such a literal to the other side's type before comparing,
2185/// so its error is the input function's — `invalid input syntax for type
2186/// integer: "abc"` — not `operator does not exist: integer = text`. An
2187/// explicit `::text` operand keeps the operator error (`1 IS DISTINCT FROM
2188/// 'a'::text`, measured on PG18), which is precisely the distinction two
2189/// `Value`s cannot carry: the first cut of this round rewrote inside
2190/// `binop::compare` and the r238 pin plus corpus 19 caught it the same day.
2191///
2192/// Error-path only — a comparison that succeeds never calls this — so the
2193/// 35.6 %-of-self-time note on `compare` is untouched.
2194#[cold]
2195#[inline(never)]
2196fn unknown_literal_cmp_error(
2197 err: EvalError,
2198 lhs: &Expr,
2199 rhs: &Expr,
2200 lv: &Value<'_>,
2201 rv: &Value<'_>,
2202) -> EvalError {
2203 let EvalError::TypeMismatch { detail } = &err else {
2204 return err;
2205 };
2206 // Two spellings of the same fall-through: `compare`'s operator error,
2207 // and the owned numeric path's conversion error (`f = 'y'` reaches
2208 // "cannot convert text to FLOAT"). Both mean the literal failed to
2209 // lift; neither is what PG says about an unknown literal.
2210 if !detail.starts_with("operator does not exist")
2211 && !detail.starts_with("cannot convert text to")
2212 {
2213 return err;
2214 }
2215 let numeric = |v: &Value<'_>| {
2216 matches!(
2217 v.data_type(),
2218 Some(
2219 spg_storage::DataType::SmallInt
2220 | spg_storage::DataType::Int
2221 | spg_storage::DataType::BigInt
2222 | spg_storage::DataType::Float
2223 | spg_storage::DataType::Real
2224 | spg_storage::DataType::Numeric { .. }
2225 )
2226 )
2227 };
2228 let rewrite = |s: &Value<'_>, other: &Value<'_>| -> Option<EvalError> {
2229 let Value::Text(text) = s else { return None };
2230 let dt = other.data_type()?;
2231 Some(EvalError::TypeMismatch {
2232 detail: alloc::format!(
2233 "invalid input syntax for type {}: \"{text}\"",
2234 crate::conversions::pg_type_name_for_error(dt)
2235 ),
2236 })
2237 };
2238 if is_unknown_string_literal(lhs)
2239 && numeric(rv)
2240 && let Some(e) = rewrite(lv, rv)
2241 {
2242 return e;
2243 }
2244 if is_unknown_string_literal(rhs)
2245 && numeric(lv)
2246 && let Some(e) = rewrite(rv, lv)
2247 {
2248 return e;
2249 }
2250 err
2251}
2252
2253fn enum_arg_type_name<'e>(args: &'e [Expr], ctx: &EvalContext<'e>) -> Option<&'e str> {
2254 args.iter()
2255 .find_map(|a| expr_enum_type_name(a, ctx.columns))
2256 .filter(|n| {
2257 ctx.catalog
2258 .is_some_and(|cat| cat.enum_types().contains_key(*n))
2259 })
2260}
2261
2262/// Cheap value-free precheck so `eval_expr`'s recursion frame carries no
2263/// binding for the enum path (stack-depth guard budget).
2264#[inline(never)]
2265fn enum_introspection_applies(args: &[Expr], ctx: &EvalContext<'_>) -> bool {
2266 enum_arg_type_name(args, ctx).is_some()
2267}
2268
2269#[inline(never)]
2270fn eval_enum_introspection(
2271 name: &str,
2272 args: &[Expr],
2273 row: &Row<'static>,
2274 ctx: &EvalContext<'_>,
2275) -> Result<Value<'static>, EvalError> {
2276 let Some(en) = enum_arg_type_name(args, ctx)
2277 .and_then(|n| ctx.catalog.and_then(|cat| cat.enum_types().get(n)))
2278 else {
2279 // The precheck guarantees this arm is unreachable; keep a typed
2280 // error rather than a panic if the two ever drift.
2281 return Err(EvalError::TypeMismatch {
2282 detail: "could not determine polymorphic type".into(),
2283 });
2284 };
2285 let labels = &en.labels;
2286 if labels.is_empty() {
2287 return Ok(Value::Null);
2288 }
2289 if name.eq_ignore_ascii_case("enum_first") {
2290 return Ok(Value::text(labels[0].clone()));
2291 }
2292 if name.eq_ignore_ascii_case("enum_last") {
2293 return Ok(Value::text(labels[labels.len() - 1].clone()));
2294 }
2295 // enum_range(NULL) = all; enum_range(lo, hi) slices inclusively,
2296 // NULL bound = open end (PG).
2297 let pos_of = |v: &Value<'_>| -> Option<usize> {
2298 match v {
2299 Value::Text(s) => labels.iter().position(|l| l == s.as_ref()),
2300 _ => None,
2301 }
2302 };
2303 let (lo, hi) = if args.len() == 2 {
2304 let a = eval_expr(&args[0], row, ctx)?;
2305 let b = eval_expr(&args[1], row, ctx)?;
2306 (
2307 pos_of(&a).unwrap_or(0),
2308 pos_of(&b).unwrap_or(labels.len() - 1),
2309 )
2310 } else {
2311 (0, labels.len() - 1)
2312 };
2313 let out: alloc::vec::Vec<Option<String>> = labels
2314 .get(lo..=hi)
2315 .unwrap_or(&[])
2316 .iter()
2317 .map(|l| Some(l.clone()))
2318 .collect();
2319 Ok(Value::TextArray(out))
2320}
2321
2322/// Apply one `[index]` subscript to a value — the single-step semantics shared
2323/// by 1-D array elements and JSON path access (`j['a']`, `j[0]`). NULL target
2324/// or index → NULL; a 1-based integer indexes a 1-D array (out of range → NULL,
2325/// non-array → error); JSON delegates to `path_get`.
2326fn apply_one_subscript(
2327 target_v: Value<'static>,
2328 index: &Expr,
2329 row: &Row<'static>,
2330 ctx: &EvalContext<'_>,
2331) -> Result<Value<'static>, EvalError> {
2332 let idx_v = eval_expr(index, row, ctx)?;
2333 if matches!(target_v, Value::Null) || matches!(idx_v, Value::Null) {
2334 return Ok(Value::Null);
2335 }
2336 // v7.38 (read01) — JSON/JSONB subscripting (`j['a']`, `j[0]`, chained
2337 // `j['a']['b']`) is object/array access, identical to the `->` operator
2338 // (text key → object field, integer → 0-based array element). PG 14+.
2339 if matches!(target_v, Value::Json(_)) {
2340 return crate::json::path_get(&target_v, &idx_v, false);
2341 }
2342 let i: i64 = match idx_v {
2343 Value::Int(n) => i64::from(n),
2344 Value::BigInt(n) => n,
2345 Value::SmallInt(n) => i64::from(n),
2346 other => {
2347 return Err(EvalError::TypeMismatch {
2348 detail: format!(
2349 "array subscript must be integer, got {}",
2350 crate::conversions::pg_type_name_for_error_opt(other.data_type())
2351 ),
2352 });
2353 }
2354 };
2355 if i < 1 {
2356 return Ok(Value::Null);
2357 }
2358 let pos = (i - 1) as usize;
2359 match array_element_at(&target_v, pos) {
2360 Some(v) => Ok(v),
2361 None if array_len(&target_v).is_some() => Ok(Value::Null),
2362 None => Err(EvalError::TypeMismatch {
2363 detail: format!(
2364 "subscript target must be an array, got {}",
2365 crate::conversions::pg_type_name_for_error_opt(target_v.data_type())
2366 ),
2367 }),
2368 }
2369}
2370
2371/// v7.38 (read01, 2D-subscript) — index a 2-D array (`arr[i][j]`). PG needs
2372/// exactly two subscripts to reach an element; a single subscript on a 2-D
2373/// array yields NULL (not the row), and any out-of-range index → NULL. Both
2374/// subscripts are 1-based.
2375fn eval_matrix_subscript(
2376 base: &Value<'static>,
2377 idx_exprs: &[&Expr],
2378 row: &Row<'static>,
2379 ctx: &EvalContext<'_>,
2380) -> Result<Value<'static>, EvalError> {
2381 if idx_exprs.len() != 2 {
2382 return Ok(Value::Null);
2383 }
2384 let mut idx = [0i64; 2];
2385 for (k, ix) in idx_exprs.iter().enumerate() {
2386 idx[k] = match eval_expr(ix, row, ctx)? {
2387 Value::Null => return Ok(Value::Null),
2388 Value::Int(n) => i64::from(n),
2389 Value::BigInt(n) => n,
2390 Value::SmallInt(n) => i64::from(n),
2391 other => {
2392 return Err(EvalError::TypeMismatch {
2393 detail: format!(
2394 "array subscript must be integer, got {}",
2395 crate::conversions::pg_type_name_for_error_opt(other.data_type())
2396 ),
2397 });
2398 }
2399 };
2400 }
2401 let (r, c) = (idx[0], idx[1]);
2402 if r < 1 || c < 1 {
2403 return Ok(Value::Null);
2404 }
2405 let (ri, ci) = ((r - 1) as usize, (c - 1) as usize);
2406 macro_rules! elem {
2407 ($rows:expr, $map:expr) => {
2408 Ok($rows
2409 .get(ri)
2410 .and_then(|inner| inner.get(ci))
2411 .map_or(Value::Null, |cell| cell.as_ref().map_or(Value::Null, $map)))
2412 };
2413 }
2414 match base {
2415 Value::IntArray2D(rows) => elem!(rows, |n| Value::Int(*n)),
2416 Value::BigIntArray2D(rows) => elem!(rows, |n| Value::BigInt(*n)),
2417 Value::BoolArray2D(rows) => elem!(rows, |b| Value::Bool(*b)),
2418 Value::TextArray2D(rows) => {
2419 elem!(rows, |s| Value::Text(alloc::borrow::Cow::Owned(s.clone())))
2420 }
2421 _ => Ok(Value::Null),
2422 }
2423}
2424
2425/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
2426/// (stack-depth guard budget); body unchanged.
2427#[inline(never)]
2428fn eval_cast_arm(
2429 expr: &Expr,
2430 target: &CastTarget,
2431 row: &Row<'static>,
2432 ctx: &EvalContext<'_>,
2433) -> Result<Value<'static>, EvalError> {
2434 let v = eval_expr(expr, row, ctx)?;
2435 // v7.39 (round 473) — `<oid>::regclass` names the relation.
2436 //
2437 // The cast itself has no catalog, so it answered the bare number:
2438 // `indexrelid::regclass` printed `100001` where PG prints `ix1`, and
2439 // pg_class / pg_index rows are read by joining on oids and rendering
2440 // them — a tool cannot match the two up. `relation_name_for_oid`
2441 // mirrors `relation_oid`'s walks so the two directions agree; an oid
2442 // that names nothing keeps rendering as the number, which is what PG
2443 // does for a dropped relation's oid too.
2444 if matches!(target, CastTarget::RegClass)
2445 && let Some(cat) = ctx.catalog
2446 {
2447 let oid = match &v {
2448 Value::Int(n) => Some(i64::from(*n)),
2449 Value::BigInt(n) => Some(*n),
2450 _ => None,
2451 };
2452 if let Some(oid) = oid
2453 && let Some(name) = crate::system_catalog::relation_name_for_oid(cat, oid)
2454 {
2455 return Ok(Value::text(name));
2456 }
2457 }
2458 // v7.38 (read01 P6.40) — a cast to a user DOMAIN (`x::posint`)
2459 // enforces the domain's NOT NULL + CHECK constraints, matching PG.
2460 // The base-type coercion already happened when `v` was produced
2461 // (the domain is a constrained alias of its base type); here we
2462 // only run the constraints.
2463 if let CastTarget::Named(name) = target
2464 && let Some(cat) = ctx.catalog
2465 {
2466 if let Some(dom) = cat.domain_types().get(name.as_str()) {
2467 return apply_domain_constraints(v, dom, name, cat);
2468 }
2469 // v7.38 (read01 P6.67) — `'label'::<user enum>` validates the
2470 // label against the enum's members (a non-member errors like
2471 // PG). A typed NULL passes through carrying the enum type.
2472 if let Some(en) = cat.enum_types().get(name.as_str()) {
2473 return apply_enum_cast(v, en, name);
2474 }
2475 // v7.39 (read01 rowtypes.c) — `'(1,x)'::<composite>` parses PG's
2476 // record text form against the type's field list; a ROW value
2477 // re-labels its fields.
2478 if let Some(comp) = cat.composite_types().get(name.as_str()) {
2479 // v7.39 (round 264) — pass the catalog so a NESTED composite
2480 // field resolves into a record rather than staying text.
2481 return apply_composite_cast_in(v, comp, ctx.catalog);
2482 }
2483 // v7.39 (round 509) — every TABLE also names a row type, and
2484 // `jsonb_populate_record(NULL::mytable, …)` is PG's canonical
2485 // spelling for "shaped like this table". PG accepts `NULL::mytable`
2486 // and refuses `1::mytable` with "cannot cast type integer to
2487 // mytable" — the type exists, the conversion does not. This only
2488 // ever worked here because a NULL skipped cast resolution entirely;
2489 // now that it does not, the row type has to be named explicitly.
2490 if cat.get(name.as_str()).is_some() {
2491 return if matches!(v, Value::Null) {
2492 Ok(Value::Null)
2493 } else {
2494 Err(EvalError::TypeMismatch {
2495 detail: alloc::format!(
2496 "cannot cast type {} to {name}",
2497 crate::eval::strings::pg_typeof_name(&v),
2498 ),
2499 })
2500 };
2501 }
2502 // v7.39 (round 513) — `regnamespace` and `regrole` resolve against
2503 // things that live outside the type table: schemas on the catalog,
2504 // roles on the engine. They belong here for the same reason the
2505 // relation check above does — this is the arm that can see them.
2506 // v7.39 (round 526) — the NUMERIC direction, which is the one a
2507 // catalog join uses: `relnamespace::regnamespace` names the
2508 // schema a relation lives in, and it errored with "unsupported
2509 // cast target" while `'public'::regnamespace` worked. Round 513
2510 // added the name direction only, so the half that reads a
2511 // catalog was the half missing.
2512 if (name.eq_ignore_ascii_case("regnamespace") || name.eq_ignore_ascii_case("regrole"))
2513 && let Some(oid) = match &v {
2514 Value::Int(n) => Some(i64::from(*n)),
2515 Value::BigInt(n) => Some(*n),
2516 _ => None,
2517 }
2518 {
2519 let named = if name.eq_ignore_ascii_case("regnamespace") {
2520 crate::system_catalog::schema_name_for_oid(oid)
2521 } else {
2522 ctx.engine.and_then(|e| e.role_name_for_oid(oid))
2523 };
2524 // PG prints the bare number for an oid that names nothing,
2525 // exactly as `regclass` does.
2526 return Ok(Value::text(
2527 named.unwrap_or_else(|| alloc::format!("{oid}")),
2528 ));
2529 }
2530 if name.eq_ignore_ascii_case("regnamespace")
2531 && let Value::Text(t) = &v
2532 {
2533 let want = t.trim().trim_matches('"');
2534 // 7.38.1 S5.1 — the name direction answers the DUAL
2535 // (oid, name) value for the schemas with a published oid:
2536 // regnamespace IS an oid in PG, and pg_dump compares it
2537 // against numeric namespace columns (`opcnamespace =
2538 // 'pg_catalog'::regnamespace`) — while the wire render
2539 // stays the NAME, as PG's does (the round-513 contract).
2540 // The RegClass dual carries exactly that pair. A user
2541 // schema without a published oid keeps plain text.
2542 return if spg_storage::is_builtin_schema(want) || cat.schema_exists(want) {
2543 Ok(match want {
2544 "pg_catalog" => Value::RegClass(11, "pg_catalog".into()),
2545 "public" => Value::RegClass(2200, "public".into()),
2546 "information_schema" => Value::RegClass(13000, "information_schema".into()),
2547 _ => Value::text(want.to_string()),
2548 })
2549 } else {
2550 Err(EvalError::TypeMismatch {
2551 detail: alloc::format!("schema \"{want}\" does not exist"),
2552 })
2553 };
2554 }
2555 if name.eq_ignore_ascii_case("regrole")
2556 && let Value::Text(t) = &v
2557 {
2558 let want = t.trim().trim_matches('"').to_string();
2559 // PG ships predefined roles that exist whether or not anybody
2560 // created them; SPG carries the rest on the engine.
2561 const PREDEFINED: &[&str] = &[
2562 "pg_read_all_data",
2563 "pg_write_all_data",
2564 "pg_monitor",
2565 "pg_read_all_settings",
2566 "pg_read_all_stats",
2567 "pg_stat_scan_tables",
2568 "pg_signal_backend",
2569 "pg_checkpoint",
2570 "pg_maintain",
2571 "pg_use_reserved_connections",
2572 "pg_create_subscription",
2573 ];
2574 let known = PREDEFINED.iter().any(|r| r.eq_ignore_ascii_case(&want))
2575 || ctx.engine.is_some_and(|e| e.role_exists(&want));
2576 return if known {
2577 Ok(Value::text(want))
2578 } else {
2579 Err(EvalError::TypeMismatch {
2580 detail: alloc::format!("role \"{want}\" does not exist"),
2581 })
2582 };
2583 }
2584 // v7.39 (round 509) — the cast target is checked even when the
2585 // operand is NULL. `cast_value_in` short-circuits a NULL before it
2586 // looks at the target, so `NULL::nosuchtype` silently answered NULL
2587 // and `pg_typeof(NULL::nosuchtype)` answered `unknown`, while
2588 // `1::nosuchtype` errored — the gap was exactly the NULL case, in
2589 // both spellings. Everything a catalog can name has been tried by
2590 // now; what is left is the builtin table.
2591 if matches!(v, Value::Null)
2592 && !crate::eval::cast::builtin_target_resolves(name, ctx.mysql_dialect)
2593 {
2594 return Err(EvalError::TypeMismatch {
2595 detail: cast::unknown_type_error_text(name),
2596 });
2597 }
2598 }
2599 // v7.39 (round 285) — `::record`, the anonymous composite type. PG
2600 // treats it as an IDENTITY cast on anything already composite: the
2601 // value keeps its fields and their names, so `(ROW(1,2)::record).f1`
2602 // and `(r).x` still resolve. Only a non-composite is refused, with
2603 // PG's wording. `record` is not a catalog type, so this cannot live
2604 // in the lookups above.
2605 if let CastTarget::Named(name) = target
2606 && name.eq_ignore_ascii_case("record")
2607 {
2608 return match v {
2609 Value::Composite(_) | Value::Null => Ok(v),
2610 other => Err(EvalError::TypeMismatch {
2611 detail: alloc::format!(
2612 "cannot cast type {} to record",
2613 crate::eval::strings::pg_typeof_name(&other),
2614 ),
2615 }),
2616 };
2617 }
2618 // v7.38 (read01, T22) — a numeric OID cast to regclass reverse-looks
2619 // up the user relation name (PG's 16384+ band, assigned in
2620 // table_names() order). System OIDs / non-matches fall through to
2621 // the integer-rendering path in cast_value.
2622 if matches!(target, CastTarget::RegClass) {
2623 // v7.39 (read01 ruleutils.c) — regclass is DUAL-shape: oid for
2624 // catalog joins (conrelid = 't'::regclass), name for display.
2625 let oid_in = match &v {
2626 Value::SmallInt(n) => Some(i64::from(*n)),
2627 Value::Int(n) => Some(i64::from(*n)),
2628 Value::BigInt(n) => Some(*n),
2629 _ => None,
2630 };
2631 if let (Some(oid), Some(cat)) = (oid_in, ctx.catalog) {
2632 if oid >= 16384 {
2633 if let Some(name) = cat.table_names().into_iter().nth((oid - 16384) as usize) {
2634 return Ok(Value::RegClass(oid, name.into()));
2635 }
2636 }
2637 }
2638 if let (Value::Text(s), Some(cat)) = (&v, ctx.catalog) {
2639 let bare = s
2640 .rsplit('.')
2641 .next()
2642 .unwrap_or(s)
2643 .trim_matches('"')
2644 .to_string();
2645 if let Some(oid) = regclass_name_to_oid(cat, &bare) {
2646 return Ok(Value::RegClass(oid, bare.into()));
2647 }
2648 // v7.39 (round 337, V62) — a name that is no relation at all is
2649 // PG's error, not a silent pass-through. `'nope'::regclass`
2650 // used to answer the TEXT `nope`, so a downstream
2651 // `pg_get_viewdef('nope'::regclass)` reported "no such view"
2652 // when the truth is there is no such relation — and a
2653 // catalog join on it quietly matched nothing. PG 18.4:
2654 // `ERROR: relation "nope" does not exist`. (`to_regclass` is
2655 // the spelling that answers NULL instead, and still does.)
2656 //
2657 // The system views SPG synthesises have no oid space, so they
2658 // keep the textual form rather than erroring.
2659 const SYSTEM_RELS: &[&str] = &[
2660 "pg_roles",
2661 "pg_user",
2662 "pg_tables",
2663 "pg_views",
2664 "pg_settings",
2665 "pg_stat_activity",
2666 "pg_stat_database",
2667 "pg_stat_user_tables",
2668 "pg_class",
2669 "pg_attribute",
2670 "pg_type",
2671 "pg_proc",
2672 "pg_namespace",
2673 "pg_constraint",
2674 "pg_index",
2675 "pg_rewrite",
2676 ];
2677 if !SYSTEM_RELS.contains(&bare.as_str()) {
2678 return Err(EvalError::TypeMismatch {
2679 detail: alloc::format!("relation \"{bare}\" does not exist"),
2680 });
2681 }
2682 }
2683 }
2684 // v7.39 (round 339, V63) — `::regproc` / `::regprocedure` resolve
2685 // against the USER function catalog too. Name resolution ran against
2686 // the static pg_proc table alone, so `'my_fn'::regproc` — the form
2687 // every catalog query and pg_dump uses to name a function — raised
2688 // `function "my_fn" does not exist` for a function that plainly did.
2689 // The cast layer has no catalog handle; this is the same interception
2690 // point the `::regclass` block above uses.
2691 if let (CastTarget::Named(tname), Some(cat), Value::Text(s)) = (target, ctx.catalog, &v) {
2692 let lower = tname.to_ascii_lowercase();
2693 if matches!(lower.as_str(), "regproc" | "regprocedure") {
2694 let raw = s.trim();
2695 // regprocedure carries the argument list: `f(int,text)`.
2696 let (name_part, args_part) = match raw.split_once('(') {
2697 Some((n, rest)) => (n.trim(), Some(rest.trim_end_matches(')'))),
2698 None => (raw, None),
2699 };
2700 let bare = name_part
2701 .strip_prefix("public.")
2702 .unwrap_or(name_part)
2703 .trim_matches('"');
2704 let cands = cat.functions_named(bare);
2705 if let Some(args_txt) = args_part {
2706 // An overload IS distinguishable here — the argument list
2707 // is what regprocedure exists to carry.
2708 let want =
2709 crate::system_catalog::canonical_arg_types(&alloc::format!("({args_txt})"));
2710 if let Some(f) = cands
2711 .iter()
2712 .find(|f| crate::system_catalog::canonical_arg_types(&f.args_repr) == want)
2713 {
2714 let rendered = alloc::format!(
2715 "{bare}({})",
2716 crate::system_catalog::canonical_arg_types(&f.args_repr)
2717 );
2718 // v7.39 (round 342, V65) — dual shape: the oid for
2719 // catalog joins, the rendering for display.
2720 let oid = crate::system_catalog::function_oid_by_signature(cat, bare, &want)
2721 .unwrap_or(0);
2722 return Ok(Value::RegProc(oid, rendered.into()));
2723 }
2724 } else {
2725 match cands.len() {
2726 0 => {}
2727 1 => {
2728 let oid = crate::system_catalog::function_oid(cat, bare).unwrap_or(0);
2729 return Ok(Value::RegProc(oid, bare.into()));
2730 }
2731 _ => {
2732 return Err(EvalError::TypeMismatch {
2733 detail: alloc::format!("more than one function named \"{bare}\""),
2734 });
2735 }
2736 }
2737 }
2738 }
2739 }
2740 // v7.38 (T-tstz Phase 1) — `<timestamptz>::text` renders the offset
2741 // (`2024-01-15 10:30:00+00`); plain timestamp does not. The runtime
2742 // value is the same tz-less `Value::Timestamp`, so consult the
2743 // inner expression's static type. Falls through to the ordinary
2744 // cast on any shape the static typer can't resolve — worst case is
2745 // today's no-offset rendering, never a wrong instant.
2746 if matches!(target, CastTarget::Text)
2747 && let Value::Timestamp(t) = &v
2748 && crate::describe::describe_expr(expr, ctx.columns)
2749 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
2750 {
2751 // v7.39 (tz epic) — per-VALUE session offset (DST zones
2752 // vary within a statement); non-ISO DateStyles carry the
2753 // zone designation instead of the numeric offset.
2754 let off = ctx.session_tz_offset_at(*t);
2755 let abbr = ctx.session_tz_abbrev_at(*t);
2756 return Ok(Value::text(format::format_timestamptz_tz(
2757 *t,
2758 &ctx.render_style,
2759 off,
2760 abbr.as_deref(),
2761 )));
2762 }
2763 // v7.39 (read01 utils/adt, datetime.c) — the relative
2764 // reserved words resolve against the transaction clock:
2765 // 'today'/'tomorrow'/'yesterday' are midnight dates, 'now'
2766 // is the current instant ('now'::date = today). Clockless
2767 // engines fall through to the parser (which rejects them).
2768 if matches!(
2769 target,
2770 CastTarget::Date | CastTarget::Timestamp | CastTarget::Timestamptz
2771 ) && let Value::Text(word) = &v
2772 && let Some(clock) = ctx.clock
2773 {
2774 let w = word.trim().to_ascii_lowercase();
2775 if matches!(w.as_str(), "today" | "tomorrow" | "yesterday" | "now") {
2776 let now_us = clock();
2777 let today = i32::try_from(now_us.div_euclid(86_400_000_000)).ok();
2778 if let Some(today) = today {
2779 let day = match w.as_str() {
2780 "tomorrow" => today + 1,
2781 "yesterday" => today - 1,
2782 _ => today,
2783 };
2784 return Ok(match (&target, w.as_str()) {
2785 (CastTarget::Date, _) => Value::Date(day),
2786 (_, "now") => Value::Timestamp(now_us),
2787 _ => Value::Timestamp(crate::conversions::date_days_to_micros(day)),
2788 });
2789 }
2790 }
2791 }
2792 // v7.39 (GUC knife 5) — text INPUT to date/timestamp under a
2793 // non-MDY DateOrder disambiguates by the session order
2794 // (`'01/02/2024'::date` is Feb 1 under DMY). The default MDY
2795 // order flows through cast_value's parse_date_literal.
2796 if ctx.render_style.date_order != format::DateOrder::Mdy {
2797 match (&target, &v) {
2798 (CastTarget::Date, Value::Text(s)) => {
2799 if let Some(d) = format::parse_date_literal_ordered(s, ctx.render_style.date_order)
2800 {
2801 return Ok(Value::Date(d));
2802 }
2803 }
2804 (CastTarget::Timestamp, Value::Text(s)) => {
2805 if let Some(t) =
2806 format::parse_timestamp_literal_ordered(s, ctx.render_style.date_order)
2807 {
2808 return Ok(Value::Timestamp(t));
2809 }
2810 }
2811 _ => {}
2812 }
2813 }
2814 // v7.39 (round 309, V30) — the mirror of the timestamptz arm
2815 // below. A literal carrying a zone NAME is legal input to the
2816 // zone-less types, and PG throws the zone away rather than
2817 // converting: `'2020-01-01 10:00:00 America/New_York'::timestamp`
2818 // is 10:00, not 15:00. Round 289 did this for a numeric `+02`
2819 // offset; a named zone still failed to parse at all.
2820 //
2821 // The name is not simply stripped — PG validates it, and says so
2822 // (`time zone "bogus/zone" not recognized`, lowercased) rather than
2823 // reporting a malformed literal. That check is why this belongs
2824 // here and not in `cast_value`: resolving a zone needs the host
2825 // functions, which only the context carries.
2826 let zoneless_target = match &target {
2827 CastTarget::Timestamp => Some("timestamp"),
2828 CastTarget::Date => Some("date"),
2829 // `::time` has no CastTarget of its own; it arrives named.
2830 CastTarget::Named(n) if n.eq_ignore_ascii_case("time") => Some("time"),
2831 _ => None,
2832 };
2833 if let Some(kind) = zoneless_target
2834 && let Value::Text(txt) = &v
2835 && let Some((wall, zone)) = split_trailing_zone_name(txt, ctx.render_style.date_order)
2836 {
2837 if ctx.zone_local_to_utc(zone, wall).is_none() {
2838 // Measured boundary: PG calls the token a ZONE NAME — and
2839 // so reports a misspelling as such — only when it is
2840 // path-shaped. A bare word it does not know (`ABCD`, `QQQ`,
2841 // `UTC_X`) makes the whole literal invalid syntax instead,
2842 // because nothing marks it as having meant a zone at all.
2843 if zone.contains('/') {
2844 return Err(EvalError::TypeMismatch {
2845 detail: alloc::format!(
2846 "time zone \"{}\" not recognized",
2847 zone.to_ascii_lowercase()
2848 ),
2849 });
2850 }
2851 } else {
2852 return Ok(match kind {
2853 "date" => {
2854 Value::Date(i32::try_from(wall.div_euclid(86_400_000_000)).map_err(|_| {
2855 EvalError::TypeMismatch {
2856 detail: "timestamp out of DATE range".into(),
2857 }
2858 })?)
2859 }
2860 "time" => Value::Time(wall.rem_euclid(86_400_000_000)),
2861 _ => Value::Timestamp(wall),
2862 });
2863 }
2864 }
2865 // v7.39 (tz epic) — timestamptz INPUT: an offset-less
2866 // literal is a wall-clock reading in the session zone
2867 // (PG); a trailing IANA zone name localises there. Both
2868 // fall through to cast_value when nothing matches (its
2869 // parse treats naive input as UTC — correct for a UTC
2870 // session).
2871 if matches!(target, CastTarget::Timestamptz)
2872 && let Value::Text(txt) = &v
2873 {
2874 let order = ctx.render_style.date_order;
2875 let sess_zone = ctx
2876 .session_gucs
2877 .and_then(|g| g.get("timezone"))
2878 .map(String::as_str);
2879 // Trailing zone name: the last space-separated token,
2880 // when it names a resolvable zone (contains a letter
2881 // and isn't consumed by the plain parse).
2882 if let Some(idx) = txt.trim_end().rfind(' ') {
2883 let (head, tail) = (txt[..idx].trim(), txt[idx + 1..].trim());
2884 let tail_is_zoneish = tail.len() > 1
2885 && tail.bytes().any(|b| b.is_ascii_alphabetic())
2886 && !tail.eq_ignore_ascii_case("bc")
2887 && !tail.eq_ignore_ascii_case("ad");
2888 if tail_is_zoneish
2889 && format::parse_timestamp_literal_tz_ordered(txt, order).is_none()
2890 && let Some((wall, false)) = format::parse_timestamp_literal_tz_ordered(head, order)
2891 && let Some(utc) = ctx.zone_local_to_utc(tail, wall)
2892 {
2893 return Ok(Value::Timestamp(utc));
2894 }
2895 }
2896 if let Some((wall, had_tz)) = format::parse_timestamp_literal_tz_ordered(txt, order) {
2897 if had_tz {
2898 return Ok(Value::Timestamp(wall));
2899 }
2900 if let Some(zone) = sess_zone
2901 && !zone.eq_ignore_ascii_case("utc")
2902 && !zone.eq_ignore_ascii_case("gmt")
2903 && let Some(utc) = ctx.zone_local_to_utc(zone, wall)
2904 {
2905 return Ok(Value::Timestamp(utc));
2906 }
2907 return Ok(Value::Timestamp(wall));
2908 }
2909 }
2910 // v7.39 (round 523) — a NAIVE timestamp cast to timestamptz is a
2911 // wall-clock reading in the session zone, exactly as the text form
2912 // above already is. This was a no-op, so under `SET TimeZone =
2913 // 'Asia/Tokyo'` a `TIMESTAMP '2020-01-01 00:00:00'::timestamptz`
2914 // named 09:00 JST — a different INSTANT, nine hours from the one PG
2915 // stores, not a different rendering of the same one.
2916 //
2917 // The source's static type is the witness: SPG keeps timestamptz in
2918 // the same `Value::Timestamp`, so only an expression that is not
2919 // ALREADY timestamptz may be shifted, or a tstz-to-tstz cast would
2920 // move the instant twice.
2921 if matches!(target, CastTarget::Timestamptz)
2922 && let Value::Timestamp(wall) = &v
2923 && !matches!(
2924 crate::describe::describe_expr(expr, ctx.columns).map(|s| s.ty),
2925 Some(spg_storage::DataType::Timestamptz)
2926 )
2927 && let Some(zone) = ctx.session_gucs.and_then(|g| g.get("timezone"))
2928 && !zone.eq_ignore_ascii_case("utc")
2929 && !zone.eq_ignore_ascii_case("gmt")
2930 && let Some(utc) = ctx.zone_local_to_utc(zone, *wall)
2931 {
2932 return Ok(Value::Timestamp(utc));
2933 }
2934 // v7.39 (round 523) — and the other direction: a timestamptz cast
2935 // DOWN to a zone-free type reads the local clock in the session
2936 // zone. `(TIMESTAMPTZ '2020-01-01 15:00:00Z')::date` answered
2937 // 2020-01-01 in Tokyo where PG answers 2020-01-02 — a whole day out
2938 // for every instant in the last nine hours of a UTC day, which is
2939 // exactly the shape a daily report groups on. `now()::timestamp`
2940 // likewise disagreed with `now() AT TIME ZONE <session zone>`, which
2941 // PG defines to be the same value.
2942 if matches!(target, CastTarget::Timestamp | CastTarget::Date)
2943 && let Value::Timestamp(t) = &v
2944 && crate::describe::describe_expr(expr, ctx.columns)
2945 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
2946 {
2947 let local = t.saturating_add(ctx.session_tz_offset_at(*t));
2948 return Ok(match target {
2949 CastTarget::Date => i32::try_from(local.div_euclid(86_400_000_000))
2950 .map_or(Value::Timestamp(local), Value::Date),
2951 _ => Value::Timestamp(local),
2952 });
2953 }
2954 // v7.39 (GUC knife 3) — the out-function casts honour the
2955 // session render style, like PG's date_out/interval_out/
2956 // float8out under DateStyle/IntervalStyle/extra_float_digits.
2957 if matches!(target, CastTarget::Text) {
2958 match &v {
2959 // v7.39 (round 524) — `bytea_out` is a render GUC too.
2960 Value::Bytes(b) if ctx.render_style.bytea_escape => {
2961 return Ok(Value::text(format::format_bytea_escape(b)));
2962 }
2963 Value::Date(d) => {
2964 return Ok(Value::text(format::format_date_styled(
2965 *d,
2966 &ctx.render_style,
2967 )));
2968 }
2969 Value::Timestamp(t) => {
2970 return Ok(Value::text(format::format_timestamp_styled(
2971 *t,
2972 &ctx.render_style,
2973 )));
2974 }
2975 Value::Interval {
2976 months,
2977 days,
2978 micros,
2979 } => {
2980 return Ok(Value::text(format::format_interval_styled(
2981 *months,
2982 *days,
2983 *micros,
2984 &ctx.render_style,
2985 )));
2986 }
2987 Value::Float(x) => {
2988 return Ok(Value::text(format::format_float_styled(
2989 *x,
2990 &ctx.render_style,
2991 )));
2992 }
2993 Value::Real(x) => {
2994 return Ok(Value::text(format::format_real_styled(
2995 *x,
2996 &ctx.render_style,
2997 )));
2998 }
2999 _ => {}
3000 }
3001 }
3002 crate::eval::cast::cast_value_ref_in(v, target, ctx.mysql_dialect)
3003}
3004
3005/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
3006/// (stack-depth guard budget); body unchanged.
3007#[inline(never)]
3008fn eval_array_arm(
3009 items: &[Expr],
3010 row: &Row<'static>,
3011 ctx: &EvalContext<'_>,
3012) -> Result<Value<'static>, EvalError> {
3013 let mut materialised: Vec<Value<'static>> = Vec::with_capacity(items.len());
3014 for elem in items {
3015 materialised.push(eval_expr(elem, row, ctx)?);
3016 }
3017 // v7.38 (read01, T10) — a constructor whose elements are all 1-D
3018 // arrays builds a 2-D array (`ARRAY[[1,2],[3,4]]`). All rows must
3019 // share a length (PG: "multidimensional arrays must have array
3020 // expressions with matching dimensions"). Int rows promote to
3021 // bigint if any row is bigint; a text row makes the whole thing text.
3022 // v7.39 (read01 round 73) — a row of ANY array kind counts. Round 72 gave
3023 // `ARRAY[true,false]` its real `bool[]` type, and this detector only knew
3024 // Int / BigInt / Text rows — so `ARRAY[ARRAY[true,false]]` stopped being a
3025 // 2-D array at all and collapsed into a 1-D text[] of rendered rows, with
3026 // `[1][2]` failing outright. A regression THIS campaign introduced, caught by
3027 // the very sweep that was chasing its own residual. Rows that are not
3028 // int/bigint arrays render into the text 2-D form below (SPG has no bool 2-D
3029 // storage variant — a recorded residual), but they stay 2-D.
3030 let all_arrays = !materialised.is_empty()
3031 && materialised.iter().all(|v| {
3032 values::array_len(v).is_some()
3033 && !matches!(
3034 v,
3035 Value::TextArray2D(_)
3036 | Value::IntArray2D(_)
3037 | Value::BigIntArray2D(_)
3038 | Value::BoolArray2D(_)
3039 )
3040 });
3041 if all_arrays {
3042 let row_len = values::array_len(&materialised[0]).unwrap_or(0);
3043 let same_len = materialised
3044 .iter()
3045 .all(|v| values::array_len(v) == Some(row_len));
3046 if !same_len {
3047 return Err(EvalError::TypeMismatch {
3048 detail: "multidimensional arrays must have array expressions \
3049 with matching dimensions"
3050 .into(),
3051 });
3052 }
3053 // v7.39 (read01 round 75) — all-BOOL rows build a real `bool[][]`. BOOL is
3054 // the one element type whose ARRAY rendering (`t`) differs from its
3055 // scalar one (`true`), so the text-backed 2-D could not be right for it:
3056 // `ARRAY[ARRAY[true,false]]::text` wants `{{t,f}}` while `[1][2]::text`
3057 // wants `false`. Every other type renders the same either way — which is
3058 // why this is the only typed 2-D SPG needs.
3059 if materialised
3060 .iter()
3061 .all(|v| matches!(v, Value::BoolArray(_)))
3062 {
3063 let rows: Vec<Vec<Option<bool>>> = materialised
3064 .into_iter()
3065 .map(|v| match v {
3066 Value::BoolArray(r) => r,
3067 _ => unreachable!("checked above"),
3068 })
3069 .collect();
3070 return Ok(Value::BoolArray2D(rows));
3071 }
3072 let any_text = materialised
3073 .iter()
3074 .any(|v| !matches!(v, Value::IntArray(_) | Value::BigIntArray(_)));
3075 let any_big = materialised
3076 .iter()
3077 .any(|v| matches!(v, Value::BigIntArray(_)));
3078 if any_text {
3079 let rows: Vec<Vec<Option<String>>> = materialised
3080 .into_iter()
3081 .map(|v| match v {
3082 Value::TextArray(r) => r,
3083 // Any other element type renders into the text 2-D form,
3084 // element by element. SPG has no typed 2-D storage beyond
3085 // int / bigint / text, so a bool 2-D array IS text — and the
3086 // SCALAR rendering is the one to use: `(arr)[1][2]::text`
3087 // must read `false`, as in PG. (`pg_typeof` reporting
3088 // `text[]` rather than `boolean[]` is the recorded residual;
3089 // a typed 2-D needs new storage variants.)
3090 other => {
3091 let n = values::array_len(&other).unwrap_or(0);
3092 (0..n)
3093 .map(|i| match values::array_element_at(&other, i) {
3094 None | Some(Value::Null) => None,
3095 Some(v) => Some(value_to_text(&v)),
3096 })
3097 .collect()
3098 }
3099 })
3100 .collect();
3101 return Ok(Value::TextArray2D(rows));
3102 }
3103 if any_big {
3104 let rows: Vec<Vec<Option<i64>>> = materialised
3105 .into_iter()
3106 .map(|v| match v {
3107 Value::BigIntArray(r) => r,
3108 Value::IntArray(r) => r.into_iter().map(|c| c.map(i64::from)).collect(),
3109 _ => unreachable!(),
3110 })
3111 .collect();
3112 return Ok(Value::BigIntArray2D(rows));
3113 }
3114 let rows: Vec<Vec<Option<i32>>> = materialised
3115 .into_iter()
3116 .map(|v| match v {
3117 Value::IntArray(r) => r,
3118 _ => unreachable!(),
3119 })
3120 .collect();
3121 return Ok(Value::IntArray2D(rows));
3122 }
3123 // v7.39 (read01 round 72) — a HOMOGENEOUS array of a non-numeric, non-text
3124 // type keeps that type, and is unambiguous, so it is decided BEFORE the
3125 // numeric/text unification below. Everything outside the numeric ladder and
3126 // text used to fall into that loop's `_ => has_text = true` — a silent
3127 // degradation, not a decision: `ARRAY[true, false]` came back as `text[]`.
3128 // It usually LOOKED right (array_to_string renders `t` either way), which is
3129 // exactly what let it sit; the array FUNCTIONS are what tripped over it.
3130 if let Some(v) = values::homogeneous_typed_array(&materialised) {
3131 return Ok(crate::describe::upgrade_timestamptz_array(
3132 v,
3133 items,
3134 ctx.columns,
3135 ));
3136 }
3137 // v7.39 (round 236) — PG resolves an ARRAY constructor's elements to ONE
3138 // element type and refuses the constructor when they have no common one:
3139 // `ARRAY[1, 'a'::text]` is "ARRAY types integer and text cannot be
3140 // matched". SPG degraded to `text[]` instead, so `ARRAY[1, true]` came
3141 // back as `{1,t}` — a column of rendered strings that then behaved like
3142 // text everywhere downstream. Same rule (and the same untyped-literal
3143 // subtlety) as the set-operation resolution in round 233: a bare string
3144 // literal is PG's `unknown` and takes the other elements' type, so it is
3145 // identified from the SYNTAX, not from the value's runtime type.
3146 unify_array_elements(items, &mut materialised)?;
3147 // Coercing the untyped elements can make the array homogeneous
3148 // (`ARRAY[true,'t']` becomes two booleans), so re-try the typed-array
3149 // path before falling into the numeric/text ladder below — otherwise
3150 // the now-uniform boolean array would still degrade to text[].
3151 if let Some(v) = values::homogeneous_typed_array(&materialised) {
3152 return Ok(crate::describe::upgrade_timestamptz_array(
3153 v,
3154 items,
3155 ctx.columns,
3156 ));
3157 }
3158 let mut has_text = false;
3159 let mut has_float = false;
3160 let mut has_numeric = false;
3161 let mut has_bigint = false;
3162 let mut has_int = false;
3163 // A NumericBig or non-finite (NaN/Inf) numeric can't be held in
3164 // NumericArray's `(i128, scale)` cells, so it forces the text[]
3165 // fallback rather than a lossy/panicking conversion.
3166 let mut numeric_representable = true;
3167 for v in &materialised {
3168 match v {
3169 Value::Null => {}
3170 Value::Int(_) | Value::SmallInt(_) => has_int = true,
3171 Value::BigInt(_) => has_bigint = true,
3172 Value::Numeric {
3173 kind: spg_storage::NumericKind::Finite,
3174 ..
3175 } => {
3176 has_numeric = true;
3177 }
3178 Value::Numeric { .. } => {
3179 has_numeric = true;
3180 numeric_representable = false;
3181 }
3182 Value::NumericBig(_) => {
3183 has_numeric = true;
3184 numeric_representable = false;
3185 }
3186 Value::Float(_) => has_float = true,
3187 Value::Text(_) | Value::Json(_) => has_text = true,
3188 // v7.39 (round 652) — a reg value belongs to the array by
3189 // its OID half. Falling into the catch-all made
3190 // `ARRAY['pg_class'::regclass]` a text array, so
3191 // `oid = ANY(…)` compared bigint against text and was
3192 // refused — while the identical `oid = 'pg_class'::regclass`
3193 // worked. Same defect the IN-list gate had, one layer down.
3194 Value::RegClass(..) | Value::RegProc(..) | Value::RegType(..) => has_bigint = true,
3195 _ => has_text = true,
3196 }
3197 }
3198 let any_numlike = has_int || has_bigint || has_numeric || has_float;
3199 if has_text || !any_numlike || (has_numeric && !numeric_representable) {
3200 let out: Vec<Option<String>> = materialised
3201 .into_iter()
3202 .map(|v| match v {
3203 Value::Null => None,
3204 Value::Text(s) | Value::Json(s) => Some(s.into_owned()),
3205 other => Some(value_to_text_for_array(&other, &ctx.render_style)),
3206 })
3207 .collect();
3208 return Ok(Value::TextArray(out));
3209 }
3210 // v7.38 (read01) — PG array-element unification across the numeric
3211 // ladder: any float → double precision[]; else any numeric →
3212 // numeric[] (each element keeps its own scale, PG's behaviour);
3213 // else the integer widths. Matches `pg_typeof(ARRAY[1, 2.5])` =
3214 // numeric[] and keeps downstream `[i]` arithmetic numeric.
3215 if has_float {
3216 let out: Vec<Option<f64>> = materialised
3217 .into_iter()
3218 .map(|v| match v {
3219 Value::Null => None,
3220 Value::Float(f) => Some(f),
3221 Value::Int(n) => Some(f64::from(n)),
3222 Value::SmallInt(n) => Some(f64::from(n)),
3223 #[allow(clippy::cast_precision_loss)]
3224 Value::BigInt(n) => Some(n as f64),
3225 #[allow(clippy::cast_precision_loss)]
3226 Value::Numeric { scaled, scale, .. } => {
3227 Some(scaled as f64 / libm::pow(10.0, f64::from(scale)))
3228 }
3229 _ => None,
3230 })
3231 .collect();
3232 return Ok(Value::FloatArray(out));
3233 }
3234 if has_numeric {
3235 let out: Vec<Option<(i128, u16)>> = materialised
3236 .into_iter()
3237 .map(|v| match v {
3238 Value::Null => None,
3239 Value::SmallInt(n) => Some((i128::from(n), 0)),
3240 Value::Int(n) => Some((i128::from(n), 0)),
3241 Value::BigInt(n) => Some((i128::from(n), 0)),
3242 Value::Numeric { scaled, scale, .. } => Some((scaled, scale)),
3243 _ => None,
3244 })
3245 .collect();
3246 return Ok(Value::NumericArray(out));
3247 }
3248 if has_bigint {
3249 let out: Vec<Option<i64>> = materialised
3250 .into_iter()
3251 .map(|v| match v {
3252 Value::Null => None,
3253 Value::Int(n) => Some(i64::from(n)),
3254 Value::SmallInt(n) => Some(i64::from(n)),
3255 Value::BigInt(n) => Some(n),
3256 // Keep in step with the `has_bigint` classification above:
3257 // whatever is counted there has to be convertible here, and
3258 // the arm below panics rather than errors. Round 652 added
3259 // the reg family to the classifier and this materialiser
3260 // took a wire-visible panic until it learned them too.
3261 Value::RegClass(oid, _) | Value::RegProc(oid, _) | Value::RegType(oid, _) => {
3262 Some(oid)
3263 }
3264 _ => unreachable!(),
3265 })
3266 .collect();
3267 return Ok(Value::BigIntArray(out));
3268 }
3269 let out: Vec<Option<i32>> = materialised
3270 .into_iter()
3271 .map(|v| match v {
3272 Value::Null => None,
3273 Value::Int(n) => Some(n),
3274 Value::SmallInt(n) => Some(i32::from(n)),
3275 _ => unreachable!(),
3276 })
3277 .collect();
3278 Ok(Value::IntArray(out))
3279}
3280
3281/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
3282/// (stack-depth guard budget); body unchanged.
3283#[inline(never)]
3284fn eval_function_call_arm(
3285 name: &str,
3286 args: &[Expr],
3287 row: &Row<'static>,
3288 ctx: &EvalContext<'_>,
3289) -> Result<Value<'static>, EvalError> {
3290 // v7.39 (read01 round 77) — named arguments (`f(x := 1)` / `f(x => 1)`).
3291 // The parser leaves them in the tree because only the catalog knows a user
3292 // function's parameter names; here they become positional, once, for
3293 // builtins and user functions alike.
3294 // v7.39 (read01 round 100) — `VARIADIC <array>` splices the array's
3295 // elements in as individual trailing arguments before dispatch, so a
3296 // variadic builtin (concat / concat_ws / format / …) sees them exactly as
3297 // if they had been written out. Done before the named-arg pass and the
3298 // positional dispatch.
3299 if args.iter().any(|a| matches!(a, Expr::Variadic(_))) {
3300 let expanded = expand_variadic_args(args, row, ctx)?;
3301 return eval_function_call_arm(name, &expanded, row, ctx);
3302 }
3303 // v7.39 (round 276) — `date_part('timezone'|…, <timestamp>)` must be
3304 // REJECTED the way EXTRACT already rejects it, and the judgement
3305 // needs the argument's STATIC declared type: SPG stores timestamptz
3306 // in the same `Value::Timestamp`, so a timestamptz legitimately
3307 // answers 0 while a plain timestamp must error. The dispatch below
3308 // receives values, not expressions, so the check belongs here —
3309 // the same place, and the same r237 trust rule (only a cast or a
3310 // column is believed), that the EXTRACT arm uses.
3311 if name.eq_ignore_ascii_case("date_part")
3312 && args.len() == 2
3313 && let Expr::Literal(spg_sql::ast::Literal::String(unit)) = &args[0]
3314 && matches!(
3315 unit.to_ascii_lowercase().as_str(),
3316 "timezone" | "timezone_hour" | "timezone_minute"
3317 )
3318 && matches!(&args[1], Expr::Cast { .. } | Expr::Column(_))
3319 && let Some(sch) = crate::describe::describe_expr(&args[1], ctx.columns)
3320 && matches!(sch.ty, spg_storage::DataType::Timestamp)
3321 {
3322 return Err(EvalError::TypeMismatch {
3323 detail: alloc::format!(
3324 "unit \"{}\" not supported for type timestamp without time zone",
3325 unit.to_ascii_lowercase()
3326 ),
3327 });
3328 }
3329 // v7.39 (round 258) — `pg_typeof` over an ENUM. An enum value travels
3330 // as `Value::Text` (its label), so the value-driven namer answered
3331 // `text`; the type lives in the EXPRESSION, which this arm still has.
3332 // `expr_enum_type_name` resolves a column or a cast statically — the
3333 // same static-only discipline round 253 used for EXTRACT's type name.
3334 if name.eq_ignore_ascii_case("pg_typeof")
3335 && let [arg] = args
3336 {
3337 // The name must be a REAL enum in the catalog. `expr_enum_type_name`
3338 // returns any named cast's target verbatim — it is only a
3339 // pre-filter for `expr_enum_labels`, which does the catalog
3340 // lookup — so using it alone hijacked every `x::float8` /
3341 // `x::int2` and reported SPG's internal spelling instead of PG's.
3342 // v7.39 (round 259) — domains report their own name too; like an
3343 // enum, a domain value travels as its BASE type's value, so the
3344 // name has to come from the expression. Both lookups are gated on
3345 // the catalog: `expr_enum_type_name` returns ANY named cast's
3346 // target verbatim, so an ungated use hijacks `x::float8`.
3347 let is_user_type = |e: &Expr| {
3348 expr_enum_type_name(e, ctx.columns)
3349 .filter(|n| {
3350 // v7.39 (round 330, V48) — the information_schema
3351 // domains are built into the server rather than
3352 // catalog objects (a catalog domain is user data and
3353 // would be dumped), so they are recognised here too.
3354 crate::system_catalog::is_information_schema_domain(n)
3355 || ctx.catalog.is_some_and(|cat| {
3356 cat.enum_types().contains_key(*n)
3357 || cat.domain_types().contains_key(*n)
3358 // v7.39 (round 263) — composites too: a cast
3359 // to one reported the generic `record`.
3360 || cat.composite_types().contains_key(*n)
3361 })
3362 })
3363 .map(alloc::string::String::from)
3364 };
3365 let is_enum = is_user_type;
3366 if let Some(en) = is_enum(arg) {
3367 return Ok(Value::text(en));
3368 }
3369 // `ARRAY[<enum>, …]` reports the array form.
3370 if let Expr::Array(items) = arg
3371 && let Some(first) = items.first()
3372 && let Some(en) = is_enum(first)
3373 {
3374 return Ok(Value::text(alloc::format!("{en}[]")));
3375 }
3376 }
3377 if args.iter().any(|a| matches!(a, Expr::NamedArg { .. })) {
3378 let positional = resolve_named_args(name, args, ctx)?;
3379 return eval_function_call_arm(name, &positional, row, ctx);
3380 }
3381 eval_function_call_positional(name, args, row, ctx)
3382}
3383
3384/// v7.39 (read01 round 100) — rewrite a call's argument list, replacing each
3385/// `VARIADIC <array>` with the array's elements as literal arguments. A NULL
3386/// array contributes no elements (PG treats `VARIADIC NULL` as empty). Regular
3387/// arguments are carried through untouched so they still evaluate against the
3388/// row in the recursive call.
3389fn expand_variadic_args(
3390 args: &[Expr],
3391 row: &Row<'static>,
3392 ctx: &EvalContext<'_>,
3393) -> Result<alloc::vec::Vec<Expr>, EvalError> {
3394 let mut out = alloc::vec::Vec::with_capacity(args.len());
3395 for a in args {
3396 if let Expr::Variadic(inner) = a {
3397 let v = eval_expr(inner, row, ctx)?;
3398 let elems = crate::select::array_value_to_elements(&v).map_err(|_| {
3399 EvalError::TypeMismatch {
3400 detail: "VARIADIC argument must be an array".into(),
3401 }
3402 })?;
3403 for e in elems {
3404 out.push(Expr::Literal(crate::value_to_literal(e)));
3405 }
3406 } else {
3407 out.push(a.clone());
3408 }
3409 }
3410 Ok(out)
3411}
3412
3413/// The declared parameter names of `fname`, or `None` when it takes none.
3414/// Builtins whose parameters PG names live in the table; everything else asks
3415/// the catalog, where a user function's `args_repr` has carried its parameter
3416/// names since the day CREATE FUNCTION stored them.
3417fn declared_param_names(fname: &str, ctx: &EvalContext<'_>) -> Option<alloc::vec::Vec<String>> {
3418 let lower = fname.to_ascii_lowercase();
3419 let builtin: &[&str] = match lower.as_str() {
3420 "make_date" => &["year", "month", "day"],
3421 "make_time" => &["hour", "min", "sec"],
3422 "make_timestamp" | "make_timestamptz" => &["year", "month", "mday", "hour", "min", "sec"],
3423 "make_interval" => &["years", "months", "weeks", "days", "hours", "mins", "secs"],
3424 _ => &[],
3425 };
3426 if !builtin.is_empty() {
3427 return Some(builtin.iter().map(|s| (*s).to_string()).collect());
3428 }
3429 let cat = ctx.catalog?;
3430 let def = cat
3431 .functions()
3432 .values()
3433 .find(|f| f.name.eq_ignore_ascii_case(&lower))?;
3434 let names = spg_storage::function_arg_names(&def.args_repr);
3435 if names.iter().all(alloc::string::String::is_empty) {
3436 return None;
3437 }
3438 Some(names)
3439}
3440
3441/// Rewrite a call's arguments into positional order. Positional arguments fill
3442/// slots left to right; a named one goes to its declared slot. Slots nobody
3443/// filled stay absent for a user function (arity is checked at the call) and
3444/// become integer 0 for the `make_*` builtins, whose trailing fields PG
3445/// defaults that way.
3446fn resolve_named_args(
3447 fname: &str,
3448 args: &[Expr],
3449 ctx: &EvalContext<'_>,
3450) -> Result<alloc::vec::Vec<Expr>, EvalError> {
3451 let Some(params) = declared_param_names(fname, ctx) else {
3452 return Err(EvalError::TypeMismatch {
3453 detail: alloc::format!("function {fname}(...) does not support named arguments"),
3454 });
3455 };
3456 let mut slots: alloc::vec::Vec<Option<Expr>> = (0..params.len()).map(|_| None).collect();
3457 let mut next_positional = 0usize;
3458 for a in args {
3459 let (idx, val) = match a {
3460 Expr::NamedArg { name, expr } => {
3461 let i = params
3462 .iter()
3463 .position(|p| p.eq_ignore_ascii_case(name))
3464 .ok_or_else(|| EvalError::TypeMismatch {
3465 detail: alloc::format!("{fname}(...) has no argument named \"{name}\""),
3466 })?;
3467 (i, (**expr).clone())
3468 }
3469 other => {
3470 let i = next_positional;
3471 next_positional += 1;
3472 (i, other.clone())
3473 }
3474 };
3475 if idx >= slots.len() {
3476 return Err(EvalError::TypeMismatch {
3477 detail: alloc::format!("{fname}(...) got too many arguments"),
3478 });
3479 }
3480 if slots[idx].is_some() {
3481 return Err(EvalError::TypeMismatch {
3482 detail: alloc::format!("{fname}(...) got multiple values for one argument"),
3483 });
3484 }
3485 slots[idx] = Some(val);
3486 }
3487 let make_family = fname.to_ascii_lowercase().starts_with("make_");
3488 let mut out = alloc::vec::Vec::with_capacity(slots.len());
3489 for slot in slots {
3490 match slot {
3491 Some(e) => out.push(e),
3492 None if make_family => {
3493 out.push(Expr::Literal(spg_sql::ast::Literal::Integer(0)));
3494 }
3495 // A user function's unfilled slot is simply not passed; the call's
3496 // own arity check phrases the error.
3497 None => {}
3498 }
3499 }
3500 Ok(out)
3501}
3502
3503fn eval_function_call_positional(
3504 name: &str,
3505 args: &[Expr],
3506 row: &Row<'static>,
3507 ctx: &EvalContext<'_>,
3508) -> Result<Value<'static>, EvalError> {
3509 // v7.39 (round 237) — COALESCE / GREATEST / LEAST resolve their
3510 // arguments to one type the way CASE and ARRAY do. Checked statically:
3511 // an argument may have side effects (`COALESCE(nextval('s'), 1)`), so
3512 // its declared type is read rather than its value.
3513 if matches!(args.len(), 2..) {
3514 let construct = if name.eq_ignore_ascii_case("coalesce") {
3515 Some("COALESCE")
3516 } else if name.eq_ignore_ascii_case("greatest") {
3517 Some("GREATEST")
3518 } else if name.eq_ignore_ascii_case("least") {
3519 Some("LEAST")
3520 } else {
3521 None
3522 };
3523 if let Some(construct) = construct {
3524 unify_branch_types_static(construct, args.iter(), ctx)?;
3525 }
3526 }
3527 // v7.39 (read01 utils/adt, enum.c) — the enum introspection
3528 // v7.39 (read01 utils/adt, enum.c) — the enum introspection
3529 // family needs the ARGUMENT'S STATIC TYPE (the value is
3530 // usually NULL::enumtype): first/last/range over the
3531 // catalog's member order. Out-of-line so eval_expr's
3532 // recursion frame stays small (stack-depth guard budget).
3533 if (name.eq_ignore_ascii_case("enum_first")
3534 || name.eq_ignore_ascii_case("enum_last")
3535 || name.eq_ignore_ascii_case("enum_range"))
3536 && enum_introspection_applies(args, ctx)
3537 {
3538 return eval_enum_introspection(name, args, row, ctx);
3539 }
3540 // v7.39 (tz epic) — AT TIME ZONE (fn form: timezone(zone, ts))
3541 // with a NAMED zone needs the host tzdb and the argument's
3542 // static type for its two directions:
3543 // naive AT ZONE -> that zone's wall clock -> UTC instant
3544 // tstz AT ZONE -> UTC instant -> that zone's wall clock
3545 // Fixed offsets / abbreviations keep the legacy path below.
3546 // v7.39 (round 523) — `to_char(tstz, fmt)` renders the LOCAL clock
3547 // in the session zone, and its zone tokens name that zone. It was
3548 // rendering the UTC reading and spelling it `UTC`, so a formatted
3549 // stamp disagreed with the same value's own `::text`.
3550 if args.len() == 2
3551 && name.eq_ignore_ascii_case("to_char")
3552 && let Some(zone) = ctx.session_gucs.and_then(|g| g.get("timezone"))
3553 && !zone.eq_ignore_ascii_case("utc")
3554 && !zone.eq_ignore_ascii_case("gmt")
3555 && crate::describe::describe_expr(&args[0], ctx.columns)
3556 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
3557 && let Value::Timestamp(t) = eval_expr(&args[0], row, ctx)?
3558 {
3559 let off = ctx.session_tz_offset_at(t);
3560 let abbrev = ctx
3561 .session_tz_abbrev_at(t)
3562 .unwrap_or_else(|| zone.to_uppercase());
3563 let vals = [
3564 Value::Timestamp(t.saturating_add(off)),
3565 eval_expr(&args[1], row, ctx)?,
3566 ];
3567 return crate::eval::strings::to_char_in_zone(&vals, Some((&abbrev, off)));
3568 }
3569 // v7.39 (round 523) — `date_trunc(unit, tstz)` truncates on the
3570 // LOCAL calendar in the session zone. It was truncating in UTC and
3571 // rendering the result in the session zone, so under `SET TimeZone =
3572 // 'Asia/Tokyo'` a day truncation answered `2020-01-01 09:00:00+09` —
3573 // not a day boundary at all, and the wrong day for anything before
3574 // 09:00. Every report grouped by day was cut nine hours late.
3575 //
3576 // The three-argument form already does exactly this, DST reverse
3577 // lookup and all, so the session zone is passed to THAT rather than
3578 // written a second time. Only a statically-known timestamptz shifts:
3579 // a naive timestamp has no zone to be read in.
3580 if args.len() == 2
3581 && (name.eq_ignore_ascii_case("date_trunc") || name.eq_ignore_ascii_case("date_bin"))
3582 && let Some(zone) = ctx.session_gucs.and_then(|g| g.get("timezone"))
3583 && !zone.eq_ignore_ascii_case("utc")
3584 && !zone.eq_ignore_ascii_case("gmt")
3585 && crate::describe::describe_expr(&args[1], ctx.columns)
3586 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
3587 {
3588 let vals = [
3589 eval_expr(&args[0], row, ctx)?,
3590 eval_expr(&args[1], row, ctx)?,
3591 Value::text(zone.clone()),
3592 ];
3593 return datetime::date_trunc(&vals, ctx);
3594 }
3595 if args.len() == 2
3596 && name.eq_ignore_ascii_case("timezone")
3597 && let zone_v = eval_expr(&args[0], row, ctx)?
3598 && let Value::Text(zone) = &zone_v
3599 && datetime::resolve_zone_offset(zone.as_ref()).is_none()
3600 && !zone.trim().eq_ignore_ascii_case("utc")
3601 && !zone.trim().eq_ignore_ascii_case("gmt")
3602 && zone.parse::<i64>().is_err()
3603 && ctx.tz_offset_fn.is_some()
3604 {
3605 let zone = zone.trim();
3606 let src_is_tstz = crate::describe::describe_expr(&args[1], ctx.columns)
3607 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz));
3608 let inner = eval_expr(&args[1], row, ctx)?;
3609 if let Value::Timestamp(t) = inner {
3610 if src_is_tstz {
3611 if let Some(off) = ctx.zone_offset_at(zone, t) {
3612 return Ok(Value::Timestamp(t + off));
3613 }
3614 } else if let Some(utc) = ctx.zone_local_to_utc(zone, t) {
3615 return Ok(Value::Timestamp(utc));
3616 }
3617 return Err(EvalError::TypeMismatch {
3618 detail: alloc::format!("time zone \"{zone}\" not recognized"),
3619 });
3620 }
3621 }
3622 // v7.29 (round-22 phase 3) - prefix fast path: LEFT(col, n)
3623 // on a TEXT column borrows the cell and clones only the
3624 // prefix. The generic path clones the WHOLE cell first -
3625 // a LEFT(body, 120) over 24k x 30 KB rows spent 383 ms
3626 // copying bytes it then threw away (7 ms without LEFT).
3627 if args.len() == 2
3628 && name.eq_ignore_ascii_case("left")
3629 && let Expr::Column(c) = &args[0]
3630 && let Some(cell) = resolve_column_borrowed(c, row, ctx)?
3631 {
3632 {
3633 match cell {
3634 Value::Null => return Ok(Value::Null),
3635 Value::Text(t) => {
3636 let n_v = eval_expr(&args[1], row, ctx)?;
3637 if let Value::SmallInt(_) | Value::Int(_) | Value::BigInt(_) = n_v {
3638 let n = match n_v {
3639 Value::SmallInt(x) => i64::from(x),
3640 Value::Int(x) => i64::from(x),
3641 Value::BigInt(x) => x,
3642 _ => 0,
3643 };
3644 return Ok(Value::text(text_prefix_chars(t, n)));
3645 }
3646 }
3647 _ => {}
3648 }
3649 }
3650 }
3651 // v7.38 (T-tstz Phase 1) — the ONE case where pg_typeof needs the
3652 // static type: timestamptz. The runtime value is a tz-less
3653 // Value::Timestamp, so the value-driven answer below can only ever
3654 // say "without time zone". For every other type the value-driven
3655 // path is strictly better (it distinguishes json vs jsonb, keeps
3656 // NULL as "unknown", and is not fooled by describe_expr's lossy
3657 // heuristics), so we consult the static typer ONLY when it says
3658 // Timestamptz and otherwise fall through untouched.
3659 if args.len() == 1
3660 && name.eq_ignore_ascii_case("pg_typeof")
3661 && crate::describe::describe_expr(&args[0], ctx.columns)
3662 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
3663 {
3664 return Ok(Value::text::<alloc::string::String>(
3665 "timestamp with time zone".into(),
3666 ));
3667 }
3668 // v7.39 (round 694) — `oid[]`. Its VALUE is a BigIntArray, so the
3669 // value-driven namer answers `bigint[]`; the declared type lives in the
3670 // expression, exactly as it does for the Timestamptz arm above and for
3671 // the enum / domain / composite arms further down. The scalar `oid`
3672 // needed the same treatment in round 667.
3673 if args.len() == 1
3674 && name.eq_ignore_ascii_case("pg_typeof")
3675 && crate::describe::describe_expr(&args[0], ctx.columns)
3676 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::OidArray))
3677 {
3678 return Ok(Value::text::<alloc::string::String>("oid[]".into()));
3679 }
3680 // v7.39 (read01 round 56) — a COMPOSITE column reports its type NAME, not
3681 // the generic `record` the runtime value would give. Composite-ness lives
3682 // outside the DataType lattice (the stored form is JSON), so the witness is
3683 // the column's `user_composite_type` — the same shape as the enum witness.
3684 if args.len() == 1
3685 && name.eq_ignore_ascii_case("pg_typeof")
3686 && let Expr::Column(c) = &args[0]
3687 && let Some(cname) = ctx
3688 .columns
3689 .iter()
3690 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
3691 .and_then(|sc| sc.user_composite_type.as_deref())
3692 {
3693 return Ok(Value::text::<alloc::string::String>(cname.into()));
3694 }
3695 // v7.39 (round 291) — `name` is a DECLARED type over a Value::Text,
3696 // so the value can never witness it; the schema is the only witness,
3697 // exactly as for a composite column above.
3698 if args.len() == 1
3699 && name.eq_ignore_ascii_case("pg_typeof")
3700 && let Expr::Column(c) = &args[0]
3701 && ctx
3702 .columns
3703 .iter()
3704 .any(|sc| sc.name.eq_ignore_ascii_case(&c.name) && sc.ty == spg_storage::DataType::Name)
3705 {
3706 return Ok(Value::text::<alloc::string::String>("name".into()));
3707 }
3708 // …and the same for a bare `'abc'::name`, where the cast TARGET is
3709 // the witness. PG computes pg_typeof statically; SPG reads the
3710 // value, which by then is an ordinary text.
3711 if args.len() == 1
3712 && name.eq_ignore_ascii_case("pg_typeof")
3713 && let Expr::Cast {
3714 target: spg_sql::ast::CastTarget::Named(n),
3715 ..
3716 } = &args[0]
3717 && n.eq_ignore_ascii_case("name")
3718 {
3719 return Ok(Value::text::<alloc::string::String>("name".into()));
3720 }
3721 // v7.39 (read01 round 116) — a bare, uncoerced string literal is PG's
3722 // `unknown` type, not text: `pg_typeof('x')` / `pg_typeof('123')` /
3723 // `pg_typeof('2024-01-01')` all report `unknown`. The literal only becomes
3724 // text once context coerces it — a cast (`'x'::text`), a concatenation, or
3725 // a function argument — each of which is a different Expr node that falls
3726 // through to the value-driven path below (which correctly says text).
3727 if args.len() == 1
3728 && name.eq_ignore_ascii_case("pg_typeof")
3729 && matches!(&args[0], Expr::Literal(spg_sql::ast::Literal::String(_)))
3730 {
3731 return Ok(Value::text::<alloc::string::String>("unknown".into()));
3732 }
3733 // v7.37.16 — pg_typeof of a NULL cell reports the COLUMN's
3734 // static type when it has one (PG: `VALUES (NULL),(1.5)`
3735 // types the column numeric and its NULL row's pg_typeof says
3736 // numeric, not unknown).
3737 //
3738 // A BARE `NULL` stays "unknown", as PG has it. That used to fall
3739 // out of TEXT being absent from the name table — a NULL literal
3740 // describes as TEXT, so the lookup returned None and the caller
3741 // reported unknown. Round 871 filled that table in, which silently
3742 // took the bare-NULL behaviour with it and broke
3743 // `pg_typeof_null_returns_unknown`. The rule is now stated rather
3744 // than emergent: an untyped NULL literal is unknown, a NULL that
3745 // was cast reports what it was cast to.
3746 if args.len() == 1 && name.eq_ignore_ascii_case("pg_typeof") {
3747 if matches!(&args[0], Expr::Literal(spg_sql::ast::Literal::Null)) {
3748 return Ok(Value::text::<alloc::string::String>("unknown".into()));
3749 }
3750 let v = eval_expr(&args[0], row, ctx)?;
3751 if matches!(v, Value::Null)
3752 && let Some(shape) = crate::describe::describe_expr(&args[0], ctx.columns)
3753 && let Some(n) = pg_typeof_name_for_datatype(shape.ty)
3754 {
3755 return Ok(Value::text(n));
3756 }
3757 // v7.39 (round 640) — a NON-null cell normally answers from the
3758 // value, which is right for every type whose identity the value
3759 // carries. `xid8` has no value of its own — a cell is a
3760 // `Value::BigInt` — so it can only ever say "bigint" unless the
3761 // schema is asked. `xid` is listed with it because a cell that
3762 // reached here as a plain integer (a synthesised catalog row
3763 // that has not been converted) should still name its column's
3764 // type rather than the storage it arrived in.
3765 //
3766 // v7.39 (round 667) — `oid` joins them for the same reason and no
3767 // other: its cell is a `Value::BigInt` too, so `pg_typeof(1::oid)`
3768 // answered `bigint`. Still not a general switch — where the value
3769 // knows its own identity it is the better witness, because an
3770 // expression's static shape is an approximation and its result is
3771 // the fact.
3772 if !matches!(v, Value::Null)
3773 && let Some(shape) = crate::describe::describe_expr(&args[0], ctx.columns)
3774 && matches!(
3775 shape.ty,
3776 spg_storage::DataType::Xid
3777 | spg_storage::DataType::Xid8
3778 | spg_storage::DataType::Oid
3779 )
3780 && let Some(n) = pg_typeof_name_for_datatype(shape.ty)
3781 {
3782 return Ok(Value::text(n));
3783 }
3784 return apply_function(name, &[v], ctx);
3785 }
3786 // v7.37 D.1 — COALESCE result-type coercion. PG gives COALESCE the
3787 // common type of its branches, so a typed sibling (`NULL::time`,
3788 // `col::time`) makes the whole expression that type and an untyped
3789 // string-literal branch is coerced to it. Without this,
3790 // `COALESCE(NULL::time, '12:00')::text` rendered the raw `12:00`
3791 // instead of `12:00:00`. Only kicks in when the picked value is a
3792 // bare Text and a non-text cast-target sibling exists.
3793 if name.eq_ignore_ascii_case("coalesce") && !args.is_empty() {
3794 // v7.39 (round 609) — PG's COALESCE does not evaluate a branch past
3795 // the first non-NULL one. This evaluated every branch into a `Vec`
3796 // and so RAISED errors PG never raises: `coalesce(1, 1/0)`,
3797 // `coalesce(NULL, 2, 1/0)` and `coalesce(1, NULL, 1/0)` all failed
3798 // with "division by zero" where PG answers 1, 2 and 1.
3799 //
3800 // A branch after the pick is still READ for its type — that is what
3801 // decides the result's, and `COALESCE(1, 2.5)` is numeric in both
3802 // engines — but its error is discarded, because PG never runs it and
3803 // so never reports it. A branch that fails contributes no type,
3804 // which is the same as SPG having no declared type to widen to.
3805 //
3806 // The two `Vec`s this replaces cost two allocations a row even for
3807 // `coalesce(id, 0)` over a plain INTEGER column, where the answer
3808 // needs none.
3809 let mut result: Option<Value<'static>> = None;
3810 let mut tbuf = [spg_storage::DataType::Int; 8];
3811 let mut ntypes = 0usize;
3812 let mut spill: Vec<spg_storage::DataType> = Vec::new();
3813 for a in args {
3814 let v = if result.is_none() {
3815 eval_expr(a, row, ctx)?
3816 } else {
3817 match eval_expr(a, row, ctx) {
3818 Ok(v) => v,
3819 Err(_) => continue,
3820 }
3821 };
3822 // v7.39 (round 649) — a NULL branch still has a TYPE, and PG
3823 // resolves COALESCE's result from the branches' declared
3824 // types, not from the values that survive. `Value::Null` has
3825 // no `data_type()`, so `coalesce(1::int, NULL::float8)`
3826 // collected only `integer` and answered integer where PG
3827 // answers double precision. Ask the expression when the
3828 // value cannot say — inside the arm that already runs, and
3829 // only on the NULL that would otherwise contribute nothing.
3830 let branch_ty = match v.data_type() {
3831 Some(t) => Some(t),
3832 None => crate::describe::describe_expr(a, ctx.columns).map(|sh| sh.ty),
3833 };
3834 if let Some(t) = branch_ty {
3835 if ntypes < tbuf.len() {
3836 tbuf[ntypes] = t;
3837 ntypes += 1;
3838 } else {
3839 spill.push(t);
3840 }
3841 }
3842 if result.is_none() && !matches!(v, Value::Null) {
3843 result = Some(v);
3844 }
3845 }
3846 let result = result.unwrap_or(Value::Null);
3847 if matches!(result, Value::Text(_)) {
3848 if let Some(target) = args.iter().find_map(coalesce_type_hint) {
3849 return crate::eval::cast::cast_value(result, target);
3850 }
3851 }
3852 // v7.38 (read01) — otherwise widen the picked value to the PG
3853 // common type of all branches (COALESCE(1, 2.5) → numeric).
3854 if spill.is_empty() {
3855 return Ok(widen_to_common(result, &tbuf[..ntypes]));
3856 }
3857 let mut types: Vec<spg_storage::DataType> = tbuf[..ntypes].to_vec();
3858 types.append(&mut spill);
3859 return Ok(widen_to_common(result, &types));
3860 }
3861 let evaluated: Result<Vec<Value<'static>>, _> =
3862 args.iter().map(|a| eval_expr(a, row, ctx)).collect();
3863 let evaluated = evaluated?;
3864 // v7.39 (read01 json.c) — to_json(timestamptz) spells the instant in
3865 // ISO 8601 WITH the session-zone offset ("2024-03-09T14:05:06+00:00"),
3866 // unlike plain timestamp. The runtime value carries no tz tag, so the
3867 // argument's static type is the witness.
3868 if (name.eq_ignore_ascii_case("to_json") || name.eq_ignore_ascii_case("to_jsonb"))
3869 && evaluated.len() == 1
3870 && let Some(Value::Timestamp(t)) = evaluated.first()
3871 && args.first().is_some_and(|a| {
3872 crate::describe::describe_expr(a, ctx.columns)
3873 .is_some_and(|sh| matches!(sh.ty, spg_storage::DataType::Timestamptz))
3874 })
3875 {
3876 let off = ctx.session_tz_offset_at(*t);
3877 let local = t + off;
3878 let days = local.div_euclid(86_400_000_000);
3879 let day_us = local.rem_euclid(86_400_000_000);
3880 let (y, mo, d) = civil_from_days(i32::try_from(days).unwrap_or(0));
3881 let secs = day_us / 1_000_000;
3882 let frac = day_us % 1_000_000;
3883 let (hh, mi, ss) = (secs / 3600, (secs / 60) % 60, secs % 60);
3884 let mut txt = alloc::format!("{y:04}-{mo:02}-{d:02}T{hh:02}:{mi:02}:{ss:02}");
3885 if frac != 0 {
3886 let f = alloc::format!("{frac:06}");
3887 txt.push('.');
3888 txt.push_str(f.trim_end_matches('0'));
3889 }
3890 let (sign, omag) = if off < 0 { ('-', -off) } else { ('+', off) };
3891 let (oh, om) = (omag / 3_600_000_000, (omag / 60_000_000) % 60);
3892 let _ = core::fmt::Write::write_fmt(&mut txt, format_args!("{sign}{oh:02}:{om:02}"));
3893 return Ok(Value::json(alloc::format!("\"{txt}\"")));
3894 }
3895 // v7.39 (enum order knife) — greatest/least over enum-typed arguments
3896 // pick by member order, not label text (PG). The witness needs the arg
3897 // ASTs, so this can't live in the value-level function dispatch.
3898 if (name.eq_ignore_ascii_case("greatest") || name.eq_ignore_ascii_case("least"))
3899 && let Some(labels) = args
3900 .iter()
3901 .find_map(|a| expr_enum_labels(a, ctx.columns, ctx.catalog))
3902 && evaluated
3903 .iter()
3904 .all(|v| matches!(v, Value::Text(_) | Value::Null))
3905 {
3906 let is_greatest = name.eq_ignore_ascii_case("greatest");
3907 let mut best: Option<&Value<'static>> = None;
3908 for v in evaluated.iter().filter(|v| !matches!(v, Value::Null)) {
3909 best = Some(match best {
3910 None => v,
3911 Some(b) => match enum_ord_cmp(labels, v, b) {
3912 Some(core::cmp::Ordering::Greater) if is_greatest => v,
3913 Some(core::cmp::Ordering::Less) if !is_greatest => v,
3914 Some(_) => b,
3915 // A non-member snuck in — fall out to the generic path.
3916 None => return apply_function(name, &evaluated, ctx),
3917 },
3918 });
3919 }
3920 return Ok(best.cloned().unwrap_or(Value::Null));
3921 }
3922 // v7.39 (round 693) — and the same for a declared COLLATION, which
3923 // `least`/`greatest` need for the same structural reason: the witness
3924 // is the argument's column, so it cannot live in the value-level
3925 // dispatch either. Measured on PG18 over a column declaring
3926 // en_US.utf8: `least(a,'d')` is `d` and `greatest(a,'d')` is `Zebra`,
3927 // where byte order gives the pair reversed.
3928 if (name.eq_ignore_ascii_case("greatest") || name.eq_ignore_ascii_case("least"))
3929 && evaluated
3930 .iter()
3931 .all(|v| matches!(v, Value::Text(_) | Value::Null))
3932 && let Some(coll) = greatest_least_collation(args, ctx)
3933 {
3934 let is_greatest = name.eq_ignore_ascii_case("greatest");
3935 let mut best: Option<&Value<'static>> = None;
3936 for v in evaluated.iter().filter(|v| !matches!(v, Value::Null)) {
3937 best = Some(match (best, v) {
3938 (None, _) => v,
3939 (Some(Value::Text(y)), Value::Text(x)) => {
3940 match crate::collate::compare(&coll, x, y) {
3941 Some(core::cmp::Ordering::Greater) if is_greatest => v,
3942 Some(core::cmp::Ordering::Less) if !is_greatest => v,
3943 Some(_) => best.unwrap_or(v),
3944 // Not a collation this build performs after all —
3945 // one answer, from the generic path.
3946 None => return apply_function(name, &evaluated, ctx),
3947 }
3948 }
3949 (Some(b), _) => b,
3950 });
3951 }
3952 return Ok(best.cloned().unwrap_or(Value::Null));
3953 }
3954 // v7.39 (round 621) — an unadorned string literal takes the type the
3955 // function's parameter asks for. `justify_interval('36 hours')` is answered
3956 // by PG and was refused here, and so were `justify_days('35 days')` and
3957 // `justify_hours('27 hours')` — the spelling everyone writes, since typing
3958 // `INTERVAL` in front of the literal is exactly what PG saves you from.
3959 //
3960 // Only a LITERAL is resolved, which is the same boundary round 620 drew for
3961 // the boolean connectives: `justify_interval(t)` over a TEXT column stays
3962 // refused, because PG refuses it too (no such overload). The arg ASTs are
3963 // needed to tell those apart, so this cannot live in the value-level
3964 // dispatch — the same reason the enum witness above sits here.
3965 if let Some(want) = unknown_literal_param_type(name) {
3966 let mut coerced = evaluated;
3967 for (i, a) in args.iter().enumerate() {
3968 if is_unknown_string_literal(a)
3969 && let Some(slot) = coerced.get_mut(i)
3970 {
3971 *slot = cast::cast_value_in(
3972 core::mem::replace(slot, Value::Null),
3973 want.clone(),
3974 false,
3975 )?;
3976 }
3977 }
3978 return apply_function(name, &coerced, ctx);
3979 }
3980 apply_function(name, &evaluated, ctx)
3981}
3982
3983/// v7.39 (round 621) — the parameter type a bare string literal resolves to.
3984///
3985/// PG resolves an `unknown` literal to whatever the chosen overload declares.
3986/// SPG has no overload resolution to hang that on, so the functions whose only
3987/// parameter is unambiguous are listed. `None` leaves the argument alone.
3988fn unknown_literal_param_type(name: &str) -> Option<spg_sql::ast::CastTarget> {
3989 match name.to_ascii_lowercase().as_str() {
3990 "justify_days" | "justify_hours" | "justify_interval" => {
3991 Some(spg_sql::ast::CastTarget::Interval)
3992 }
3993 _ => None,
3994 }
3995}
3996
3997/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
3998/// (stack-depth guard budget); body unchanged.
3999#[inline(never)]
4000fn eval_any_all_arm(
4001 expr: &Expr,
4002 op: &BinOp,
4003 array: &Expr,
4004 is_any: bool,
4005 row: &Row<'static>,
4006 ctx: &EvalContext<'_>,
4007) -> Result<Value<'static>, EvalError> {
4008 let lhs = eval_expr(expr, row, ctx)?;
4009 let arr = eval_expr(array, row, ctx)?;
4010 any_all_over(lhs, arr, op, is_any)
4011}
4012
4013/// v7.39 (round 597) — the ANY/ALL comparison with both sides already in
4014/// hand. Split out so a CONSTANT right-hand array can be built once at
4015/// compile time instead of once per row: `WHERE id = ANY (ARRAY[1..10])`
4016/// rebuilt the array for all 500k rows and cost 268 ms against PG18's 8.3,
4017/// rising to 494 ms at twenty elements, while the equivalent
4018/// `id IN (1..10)` took 2.3. The body below is unchanged; it never touched
4019/// `row`.
4020pub(crate) fn any_all_over(
4021 lhs: Value<'static>,
4022 arr: Value<'static>,
4023 op: &BinOp,
4024 is_any: bool,
4025) -> Result<Value<'static>, EvalError> {
4026 if matches!(arr, Value::Null) {
4027 return Ok(Value::Null);
4028 }
4029 // v7.38 (read01) — an unknown-string RHS (`x = ANY('{1,2,3}')`)
4030 // takes the LHS's type: coerce the external array text to the array
4031 // type matching the LHS's element type, like PG.
4032 let arr = match &arr {
4033 Value::Text(_) => {
4034 // The LHS's element type, or TEXT when the LHS is an
4035 // untyped NULL (PG's unknown → text default).
4036 let arr_ty = match lhs.data_type() {
4037 Some(spg_storage::DataType::SmallInt) => spg_storage::DataType::SmallIntArray,
4038 Some(spg_storage::DataType::Int) => spg_storage::DataType::IntArray,
4039 Some(spg_storage::DataType::BigInt) => spg_storage::DataType::BigIntArray,
4040 Some(spg_storage::DataType::Numeric { .. }) => spg_storage::DataType::NumericArray,
4041 Some(spg_storage::DataType::Float) => spg_storage::DataType::FloatArray,
4042 Some(spg_storage::DataType::Bool) => spg_storage::DataType::BoolArray,
4043 Some(spg_storage::DataType::Date) => spg_storage::DataType::DateArray,
4044 _ => spg_storage::DataType::TextArray,
4045 };
4046 crate::conversions::coerce_value(arr.clone(), arr_ty, "", 0).unwrap_or(arr)
4047 }
4048 _ => arr,
4049 };
4050 // Build the element list generically so every scalar array type
4051 // (numeric[], float8[], bool[], date[], …) is accepted, not just
4052 // int/bigint/text.
4053 let Some(len) = array_len(&arr) else {
4054 return Err(EvalError::TypeMismatch {
4055 detail: format!(
4056 "ANY/ALL right-hand side must be an array, got {}",
4057 crate::conversions::pg_type_name_for_error_opt(arr.data_type())
4058 ),
4059 });
4060 };
4061 let elems: Vec<Option<Value>> = (0..len)
4062 .map(|i| match array_element_at(&arr, i) {
4063 Some(Value::Null) | None => None,
4064 Some(v) => Some(v),
4065 })
4066 .collect();
4067 // PG: `x op ANY (empty)` → false and `x op ALL (empty)` →
4068 // true, decided purely by emptiness — the comparison is
4069 // never evaluated, so a NULL LHS is irrelevant. This must
4070 // short-circuit before `saw_null` is seeded from the LHS,
4071 // otherwise `NULL op ANY/ALL (empty)` wrongly yields NULL.
4072 if elems.is_empty() {
4073 return Ok(Value::Bool(!is_any));
4074 }
4075 let mut saw_null = matches!(lhs, Value::Null);
4076 let mut saw_match = false;
4077 let mut saw_mismatch = false;
4078 for elem in elems {
4079 let elem_v = match elem {
4080 Some(v) => v,
4081 None => {
4082 saw_null = true;
4083 continue;
4084 }
4085 };
4086 if matches!(lhs, Value::Null) {
4087 saw_null = true;
4088 continue;
4089 }
4090 match apply_binary(*op, lhs.clone(), elem_v) {
4091 Ok(Value::Bool(true)) => saw_match = true,
4092 Ok(Value::Bool(false)) => saw_mismatch = true,
4093 Ok(Value::Null) => saw_null = true,
4094 Ok(other) => {
4095 return Err(EvalError::TypeMismatch {
4096 detail: format!(
4097 "ANY/ALL comparison didn't return Bool: {}",
4098 crate::conversions::pg_type_name_for_error_opt(other.data_type())
4099 ),
4100 });
4101 }
4102 Err(e) => return Err(e),
4103 }
4104 }
4105 let result = if is_any {
4106 if saw_match {
4107 Value::Bool(true)
4108 } else if saw_null {
4109 Value::Null
4110 } else {
4111 Value::Bool(false)
4112 }
4113 } else if saw_mismatch {
4114 Value::Bool(false)
4115 } else if saw_null {
4116 Value::Null
4117 } else {
4118 Value::Bool(true)
4119 };
4120 Ok(result)
4121}
4122
4123/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
4124/// (stack-depth guard budget); body unchanged.
4125#[inline(never)]
4126fn eval_case_arm(
4127 operand: &Option<alloc::boxed::Box<Expr>>,
4128 branches: &[(Expr, Expr)],
4129 else_branch: &Option<alloc::boxed::Box<Expr>>,
4130 row: &Row<'static>,
4131 ctx: &EvalContext<'_>,
4132) -> Result<Value<'static>, EvalError> {
4133 // v7.39 (round 237) — PG resolves the RESULT branches to one type and
4134 // refuses the CASE when they have no common one, before running
4135 // anything. SPG returned whichever branch fired, so
4136 // `CASE WHEN true THEN 1 ELSE 'a'::text END` answered `1` and the same
4137 // expression answered text on another row.
4138 {
4139 // PG resolves the ELSE branch FIRST and then the WHEN results, and
4140 // its message names the running type before the conflicting one —
4141 // which is why `THEN 1 ELSE 'a'::text` reports "text and integer"
4142 // while a two-WHEN `THEN 1 ... THEN true` reports "integer and
4143 // boolean". Probed against 18.4; the order is observable.
4144 let mut results: Vec<&Expr> = Vec::with_capacity(branches.len() + 1);
4145 if let Some(e) = else_branch {
4146 results.push(e);
4147 }
4148 results.extend(branches.iter().map(|(_, r)| r));
4149 unify_branch_types_static("CASE", results, ctx)?;
4150 }
4151 let operand_value = match operand {
4152 Some(o) => Some(eval_expr(o, row, ctx)?),
4153 None => None,
4154 };
4155 // v7.37 D.1 — CASE result-type coercion (same rule as COALESCE): a
4156 // typed result branch (`... THEN '10:00'::time`) makes the whole
4157 // CASE that type, so an untyped string-literal branch is coerced to
4158 // it. Compute the hint once from every THEN/ELSE branch.
4159 let case_hint = branches
4160 .iter()
4161 .map(|(_, t)| t)
4162 .chain(else_branch.iter().map(|b| b.as_ref()))
4163 .find_map(coalesce_type_hint);
4164 // v7.38 (read01) — the CASE result is PG's common type of every
4165 // THEN/ELSE branch, so a taken integer branch is widened to
4166 // numeric when a sibling branch is numeric (and `pg_typeof` /
4167 // downstream division match PG). Only one branch is evaluated, so
4168 // the type must come from the branch expressions statically.
4169 let branch_types: Vec<spg_storage::DataType> = branches
4170 .iter()
4171 .map(|(_, t)| t)
4172 .chain(else_branch.iter().map(|b| b.as_ref()))
4173 .filter_map(|e| crate::describe::describe_expr(e, ctx.columns).map(|s| s.ty))
4174 .collect();
4175 let coerce = |v: Value<'static>| -> Result<Value<'static>, EvalError> {
4176 let v = match (&v, &case_hint) {
4177 (Value::Text(_), Some(target)) => cast::cast_value(v, target.clone())?,
4178 _ => v,
4179 };
4180 Ok(widen_to_common(v, &branch_types))
4181 };
4182 for (when_expr, then_expr) in branches {
4183 let when_value = eval_expr(when_expr, row, ctx)?;
4184 let matched = match &operand_value {
4185 // v7.39 (round 346, M1) — the WHEN condition is a truth value,
4186 // not a boolean-shaped one: `CASE WHEN 1 THEN 'a' END` used to
4187 // answer NULL in BOTH dialects, where MariaDB answers `a` and
4188 // PG raises `argument of CASE/WHEN must be type boolean`.
4189 None => predicate_is_true(&when_value, "CASE/WHEN", ctx.mysql_dialect)?,
4190 // v7.39 (round 412) — under the MySQL default collation the
4191 // `CASE op WHEN v` equality folds Text/BpChar operands (CI +
4192 // accent + PAD SPACE), matching `op = v` outside CASE.
4193 Some(op_v) => {
4194 let (l, r) = if ctx.mysql_dialect {
4195 match (op_v, &when_value) {
4196 // v7.38.17 — CHAR pads, TEXT does not.
4197 (Value::BpChar(x), Value::BpChar(y)) => (
4198 Value::text(spg_storage::mysql_compare_fold_char(x)),
4199 Value::text(spg_storage::mysql_compare_fold_char(y)),
4200 ),
4201 (Value::Text(x), Value::Text(y)) => (
4202 Value::text(spg_storage::mysql_compare_fold(x)),
4203 Value::text(spg_storage::mysql_compare_fold(y)),
4204 ),
4205 _ => (op_v.clone(), when_value),
4206 }
4207 } else {
4208 (op_v.clone(), when_value)
4209 };
4210 matches!(
4211 apply_binary(spg_sql::ast::BinOp::Eq, l, r)?,
4212 Value::Bool(true)
4213 )
4214 }
4215 };
4216 if matched {
4217 return coerce(eval_expr(then_expr, row, ctx)?);
4218 }
4219 }
4220 match else_branch {
4221 Some(e) => coerce(eval_expr(e, row, ctx)?),
4222 None => Ok(Value::Null),
4223 }
4224}
4225
4226/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
4227/// (stack-depth guard budget); body unchanged.
4228#[inline(never)]
4229fn eval_array_slice_arm(
4230 target: &Expr,
4231 lo: &Option<alloc::boxed::Box<Expr>>,
4232 hi: &Option<alloc::boxed::Box<Expr>>,
4233 row: &Row<'static>,
4234 ctx: &EvalContext<'_>,
4235) -> Result<Value<'static>, EvalError> {
4236 let target_v = eval_expr(target, row, ctx)?;
4237 if matches!(target_v, Value::Null) {
4238 return Ok(Value::Null);
4239 }
4240 let bound = |e: Option<&Expr>| -> Result<Option<i64>, EvalError> {
4241 match e {
4242 None => Ok(None),
4243 Some(b) => match eval_expr(b, row, ctx)? {
4244 Value::Null => Ok(None),
4245 Value::Int(n) => Ok(Some(i64::from(n))),
4246 Value::BigInt(n) => Ok(Some(n)),
4247 Value::SmallInt(n) => Ok(Some(i64::from(n))),
4248 other => Err(EvalError::TypeMismatch {
4249 detail: format!(
4250 "array slice bound must be integer, got {}",
4251 crate::conversions::pg_type_name_for_error_opt(other.data_type())
4252 ),
4253 }),
4254 },
4255 }
4256 };
4257 let lo_b = bound(lo.as_deref())?;
4258 let hi_b = bound(hi.as_deref())?;
4259 fn window(len: usize, lo: Option<i64>, hi: Option<i64>) -> (usize, usize) {
4260 let start = lo.map_or(0, |l| (l.max(1) - 1) as usize).min(len);
4261 let end = hi.map_or(len, |h| h.max(0) as usize).min(len);
4262 (start, end.max(start))
4263 }
4264 match target_v {
4265 Value::TextArray(items) => {
4266 let (s, e) = window(items.len(), lo_b, hi_b);
4267 Ok(Value::TextArray(items[s..e].to_vec()))
4268 }
4269 Value::IntArray(items) => {
4270 let (s, e) = window(items.len(), lo_b, hi_b);
4271 Ok(Value::IntArray(items[s..e].to_vec()))
4272 }
4273 Value::BigIntArray(items) => {
4274 let (s, e) = window(items.len(), lo_b, hi_b);
4275 Ok(Value::BigIntArray(items[s..e].to_vec()))
4276 }
4277 other => Err(EvalError::TypeMismatch {
4278 detail: format!(
4279 "slice target must be an array, got {}",
4280 crate::conversions::pg_type_name_for_error_opt(other.data_type())
4281 ),
4282 }),
4283 }
4284}
4285
4286/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
4287/// (stack-depth guard budget); body unchanged.
4288#[inline(never)]
4289fn eval_in_list_arm(
4290 expr: &Expr,
4291 list: &[Expr],
4292 negated: bool,
4293 row: &Row<'static>,
4294 ctx: &EvalContext<'_>,
4295) -> Result<Value<'static>, EvalError> {
4296 // v7.39 (round 238) — PG resolves the whole list's type BEFORE comparing
4297 // anything, so `1 IN (1, 'a'::text)` is refused. SPG compared item by
4298 // item and broke on the first match, so the offending element was never
4299 // reached and the predicate quietly answered true. Checked statically,
4300 // like round 237: evaluating the rest of the list to inspect it would
4301 // change when side effects fire.
4302 require_in_list_comparable(expr, list, ctx)?;
4303 // v7.39 (round 364, M4 P2) — a MySQL session folds text before the
4304 // membership test, so `t IN ('FOO')` matches 'Foo'. `BINARY` is not
4305 // reachable through a bare column needle here; the fold is text-only.
4306 // v7.39 (round 370, M4 P4a) — an explicit `COLLATE utf8mb4_bin` needle
4307 // column is byte-wise, so it does not fold. v7.39 (round 371, M4 P4b) —
4308 // a per-expression `… COLLATE utf8mb4_bin` / `BINARY …` on the needle
4309 // OR any list item forces the whole membership test byte-wise.
4310 let in_fold = ctx.mysql_dialect
4311 && !resolve::operand_is_binary_column(expr, ctx)
4312 && !resolve::is_binary_coerced(expr)
4313 && !list.iter().any(|i| resolve::is_binary_coerced(i));
4314 let needle = mysql_collation_key(eval_expr(expr, row, ctx)?, in_fold);
4315 let needle_null = matches!(needle, Value::Null);
4316 let mut saw_null = needle_null && !list.is_empty();
4317 let mut matched = false;
4318 if !needle_null {
4319 for item in list {
4320 let v = mysql_collation_key(eval_expr(item, row, ctx)?, in_fold);
4321 if matches!(v, Value::Null) {
4322 saw_null = true;
4323 continue;
4324 }
4325 match apply_binary(BinOp::Eq, needle.clone(), v)? {
4326 Value::Bool(true) => {
4327 matched = true;
4328 break;
4329 }
4330 Value::Bool(false) => {}
4331 Value::Null => saw_null = true,
4332 other => {
4333 return Err(EvalError::TypeMismatch {
4334 detail: format!(
4335 "IN comparison didn't return Bool: {}",
4336 crate::conversions::pg_type_name_for_error_opt(other.data_type())
4337 ),
4338 });
4339 }
4340 }
4341 }
4342 }
4343 let inner = if matched {
4344 Value::Bool(true)
4345 } else if saw_null {
4346 Value::Null
4347 } else {
4348 Value::Bool(false)
4349 };
4350 Ok(match (negated, inner) {
4351 (true, Value::Bool(b)) => Value::Bool(!b),
4352 (_, v) => v,
4353 })
4354}
4355
4356/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
4357/// (stack-depth guard budget); body unchanged.
4358#[inline(never)]
4359fn eval_like_arm(
4360 expr: &Expr,
4361 pattern: &Expr,
4362 negated: bool,
4363 case_insensitive: bool,
4364 row: &Row<'static>,
4365 ctx: &EvalContext<'_>,
4366) -> Result<Value<'static>, EvalError> {
4367 let v = eval_expr(expr, row, ctx)?;
4368 let p = eval_expr(pattern, row, ctx)?;
4369 // NULL on either side propagates to NULL — same as PG.
4370 // v7.39 (bpchar epic) — LIKE matches bpchar on its PADDED
4371 // stored form, per PG's bpchar pattern operators.
4372 let (text, pat) = match (v, p) {
4373 (Value::Null, _) | (_, Value::Null) => return Ok(Value::Null),
4374 (Value::Text(a) | Value::BpChar(a), Value::Text(b) | Value::BpChar(b)) => (a, b),
4375 (Value::Text(_) | Value::BpChar(_), other) | (other, _) => {
4376 return Err(EvalError::TypeMismatch {
4377 detail: format!(
4378 "LIKE requires text operands, got {}",
4379 crate::conversions::pg_type_name_for_error_opt(other.data_type())
4380 ),
4381 });
4382 }
4383 };
4384 // v7.25 (round-17) — ILIKE folds both operands (PG
4385 // lowercases per the default collation).
4386 // v7.39 (round 364, M4 P2) — a MySQL session's default collation is
4387 // accent- and case-insensitive, so `LIKE` folds both sides the way
4388 // `ILIKE` does; the wildcards `%` / `_` are not Latin letters so the
4389 // fold leaves them alone.
4390 // v7.39 (round 370, M4 P4a) — an explicit `COLLATE utf8mb4_bin` column
4391 // matches byte-wise, so it does not fold. v7.39 (round 371, M4 P4b) —
4392 // a per-expression `… COLLATE utf8mb4_bin` / `BINARY …` on either the
4393 // value or the pattern forces byte-wise too.
4394 let mysql = ctx.mysql_dialect
4395 && !resolve::operand_is_binary_column(expr, ctx)
4396 && !resolve::operand_is_binary_column(pattern, ctx)
4397 && !resolve::is_binary_coerced(expr)
4398 && !resolve::is_binary_coerced(pattern);
4399 let m = if case_insensitive {
4400 like_match(&text.to_lowercase(), &pat.to_lowercase())?
4401 } else if mysql {
4402 like_match(
4403 &spg_storage::mysql_ci_fold(&text),
4404 &spg_storage::mysql_ci_fold(&pat),
4405 )?
4406 } else {
4407 like_match(&text, &pat)?
4408 };
4409 Ok(Value::Bool(if negated { !m } else { m }))
4410}
4411
4412/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
4413/// (stack-depth guard budget); body unchanged.
4414#[inline(never)]
4415fn eval_extract_arm(
4416 field: &spg_sql::ast::ExtractField,
4417 source: &Expr,
4418 row: &Row<'static>,
4419 ctx: &EvalContext<'_>,
4420) -> Result<Value<'static>, EvalError> {
4421 let v = eval_expr(source, row, ctx)?;
4422 extract_from_value(field, v, source, ctx)
4423}
4424
4425/// v7.39 (round 595) — the field extraction, with the source value already
4426/// in hand. Split out so the compiled-predicate program can pop the source
4427/// off its stack instead of handing the whole node back to the interpreter:
4428/// one non-compilable node used to disqualify the entire WHERE, and
4429/// `WHERE extract(year FROM t) = 2020` was interpreting the column read and
4430/// the comparison too. The body below is unchanged; it never touched `row`.
4431pub(crate) fn extract_from_value(
4432 field: &spg_sql::ast::ExtractField,
4433 v: Value<'static>,
4434 source: &Expr,
4435 ctx: &EvalContext<'_>,
4436) -> Result<Value<'static>, EvalError> {
4437 // v7.39 (round 382) — MySQL coerces a date/time STRING to its temporal
4438 // value for EXTRACT (`EXTRACT(YEAR FROM '2020-05-15')` is 2020, and the
4439 // time fields read a `'... HH:MM:SS'` string); PG needs a typed source.
4440 let v = match &v {
4441 Value::Text(s) if ctx.mysql_dialect => text_as_temporal(s).unwrap_or(v),
4442 _ => v,
4443 };
4444 // v7.39 (tz epic) — timezone[_hour|_minute] of a timestamptz
4445 // reports the SESSION offset at that instant (PG: 32400 for
4446 // Tokyo; -14400 for New York in July).
4447 if matches!(
4448 field,
4449 spg_sql::ast::ExtractField::Timezone
4450 | spg_sql::ast::ExtractField::TimezoneHour
4451 | spg_sql::ast::ExtractField::TimezoneMinute
4452 ) && let Value::Timestamp(t) = &v
4453 && crate::describe::describe_expr(source, ctx.columns)
4454 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz))
4455 {
4456 let off_secs = ctx.session_tz_offset_at(*t) / 1_000_000;
4457 let n = match field {
4458 spg_sql::ast::ExtractField::Timezone => off_secs,
4459 spg_sql::ast::ExtractField::TimezoneHour => off_secs / 3600,
4460 _ => (off_secs / 60) % 60,
4461 };
4462 // v7.39 (round 253) — numeric, like every other EXTRACT result.
4463 return Ok(Value::Numeric {
4464 scaled: i128::from(n),
4465 scale: 0,
4466 kind: spg_storage::NumericKind::Finite,
4467 });
4468 }
4469 // v7.39 (round 523) — and every OTHER field of a timestamptz reads
4470 // the local clock in the session zone, which is the whole reason PG
4471 // has the type. `extract(hour from …)` answered the UTC hour under
4472 // `SET TimeZone = 'Asia/Tokyo'` — 0 where PG says 9 — and
4473 // `extract(dow …)` therefore named the wrong DAY, so a report
4474 // grouped by weekday put nine hours of every Sunday under Saturday.
4475 // Only fields of the local clock shift; epoch and julian are
4476 // absolute, and the timezone fields answered above.
4477 let v = match &v {
4478 Value::Timestamp(t)
4479 if !matches!(
4480 field,
4481 spg_sql::ast::ExtractField::Epoch
4482 | spg_sql::ast::ExtractField::Julian
4483 | spg_sql::ast::ExtractField::Timezone
4484 | spg_sql::ast::ExtractField::TimezoneHour
4485 | spg_sql::ast::ExtractField::TimezoneMinute
4486 ) && crate::describe::describe_expr(source, ctx.columns)
4487 .is_some_and(|s| matches!(s.ty, spg_storage::DataType::Timestamptz)) =>
4488 {
4489 Value::Timestamp(t.saturating_add(ctx.session_tz_offset_at(*t)))
4490 }
4491 _ => v,
4492 };
4493 // v7.39 (round 253) — the source's PG type name for error wording,
4494 // upgraded from the declared type when statically known (a tstz
4495 // VALUE is indistinguishable from a timestamp). Only a cast /
4496 // column is trusted (the r237 lesson: describe_expr reports a
4497 // binary operator as its left operand's type).
4498 let static_declared = matches!(source, Expr::Cast { .. } | Expr::Column(_))
4499 .then(|| crate::describe::describe_expr(source, ctx.columns))
4500 .flatten()
4501 .map(|sch| sch.ty);
4502 let src_name = match static_declared {
4503 Some(spg_storage::DataType::Timestamptz) => "timestamp with time zone",
4504 _ => datetime::value_src_type_name(&v),
4505 };
4506 // PG rejects the timezone family on a plain timestamp (0A000);
4507 // only reject when the declared type is STATICALLY timestamp — a
4508 // dynamic value stays lenient (the pre-r253 zero answer).
4509 if matches!(
4510 field,
4511 spg_sql::ast::ExtractField::Timezone
4512 | spg_sql::ast::ExtractField::TimezoneHour
4513 | spg_sql::ast::ExtractField::TimezoneMinute
4514 ) && matches!(static_declared, Some(spg_storage::DataType::Timestamp))
4515 {
4516 return Err(EvalError::TypeMismatch {
4517 detail: alloc::format!(
4518 "unit \"{}\" not supported for type timestamp without time zone",
4519 alloc::format!("{field}").to_lowercase()
4520 ),
4521 });
4522 }
4523 // v7.39 (round 418) — MySQL's compound units (`DAY_SECOND`, `YEAR_MONTH`,
4524 // …) reach here as `ExtractField::Other`, which PG rejects. Under the
4525 // MySQL dialect they pack several components into one integer instead.
4526 if ctx.mysql_dialect
4527 && let spg_sql::ast::ExtractField::Other(name) = field
4528 && let Some(packed) = crate::eval::datetime::mysql_compound_extract(name, &v)
4529 {
4530 return Ok(packed);
4531 }
4532 extract_field(field, &v, src_name)
4533}
4534
4535/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
4536/// (stack-depth guard budget); body unchanged.
4537#[inline(never)]
4538fn eval_array_subscript_arm(
4539 expr: &Expr,
4540 row: &Row<'static>,
4541 ctx: &EvalContext<'_>,
4542) -> Result<Value<'static>, EvalError> {
4543 // Collect the whole subscript chain so PG's multi-dimensional
4544 // access (`arr[i][j]` is ONE N-subscript op) is distinguishable
4545 // from chained 1-D indexing. `arr[1][2]` parses as
4546 // `(arr[1])[2]`; PG indexes the matrix directly and returns NULL
4547 // for a partial subscript (`arr[1]` on a 2-D array is NULL).
4548 let mut idx_exprs: Vec<&Expr> = Vec::new();
4549 let mut base = expr;
4550 while let Expr::ArraySubscript { target, index } = base {
4551 idx_exprs.push(index);
4552 base = target;
4553 }
4554 idx_exprs.reverse();
4555 let base_v = eval_expr(base, row, ctx)?;
4556 if matches!(
4557 base_v,
4558 Value::IntArray2D(_)
4559 | Value::BigIntArray2D(_)
4560 | Value::TextArray2D(_)
4561 | Value::BoolArray2D(_)
4562 ) {
4563 return eval_matrix_subscript(&base_v, &idx_exprs, row, ctx);
4564 }
4565 // 1-D array / JSON: apply each subscript left-to-right. This
4566 // reproduces the prior single-subscript semantics exactly, and
4567 // chained JSON (`j['a']['b']`) still resolves step by step.
4568 let mut cur = base_v;
4569 for ix in idx_exprs {
4570 cur = apply_one_subscript(cur, ix, row, ctx)?;
4571 }
4572 Ok(cur)
4573}
4574
4575/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
4576/// (stack-depth guard budget); body unchanged.
4577#[inline(never)]
4578fn eval_field_access_arm(
4579 base: &Expr,
4580 field: &str,
4581 row: &Row<'static>,
4582 ctx: &EvalContext<'_>,
4583) -> Result<Value<'static>, EvalError> {
4584 // v7.38 (read01, T9) — composite field access `(expr).field`.
4585 // The base evaluates to a record; look the member up by name
4586 // (`f1`..`fN` for an anonymous ROW, base column names for a
4587 // whole-row). A NULL record yields NULL (PG semantics).
4588 let v = eval_expr(base, row, ctx)?;
4589 match v {
4590 Value::Null => Ok(Value::Null),
4591 Value::Composite(fields) => fields
4592 .into_iter()
4593 .find(|(name, _)| name == field)
4594 .map(|(_, val)| val)
4595 .ok_or_else(|| missing_field_error(base, field, ctx)),
4596 _ => Err(not_a_composite_error(base, field, ctx)),
4597 }
4598}
4599
4600/// v7.39 (round 285) — PG words a missing composite field three ways, and
4601/// which one you get depends on the base expression's STATIC type, not on
4602/// the value:
4603///
4604/// * a named composite — `column "nosuch" not found in data type rc9`
4605/// * a whole-row table reference — `column rt8.nosuch does not exist`
4606/// (unquoted, and qualified — the odd one out)
4607/// * an anonymous ROW or `::record` — `could not identify column
4608/// "nosuch" in record data type`
4609///
4610/// All three read off live PG 18.4. A `Value::Composite` carries its field
4611/// names but not its type name, so the base expression is what decides.
4612fn missing_field_error(base: &Expr, field: &str, ctx: &EvalContext<'_>) -> EvalError {
4613 // v7.39 (round 307, V25) — the named type may arrive by cast OR from
4614 // the schema of a column that a projection produced, so ask once and
4615 // let the catalog say whether it is a composite. Before this only
4616 // the cast spelling was recognised, which is why a composite that
4617 // came through a derived table or a CTE — where the base is a plain
4618 // column — fell through to the anonymous-record wording.
4619 if let Some(name) = base_named_type(base, ctx)
4620 && ctx
4621 .catalog
4622 .is_some_and(|c| c.composite_types().contains_key(name))
4623 {
4624 return EvalError::TypeMismatch {
4625 detail: alloc::format!("column \"{field}\" not found in data type {name}"),
4626 };
4627 }
4628 if let Expr::Column(c) = base
4629 && c.qualifier.is_none()
4630 {
4631 // A column DECLARED as a named composite reports that type — the
4632 // schema records it in `user_composite_type`, which is the only
4633 // place the name survives (a `Value::Composite` does not carry it).
4634 if let Some(name) = ctx
4635 .columns
4636 .iter()
4637 .find(|col| col.name.eq_ignore_ascii_case(&c.name))
4638 .and_then(|col| col.user_composite_type.as_ref())
4639 {
4640 return EvalError::TypeMismatch {
4641 detail: alloc::format!("column \"{field}\" not found in data type {name}"),
4642 };
4643 }
4644 // A whole-row reference to a real table is the odd wording out:
4645 // qualified, and unquoted.
4646 if ctx.catalog.is_some_and(|cat| cat.get(&c.name).is_some()) {
4647 return EvalError::TypeMismatch {
4648 detail: alloc::format!("column {}.{field} does not exist", c.name),
4649 };
4650 }
4651 }
4652 EvalError::TypeMismatch {
4653 detail: alloc::format!("could not identify column \"{field}\" in record data type"),
4654 }
4655}
4656
4657/// v7.39 (round 307, V25) — the user-declared type name behind a field
4658/// access, if any: either written as a cast (`ROW(…)::rc9`) or carried on
4659/// the column's schema.
4660///
4661/// All three schema slots are consulted rather than just the composite
4662/// one. A projection currently files the name of a `::rc9` cast under
4663/// `user_enum_type` — `expr_enum_type_name` answers for ANY named cast,
4664/// without asking whether the name is an enum — so keying off one slot
4665/// would answer for some shapes and not others. The caller decides what
4666/// the name MEANS by asking the catalog, which is the only thing that
4667/// actually knows; this function just finds it.
4668fn base_named_type<'c>(base: &'c Expr, ctx: &'c EvalContext<'_>) -> Option<&'c str> {
4669 match base {
4670 Expr::Cast {
4671 target: CastTarget::Named(name),
4672 ..
4673 } => Some(name.as_str()),
4674 Expr::Column(c) => ctx
4675 .columns
4676 .iter()
4677 .find(|sc| sc.name.eq_ignore_ascii_case(&c.name))
4678 .and_then(|sc| {
4679 sc.user_composite_type
4680 .as_deref()
4681 .or(sc.user_domain_type.as_deref())
4682 .or(sc.user_enum_type.as_deref())
4683 }),
4684 _ => None,
4685 }
4686}
4687
4688/// v7.39 (round 307, V25) — PG's wording when field notation is applied
4689/// to something that is not a composite at all. It names the type:
4690/// `column notation .f applied to type pos9, which is not a composite
4691/// type` — for a domain and an enum alike. Only when the base has no
4692/// user-declared type at all does the generic message stand.
4693fn not_a_composite_error(base: &Expr, field: &str, ctx: &EvalContext<'_>) -> EvalError {
4694 if let Some(name) = base_named_type(base, ctx)
4695 && ctx.catalog.is_some_and(|c| {
4696 c.enum_types().contains_key(name) || c.domain_types().contains_key(name)
4697 })
4698 {
4699 return EvalError::TypeMismatch {
4700 detail: alloc::format!(
4701 "column notation .{field} applied to type {name}, which is not a composite type"
4702 ),
4703 };
4704 }
4705 EvalError::TypeMismatch {
4706 detail: alloc::format!("field access `.{field}` requires a composite (record) value"),
4707 }
4708}
4709
4710/// Out-of-lined `eval_expr` arm — keeps the recursive frame small
4711/// (stack-depth guard budget); body unchanged.
4712#[inline(never)]
4713/// v7.39 (round 328, V45) — the three-valued boolean tests. None of them
4714/// ever answers NULL: a NULL input is "not true" and "not false", and IS
4715/// UNKNOWN is precisely the NULL case. Verified against PG 18.4 —
4716/// `NULL::bool IS TRUE` is false, `IS NOT TRUE` true, `IS UNKNOWN` true,
4717/// and `false IS NOT FALSE` false.
4718fn eval_bool_test_arm(
4719 expr: &Expr,
4720 value: Option<bool>,
4721 negated: bool,
4722 row: &Row<'static>,
4723 ctx: &EvalContext<'_>,
4724) -> Result<Value<'static>, EvalError> {
4725 let v = eval_expr(expr, row, ctx)?;
4726 let hit = match (value, &v) {
4727 // IS UNKNOWN — the input is NULL.
4728 (None, Value::Null) => true,
4729 // v7.39 (round 625) — and PG rejects a non-boolean here too:
4730 // `argument of IS UNKNOWN must be type boolean`. MySQL has no
4731 // IS UNKNOWN, so there is no dialect branch.
4732 (None, Value::Bool(_)) => false,
4733 (None, other) => {
4734 return Err(EvalError::TypeMismatch {
4735 detail: alloc::format!(
4736 "argument of IS {}UNKNOWN must be type boolean, not type {}",
4737 if negated { "NOT " } else { "" },
4738 crate::conversions::pg_type_name_for_error_opt(other.data_type())
4739 ),
4740 });
4741 }
4742 (Some(_), Value::Null) => false,
4743 (Some(want), Value::Bool(b)) => *b == want,
4744 // v7.39 (round 397) — MySQL reads a non-boolean as a truth value
4745 // for `IS TRUE` / `IS FALSE` (`5 IS TRUE` is 1, `0 IS FALSE` is 1,
4746 // `'abc' IS TRUE` is 0). PG rejects a non-boolean at parse time, so
4747 // this only fires under the dialect; a NULL is already handled.
4748 (Some(want), other) if ctx.mysql_dialect => mysql_truthy(other) == want,
4749 // v7.39 (round 625, S05b/F29) — on PG a non-boolean is REJECTED, and
4750 // the comment above said so while the arm below answered `false`
4751 // anyway. `1 IS TRUE` came back false, which reads as "the test was
4752 // run and did not hold" rather than "you cannot ask this of an
4753 // integer" — the wrong answer for every non-boolean type, eight of
4754 // them measured. PG's own sentence, which names the operator and the
4755 // type it got.
4756 (Some(_), other) => {
4757 return Err(EvalError::TypeMismatch {
4758 detail: alloc::format!(
4759 "argument of IS {}{} must be type boolean, not type {}",
4760 if negated { "NOT " } else { "" },
4761 if value == Some(true) { "TRUE" } else { "FALSE" },
4762 crate::conversions::pg_type_name_for_error_opt(other.data_type())
4763 ),
4764 });
4765 }
4766 };
4767 Ok(Value::Bool(hit != negated))
4768}
4769
4770fn eval_is_null_arm(
4771 expr: &Expr,
4772 negated: bool,
4773 row: &Row<'static>,
4774 ctx: &EvalContext<'_>,
4775) -> Result<Value<'static>, EvalError> {
4776 // v7.38 (read01 P4.11) — `ROW(...) IS [NOT] NULL` is evaluated
4777 // field-wise, not as a whole-value null test: a row IS NULL when
4778 // every field is null, and IS NOT NULL when every field is
4779 // non-null — so the two are NOT simple negations (ROW(1,NULL) is
4780 // neither). A field that is itself a row is a non-null value, so
4781 // the check does not recurse. The `(a, b) IS NULL` tuple spelling
4782 // is already desugared to `a IS NULL AND b IS NULL` in the parser;
4783 // this covers the explicit `ROW(...)` constructor.
4784 if let Expr::FunctionCall { name, args } = expr
4785 && name.eq_ignore_ascii_case("row")
4786 {
4787 let mut all_null = true;
4788 let mut all_non_null = true;
4789 for a in args {
4790 if matches!(eval_expr(a, row, ctx)?, Value::Null) {
4791 all_non_null = false;
4792 } else {
4793 all_null = false;
4794 }
4795 }
4796 return Ok(Value::Bool(if negated { all_non_null } else { all_null }));
4797 }
4798 // v7.39 (round 962) — the same field-wise rule for a row-valued
4799 // EXPRESSION, not just the `ROW(...)` spelling. P4.11 keyed on the
4800 // syntax, so every other way to hold a row got the whole-value test:
4801 // measured against PG18.4, `SELECT an IS NULL FROM an` on a row whose
4802 // every column is NULL answered `t` there and `f` here, and a column
4803 // declared with a composite type behaved the same way. Round 961 made
4804 // whole-row references reachable through a projection, which is what
4805 // surfaced it.
4806 //
4807 // Fields are tested exactly as the `ROW(...)` arm tests its
4808 // arguments, without recursing — a field that is itself a row is a
4809 // non-null value.
4810 let v = eval_expr(expr, row, ctx)?;
4811 if let Value::Composite(fields) = &v {
4812 let mut all_null = true;
4813 let mut all_non_null = true;
4814 for (_, f) in fields {
4815 if matches!(f, Value::Null) {
4816 all_non_null = false;
4817 } else {
4818 all_null = false;
4819 }
4820 }
4821 return Ok(Value::Bool(if negated { all_non_null } else { all_null }));
4822 }
4823 let is_null = matches!(v, Value::Null);
4824 Ok(Value::Bool(if negated { !is_null } else { is_null }))
4825}
4826
4827pub fn eval_expr(
4828 expr: &Expr,
4829 row: &Row<'static>,
4830 ctx: &EvalContext<'_>,
4831) -> Result<Value<'static>, EvalError> {
4832 // v7.38 (read01 P3.25) — guard against a native stack overflow on a
4833 // pathologically nested expression (`a AND a AND … ` × thousands): the
4834 // recursion base is seeded on the outermost call, and once a deeper
4835 // call has consumed more than the budget we return an error the way
4836 // PG's check_stack_depth() does, rather than aborting the process.
4837 let sp = eval_stack_ptr();
4838 let base = ctx.recursion_base.get();
4839 if base == 0 {
4840 ctx.recursion_base.set(sp);
4841 } else if base.saturating_sub(sp) > MAX_EVAL_STACK_BYTES {
4842 return Err(EvalError::StackDepthExceeded);
4843 }
4844 match expr {
4845 Expr::AggregateOrdered { .. } => Err(EvalError::TypeMismatch {
4846 detail: "aggregate ORDER BY is only valid inside an aggregating SELECT".into(),
4847 }),
4848 // A named argument is only meaningful inside a call, where the callee's
4849 // parameter names give it a slot. Anywhere else it is a syntax error,
4850 // and saying so beats silently evaluating it as if the name were absent.
4851 Expr::NamedArg { name, .. } => Err(EvalError::TypeMismatch {
4852 detail: alloc::format!("named argument \"{name}\" is only valid in a function call"),
4853 }),
4854 Expr::Literal(l) => Ok(literal_to_value(l)),
4855 Expr::Column(c) => resolve_column(c, row, ctx),
4856 Expr::Placeholder(n) => {
4857 let idx = usize::from(*n).saturating_sub(1);
4858 ctx.params
4859 .get(idx)
4860 .cloned()
4861 .ok_or_else(|| EvalError::PlaceholderOutOfRange {
4862 n: *n,
4863 bound: u16::try_from(ctx.params.len()).unwrap_or(u16::MAX),
4864 })
4865 }
4866 // v7.39 (round 620) — an unadorned string literal carries PG's
4867 // `unknown` type, and a boolean connective is a context that resolves
4868 // it TO boolean. `'true' AND true`, `'f' OR false` and `NOT 'a'` are
4869 // answered by PG (`t`, `f`, and the input-syntax error respectively)
4870 // and were all refused here with `argument of … must be type boolean,
4871 // not type text` — the message PG reserves for an operand that really
4872 // IS text (`''::TEXT AND true`), which stays refused. Out-of-line and
4873 // behind a literal-shaped guard: this is the recursive frame the
4874 // 768 KiB stack budget is tuned against.
4875 Expr::Unary {
4876 op: spg_sql::ast::UnOp::Not,
4877 expr,
4878 } if !ctx.mysql_dialect && is_unknown_string_literal(expr) => apply_unary(
4879 spg_sql::ast::UnOp::Not,
4880 coerce_unknown_literal_to_bool(expr)?,
4881 ),
4882 Expr::Unary { op, expr } => {
4883 let v = eval_expr(expr, row, ctx)?;
4884 // The MySQL-specific unary readings (NOT any truth value, `-`/`~`
4885 // on a string, `~` unsigned) live out-of-line: `eval_expr` is the
4886 // recursive frame the 768 KiB stack-depth budget is tuned
4887 // against, and locals added here cost one nesting level each (the
4888 // round-305 / round-383 frame cliff).
4889 if ctx.mysql_dialect {
4890 if let Some(r) = mysql_unary_arm(*op, &v) {
4891 return r;
4892 }
4893 }
4894 apply_unary(*op, v)
4895 }
4896 // v7.39 (round 346, M1) — MariaDB reads both sides of AND / OR as
4897 // truth values (`1 AND 2` is 1, measured). apply_binary has no
4898 // dialect, so the coercion happens here, where it does. The body
4899 // is out-of-line: `eval_expr` is the recursive frame the 768 KiB
4900 // stack-depth budget is tuned against, and locals added here cost
4901 // one nesting level each (the round-305 frame cliff).
4902 Expr::Binary { lhs, op, rhs }
4903 if ctx.mysql_dialect && matches!(op, BinOp::And | BinOp::Or | BinOp::LogicalXor) =>
4904 {
4905 eval_mysql_connective(lhs, *op, rhs, row, ctx)
4906 }
4907 // v7.39 (round 620/621) — the unknown-literal resolution and the
4908 // short circuit, both out of line. Placed AFTER the MySQL arm so the
4909 // dialect keeps its own reading of these connectives.
4910 Expr::Binary { lhs, op, rhs } if matches!(op, BinOp::And | BinOp::Or) => {
4911 eval_connective(lhs, *op, rhs, row, ctx)
4912 }
4913 Expr::Binary { lhs, op, rhs } => {
4914 // v7.32 (P4 borrow channel) — comparison fast path. A pure
4915 // comparison op only reads its operands and returns Bool,
4916 // and for non-NUMERIC / non-INTERVAL / non-CI-collation
4917 // operands `apply_binary` IS just the NULL-3VL check plus
4918 // the ref-based `compare` (NUMERIC routes through fixed-
4919 // point `apply_binary_numeric`; INTERVAL through
4920 // `apply_binary_interval`; CI columns fold). So read the
4921 // operands borrowed — a column cell is no longer cloned
4922 // just to compare it (`WHERE thread_id != ''` alone cloned
4923 // one Text cell per scanned row). Anything that needs the
4924 // owned path falls through unchanged.
4925 if matches!(
4926 op,
4927 BinOp::Eq | BinOp::NotEq | BinOp::Lt | BinOp::LtEq | BinOp::Gt | BinOp::GtEq
4928 ) {
4929 let lc = eval_expr_cow(lhs, row, ctx)?;
4930 let rc = eval_expr_cow(rhs, row, ctx)?;
4931 // v7.39 (enum order knife) — enum-typed operands compare by
4932 // member order, not label text. Cold unless both sides are
4933 // Text and the catalog has enum types at all.
4934 if matches!(lc.as_ref(), Value::Text(_)) && matches!(rc.as_ref(), Value::Text(_)) {
4935 if let Some(r) = enum_compare_hook(*op, lhs, rhs, lc.as_ref(), rc.as_ref(), ctx)
4936 {
4937 return r;
4938 }
4939 // v7.39 (round 693) — and the collation hook, under the
4940 // same Text/Text gate for the same reason.
4941 if let Some(r) =
4942 collate_compare_hook(*op, lhs, rhs, lc.as_ref(), rc.as_ref(), ctx)
4943 {
4944 return r;
4945 }
4946 }
4947 // v7.39 (round 351, M11) — the three conditions fold into
4948 // ONE call. Adding a fourth as another `||` here tipped the
4949 // 768 KiB stack guard on its own (measured — this is the
4950 // hottest recursive frame there is); one call is cheaper
4951 // than the three it replaces.
4952 let owned_path = needs_owned_compare(lc.as_ref(), rc.as_ref(), lhs, rhs, ctx);
4953 if !owned_path {
4954 if lc.as_ref().is_null() || rc.as_ref().is_null() {
4955 return Ok(Value::Null);
4956 }
4957 return compare(*op, lc.as_ref(), rc.as_ref()).map_err(|e| {
4958 unknown_literal_cmp_error(e, lhs, rhs, lc.as_ref(), rc.as_ref())
4959 });
4960 }
4961 let (l, r) = collation_fold_for_compare(
4962 *op,
4963 lhs,
4964 rhs,
4965 lc.into_owned(),
4966 rc.into_owned(),
4967 ctx,
4968 );
4969 // The owned call consumes the values; the rewrite needs the
4970 // literal's text and the other side's type only on the ERROR
4971 // path, so capture those two up front — the capture itself is
4972 // gated on the cheap expr test, so a comparison with no
4973 // unknown literal pays one branch.
4974 let probe = (is_unknown_string_literal(lhs) || is_unknown_string_literal(rhs))
4975 .then(|| (l.clone(), r.clone()));
4976 return apply_binary_in(*op, l, r, ctx.mysql_dialect).map_err(|e| match &probe {
4977 Some((pl, pr)) => unknown_literal_cmp_error(e, lhs, rhs, pl, pr),
4978 None => e,
4979 });
4980 }
4981 let l = eval_expr(lhs, row, ctx)?;
4982 let r = eval_expr(rhs, row, ctx)?;
4983 // v7.17.0 Phase 2.5 — collation-aware text comparison.
4984 // When either operand of a comparison op references a
4985 // column declared `COLLATE "case_insensitive"` (or any
4986 // MySQL `_ci` collation), case-fold both sides before
4987 // the byte-wise compare so `WHERE name = 'foo'` matches
4988 // stored `'Foo'`. Non-Text values fall straight through
4989 // — the helper is a no-op outside Text-Text equality
4990 // and inequality.
4991 let (l, r) = collation_fold_for_compare(*op, lhs, rhs, l, r, ctx);
4992 // v7.39 (GUC knife 4) — `date/interval/float || text` textifies
4993 // through the out-functions, which honour the session render
4994 // style. Pre-render the style-sensitive operand here (the
4995 // orthodox home is an implicit-cast node at type resolution;
4996 // until then this keeps apply_binary style-free). Default
4997 // style short-circuits — text_concat's own value_to_text
4998 // produces the identical bytes.
4999 if matches!(op, spg_sql::ast::BinOp::Concat)
5000 && ctx.render_style != format::RenderStyle::default()
5001 {
5002 let styled = |v: Value<'static>| -> Value<'static> {
5003 match &v {
5004 Value::Date(_)
5005 | Value::Timestamp(_)
5006 | Value::Interval { .. }
5007 | Value::Float(_)
5008 | Value::Real(_) => {
5009 Value::text(values::value_to_text_styled(&v, &ctx.render_style))
5010 }
5011 _ => v,
5012 }
5013 };
5014 let (sl, sr) = (styled(l), styled(r));
5015 return apply_binary(*op, sl, sr);
5016 }
5017 // v7.38.13 — in PG mode `apply_binary_mysql_unsigned` checks a
5018 // dialect flag and forwards, and `apply_binary_in` does the
5019 // same; both take two 48-byte `Value`s by value. Skip them.
5020 if ctx.mysql_dialect {
5021 apply_binary_mysql_unsigned(*op, lhs, rhs, l, r, ctx)
5022 } else {
5023 binop::apply_binary(*op, l, r)
5024 }
5025 }
5026 Expr::Cast { expr, target } => eval_cast_arm(expr, target, row, ctx),
5027 Expr::FieldAccess { base, field } => eval_field_access_arm(base, field, row, ctx),
5028 Expr::IsNull { expr, negated } => eval_is_null_arm(expr, *negated, row, ctx),
5029 // v7.39 (round 328, V45) — `x IS [NOT] TRUE | FALSE | UNKNOWN`.
5030 // Out-of-line like its IS NULL neighbour: an inline body here
5031 // grows every frame of the recursive evaluator, which is what
5032 // tipped the 512KB depth guard in round 305.
5033 Expr::BoolTest {
5034 expr,
5035 value,
5036 negated,
5037 } => eval_bool_test_arm(expr, *value, *negated, row, ctx),
5038 Expr::FunctionCall { name, args } => eval_function_call_arm(name, args, row, ctx),
5039 // v7.39 (read01 round 100) — VARIADIC is spliced into its enclosing
5040 // call before the args are evaluated (see eval_function_call_arm); a
5041 // bare one reaching here was written outside a function call.
5042 Expr::Variadic(_) => Err(EvalError::TypeMismatch {
5043 detail: "VARIADIC is only valid as a function-call argument".into(),
5044 }),
5045 Expr::Like {
5046 expr,
5047 pattern,
5048 negated,
5049 case_insensitive,
5050 } => eval_like_arm(expr, pattern, *negated, *case_insensitive, row, ctx),
5051 Expr::Extract { field, source } => eval_extract_arm(field, source, row, ctx),
5052 // v4.10: subquery nodes should have been resolved into
5053 // Literal / InList nodes by Engine::resolve_select_subqueries
5054 // before the row loop. Anything reaching here is a bug.
5055 Expr::ScalarSubquery(_)
5056 | Expr::Exists { .. }
5057 | Expr::InSubquery { .. }
5058 | Expr::RowInSubquery { .. }
5059 | Expr::RowCmpSubquery { .. } => Err(EvalError::TypeMismatch {
5060 detail: "subquery reached row eval — engine resolver bug".into(),
5061 }),
5062 // v7.30.2 (mailrs round-25) — flat `expr [NOT] IN (a, b, …)`.
5063 // Iterative scan with PG three-valued logic: TRUE on the first
5064 // Eq match; if nothing matched, NULL when the needle is NULL or
5065 // any comparison was NULL; FALSE otherwise. Empty list (only
5066 // reachable via an empty subquery result) is FALSE / TRUE even
5067 // for a NULL needle — no comparison ever happens.
5068 Expr::InList {
5069 expr,
5070 list,
5071 negated,
5072 } => eval_in_list_arm(expr, list, *negated, row, ctx),
5073 // v4.12: window functions should have been rewritten into
5074 // synthetic __win_N column references by
5075 // exec_select_with_window before row eval. Anything
5076 // reaching here is similarly a bug.
5077 Expr::WindowFunction { .. } => Err(EvalError::TypeMismatch {
5078 detail: "window function reached row eval — engine rewrite bug".into(),
5079 }),
5080 // v7.10.10 — `ARRAY[expr, expr, …]` constructor.
5081 // v7.11.13 — element-type detection: all integers →
5082 // IntArray (or BigIntArray when widening), any Text →
5083 // TextArray. Non-TEXT non-integer elements (Bool, Float)
5084 // stringify into TextArray as the safe default.
5085 Expr::Array(items) => eval_array_arm(items, row, ctx),
5086 // v7.10.12 — `arr[i]` PG-style 1-based indexing.
5087 // Out-of-range indices (including i ≤ 0) return NULL.
5088 Expr::ArraySubscript { .. } => eval_array_subscript_arm(expr, row, ctx),
5089 // Array slice `arr[lo:hi]` — PG 1-based, both ends
5090 // inclusive, out-of-range bounds clamp, missing bounds
5091 // extend to the array's ends. Result keeps the element
5092 // type; an empty window yields an empty array.
5093 Expr::ArraySlice { target, lo, hi } => eval_array_slice_arm(target, lo, hi, row, ctx),
5094 // v7.10.12 — `x op ANY(arr)` / `x op ALL(arr)`. PG
5095 // 3VL: ANY → true if any element compares-true; NULL if
5096 // no true but some NULL; false otherwise. ALL: false if
5097 // any compares-false; NULL if no false but some NULL;
5098 // true otherwise.
5099 Expr::AnyAll {
5100 expr,
5101 op,
5102 array,
5103 is_any,
5104 } => eval_any_all_arm(expr, op, array, *is_any, row, ctx),
5105 // v7.13.0 — CASE WHEN … END (mailrs round-5 G9).
5106 // Short-circuit on the first matching branch. Searched form
5107 // (operand=None) treats each branch's WHEN as a Bool
5108 // predicate. Simple form (operand=Some) compares with =.
5109 // ELSE on no match; NULL if no ELSE.
5110 Expr::Case {
5111 operand,
5112 branches,
5113 else_branch,
5114 } => eval_case_arm(operand, branches, else_branch, row, ctx),
5115 }
5116}
5117
5118/// v7.10.10 — best-effort text rendering for non-TEXT array
5119/// elements (numbers, bools, etc.). The PG rule is that
5120/// `ARRAY[1, 2]` is `int[]`, but SPG's v7.10 only models TEXT[],
5121/// so we widen by stringifying. NUMERIC formatting goes through
5122/// the existing canonical helpers to stay consistent with
5123/// `format_numeric` / `format_date` etc.
5124/// v7.37 D.1 — the COALESCE result-type hint: a sibling branch's explicit
5125/// cast target (`NULL::time`, `col::time`), unless it is Text (Text carries no
5126/// coercion). Returns the first non-Text `CastTarget` found, mirroring PG's
5127/// left-to-right common-type resolution for the common single-typed-branch case.
5128fn coalesce_type_hint(e: &Expr) -> Option<CastTarget> {
5129 match e {
5130 Expr::Cast { target, .. } if !matches!(target, CastTarget::Text) => Some(target.clone()),
5131 _ => None,
5132 }
5133}
5134
5135/// v7.38 (read01) — widen `v` to the already-resolved common type `common`
5136/// of a `CASE`/`COALESCE`/`GREATEST`/`LEAST`/`NULLIF` result, so the value's
5137/// type matches the one PG reports and downstream operators (e.g. `/`) see
5138/// the widened type (integer division vs numeric division). Only widens;
5139/// anything already at the common type, or that fails to coerce, is returned
5140/// untouched (this must never turn a working expression into an error).
5141/// NUMERIC is scale-preserving: an existing exact-numeric keeps its own scale
5142/// (PG renders `COALESCE(1.50, 2)` as `1.50`); only integers promote, to
5143/// scale 0.
5144pub(crate) fn widen_value_to(v: Value<'static>, common: spg_storage::DataType) -> Value<'static> {
5145 use spg_storage::DataType as DT;
5146 // Only widen numeric- and temporal-category results: these are the ones
5147 // whose type actually changes a downstream value (integer vs numeric
5148 // division, date vs timestamp). Widening a string result (varchar ∪ text)
5149 // would only relabel the type while risking a spurious length-limit error
5150 // when coercing into a modelled-length varchar/char, so leave it as-is.
5151 if !matches!(
5152 common,
5153 DT::SmallInt
5154 | DT::Int
5155 | DT::BigInt
5156 | DT::Numeric { .. }
5157 // v7.39 (round 649) — `real` was absent here too, so even once
5158 // `common_type` learned to rank it, the value was handed back
5159 // unwidened: `coalesce(1::int, 1::real)` stayed integer where
5160 // PG says real. Two lists, one ladder — the gap had to be
5161 // closed in both.
5162 | DT::Real
5163 | DT::Float
5164 | DT::Date
5165 | DT::Time
5166 | DT::Timestamp
5167 | DT::Timestamptz
5168 ) {
5169 return v;
5170 }
5171 if matches!(v, Value::Null) {
5172 return v;
5173 }
5174 if v.data_type() == Some(common) {
5175 return v;
5176 }
5177 if matches!(common, spg_storage::DataType::Numeric { .. })
5178 && matches!(v, Value::Numeric { .. } | Value::NumericBig(_))
5179 {
5180 return v;
5181 }
5182 let target = if matches!(common, spg_storage::DataType::Numeric { .. }) {
5183 spg_storage::DataType::Numeric {
5184 precision: 0,
5185 scale: 0,
5186 }
5187 } else {
5188 common
5189 };
5190 match crate::conversions::coerce_value(v.clone(), target, "", 0) {
5191 Ok(cv) => cv,
5192 Err(_) => v,
5193 }
5194}
5195
5196/// Widen `v` to the PG common type of the sibling `types`, or leave it as-is
5197/// when the types don't resolve to a single widening type. See
5198/// [`widen_value_to`] and [`crate::describe::common_type`].
5199pub(crate) fn widen_to_common(
5200 v: Value<'static>,
5201 types: &[spg_storage::DataType],
5202) -> Value<'static> {
5203 match crate::describe::common_type(types) {
5204 Some(common) => widen_value_to(v, common),
5205 None => v,
5206 }
5207}
5208
5209pub(crate) fn value_to_text_for_array(v: &Value, style: &format::RenderStyle) -> String {
5210 match v {
5211 Value::Text(s) | Value::Json(s) => s.to_string(),
5212 Value::Int(n) => n.to_string(),
5213 Value::BigInt(n) => n.to_string(),
5214 Value::SmallInt(n) => n.to_string(),
5215 // PG renders booleans in array external form as `t` / `f`
5216 // (the bool type's output function), not `true` / `false`.
5217 Value::Bool(b) => {
5218 if *b {
5219 "t".into()
5220 } else {
5221 "f".into()
5222 }
5223 }
5224 Value::Float(x) => format::format_float_styled(*x, style),
5225 Value::Real(x) => format::format_real_styled(*x, style),
5226 Value::Date(d) => format::format_date_styled(*d, style),
5227 Value::Timestamp(t) => format::format_timestamp_styled(*t, style),
5228 Value::Numeric {
5229 scaled,
5230 scale,
5231 kind,
5232 } => format_numeric_kind(*kind, *scaled, *scale),
5233 // v7.39 — everything else renders its canonical PG text (this
5234 // Debug fallback is how `ARRAY['\xff'::bytea]` printed
5235 // `{Bytes([255])}` on the wire).
5236 _ => values::value_to_text_styled(v, style),
5237 }
5238}
5239
5240/// SQL `LIKE` matcher. Wildcards are `%` (any run, possibly empty) and `_`
5241/// (exactly one char). `\` escapes the next pattern char so `\%` matches a
5242/// literal `%`. Matches the whole input — no implicit anchoring needed
5243/// since SQL `LIKE` is always full-string. Errs on a trailing unpaired
5244/// escape the matcher actually reaches with text left (PG's lazy 22025).
5245fn like_match(text: &str, pattern: &str) -> Result<bool, EvalError> {
5246 let pat: Vec<char> = pattern.chars().collect();
5247 like_match_str(text, &pat, 0)
5248}
5249
5250/// v7.37.16 — pg_typeof spelling for a STATIC column type (the
5251/// NULL-cell fallback; the value-level table is `pg_typeof_name`).
5252/// TEXT maps to None because a NULL literal describes as TEXT — the
5253/// unknown stand-in — and pg_typeof(NULL) must stay "unknown"; every
5254/// type not listed also returns None (caller keeps the value answer).
5255pub(crate) fn pg_typeof_name_for_datatype(t: spg_storage::DataType) -> Option<&'static str> {
5256 use spg_storage::DataType as D;
5257 Some(match t {
5258 D::SmallInt => "smallint",
5259 D::Int => "integer",
5260 D::BigInt => "bigint",
5261 D::Float => "double precision",
5262 D::Real => "real",
5263 D::Numeric { .. } => "numeric",
5264 D::Bool => "boolean",
5265 D::Date => "date",
5266 D::Time => "time without time zone",
5267 D::Timestamp => "timestamp without time zone",
5268 D::Timestamptz => "timestamp with time zone",
5269 D::Name => "name",
5270 D::Xid => "xid",
5271 D::Xid8 => "xid8",
5272 D::Oid => "oid",
5273 // v7.39 (round 694) — and its array, for the same reason.
5274 D::OidArray => "oid[]",
5275 D::Uuid => "uuid",
5276 D::Interval => "interval",
5277 // v7.39 (round 871) — the rest of what a NULL cast can be
5278 // annotated with. This table decided which types survived
5279 // `pg_typeof(NULL::t)`: the twenty above answered, everything
5280 // else fell to `_ => None` and reported `unknown`. That reads
5281 // as a NULL problem and is not one — `NULL::uuid` was right all
5282 // along while `NULL::text` was wrong, because one was listed
5283 // and the other was not.
5284 //
5285 // Names are PG18's own, taken from running `pg_typeof` there
5286 // rather than from memory: `bit varying` not `varbit`, `bit`
5287 // for any width, `character varying`, `"char"` quoted.
5288 D::Text => "text",
5289 D::Multirange(k) => match k {
5290 spg_storage::RangeKind::Int4 => "int4multirange",
5291 spg_storage::RangeKind::Int8 => "int8multirange",
5292 spg_storage::RangeKind::Num => "nummultirange",
5293 spg_storage::RangeKind::Ts => "tsmultirange",
5294 spg_storage::RangeKind::TsTz => "tstzmultirange",
5295 spg_storage::RangeKind::Date => "datemultirange",
5296 },
5297 D::Varchar(_) => "character varying",
5298 // PG names `char(n)` "character"; the one-byte internal type
5299 // spelled `"char"` is a DIFFERENT type there, and SPG maps both
5300 // onto `Char(u32)` — so this arm must answer for the declared
5301 // one. Round 871's first attempt said `"char"` here and would
5302 // have reported `char(5)` as the internal type.
5303 D::Char(_) => "character",
5304 D::Json => "json",
5305 D::Jsonb => "jsonb",
5306 D::Bytes => "bytea",
5307 D::Inet => "inet",
5308 D::Cidr => "cidr",
5309 D::Macaddr => "macaddr",
5310 D::Macaddr8 => "macaddr8",
5311 D::Bit(_) => "bit",
5312 D::BitVarying(_) => "bit varying",
5313 D::Xml => "xml",
5314 D::Money => "money",
5315 D::Point => "point",
5316 D::Lseg => "lseg",
5317 D::Path => "path",
5318 D::PgBox => "box",
5319 D::Polygon => "polygon",
5320 D::Line => "line",
5321 D::Circle => "circle",
5322 D::TextArray => "text[]",
5323 D::IntArray => "integer[]",
5324 D::BigIntArray => "bigint[]",
5325 D::SmallIntArray => "smallint[]",
5326 D::FloatArray => "double precision[]",
5327 D::NumericArray => "numeric[]",
5328 D::BoolArray => "boolean[]",
5329 D::DateArray => "date[]",
5330 D::TimestampArray => "timestamp without time zone[]",
5331 D::TimestamptzArray => "timestamp with time zone[]",
5332 D::UuidArray => "uuid[]",
5333 D::JsonArray => "json[]",
5334 D::JsonbArray => "jsonb[]",
5335 D::BytesArray => "bytea[]",
5336 D::VarcharArray => "character varying[]",
5337 D::CharArray => "\"char\"[]",
5338 D::IntervalArray => "interval[]",
5339 _ => return None,
5340 })
5341}
5342
5343/// v7.37.16 — zero-allocation LIKE core: the text side walks a `&str`
5344/// cursor (char-semantic — `_` consumes one CHARACTER, `%` backtracks
5345/// only at char boundaries) instead of collecting a `Vec<char>` per
5346/// call. The old per-row collect was ~50 ns/row of pure allocator
5347/// traffic on a 50 k-row `WHERE s LIKE '%…%'` scan (the heavy.rs
5348/// like_filter 2.8× loss); the pattern side stays a compile-once
5349/// `&[char]` (see `Step::Like`).
5350pub(crate) fn like_match_str(text: &str, pat: &[char], mut pi: usize) -> Result<bool, EvalError> {
5351 let mut t = text;
5352 while pi < pat.len() {
5353 match pat[pi] {
5354 '%' => {
5355 // Collapse consecutive `%` and try every possible split.
5356 while pi < pat.len() && pat[pi] == '%' {
5357 pi += 1;
5358 }
5359 if pi == pat.len() {
5360 return Ok(true);
5361 }
5362 let mut rest = t;
5363 loop {
5364 if like_match_str(rest, pat, pi)? {
5365 return Ok(true);
5366 }
5367 match rest.chars().next() {
5368 Some(c) => rest = &rest[c.len_utf8()..],
5369 None => return Ok(false),
5370 }
5371 }
5372 }
5373 '_' => match t.chars().next() {
5374 Some(c) => {
5375 t = &t[c.len_utf8()..];
5376 pi += 1;
5377 }
5378 None => return Ok(false),
5379 },
5380 // v7.39 (round 144, like_match.c) — a trailing unpaired escape is
5381 // PG's 22025 error, but LAZILY: only when the matcher reaches it
5382 // with text left. A branch where the text is already exhausted
5383 // returns false without ever "seeing" the trailing escape
5384 // ('x' LIKE 'x\' is false; 'xy' LIKE 'x\' errors).
5385 '\\' if pi + 1 >= pat.len() => {
5386 if t.is_empty() {
5387 return Ok(false);
5388 }
5389 return Err(EvalError::TypeMismatch {
5390 detail: "LIKE pattern must not end with escape character".into(),
5391 });
5392 }
5393 '\\' => {
5394 let want = pat[pi + 1];
5395 match t.chars().next() {
5396 Some(c) if c == want => {
5397 t = &t[c.len_utf8()..];
5398 pi += 2;
5399 }
5400 _ => return Ok(false),
5401 }
5402 }
5403 c => match t.chars().next() {
5404 Some(tc) if tc == c => {
5405 t = &t[c.len_utf8()..];
5406 pi += 1;
5407 }
5408 _ => return Ok(false),
5409 },
5410 }
5411 }
5412 Ok(t.is_empty())
5413}
5414
5415/// v7.24 (round-15) — `string_to_array(text, delimiter)`.
5416fn fn_string_to_array(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
5417 // v7.37.17 (17.6 siblings) — the 3-arg PG form adds
5418 // `null_string`: elements equal to it become SQL NULL.
5419 let (text_arg, delim_arg, null_arg) = match args {
5420 [t, d] => (t, d, None),
5421 [t, d, n] => (t, d, Some(n)),
5422 _ => {
5423 return Err(EvalError::TypeMismatch {
5424 detail: alloc::format!(
5425 "string_to_array expects 2 or 3 arguments, got {}",
5426 args.len()
5427 ),
5428 });
5429 }
5430 };
5431 let null_string: Option<&str> = match null_arg {
5432 None | Some(Value::Null) => None,
5433 Some(Value::Text(s)) => Some(s.as_ref()),
5434 Some(other) => {
5435 return Err(EvalError::TypeMismatch {
5436 detail: alloc::format!(
5437 "string_to_array null_string must be text, got {}",
5438 crate::conversions::pg_type_name_for_error_opt(other.data_type())
5439 ),
5440 });
5441 }
5442 };
5443 let text = match text_arg {
5444 Value::Null => return Ok(Value::Null),
5445 Value::Text(t) => t,
5446 other => {
5447 return Err(EvalError::TypeMismatch {
5448 detail: alloc::format!(
5449 "string_to_array expects text, got {}",
5450 crate::conversions::pg_type_name_for_error_opt(other.data_type())
5451 ),
5452 });
5453 }
5454 };
5455 // PG (9.1+): empty input → empty array, regardless of delimiter.
5456 if text.is_empty() {
5457 return Ok(Value::TextArray(Vec::new()));
5458 }
5459 let nullify = |p: String| -> Option<String> {
5460 if null_string == Some(p.as_str()) {
5461 None
5462 } else {
5463 Some(p)
5464 }
5465 };
5466 let parts: Vec<Option<String>> = match delim_arg {
5467 // NULL delimiter → one element per character.
5468 Value::Null => text.chars().map(|c| nullify(c.to_string())).collect(),
5469 Value::Text(d) if d.is_empty() => alloc::vec![nullify(text.to_string())],
5470 Value::Text(d) => text
5471 .split(d.as_ref())
5472 .map(|p| nullify(p.to_string()))
5473 .collect(),
5474 other => {
5475 return Err(EvalError::TypeMismatch {
5476 detail: alloc::format!(
5477 "string_to_array delimiter must be text, got {}",
5478 crate::conversions::pg_type_name_for_error_opt(other.data_type())
5479 ),
5480 });
5481 }
5482 };
5483 Ok(Value::TextArray(parts))
5484}
5485
5486/// v6.4.3 — `error_on_null(v)`. Returns `v` unchanged if non-NULL;
5487/// errors otherwise. Convenience to assert NOT NULL inside an
5488/// expression without wrapping it in COALESCE + raise hacks.
5489fn error_on_null(args: &[Value<'_>]) -> Result<Value<'static>, EvalError> {
5490 if args.len() != 1 {
5491 return Err(EvalError::TypeMismatch {
5492 detail: format!("error_on_null() takes 1 arg, got {}", args.len()),
5493 });
5494 }
5495 if matches!(args[0], Value::Null) {
5496 return Err(EvalError::TypeMismatch {
5497 detail: "error_on_null(): argument is NULL".into(),
5498 });
5499 }
5500 Ok(args[0].clone().into_owned())
5501}
5502
5503/// Helper: coerce a Value to an Option<String> for regex args. NULL
5504/// propagates as None (caller short-circuits to Value::Null).
5505fn text_arg(v: &Value) -> Result<Option<String>, EvalError> {
5506 match v {
5507 Value::Text(s) => Ok(Some(s.to_string())),
5508 Value::Null => Ok(None),
5509 other => Err(EvalError::TypeMismatch {
5510 detail: alloc::format!(
5511 "regex function expects TEXT arg, got {}",
5512 crate::conversions::pg_type_name_for_error_opt(other.data_type())
5513 ),
5514 }),
5515 }
5516}
5517
5518// Month-name tables shared by the date formatters in `eval::strings`
5519// (`date_format_mysql`) and `eval::datetime` via `use super::`. Kept in
5520// `eval.rs` alongside `civil_from_days` so the calendar primitives live
5521// in one place.
5522const MONTH_FULL: [&str; 12] = [
5523 "January",
5524 "February",
5525 "March",
5526 "April",
5527 "May",
5528 "June",
5529 "July",
5530 "August",
5531 "September",
5532 "October",
5533 "November",
5534 "December",
5535];
5536const MONTH_ABBR: [&str; 12] = [
5537 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
5538];
5539
5540/// Howard Hinnant's `civil_from_days` — converts days since the Unix
5541/// epoch back to a proleptic-Gregorian (year, month, day) triple. Stays
5542/// in `eval.rs` (shared with the date SQL functions here and with
5543/// `eval::strings`); the inverse `days_from_civil` lives in
5544/// `eval::format`. Both keep the engine off `std` time facilities.
5545#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
5546fn civil_from_days(days: i32) -> (i32, u32, u32) {
5547 let z = i64::from(days) + 719_468;
5548 let era = z.div_euclid(146_097);
5549 // doe ∈ [0, 146_097); fits in u32 with room to spare. Same for
5550 // every other quantity below — `as u32` truncations are safe by
5551 // construction.
5552 let doe = (z - era * 146_097) as u32;
5553 let yoe = (doe.saturating_sub(doe / 1460) + doe / 36524 - doe / 146_096) / 365;
5554 let y_base = i64::from(yoe) + era * 400;
5555 let doy = doe.saturating_sub(365 * yoe + yoe / 4 - yoe / 100);
5556 let mp = (5 * doy + 2) / 153;
5557 let d = doy.saturating_sub((153 * mp + 2) / 5) + 1;
5558 let m = if mp < 10 { mp + 3 } else { mp - 9 };
5559 let y = if m <= 2 { y_base + 1 } else { y_base };
5560 (y as i32, m, d)
5561}
5562
5563/// Add `months` (signed) to a `(year, month, day)` triple using PG's
5564/// clamp-to-last-day rule (so `'2024-01-31' + 1 month` → `'2024-02-29'`).
5565fn add_months_to_civil(y: i32, m: u32, d: u32, months: i32) -> (i32, u32, u32) {
5566 let total_months = i64::from(y) * 12 + i64::from(m) - 1 + i64::from(months);
5567 let new_year = i32::try_from(total_months.div_euclid(12)).unwrap_or(i32::MAX);
5568 let new_month_zero = total_months.rem_euclid(12);
5569 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
5570 let new_month = (new_month_zero as u32) + 1;
5571 let max_day = days_in_month(new_year, new_month);
5572 (new_year, new_month, d.min(max_day))
5573}
5574
5575const fn days_in_month(y: i32, m: u32) -> u32 {
5576 match m {
5577 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
5578 2 => {
5579 // Proleptic Gregorian leap rule.
5580 if y.rem_euclid(4) == 0 && (y.rem_euclid(100) != 0 || y.rem_euclid(400) == 0) {
5581 29
5582 } else {
5583 28
5584 }
5585 }
5586 // 4 / 6 / 9 / 11 plus any out-of-range month (callers normalise
5587 // first, but be defensive) get the 30-day fallback.
5588 _ => 30,
5589 }
5590}
5591
5592pub(crate) fn literal_to_value(l: &Literal) -> Value<'static> {
5593 match l {
5594 Literal::Integer(n) => {
5595 if let Ok(small) = i32::try_from(*n) {
5596 Value::Int(small)
5597 } else {
5598 Value::BigInt(*n)
5599 }
5600 }
5601 Literal::Float(x) => Value::Float(*x),
5602 Literal::Numeric { unscaled, scale } => Value::Numeric {
5603 scaled: *unscaled,
5604 scale: *scale,
5605 kind: spg_storage::NumericKind::Finite,
5606 },
5607 Literal::NumericBig(s) => crate::conversions::big_literal_to_value(s),
5608 // v7.38.8 — already decoded, so the row loop neither clones a
5609 // string nor coerces one back into a timestamp.
5610 Literal::Timestamp { micros, .. } => Value::Timestamp(*micros),
5611 Literal::Date { days, .. } => Value::Date(*days),
5612 Literal::String(s) => Value::text(s.clone()),
5613 Literal::Vector(v) => Value::vector(v.clone()),
5614 Literal::TextArray(items) => Value::TextArray(items.clone()),
5615 Literal::IntArray(items) => Value::IntArray(items.clone()),
5616 Literal::BigIntArray(items) => Value::BigIntArray(items.clone()),
5617 Literal::Bool(b) => Value::Bool(*b),
5618 Literal::Null => Value::Null,
5619 Literal::Interval {
5620 months,
5621 days,
5622 micros,
5623 ..
5624 } => Value::Interval {
5625 months: *months,
5626 days: *days,
5627 micros: *micros,
5628 },
5629 }
5630}
5631
5632impl crate::Engine {
5633 /// v7.39 (read01 round 63) — run a user function whose body has its own
5634 /// FROM. The arguments are substituted into the body as literals and the
5635 /// SELECT goes through the REAL executor, so it sees exactly the rows a
5636 /// hand-written query would: the row-header visibility filter applies, and
5637 /// under in-place MVCC a dead row stays dead.
5638 ///
5639 /// PG returns the FIRST row of a scalar SQL function's body (and NULL when
5640 /// it returns none).
5641 pub(crate) fn run_user_fn_query(
5642 &self,
5643 def: &spg_storage::FunctionDef,
5644 stmt: &spg_sql::ast::SelectStatement,
5645 arg_names: &[alloc::string::String],
5646 args: &spg_storage::Row<'static>,
5647 fn_depth: u16,
5648 ) -> Result<Value<'static>, EvalError> {
5649 const MAX_QUERY_FN_DEPTH: u16 = 8;
5650 if fn_depth >= MAX_QUERY_FN_DEPTH {
5651 return Err(EvalError::TypeMismatch {
5652 detail: alloc::format!(
5653 "function {:?}: a body with its own FROM may nest at most {MAX_QUERY_FN_DEPTH} deep",
5654 def.name
5655 ),
5656 });
5657 }
5658 // Bind the arguments — the same helper the set-returning path uses, so
5659 // both resolve an argument identically (a COLUMN of the body's own FROM
5660 // shadows a same-named argument, as in PG).
5661 let owned: alloc::vec::Vec<Value<'static>> =
5662 args.values.iter().map(|v| v.clone().into_owned()).collect();
5663 let bound =
5664 bind_user_fn_args(self.active_catalog(), stmt, arg_names, &owned).map_err(|e| {
5665 EvalError::TypeMismatch {
5666 detail: alloc::format!("function {:?}: {e}", def.name),
5667 }
5668 })?;
5669
5670 // v7.39 (round 334, V55) — a SECURITY DEFINER body is authorised as
5671 // the function's OWNER. Measured on PG 18.4: a definer function
5672 // owned by `owner55` counts rows of a table `caller55` cannot read,
5673 // while the SECURITY INVOKER sibling is refused.
5674 let as_role = def.security_definer.then(|| def.owner.as_deref()).flatten();
5675 let out = self
5676 .exec_select_cancel_as(&bound, crate::CancelToken::none(), as_role)
5677 .map_err(|e| EvalError::TypeMismatch {
5678 detail: alloc::format!("function {:?}: {e}", def.name),
5679 })?;
5680 let crate::QueryResult::Rows { rows, .. } = out else {
5681 return Ok(Value::Null);
5682 };
5683 let Some(first) = rows.first() else {
5684 // No row: PG's scalar SQL function returns NULL.
5685 return Ok(Value::Null);
5686 };
5687 let v = first.values.first().cloned().unwrap_or(Value::Null);
5688 let declared = def.returns.trim();
5689 if declared.eq_ignore_ascii_case("VOID") {
5690 return Ok(Value::Null);
5691 }
5692 crate::eval::cast::cast_value(v.into_owned(), declared_return_cast_target(declared))
5693 .or_else(|_| Ok(Value::Null))
5694 }
5695}
5696
5697/// v7.39 (read01 round 65) — bind a call's arguments into a function body's
5698/// SELECT, as literals. Shared by the scalar path (round 63) and the
5699/// set-returning one, so both resolve an argument the same way — including the
5700/// rule that a COLUMN of the body's own FROM shadows a same-named argument.
5701pub(crate) fn bind_user_fn_args(
5702 cat: &spg_storage::Catalog,
5703 stmt: &spg_sql::ast::SelectStatement,
5704 arg_names: &[alloc::string::String],
5705 args: &[Value<'static>],
5706) -> Result<spg_sql::ast::SelectStatement, EvalError> {
5707 let mut bound = stmt.clone();
5708 let mut binds: alloc::collections::BTreeMap<alloc::string::String, spg_sql::ast::Expr> =
5709 alloc::collections::BTreeMap::new();
5710 for (i, name) in arg_names.iter().enumerate() {
5711 if name.is_empty() {
5712 continue;
5713 }
5714 let shadowed = body_from_tables(stmt).iter().any(|t| {
5715 cat.get(t).is_some_and(|tb| {
5716 tb.schema()
5717 .columns
5718 .iter()
5719 .any(|c| c.name.eq_ignore_ascii_case(name))
5720 })
5721 });
5722 if shadowed {
5723 continue;
5724 }
5725 let v = args.get(i).cloned().unwrap_or(Value::Null);
5726 let lit =
5727 crate::substitute::value_to_literal_expr(v).map_err(|e| EvalError::TypeMismatch {
5728 detail: alloc::format!("argument {name} cannot be bound into the body: {e}"),
5729 })?;
5730 binds.insert(name.to_ascii_lowercase(), lit);
5731 }
5732 substitute_arg_refs_in_select(&mut bound, &binds);
5733 Ok(bound)
5734}
5735
5736/// The base tables a function body's FROM names — used to decide whether an
5737/// argument name is shadowed by a column of the same name.
5738fn body_from_tables(
5739 stmt: &spg_sql::ast::SelectStatement,
5740) -> alloc::vec::Vec<alloc::string::String> {
5741 let mut out = alloc::vec::Vec::new();
5742 if let Some(from) = &stmt.from {
5743 out.push(from.primary.name.clone());
5744 for j in &from.joins {
5745 out.push(j.table.name.clone());
5746 }
5747 }
5748 out
5749}
5750
5751/// Replace every bare reference to an argument name with its literal value.
5752fn substitute_arg_refs_in_select(
5753 stmt: &mut spg_sql::ast::SelectStatement,
5754 binds: &alloc::collections::BTreeMap<alloc::string::String, spg_sql::ast::Expr>,
5755) {
5756 use spg_sql::ast::{Expr, SelectItem};
5757 fn walk(e: &mut Expr, binds: &alloc::collections::BTreeMap<alloc::string::String, Expr>) {
5758 match e {
5759 Expr::Column(c) => {
5760 if c.qualifier.is_none()
5761 && let Some(lit) = binds.get(&c.name.to_ascii_lowercase())
5762 {
5763 *e = lit.clone();
5764 }
5765 }
5766 Expr::Binary { lhs, rhs, .. } => {
5767 walk(lhs, binds);
5768 walk(rhs, binds);
5769 }
5770 Expr::Unary { expr, .. } | Expr::Cast { expr, .. } => walk(expr, binds),
5771 Expr::FunctionCall { args, .. } => args.iter_mut().for_each(|a| walk(a, binds)),
5772 Expr::Case {
5773 operand,
5774 branches,
5775 else_branch,
5776 } => {
5777 if let Some(o) = operand {
5778 walk(o, binds);
5779 }
5780 for (c, v) in branches.iter_mut() {
5781 walk(c, binds);
5782 walk(v, binds);
5783 }
5784 if let Some(x) = else_branch {
5785 walk(x, binds);
5786 }
5787 }
5788 Expr::InList { expr, list, .. } => {
5789 walk(expr, binds);
5790 list.iter_mut().for_each(|it| walk(it, binds));
5791 }
5792 Expr::AnyAll { expr, array, .. } => {
5793 walk(expr, binds);
5794 walk(array, binds);
5795 }
5796 Expr::Array(items) => items.iter_mut().for_each(|it| walk(it, binds)),
5797 Expr::ArraySubscript { target, index } => {
5798 walk(target, binds);
5799 walk(index, binds);
5800 }
5801 _ => {}
5802 }
5803 }
5804 for item in &mut stmt.items {
5805 if let SelectItem::Expr { expr, .. } = item {
5806 walk(expr, binds);
5807 }
5808 }
5809 if let Some(w) = &mut stmt.where_ {
5810 walk(w, binds);
5811 }
5812 if let Some(h) = &mut stmt.having {
5813 walk(h, binds);
5814 }
5815 if let Some(gs) = &mut stmt.group_by {
5816 gs.iter_mut().for_each(|g| walk(g, binds));
5817 }
5818 for o in &mut stmt.order_by {
5819 walk(&mut o.expr, binds);
5820 }
5821 if let Some(from) = &mut stmt.from {
5822 for j in &mut from.joins {
5823 if let Some(on) = &mut j.on {
5824 walk(on, binds);
5825 }
5826 }
5827 }
5828 // v7.39 (read01 round 69) — the UNION peers. A body like
5829 // `SELECT k UNION ALL SELECT k * 10` has its second half in `unions`, and
5830 // leaving it unsubstituted made the argument look like a missing column.
5831 for (_, peer) in &mut stmt.unions {
5832 substitute_arg_refs_in_select(peer, binds);
5833 }
5834 for cte in &mut stmt.ctes {
5835 if let Some(s) = cte.body.as_select_mut() {
5836 substitute_arg_refs_in_select(s, binds);
5837 }
5838 }
5839}
5840
5841/// v7.38.4 (sentori step 54) — the cast target for a function's DECLARED
5842/// return type.
5843///
5844/// `def.returns` holds the type as the user wrote it, so an array is
5845/// `bigint[]`; a `CastTarget::Named` spells the same type `bigint_array`.
5846/// The coercion therefore could not resolve `RETURNS bigint[]`, and the
5847/// `or_else(NULL)` under every call site turned "I could not coerce this"
5848/// into a NULL answer: the body computed `{1,2}` and the caller got
5849/// nothing, with no error anywhere. Their version keys compare through one
5850/// of these, so every version-targeted push reached zero devices while
5851/// reporting success.
5852///
5853/// Shared by both coercion sites — the pure-expression body and the one
5854/// with its own FROM — because they had the same line written twice and
5855/// fixing one would have left the other.
5856pub(crate) fn declared_return_cast_target(declared: &str) -> spg_sql::ast::CastTarget {
5857 let name = declared.trim().strip_suffix("[]").map_or_else(
5858 || alloc::string::String::from(declared.trim()),
5859 |base| alloc::format!("{}_array", base.trim()),
5860 );
5861 spg_sql::ast::CastTarget::Named(name)
5862}
5863
5864impl crate::Engine {
5865 /// v7.39 (read01 round 64) — call a plpgsql function as a scalar. The body
5866 /// runs on the interpreter the DO block and triggers already use, with the
5867 /// arguments bound as locals; its `SELECT … INTO` and `FOR … IN SELECT`
5868 /// resolvers go through the READ path, so what the body sees is what a
5869 /// hand-written query would see (visibility filter and all).
5870 ///
5871 /// A body that writes is refused — the call arrives through expression
5872 /// evaluation, which holds the engine immutably. Refusing is the honest
5873 /// answer; silently dropping the write would be the worst one.
5874 pub(crate) fn call_plpgsql_scalar_fn(
5875 &self,
5876 def: &spg_storage::FunctionDef,
5877 arg_names: &[alloc::string::String],
5878 args: &spg_storage::Row<'static>,
5879 ) -> Result<Value<'static>, EvalError> {
5880 let block =
5881 spg_sql::parse_function_body(def.body.trim()).map_err(|e| EvalError::TypeMismatch {
5882 detail: alloc::format!("function {:?} body does not parse: {e}", def.name),
5883 })?;
5884 let mut locals: alloc::collections::BTreeMap<alloc::string::String, Value<'static>> =
5885 alloc::collections::BTreeMap::new();
5886 for (i, n) in arg_names.iter().enumerate() {
5887 if n.is_empty() {
5888 continue;
5889 }
5890 locals.insert(
5891 n.to_ascii_lowercase(),
5892 args.values.get(i).cloned().unwrap_or(Value::Null),
5893 );
5894 }
5895 let dts = self
5896 .session_param("default_text_search_config")
5897 .map(alloc::string::String::from);
5898
5899 let select_into = |stmt: &spg_sql::ast::Statement| -> Result<
5900 Value<'static>,
5901 crate::triggers::TriggerError,
5902 > {
5903 let spg_sql::ast::Statement::Select(s) = stmt else {
5904 return Err(crate::triggers::TriggerError::EvalFailed {
5905 function: def.name.clone(),
5906 cause: EvalError::TypeMismatch {
5907 detail: "SELECT … INTO body must be a SELECT".into(),
5908 },
5909 });
5910 };
5911 let r = self
5912 .exec_select_cancel(s, crate::CancelToken::none())
5913 .map_err(|e| crate::triggers::TriggerError::EvalFailed {
5914 function: def.name.clone(),
5915 cause: EvalError::TypeMismatch {
5916 detail: alloc::format!("SELECT … INTO failed: {e}"),
5917 },
5918 })?;
5919 match r {
5920 crate::QueryResult::Rows { rows, .. } => Ok(rows
5921 .into_iter()
5922 .next()
5923 .and_then(|row| row.values.into_iter().next())
5924 .unwrap_or(Value::Null)),
5925 _ => Ok(Value::Null),
5926 }
5927 };
5928 let for_query = |stmt: &spg_sql::ast::Statement| -> Result<
5929 (
5930 alloc::vec::Vec<alloc::string::String>,
5931 alloc::vec::Vec<alloc::vec::Vec<Value<'static>>>,
5932 ),
5933 crate::triggers::TriggerError,
5934 > {
5935 let spg_sql::ast::Statement::Select(s) = stmt else {
5936 return Err(crate::triggers::TriggerError::EvalFailed {
5937 function: def.name.clone(),
5938 cause: EvalError::TypeMismatch {
5939 detail: "FOR … IN body must be a SELECT".into(),
5940 },
5941 });
5942 };
5943 let r = self
5944 .exec_select_cancel(s, crate::CancelToken::none())
5945 .map_err(|e| crate::triggers::TriggerError::EvalFailed {
5946 function: def.name.clone(),
5947 cause: EvalError::TypeMismatch {
5948 detail: alloc::format!("FOR … IN SELECT failed: {e}"),
5949 },
5950 })?;
5951 match r {
5952 crate::QueryResult::Rows { columns, rows } => Ok((
5953 columns.iter().map(|c| c.name.clone()).collect(),
5954 rows.into_iter().map(|row| row.values).collect(),
5955 )),
5956 _ => Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new())),
5957 }
5958 };
5959
5960 let out = crate::triggers::call_plpgsql_scalar(
5961 &def.name,
5962 &block,
5963 locals,
5964 dts.as_deref(),
5965 Some(&select_into),
5966 Some(&for_query),
5967 // A scalar call has no set to build; RETURN NEXT / RETURN QUERY are
5968 // errors here, as in PG.
5969 // (second None below: the read path holds the engine immutably, so
5970 // RAISE messages have nowhere session-bound to go — B3 residual.)
5971 None,
5972 None,
5973 )
5974 .map_err(|e| EvalError::TypeMismatch {
5975 detail: alloc::format!("{e}"),
5976 })?;
5977 let declared = def.returns.trim();
5978 let Some(v) = out else {
5979 if declared.eq_ignore_ascii_case("VOID") {
5980 return Ok(Value::Null);
5981 }
5982 // PG: a non-void function that falls out of the bottom.
5983 return Err(EvalError::TypeMismatch {
5984 detail: alloc::format!(
5985 "control reached end of function {:?} without RETURN",
5986 def.name
5987 ),
5988 });
5989 };
5990 if declared.eq_ignore_ascii_case("VOID") {
5991 return Ok(Value::Null);
5992 }
5993 crate::eval::cast::cast_value(
5994 v.into_owned(),
5995 spg_sql::ast::CastTarget::Named(alloc::string::String::from(declared)),
5996 )
5997 .or_else(|_| Ok(Value::Null))
5998 }
5999}
6000
6001impl crate::Engine {
6002 /// v7.39 (read01 round 66) — run a `RETURNS SETOF` plpgsql function and
6003 /// collect the rows `RETURN NEXT` / `RETURN QUERY` appended. Same
6004 /// interpreter, same read-path resolvers as the scalar call — the only
6005 /// difference is that a SINK is provided, which is what makes those two
6006 /// statements legal.
6007 pub(crate) fn call_plpgsql_setof_fn(
6008 &self,
6009 def: &spg_storage::FunctionDef,
6010 arg_names: &[alloc::string::String],
6011 args: &[Value<'static>],
6012 ) -> Result<alloc::vec::Vec<alloc::vec::Vec<Value<'static>>>, EvalError> {
6013 let block =
6014 spg_sql::parse_function_body(def.body.trim()).map_err(|e| EvalError::TypeMismatch {
6015 detail: alloc::format!("function {:?} body does not parse: {e}", def.name),
6016 })?;
6017 let mut locals: alloc::collections::BTreeMap<alloc::string::String, Value<'static>> =
6018 alloc::collections::BTreeMap::new();
6019 for (i, n) in arg_names.iter().enumerate() {
6020 if n.is_empty() {
6021 continue;
6022 }
6023 locals.insert(
6024 n.to_ascii_lowercase(),
6025 args.get(i).cloned().unwrap_or(Value::Null),
6026 );
6027 }
6028 let dts = self
6029 .session_param("default_text_search_config")
6030 .map(alloc::string::String::from);
6031 let run_select = |stmt: &spg_sql::ast::Statement,
6032 what: &str|
6033 -> Result<crate::QueryResult, crate::triggers::TriggerError> {
6034 let spg_sql::ast::Statement::Select(s) = stmt else {
6035 return Err(crate::triggers::TriggerError::EvalFailed {
6036 function: def.name.clone(),
6037 cause: EvalError::TypeMismatch {
6038 detail: alloc::format!("{what} body must be a SELECT"),
6039 },
6040 });
6041 };
6042 self.exec_select_cancel(s, crate::CancelToken::none())
6043 .map_err(|e| crate::triggers::TriggerError::EvalFailed {
6044 function: def.name.clone(),
6045 cause: EvalError::TypeMismatch {
6046 detail: alloc::format!("{what} failed: {e}"),
6047 },
6048 })
6049 };
6050 let select_into = |stmt: &spg_sql::ast::Statement| -> Result<
6051 Value<'static>,
6052 crate::triggers::TriggerError,
6053 > {
6054 match run_select(stmt, "SELECT … INTO")? {
6055 crate::QueryResult::Rows { rows, .. } => Ok(rows
6056 .into_iter()
6057 .next()
6058 .and_then(|r| r.values.into_iter().next())
6059 .unwrap_or(Value::Null)),
6060 _ => Ok(Value::Null),
6061 }
6062 };
6063 let for_query = |stmt: &spg_sql::ast::Statement| -> Result<
6064 (
6065 alloc::vec::Vec<alloc::string::String>,
6066 alloc::vec::Vec<alloc::vec::Vec<Value<'static>>>,
6067 ),
6068 crate::triggers::TriggerError,
6069 > {
6070 match run_select(stmt, "FOR … IN / RETURN QUERY")? {
6071 crate::QueryResult::Rows { columns, rows } => Ok((
6072 columns.iter().map(|c| c.name.clone()).collect(),
6073 rows.into_iter().map(|r| r.values).collect(),
6074 )),
6075 _ => Ok((alloc::vec::Vec::new(), alloc::vec::Vec::new())),
6076 }
6077 };
6078 let sink: core::cell::RefCell<alloc::vec::Vec<alloc::vec::Vec<Value<'static>>>> =
6079 core::cell::RefCell::new(alloc::vec::Vec::new());
6080 crate::triggers::call_plpgsql_scalar(
6081 &def.name,
6082 &block,
6083 locals,
6084 dts.as_deref(),
6085 Some(&select_into),
6086 Some(&for_query),
6087 Some(&sink),
6088 // Read path — immutable engine borrow; B3 residual.
6089 None,
6090 )
6091 .map_err(|e| EvalError::TypeMismatch {
6092 detail: alloc::format!("{e}"),
6093 })?;
6094 Ok(sink.into_inner())
6095 }
6096}
6097
6098/// v7.39 (round 236) — resolve an ARRAY constructor's element types the way
6099/// PG does. Typed elements must share a type category; a bare string or NULL
6100/// literal is untyped and converts to whatever the typed elements resolved
6101/// to, reporting the value (not a type mismatch) when it will not convert.
6102fn unify_array_elements(
6103 items: &[Expr],
6104 materialised: &mut [Value<'static>],
6105) -> Result<(), EvalError> {
6106 unify_construct_values("ARRAY", items, materialised)
6107}
6108
6109/// v7.39 (round 238) — an `IN (...)` list must be comparable with its
6110/// needle. Reports the operator the way PG does
6111/// ("operator does not exist: integer = text"), and — like round 237 —
6112/// judges only the operands whose type is genuinely known.
6113fn require_in_list_comparable(
6114 needle: &Expr,
6115 list: &[Expr],
6116 ctx: &EvalContext<'_>,
6117) -> Result<(), EvalError> {
6118 let known_ty = |e: &Expr| {
6119 matches!(e, Expr::Cast { .. } | Expr::Literal(_) | Expr::Column(_))
6120 .then(|| crate::describe::describe_expr(e, ctx.columns).map(|s| s.ty))
6121 .flatten()
6122 };
6123 // An untyped literal adopts the needle's type, so it never conflicts.
6124 //
6125 // v7.39 (round 652) — and so does a cast to one of the reg types.
6126 // `'pg_class'::regclass` carries an oid AND a name, which is why
6127 // `compare` has arms for both, but it DESCRIBES as text — SPG has no
6128 // `DataType` for it. This check ran before any comparison and
6129 // refused `WHERE oid IN ('pg_class'::regclass, …)` as `bigint =
6130 // text`, while the identical `oid = 'pg_class'::regclass` worked.
6131 // That shape is how pg_dump, ORMs and monitoring queries name a
6132 // handful of relations at once, so it is not a corner.
6133 let reg_cast = |e: &Expr| {
6134 match e {
6135 Expr::Cast { target, .. } => match target {
6136 spg_sql::ast::CastTarget::RegClass | spg_sql::ast::CastTarget::RegType => true,
6137 // `regproc` and `regnamespace` have no variant of their
6138 // own; they arrive through the generic named path.
6139 spg_sql::ast::CastTarget::Named(n) => {
6140 n.eq_ignore_ascii_case("regproc")
6141 || n.eq_ignore_ascii_case("regnamespace")
6142 || n.eq_ignore_ascii_case("regtype")
6143 || n.eq_ignore_ascii_case("regclass")
6144 }
6145 _ => false,
6146 },
6147 _ => false,
6148 }
6149 };
6150 let untyped = |e: &Expr| {
6151 reg_cast(e)
6152 || matches!(
6153 e,
6154 Expr::Literal(spg_sql::ast::Literal::String(_))
6155 | Expr::Literal(spg_sql::ast::Literal::Null)
6156 )
6157 };
6158 // The needle can be untyped too (`NULL IN (1,2)`): SPG has no `Unknown`
6159 // DataType, so a bare NULL describes as TEXT and would look like a text
6160 // needle conflicting with integer list items.
6161 if untyped(needle) {
6162 return Ok(());
6163 }
6164 let Some(nt) = known_ty(needle) else {
6165 return Ok(());
6166 };
6167 for item in list {
6168 if untyped(item) {
6169 continue;
6170 }
6171 let Some(it) = known_ty(item) else { continue };
6172 if !crate::conversions::types_unify(nt, it) {
6173 return Err(EvalError::TypeMismatch {
6174 detail: alloc::format!(
6175 "operator does not exist: {} = {}",
6176 crate::conversions::pg_type_name_for_error(nt),
6177 crate::conversions::pg_type_name_for_error(it),
6178 ),
6179 });
6180 }
6181 }
6182 Ok(())
6183}
6184
6185/// v7.39 (round 237) — STATIC branch-type resolution for the constructs
6186/// whose branches must not all be evaluated: CASE runs only the branch it
6187/// takes, and a COALESCE / GREATEST argument may have side effects
6188/// (`COALESCE(nextval('s'), 1)`), so the check reads each branch's declared
6189/// type instead of its value. Same rule and wording as the value-driven
6190/// ARRAY path below; an untyped literal is converted here (a literal has no
6191/// side effects) so a value that will not convert is reported as PG does.
6192/// v7.39 (round 609) — takes anything that yields the branches, so the
6193/// COALESCE caller no longer builds a `Vec<&Expr>` of them for every row.
6194pub(crate) fn unify_branch_types_static<'e>(
6195 construct: &str,
6196 branches: impl IntoIterator<Item = &'e Expr> + Clone,
6197 ctx: &EvalContext<'_>,
6198) -> Result<(), EvalError> {
6199 use spg_storage::DataType;
6200 let untyped = |e: &Expr| {
6201 matches!(
6202 e,
6203 Expr::Literal(spg_sql::ast::Literal::String(_))
6204 | Expr::Literal(spg_sql::ast::Literal::Null)
6205 )
6206 };
6207 let mut resolved: Option<DataType> = None;
6208 for e in branches.clone() {
6209 if untyped(e) {
6210 continue;
6211 }
6212 // Only branches whose type is GENUINELY known take part. A general
6213 // `describe_expr` is a best-effort hint for wire type tags, not a
6214 // type checker: it reports a binary operator as its left operand's
6215 // type, so `payload->'a'` (jsonb in PG) came back as text and this
6216 // check refused a working `COALESCE(payload->'a', '{}'::jsonb)`.
6217 // Refusing a valid query is worse than missing an invalid one, so
6218 // the check confines itself to an explicit cast, a typed literal and
6219 // a plain column reference.
6220 let known = matches!(e, Expr::Cast { .. } | Expr::Literal(_) | Expr::Column(_));
6221 if !known {
6222 continue;
6223 }
6224 // 7.38.1 S5.1 — a reg* cast is an OID wearing a name: describe
6225 // says Text (the wire render), but it compares and unions with
6226 // numeric catalog columns (pg_dump: `SELECT classid … UNION
6227 // ALL SELECT 'pg_opfamily'::regclass …`). Its static claim is
6228 // not genuinely known here, so it sits the check out — the
6229 // dual RegClass value reconciles at runtime.
6230 if matches!(
6231 e,
6232 Expr::Cast {
6233 target: spg_sql::ast::CastTarget::RegType | spg_sql::ast::CastTarget::RegClass,
6234 ..
6235 }
6236 ) {
6237 continue;
6238 }
6239 let Some(ty) = crate::describe::describe_expr_type(e, ctx.columns) else {
6240 continue;
6241 };
6242 match resolved {
6243 None => resolved = Some(ty),
6244 Some(prev) if crate::conversions::types_unify(prev, ty) => {
6245 if matches!(prev, DataType::Int | DataType::SmallInt) {
6246 resolved = Some(ty);
6247 }
6248 }
6249 Some(prev) => {
6250 return Err(EvalError::TypeMismatch {
6251 detail: alloc::format!(
6252 "{construct} types {} and {} cannot be matched",
6253 crate::conversions::pg_type_name_for_error(prev),
6254 crate::conversions::pg_type_name_for_error(ty),
6255 ),
6256 });
6257 }
6258 }
6259 }
6260 let Some(target) = resolved else {
6261 return Ok(());
6262 };
6263 if matches!(target, DataType::Text) {
6264 return Ok(());
6265 }
6266 // v7.39 (round 398) — MySQL aggregates a mixed int/string CASE /
6267 // COALESCE to a string (`CASE WHEN 1 THEN 1 ELSE 'x' END` is '1', not an
6268 // error); PG requires the untyped string literals to coerce to the
6269 // resolved numeric type, so it refuses. Under the dialect, skip that
6270 // coercion check — the value is returned as-is / widened by the caller.
6271 if ctx.mysql_dialect {
6272 return Ok(());
6273 }
6274 for e in branches {
6275 if !untyped(e) {
6276 continue;
6277 }
6278 if let Expr::Literal(spg_sql::ast::Literal::String(lit)) = e {
6279 crate::conversions::coerce_value(Value::text(lit.clone()), target, "", 0).map_err(
6280 |err| match err {
6281 crate::EngineError::Eval(ev) => ev,
6282 other => EvalError::TypeMismatch {
6283 detail: alloc::format!("{other}"),
6284 },
6285 },
6286 )?;
6287 }
6288 }
6289 Ok(())
6290}
6291
6292/// v7.39 (round 237) — the same resolution for every construct that builds
6293/// one value out of several branches: ARRAY, CASE, COALESCE, GREATEST and
6294/// LEAST. PG names the construct in the message ("CASE types text and
6295/// integer cannot be matched"), which is why the caller passes it in.
6296pub(crate) fn unify_construct_values(
6297 construct: &str,
6298 items: &[Expr],
6299 materialised: &mut [Value<'static>],
6300) -> Result<(), EvalError> {
6301 use spg_storage::DataType;
6302 let untyped = |e: &Expr| {
6303 matches!(
6304 e,
6305 Expr::Literal(spg_sql::ast::Literal::String(_))
6306 | Expr::Literal(spg_sql::ast::Literal::Null)
6307 )
6308 };
6309 // The type the typed elements agree on, if any.
6310 let mut resolved: Option<DataType> = None;
6311 for (i, v) in materialised.iter().enumerate() {
6312 if items.get(i).is_some_and(untyped) {
6313 continue;
6314 }
6315 let Some(ty) = v.data_type() else { continue };
6316 match resolved {
6317 None => resolved = Some(ty),
6318 Some(prev) if crate::conversions::types_unify(prev, ty) => {
6319 // Keep the wider of the two so the coercion below targets it.
6320 if matches!(prev, DataType::Int | DataType::SmallInt) {
6321 resolved = Some(ty);
6322 }
6323 }
6324 Some(prev) => {
6325 return Err(EvalError::TypeMismatch {
6326 detail: alloc::format!(
6327 "{construct} types {} and {} cannot be matched",
6328 crate::conversions::pg_type_name_for_error(prev),
6329 crate::conversions::pg_type_name_for_error(ty),
6330 ),
6331 });
6332 }
6333 }
6334 }
6335 // Untyped literals adopt that type; a failure names the value, as PG does.
6336 let Some(target) = resolved else {
6337 return Ok(());
6338 };
6339 if matches!(target, DataType::Text) {
6340 return Ok(());
6341 }
6342 for (i, v) in materialised.iter_mut().enumerate() {
6343 if !items.get(i).is_some_and(untyped) || matches!(v, Value::Null) {
6344 continue;
6345 }
6346 *v = crate::conversions::coerce_value(v.clone(), target, "", i).map_err(|e| match e {
6347 crate::EngineError::Eval(ev) => ev,
6348 other => EvalError::TypeMismatch {
6349 detail: alloc::format!("{other}"),
6350 },
6351 })?;
6352 }
6353 Ok(())
6354}
6355
6356/// v7.39 (round 309, V30) — split `'<timestamp> <zone name>'` into the
6357/// wall-clock reading and the zone token, for the zone-less target types.
6358///
6359/// Returns `None` when the literal parses on its own (nothing to strip)
6360/// or when the trailing token is not zone-SHAPED — those keep the
6361/// ordinary "invalid input syntax" path, which is what PG answers for
6362/// `'2020-01-01 10:00:00 xyz'`. Whether a zone-shaped token is a REAL
6363/// zone is the caller's question; getting that wrong is a different
6364/// error in PG, and conflating the two would report a malformed literal
6365/// for a merely-misspelled zone.
6366///
6367/// Deliberately does not accept a bare time (`'10:00:00 America/New_York'`):
6368/// PG refuses a named zone there, and only reaches this spelling through
6369/// a full timestamp literal.
6370fn split_trailing_zone_name(txt: &str, order: format::DateOrder) -> Option<(i64, &str)> {
6371 // Already valid without help — leave it alone.
6372 if format::parse_timestamp_literal_wall_ordered(txt, order).is_some() {
6373 return None;
6374 }
6375 let trimmed = txt.trim_end();
6376 let idx = trimmed.rfind(' ')?;
6377 let (head, tail) = (trimmed[..idx].trim(), trimmed[idx + 1..].trim());
6378 // An era marker is part of the timestamp, not a zone.
6379 let zone_shaped = tail.len() > 1
6380 && tail.bytes().any(|b| b.is_ascii_alphabetic())
6381 && !tail.eq_ignore_ascii_case("bc")
6382 && !tail.eq_ignore_ascii_case("ad");
6383 if !zone_shaped {
6384 return None;
6385 }
6386 let wall = format::parse_timestamp_literal_wall_ordered(head, order)?;
6387 Some((wall, tail))
6388}
6389
6390#[cfg(test)]
6391mod tests {
6392 use super::*;
6393 use alloc::vec;
6394 use spg_sql::ast::UnOp;
6395 use spg_storage::{ColumnSchema, DataType, Row};
6396
6397 fn col(name: &str, ty: DataType) -> ColumnSchema {
6398 ColumnSchema::new(name, ty, true)
6399 }
6400
6401 fn ctx<'a>(cols: &'a [ColumnSchema], alias: Option<&'a str>) -> EvalContext<'a> {
6402 EvalContext::new(cols, alias)
6403 }
6404
6405 /// v7.32 (P4 borrow channel) differential: the borrowed comparison
6406 /// fast path in `eval_expr`'s Binary arm must be byte-for-byte the
6407 /// pre-P4 owned path (`apply_binary` on cloned operands) across a
6408 /// cross-type value matrix and every comparison operator — covering
6409 /// the fast-path types (Text/Int/Float/Date/Timestamp/Bool/Null) and
6410 /// the owned-fallback types (Numeric/Interval).
6411 #[test]
6412 fn borrowed_compare_equals_owned_apply_binary() {
6413 let vals = vec![
6414 Value::Null,
6415 Value::Bool(true),
6416 Value::Bool(false),
6417 Value::SmallInt(3),
6418 Value::Int(3),
6419 Value::Int(-1),
6420 Value::BigInt(3),
6421 Value::BigInt(100),
6422 Value::Float(3.0),
6423 Value::Float(2.5),
6424 Value::text(String::new()),
6425 Value::text("a"),
6426 Value::text("b"),
6427 Value::Date(10),
6428 Value::Timestamp(1000),
6429 Value::Numeric {
6430 scaled: 30,
6431 scale: 1,
6432 kind: spg_storage::NumericKind::Finite,
6433 },
6434 Value::Interval {
6435 months: 0,
6436 days: 0,
6437 micros: 5,
6438 },
6439 ];
6440 let ops = [
6441 BinOp::Eq,
6442 BinOp::NotEq,
6443 BinOp::Lt,
6444 BinOp::LtEq,
6445 BinOp::Gt,
6446 BinOp::GtEq,
6447 ];
6448 let cs = vec![col("x", DataType::Int), col("y", DataType::Int)];
6449 let c = ctx(&cs, None);
6450 let lhs = Expr::Column(ColumnName {
6451 qualifier: None,
6452 name: "x".into(),
6453 });
6454 let rhs = Expr::Column(ColumnName {
6455 qualifier: None,
6456 name: "y".into(),
6457 });
6458 for l in &vals {
6459 for r in &vals {
6460 let row = Row::new(vec![l.clone(), r.clone()]);
6461 for op in ops {
6462 let got = eval_expr(
6463 &Expr::Binary {
6464 lhs: alloc::boxed::Box::new(lhs.clone()),
6465 op,
6466 rhs: alloc::boxed::Box::new(rhs.clone()),
6467 },
6468 &row,
6469 &c,
6470 );
6471 // Pre-P4 reference: owned operands through apply_binary
6472 // (collation fold is a no-op for non-CI columns).
6473 let want = apply_binary(op, l.clone(), r.clone());
6474 assert_eq!(
6475 format!("{got:?}"),
6476 format!("{want:?}"),
6477 "op={op:?} l={l:?} r={r:?}"
6478 );
6479 }
6480 }
6481 }
6482 }
6483
6484 fn lit(n: i64) -> Expr {
6485 Expr::Literal(Literal::Integer(n))
6486 }
6487
6488 fn null() -> Expr {
6489 Expr::Literal(Literal::Null)
6490 }
6491
6492 fn col_ref(name: &str) -> Expr {
6493 Expr::Column(ColumnName {
6494 qualifier: None,
6495 name: name.into(),
6496 })
6497 }
6498
6499 #[test]
6500 fn literal_evaluates_to_value() {
6501 let r = Row::new(vec![]);
6502 let cs: [ColumnSchema; 0] = [];
6503 let c = ctx(&cs, None);
6504 assert_eq!(eval_expr(&lit(42), &r, &c).unwrap(), Value::Int(42));
6505 assert_eq!(
6506 eval_expr(&Expr::Literal(Literal::Float(1.5)), &r, &c).unwrap(),
6507 Value::Float(1.5)
6508 );
6509 assert_eq!(eval_expr(&null(), &r, &c).unwrap(), Value::Null);
6510 }
6511
6512 #[test]
6513 fn column_lookup_unqualified() {
6514 let cs = vec![col("a", DataType::Int), col("b", DataType::Text)];
6515 let r = Row::new(vec![Value::Int(7), Value::text("hi")]);
6516 let c = ctx(&cs, None);
6517 assert_eq!(eval_expr(&col_ref("a"), &r, &c).unwrap(), Value::Int(7));
6518 assert_eq!(eval_expr(&col_ref("b"), &r, &c).unwrap(), Value::text("hi"));
6519 }
6520
6521 #[test]
6522 fn column_not_found_errors() {
6523 let cs = vec![col("a", DataType::Int)];
6524 let r = Row::new(vec![Value::Int(0)]);
6525 let c = ctx(&cs, None);
6526 let err = eval_expr(&col_ref("ghost"), &r, &c).unwrap_err();
6527 assert!(matches!(err, EvalError::ColumnNotFound { ref name } if name == "ghost"));
6528 }
6529
6530 #[test]
6531 fn qualified_column_matches_alias() {
6532 let cs = vec![col("a", DataType::Int)];
6533 let r = Row::new(vec![Value::Int(5)]);
6534 let c = ctx(&cs, Some("u"));
6535 let qualified = Expr::Column(ColumnName {
6536 qualifier: Some("u".into()),
6537 name: "a".into(),
6538 });
6539 assert_eq!(eval_expr(&qualified, &r, &c).unwrap(), Value::Int(5));
6540 }
6541
6542 #[test]
6543 fn qualified_column_unknown_alias_errors() {
6544 let cs = vec![col("a", DataType::Int)];
6545 let r = Row::new(vec![Value::Int(5)]);
6546 let c = ctx(&cs, Some("u"));
6547 let wrong = Expr::Column(ColumnName {
6548 qualifier: Some("x".into()),
6549 name: "a".into(),
6550 });
6551 assert!(matches!(
6552 eval_expr(&wrong, &r, &c).unwrap_err(),
6553 EvalError::UnknownQualifier { .. }
6554 ));
6555 }
6556
6557 #[test]
6558 fn arithmetic_with_widening() {
6559 let r = Row::new(vec![]);
6560 let cs: [ColumnSchema; 0] = [];
6561 let c = ctx(&cs, None);
6562 let e = Expr::Binary {
6563 lhs: alloc::boxed::Box::new(lit(2)),
6564 op: BinOp::Add,
6565 rhs: alloc::boxed::Box::new(Expr::Literal(Literal::Float(0.5))),
6566 };
6567 assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Float(2.5));
6568 }
6569
6570 #[test]
6571 fn division_by_zero_errors() {
6572 let r = Row::new(vec![]);
6573 let cs: [ColumnSchema; 0] = [];
6574 let c = ctx(&cs, None);
6575 let e = Expr::Binary {
6576 lhs: alloc::boxed::Box::new(lit(1)),
6577 op: BinOp::Div,
6578 rhs: alloc::boxed::Box::new(lit(0)),
6579 };
6580 assert_eq!(
6581 eval_expr(&e, &r, &c).unwrap_err(),
6582 EvalError::DivisionByZero
6583 );
6584 }
6585
6586 #[test]
6587 fn comparison_returns_bool() {
6588 let r = Row::new(vec![]);
6589 let cs: [ColumnSchema; 0] = [];
6590 let c = ctx(&cs, None);
6591 let e = Expr::Binary {
6592 lhs: alloc::boxed::Box::new(lit(1)),
6593 op: BinOp::Lt,
6594 rhs: alloc::boxed::Box::new(lit(2)),
6595 };
6596 assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Bool(true));
6597 }
6598
6599 #[test]
6600 fn null_propagates_through_arithmetic() {
6601 let r = Row::new(vec![]);
6602 let cs: [ColumnSchema; 0] = [];
6603 let c = ctx(&cs, None);
6604 let e = Expr::Binary {
6605 lhs: alloc::boxed::Box::new(lit(1)),
6606 op: BinOp::Add,
6607 rhs: alloc::boxed::Box::new(null()),
6608 };
6609 assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Null);
6610 }
6611
6612 #[test]
6613 fn stack_depth_guard_trips_on_pathological_nesting() {
6614 // Built directly as an AST — the parser's own budgets (256
6615 // chained binary operators, 64 nesting levels) reject such SQL
6616 // long before eval sees it, so this exercises the eval-side
6617 // guard on its own. 30 000 frames overshoot the 768 KiB budget
6618 // at any conceivable frame size; the guard errors at the byte
6619 // budget, far below the worker stack, so deeper is safer.
6620 let mut e = Expr::Literal(Literal::Bool(true));
6621 for _ in 0..30_000 {
6622 e = Expr::Binary {
6623 lhs: alloc::boxed::Box::new(e),
6624 op: BinOp::And,
6625 rhs: alloc::boxed::Box::new(Expr::Literal(Literal::Bool(true))),
6626 };
6627 }
6628 let r = Row::new(vec![]);
6629 let cs: [ColumnSchema; 0] = [];
6630 let c = ctx(&cs, None);
6631 let err = eval_expr(&e, &r, &c).unwrap_err();
6632 assert!(matches!(err, EvalError::StackDepthExceeded), "{err:?}");
6633 // Dropping a 30 000-deep Box chain recurses in the drop glue —
6634 // deeper than the eval guard allows the EVAL side to go — so
6635 // leak it rather than gamble on the test thread's stack.
6636 core::mem::forget(e);
6637 }
6638
6639 #[test]
6640 fn and_three_valued_logic() {
6641 let r = Row::new(vec![]);
6642 let cs: [ColumnSchema; 0] = [];
6643 let c = ctx(&cs, None);
6644 let tt = |a: bool, b_null: bool| Expr::Binary {
6645 lhs: alloc::boxed::Box::new(Expr::Literal(Literal::Bool(a))),
6646 op: BinOp::And,
6647 rhs: alloc::boxed::Box::new(if b_null {
6648 null()
6649 } else {
6650 Expr::Literal(Literal::Bool(true))
6651 }),
6652 };
6653 // FALSE AND NULL → FALSE
6654 assert_eq!(
6655 eval_expr(&tt(false, true), &r, &c).unwrap(),
6656 Value::Bool(false)
6657 );
6658 // TRUE AND NULL → NULL
6659 assert_eq!(eval_expr(&tt(true, true), &r, &c).unwrap(), Value::Null);
6660 // TRUE AND TRUE → TRUE
6661 assert_eq!(
6662 eval_expr(&tt(true, false), &r, &c).unwrap(),
6663 Value::Bool(true)
6664 );
6665 }
6666
6667 #[test]
6668 fn or_three_valued_logic() {
6669 let r = Row::new(vec![]);
6670 let cs: [ColumnSchema; 0] = [];
6671 let c = ctx(&cs, None);
6672 let or_with_null = |a: bool| Expr::Binary {
6673 lhs: alloc::boxed::Box::new(Expr::Literal(Literal::Bool(a))),
6674 op: BinOp::Or,
6675 rhs: alloc::boxed::Box::new(null()),
6676 };
6677 // TRUE OR NULL → TRUE
6678 assert_eq!(
6679 eval_expr(&or_with_null(true), &r, &c).unwrap(),
6680 Value::Bool(true)
6681 );
6682 // FALSE OR NULL → NULL
6683 assert_eq!(
6684 eval_expr(&or_with_null(false), &r, &c).unwrap(),
6685 Value::Null
6686 );
6687 }
6688
6689 #[test]
6690 fn not_on_null_is_null() {
6691 let r = Row::new(vec![]);
6692 let cs: [ColumnSchema; 0] = [];
6693 let c = ctx(&cs, None);
6694 let e = Expr::Unary {
6695 op: UnOp::Not,
6696 expr: alloc::boxed::Box::new(null()),
6697 };
6698 assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Null);
6699 }
6700
6701 #[test]
6702 fn text_comparison_lexicographic() {
6703 let r = Row::new(vec![]);
6704 let cs: [ColumnSchema; 0] = [];
6705 let c = ctx(&cs, None);
6706 let e = Expr::Binary {
6707 lhs: alloc::boxed::Box::new(Expr::Literal(Literal::String("apple".into()))),
6708 op: BinOp::Lt,
6709 rhs: alloc::boxed::Box::new(Expr::Literal(Literal::String("banana".into()))),
6710 };
6711 assert_eq!(eval_expr(&e, &r, &c).unwrap(), Value::Bool(true));
6712 }
6713
6714 #[test]
6715 fn interval_format_basics() {
6716 // v7.37.5 β — three-arg signature. PG byte-equal:
6717 // `'1 day'` ≠ `'24 hours'` now, the format reflects it.
6718 assert_eq!(format_interval(0, 0, 0), "00:00:00");
6719 assert_eq!(format_interval(0, 1, 0), "1 day");
6720 assert_eq!(format_interval(0, -1, 0), "-1 days");
6721 assert_eq!(format_interval(0, 0, 86_400_000_000), "24:00:00");
6722 assert_eq!(format_interval(0, 0, 3_600_000_000), "01:00:00");
6723 assert_eq!(format_interval(0, 1, 9_000_000), "1 day 00:00:09");
6724 assert_eq!(format_interval(14, 0, 0), "1 year 2 mons");
6725 assert_eq!(format_interval(-1, 0, 0), "-1 mons");
6726 }
6727
6728 #[test]
6729 fn interval_format_pg_byte_equal_day_vs_24h() {
6730 // v7.37.5 β — the PG-canonical distinction `'1 day'` ≠ `'24 hours'`
6731 // is preserved in the formatter, not just the parser.
6732 assert_eq!(format_interval(0, 1, 0), "1 day");
6733 assert_eq!(format_interval(0, 0, 86_400_000_000), "24:00:00");
6734 assert_ne!(
6735 format_interval(0, 1, 0),
6736 format_interval(0, 0, 86_400_000_000),
6737 );
6738 }
6739}