Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/rupg-exec/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ pub(crate) fn rows(
"pg_rewrite" => rewrites(table, catalog),
"pg_init_privs" => init_privs(table, catalog, session)?,
"pg_database" => databases(table, session),
"pg_db_role_setting" => db_role_settings(table, session),
"pg_ts_template" => ts_templates(table, catalog),
"pg_ts_parser" => ts_parsers(table, catalog),
"pg_ts_dict" => ts_dictionaries(table, catalog),
Expand Down Expand Up @@ -271,6 +272,21 @@ fn databases<'a>(table: &'a Table, session: &dyn Session) -> Vec<Row<'a>> {
.collect()
}

/// The rows of `pg_db_role_setting`: the settings of `ALTER DATABASE ... SET`.
fn db_role_settings<'a>(table: &'a Table, session: &dyn Session) -> Vec<Row<'a>> {
session
.db_role_settings()
.into_iter()
.map(|(database, role, settings)| {
let mut row = Row::new(table);
row.set("setdatabase", oid(database))
.set("setrole", oid(role))
.set("setconfig", text_array(&settings));
row
})
.collect()
}

fn namespaces<'a>(table: &'a Table, catalog: &Catalog) -> Vec<Row<'a>> {
catalog
.schemas()
Expand Down
4 changes: 4 additions & 0 deletions crates/rupg-func/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ pub trait Session {
fn databases(&self) -> Arc<[rupg_catalog::Database]> {
Arc::from(Vec::new())
}
/// The rows of `pg_db_role_setting`: the OID of the database, the OID of the role, and the settings, each as `name=value`. A session with no server has no rows.
fn db_role_settings(&self) -> Vec<(u32, u32, Vec<String>)> {
Vec::new()
}
/// The rows of the table of the sessions in the order of their numbers, from the snapshot of the transaction. The first call of a transaction takes the snapshot.
fn backends(&self) -> Arc<[Backend]> {
Arc::from(Vec::new())
Expand Down
11 changes: 6 additions & 5 deletions crates/rupg-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -833,15 +833,15 @@ fn start_tls(shared: &Shared, tls: &Tls, wire: &mut Wire, early: &[u8], direct:
}
}

/// The checks of `InitPostgres` after the authentication: the role and the database.
fn admit(shared: &Shared, start: &Start) -> std::result::Result<(), Error> {
/// The checks of `InitPostgres` after the authentication: the role and the database. It gives the OID of the database.
fn admit(shared: &Shared, start: &Start) -> std::result::Result<u32, Error> {
if start.user != shared.roles.superuser {
return Err(Error::new(
SqlState::INVALID_AUTHORIZATION_SPECIFICATION,
format!("role \"{}\" does not exist", start.user),
));
}
shared.store.admit(&start.database).map(drop)
shared.store.admit(&start.database)
}

/// A session in the cancel registry.
Expand Down Expand Up @@ -949,8 +949,9 @@ fn session(shared: &Shared, wire: &mut Wire, start: &Start, protocol: u32) {
return;
};
wire.out.authentication_ok();
let settings =
admit(shared, start).and_then(|()| connection::session_settings(&shared.base, start));
let settings = admit(shared, start).and_then(|database| {
connection::session_settings(&shared.base, start, &shared.store.database_settings(database))
});
let mut settings = match settings {
Ok(settings) => settings,
Err(error) => {
Expand Down
65 changes: 56 additions & 9 deletions crates/rupg-session/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,16 +246,26 @@ pub fn split_options(options: &str) -> Result<Vec<(String, String)>, Error> {
Ok(settings)
}

/// The settings of a new session: the base settings, the values that the server sets for the session, then the `options` of the startup packet and its other parameters, as `process_startup_options` applies them.
/// The settings of a new session: the base settings, the values that the server sets for the session, the settings of `ALTER DATABASE ... SET` in `database`, then the `options` of the startup packet and its other parameters, as `process_startup_options` applies them.
///
/// # Errors
///
/// A bad `options` parameter and a setting that is not valid. The server sends them as `FATAL`.
pub fn session_settings(base: &Settings, start: &Start) -> Result<Settings, Error> {
pub fn session_settings(
base: &Settings,
start: &Start,
database: &[String],
) -> Result<Settings, Error> {
let mut settings = base.clone();
settings.set_superuser(start.superuser);
settings.set_internal("is_superuser", if start.superuser { "on" } else { "off" })?;
settings.set_internal("session_authorization", &start.user)?;
// `process_settings`: a stored setting that is no longer valid gives a warning in PostgreSQL, and the session starts without it.
for item in database {
if let Some((name, value)) = item.split_once('=') {
drop(settings.set_database(name, value));
}
}
let options = match &start.options {
Some(options) => split_options(options)?,
None => Vec::new(),
Expand Down Expand Up @@ -1273,6 +1283,9 @@ impl Connection {
} else if let Node::DropdbStmt(stmt) = node {
drop(self.portals.close(b""));
self.drop_database(stmt, &mut notices)
} else if let Node::AlterDatabaseSetStmt(stmt) = node {
drop(self.portals.close(b""));
self.alter_database_set(stmt)
} else if let Node::DeclareCursorStmt(stmt) = node {
drop(self.portals.close(b""));
self.declare(stmt, text, None, &[], &mut notices)
Expand Down Expand Up @@ -1587,7 +1600,7 @@ mod tests {
/// A session with the process ID `pid` on the store of a server.
fn connect_to(store: &Arc<Store>, pid: i32) -> (Connection, Vec<String>) {
let start = start(None, &[("application_name", "test")]);
let settings = session_settings(&base_settings(&[]).unwrap(), &start).unwrap();
let settings = session_settings(&base_settings(&[]).unwrap(), &start, &[]).unwrap();
let mut connection = Connection::new(&start, settings, PROTOCOL_3_0);
connection.set_store(Arc::clone(store));
// The oracle runs the scripts on a Unix socket.
Expand Down Expand Up @@ -3465,10 +3478,44 @@ mod tests {
big_stack(|| script(include_str!("connection/create_database.test")));
}

/// A session of a database other than `postgres`.
/// `ALTER DATABASE ... SET`: the checks of the names and the values, and the rows of `pg_db_role_setting`. `connection/alter_database_set.test` is the output of PostgreSQL 19 for the same script in a new database.
#[test]
fn alter_database_set() {
big_stack(|| script(include_str!("connection/alter_database_set.test")));
}

/// A new session of a database takes the settings of `ALTER DATABASE ... SET` with the source `database`, and `RESET` goes back to them. A session of another database does not take them.
#[test]
fn database_settings() {
big_stack(|| {
let store = Arc::new(Store::new());
let (mut a, _) = connect_to(&store, 1234);
for sql in [
"CREATE DATABASE x",
"ALTER DATABASE x SET work_mem TO '16MB'",
"ALTER DATABASE x SET my.thing TO 'v'",
] {
assert!(!line_of(&mut a, sql, "").starts_with("ERROR"), "{sql}");
}
assert_eq!(line_of(&mut a, "SHOW work_mem", "row"), "row 4MB");
let mut b = connect_database(&store, 1235, "x");
assert_eq!(line_of(&mut b, "SHOW work_mem", "row"), "row 16MB");
assert_eq!(line_of(&mut b, "SHOW my.thing", "row"), "row v");
let source = "SELECT source FROM pg_settings WHERE name = 'work_mem'";
assert_eq!(line_of(&mut b, source, "row"), "row database");
line_of(&mut b, "SET work_mem TO '1MB'", "SET");
line_of(&mut b, "RESET work_mem", "RESET");
assert_eq!(line_of(&mut b, "SHOW work_mem", "row"), "row 16MB");
let mut c = connect_database(&store, 1236, "postgres");
assert_eq!(line_of(&mut c, "SHOW work_mem", "row"), "row 4MB");
});
}

/// A session of a database other than `postgres`, with the settings of `ALTER DATABASE ... SET` for the database.
fn connect_database(store: &Arc<Store>, pid: i32, database: &str) -> Connection {
let start = Start { database: database.into(), ..start(None, &[]) };
let settings = session_settings(&base_settings(&[]).unwrap(), &start).unwrap();
let stored = store.database_settings(store.admit(database).unwrap());
let settings = session_settings(&base_settings(&[]).unwrap(), &start, &stored).unwrap();
let mut connection = Connection::new(&start, settings, PROTOCOL_3_0);
connection.set_store(Arc::clone(store));
let mut out = OutBuf::new();
Expand Down Expand Up @@ -4591,12 +4638,12 @@ mod tests {
"invalid command-line argument for server process: -x"
);
let base = base_settings(&[]).unwrap();
let settings = session_settings(&base, &start(Some("-c work_mem=16MB"), &[])).unwrap();
let settings = session_settings(&base, &start(Some("-c work_mem=16MB"), &[]), &[]).unwrap();
assert_eq!(settings.get("work_mem").as_deref(), Some("16MB"));
let latin1 = session_settings(&base, &start(None, &[("client_encoding", "LATIN1")]));
let latin1 = session_settings(&base, &start(None, &[("client_encoding", "LATIN1")]), &[]);
assert_eq!(latin1.unwrap_err().state(), SqlState::FEATURE_NOT_SUPPORTED);
let ascii =
session_settings(&base, &start(None, &[("client_encoding", "sql_ascii")])).unwrap();
let ascii = session_settings(&base, &start(None, &[("client_encoding", "sql_ascii")]), &[])
.unwrap();
assert_eq!(ascii.get("client_encoding").as_deref(), Some("SQL_ASCII"));
}

Expand Down
53 changes: 53 additions & 0 deletions crates/rupg-session/src/connection/alter_database_set.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
!details
> SET TimeZone = 'UTC'
SET
> CREATE DATABASE rupg_ads_scratch
CREATE DATABASE
> ALTER DATABASE rupg_ads_nosuch SET work_mem = '1MB'
ERROR 3D000 database "rupg_ads_nosuch" does not exist
> ALTER DATABASE rupg_ads_scratch SET nosuch = 1
ERROR 42704 unrecognized configuration parameter "nosuch"
> ALTER DATABASE rupg_ads_scratch SET shared_buffers = '1MB'
ERROR 55P02 parameter "shared_buffers" cannot be changed without restarting the server
> ALTER DATABASE rupg_ads_scratch SET work_mem = 'abc'
ERROR 22023 invalid value for parameter "work_mem": "abc"
> ALTER DATABASE rupg_ads_scratch SET log_connections = on
ERROR 55P02 parameter "log_connections" cannot be set after connection start
> ALTER DATABASE rupg_ads_scratch SET archive_command = 'x'
ERROR 55P02 parameter "archive_command" cannot be changed now
> ALTER DATABASE rupg_ads_scratch SET my.thing = 'x'
ALTER DATABASE
> ALTER DATABASE rupg_ads_scratch RESET my.other
ALTER DATABASE
> ALTER DATABASE rupg_ads_scratch RESET nosuch
ERROR 42704 unrecognized configuration parameter "nosuch"
> ALTER DATABASE rupg_ads_scratch SET WORK_MEM TO '16MB'
ALTER DATABASE
> ALTER DATABASE rupg_ads_scratch SET lc_messages TO 'C'
ALTER DATABASE
> ALTER DATABASE rupg_ads_scratch SET search_path TO a, "B"
ALTER DATABASE
> ALTER DATABASE rupg_ads_scratch SET work_mem TO '32MB'
ALTER DATABASE
> ALTER DATABASE rupg_ads_scratch SET DateStyle FROM CURRENT
ALTER DATABASE
> SELECT setconfig FROM pg_db_role_setting s JOIN pg_database d ON d.oid = s.setdatabase WHERE d.datname = 'rupg_ads_scratch'
fields setconfig@pg_db_role_setting.3:1009:-1:-1
row {my.thing=x,work_mem=32MB,lc_messages=C,"search_path=a, \"B\"","DateStyle=ISO, MDY"}
SELECT 1
> ALTER DATABASE rupg_ads_scratch RESET work_mem
ALTER DATABASE
> ALTER DATABASE rupg_ads_scratch SET lc_messages TO DEFAULT
ALTER DATABASE
> SELECT setconfig FROM pg_db_role_setting s JOIN pg_database d ON d.oid = s.setdatabase WHERE d.datname = 'rupg_ads_scratch'
fields setconfig@pg_db_role_setting.3:1009:-1:-1
row {my.thing=x,"search_path=a, \"B\"","DateStyle=ISO, MDY"}
SELECT 1
> ALTER DATABASE rupg_ads_scratch RESET ALL
ALTER DATABASE
> SELECT count(*) FROM pg_db_role_setting s JOIN pg_database d ON d.oid = s.setdatabase WHERE d.datname = 'rupg_ads_scratch'
fields count:20:8:-1
row 0
SELECT 1
> DROP DATABASE rupg_ads_scratch
DROP DATABASE
56 changes: 53 additions & 3 deletions crates/rupg-session/src/connection/databases.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
//! `CREATE DATABASE` and `DROP DATABASE`: the checks of `createdb` and `dropdb` in the order of PostgreSQL, on the rows of `pg_database` that the store keeps. The engine has the encoding `UTF8` and the locale `C` only, so a new database must have them. The checks of PostgreSQL come first, and then the error `0A000` for an encoding or a locale that the engine does not have.
//! `CREATE DATABASE`, `ALTER DATABASE ... SET` and `DROP DATABASE`: the checks of `createdb` and `dropdb` in the order of PostgreSQL, on the rows of `pg_database` that the store keeps. The engine has the encoding `UTF8` and the locale `C` only, so a new database must have them. The checks of PostgreSQL come first, and then the error `0A000` for an encoding or a locale that the engine does not have.

use rupg_catalog::Database;
use rupg_common::{Error, SqlState};
use rupg_func::{char_to_encoding, encoding_to_char};
use rupg_pgcatalog::builtin;
use rupg_sql::Severity;
use rupg_sql::nodes::{CreatedbStmt, DefElem, DropdbStmt, Node};
use rupg_sql::nodes::{
AlterDatabaseSetStmt, CreatedbStmt, DefElem, DropdbStmt, Node, VariableSetKind,
};
use rupg_wire::CommandTag;

use super::copy::{def_boolean, def_string};
use super::{Connection, Done};
use crate::store::NewDatabase;
use crate::utility::Notice;
use crate::utility::{self, Notice};

/// `FirstNormalObjectId`.
const FIRST_NORMAL_OBJECT_ID: u32 = 16384;
Expand Down Expand Up @@ -687,6 +689,54 @@ impl Connection {
self.store.drop_database(&row, force, self.activity.number)?;
Ok(Done::Tag(CommandTag::DropDatabase))
}

/// `AlterDatabaseSet` and `AlterSetting`: the change of the stored settings of a database, which a new session of the database applies. `GUCArrayAdd` puts a new value in place of the value of the same parameter, and `GUCArrayDelete` and `RESET ALL` remove values.
pub(super) fn alter_database_set(
&mut self,
stmt: &AlterDatabaseSetStmt,
) -> Result<Done, Error> {
let name = stmt.dbname.as_deref().unwrap_or_default();
let Some(row) = self.store.database(name) else {
return Err(Error::new(
SqlState::UNDEFINED_DATABASE,
format!("database \"{name}\" does not exist"),
));
};
if !self.superuser() && row.owner != self.role {
return Err(Error::new(
SqlState::INSUFFICIENT_PRIVILEGE,
format!("must be owner of database {name}"),
));
}
let Some(set) = &stmt.setstmt else {
return Err(Error::internal("ALTER DATABASE SET with no setting"));
};
let parameter = set.name.as_deref().unwrap_or_default();
// `ExtractSetVariableArgs`.
let value = match set.kind {
VariableSetKind::VAR_SET_VALUE => utility::flatten(parameter, &set.args)?,
VariableSetKind::VAR_SET_CURRENT => Some(self.settings.borrow().show(parameter)?.1),
_ => None,
};
if set.kind == VariableSetKind::VAR_RESET_ALL {
self.store.change_database_settings(row.oid, Vec::clear);
return Ok(Done::Tag(CommandTag::AlterDatabase));
}
let stored = self.settings.borrow().check_stored(parameter, value.as_deref())?;
let prefix = format!("{stored}=");
self.store.change_database_settings(row.oid, |settings| {
let at = settings.iter().position(|item| item.starts_with(&prefix));
match (value, at) {
(Some(value), Some(at)) => settings[at] = format!("{prefix}{value}"),
(Some(value), None) => settings.push(format!("{prefix}{value}")),
(None, Some(at)) => {
settings.remove(at);
}
(None, None) => {}
}
});
Ok(Done::Tag(CommandTag::AlterDatabase))
}
}

#[cfg(test)]
Expand Down
2 changes: 2 additions & 0 deletions crates/rupg-session/src/connection/extended.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,8 @@ impl Connection {
position: error.position().map(|at| character_position(&statement.text, at)),
error,
})?
} else if let Node::AlterDatabaseSetStmt(stmt) = node {
self.alter_database_set(stmt)?
} else {
let mut notices = Vec::new();
let ts_config = self.ts_config(node)?;
Expand Down
44 changes: 44 additions & 0 deletions crates/rupg-session/src/guc/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub enum Source {
File,
/// A value of the command line of the server.
Argument,
/// A value that `ALTER DATABASE ... SET` stored for the database of the session.
Database,
/// A value of the startup packet.
Client,
/// A value that the server sets over the others at start, such as `config_file`.
Expand All @@ -40,6 +42,7 @@ impl Source {
Source::Default => "default",
Source::File => "configuration file",
Source::Argument => "command line",
Source::Database => "database",
Source::Client => "client",
Source::Override => "override",
Source::Session => "session",
Expand Down Expand Up @@ -467,6 +470,47 @@ impl Settings {
Ok(())
}

/// A value that `ALTER DATABASE ... SET` stored for the database of the session, as `ApplySetting` sets it with `PGC_S_DATABASE` at the start of the session. It comes before the values of the client. A parameter that a statement cannot set is not set.
///
/// # Errors
///
/// A name that is not a parameter and not a valid placeholder name, a parameter that cannot change after the start of the session, or a value that the parameter does not take.
pub fn set_database(&mut self, name: &str, text: &str) -> Result<(), Error> {
let Some(parameter) = find(name) else {
placeholder_name(name)?;
let key = name.to_ascii_lowercase();
let value =
Value { setting: Setting::String(text.to_owned()), source: Source::Database };
let slot = self.slot(&key, None, name);
slot.current = value.clone();
slot.reset = value;
return Ok(());
};
self.allowed(parameter, Origin::Statement)?;
let setting = self.parse(parameter, text)?;
self.store_reset(parameter, Value { setting, source: Source::Database });
Ok(())
}

/// `validate_option_array_item` for `ALTER DATABASE ... SET`: the check of the name, of the context and of the value, with no change. `text` is `None` for `RESET`. It gives the name that the stored setting has, which is the name of the parameter as PostgreSQL spells it.
///
/// # Errors
///
/// A name that is not a parameter and not a valid placeholder name, a parameter that a statement cannot set, and a value that the parameter does not take.
pub fn check_stored(&self, name: &str, text: Option<&str>) -> Result<String, Error> {
let Some(parameter) = find(name) else {
if text.is_some() || !valid_custom_name(name) {
placeholder_name(name)?;
}
return Ok(name.to_owned());
};
self.allowed(parameter, Origin::Statement)?;
if let Some(text) = text {
self.parse(parameter, text)?;
}
Ok(parameter.name.to_owned())
}

/// Reads a value of a parameter with the rules of `set`, and does not keep it.
///
/// # Errors
Expand Down
Loading
Loading