Skip to content

Commit f9e21b3

Browse files
committed
feat(pgwire): support current_setting() SQL function
Extracts SHOW's GUC resolution into a shared resolve_guc helper and adds a current_setting('name' [, missing_ok]) handler that reuses it, covering the common ORM/driver pattern of querying server_version and other runtime parameters via SELECT rather than SHOW.
1 parent e137985 commit f9e21b3

5 files changed

Lines changed: 277 additions & 29 deletions

File tree

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
3+
//! `current_setting(setting_name [, missing_ok])` — PostgreSQL's function
4+
//! form of `SHOW <param>`. Resolves from the same GUC sources as `SHOW`
5+
//! (see `session_show::resolve_guc`), but as a scalar function call so it
6+
//! can appear in `SELECT` lists like any other function.
7+
8+
use std::sync::Arc;
9+
10+
use pgwire::api::results::{DataRowEncoder, QueryResponse, Response};
11+
use pgwire::error::{ErrorInfo, PgWireError, PgWireResult};
12+
13+
use super::super::types::text_field;
14+
use super::core::NodeDbPgHandler;
15+
16+
/// Parse `SELECT current_setting('name')` or
17+
/// `SELECT current_setting('name', true|false)` into `(setting_name,
18+
/// missing_ok)`. Case-insensitive on the keyword; the setting name is
19+
/// lowercased to match `resolve_guc`'s expectation. Returns `None` if the
20+
/// SQL is not a `current_setting` call so normal planning can proceed.
21+
///
22+
/// Only string-literal arguments are handled. If the second argument isn't
23+
/// a clear `true`/`false` literal, `missing_ok` defaults to `false`
24+
/// (conservative — an unrecognised second arg should not silently suppress
25+
/// errors).
26+
pub fn parse_current_setting(sql: &str) -> Option<(String, bool)> {
27+
let trimmed = sql.trim().trim_end_matches(';').trim();
28+
let trimmed_upper = trimmed.to_uppercase();
29+
if !trimmed_upper.starts_with("SELECT ") {
30+
return None;
31+
}
32+
33+
let rest = trimmed[7..].trim();
34+
let rest_upper = rest.to_uppercase();
35+
if !rest_upper.starts_with("CURRENT_SETTING(") {
36+
return None;
37+
}
38+
39+
let paren = rest.find('(')?;
40+
let close = rest.rfind(')')?;
41+
if close <= paren + 1 {
42+
return None;
43+
}
44+
let args = &rest[paren + 1..close];
45+
46+
let parts: Vec<&str> = args.split(',').map(str::trim).collect();
47+
let (name_arg, missing_ok_arg) = match parts.as_slice() {
48+
[name] => (*name, None),
49+
[name, missing_ok] => (*name, Some(*missing_ok)),
50+
_ => return None,
51+
};
52+
53+
let name = strip_quotes(name_arg)?;
54+
if name.is_empty() {
55+
return None;
56+
}
57+
58+
let missing_ok = match missing_ok_arg {
59+
Some(v) => v.eq_ignore_ascii_case("true"),
60+
None => false,
61+
};
62+
63+
Some((name.to_lowercase(), missing_ok))
64+
}
65+
66+
/// Strip a single-quoted SQL string literal into its raw inner value.
67+
/// Escapes are not honored.
68+
fn strip_quotes(s: &str) -> Option<String> {
69+
let s = s.trim();
70+
if s.len() >= 2 {
71+
let first = s.as_bytes()[0];
72+
let last = s.as_bytes()[s.len() - 1];
73+
if first == b'\'' && last == b'\'' {
74+
let inner = &s[1..s.len() - 1];
75+
if !inner.contains('\'') {
76+
return Some(inner.to_string());
77+
}
78+
}
79+
}
80+
None
81+
}
82+
83+
impl NodeDbPgHandler {
84+
/// Handle `SELECT current_setting('name' [, missing_ok])`.
85+
pub(super) fn handle_current_setting(
86+
&self,
87+
addr: &std::net::SocketAddr,
88+
sql: &str,
89+
) -> PgWireResult<Vec<Response>> {
90+
let (name, missing_ok) = match parse_current_setting(sql) {
91+
Some(parsed) => parsed,
92+
None => {
93+
return Err(PgWireError::UserError(Box::new(ErrorInfo::new(
94+
"ERROR".to_owned(),
95+
"42601".to_owned(),
96+
"syntax error: current_setting('name' [, missing_ok])".to_owned(),
97+
))));
98+
}
99+
};
100+
101+
let schema = Arc::new(vec![text_field("current_setting")]);
102+
let mut encoder = DataRowEncoder::new(schema.clone());
103+
104+
match self.resolve_guc(addr, &name) {
105+
Ok(value) => {
106+
encoder.encode_field(&value)?;
107+
}
108+
Err(e) => {
109+
if missing_ok {
110+
encoder.encode_field(&Option::<String>::None)?;
111+
} else {
112+
return Err(e);
113+
}
114+
}
115+
}
116+
117+
let row = encoder.take_row();
118+
Ok(vec![Response::Query(QueryResponse::new(
119+
schema,
120+
futures::stream::iter(vec![Ok(row)]),
121+
))])
122+
}
123+
}
124+
125+
#[cfg(test)]
126+
mod tests {
127+
use super::*;
128+
129+
#[test]
130+
fn parses_single_arg_form() {
131+
assert_eq!(
132+
parse_current_setting("SELECT current_setting('server_version_num')"),
133+
Some(("server_version_num".to_string(), false))
134+
);
135+
}
136+
137+
#[test]
138+
fn parses_missing_ok_true() {
139+
assert_eq!(
140+
parse_current_setting("SELECT current_setting('nodedb.foo', true)"),
141+
Some(("nodedb.foo".to_string(), true))
142+
);
143+
}
144+
145+
#[test]
146+
fn parses_missing_ok_false() {
147+
assert_eq!(
148+
parse_current_setting("SELECT current_setting('nodedb.foo', false)"),
149+
Some(("nodedb.foo".to_string(), false))
150+
);
151+
}
152+
153+
#[test]
154+
fn lowercases_setting_name() {
155+
assert_eq!(
156+
parse_current_setting("SELECT current_setting('SERVER_VERSION')"),
157+
Some(("server_version".to_string(), false))
158+
);
159+
}
160+
161+
#[test]
162+
fn is_case_insensitive_on_keyword() {
163+
assert_eq!(
164+
parse_current_setting("select CURRENT_SETTING('server_version')"),
165+
Some(("server_version".to_string(), false))
166+
);
167+
}
168+
169+
#[test]
170+
fn accepts_trailing_semicolon() {
171+
assert_eq!(
172+
parse_current_setting("SELECT current_setting('server_version');"),
173+
Some(("server_version".to_string(), false))
174+
);
175+
}
176+
177+
#[test]
178+
fn unclear_second_arg_defaults_to_false() {
179+
assert_eq!(
180+
parse_current_setting("SELECT current_setting('server_version', maybe)"),
181+
Some(("server_version".to_string(), false))
182+
);
183+
}
184+
185+
#[test]
186+
fn rejects_unrelated_select() {
187+
assert_eq!(parse_current_setting("SELECT 1"), None);
188+
assert_eq!(parse_current_setting("SELECT version()"), None);
189+
}
190+
191+
#[test]
192+
fn rejects_empty_arg() {
193+
assert_eq!(parse_current_setting("SELECT current_setting()"), None);
194+
assert_eq!(parse_current_setting("SELECT current_setting('')"), None);
195+
}
196+
}

nodedb/src/control/server/pgwire/handler/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
mod copy_handler;
44
mod core;
5+
mod current_setting;
56
mod cursor_cmds;
67
mod cursor_query;
78
mod dispatch;

nodedb/src/control/server/pgwire/handler/session_show.rs

Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ impl NodeDbPgHandler {
2020
addr: &std::net::SocketAddr,
2121
sql: &str,
2222
) -> PgWireResult<Vec<Response>> {
23-
use super::super::session::{is_known_pg_runtime_parameter, parse_show_command};
23+
use super::super::session::parse_show_command;
2424
use pgwire::error::ErrorInfo;
2525

2626
let param = match parse_show_command(sql) {
@@ -72,48 +72,59 @@ impl NodeDbPgHandler {
7272
))]);
7373
}
7474

75-
// Resolve the value from the runtime-parameter sources in order:
76-
// built-in PG runtime constants first, then a value explicitly set
77-
// by `SET` in this session. If neither matches and the parameter
78-
// is not on the known-parameter allowlist, return `42704`
79-
// (`undefined_object`) — the same SQLSTATE PostgreSQL uses when
80-
// a client requests an unrecognised runtime parameter. This
81-
// prevents administrative commands like `SHOW DATABASES`,
82-
// `SHOW ROLES`, `SHOW STATS`, `SHOW METRICS`, `SHOW MEMORY`
83-
// from being silently swallowed as if they were unset session
84-
// parameters; those commands are routed through the DDL / AST
85-
// router before this handler is reached.
86-
let builtin = match param.as_str() {
75+
let value = self.resolve_guc(addr, &param)?;
76+
77+
let schema = Arc::new(vec![text_field(&param)]);
78+
let mut encoder = DataRowEncoder::new(schema.clone());
79+
encoder.encode_field(&value)?;
80+
let row = encoder.take_row();
81+
Ok(vec![Response::Query(QueryResponse::new(
82+
schema,
83+
futures::stream::iter(vec![Ok(row)]),
84+
))])
85+
}
86+
87+
/// Resolve a runtime parameter (GUC) value the same way `SHOW <param>`
88+
/// does: built-in PG-compat constants first, then a value explicitly
89+
/// set by `SET` in this session. If neither matches and the parameter
90+
/// is not on the known-parameter allowlist, return `42704`
91+
/// (`undefined_object`) — the same SQLSTATE PostgreSQL uses when a
92+
/// client requests an unrecognised runtime parameter. This prevents
93+
/// administrative commands like `SHOW DATABASES`, `SHOW ROLES`,
94+
/// `SHOW STATS`, `SHOW METRICS`, `SHOW MEMORY` from being silently
95+
/// swallowed as if they were unset session parameters; those commands
96+
/// are routed through the DDL / AST router before this handler is
97+
/// reached.
98+
pub(super) fn resolve_guc(
99+
&self,
100+
addr: &std::net::SocketAddr,
101+
param: &str,
102+
) -> PgWireResult<String> {
103+
use super::super::session::is_known_pg_runtime_parameter;
104+
use pgwire::error::ErrorInfo;
105+
106+
let builtin = match param {
87107
"server_version" => Some(format!("NodeDB {}", crate::version::VERSION)),
88108
"server_version_num" => Some(nodedb_types::pg_compat::PG_COMPAT_VERSION_NUM.to_owned()),
89109
"server_encoding" => Some("UTF8".into()),
90110
_ => None,
91111
};
92-
let session_value = self.sessions.get_parameter(addr, &param);
112+
let session_value = self.sessions.get_parameter(addr, param);
93113

94-
let value = match (builtin, session_value) {
95-
(Some(v), _) => v,
96-
(None, Some(v)) => v,
114+
match (builtin, session_value) {
115+
(Some(v), _) => Ok(v),
116+
(None, Some(v)) => Ok(v),
97117
(None, None) => {
98-
if !is_known_pg_runtime_parameter(&param) {
118+
if !is_known_pg_runtime_parameter(param) {
99119
return Err(PgWireError::UserError(Box::new(ErrorInfo::new(
100120
"ERROR".to_owned(),
101121
"42704".to_owned(),
102122
format!("unrecognized configuration parameter \"{param}\""),
103123
))));
104124
}
105-
String::new()
125+
Ok(String::new())
106126
}
107-
};
108-
109-
let schema = Arc::new(vec![text_field(&param)]);
110-
let mut encoder = DataRowEncoder::new(schema.clone());
111-
encoder.encode_field(&value)?;
112-
let row = encoder.take_row();
113-
Ok(vec![Response::Query(QueryResponse::new(
114-
schema,
115-
futures::stream::iter(vec![Ok(row)]),
116-
))])
127+
}
117128
}
118129

119130
/// SHOW ALL — return all session parameters.

nodedb/src/control/server/pgwire/handler/sql_exec.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,10 @@ impl NodeDbPgHandler {
292292
.await;
293293
}
294294

295+
if upper.starts_with("SELECT CURRENT_SETTING") {
296+
return self.handle_current_setting(addr, sql_trimmed);
297+
}
298+
295299
// ── USE DATABASE — session reset ──────────────────────────────
296300
// Intercepted before the DDL router because it requires access to both
297301
// `self.sessions` and `addr` for the per-connection state reset.

nodedb/tests/wire_server_version.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,42 @@ async fn pgwire_show_server_version_num_returns_pg_compat_number() {
4444
);
4545
}
4646

47+
#[tokio::test]
48+
async fn pgwire_current_setting_server_version_num() {
49+
let srv = TestServer::start().await;
50+
let rows = srv
51+
.query_text("SELECT current_setting('server_version_num')")
52+
.await
53+
.unwrap();
54+
assert_eq!(rows.len(), 1, "got {rows:?}");
55+
assert!(
56+
rows[0].trim().parse::<i64>().is_ok(),
57+
"must be numeric, got {rows:?}"
58+
);
59+
}
60+
61+
#[tokio::test]
62+
async fn pgwire_current_setting_server_version_is_nodedb() {
63+
let srv = TestServer::start().await;
64+
let rows = srv
65+
.query_text("SELECT current_setting('server_version')")
66+
.await
67+
.unwrap();
68+
assert_eq!(rows.len(), 1);
69+
assert!(rows[0].contains("NodeDB"), "got {rows:?}");
70+
}
71+
72+
#[tokio::test]
73+
async fn pgwire_current_setting_unknown_missing_ok_true_is_null() {
74+
let srv = TestServer::start().await;
75+
// missing_ok = true → NULL (empty text over the wire), not an error.
76+
let rows = srv
77+
.query_text("SELECT current_setting('nodedb.does_not_exist', true)")
78+
.await
79+
.unwrap();
80+
assert_eq!(rows.len(), 1, "got {rows:?}");
81+
}
82+
4783
/// No file under `src/control/server/` may embed digits directly inside a
4884
/// `"NodeDB ..."`, `"NodeDB/..."`, or `nodedb_version:...` literal — every
4985
/// wire-surface version must format `crate::version::VERSION` in.

0 commit comments

Comments
 (0)