Skip to main content

surrealdb_core/dbs/
session.rs

1use std::fmt;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use anyhow::Result;
6use chrono::Utc;
7use revision::revisioned;
8use serde::{Deserialize, Serialize};
9use surrealdb_types::ToSql;
10use uuid::Uuid;
11
12use crate::iam::{Auth, Level, Role};
13use crate::kvs::impl_kv_value_revisioned;
14use crate::types::{PublicValue, PublicVariables};
15use crate::val::{Object, Value};
16
17/// Caller-supplied session input for one WebSocket connection or one HTTP/RPC request.
18///
19/// **Lifetime:** shared by many queries on that connection or request.
20/// **Source of truth:** JWT/basic auth, RPC headers, `USE` namespace/database, variables.
21///
22/// At the start of work, [`crate::kvs::Datastore::setup_options`] derives the stack-local
23/// [`crate::dbs::Options`] frame; [`crate::ctx::Context::attach_session`] copies tenant identity
24/// and realtime capability into ambient [`crate::ctx::Context`].
25#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
26pub struct Session {
27	/// The current session [`Auth`] information
28	pub au: Arc<Auth>,
29	/// Whether realtime queries are supported
30	pub rt: bool,
31	/// The current connection IP address
32	pub ip: Option<String>,
33	/// The current connection origin
34	pub or: Option<String>,
35	/// The current session ID
36	pub id: Option<Uuid>,
37	/// The currently selected namespace
38	pub ns: Option<String>,
39	/// The currently selected database
40	pub db: Option<String>,
41	/// The current access method
42	pub ac: Option<String>,
43	/// The current authentication token
44	pub tk: Option<PublicValue>,
45	/// The current record authentication data
46	pub rd: Option<PublicValue>,
47	/// The current expiration time of the session
48	pub exp: Option<i64>,
49	/// The variables set
50	pub variables: PublicVariables,
51	/// Strategy for the new streaming planner/executor.
52	pub new_planner_strategy: NewPlannerStrategy,
53	/// When true, EXPLAIN ANALYZE output omits elapsed durations, making
54	/// output deterministic for testing.
55	pub redact_volatile_explain_attrs: bool,
56}
57
58#[revisioned(revision = 1)]
59#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
60pub enum NewPlannerStrategy {
61	/// Try the new planner for read-only statements, fall back to compute on Unimplemented.
62	#[default]
63	BestEffortReadOnlyStatements,
64	/// Skip the new planner entirely; always use the compute executor.
65	ComputeOnly,
66	/// Require the new planner for all read-only statements.
67	/// Promotes Error::PlannerUnimplemented to Error::Query (hard error) instead of falling back.
68	AllReadOnlyStatements,
69}
70
71impl fmt::Display for NewPlannerStrategy {
72	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73		match self {
74			Self::BestEffortReadOnlyStatements => f.write_str("best-effort"),
75			Self::ComputeOnly => f.write_str("compute-only"),
76			Self::AllReadOnlyStatements => f.write_str("all-read-only"),
77		}
78	}
79}
80
81impl FromStr for NewPlannerStrategy {
82	type Err = String;
83
84	fn from_str(s: &str) -> Result<Self, Self::Err> {
85		match s {
86			"best-effort" => Ok(Self::BestEffortReadOnlyStatements),
87			"compute-only" => Ok(Self::ComputeOnly),
88			"all-read-only" => Ok(Self::AllReadOnlyStatements),
89			_ => Err(format!(
90				"unknown planner strategy: '{s}' (expected 'best-effort', 'compute-only', or 'all-read-only')"
91			)),
92		}
93	}
94}
95
96impl Session {
97	/// Set the selected namespace for the session
98	pub fn with_ns(mut self, ns: &str) -> Session {
99		self.ns = Some(ns.to_owned());
100		self
101	}
102
103	/// Set the selected database for the session
104	pub fn with_db(mut self, db: &str) -> Session {
105		self.db = Some(db.to_owned());
106		self
107	}
108
109	/// Set the selected access method for the session
110	pub fn with_ac(mut self, ac: &str) -> Session {
111		self.ac = Some(ac.to_owned());
112		self
113	}
114
115	// Set the realtime functionality of the session
116	pub fn with_rt(mut self, rt: bool) -> Session {
117		self.rt = rt;
118		self
119	}
120
121	/// Set the new planner strategy for the session
122	pub fn new_planner_strategy(mut self, strategy: NewPlannerStrategy) -> Session {
123		self.new_planner_strategy = strategy;
124		self
125	}
126
127	/// Retrieves the selected namespace
128	pub(crate) fn ns(&self) -> Option<Arc<str>> {
129		self.ns.as_deref().map(Into::into)
130	}
131
132	/// Retrieves the selected database
133	pub(crate) fn db(&self) -> Option<Arc<str>> {
134		self.db.as_deref().map(Into::into)
135	}
136
137	/// Checks if live queries are allowed
138	pub(crate) fn live(&self) -> bool {
139		self.rt
140	}
141
142	/// Checks if the session has expired
143	pub(crate) fn expired(&self) -> bool {
144		match self.exp {
145			Some(exp) => Utc::now().timestamp() > exp,
146			// It is currently possible to have sessions without expiration.
147			None => false,
148		}
149	}
150
151	pub(crate) fn values(&self) -> Vec<(&'static str, Value)> {
152		use crate::sql::expression::convert_public_value_to_internal;
153
154		let access = self.ac.as_deref().map(Value::from).unwrap_or(Value::None);
155		let auth = self.rd.clone().map(convert_public_value_to_internal).unwrap_or(Value::None);
156		let token = self.tk.clone().map(convert_public_value_to_internal).unwrap_or(Value::None);
157		let session = Value::from(map! {
158			"ac" => access.clone(),
159			"exp" => self.exp.map(Value::from).unwrap_or(Value::None),
160			"db" => self.db.as_deref().map(Value::from).unwrap_or(Value::None),
161			"id" => self.id.map(Value::from).unwrap_or(Value::None),
162			"ip" => self.ip.as_deref().map(Value::from).unwrap_or(Value::None),
163			"ns" => self.ns.as_deref().map(Value::from).unwrap_or(Value::None),
164			"or" => self.or.as_deref().map(Value::from).unwrap_or(Value::None),
165			"rd" => auth.clone(),
166			"tk" => token.clone(),
167		});
168
169		vec![("access", access), ("auth", auth), ("token", token), ("session", session)]
170	}
171
172	/// Create a system session for a given level and role
173	pub fn for_level(level: Level, role: Role) -> Session {
174		// Create a new session
175		let mut sess = Session::default();
176		// Set the session details
177		match level {
178			Level::Root => {
179				sess.au = Arc::new(Auth::for_root(role));
180			}
181			Level::Namespace(ns) => {
182				sess.au = Arc::new(Auth::for_ns(role, &ns));
183				sess.ns = Some(ns);
184			}
185			Level::Database(ns, db) => {
186				sess.au = Arc::new(Auth::for_db(role, &ns, &db));
187				sess.ns = Some(ns);
188				sess.db = Some(db);
189			}
190			_ => {}
191		}
192		sess
193	}
194
195	/// Create a record user session for a given NS and DB
196	pub fn for_record(ns: &str, db: &str, ac: &str, rid: PublicValue) -> Session {
197		Session {
198			ac: Some(ac.to_owned()),
199			au: Arc::new(Auth::for_record(rid.to_sql(), ns, db, ac)),
200			rt: false,
201			ip: None,
202			or: None,
203			id: None,
204			ns: Some(ns.to_owned()),
205			db: Some(db.to_owned()),
206			tk: None,
207			rd: Some(rid),
208			exp: None,
209			variables: Default::default(),
210			new_planner_strategy: NewPlannerStrategy::default(),
211			redact_volatile_explain_attrs: false,
212		}
213	}
214
215	/// Create a system session for the root level with Owner role
216	pub fn owner() -> Session {
217		Session::for_level(Level::Root, Role::Owner)
218	}
219
220	/// Create a system session for the root level with Editor role
221	pub fn editor() -> Session {
222		Session::for_level(Level::Root, Role::Editor)
223	}
224
225	/// Create a system session for the root level with Viewer role
226	pub fn viewer() -> Session {
227		Session::for_level(Level::Root, Role::Viewer)
228	}
229}
230
231/// The durable form of a client-attached RPC [`Session`], stored under
232/// [`crate::key::root::se::Se`] (`/!se{id}`) so the session survives the
233/// process that attached it and is reachable from any cluster node sharing
234/// the datastore.
235///
236/// [`Session`] itself is serde-only and holds public value types, while every
237/// stored KV value in this crate is `revision`-encoded — so this mirror
238/// carries the same fields converted to their internal revisioned forms, plus
239/// the absolute expiry of the durable copy.
240///
241/// This is an internal storage representation, scoped to the crate like the
242/// `revision`-encoded value types it holds ([`Value`], [`Object`]); callers
243/// go through the public [`Session`] via [`from_session`](Self::from_session)
244/// and [`into_session`](Self::into_session).
245#[revisioned(revision = 1)]
246#[derive(Clone, Debug, PartialEq)]
247pub(crate) struct DurableSession {
248	/// When the durable copy expires, in milliseconds since the UNIX epoch.
249	/// Enforced lazily on load and by the periodic purge task. Unrelated to
250	/// the authentication expiry in `exp`, which stays enforced at query time
251	/// via [`Session::expired`].
252	pub(crate) expires_at: u64,
253	/// The session [`Auth`] information
254	pub(crate) au: Auth,
255	/// Whether realtime queries are supported
256	pub(crate) rt: bool,
257	/// The connection IP address
258	pub(crate) ip: Option<String>,
259	/// The connection origin
260	pub(crate) or: Option<String>,
261	/// The session ID
262	pub(crate) id: Option<Uuid>,
263	/// The selected namespace
264	pub(crate) ns: Option<String>,
265	/// The selected database
266	pub(crate) db: Option<String>,
267	/// The access method
268	pub(crate) ac: Option<String>,
269	/// The authentication token
270	pub(crate) tk: Option<Value>,
271	/// The record authentication data
272	pub(crate) rd: Option<Value>,
273	/// The expiration time of the session authentication
274	pub(crate) exp: Option<i64>,
275	/// The variables set on the session
276	pub(crate) variables: Object,
277	/// Strategy for the new streaming planner/executor
278	pub(crate) new_planner_strategy: NewPlannerStrategy,
279	/// When true, EXPLAIN ANALYZE output omits elapsed durations
280	pub(crate) redact_volatile_explain_attrs: bool,
281}
282
283impl_kv_value_revisioned!(DurableSession);
284
285impl DurableSession {
286	/// Capture the durable form of a session, expiring at `expires_at`
287	/// (milliseconds since the UNIX epoch).
288	pub(crate) fn from_session(session: &Session, expires_at: u64) -> Self {
289		use crate::sql::expression::convert_public_value_to_internal;
290		Self {
291			expires_at,
292			au: (*session.au).clone(),
293			rt: session.rt,
294			ip: session.ip.clone(),
295			or: session.or.clone(),
296			id: session.id,
297			ns: session.ns.clone(),
298			db: session.db.clone(),
299			ac: session.ac.clone(),
300			tk: session.tk.clone().map(convert_public_value_to_internal),
301			rd: session.rd.clone().map(convert_public_value_to_internal),
302			exp: session.exp,
303			variables: session
304				.variables
305				.clone()
306				.into_iter()
307				.map(|(k, v)| (k, convert_public_value_to_internal(v)))
308				.collect(),
309			new_planner_strategy: session.new_planner_strategy,
310			redact_volatile_explain_attrs: session.redact_volatile_explain_attrs,
311		}
312	}
313
314	/// Restore the in-memory session this durable copy was captured from.
315	pub(crate) fn into_session(self) -> Result<Session> {
316		use crate::val::convert_value_to_public_value;
317		Ok(Session {
318			au: Arc::new(self.au),
319			rt: self.rt,
320			ip: self.ip,
321			or: self.or,
322			id: self.id,
323			ns: self.ns,
324			db: self.db,
325			ac: self.ac,
326			tk: self.tk.map(convert_value_to_public_value).transpose()?,
327			rd: self.rd.map(convert_value_to_public_value).transpose()?,
328			exp: self.exp,
329			variables: self
330				.variables
331				.into_iter()
332				.map(|(k, v)| Ok((k.into_string(), convert_value_to_public_value(v)?)))
333				.collect::<Result<PublicVariables>>()?,
334			new_planner_strategy: self.new_planner_strategy,
335			redact_volatile_explain_attrs: self.redact_volatile_explain_attrs,
336		})
337	}
338}
339
340#[cfg(test)]
341mod tests {
342	use super::*;
343
344	#[test]
345	fn json_round_trip_preserves_auth_and_context() {
346		// A root-Owner session with selected ns/db and an expiry — the shape
347		// a persisting `RpcProtocol` writes to durable storage and restores.
348		let original = Session {
349			id: Some(Uuid::from_u128(1)),
350			exp: Some(1_700_000_000),
351			..Session::owner().with_ns("app").with_db("app")
352		};
353
354		let json = serde_json::to_string(&original).expect("serialize");
355		let restored: Session = serde_json::from_str(&json).expect("deserialize");
356
357		// The whole struct round-trips, including the `Arc<Auth>` that the
358		// `arc_auth` helper serializes as its inner `Auth`.
359		assert_eq!(original, restored);
360		assert!(restored.au.is_root());
361		assert_eq!(restored.ns.as_deref(), Some("app"));
362		assert_eq!(restored.db.as_deref(), Some("app"));
363	}
364
365	/// A record-access session carrying every convertible field — auth
366	/// principal, token and record-auth values, and session variables with
367	/// non-JSON types (record ids, datetimes, decimals, nested objects) —
368	/// survives the Session -> DurableSession -> revision bytes -> Session
369	/// journey unchanged.
370	#[test]
371	fn durable_session_round_trip_preserves_all_fields() {
372		use std::collections::BTreeMap;
373
374		use surrealdb_types::{Number, Value as PV};
375
376		let mut variables = PublicVariables::default();
377		variables.insert("str", PV::String("hello".to_owned()));
378		variables.insert("dec", PV::Number(Number::Decimal("1.5".parse().unwrap())));
379		variables.insert("rid", PV::RecordId(surrealdb_types::RecordId::new("person", "tobie")));
380		variables.insert("dt", PV::Datetime(surrealdb_types::Datetime::now()));
381		variables.insert(
382			"obj",
383			PV::Object(surrealdb_types::Object::from(BTreeMap::from([("nested", PV::Bool(true))]))),
384		);
385
386		let original = Session {
387			rt: true,
388			ip: Some("10.0.0.1".to_owned()),
389			or: Some("example.com".to_owned()),
390			id: Some(Uuid::from_u128(7)),
391			exp: Some(1_700_000_000),
392			tk: Some(PV::Object(surrealdb_types::Object::from(BTreeMap::from([(
393				"iss",
394				PV::String("surrealdb".to_owned()),
395			)])))),
396			rd: Some(PV::RecordId(surrealdb_types::RecordId::new("person", "tobie"))),
397			variables,
398			redact_volatile_explain_attrs: true,
399			..Session::for_record(
400				"app",
401				"app",
402				"account",
403				PublicValue::RecordId(surrealdb_types::RecordId::new("person", "tobie")),
404			)
405		};
406
407		let durable = DurableSession::from_session(&original, 123_456_789);
408		assert_eq!(durable.expires_at, 123_456_789);
409
410		// The stored form must survive the actual KV encoding.
411		let bytes = revision::to_vec(&durable).expect("revision encode");
412		let decoded: DurableSession = revision::from_slice(&bytes).expect("revision decode");
413		assert_eq!(durable, decoded);
414
415		let restored = decoded.into_session().expect("convert back");
416		assert_eq!(original, restored);
417	}
418}