From f0f1b6e4cdee39adbe73de9ae0687a557b6dbe4f Mon Sep 17 00:00:00 2001 From: tamnd Date: Tue, 13 Oct 2026 14:50:23 +0700 Subject: [PATCH] Input of aclitem --- crates/rupg-analyze/src/coerce.rs | 8 +- crates/rupg-analyze/src/lib.rs | 5 + crates/rupg-func/src/acl.rs | 312 ++++++++++++++++-- crates/rupg-func/src/io.rs | 17 +- crates/rupg-func/src/lib.rs | 4 + crates/rupg-session/src/connection.rs | 25 +- crates/rupg-session/src/connection/acl.test | 227 +++++++++++++ crates/rupg-session/src/connection/copy.rs | 17 +- .../rupg-session/src/connection/extended.rs | 22 +- crates/rupg-session/src/connection/prepare.rs | 4 +- crates/rupg-session/src/query.rs | 21 ++ 11 files changed, 612 insertions(+), 50 deletions(-) create mode 100644 crates/rupg-session/src/connection/acl.test diff --git a/crates/rupg-analyze/src/coerce.rs b/crates/rupg-analyze/src/coerce.rs index ceca7f75..02bd5923 100644 --- a/crates/rupg-analyze/src/coerce.rs +++ b/crates/rupg-analyze/src/coerce.rs @@ -203,12 +203,12 @@ impl Analyzer<'_> { Value::Text(text) => { let text = text.clone(); // The place of the literal replaces any place that the input function gives, as `pcb_error_callback` does. - self.env.input(base, &text, input_typmod).map_err(|e| { - match expr.location { + self.env.input_at(base, &text, input_typmod, expr.location).map_err( + |e| match expr.location { Some(at) => e.with_position(Some(at)), None => e, - } - })? + }, + )? } _ => Value::Null, }; diff --git a/crates/rupg-analyze/src/lib.rs b/crates/rupg-analyze/src/lib.rs index 82136bfb..f024b9cb 100644 --- a/crates/rupg-analyze/src/lib.rs +++ b/crates/rupg-analyze/src/lib.rs @@ -72,6 +72,11 @@ pub trait Env { fn database(&self) -> String; /// The input function of the type: the value of a string literal. The settings of the session, such as `DateStyle`, apply. fn input(&self, ty: u32, text: &str, typmod: i32) -> Result; + /// [`Env::input`] for a literal at this place in the query text, so that a warning of the input function points at the literal. + fn input_at(&self, ty: u32, text: &str, typmod: i32, at: Option) -> Result { + let _ = at; + self.input(ty, text, typmod) + } /// The objects that the user made, or `None` when the session has no catalog. fn catalog(&self) -> Option<&rupg_catalog::Catalog> { None diff --git a/crates/rupg-func/src/acl.rs b/crates/rupg-func/src/acl.rs index 922d67cc..b471a877 100644 --- a/crates/rupg-func/src/acl.rs +++ b/crates/rupg-func/src/acl.rs @@ -1,4 +1,4 @@ -//! The access privilege inquiry functions: `has_table_privilege` and the other `has_*_privilege` functions, `pg_has_role`, and `acldefault` and `aclexplode`, which give the items of an `aclitem[]` value. +//! The access privilege inquiry functions: `has_table_privilege` and the other `has_*_privilege` functions, `pg_has_role`, and `acldefault` and `aclexplode`, which give the items of an `aclitem[]` value. The module also has the input of `aclitem`, `makeaclitem`, `aclcontains` and the hash functions of `aclitem`. //! //! A port of the functions of `src/backend/utils/adt/acl.c` and of the checks of `aclchk.c` that they call. The privileges are the `aclitem[]` values of the static rows, which have the privileges that `initdb` gives. Each function has forms with a role name or OID first, or no role for the current user, and with the object as text or as an OID. The form of a call is clear from its values: a name is text and an OID is an OID. A form with the OID of an object that does not exist gives null. @@ -8,7 +8,7 @@ use rupg_pgcatalog::builtin::{self, Owned}; use rupg_types::{RegKind, Value, qualified_name_list}; use crate::reg; -use crate::{Call, Kernel, Session, bad_value, type_error}; +use crate::{Call, Kernel, Session, bad_value, not_yet, type_error}; const ACL_INSERT: u32 = 1 << 0; const ACL_SELECT: u32 = 1 << 1; @@ -891,30 +891,14 @@ pub(crate) fn aclexplode(call: &Call<'_>, args: &[Value]) -> Result, args: &[Value]) -> Result bool { + c >= 0x80 || c.is_ascii_alphanumeric() || c == b'_' +} + +/// The text after the white space at its start. +fn skip_spaces(s: &[u8]) -> &[u8] { + let n = s.iter().take_while(|&&c| is_space(char::from(c))).count(); + &s[n..] +} + +/// `getid`: a role name after any white space, with double quotes around all or part of it or with no quotes. In quotes, `""` is a quote. It gives the name and the text after the name and any white space. +/// +/// # Errors +/// +/// `42622` for a name of `NAMEDATALEN` bytes or more. +fn getid(s: &[u8]) -> Result<(String, &[u8])> { + let mut s = skip_spaces(s); + let mut name = Vec::new(); + let mut in_quotes = false; + while let Some((&c, rest)) = s.split_first() { + if !(in_quotes || c == b'"' || is_name_byte(c)) { + break; + } + s = rest; + if c == b'"' { + if !in_quotes { + in_quotes = true; + continue; + } + if s.first() != Some(&b'"') { + in_quotes = false; + continue; + } + s = &s[1..]; + } + if name.len() >= NAMEDATALEN - 1 { + return Err(Error::new(SqlState::NAME_TOO_LONG, "identifier too long") + .with_detail(format!("Identifier must be less than {NAMEDATALEN} characters."))); + } + name.push(c); + } + Ok((String::from_utf8_lossy(&name).into_owned(), skip_spaces(s))) +} + +/// The error `22P02` of a bad `aclitem` text. +fn acl_syntax(message: impl Into) -> Error { + Error::new(SqlState::INVALID_TEXT_REPRESENTATION, message) +} + +/// The OID of a role of an `aclitem` text. In the text of a stored value, a role that does not exist shows as its OID, as `aclitemout` writes it. +/// +/// # Errors +/// +/// `42704` for a role that does not exist. +fn acl_role(name: &str, session: &dyn Session, stored: bool) -> Result { + if stored + && role_oid(name, session).is_none() + && let Ok(oid) = name.parse::() + { + return Ok(oid); + } + get_role_oid(name, session) +} + +/// `aclparse`: an item in the form `grantee=privileges/grantor`, and the text after it. The key word `group` or `user` can come before the grantee, and an empty grantee is PUBLIC. A `*` after a privilege is its grant option. An item with no grantor has the bootstrap superuser as its grantor, with a warning. `stored` reads the text of a stored value. +/// +/// # Errors +/// +/// `22P02` for a bad item, `42622` for a name that is too long, and `42704` for a role that does not exist. +fn aclparse<'a>(s: &'a [u8], session: &dyn Session, stored: bool) -> Result<(AclItem, &'a [u8])> { + let (mut name, mut s) = getid(s)?; + if s.first() != Some(&b'=') { + if name != "group" && name != "user" { + return Err(acl_syntax(format!("unrecognized key word: \"{name}\"")) + .with_hint("ACL key word must be \"group\" or \"user\".")); + } + (name, s) = getid(s)?; + if name.is_empty() { + return Err(acl_syntax("missing name") + .with_hint("A name must follow the \"group\" or \"user\" key word.")); + } + } + let Some(mut s) = s.strip_prefix(b"=") else { + return Err(acl_syntax("missing \"=\" sign")); + }; + let mut bits = 0; + let mut read = 0; + while let Some((&c, rest)) = s.split_first() { + if c == b'*' { + bits |= go(read); + } else if c.is_ascii_alphabetic() { + let Some(bit) = RIGHTS.find(char::from(c)) else { + return Err(acl_syntax(format!( + "invalid mode character: must be one of \"{RIGHTS}\"" + ))); + }; + read = 1 << bit; + bits |= read; + } else { + break; + } + s = rest; + } + let grantee = if name.is_empty() { PUBLIC } else { acl_role(&name, session, stored)? }; + let grantor = if let Some(rest) = s.strip_prefix(b"/") { + let (name, rest) = getid(rest)?; + s = rest; + if name.is_empty() { + return Err(acl_syntax("a name must follow the \"/\" sign")); + } + acl_role(&name, session, stored)? + } else { + session.warn(Error::new( + SqlState::INVALID_GRANTOR, + format!("defaulting grantor to user ID {BOOTSTRAP_SUPERUSER}"), + )); + BOOTSTRAP_SUPERUSER + }; + Ok((AclItem { grantee, bits, grantor }, s)) +} + +/// `aclitemin`: an `aclitem` value as the text of `aclitemout`. +/// +/// # Errors +/// +/// The errors of [`aclparse`], and `22P02` for text after the item. +pub(crate) fn aclitem_in(text: &str, session: &dyn Session) -> Result { + let (item, rest) = aclparse(text.as_bytes(), session, false)?; + if !skip_spaces(rest).is_empty() { + return Err(acl_syntax("extra garbage at the end of the ACL specification")); + } + Ok(acl_item(item.grantee, item.bits, item.grantor)) +} + +/// The parts of an `aclitem` value, which has the text of `aclitemout`. +fn stored_item(value: &Value, session: &dyn Session) -> Result { + let text = value.as_str().ok_or_else(bad_value)?; + Ok(aclparse(text.as_bytes(), session, true)?.0) +} + +/// `check_acl`: the items of an `aclitem[]` value, which must have one dimension and no null items. +/// +/// # Errors +/// +/// `22023` for an array that does not have one dimension, and `22004` for a null item. +fn acl_items(value: &Value, session: &dyn Session) -> Result> { + let Value::Array(acl) = value else { return Err(bad_value()) }; + if acl.dims.len() != 1 { + return Err(Error::new( + SqlState::INVALID_PARAMETER_VALUE, + "ACL arrays must be one-dimensional", + )); + } + if acl.values.iter().any(Option::is_none) { + return Err(Error::new( + SqlState::NULL_VALUE_NOT_ALLOWED, + "ACL arrays must not contain null values", + )); + } + acl.values.iter().flatten().map(|item| stored_item(item, session)).collect() +} + +/// `aclitem_eq`: the grantees, the privileges and the grantors are equal. The text of a value has one form, so the texts are equal. +fn aclitem_eq(_call: &Call<'_>, args: &[Value]) -> Result { + Ok(Value::Bool(text(args, 0)? == text(args, 1)?)) +} + +/// `aclcontains`: true when the array has an item with the grantee and the grantor of the item, and with all its privileges and grant options. +/// +/// # Errors +/// +/// The errors of [`acl_items`]. +fn aclcontains(call: &Call<'_>, args: &[Value]) -> Result { + let [acl, item] = args else { return Err(bad_value()) }; + let items = acl_items(acl, call.session)?; + let item = stored_item(item, call.session)?; + Ok(Value::Bool(items.iter().any(|a| { + a.grantee == item.grantee && a.grantor == item.grantor && (a.bits & item.bits) == item.bits + }))) +} + +/// The privileges of `makeaclitem`: the privileges of all kinds of objects. +const ANY_PRIVS: PrivMap = &[ + ("SELECT", ACL_SELECT), + ("INSERT", ACL_INSERT), + ("UPDATE", ACL_UPDATE), + ("DELETE", ACL_DELETE), + ("TRUNCATE", ACL_TRUNCATE), + ("REFERENCES", ACL_REFERENCES), + ("TRIGGER", ACL_TRIGGER), + ("EXECUTE", ACL_EXECUTE), + ("USAGE", ACL_USAGE), + ("CREATE", ACL_CREATE), + ("TEMP", ACL_CREATE_TEMP), + ("TEMPORARY", ACL_CREATE_TEMP), + ("CONNECT", ACL_CONNECT), + ("SET", ACL_SET), + ("ALTER SYSTEM", ACL_ALTER_SYSTEM), + ("MAINTAIN", ACL_MAINTAIN), +]; + +/// `makeaclitem(grantee, grantor, privileges, is_grantable)`: an item with the privileges of a list of names, with their grant options when `is_grantable` is true. +/// +/// # Errors +/// +/// `22023` for a name that is not the name of a privilege. +fn makeaclitem(_call: &Call<'_>, args: &[Value]) -> Result { + let (Some(Value::Oid(grantee)), Some(Value::Oid(grantor)), Some(Value::Bool(grantable))) = + (args.first(), args.get(1), args.get(3)) + else { + return Err(bad_value()); + }; + let privs = privileges(text(args, 2)?, ANY_PRIVS)?; + let bits = if *grantable { privs | go(privs) } else { privs }; + Ok(Value::text(acl_item(*grantee, bits, *grantor))) +} + +/// The hash of `hash_aclitem`: the sum of the privileges with no grant options, the grantee and the grantor, in 32 bits. +fn aclitem_sum(item: AclItem) -> u32 { + (item.bits & 0xFFFF).wrapping_add(item.grantee).wrapping_add(item.grantor) +} + +/// `hash_aclitem`. +fn hash_aclitem(call: &Call<'_>, args: &[Value]) -> Result { + let [item] = args else { return Err(bad_value()) }; + Ok(Value::Int4(aclitem_sum(stored_item(item, call.session)?).cast_signed())) +} + +/// `hash_aclitem_extended`: the sum of `hash_aclitem` for the seed 0. The engine does not have the hash of another seed yet. +fn hash_aclitem_extended(call: &Call<'_>, args: &[Value]) -> Result { + let [item, Value::Int8(seed)] = args else { return Err(bad_value()) }; + if *seed != 0 { + return Err(not_yet("hash_aclitem_extended with a seed that is not 0")); + } + Ok(Value::Int8(i64::from(aclitem_sum(stored_item(item, call.session)?)))) +} + +/// `aclinsert`, which PostgreSQL no longer has. +fn aclinsert(_call: &Call<'_>, _args: &[Value]) -> Result { + Err(Error::new(SqlState::FEATURE_NOT_SUPPORTED, "aclinsert is no longer supported")) +} + +/// `aclremove`, which PostgreSQL no longer has. +fn aclremove(_call: &Call<'_>, _args: &[Value]) -> Result { + Err(Error::new(SqlState::FEATURE_NOT_SUPPORTED, "aclremove is no longer supported")) +} + +/// `putid`: the name of a role in an `aclitem` value, in double quotes when it has a character that is not an ASCII letter, an ASCII digit or `_`. A role that does not exist shows as its OID. fn put_role(out: &mut String, role: u32) { let Some(row) = builtin::roles().iter().find(|r| r.oid == role) else { out.push_str(&role.to_string()); return; }; - if row.name.chars().all(|c| c.is_alphanumeric() || c == '_') { + if row.name.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_') { out.push_str(row.name); return; } @@ -1010,9 +1253,22 @@ pub(crate) fn by_src(src: &str) -> Option { if matches!(src, "row_security_active" | "row_security_active_name") { return Some(row_security_active); } - if src == "acldefault_sql" { - return Some(acldefault); - } + let kernel: Kernel = match src { + "acldefault_sql" => acldefault, + "aclitem_eq" => aclitem_eq, + "aclcontains" => aclcontains, + "makeaclitem" => makeaclitem, + "hash_aclitem" => hash_aclitem, + "hash_aclitem_extended" => hash_aclitem_extended, + "aclinsert" => aclinsert, + "aclremove" => aclremove, + _ => return privilege_kernel(src), + }; + Some(kernel) +} + +/// The kernel of a `has_*_privilege` function or of `pg_has_role`. +fn privilege_kernel(src: &str) -> Option { let family = if src.starts_with("pg_has_role") { "pg_has_role" } else { diff --git a/crates/rupg-func/src/io.rs b/crates/rupg-func/src/io.rs index 85c148e3..b5052fab 100644 --- a/crates/rupg-func/src/io.rs +++ b/crates/rupg-func/src/io.rs @@ -119,9 +119,7 @@ pub fn input_supported(ty: u32) -> bool { if let Some((elem, _)) = array_of(ty) { return input_supported(elem); } - output_supported(ty) - && !matches!(ty, oid::ACLITEM | oid::ANYARRAY | oid::VOID) - && !is_row_type(ty) + output_supported(ty) && !matches!(ty, oid::ANYARRAY | oid::VOID) && !is_row_type(ty) } /// The name of a type for an error. @@ -439,6 +437,7 @@ fn input_value( } XID => Value::Oid(types::uint32_in(text, "xid")?), CID => Value::Oid(types::uint32_in(text, "cid")?), + oid::ACLITEM => Value::Text(crate::acl::aclitem_in(text, session).map_err(Failed::Soft)?), oid::TEXT | oid::UNKNOWN | oid::CSTRING | oid::REFCURSOR => Value::text(text), oid::VARCHAR => Value::text(types::varchar_in(text, typmod)?), oid::BPCHAR => Value::text(types::bpchar_in(text, typmod)?), @@ -512,6 +511,14 @@ fn no_input(ty: u32) -> Error { ) } +/// `getTypeBinaryInputInfo` and `getTypeBinaryOutputInfo`: the error of a type that has no receive or no send function, such as `aclitem`. +fn no_binary(ty: u32, what: &str) -> Error { + Error::new( + SqlState::UNDEFINED_FUNCTION, + format!("no binary {what} function available for type {}", type_name(ty)), + ) +} + /// An error of the engine as the error of a type function, for an element of an array. fn types_error(error: &Error) -> types::TypeError { types::TypeError { @@ -628,6 +635,7 @@ fn receive_from(ty: u32, recv: &mut Recv<'_>, typmod: i32) -> std::result::Resul oid::PG_NDISTINCT | oid::PG_DEPENDENCIES | oid::PG_MCV_LIST => { return Err(Received::Engine(no_input(ty))); } + oid::ACLITEM => return Err(Received::Engine(no_binary(ty, "input"))), _ => { return Err(Received::Engine(not_yet(format!( "binary input of type {}", @@ -658,7 +666,8 @@ pub fn send(ty: u32, value: &Value, out: &mut Vec) -> Result<()> { return failed.map_or(Ok(()), Err); } match (ty, value) { - (oid::ACLITEM | oid::ANYARRAY, _) => { + (oid::ACLITEM, _) => return Err(no_binary(ty, "output")), + (oid::ANYARRAY, _) => { return Err(not_yet(format!("binary output of type {}", type_name(ty)))); } (oid::MACADDR, Value::Int8(v)) => out.extend_from_slice(&v.to_be_bytes()[2..]), diff --git a/crates/rupg-func/src/lib.rs b/crates/rupg-func/src/lib.rs index 082457ad..0fc60f3e 100644 --- a/crates/rupg-func/src/lib.rs +++ b/crates/rupg-func/src/lib.rs @@ -167,6 +167,10 @@ pub trait Session { let _ = (ty, value); Err(not_yet("the check constraints of a domain")) } + /// `ereport(WARNING)` in a function: a warning for the client, such as the warning of the input of an `aclitem` value with no grantor. A session with no client drops it. + fn warn(&self, warning: Error) { + let _ = warning; + } } /// A call of a function. diff --git a/crates/rupg-session/src/connection.rs b/crates/rupg-session/src/connection.rs index e8dbbd4f..12f0aebb 100644 --- a/crates/rupg-session/src/connection.rs +++ b/crates/rupg-session/src/connection.rs @@ -643,6 +643,8 @@ impl Connection { } // The columns of a query that failed at run time. PostgreSQL sends `RowDescription` when the executor starts, so the error comes after it. let mut described = None; + // The format and the number of columns of a `COPY` that failed at run time. + let mut copying = None; let result = if let Err(error) = self.interrupted() { Err(error) } else if self.transaction.failed() && !utility::exits_transaction(node) { @@ -664,7 +666,7 @@ impl Connection { self.lock(stmt) } else if let Node::CopyStmt(stmt) = node { drop(self.portals.close(b"")); - self.copy(stmt) + self.copy(stmt, &mut copying) } else { drop(self.portals.close(b"")); let mut cx = Context { @@ -685,6 +687,9 @@ impl Connection { columns.iter().map(|column| column.field(0)).collect(); out.row_description(&fields); } + if let Some((binary, columns)) = copying { + copy::copy_out_response(out, binary, columns); + } let position = error.position().map(|at| character_position(text, at)); self.fail(&error, position, out); return true; @@ -729,11 +734,17 @@ impl Connection { ) -> Result { self.settings.borrow_mut().take_snapshot(); let reader = self.reader(); - let plan = query::plan(node, &reader, &Params::default())?; + let plan = query::plan(node, &reader, &Params::default()); + notices.extend(reader.take_warnings().into_iter().map(Notice::warning)); + let plan = plan?; notices.extend(plan.notices().iter().map(|error| Notice::warning(error.clone()))); - let plan = plan.fold(None, &reader)?; + let plan = plan.fold(None, &reader); + notices.extend(reader.take_warnings().into_iter().map(Notice::warning)); + let plan = plan?; let columns = query::columns(&plan); - match query::run(&plan, &[], &reader, &[]) { + let rows = query::run(&plan, &[], &reader, &[]); + notices.extend(reader.take_warnings().into_iter().map(Notice::warning)); + match rows { Ok(rows) => Ok(Done::Rows { columns, rows, tag: CommandTag::Select }), Err(error) => { *described = Some(columns); @@ -1803,6 +1814,12 @@ mod tests { big_stack(|| script(include_str!("connection/snap.test"))); } + /// `aclitem`: the text forms with the errors and the warning of a bad item, the soft errors, `makeaclitem`, `aclcontains`, `aclexplode`, the hash functions, the arrays, the text form of `COPY` and the error of the binary form. A warning of the input of a literal points at the literal, in a simple query and in `Parse`. `connection/acl.test` is the output of PostgreSQL 19 for the same script in a new database. + #[test] + fn acl() { + big_stack(|| script(include_str!("connection/acl.test"))); + } + /// The geometric types `point`, `lseg`, `box`, `path`, `polygon`, `line` and `circle`: the text forms of each format with the errors of a bad value, `extra_float_digits`, the arrays, the soft errors and the text and binary forms of `COPY`. `connection/geo.test` is the output of PostgreSQL 19 for the same script in a new database. #[test] fn geo() { diff --git a/crates/rupg-session/src/connection/acl.test b/crates/rupg-session/src/connection/acl.test new file mode 100644 index 00000000..c8122c05 --- /dev/null +++ b/crates/rupg-session/src/connection/acl.test @@ -0,0 +1,227 @@ +> SELECT 'postgres=arwdDxtm/postgres'::aclitem, '=r/postgres'::aclitem, 'postgres=r*w/postgres'::aclitem, 'postgres=arwdDxtXUCTcsAm/postgres'::aclitem +fields aclitem:1033:16:-1,aclitem:1033:16:-1,aclitem:1033:16:-1,aclitem:1033:16:-1 +row postgres=arwdDxtm/postgres,=r/postgres,postgres=r*w/postgres,postgres=arwdDxtXUCTcsAm/postgres +SELECT 1 +> SELECT 'postgres=a*r*w*d*D*x*t*X*U*C*T*c*s*A*m*/postgres'::aclitem, '=/postgres'::aclitem, 'postgres=***r/postgres'::aclitem, 'postgres=rr/postgres'::aclitem +fields aclitem:1033:16:-1,aclitem:1033:16:-1,aclitem:1033:16:-1,aclitem:1033:16:-1 +row postgres=a*r*w*d*D*x*t*X*U*C*T*c*s*A*m*/postgres,=/postgres,postgres=r/postgres,postgres=r/postgres +SELECT 1 +> SELECT ' postgres=a/postgres '::aclitem, 'postgres =a/postgres'::aclitem, 'postgres=a/ postgres'::aclitem, E'\tpostgres=a/postgres\n'::aclitem +fields aclitem:1033:16:-1,aclitem:1033:16:-1,aclitem:1033:16:-1,aclitem:1033:16:-1 +row postgres=a/postgres,postgres=a/postgres,postgres=a/postgres,postgres=a/postgres +SELECT 1 +> SELECT '"postgres"=a/"postgres"'::aclitem, 'group postgres=r/postgres'::aclitem, 'user postgres=r/postgres'::aclitem +fields aclitem:1033:16:-1,aclitem:1033:16:-1,aclitem:1033:16:-1 +row postgres=a/postgres,postgres=r/postgres,postgres=r/postgres +SELECT 1 +> SELECT 'group"postgres"=r/postgres'::aclitem +ERROR 42704 role "grouppostgres" does not exist at 8 +> SELECT 'group"postgres" =r/postgres'::aclitem, 'user"postgres"=r/postgres'::aclitem +ERROR 42704 role "grouppostgres" does not exist at 8 +> SELECT 'postgres=a /postgres'::aclitem +WARNING 0L000 defaulting grantor to user ID 10 at 8 +ERROR 22P02 extra garbage at the end of the ACL specification at 8 +> SELECT 'postgres= a/postgres'::aclitem +WARNING 0L000 defaulting grantor to user ID 10 at 8 +ERROR 22P02 extra garbage at the end of the ACL specification at 8 +> SELECT 'postgres=r'::aclitem +WARNING 0L000 defaulting grantor to user ID 10 at 8 +fields aclitem:1033:16:-1 +row postgres=r/postgres +SELECT 1 +> SELECT '=rw*'::aclitem, 'postgres=r/postgres'::aclitem +WARNING 0L000 defaulting grantor to user ID 10 at 8 +fields aclitem:1033:16:-1,aclitem:1033:16:-1 +row =rw*/postgres,postgres=r/postgres +SELECT 1 +> SELECT 'postgres'::aclitem +ERROR 22P02 unrecognized key word: "postgres" at 8 +> SELECT 'Group postgres=r/postgres'::aclitem +ERROR 22P02 unrecognized key word: "Group" at 8 +> SELECT 'postgres r/postgres'::aclitem +ERROR 22P02 unrecognized key word: "postgres" at 8 +> SELECT 'group =r/postgres'::aclitem +ERROR 42704 role "group" does not exist at 8 +> SELECT 'group postgres r/postgres'::aclitem +ERROR 22P02 missing "=" sign at 8 +> SELECT ''::aclitem +ERROR 22P02 unrecognized key word: "" at 8 +> SELECT 'postgres=r/'::aclitem +ERROR 22P02 a name must follow the "/" sign at 8 +> SELECT 'postgres=r/ '::aclitem +ERROR 22P02 a name must follow the "/" sign at 8 +> SELECT 'postgres=q/postgres'::aclitem +ERROR 22P02 invalid mode character: must be one of "arwdDxtXUCTcsAm" at 8 +> SELECT 'postgres=r-/postgres'::aclitem +WARNING 0L000 defaulting grantor to user ID 10 at 8 +ERROR 22P02 extra garbage at the end of the ACL specification at 8 +> SELECT 'postgres=r/postgres x'::aclitem +ERROR 22P02 extra garbage at the end of the ACL specification at 8 +> SELECT 'postgres=r/postgres/postgres'::aclitem +ERROR 22P02 extra garbage at the end of the ACL specification at 8 +> SELECT 'nobody=r/postgres'::aclitem +ERROR 42704 role "nobody" does not exist at 8 +> SELECT 'postgres=r/nobody'::aclitem +ERROR 42704 role "nobody" does not exist at 8 +> SELECT '"no body"=r/postgres'::aclitem +ERROR 42704 role "no body" does not exist at 8 +> SELECT '"no""body"=r/postgres'::aclitem +ERROR 42704 role "no"body" does not exist at 8 +> SELECT '"postgres=r/postgres'::aclitem +ERROR 22P02 unrecognized key word: "postgres=r/postgres" at 8 +> SELECT 'nobody=q/postgres'::aclitem +ERROR 22P02 invalid mode character: must be one of "arwdDxtXUCTcsAm" at 8 +> SELECT 'nobody=r'::aclitem +ERROR 42704 role "nobody" does not exist at 8 +> SELECT 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=r/postgres'::aclitem +ERROR 42704 role "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" does not exist at 8 +> SELECT 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=r/postgres'::aclitem +ERROR 42622 identifier too long at 8 +> SELECT 'postgres=r/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'::aclitem +ERROR 42622 identifier too long at 8 +> SELECT '{postgres=r/postgres,=w/postgres}'::aclitem[], '{}'::aclitem[], '{"postgres=r/postgres",NULL}'::aclitem[] +fields aclitem:1034:-1:-1,aclitem:1034:-1:-1,aclitem:1034:-1:-1 +row {postgres=r/postgres,=w/postgres},{},{postgres=r/postgres,NULL} +SELECT 1 +> SELECT '{postgres=r,=w}'::aclitem[] +WARNING 0L000 defaulting grantor to user ID 10 at 8 +WARNING 0L000 defaulting grantor to user ID 10 at 8 +fields aclitem:1034:-1:-1 +row {postgres=r/postgres,=w/postgres} +SELECT 1 +> SELECT '{postgres=r/postgres,nobody=w/postgres}'::aclitem[] +ERROR 42704 role "nobody" does not exist at 8 +> SELECT pg_input_is_valid('postgres=r/postgres', 'aclitem'), pg_input_is_valid('postgres=q/postgres', 'aclitem'), pg_input_is_valid('nobody=r/postgres', 'aclitem'), pg_input_is_valid('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=r/postgres', 'aclitem') +fields pg_input_is_valid:16:1:-1,pg_input_is_valid:16:1:-1,pg_input_is_valid:16:1:-1,pg_input_is_valid:16:1:-1 +row t,f,f,f +SELECT 1 +> SELECT * FROM pg_input_error_info('postgres=q/postgres', 'aclitem') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid mode character: must be one of "arwdDxtXUCTcsAm",NULL,NULL,22P02 +SELECT 1 +> SELECT * FROM pg_input_error_info('postgres', 'aclitem') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row unrecognized key word: "postgres",NULL,ACL key word must be "group" or "user".,22P02 +SELECT 1 +> SELECT * FROM pg_input_error_info('group =r/postgres', 'aclitem') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row role "group" does not exist,NULL,NULL,42704 +SELECT 1 +> SELECT * FROM pg_input_error_info('nobody=r/postgres', 'aclitem') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row role "nobody" does not exist,NULL,NULL,42704 +SELECT 1 +> SELECT * FROM pg_input_error_info('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa=r/postgres', 'aclitem') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row identifier too long,Identifier must be less than 64 characters.,NULL,42622 +SELECT 1 +> SELECT * FROM pg_input_error_info('{postgres=r/postgres,=q/postgres}', 'aclitem[]') +fields message:25:-1:-1,detail:25:-1:-1,hint:25:-1:-1,sql_error_code:25:-1:-1 +row invalid mode character: must be one of "arwdDxtXUCTcsAm",NULL,NULL,22P02 +SELECT 1 +> SELECT 'postgres=r/postgres'::aclitem = 'postgres=r/postgres'::aclitem, 'postgres=r/postgres'::aclitem = 'postgres=r*/postgres'::aclitem, '=r/postgres'::aclitem = 'postgres=r/postgres'::aclitem, aclitemeq('=r/postgres', '=r/postgres') +fields ?column?:16:1:-1,?column?:16:1:-1,?column?:16:1:-1,aclitemeq:16:1:-1 +row t,f,f,t +SELECT 1 +> SELECT makeaclitem('postgres'::regrole, 'postgres'::regrole, 'SELECT, UPDATE', true), makeaclitem(0, 'postgres'::regrole, 'select', false), makeaclitem('postgres'::regrole, 'postgres'::regrole, ' temp , Alter System,MAINTAIN', false) +fields makeaclitem:1033:16:-1,makeaclitem:1033:16:-1,makeaclitem:1033:16:-1 +row postgres=r*w*/postgres,=r/postgres,postgres=TAm/postgres +SELECT 1 +> SELECT makeaclitem('postgres'::regrole, 'postgres'::regrole, 'INSERT,SELECT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER,EXECUTE,USAGE,CREATE,TEMPORARY,CONNECT,SET,ALTER SYSTEM,MAINTAIN', true) +fields makeaclitem:1033:16:-1 +row postgres=a*r*w*d*D*x*t*X*U*C*T*c*s*A*m*/postgres +SELECT 1 +> SELECT makeaclitem('postgres'::regrole, 'postgres'::regrole, 'SELECT, foo', false) +ERROR 22023 unrecognized privilege type: "foo" +> SELECT makeaclitem('postgres'::regrole, 'postgres'::regrole, '', false) +ERROR 22023 unrecognized privilege type: "" +> SELECT makeaclitem('postgres'::regrole, 'postgres'::regrole, 'SELECT,', false) +ERROR 22023 unrecognized privilege type: "" +> SELECT makeaclitem(12345, 'postgres'::regrole, 'SELECT', false), makeaclitem('postgres'::regrole, 0, 'SELECT', false) +fields makeaclitem:1033:16:-1,makeaclitem:1033:16:-1 +row 12345=r/postgres,postgres=r/0 +SELECT 1 +> SELECT makeaclitem(NULL, 'postgres'::regrole, 'SELECT', false), makeaclitem('postgres'::regrole, 'postgres'::regrole, 'SELECT', NULL) +fields makeaclitem:1033:16:-1,makeaclitem:1033:16:-1 +row NULL,NULL +SELECT 1 +> SELECT '{postgres=r/postgres}'::aclitem[] @> 'postgres=r/postgres'::aclitem, aclcontains('{postgres=rw/postgres}', 'postgres=r/postgres') +fields ?column?:16:1:-1,aclcontains:16:1:-1 +row t,t +SELECT 1 +> SELECT aclcontains('{postgres=rw/postgres}', 'postgres=r*/postgres'), aclcontains('{postgres=r*w/postgres}', 'postgres=r/postgres'), aclcontains('{postgres=r/postgres}', '=r/postgres'), aclcontains('{=/postgres}', '=/postgres') +fields aclcontains:16:1:-1,aclcontains:16:1:-1,aclcontains:16:1:-1,aclcontains:16:1:-1 +row f,t,f,t +SELECT 1 +> SELECT aclcontains('{}', '=r/postgres') +ERROR 22023 ACL arrays must be one-dimensional +> SELECT aclcontains('{{=r/postgres}}', '=r/postgres') +ERROR 22023 ACL arrays must be one-dimensional +> SELECT aclcontains('{=r/postgres,NULL}', '=r/postgres') +ERROR 22004 ACL arrays must not contain null values +> SELECT privilege_type, is_grantable FROM aclexplode('{postgres=r*w/postgres,=a/postgres}'::aclitem[]) +fields privilege_type:25:-1:-1,is_grantable:16:1:-1 +row SELECT,t +row UPDATE,f +row INSERT,f +SELECT 3 +> SELECT grantee, privilege_type FROM aclexplode(ARRAY[makeaclitem(12345, 'postgres'::regrole, 'SELECT', false)]) +fields grantee:26:4:-1,privilege_type:25:-1:-1 +row 12345,SELECT +SELECT 1 +> SELECT aclcontains(ARRAY[makeaclitem(12345, 'postgres'::regrole, 'SELECT, UPDATE', false)], makeaclitem(12345, 'postgres'::regrole, 'UPDATE', false)), hash_aclitem(makeaclitem(12345, 0, 'SELECT', false)) +fields aclcontains:16:1:-1,hash_aclitem:23:4:-1 +row t,12347 +SELECT 1 +> SELECT aclcontains('{=r/postgres,postgres=rw/postgres}', 'postgres=w/postgres'), aclcontains('{postgres=rw/postgres}', 'postgres=w/pg_monitor'), aclcontains('{pg_monitor=rw/postgres}', 'pg_monitor=w/postgres') +fields aclcontains:16:1:-1,aclcontains:16:1:-1,aclcontains:16:1:-1 +row t,f,t +SELECT 1 +> SELECT aclinsert('{}', '=r/postgres') +ERROR 0A000 aclinsert is no longer supported +> SELECT aclremove('{}', '=r/postgres') +ERROR 0A000 aclremove is no longer supported +> SELECT hash_aclitem('postgres=r/postgres'), hash_aclitem('=/postgres'), hash_aclitem('postgres=a*r*w*d*D*x*t*X*U*C*T*c*s*A*m*/postgres'), hash_aclitem_extended('postgres=r/postgres', 0) +fields hash_aclitem:23:4:-1,hash_aclitem:23:4:-1,hash_aclitem:23:4:-1,hash_aclitem_extended:20:8:-1 +row 22,10,32787,22 +SELECT 1 +> SELECT acldefault('r', 'postgres'::regrole) @> 'postgres=r/postgres'::aclitem, acldefault('f', 'postgres'::regrole) +fields ?column?:16:1:-1,acldefault:1034:-1:-1 +row t,{=X/postgres,postgres=X/postgres} +SELECT 1 +> SELECT '{postgres=r/postgres}'::aclitem[] = '{postgres=r/postgres}'::aclitem[], '{postgres=r/postgres}'::aclitem[] = '{postgres=w/postgres}'::aclitem[] +fields ?column?:16:1:-1,?column?:16:1:-1 +row t,f +SELECT 1 +> SELECT 'pg_monitor=r/pg_read_all_data'::aclitem, 'pg_database_owner=U/postgres'::aclitem +fields aclitem:1033:16:-1,aclitem:1033:16:-1 +row pg_monitor=r/pg_read_all_data,pg_database_owner=U/postgres +SELECT 1 +> SELECT 'postgres=r/postgres'::aclitem::text, 'postgres=r/postgres'::text::aclitem +fields text:25:-1:-1,aclitem:1033:16:-1 +row postgres=r/postgres,postgres=r/postgres +SELECT 1 +> COPY (SELECT 'postgres=r/postgres'::aclitem, '{=r/postgres,postgres=r*/postgres}'::aclitem[]) TO stdout +copy out 0 0,0 +data postgres=r/postgres\t{=r/postgres,postgres=r*/postgres}\n +copy done +COPY 1 +> COPY (SELECT 'postgres=r/postgres'::aclitem) TO stdout (FORMAT csv) +copy out 0 0 +data postgres=r/postgres\n +copy done +COPY 1 +> COPY (SELECT 'postgres=r/postgres'::aclitem) TO stdout (FORMAT binary) +copy out 1 1 +ERROR 42883 no binary output function available for type aclitem +> COPY (SELECT '{=r/postgres}'::aclitem[]) TO stdout (FORMAT binary) +copy out 1 1 +ERROR 42883 no binary output function available for type aclitem +P a|SELECT 'postgres=r'::aclitem +WARNING 0L000 defaulting grantor to user ID 10 at 8 +X a +row postgres=r/postgres +SELECT 1 +P b|SELECT 'postgres=r /x'::aclitem +WARNING 0L000 defaulting grantor to user ID 10 at 8 +ERROR 22P02 extra garbage at the end of the ACL specification at 8 diff --git a/crates/rupg-session/src/connection/copy.rs b/crates/rupg-session/src/connection/copy.rs index 366ec055..51dac591 100644 --- a/crates/rupg-session/src/connection/copy.rs +++ b/crates/rupg-session/src/connection/copy.rs @@ -66,8 +66,7 @@ struct Options { impl CopyOut { /// `CopyOutResponse`, the `CopyData` messages and `CopyDone`. pub(super) fn send_data(&self, out: &mut OutBuf) { - let format = i8::from(self.binary); - out.copy_out_response(format, &vec![i16::from(self.binary); self.columns]); + copy_out_response(out, self.binary, self.columns); for data in &self.data { out.copy_data(data); } @@ -81,9 +80,18 @@ impl CopyOut { } } +/// `CopyOutResponse` with the same format for all the columns. +pub(super) fn copy_out_response(out: &mut OutBuf, binary: bool, columns: usize) { + out.copy_out_response(i8::from(binary), &vec![i16::from(binary); columns]); +} + impl Connection { - /// `DoCopy` for `COPY ... TO STDOUT`. - pub(super) fn copy(&self, stmt: &CopyStmt) -> Result { + /// `DoCopy` for `COPY ... TO STDOUT`. PostgreSQL sends `CopyOutResponse` before it runs the query, so `started` gets the format and the number of columns at that point. The caller then sends `CopyOutResponse` before an error of the run. + pub(super) fn copy( + &self, + stmt: &CopyStmt, + started: &mut Option<(bool, usize)>, + ) -> Result { if stmt.is_from { return Err(Error::new( SqlState::FEATURE_NOT_SUPPORTED, @@ -156,6 +164,7 @@ impl Connection { } let binary = options.format == Format::Binary; let formats = vec![i16::from(binary); selected.len()]; + *started = Some((binary, selected.len())); let rows = query::run(&plan, &[], &reader, &formats)?; let names: Vec<&str> = selected.iter().map(|&i| columns[i].as_str()).collect(); let data = diff --git a/crates/rupg-session/src/connection/extended.rs b/crates/rupg-session/src/connection/extended.rs index b7dfb701..a48b8851 100644 --- a/crates/rupg-session/src/connection/extended.rs +++ b/crates/rupg-session/src/connection/extended.rs @@ -15,7 +15,7 @@ use rupg_sql::nodes::Node; use rupg_types::{Array, Value, oid}; use rupg_wire::{Bind, CommandTag, Field, Oids, OutBuf, ProtocolError, Target}; -use super::{Connection, Done, character_position, prepare}; +use super::{Connection, Done, character_position, copy, prepare}; use crate::guc::{self, Setting}; use crate::param; use crate::query::{self, Column, Row}; @@ -230,12 +230,17 @@ impl Connection { 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 { + let reader = self.reader(); + let made = query::plan(node, &reader, &given); + let mut notices: Vec = + reader.take_warnings().into_iter().map(Notice::warning).collect(); + drop(reader); + self.notices(&mut notices, &text, &[], out); + let made = made.map_err(|error| Failed { position: error.position().map(|at| character_position(&text, at)), error, })?; - let mut notices: Vec = - made.notices().iter().map(|error| Notice::warning(error.clone())).collect(); + notices.extend(made.notices().iter().map(|error| Notice::warning(error.clone()))); self.notices(&mut notices, &text, &[], out); columns = Some(query::columns(&made)); params = made.params().to_vec(); @@ -437,7 +442,14 @@ impl Connection { } else if let Node::LockStmt(stmt) = node { self.lock(stmt)? } else if let Node::CopyStmt(stmt) = node { - self.copy(stmt)? + let mut started = None; + let result = self.copy(stmt, &mut started); + if result.is_err() + && let Some((binary, columns)) = started + { + copy::copy_out_response(out, binary, columns); + } + result? } else { let mut notices = Vec::new(); let mut cx = Context { diff --git a/crates/rupg-session/src/connection/prepare.rs b/crates/rupg-session/src/connection/prepare.rs index 3f080a27..208fb070 100644 --- a/crates/rupg-session/src/connection/prepare.rs +++ b/crates/rupg-session/src/connection/prepare.rs @@ -60,8 +60,10 @@ impl Connection { self.settings.borrow_mut().take_snapshot(); let reader = self.reader(); let types = rupg_analyze::param_types(&stmt.argtypes, &reader)?; - let plan = query::plan(node, &reader, &Params { types, variable: true })?; + let plan = query::plan(node, &reader, &Params { types, variable: true }); + notices.extend(reader.take_warnings().into_iter().map(Notice::warning)); drop(reader); + let plan = plan?; notices.extend(plan.notices().iter().map(|error| Notice::warning(error.clone()))); let params = plan.params().to_vec(); let columns = Some(query::columns(&plan)); diff --git a/crates/rupg-session/src/query.rs b/crates/rupg-session/src/query.rs index d6ce9044..8cb698b6 100644 --- a/crates/rupg-session/src/query.rs +++ b/crates/rupg-session/src/query.rs @@ -97,6 +97,8 @@ pub(crate) struct Reader<'a> { statements: Option<&'a Statements>>, /// The flag that a `CancelRequest` sets, or `None` for a reader that is not a session. cancel: Option<&'a AtomicBool>, + /// The warnings of the functions that the statement called, such as the input function of `aclitem`. The session sends them before the result. + warnings: RefCell>, } /// The zone of a `TimeZone` value, or the value if it is not a zone. @@ -127,9 +129,15 @@ impl<'a> Reader<'a> { activity: None, statements: None, cancel: None, + warnings: RefCell::new(Vec::new()), } } + /// The warnings of the functions since the last call. + pub(crate) fn take_warnings(&self) -> Vec { + self.warnings.take() + } + /// The reader of a session, which reads the table of the sessions in `store`. pub(crate) fn with_activity(mut self, activity: &'a Activity, store: &'a Store) -> Reader<'a> { self.activity = Some((activity, store)); @@ -336,6 +344,10 @@ impl rupg_func::Session for Reader<'_> { rupg_exec::input_failure(ty, value, self) } + fn warn(&self, warning: Error) { + self.warnings.borrow_mut().push(warning); + } + fn version(&self) -> String { crate::connection::version_text() } @@ -416,6 +428,15 @@ impl Env for Reader<'_> { Ok(value) } + fn input_at(&self, ty: u32, text: &str, typmod: i32, at: Option) -> Result { + let before = self.warnings.borrow().len(); + let value = self.input(ty, text, typmod); + let mut warnings = self.warnings.borrow_mut(); + let new = warnings.split_off(before); + warnings.extend(new.into_iter().map(|warning| warning.with_position(at))); + value + } + fn catalog(&self) -> Option<&Catalog> { Some(&self.catalog) }