Skip to content

Commit 78e66c0

Browse files
committed
fix(pgwire): advertise libpq-parseable server version
1 parent 8a029bc commit 78e66c0

10 files changed

Lines changed: 415 additions & 64 deletions

File tree

docs/protocols.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ psql -h localhost -p 6432
2828

2929
**SQL coverage:** Everything in the [query language reference](query-language.md).
3030

31-
**Driver compatibility:** NodeDB advertises `server_version` (`NodeDB <version>`) and a PostgreSQL-compatible `server_version_num` in the startup parameter burst, and supports the probes drivers issue on connect: `version()` returns a PostgreSQL-compatible string, `current_setting(name [, missing_ok])` resolves the same settings as `SHOW`, and `::regclass`/`::regtype` casts, `ANY(current_schemas(...))`, and cross-catalog-table JOINs evaluate PostgreSQL-identically. Binary result formats requested at Bind are honored per column; types the encoder cannot yet emit in binary (timestamp, numeric, json/jsonb, arrays) downgrade to text, with the Describe-phase RowDescription kept in sync.
31+
**Driver compatibility:** NodeDB advertises a libpq-parseable `server_version` (`15.0 (NodeDB <version>)`) and the matching PostgreSQL-compatible `server_version_num` in the startup parameter burst. It also supports the probes drivers issue on connect: `version()` returns a PostgreSQL-compatible string, `current_setting(name [, missing_ok])` resolves the same settings as `SHOW`, and `::regclass`/`::regtype` casts, `ANY(current_schemas(...))`, and cross-catalog-table JOINs evaluate PostgreSQL-identically. Binary result formats requested at Bind are honored per column; types the encoder cannot yet emit in binary (timestamp, numeric, json/jsonb, arrays) downgrade to text, with the Describe-phase RowDescription kept in sync.
3232

3333
**Introspection:** NodeDB exposes PostgreSQL-compatible `pg_catalog` virtual tables (e.g., `pg_class`, `pg_namespace`, `pg_attribute`, `pg_type`) so that standard Postgres clients, ORMs, and business intelligence tools can introspect the database schema without modification (`psql \d`, driver type caches, ORM bootstraps). Queries against `pg_catalog.*` tables are transparently rewritten to pull from NodeDB's internal catalog.
3434

docs/query-language.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1076,7 +1076,9 @@ RESET nodedb.consistency;
10761076

10771077
-- Server version (PostgreSQL-compatible)
10781078
SELECT version(); -- 'PostgreSQL 15 ... NodeDB ...'
1079-
SHOW server_version_num;
1079+
SHOW server_version; -- '15.0 (NodeDB <version>)'
1080+
SELECT current_setting('server_version');
1081+
SHOW server_version_num; -- '150000'
10801082
SELECT current_setting('server_version_num');
10811083
SELECT current_setting('nodedb.foo', true); -- missing_ok: NULL if unset
10821084

nodedb-types/src/pg_compat.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ pub const PG_COMPAT_VERSION: &str = "15.0";
1111
/// `server_version_num` value: MAJOR*10000 + MINOR*100 + PATCH (15.0 -> 150000).
1212
pub const PG_COMPAT_VERSION_NUM: &str = "150000";
1313

14+
/// PostgreSQL-compatible `server_version` value announced during pgwire
15+
/// startup and exposed through runtime settings. libpq parses the leading
16+
/// numeric version; the suffix preserves NodeDB's build identity.
17+
pub fn server_version_string(nodedb_version: &str) -> String {
18+
format!("{PG_COMPAT_VERSION} (NodeDB {nodedb_version})")
19+
}
20+
1421
/// The string returned by the SQL `version()` function, mirroring
1522
/// PostgreSQL's `"PostgreSQL <ver> on <triple> ..."` shape so clients that
1623
/// parse the leading `"PostgreSQL <major>"` succeed.
@@ -25,6 +32,11 @@ pub fn version_string() -> String {
2532
mod tests {
2633
use super::*;
2734

35+
#[test]
36+
fn server_version_starts_numeric_and_preserves_nodedb_build() {
37+
assert_eq!(server_version_string("0.4.0"), "15.0 (NodeDB 0.4.0)");
38+
}
39+
2840
#[test]
2941
fn version_string_starts_with_postgres_major() {
3042
assert!(version_string().starts_with("PostgreSQL 15"));

nodedb/src/control/server/pgwire/factory/provider.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ use pgwire::api::auth::DefaultServerParameterProvider;
88
/// of parameters and has no `server_version_num`) and augments it with
99
/// `server_version_num` so PostgreSQL clients that inspect the numeric server
1010
/// version at connect time (e.g. drivers gating feature use on it) receive it
11-
/// in the startup `ParameterStatus` burst. `server_version` is overridden to
12-
/// NodeDB's own version string.
11+
/// in the startup `ParameterStatus` burst. `server_version` starts with the
12+
/// compatible PostgreSQL version so libpq can parse it, followed by NodeDB's
13+
/// build identity.
1314
#[derive(Debug)]
1415
pub(super) struct NodeDbParameterProvider {
1516
inner: DefaultServerParameterProvider,
@@ -18,7 +19,8 @@ pub(super) struct NodeDbParameterProvider {
1819
impl NodeDbParameterProvider {
1920
fn new() -> Self {
2021
let mut inner = DefaultServerParameterProvider::default();
21-
inner.server_version = format!("NodeDB {}", crate::version::VERSION);
22+
inner.server_version =
23+
nodedb_types::pg_compat::server_version_string(crate::version::VERSION);
2224
Self { inner }
2325
}
2426
}
@@ -48,8 +50,9 @@ mod tests {
4850
use pgwire::api::auth::ServerParameterProvider;
4951

5052
/// The custom provider used by BOTH startup paths must emit NodeDB's own
51-
/// `server_version` and the PG-compat `server_version_num` in the startup
52-
/// parameter set, on top of pgwire's default fixed parameters.
53+
/// a libpq-parseable `server_version` and the PG-compat
54+
/// `server_version_num` in the startup parameter set, on top of pgwire's
55+
/// default fixed parameters.
5356
#[test]
5457
fn parameter_provider_advertises_server_version_num_and_nodedb_version() {
5558
let addr = "127.0.0.1:5432"
@@ -69,8 +72,10 @@ mod tests {
6972
);
7073
assert_eq!(
7174
params.get("server_version").cloned(),
72-
Some(format!("NodeDB {}", crate::version::VERSION)),
73-
"startup params must advertise NodeDB server_version, got {params:?}"
75+
Some(nodedb_types::pg_compat::server_version_string(
76+
crate::version::VERSION,
77+
)),
78+
"startup params must advertise a libpq-parseable server_version, got {params:?}"
7479
);
7580
}
7681
}

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ impl NodeDbPgHandler {
104104
use pgwire::error::ErrorInfo;
105105

106106
let builtin = match param {
107-
"server_version" => Some(format!("NodeDB {}", crate::version::VERSION)),
107+
"server_version" => Some(nodedb_types::pg_compat::server_version_string(
108+
crate::version::VERSION,
109+
)),
108110
"server_version_num" => Some(nodedb_types::pg_compat::PG_COMPAT_VERSION_NUM.to_owned()),
109111
"server_encoding" => Some("UTF8".into()),
110112
_ => None,
@@ -138,9 +140,15 @@ impl NodeDbPgHandler {
138140
let mut rows = Vec::with_capacity(params.len());
139141
let mut encoder = DataRowEncoder::new(schema.clone());
140142

141-
for (key, value) in &params {
143+
for (key, session_value) in &params {
144+
let value = match key.as_str() {
145+
"server_version" | "server_version_num" | "server_encoding" => {
146+
self.resolve_guc(addr, key)?
147+
}
148+
_ => session_value.clone(),
149+
};
142150
encoder.encode_field(key)?;
143-
encoder.encode_field(value)?;
151+
encoder.encode_field(&value)?;
144152
rows.push(Ok(encoder.take_row()));
145153
}
146154

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,25 @@ impl NodeDbPgHandler {
238238
// clear the session's effective_tenant_id override (not just an
239239
// entry in the parameter bag). All policy checks (superuser,
240240
// no-active-txn) live in handle_reset_tenant.
241-
if param == "tenant" {
241+
if param == "tenant" || param == "nodedb.tenant_id" {
242242
return self.handle_reset_tenant(identity, addr);
243243
}
244-
self.sessions.set_parameter(addr, param, String::new());
244+
if param == "all" {
245+
if self.sessions.get_effective_tenant_id(addr).is_some() {
246+
self.handle_reset_tenant(identity, addr)?;
247+
}
248+
self.sessions.reset_all_parameters(addr);
249+
return Ok(vec![Response::Execution(Tag::new("RESET"))]);
250+
}
251+
if !crate::control::server::shared::session::is_known_settable_runtime_parameter(&param)
252+
{
253+
return Err(PgWireError::UserError(Box::new(ErrorInfo::new(
254+
"ERROR".to_owned(),
255+
"42704".to_owned(),
256+
format!("unrecognized configuration parameter \"{param}\""),
257+
))));
258+
}
259+
self.sessions.reset_parameter(addr, &param);
245260
return Ok(vec![Response::Execution(Tag::new("RESET"))]);
246261
}
247262

nodedb/src/control/server/shared/session/params.rs

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,41 @@ impl SessionStore {
1616
});
1717
}
1818

19+
/// Reset one mutable session parameter to its connection default.
20+
pub fn reset_parameter(&self, addr: &SocketAddr, key: &str) {
21+
let defaults = super::state::default_parameters();
22+
let default = defaults
23+
.into_iter()
24+
.find(|(name, _)| name.eq_ignore_ascii_case(key));
25+
self.write_session(addr, |session| {
26+
session
27+
.parameters
28+
.retain(|name, _| !name.eq_ignore_ascii_case(key));
29+
if let Some((name, value)) = default {
30+
session.parameters.insert(name, value);
31+
}
32+
});
33+
}
34+
35+
/// Reset every mutable session parameter and tenant override.
36+
pub fn reset_all_parameters(&self, addr: &SocketAddr) {
37+
self.write_session(addr, |session| {
38+
session.parameters = super::state::default_parameters();
39+
session.effective_tenant_id = None;
40+
});
41+
}
42+
1943
/// Get a session parameter.
2044
pub fn get_parameter(&self, addr: &SocketAddr, key: &str) -> Option<String> {
21-
self.read_session(addr, |s| s.parameters.get(key).cloned())?
45+
self.read_session(addr, |session| {
46+
session.parameters.get(key).cloned().or_else(|| {
47+
session
48+
.parameters
49+
.iter()
50+
.find(|(name, _)| name.eq_ignore_ascii_case(key))
51+
.map(|(_, value)| value.clone())
52+
})
53+
})?
2254
}
2355

2456
/// Get all session parameters.
@@ -95,7 +127,9 @@ pub const KNOWN_PG_RUNTIME_PARAMETERS: &[&str] = &[
95127
"application_name",
96128
"client_encoding",
97129
"client_min_messages",
130+
"cross_shard_txn",
98131
"datestyle",
132+
"default_read_consistency",
99133
"default_transaction_isolation",
100134
"default_transaction_read_only",
101135
"extra_float_digits",
@@ -170,8 +204,8 @@ pub const SETTABLE_RUNTIME_PARAMETERS: &[&str] = &[
170204
"session_authorization",
171205
// NodeDB-specific session knobs settable via SET.
172206
"nodedb.consistency",
173-
"nodedb.read_consistency",
174-
"nodedb.cross_shard_mode",
207+
"default_read_consistency",
208+
"cross_shard_txn",
175209
"nodedb.tenant_id",
176210
"nodedb.auth_session",
177211
// Distributed shuffle-join override (permanent operator hint; the automatic
@@ -240,6 +274,58 @@ pub fn parse_show_command(sql: &str) -> Option<String> {
240274
mod tests {
241275
use super::*;
242276

277+
#[test]
278+
fn reset_parameter_restores_canonical_defaults() {
279+
let store = SessionStore::new();
280+
let addr = "127.0.0.1:5000".parse().expect("socket address");
281+
store.ensure_session(addr);
282+
283+
store.set_parameter(&addr, "datestyle".into(), "SQL, DMY".into());
284+
store.reset_parameter(&addr, "datestyle");
285+
assert_eq!(
286+
store.get_parameter(&addr, "datestyle"),
287+
Some("ISO, MDY".into())
288+
);
289+
290+
store.set_parameter(&addr, "default_read_consistency".into(), "eventual".into());
291+
store.reset_parameter(&addr, "default_read_consistency");
292+
assert_eq!(
293+
store.get_parameter(&addr, "default_read_consistency"),
294+
Some("strong".into())
295+
);
296+
297+
store.set_parameter(
298+
&addr,
299+
"cross_shard_txn".into(),
300+
"best_effort_non_atomic".into(),
301+
);
302+
store.reset_parameter(&addr, "cross_shard_txn");
303+
assert_eq!(
304+
store.get_parameter(&addr, "cross_shard_txn"),
305+
Some("strict".into())
306+
);
307+
}
308+
309+
#[test]
310+
fn reset_all_parameters_restores_defaults() {
311+
let store = SessionStore::new();
312+
let addr = "127.0.0.1:5000".parse().expect("socket address");
313+
store.ensure_session(addr);
314+
store.set_parameter(&addr, "application_name".into(), "worker".into());
315+
store.set_parameter(&addr, "nodedb.consistency".into(), "eventual".into());
316+
317+
store.reset_all_parameters(&addr);
318+
319+
assert_eq!(
320+
store.get_parameter(&addr, "application_name"),
321+
Some(String::new())
322+
);
323+
assert_eq!(
324+
store.get_parameter(&addr, "nodedb.consistency"),
325+
Some("strong".into())
326+
);
327+
}
328+
243329
#[test]
244330
fn parse_set_equals() {
245331
let (k, v) = parse_set_command("SET client_encoding = 'UTF8'").unwrap();

nodedb/src/control/server/shared/session/state.rs

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -196,36 +196,59 @@ pub struct ConnSession {
196196
pub own_write_versions: HashMap<(DatabaseId, TenantId, String), Lsn>,
197197
}
198198

199+
pub(super) fn default_parameters() -> HashMap<String, String> {
200+
let mut parameters = HashMap::new();
201+
// Default session parameters (PostgreSQL compatibility).
202+
parameters.insert("application_name".into(), String::new());
203+
parameters.insert("client_encoding".into(), "UTF8".into());
204+
parameters.insert("client_min_messages".into(), "notice".into());
205+
parameters.insert("server_encoding".into(), "UTF8".into());
206+
parameters.insert("DateStyle".into(), "ISO, MDY".into());
207+
parameters.insert("TimeZone".into(), "UTC".into());
208+
parameters.insert(
209+
"default_transaction_isolation".into(),
210+
"read committed".into(),
211+
);
212+
parameters.insert("default_transaction_read_only".into(), "off".into());
213+
parameters.insert("extra_float_digits".into(), "1".into());
214+
parameters.insert("IntervalStyle".into(), "postgres".into());
215+
parameters.insert("lc_collate".into(), "C".into());
216+
parameters.insert("lc_ctype".into(), "C".into());
217+
parameters.insert("lc_messages".into(), "C".into());
218+
parameters.insert("lc_monetary".into(), "C".into());
219+
parameters.insert("lc_numeric".into(), "C".into());
220+
parameters.insert("lc_time".into(), "C".into());
221+
parameters.insert("standard_conforming_strings".into(), "on".into());
222+
parameters.insert("integer_datetimes".into(), "on".into());
223+
parameters.insert("search_path".into(), "public".into());
224+
parameters.insert("statement_timeout".into(), "0".into());
225+
parameters.insert("transaction_isolation".into(), "read committed".into());
226+
parameters.insert("transaction_read_only".into(), "off".into());
227+
// Version info (PostgreSQL compatibility — tools like psql check this).
228+
parameters.insert(
229+
"server_version".into(),
230+
nodedb_types::pg_compat::server_version_string(crate::version::VERSION),
231+
);
232+
parameters.insert(
233+
"server_version_num".into(),
234+
nodedb_types::pg_compat::PG_COMPAT_VERSION_NUM.into(),
235+
);
236+
// NodeDB-specific defaults.
237+
parameters.insert("nodedb.consistency".into(), "strong".into());
238+
parameters.insert("default_read_consistency".into(), "strong".into());
239+
parameters.insert("cross_shard_txn".into(), "strict".into());
240+
parameters.insert("rounding_mode".into(), "HALF_EVEN".into());
241+
parameters
242+
}
243+
199244
impl ConnSession {
200245
pub(super) fn new() -> Self {
201-
let mut parameters = HashMap::new();
202-
// Default session parameters (PostgreSQL compatibility).
203-
parameters.insert("client_encoding".into(), "UTF8".into());
204-
parameters.insert("server_encoding".into(), "UTF8".into());
205-
parameters.insert("DateStyle".into(), "ISO, MDY".into());
206-
parameters.insert("TimeZone".into(), "UTC".into());
207-
parameters.insert("standard_conforming_strings".into(), "on".into());
208-
parameters.insert("integer_datetimes".into(), "on".into());
209-
parameters.insert("search_path".into(), "public".into());
210-
parameters.insert("transaction_isolation".into(), "read committed".into());
211-
// Version info (PostgreSQL compatibility — tools like psql check this).
212-
parameters.insert(
213-
"server_version".into(),
214-
format!("NodeDB {}", crate::version::VERSION),
215-
);
216-
parameters.insert(
217-
"server_version_num".into(),
218-
nodedb_types::pg_compat::PG_COMPAT_VERSION_NUM.into(),
219-
);
220-
// NodeDB-specific defaults.
221-
parameters.insert("nodedb.consistency".into(), "strong".into());
222-
parameters.insert("rounding_mode".into(), "HALF_EVEN".into());
223246
Self {
247+
parameters: default_parameters(),
224248
tx_state: TransactionState::Idle,
225249
current_database: None,
226250
effective_tenant_id: None,
227251
identity: None,
228-
parameters,
229252
tx_buffer: Vec::new(),
230253
tx_snapshot_lsn: None,
231254
tx_snapshot_epoch: None,

0 commit comments

Comments
 (0)