diff --git a/CHANGELOG.md b/CHANGELOG.md index 1caeb70..07cf24e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All changes that a user can see are in this file. The version is 0.M.patch, wher ## Unreleased +### Added + +- `set_config` (spec/06 section 6.13), which is `SET` as a function. It takes the same values and gives the same errors as `SET`, and gives the new value as `SHOW` does. A null value resets the parameter. The change counts for the rest of the query, so a `set_config` of `TimeZone` changes the output of the values after it in the same row. An error or a rollback undoes the change, and a local change ends with its transaction. + +### Fixed + +- After the first query of a transaction, `SET TRANSACTION ISOLATION LEVEL` with a new level, `SET TRANSACTION [NOT] DEFERRABLE` and `SET TRANSACTION READ WRITE` in a read-only transaction now give `25001`, as in PostgreSQL. A query takes the snapshot of the transaction when it is analyzed, in a `Query` or a `Parse` message. + ## 0.0.18 (2026-10-08) The eighteenth patch release on the 0.0 line. `SELECT` now has subqueries in expressions, the 12 OID alias types such as `regclass` and `regtype`, and the catalog visibility and comment functions. No crate is published. diff --git a/crates/rupg-exec/src/tests.rs b/crates/rupg-exec/src/tests.rs index 8240eb4..24765fd 100644 --- a/crates/rupg-exec/src/tests.rs +++ b/crates/rupg-exec/src/tests.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use rupg_analyze::{Env, Params, analyze}; use rupg_common::Result; use rupg_sql::nodes::Node; @@ -9,7 +11,7 @@ use crate::prepare; /// A session at 2000-01-11 00:00 UTC with the default settings. struct TestSession { - zone: FixedZone, + zone: Rc, } impl rupg_func::Session for TestSession { @@ -19,8 +21,8 @@ impl rupg_func::Session for TestSession { fn interval_style(&self) -> IntervalStyle { IntervalStyle::Postgres } - fn zone(&self) -> Result<&dyn TimeZone> { - Ok(&self.zone) + fn zone(&self) -> Result> { + Ok(Rc::clone(&self.zone) as Rc) } fn extra_float_digits(&self) -> i32 { 1 @@ -61,6 +63,9 @@ impl rupg_func::Session for TestSession { fn setting(&self, _: &str) -> Option { None } + fn set_setting(&self, _: &str, _: Option<&str>, _: bool) -> Result { + Err(rupg_common::Error::internal("the test session has no settings to change")) + } } impl Env for TestSession { @@ -87,7 +92,7 @@ impl Env for TestSession { /// The text of each column of each row of a query, `NULL` for a null value, or the error. fn run(sql: &str) -> Result>> { - let session = TestSession { zone: FixedZone::utc() }; + let session = TestSession { zone: Rc::new(FixedZone::utc()) }; let (stmts, _) = rupg_sql::parse(sql).map_err(rupg_common::Error::from)?; let Some(Some(Node::RawStmt(raw))) = stmts.first() else { panic!("no statement in {sql}") }; let query = analyze(raw.stmt.as_ref().expect("a statement"), &session, &Params::default())?; diff --git a/crates/rupg-func/src/io.rs b/crates/rupg-func/src/io.rs index cc892be..6db63a7 100644 --- a/crates/rupg-func/src/io.rs +++ b/crates/rupg-func/src/io.rs @@ -155,7 +155,7 @@ pub fn output(ty: u32, value: &Value, session: &dyn Session, out: &mut Vec) types::timestamp_out(*v, session.date_format(), out).map_err(type_error)?; } (_, Value::TimestampTz(v)) => { - types::timestamptz_out(*v, session.date_format(), session.zone()?, out) + types::timestamptz_out(*v, session.date_format(), &*session.zone()?, out) .map_err(type_error)?; } (_, Value::Interval(v)) => types::interval_out(v, session.interval_style(), out), @@ -249,7 +249,7 @@ pub fn input(ty: u32, text: &str, typmod: i32, session: &dyn Session) -> Result< } oid::DATE | oid::TIME | oid::TIMETZ | oid::TIMESTAMP | oid::TIMESTAMPTZ => { let zone = session.zone()?; - let cx = datetime_input(session, zone); + let cx = datetime_input(session, &*zone); match ty { oid::DATE => Value::Date(types::date_in(text, &cx).map_err(type_error)?), oid::TIME => Value::Time(types::time_in(text, typmod, &cx).map_err(type_error)?), diff --git a/crates/rupg-func/src/lib.rs b/crates/rupg-func/src/lib.rs index 8b04ac1..6bebe6d 100644 --- a/crates/rupg-func/src/lib.rs +++ b/crates/rupg-func/src/lib.rs @@ -15,6 +15,7 @@ mod reg; mod text; use std::collections::BTreeMap; +use std::rc::Rc; use std::sync::OnceLock; use rupg_common::{Error, Result, SqlState}; @@ -39,7 +40,7 @@ pub trait Session { /// # Errors /// /// `0A000` for a zone whose rules the engine does not have yet. - fn zone(&self) -> Result<&dyn TimeZone>; + fn zone(&self) -> Result>; /// The `extra_float_digits` setting. fn extra_float_digits(&self) -> i32; /// The `bytea_output` setting. @@ -66,6 +67,12 @@ pub trait Session { fn version(&self) -> String; /// The value of a setting as `SHOW` gives it, or `None` for a name that is not a setting. fn setting(&self, name: &str) -> Option; + /// `set_config_option` for `set_config`: sets a parameter for the session, or for the transaction when `local` is true, and gives the new value as `SHOW` gives it. `value` is `None` for a reset. + /// + /// # Errors + /// + /// The errors of `SET` for the name and the value. + fn set_setting(&self, name: &str, value: Option<&str>, local: bool) -> Result; } /// A call of a function. diff --git a/crates/rupg-func/src/misc.rs b/crates/rupg-func/src/misc.rs index c67f723..9ee6584 100644 --- a/crates/rupg-func/src/misc.rs +++ b/crates/rupg-func/src/misc.rs @@ -145,6 +145,27 @@ fn current_setting(call: &Call<'_>, args: &[Value]) -> Result { )) } +/// `set_config_by_name`: `set_config`, which is `SET` as a function. A null value resets the parameter, and a null `is_local` is false. +fn set_config(call: &Call<'_>, args: &[Value]) -> Result { + let name = match args.first() { + Some(Value::Null) => { + return Err(Error::new( + SqlState::NULL_VALUE_NOT_ALLOWED, + "SET requires parameter name", + )); + } + Some(name) => name.as_str().ok_or_else(bad_value)?, + None => return Err(bad_value()), + }; + let value = match args.get(1) { + Some(Value::Null) => None, + Some(value) => Some(value.as_str().ok_or_else(bad_value)?), + None => return Err(bad_value()), + }; + let local = args.get(2).and_then(Value::as_bool).unwrap_or(false); + Ok(Value::Text(call.session.set_setting(name, value, local)?)) +} + /// The kernel of a function of this module by its `prosrc`. pub(crate) fn by_src(src: &str) -> Option { Some(match src { @@ -162,6 +183,7 @@ pub(crate) fn by_src(src: &str) -> Option { "pg_client_encoding" => pg_client_encoding, "format_type" => format_type, "show_config_by_name" | "show_config_by_name_missing_ok" => current_setting, + "set_config_by_name" => set_config, _ => return None, }) } diff --git a/crates/rupg-func/src/tests.rs b/crates/rupg-func/src/tests.rs index 33802d7..0c49387 100644 --- a/crates/rupg-func/src/tests.rs +++ b/crates/rupg-func/src/tests.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use rupg_common::{Result, SqlState}; use rupg_pgcatalog::builtin; use rupg_types::{ @@ -8,12 +10,12 @@ use rupg_types::{ use crate::{Call, Session, kernel, output}; struct TestSession { - zone: FixedZone, + zone: Rc, } impl TestSession { fn new() -> TestSession { - TestSession { zone: FixedZone::utc() } + TestSession { zone: Rc::new(FixedZone::utc()) } } } @@ -24,8 +26,8 @@ impl Session for TestSession { fn interval_style(&self) -> IntervalStyle { IntervalStyle::Postgres } - fn zone(&self) -> Result<&dyn TimeZone> { - Ok(&self.zone) + fn zone(&self) -> Result> { + Ok(Rc::clone(&self.zone) as Rc) } fn extra_float_digits(&self) -> i32 { 1 @@ -66,6 +68,9 @@ impl Session for TestSession { fn setting(&self, name: &str) -> Option { (name == "datestyle").then(|| "ISO, MDY".to_string()) } + fn set_setting(&self, _: &str, _: Option<&str>, _: bool) -> Result { + Err(rupg_common::Error::internal("the test session has no settings to change")) + } } /// A call of the function in `pg_catalog` with this name and these argument types. diff --git a/crates/rupg-session/src/connection.rs b/crates/rupg-session/src/connection.rs index 721d895..c39ff3d 100644 --- a/crates/rupg-session/src/connection.rs +++ b/crates/rupg-session/src/connection.rs @@ -2,6 +2,7 @@ //! //! The connection does no I/O. The server gives it the bytes that the client sent and an output buffer, and [`Connection::step`] tells the server what to do next. See `spec/06-server-and-wire.md` section 6.1. +use std::cell::{Ref, RefCell}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -257,7 +258,7 @@ pub struct Step { /// The state of one session in the main loop. #[derive(Debug)] pub struct Connection { - settings: Settings, + settings: RefCell, transaction: Transaction, session: Session, user: String, @@ -283,7 +284,7 @@ impl Connection { /// A session with the settings that [`session_settings`] gave. `protocol` is the version of the protocol that the client uses. pub fn new(start: &Start, settings: Settings, protocol: u32) -> Connection { let mut connection = Connection { - settings, + settings: RefCell::new(settings), transaction: Transaction::default(), session: Session::new(), user: start.user.clone(), @@ -342,13 +343,13 @@ impl Connection { } /// The settings of the session. - pub fn settings(&self) -> &Settings { - &self.settings + pub fn settings(&self) -> Ref<'_, Settings> { + self.settings.borrow() } /// The messages after `AuthenticationOk`: a `ParameterStatus` for each parameter that the client must know, then `BackendKeyData`. The first `ReadyForQuery` comes from the first [`Connection::step`]. pub fn greet(&mut self, key: &CancelKey, out: &mut OutBuf) { - for (name, value) in self.settings.startup_reports() { + for (name, value) in self.settings.get_mut().startup_reports() { out.parameter_status(name.as_bytes(), value.as_bytes()); } out.parameter_status(b"rupg.version", VERSION.as_bytes()); @@ -358,7 +359,7 @@ impl Connection { /// True when the client encoding is `UTF8`, so the strings of the messages must be valid UTF-8. fn utf8(&self) -> bool { - self.settings.get("client_encoding").is_none_or(|name| name != "SQL_ASCII") + self.settings.borrow().get("client_encoding").is_none_or(|name| name != "SQL_ASCII") } /// Reads at most one message from `input` and writes the answer to `out`. @@ -448,7 +449,7 @@ impl Connection { /// The parameters that changed, then `ReadyForQuery`. fn ready_for_query(&mut self, out: &mut OutBuf) { self.session.set_utf8(self.utf8()); - let mut reports = self.settings.reports(); + let mut reports = self.settings.get_mut().reports(); // PostgreSQL changes `is_superuser` inside the change of `session_authorization`, so it reports `session_authorization` first. let at = |reports: &[(&str, String)], name| reports.iter().position(|(n, _)| *n == name); if let (Some(user), Some(superuser)) = @@ -483,7 +484,7 @@ impl Connection { locations: &[Option], out: &mut OutBuf, ) { - let least = match self.settings.get("client_min_messages").as_deref() { + let least = match self.settings.borrow().get("client_min_messages").as_deref() { Some("warning") => 1, Some("error") => 2, _ => 0, @@ -559,7 +560,7 @@ impl Connection { } else { drop(self.portals.close(b"")); let mut cx = Context { - settings: &mut self.settings, + settings: self.settings.get_mut(), transaction: &mut self.transaction, user: &self.user, notices: &mut notices, @@ -607,6 +608,7 @@ impl Connection { /// Plans and runs a query of a `Query` message. The warnings of the analysis go to `notices`. fn select(&self, node: &Node, notices: &mut Vec) -> Result { + self.settings.borrow_mut().take_snapshot(); let reader = self.reader(); let plan = query::plan(node, &reader, &Params::default())?; notices.extend(plan.notices().iter().map(|error| Notice::warning(error.clone()))); @@ -618,7 +620,7 @@ impl Connection { fn start_xact(&mut self) { if !self.xact_started { if self.transaction.start_command() { - self.settings.start_transaction(None); + self.settings.get_mut().start_transaction(None); self.transaction_start = self.statement_start; } self.xact_started = true; @@ -634,12 +636,12 @@ impl Connection { } self.xact_started = false; let (ending, chained) = self.transaction.finish_command(); - let kept = chained.then(|| self.settings.characteristics()); + let kept = chained.then(|| self.settings.get_mut().characteristics()); if ending != Ending::None { - self.settings.end(ending == Ending::Commit); + self.settings.get_mut().end(ending == Ending::Commit); } if kept.is_some() { - self.settings.start_transaction(kept); + self.settings.get_mut().start_transaction(kept); self.transaction_start = self.statement_start; } if chained || self.transaction.block() == Block::Default { @@ -658,7 +660,7 @@ impl Connection { write_error(out, "ERROR", error, position, false); self.xact_started = false; if self.transaction.abort_current() == Ending::Rollback { - self.settings.end(false); + self.settings.get_mut().end(false); } if self.transaction.failed() { self.portals.retain(|_, portal| { @@ -910,6 +912,263 @@ mod tests { ); } + #[test] + fn set_config() { + big_stack(set_config_cases); + } + + /// `set_config` in a query. Each result is the result of PostgreSQL 19. + fn set_config_cases() { + let (mut c, _) = connect(); + let one = |c: &mut Connection, sql: &str| send(c, &query(sql)); + let row = |c: &mut Connection, sql: &str| one(c, sql)[1].clone(); + assert_eq!( + one(&mut c, "SELECT set_config('work_mem', '16MB', false); SHOW work_mem"), + [ + "columns set_config", + "row 16MB", + "SELECT 1", + "columns work_mem", + "row 16MB", + "SHOW", + "ready I" + ] + ); + assert_eq!(row(&mut c, "SELECT set_config('work_mem', '1024', false)"), "row 1MB"); + assert_eq!(row(&mut c, "SELECT set_config('work_mem', NULL, false)"), "row 4MB"); + assert_eq!(row(&mut c, "SELECT set_config('work_mem', '16MB', NULL)"), "row 16MB"); + assert_eq!(row(&mut c, "SELECT set_config('Work_Mem', '2MB', false)"), "row 2MB"); + assert_eq!(one(&mut c, "RESET work_mem"), ["RESET", "ready I"]); + assert_eq!( + row(&mut c, "SELECT pg_typeof(set_config('work_mem', '4MB', false))"), + "row text" + ); + for (sql, error) in [ + ("SELECT set_config(NULL, 'x', false)", "ERROR 22004 SET requires parameter name"), + ( + "SELECT set_config('nope', 'x', false)", + "ERROR 42704 unrecognized configuration parameter \"nope\"", + ), + ( + "SELECT set_config('', 'x', false)", + "ERROR 42704 unrecognized configuration parameter \"\"", + ), + ( + "SELECT set_config('role', 'nobody', false)", + "ERROR 22023 role \"nobody\" does not exist", + ), + ( + "SELECT set_config('session_authorization', 'nobody', false)", + "ERROR 22023 role \"nobody\" does not exist", + ), + ( + "SELECT set_config('server_version', '1', false)", + "ERROR 55P02 parameter \"server_version\" cannot be changed", + ), + ( + "SELECT set_config('work_mem', 'abc', false)", + "ERROR 22023 invalid value for parameter \"work_mem\": \"abc\"", + ), + ( + "SELECT set_config('work_mem', '', false)", + "ERROR 22023 invalid value for parameter \"work_mem\": \"\"", + ), + ] { + assert_eq!(one(&mut c, sql), [error, "ready I"], "{sql}"); + } + // A placeholder of an extension, and its reset to an empty string. + assert_eq!(row(&mut c, "SELECT set_config('my.var', 'x', false)"), "row x"); + assert_eq!(row(&mut c, "SELECT current_setting('my.var')"), "row x"); + assert_eq!(row(&mut c, "SELECT set_config('my.var', NULL, false)"), "row "); + assert_eq!(row(&mut c, "SELECT set_config('a.b.c', 'x', false)"), "row x"); + // A local value ends with its transaction. + assert_eq!(row(&mut c, "SELECT set_config('work_mem', '8MB', true)"), "row 8MB"); + assert_eq!(row(&mut c, "SHOW work_mem"), "row 4MB"); + assert_eq!(one(&mut c, "BEGIN"), ["BEGIN", "ready T"]); + assert_eq!(row(&mut c, "SELECT set_config('work_mem', '8MB', true)"), "row 8MB"); + assert_eq!(row(&mut c, "SHOW work_mem"), "row 8MB"); + assert_eq!(one(&mut c, "COMMIT"), ["COMMIT", "ready I"]); + assert_eq!(row(&mut c, "SHOW work_mem"), "row 4MB"); + // An error or a rollback undoes the change. + assert_eq!( + one(&mut c, "SELECT set_config('work_mem', '16MB', false), 1/0"), + ["ERROR 22012 division by zero", "ready I"] + ); + assert_eq!(row(&mut c, "SHOW work_mem"), "row 4MB"); + assert_eq!(one(&mut c, "BEGIN"), ["BEGIN", "ready T"]); + assert_eq!(row(&mut c, "SELECT set_config('work_mem', '16MB', false)"), "row 16MB"); + assert_eq!(one(&mut c, "ROLLBACK"), ["ROLLBACK", "ready I"]); + assert_eq!(row(&mut c, "SHOW work_mem"), "row 4MB"); + // The new value counts for the rest of the query, and a reported parameter is reported. + assert_eq!( + one( + &mut c, + "SELECT set_config('timezone', 'Etc/GMT-3', false), '2000-01-01 00:00+00'::timestamptz::text, '2000-01-01 00:00+00'::timestamptz" + ), + [ + "columns set_config,text,timestamptz", + "row Etc/GMT-3,2000-01-01 03:00:00+03,2000-01-01 03:00:00+03", + "SELECT 1", + "TimeZone=Etc/GMT-3", + "ready I" + ] + ); + assert_eq!( + one( + &mut c, + "SELECT '2000-01-01 00:00+00'::timestamptz::text, set_config('timezone', 'UTC', false), '2000-01-01 00:00+00'::timestamptz::text" + ), + [ + "columns text,set_config,text", + "row 2000-01-01 03:00:00+03,UTC,2000-01-01 00:00:00+00", + "SELECT 1", + "TimeZone=UTC", + "ready I" + ] + ); + assert_eq!( + one( + &mut c, + "SELECT set_config('search_path', 'pg_catalog', false), current_schemas(false)" + ), + [ + "columns set_config,current_schemas", + "row pg_catalog,{pg_catalog}", + "SELECT 1", + "search_path=pg_catalog", + "ready I" + ] + ); + assert_eq!( + one(&mut c, "RESET search_path"), + ["RESET", "search_path=\"$user\", public", "ready I"] + ); + assert_eq!( + one(&mut c, "SELECT set_config('datestyle', 'german', false)"), + [ + "columns set_config", + "row German, DMY", + "SELECT 1", + "DateStyle=German, DMY", + "ready I" + ] + ); + assert_eq!(one(&mut c, "RESET datestyle"), ["RESET", "DateStyle=ISO, MDY", "ready I"]); + // A query takes a snapshot, so the isolation level and the deferrable mode cannot change after it. + assert_eq!( + row(&mut c, "SELECT set_config('transaction_isolation', 'read committed', false)"), + "row read committed" + ); + assert_eq!( + one(&mut c, "SELECT set_config('transaction_isolation', 'serializable', false)"), + [ + "ERROR 25001 SET TRANSACTION ISOLATION LEVEL must be called before any query", + "ready I" + ] + ); + assert_eq!( + one(&mut c, "SELECT set_config('transaction_deferrable', 'on', false)"), + [ + "ERROR 25001 SET TRANSACTION [NOT] DEFERRABLE must be called before any query", + "ready I" + ] + ); + assert_eq!( + row(&mut c, "SELECT set_config('transaction_read_only', 'on', false)"), + "row on" + ); + assert_eq!( + row(&mut c, "SELECT set_config('transaction_read_only', 'off', false)"), + "row off" + ); + assert_eq!( + row(&mut c, "SELECT set_config('default_transaction_read_only', 'on', true)"), + "row on" + ); + } + + #[test] + fn transaction_modes_after_a_query() { + big_stack(transaction_modes_after_a_query_cases); + } + + /// `SET TRANSACTION` after the first snapshot of the transaction. Each result is the result of PostgreSQL 19. + fn transaction_modes_after_a_query_cases() { + let (mut c, _) = connect(); + let one = |c: &mut Connection, sql: &str| send(c, &query(sql)); + assert_eq!(one(&mut c, "BEGIN READ ONLY")[0], "BEGIN"); + assert_eq!(one(&mut c, "SELECT 1")[1], "row 1"); + assert_eq!(one(&mut c, "SET TRANSACTION READ ONLY"), ["SET", "ready T"]); + assert_eq!( + one(&mut c, "SET TRANSACTION READ WRITE"), + ["ERROR 25001 transaction read-write mode must be set before any query", "ready E"] + ); + assert_eq!(one(&mut c, "ROLLBACK"), ["ROLLBACK", "ready I"]); + assert_eq!(one(&mut c, "BEGIN READ ONLY")[0], "BEGIN"); + assert_eq!(one(&mut c, "SET TRANSACTION READ WRITE"), ["SET", "ready T"]); + assert_eq!(one(&mut c, "SHOW transaction_read_only")[1], "row off"); + assert_eq!(one(&mut c, "ROLLBACK"), ["ROLLBACK", "ready I"]); + // SHOW takes no snapshot. + assert_eq!(one(&mut c, "BEGIN")[0], "BEGIN"); + assert_eq!(one(&mut c, "SHOW work_mem")[1], "row 4MB"); + assert_eq!( + one(&mut c, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ"), + ["SET", "ready T"] + ); + assert_eq!(one(&mut c, "SHOW transaction_isolation")[1], "row repeatable read"); + assert_eq!(one(&mut c, "ROLLBACK"), ["ROLLBACK", "ready I"]); + assert_eq!(one(&mut c, "BEGIN")[0], "BEGIN"); + assert_eq!(one(&mut c, "SELECT 1")[1], "row 1"); + assert_eq!( + one(&mut c, "SET TRANSACTION NOT DEFERRABLE"), + [ + "ERROR 25001 SET TRANSACTION [NOT] DEFERRABLE must be called before any query", + "ready E" + ] + ); + assert_eq!(one(&mut c, "ROLLBACK"), ["ROLLBACK", "ready I"]); + assert_eq!( + one(&mut c, "SELECT 1; SET TRANSACTION ISOLATION LEVEL REPEATABLE READ"), + [ + "columns ?column?", + "row 1", + "SELECT 1", + "ERROR 25001 SET TRANSACTION ISOLATION LEVEL must be called before any query", + "ready I" + ] + ); + assert_eq!( + one( + &mut c, + "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; SELECT 1; SHOW transaction_isolation" + ), + [ + "SET", + "columns ?column?", + "row 1", + "SELECT 1", + "columns transaction_isolation", + "row repeatable read", + "SHOW", + "ready I" + ] + ); + // Parse of a query in a block takes the snapshot too. + assert_eq!(one(&mut c, "BEGIN")[0], "BEGIN"); + assert_eq!( + send(&mut c, &[parse("", "SELECT 1", &[]), sync()].concat()), + ["ParseComplete", "ready T"] + ); + assert_eq!( + one(&mut c, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ"), + [ + "ERROR 25001 SET TRANSACTION ISOLATION LEVEL must be called before any query", + "ready E" + ] + ); + assert_eq!(one(&mut c, "ROLLBACK"), ["ROLLBACK", "ready I"]); + } + #[test] fn a_failed_block() { big_stack(a_failed_block_cases); diff --git a/crates/rupg-session/src/connection/extended.rs b/crates/rupg-session/src/connection/extended.rs index 8c5616d..f1add21 100644 --- a/crates/rupg-session/src/connection/extended.rs +++ b/crates/rupg-session/src/connection/extended.rs @@ -149,6 +149,7 @@ impl Connection { return Err(aborted().into()); } if query::is_query(node) { + self.settings.borrow_mut().take_snapshot(); let given = Params { types: params.clone(), variable: true }; let made = query::plan(node, &self.reader(), &given).map_err(|error| Failed { position: error.position().map(|at| character_position(&text, at)), @@ -162,7 +163,7 @@ impl Connection { plan = Some(made); } else { utility::check(node, &text)?; - columns = utility::columns(node, &self.settings)? + columns = utility::columns(node, &self.settings.borrow())? .map(|names| names.into_iter().map(Column::text).collect()); } } @@ -192,6 +193,9 @@ impl Connection { if self.transaction.failed() && !(exits && statement.params.is_empty()) { return Err(aborted().into()); } + if statement.plan.is_some() || !statement.params.is_empty() { + self.settings.borrow_mut().take_snapshot(); + } self.portals.make_room(bind.portal)?; let formats = values.formats; let mut params = Vec::new(); @@ -235,7 +239,7 @@ impl Connection { /// `bind_param_error_callback`: the context line of an error in the value of parameter `number`. `text` is the value in the text format, which the line shows in quotes with at most `log_parameter_max_length_on_error` bytes. fn param_context(&self, portal: &[u8], number: usize, text: Option<&str>) -> String { let max = match guc::find("log_parameter_max_length_on_error") - .map(|parameter| self.settings.setting(parameter)) + .map(|parameter| self.settings.borrow().setting(parameter)) { Some(Setting::Int(max)) => max, _ => 0, @@ -320,12 +324,13 @@ impl Connection { let mut tag = None; if ready { let done = if let Some(plan) = &statement.plan { + self.settings.borrow_mut().take_snapshot(); let rows = query::run(plan, ¶ms, &self.reader(), &formats)?; Done::Rows { columns: Vec::new(), rows, tag: CommandTag::Select } } else { let mut notices = Vec::new(); let mut cx = Context { - settings: &mut self.settings, + settings: self.settings.get_mut(), transaction: &mut self.transaction, user: &self.user, notices: &mut notices, diff --git a/crates/rupg-session/src/guc/settings.rs b/crates/rupg-session/src/guc/settings.rs index 66b31f3..734b6af 100644 --- a/crates/rupg-session/src/guc/settings.rs +++ b/crates/rupg-session/src/guc/settings.rs @@ -220,6 +220,8 @@ pub struct Settings { reported: Arc>, generation: u64, superuser: bool, + /// `FirstSnapshotSet`: a statement of the current transaction took a snapshot, so the isolation level and the deferrable mode cannot change. + snapshot: bool, } impl Settings { @@ -235,6 +237,7 @@ impl Settings { reported: Arc::default(), generation: 0, superuser, + snapshot: false, } } @@ -480,6 +483,7 @@ impl Settings { }; let setting = check::check(parameter, setting, ¤t, &reset)?; let shown = parameter.show(&setting); + self.check_transaction(parameter, &shown, ¤t)?; if super::refuses(parameter, &shown) { return Err(Error::new( SqlState::FEATURE_NOT_SUPPORTED, @@ -492,6 +496,36 @@ impl Settings { Ok(setting) } + /// `check_transaction_isolation`, `check_transaction_deferrable` and `check_transaction_read_only`: after the first snapshot of a transaction, its isolation level cannot change, its deferrable mode cannot be set, and it cannot become read-write. + fn check_transaction( + &self, + parameter: &Parameter, + shown: &str, + current: &str, + ) -> Result<(), Error> { + if !self.snapshot { + return Ok(()); + } + let message = match parameter.name { + "transaction_isolation" if shown != current => { + "SET TRANSACTION ISOLATION LEVEL must be called before any query" + } + "transaction_deferrable" => { + "SET TRANSACTION [NOT] DEFERRABLE must be called before any query" + } + "transaction_read_only" if shown == "off" && current == "on" => { + "transaction read-write mode must be set before any query" + } + _ => return Ok(()), + }; + Err(Error::new(SqlState::ACTIVE_SQL_TRANSACTION, message)) + } + + /// `GetTransactionSnapshot` in a transaction: a statement that analyzes or runs a query takes the first snapshot of the transaction, and [`Settings::start_transaction`] clears it. + pub fn take_snapshot(&mut self) { + self.snapshot = true; + } + /// Makes the slot of a parameter if the session did not change it before. fn slot(&mut self, key: &str, parameter: Option<&'static Parameter>, name: &str) -> &mut Slot { let slots = Arc::make_mut(&mut self.slots); @@ -571,6 +605,7 @@ impl Settings { /// `StartTransaction`: a new transaction takes `transaction_isolation`, `transaction_read_only` and `transaction_deferrable` from their defaults, or from the transaction before it after `AND CHAIN`. PostgreSQL sets the variables of these parameters directly, so the change is not in the stack of the transaction and the end of the transaction does not restore it. pub fn start_transaction(&mut self, chained: Option) { + self.snapshot = false; for (i, (name, default)) in CHARACTERISTICS.into_iter().enumerate() { let (Some(parameter), Some(default)) = (find(name), find(default)) else { continue }; let setting = match &chained { diff --git a/crates/rupg-session/src/query.rs b/crates/rupg-session/src/query.rs index 5310994..b42ccc5 100644 --- a/crates/rupg-session/src/query.rs +++ b/crates/rupg-session/src/query.rs @@ -2,6 +2,9 @@ //! //! [`Reader`] gives the analyzer and the functions the settings, the user, the database, the time zone, the times of the transaction and the statement, and the process ID. The time zone rules of the IANA data come later, so the engine has the zones with one offset: a number of hours, an interval, a POSIX zone without daylight saving time, the names of UTC and GMT, and `Etc/GMT+N`. A function that needs another zone gives `0A000`, so it never gives a wrong time. +use std::cell::{Ref, RefCell}; +use std::rc::Rc; + use rupg_analyze::{Env, Params}; use rupg_common::{Error, Result, SqlState}; use rupg_exec::Plan; @@ -13,8 +16,9 @@ use rupg_types::{ }; use rupg_wire::Field; -use crate::guc::{self, Settings, Zone}; +use crate::guc::{self, Action, Origin, Settings, Zone}; use crate::param::type_error; +use crate::utility; /// The schemas of a new database, from `pg_namespace.dat`. const SCHEMAS: [(&str, u32); 3] = [("pg_catalog", 11), ("pg_toast", 99), ("public", 2200)]; @@ -67,30 +71,34 @@ pub(crate) fn now(clock: &dyn Clock) -> i64 { i64::try_from(clock.wall_us()).unwrap_or(i64::MAX).saturating_sub(EPOCH_SHIFT_US) } -/// What a statement reads of the session. +/// What a statement reads of the session, and the settings that `set_config` changes. #[derive(Debug)] pub(crate) struct Reader<'a> { - pub(crate) settings: &'a Settings, + settings: &'a RefCell, pub(crate) user: &'a str, pub(crate) database: &'a str, pub(crate) transaction_start: i64, pub(crate) statement_start: i64, pub(crate) clock: &'a dyn Clock, pub(crate) pid: i32, - /// The zone of `TimeZone`, or the name of a zone whose rules the engine does not have yet. - zone: std::result::Result, + /// The text of `TimeZone` and its zone, or the name of a zone whose rules the engine does not have yet. A call of `set_config` can change the setting, so the zone is made again when the text changes. + zone: RefCell<(String, ZoneOf)>, } +/// The zone of a `TimeZone` value, or the name of a zone whose rules the engine does not have yet. +type ZoneOf = std::result::Result, String>; + impl<'a> Reader<'a> { pub(crate) fn new( - settings: &'a Settings, + settings: &'a RefCell, user: &'a str, database: &'a str, times: (i64, i64), clock: &'a dyn Clock, pid: i32, ) -> Reader<'a> { - let name = settings.get("TimeZone").unwrap_or_else(|| "UTC".to_owned()); + let name = settings.borrow().get("TimeZone").unwrap_or_else(|| "UTC".to_owned()); + let zone = fixed_zone(&name).map(Rc::new); Reader { settings, user, @@ -99,13 +107,18 @@ impl<'a> Reader<'a> { statement_start: times.1, clock, pid, - zone: fixed_zone(&name), + zone: RefCell::new((name, zone)), } } + /// The settings of the session. + pub(crate) fn settings(&self) -> Ref<'_, Settings> { + self.settings.borrow() + } + /// The schemas of `search_path` that exist, in order, without the implicit schemas, as `recomputeNamespacePath` finds them. `$user` names the schema of the user, and `pg_temp` the temporary schema, which do not exist yet. fn path(&self) -> Vec<(&'static str, u32)> { - let text = self.settings.get("search_path").unwrap_or_default(); + let text = self.settings().get("search_path").unwrap_or_default(); let mut path: Vec<(&'static str, u32)> = Vec::new(); for name in guc::split_identifiers(&text, ',').unwrap_or_default() { let name = if name == "$user" { self.user.to_owned() } else { name }; @@ -163,11 +176,11 @@ fn date_format(text: &str) -> DateFormat { impl rupg_func::Session for Reader<'_> { fn date_format(&self) -> DateFormat { - self.settings.get("DateStyle").map_or(DateFormat::ISO_MDY, |text| date_format(&text)) + self.settings().get("DateStyle").map_or(DateFormat::ISO_MDY, |text| date_format(&text)) } fn interval_style(&self) -> IntervalStyle { - match self.settings.get("IntervalStyle").as_deref() { + match self.settings().get("IntervalStyle").as_deref() { Some("postgres_verbose") => IntervalStyle::PostgresVerbose, Some("sql_standard") => IntervalStyle::SqlStandard, Some("iso_8601") => IntervalStyle::Iso8601, @@ -175,9 +188,15 @@ impl rupg_func::Session for Reader<'_> { } } - fn zone(&self) -> Result<&dyn TimeZone> { - match &self.zone { - Ok(zone) => Ok(zone), + fn zone(&self) -> Result> { + let name = self.settings().get("TimeZone").unwrap_or_else(|| "UTC".to_owned()); + let mut zone = self.zone.borrow_mut(); + if zone.0 != name { + let made = fixed_zone(&name).map(Rc::new); + *zone = (name, made); + } + match &zone.1 { + Ok(zone) => Ok(Rc::clone(zone) as Rc), Err(name) => Err(Error::new( SqlState::FEATURE_NOT_SUPPORTED, format!("the time zone \"{name}\" is not supported yet"), @@ -187,18 +206,18 @@ impl rupg_func::Session for Reader<'_> { } fn extra_float_digits(&self) -> i32 { - self.settings.get("extra_float_digits").and_then(|v| v.parse().ok()).unwrap_or(1) + self.settings().get("extra_float_digits").and_then(|v| v.parse().ok()).unwrap_or(1) } fn bytea_output(&self) -> ByteaOutput { - match self.settings.get("bytea_output").as_deref() { + match self.settings().get("bytea_output").as_deref() { Some("escape") => ByteaOutput::Escape, _ => ByteaOutput::Hex, } } fn array_nulls(&self) -> bool { - self.settings.get("array_nulls").as_deref() != Some("off") + self.settings().get("array_nulls").as_deref() != Some("off") } fn transaction_start(&self) -> i64 { @@ -238,7 +257,17 @@ impl rupg_func::Session for Reader<'_> { } fn setting(&self, name: &str) -> Option { - self.settings.get(name) + self.settings().get(name) + } + + fn set_setting(&self, name: &str, value: Option<&str>, local: bool) -> Result { + if let Some(value) = value { + utility::check_role(name, value, self.user)?; + } + let action = if local { Action::Local } else { Action::Set }; + let mut settings = self.settings.borrow_mut(); + settings.set(name, value, action, Origin::Statement)?; + Ok(settings.show(name)?.1) } } diff --git a/crates/rupg-session/src/utility.rs b/crates/rupg-session/src/utility.rs index 53b4b88..8cedce4 100644 --- a/crates/rupg-session/src/utility.rs +++ b/crates/rupg-session/src/utility.rs @@ -252,21 +252,20 @@ fn assign( action: Action, ) -> Result<(), Error> { if let Some(value) = value { - let parameter = guc::find(name); - match parameter.map(|parameter| parameter.name) { - // check_role and check_session_authorization: the session user is the only role yet. - Some("role") if value != "none" && value != cx.user => { - return Err(no_role(value)); - } - Some("session_authorization") if value != cx.user => { - return Err(no_role(value)); - } - _ => {} - } + check_role(name, value, cx.user)?; } cx.settings.set(name, value, action, Origin::Statement) } +/// `check_role` and `check_session_authorization` for a value of `role` or `session_authorization`. The session user is the only role yet. +pub(crate) fn check_role(name: &str, value: &str, user: &str) -> Result<(), Error> { + match guc::find(name).map(|parameter| parameter.name) { + Some("role") if value != "none" && value != user => Err(no_role(value)), + Some("session_authorization") if value != user => Err(no_role(value)), + _ => Ok(()), + } +} + fn no_role(name: &str) -> Error { Error::new(SqlState::INVALID_PARAMETER_VALUE, format!("role \"{name}\" does not exist")) }