Skip to main content

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}