From d3293977f5687d6a52781178531ca198b8337122 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:08:34 +0700 Subject: [PATCH 1/2] Add the changelog entry for #441 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c7881e1..915913bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All changes that a user can see are in this file. The version is 0.M.patch, wher - `EXPLAIN` in the text, JSON, XML and YAML forms, with a planner that makes the plan of a query with the costs of PostgreSQL (#439). - `ALTER DATABASE ... SET` and `RESET`, with the settings applied at the start of each session of the database and shown in `pg_db_role_setting` (#440). +- `COPY ... FROM 'file'`, which reads a file of the server, for a superuser (#441). ## 0.1.51 (2026-10-20) From ee6a455a5f05e61f2e0352d20accff99f736fc08 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:12:42 +0700 Subject: [PATCH 2/2] Run VACUUM and ANALYZE as checks --- crates/rupg-analyze/src/ddl/copy.rs | 2 +- crates/rupg-analyze/src/ddl/lock.rs | 2 +- crates/rupg-analyze/src/ddl/mod.rs | 2 + crates/rupg-analyze/src/ddl/vacuum.rs | 81 ++++++++ crates/rupg-analyze/src/lib.rs | 2 +- crates/rupg-session/src/connection.rs | 10 + .../rupg-session/src/connection/databases.rs | 4 +- .../rupg-session/src/connection/extended.rs | 9 + crates/rupg-session/src/connection/vacuum.rs | 193 ++++++++++++++++++ .../rupg-session/src/connection/vacuum.test | 170 +++++++++++++++ crates/rupg-session/src/guc.rs | 2 +- crates/rupg-session/src/utility.rs | 3 +- 12 files changed, 473 insertions(+), 7 deletions(-) create mode 100644 crates/rupg-analyze/src/ddl/vacuum.rs create mode 100644 crates/rupg-session/src/connection/vacuum.rs create mode 100644 crates/rupg-session/src/connection/vacuum.test diff --git a/crates/rupg-analyze/src/ddl/copy.rs b/crates/rupg-analyze/src/ddl/copy.rs index 6cdfbe41..80ec6db6 100644 --- a/crates/rupg-analyze/src/ddl/copy.rs +++ b/crates/rupg-analyze/src/ddl/copy.rs @@ -243,7 +243,7 @@ pub fn attnums(columns: &[String], names: &[String], rel: Option<&str>) -> Resul } /// The names of the user columns of a relation of the catalog or of the built-in catalog. -fn relation_columns(env: &dyn Env, relid: u32) -> Vec { +pub(super) fn relation_columns(env: &dyn Env, relid: u32) -> Vec { if let Some(rel) = env.catalog().and_then(|c| c.relation(relid)) { return rel.columns.iter().filter(|c| !c.dropped).map(|c| c.name.clone()).collect(); } diff --git a/crates/rupg-analyze/src/ddl/lock.rs b/crates/rupg-analyze/src/ddl/lock.rs index e18cf74c..b6145ec5 100644 --- a/crates/rupg-analyze/src/ddl/lock.rs +++ b/crates/rupg-analyze/src/ddl/lock.rs @@ -192,7 +192,7 @@ pub(crate) fn relkind(env: &dyn Env, relid: u32) -> u8 { } /// The name of a relation of the catalog or of the built-in catalog. -fn class_name(env: &dyn Env, relid: u32) -> String { +pub(super) fn class_name(env: &dyn Env, relid: u32) -> String { match env.catalog().and_then(|c| c.relation(relid)) { Some(rel) => rel.name.clone(), None => builtin::named_by_oid(Named::Class, relid) diff --git a/crates/rupg-analyze/src/ddl/mod.rs b/crates/rupg-analyze/src/ddl/mod.rs index eafe9658..fedd1884 100644 --- a/crates/rupg-analyze/src/ddl/mod.rs +++ b/crates/rupg-analyze/src/ddl/mod.rs @@ -32,6 +32,7 @@ mod tests; mod truncate; mod tsearch; mod type_change; +mod vacuum; mod view; pub use copy::{ @@ -44,6 +45,7 @@ pub(crate) use lock::relkind; pub use lock::{Allowed, lock_table}; pub use rule::{RuleTree, rule_tree}; pub(crate) use table::check_attribute_type; +pub use vacuum::vacuum_relations; use rupg_catalog::{Catalog, ObjRef, PG_CLASS, PG_TYPE, RelKind}; use rupg_common::{Error, Result, SqlState}; diff --git a/crates/rupg-analyze/src/ddl/vacuum.rs b/crates/rupg-analyze/src/ddl/vacuum.rs new file mode 100644 index 00000000..d7c8125c --- /dev/null +++ b/crates/rupg-analyze/src/ddl/vacuum.rs @@ -0,0 +1,81 @@ +//! The relations of `VACUUM` and `ANALYZE`, as `expand_vacuum_rel` and `vacuum_rel` of `vacuum.c` and `analyze_rel` of `analyze.c` check them. rupg keeps no dead row versions and no statistics yet, so the statement checks the names and changes nothing. + +use rupg_common::{Error, Result, SqlState}; +use rupg_sql::nodes::{List, Node}; + +use super::copy::relation_columns; +use super::lock::{class_name, lookup, relkind}; +use crate::{Analyzer, Env, Params}; + +/// The OID of `pg_statistic`, which `analyze_rel` skips. +const PG_STATISTIC: u32 = 2619; + +/// `vacuum` for the relations `rels` of a `VacuumStmt`, after `ExecVacuum` has read the options. `vacuum` is true for `VACUUM`, and `analyze` is true for `ANALYZE` or for `VACUUM` with the option `ANALYZE`. The warnings for the relations that the statement skips go to `warnings`, also when a later relation gives an error. +/// +/// # Errors +/// +/// The errors of `RangeVarGetRelid` for a name, and `42703` and `42701` for a column list of `ANALYZE` with a name that is not a column or that the list gives twice. +pub fn vacuum_relations( + rels: &List, + env: &dyn Env, + vacuum: bool, + analyze: bool, + warnings: &mut Vec, +) -> Result<()> { + let params = Params::default(); + let an = Analyzer::new(env, ¶ms); + let mut found = Vec::new(); + for node in rels.iter().flatten() { + let Node::VacuumRelation(vrel) = node else { continue }; + let Some(rv) = &vrel.relation else { continue }; + found.push((lookup(&an, rv)?, &vrel.va_cols)); + } + for (relid, va_cols) in found { + let kind = relkind(env, relid); + let name = class_name(env, relid); + if vacuum && !matches!(kind, b'r' | b'm' | b't' | b'p') { + warnings.push(skipping(&name, "vacuum")); + continue; + } + if !analyze || relid == PG_STATISTIC { + continue; + } + check_columns(env, relid, &name, va_cols)?; + if !vacuum && !matches!(kind, b'r' | b'm' | b'p') { + warnings.push(skipping(&name, "analyze")); + } + } + Ok(()) +} + +/// `validate_va_cols_list`: each name of the list is a user column of the relation, and no name comes twice. +fn check_columns(env: &dyn Env, relid: u32, rel: &str, va_cols: &List) -> Result<()> { + let columns = relation_columns(env, relid); + let mut seen = Vec::new(); + for node in va_cols.iter().flatten() { + let Node::String(name) = node else { continue }; + let name: &str = name; + let Some(at) = columns.iter().position(|c| c == name) else { + return Err(Error::new( + SqlState::UNDEFINED_COLUMN, + format!("column \"{name}\" of relation \"{rel}\" does not exist"), + )); + }; + if seen.contains(&at) { + return Err(Error::new( + SqlState::DUPLICATE_COLUMN, + format!("column \"{name}\" of relation \"{rel}\" appears more than once"), + )); + } + seen.push(at); + } + Ok(()) +} + +/// The warning of `vacuum_rel` or `analyze_rel` for a relation that is not a table. +fn skipping(name: &str, what: &str) -> Error { + Error::new( + SqlState::WARNING, + format!("skipping \"{name}\" --- cannot {what} non-tables or special system tables"), + ) +} diff --git a/crates/rupg-analyze/src/lib.rs b/crates/rupg-analyze/src/lib.rs index 316c3144..bccc8c01 100644 --- a/crates/rupg-analyze/src/lib.rs +++ b/crates/rupg-analyze/src/lib.rs @@ -58,7 +58,7 @@ pub use cte::{Cte, CteRef}; pub use ddl::{ Allowed, Body, CopyRelation, CopyTarget, Defined, Message, RuleTree, Transform, attnums, copy_filter, copy_insert, copy_relation, copy_target, define, function_body, is_definition, - lock_table, rewrite_insert, rule_tree, + lock_table, rewrite_insert, rule_tree, vacuum_relations, }; pub use expr::{ Aggref, BoolOp, BoolTest, Case, CastForm, Expr, ExprKind, Func, FuncForm, GroupingFunc, IsJson, diff --git a/crates/rupg-session/src/connection.rs b/crates/rupg-session/src/connection.rs index aa82aa9b..b29caed9 100644 --- a/crates/rupg-session/src/connection.rs +++ b/crates/rupg-session/src/connection.rs @@ -45,6 +45,7 @@ mod prepare; mod rewrite; mod savepoint; mod upsert; +mod vacuum; mod writes; pub(crate) use activity::Activity; @@ -1301,6 +1302,9 @@ impl Connection { } else if let Node::AlterDatabaseSetStmt(stmt) = node { drop(self.portals.close(b"")); self.alter_database_set(stmt) + } else if let Node::VacuumStmt(stmt) = node { + drop(self.portals.close(b"")); + self.vacuum(stmt, &mut notices) } else if let Node::DeclareCursorStmt(stmt) = node { drop(self.portals.close(b"")); self.declare(stmt, text, None, &[], &mut notices) @@ -3493,6 +3497,12 @@ mod tests { big_stack(|| script(include_str!("connection/create_database.test"))); } + /// `VACUUM` and `ANALYZE`: the options and their checks, the column lists, a view, a sequence, an index and a view of the catalog that the statement skips, names that do not exist, a catalog, and a transaction block. `connection/vacuum.test` is the output of PostgreSQL 19 for the same script in a new database. + #[test] + fn vacuum() { + big_stack(|| script(include_str!("connection/vacuum.test"))); + } + /// `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() { diff --git a/crates/rupg-session/src/connection/databases.rs b/crates/rupg-session/src/connection/databases.rs index f001e697..65a5be0a 100644 --- a/crates/rupg-session/src/connection/databases.rs +++ b/crates/rupg-session/src/connection/databases.rs @@ -247,8 +247,8 @@ fn not_supported(what: String) -> Error { } impl Connection { - /// `PreventInTransactionBlock` for `CREATE DATABASE` and `DROP DATABASE`. The caller commits the statement at once, also in the extended protocol. - fn prevent_block(&self, statement: &str) -> Result<(), Error> { + /// `PreventInTransactionBlock` for `CREATE DATABASE`, `DROP DATABASE` and `VACUUM`. The caller commits the statement at once, also in the extended protocol. + pub(super) fn prevent_block(&self, statement: &str) -> Result<(), Error> { if self.transaction.in_block() { return Err(Error::new( SqlState::ACTIVE_SQL_TRANSACTION, diff --git a/crates/rupg-session/src/connection/extended.rs b/crates/rupg-session/src/connection/extended.rs index ed14ef31..0d048494 100644 --- a/crates/rupg-session/src/connection/extended.rs +++ b/crates/rupg-session/src/connection/extended.rs @@ -587,6 +587,15 @@ impl Connection { position: error.position().map(|at| character_position(&statement.text, at)), error, })? + } else if let Node::VacuumStmt(stmt) = node { + let mut notices = Vec::new(); + let result = self.vacuum(stmt, &mut notices); + self.notices(&mut notices, &statement.text, &[], out); + immediate_commit = stmt.is_vacuumcmd; + result.map_err(|error| Failed { + position: error.position().map(|at| character_position(&statement.text, at)), + error, + })? } else if let Node::AlterDatabaseSetStmt(stmt) = node { self.alter_database_set(stmt)? } else { diff --git a/crates/rupg-session/src/connection/vacuum.rs b/crates/rupg-session/src/connection/vacuum.rs new file mode 100644 index 00000000..36d82460 --- /dev/null +++ b/crates/rupg-session/src/connection/vacuum.rs @@ -0,0 +1,193 @@ +//! `VACUUM` and `ANALYZE`, as `ExecVacuum` and `vacuum` of `vacuum.c` run them: the options, the checks of the options and of the relations, and the warnings for the relations that the statement skips. rupg keeps no dead row versions and no statistics yet, so the statement changes nothing. + +use rupg_common::{Error, SqlState}; +use rupg_sql::nodes::{DefElem, Node, VacuumStmt}; +use rupg_wire::CommandTag; + +use super::copy::{def_boolean, def_string}; +use super::{Connection, Done}; +use crate::guc; +use crate::utility::Notice; + +/// `MIN_BAS_VAC_RING_SIZE_KB` and `MAX_BAS_VAC_RING_SIZE_KB`. +const RING_SIZE_KB: (i32, i32) = (128, 16 * 1024 * 1024); + +/// `MAX_PARALLEL_WORKER_LIMIT`. +const MAX_PARALLEL_WORKERS: i32 = 1024; + +/// The options of `VACUUM` and `ANALYZE` that the checks of `ExecVacuum` read. +#[derive(Debug, Default)] +struct Parsed { + analyze: bool, + freeze: bool, + full: bool, + disable_page_skipping: bool, + process_toast: bool, + only_database_stats: bool, + /// True when a flag of `params.options` other than `VERBOSE`, `PROCESS_MAIN` and `PROCESS_TOAST` is on, for the check of `ONLY_DATABASE_STATS`. + other: bool, + workers: i32, + ring_size: Option, +} + +/// `defGetInt32`. +fn def_int(def: &DefElem) -> Result { + match def.arg { + Some(Node::Integer(n)) => Ok(n), + _ => { + let name = def.defname.as_deref().unwrap_or_default(); + Err(Error::new(SqlState::SYNTAX_ERROR, format!("{name} requires an integer value"))) + } + } +} + +/// The option list of `ExecVacuum`. +fn parse_options(stmt: &VacuumStmt) -> Result { + let command = if stmt.is_vacuumcmd { "VACUUM" } else { "ANALYZE" }; + let mut parsed = Parsed { process_toast: true, ..Parsed::default() }; + for def in stmt.options.iter().flatten() { + let Node::DefElem(def) = def else { continue }; + let name = def.defname.as_deref().unwrap_or_default(); + let at = usize::try_from(def.location).ok(); + match name { + "verbose" | "skip_locked" => { + let on = def_boolean(def)?; + parsed.other |= on && name == "skip_locked"; + } + "buffer_usage_limit" => { + let text = def_string(def)?; + let limit = guc::find("vacuum_buffer_usage_limit") + .ok_or_else(|| Error::internal("no parameter vacuum_buffer_usage_limit")); + let value = limit?.parse_int(&text); + let (min, max) = RING_SIZE_KB; + match value { + Ok(size) if size == 0 || (min..=max).contains(&size) => { + parsed.ring_size = Some(size); + } + Ok(_) | Err(_) => { + let error = Error::new( + SqlState::INVALID_PARAMETER_VALUE, + format!( + "BUFFER_USAGE_LIMIT option must be 0 or between {min} kB and {max} kB" + ), + ); + return Err(match value { + Err(Some(hint)) => error.with_hint(hint), + _ => error, + }); + } + } + } + _ if !stmt.is_vacuumcmd => return Err(unrecognized(command, name, at)), + "analyze" => parsed.analyze = def_boolean(def)?, + "freeze" => parsed.freeze = def_boolean(def)?, + "full" => parsed.full = def_boolean(def)?, + "disable_page_skipping" => parsed.disable_page_skipping = def_boolean(def)?, + "index_cleanup" => { + if def.arg.is_some() && !def_string(def)?.eq_ignore_ascii_case("auto") { + def_boolean(def)?; + } + } + "process_main" => { + def_boolean(def)?; + } + "process_toast" => parsed.process_toast = def_boolean(def)?, + "truncate" => { + def_boolean(def)?; + } + "parallel" => { + let workers = def_int(def)?; + if !(0..=MAX_PARALLEL_WORKERS).contains(&workers) { + return Err(Error::new( + SqlState::SYNTAX_ERROR, + format!("PARALLEL option must be between 0 and {MAX_PARALLEL_WORKERS}"), + ) + .with_position(at)); + } + parsed.workers = workers; + } + "skip_database_stats" => parsed.other |= def_boolean(def)?, + "only_database_stats" => parsed.only_database_stats = def_boolean(def)?, + _ => return Err(unrecognized(command, name, at)), + } + } + parsed.other |= parsed.analyze || parsed.freeze || parsed.full || parsed.disable_page_skipping; + Ok(parsed) +} + +/// The error of an option that the command does not have. +fn unrecognized(command: &str, name: &str, at: Option) -> Error { + Error::new(SqlState::SYNTAX_ERROR, format!("unrecognized {command} option \"{name}\"")) + .with_position(at) +} + +/// The checks of `ExecVacuum` on the options together. +fn check_options(stmt: &VacuumStmt, parsed: &Parsed) -> Result<(), Error> { + let not_supported = |message: &str| Error::new(SqlState::FEATURE_NOT_SUPPORTED, message); + if parsed.full && parsed.workers > 0 { + return Err(not_supported("VACUUM FULL cannot be performed in parallel")); + } + if parsed.ring_size.is_some() && parsed.full && !parsed.analyze { + return Err(not_supported("BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL")); + } + let analyze = parsed.analyze || !stmt.is_vacuumcmd; + let columns = stmt.rels.iter().flatten().any(|node| match node { + Node::VacuumRelation(vrel) => !vrel.va_cols.is_empty(), + _ => false, + }); + if !analyze && columns { + return Err(not_supported( + "ANALYZE option must be specified when a column list is provided", + )); + } + if parsed.full && parsed.disable_page_skipping { + return Err(not_supported("VACUUM option DISABLE_PAGE_SKIPPING cannot be used with FULL")); + } + if parsed.full && !parsed.process_toast { + return Err(not_supported("PROCESS_TOAST required with VACUUM FULL")); + } + if parsed.only_database_stats { + if !stmt.rels.is_empty() { + return Err(not_supported( + "ONLY_DATABASE_STATS cannot be specified with a list of tables", + )); + } + if parsed.other { + return Err(not_supported( + "ONLY_DATABASE_STATS cannot be specified with other VACUUM options", + )); + } + } + Ok(()) +} + +impl Connection { + /// `ExecVacuum`, then `vacuum`: `PreventInTransactionBlock` for `VACUUM`, and the checks of [`rupg_analyze::vacuum_relations`] on each relation of the statement. The session has only a superuser role, so the check of `vacuum_is_permitted_for_relation` always passes. + pub(super) fn vacuum( + &self, + stmt: &VacuumStmt, + notices: &mut Vec, + ) -> Result { + let parsed = parse_options(stmt)?; + check_options(stmt, &parsed)?; + let tag = if stmt.is_vacuumcmd { + self.prevent_block("VACUUM")?; + CommandTag::Vacuum + } else { + CommandTag::Analyze + }; + let analyze = parsed.analyze || !stmt.is_vacuumcmd; + let reader = self.reader(); + let mut warnings = Vec::new(); + let result = rupg_analyze::vacuum_relations( + &stmt.rels, + &reader, + stmt.is_vacuumcmd, + analyze, + &mut warnings, + ); + notices.extend(warnings.into_iter().map(Notice::warning)); + result?; + Ok(Done::Tag(tag)) + } +} diff --git a/crates/rupg-session/src/connection/vacuum.test b/crates/rupg-session/src/connection/vacuum.test new file mode 100644 index 00000000..ccce1d95 --- /dev/null +++ b/crates/rupg-session/src/connection/vacuum.test @@ -0,0 +1,170 @@ +!details +> SET TimeZone = 'UTC' +SET +> CREATE TABLE t (a int, b text) +CREATE TABLE +> CREATE VIEW v AS SELECT 1 AS x +CREATE VIEW +> CREATE SEQUENCE s +CREATE SEQUENCE +> CREATE INDEX ti ON t (a) +CREATE INDEX +> VACUUM +VACUUM +> VACUUM t +VACUUM +> VACUUM FULL t +VACUUM +> VACUUM FREEZE t +VACUUM +> VACUUM ANALYZE t +VACUUM +> VACUUM FULL FREEZE ANALYZE t (a) +VACUUM +> VACUUM (ANALYZE, FREEZE) t (a) +VACUUM +> VACUUM t (a) +ERROR 0A000 ANALYZE option must be specified when a column list is provided +> VACUUM (ANALYZE) t (a, a) +ERROR 42701 column "a" of relation "t" appears more than once +> VACUUM (ANALYZE) t (c) +ERROR 42703 column "c" of relation "t" does not exist +> VACUUM (ANALYZE) t, v +WARNING 01000 skipping "v" --- cannot vacuum non-tables or special system tables +VACUUM +> VACUUM v +WARNING 01000 skipping "v" --- cannot vacuum non-tables or special system tables +VACUUM +> VACUUM s +WARNING 01000 skipping "s" --- cannot vacuum non-tables or special system tables +VACUUM +> VACUUM ti +WARNING 01000 skipping "ti" --- cannot vacuum non-tables or special system tables +VACUUM +> VACUUM nosuch +ERROR 42P01 relation "nosuch" does not exist +> VACUUM nosuch.t +ERROR 3F000 schema "nosuch" does not exist +> VACUUM public.nosuch +ERROR 42P01 relation "public.nosuch" does not exist +> VACUUM t, nosuch +ERROR 42P01 relation "nosuch" does not exist +> VACUUM v, nosuch +ERROR 42P01 relation "nosuch" does not exist +> VACUUM (FOO) t +ERROR 42601 unrecognized VACUUM option "foo" at 9 +> VACUUM (VERBOSE false, SKIP_LOCKED) t +VACUUM +> VACUUM (PARALLEL -1) t +ERROR 42601 PARALLEL option must be between 0 and 1024 at 9 +> VACUUM (PARALLEL 2000) t +ERROR 42601 PARALLEL option must be between 0 and 1024 at 9 +> VACUUM (PARALLEL) t +ERROR 42601 parallel requires an integer value +> VACUUM (PARALLEL 'x') t +ERROR 42601 parallel requires an integer value +> VACUUM (PARALLEL 2) t +VACUUM +> VACUUM (FULL, PARALLEL 2) t +ERROR 0A000 VACUUM FULL cannot be performed in parallel +> VACUUM (BUFFER_USAGE_LIMIT '10kB') t +ERROR 22023 BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB +> VACUUM (BUFFER_USAGE_LIMIT 0) t +VACUUM +> VACUUM (BUFFER_USAGE_LIMIT '256kB') t +VACUUM +> VACUUM (BUFFER_USAGE_LIMIT '1 MB') t +VACUUM +> VACUUM (BUFFER_USAGE_LIMIT '1x') t +ERROR 22023 BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB +HINT Valid units for this parameter are "B", "kB", "MB", "GB", and "TB". +> VACUUM (BUFFER_USAGE_LIMIT '17GB') t +ERROR 22023 BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB +> VACUUM (FULL, BUFFER_USAGE_LIMIT '256kB') t +ERROR 0A000 BUFFER_USAGE_LIMIT cannot be specified for VACUUM FULL +> VACUUM (FULL, ANALYZE, BUFFER_USAGE_LIMIT '256kB') t +VACUUM +> VACUUM (FULL, DISABLE_PAGE_SKIPPING) t +ERROR 0A000 VACUUM option DISABLE_PAGE_SKIPPING cannot be used with FULL +> VACUUM (FULL, PROCESS_TOAST false) t +ERROR 0A000 PROCESS_TOAST required with VACUUM FULL +> VACUUM (ONLY_DATABASE_STATS) t +ERROR 0A000 ONLY_DATABASE_STATS cannot be specified with a list of tables +> VACUUM (ONLY_DATABASE_STATS, FREEZE) +ERROR 0A000 ONLY_DATABASE_STATS cannot be specified with other VACUUM options +> VACUUM (ONLY_DATABASE_STATS, VERBOSE false) +VACUUM +> VACUUM (INDEX_CLEANUP auto, TRUNCATE false, PROCESS_MAIN off, SKIP_DATABASE_STATS) t +VACUUM +> VACUUM (INDEX_CLEANUP on, INDEX_CLEANUP) t +VACUUM +> VACUUM (INDEX_CLEANUP maybe) t +ERROR 42601 index_cleanup requires a Boolean value +> VACUUM (ANALYZE 'yes') t +ERROR 42601 analyze requires a Boolean value +> VACUUM (ANALYZE 2) t +ERROR 42601 analyze requires a Boolean value +> ANALYZE +ANALYZE +> ANALYZE t +ANALYZE +> ANALYZE t (b) +ANALYZE +> ANALYZE t (c) +ERROR 42703 column "c" of relation "t" does not exist +> ANALYZE t (a, b, a) +ERROR 42701 column "a" of relation "t" appears more than once +> ANALYZE v +WARNING 01000 skipping "v" --- cannot analyze non-tables or special system tables +ANALYZE +> ANALYZE v (x) +WARNING 01000 skipping "v" --- cannot analyze non-tables or special system tables +ANALYZE +> ANALYZE v (y) +ERROR 42703 column "y" of relation "v" does not exist +> ANALYZE s +WARNING 01000 skipping "s" --- cannot analyze non-tables or special system tables +ANALYZE +> ANALYZE (FULL) t +ERROR 42601 unrecognized ANALYZE option "full" at 10 +> ANALYZE (SKIP_LOCKED, VERBOSE off, BUFFER_USAGE_LIMIT 0) t +ANALYZE +> ANALYZE (BUFFER_USAGE_LIMIT 5) t +ERROR 22023 BUFFER_USAGE_LIMIT option must be 0 or between 128 kB and 16777216 kB +> ANALYZE pg_class +ANALYZE +> VACUUM pg_class +VACUUM +> ANALYZE pg_statistic (nosuch) +ANALYZE +> ANALYZE pg_tables +WARNING 01000 skipping "pg_tables" --- cannot analyze non-tables or special system tables +ANALYZE +> VACUUM pg_catalog.pg_tables +WARNING 01000 skipping "pg_tables" --- cannot vacuum non-tables or special system tables +VACUUM +> ANALYZE t, v, s +WARNING 01000 skipping "v" --- cannot analyze non-tables or special system tables +WARNING 01000 skipping "s" --- cannot analyze non-tables or special system tables +ANALYZE +> BEGIN +BEGIN +> ANALYZE t +ANALYZE +> ANALYZE t, v +WARNING 01000 skipping "v" --- cannot analyze non-tables or special system tables +ANALYZE +> VACUUM t +ERROR 25001 VACUUM cannot run inside a transaction block +> ROLLBACK +ROLLBACK +> BEGIN +BEGIN +> VACUUM (ANALYZE) t +ERROR 25001 VACUUM cannot run inside a transaction block +> ROLLBACK +ROLLBACK +> SELECT 1 +fields ?column?:23:4:-1 +row 1 +SELECT 1 diff --git a/crates/rupg-session/src/guc.rs b/crates/rupg-session/src/guc.rs index 6de7fd79..15021e04 100644 --- a/crates/rupg-session/src/guc.rs +++ b/crates/rupg-session/src/guc.rs @@ -442,7 +442,7 @@ impl Parameter { } /// `parse_int` of `guc.c`: a decimal, octal or hexadecimal integer or a decimal fraction, then an optional unit. The error is the hint, if there is one. - fn parse_int(&self, text: &str) -> Result> { + pub(crate) fn parse_int(&self, text: &str) -> Result> { let (mut value, rest) = match strtol(text) { Some((value, rest)) if !rest.starts_with(['.', 'e', 'E']) => (value, rest), _ => strtod(text).ok_or(None)?, diff --git a/crates/rupg-session/src/utility.rs b/crates/rupg-session/src/utility.rs index 08ea7002..5a27fbfd 100644 --- a/crates/rupg-session/src/utility.rs +++ b/crates/rupg-session/src/utility.rs @@ -88,7 +88,8 @@ pub fn check(node: &Node, text: &str) -> Result<(), Error> { | Node::DeallocateStmt(_) | Node::CreatedbStmt(_) | Node::DropdbStmt(_) - | Node::AlterDatabaseSetStmt(_) => Ok(()), + | Node::AlterDatabaseSetStmt(_) + | Node::VacuumStmt(_) => Ok(()), _ => Err(not_supported(text)), } }