spg_engine/session.rs
1//! Session-parameter handling split out of `lib.rs` (lib.rs split 16):
2//! `set_session_param` records a `SET <name> = <value>` (folding the
3//! MySQL/PG FK-check + string-dialect toggles into engine state),
4//! `session_param` reads one back (the FTS dispatcher consults
5//! `default_text_search_config`), and `ev_ctx` builds an `EvalContext`
6//! pre-chained with that config. Whole `impl Engine` methods; the
7//! execute dispatcher drives `set_session_param`, `select.rs` drives
8//! `ev_ctx`, and `dml.rs` / `plpgsql.rs` read via `session_param`.
9
10use alloc::string::String;
11
12use spg_storage::ColumnSchema;
13
14use crate::Engine;
15use crate::eval::EvalContext;
16
17impl Engine {
18 /// v7.12.1 — record a `SET <name> = <value>` parameter. Names
19 /// are case-folded to lowercase to match PG; values keep their
20 /// caller-supplied form so observability paths see what was
21 /// requested. Only `default_text_search_config` is consulted by
22 /// the engine today.
23 pub(crate) fn set_session_param(&mut self, name: String, value: spg_sql::ast::SetValue) {
24 let normalised = match value {
25 spg_sql::ast::SetValue::String(s) => s,
26 spg_sql::ast::SetValue::Ident(s) => s,
27 spg_sql::ast::SetValue::Number(s) => s,
28 spg_sql::ast::SetValue::Default => String::new(),
29 };
30 let key = name.to_ascii_lowercase();
31 // v7.14.0 — mysqldump preamble emits
32 // `SET FOREIGN_KEY_CHECKS=0` so it can CREATE TABLE in any
33 // order despite cross-table FK references; the closing
34 // section emits `SET FOREIGN_KEY_CHECKS=1` (or
35 // `=@OLD_FOREIGN_KEY_CHECKS` which resolves to "ON" in our
36 // session-variable-aware path). Match both shapes.
37 // Also accept PG's `session_replication_role = 'replica'`
38 // which suppresses trigger + FK enforcement during a
39 // logical replication apply (pg_dump preserves this for
40 // schema-only mode but it shows up in some restores).
41 let value_off = matches!(
42 normalised.to_ascii_lowercase().as_str(),
43 "0" | "off" | "false"
44 );
45 let value_on = matches!(
46 normalised.to_ascii_lowercase().as_str(),
47 "1" | "on" | "true"
48 );
49 if key == "foreign_key_checks"
50 || key == "session_replication_role" && normalised.eq_ignore_ascii_case("replica")
51 {
52 if value_off || key == "session_replication_role" {
53 self.foreign_key_checks = false;
54 } else if value_on
55 || (key == "session_replication_role" && normalised.eq_ignore_ascii_case("origin"))
56 {
57 self.foreign_key_checks = true;
58 // Drain pending FK queue against the now-complete
59 // catalog. Errors here surface as the SET reply —
60 // caller knows enabling checks revealed orphans.
61 let _ = self.drain_pending_foreign_keys();
62 }
63 }
64 // v7.22 (round-13 T3) — string-literal dialect signals.
65 // `SET sql_mode = …` is something only MySQL clients and
66 // mysqldump preambles emit → MySQL escape semantics.
67 // `SET standard_conforming_strings = on|off` is PG's own
68 // switch for exactly this behaviour (every pg_dump preamble
69 // sets it to on). The same SQL text lexes differently per
70 // dialect, so a flip invalidates the plan cache.
71 let new_escapes = if key == "sql_mode" {
72 Some(true)
73 } else if key == "standard_conforming_strings" {
74 Some(value_off)
75 } else {
76 None
77 };
78 if let Some(flag) = new_escapes
79 && flag != self.backslash_escapes
80 {
81 self.backslash_escapes = flag;
82 self.plan_cache.clear();
83 }
84 self.session_params.insert(key, normalised);
85 }
86
87 /// v7.12.1 — read a session parameter set via `SET`. Used by
88 /// the FTS function dispatcher to resolve the default config
89 /// for `to_tsvector(text)` / `plainto_tsquery(text)` etc.
90 #[must_use]
91 pub fn session_param(&self, name: &str) -> Option<&str> {
92 self.session_params
93 .get(&name.to_ascii_lowercase())
94 .map(String::as_str)
95 }
96
97 /// v7.12.1 — build an `EvalContext` chained with the session's
98 /// `default_text_search_config`. Engine-internal callers use
99 /// this instead of `EvalContext::new` so the FTS function
100 /// dispatcher sees the SET configuration.
101 pub(crate) fn ev_ctx<'a>(
102 &'a self,
103 columns: &'a [ColumnSchema],
104 alias: Option<&'a str>,
105 ) -> EvalContext<'a> {
106 EvalContext::new(columns, alias)
107 .with_default_text_search_config(self.session_param("default_text_search_config"))
108 }
109}