Skip to main content

reddb_server/runtime/
query_request.rs

1use super::{RedDBRuntime, RuntimeQueryResult};
2use crate::storage::query::planner::shape::{bind_parameterized_query, parameterize_query_expr};
3use crate::storage::query::user_params;
4use crate::{RedDBError, RedDBResult};
5use reddb_rql::ast::QueryExpr;
6use reddb_rql::modes::parse_multi;
7use reddb_types::Value;
8use std::collections::BTreeMap;
9use std::num::NonZeroUsize;
10use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
11
12const DEFAULT_PREPARED_CAPACITY: usize = 1_024;
13
14/// Transport-neutral parameter value vocabulary from ADR 0015.
15///
16/// `UInt64` and `DecimalText` extend the ADR's ten-variant enumeration with
17/// the lossless wire/body number vocabulary. The legacy binary protocol
18/// binds unsigned parameters over the full u64 range, while JSON transports
19/// carry beyond-native precision in `$decimal` envelopes; the seam preserves
20/// both instead of narrowing them before execution.
21#[derive(Debug, Clone, PartialEq)]
22pub enum ParamValue {
23    Null,
24    Bool(bool),
25    Int64(i64),
26    UInt64(u64),
27    Float64(f64),
28    DecimalText(String),
29    Text(String),
30    Bytes(Vec<u8>),
31    Vector(Vec<f32>),
32    Json(Vec<u8>),
33    Timestamp(i64),
34    Uuid([u8; 16]),
35}
36
37impl From<ParamValue> for Value {
38    fn from(value: ParamValue) -> Self {
39        match value {
40            ParamValue::Null => Self::Null,
41            ParamValue::Bool(value) => Self::Boolean(value),
42            ParamValue::Int64(value) => Self::Integer(value),
43            ParamValue::UInt64(value) => Self::UnsignedInteger(value),
44            ParamValue::Float64(value) => Self::Float(value),
45            ParamValue::DecimalText(value) => Self::DecimalText(value),
46            ParamValue::Text(value) => Self::text(value),
47            ParamValue::Bytes(value) => Self::Blob(value),
48            ParamValue::Vector(value) => Self::Vector(value),
49            ParamValue::Json(value) => Self::Json(value),
50            ParamValue::Timestamp(value) => Self::Timestamp(value),
51            ParamValue::Uuid(value) => Self::Uuid(value),
52        }
53    }
54}
55
56impl ParamValue {
57    /// Decode the shared JSON parameter vocabulary used by HTTP, MCP, and
58    /// stdio JSON-RPC. Typed single-key envelopes preserve values ordinary
59    /// JSON cannot represent losslessly.
60    pub fn decode_json(value: &crate::json::Value) -> Result<Self, String> {
61        use crate::json::Value as JsonValue;
62
63        match value {
64            JsonValue::Null => Ok(Self::Null),
65            JsonValue::Bool(value) => Ok(Self::Bool(*value)),
66            JsonValue::Integer(value) => Ok(Self::Int64(*value)),
67            JsonValue::Number(value) => {
68                if value.is_finite()
69                    && value.fract() == 0.0
70                    && *value >= i64::MIN as f64
71                    && *value <= i64::MAX as f64
72                {
73                    Ok(Self::Int64(*value as i64))
74                } else {
75                    Ok(Self::Float64(*value))
76                }
77            }
78            JsonValue::Decimal(value) => Ok(Self::DecimalText(value.clone())),
79            JsonValue::String(value) => Ok(Self::Text(value.clone())),
80            JsonValue::Array(items) => {
81                if items.iter().all(|value| {
82                    matches!(
83                        value,
84                        JsonValue::Integer(_) | JsonValue::Number(_) | JsonValue::Decimal(_)
85                    )
86                }) {
87                    // Float32Array JSON often carries more digits than our
88                    // parser's native-number cutoff and arrives as Decimal.
89                    // It is still a numeric vector, not an opaque JSON value.
90                    Ok(Self::Vector(
91                        items
92                            .iter()
93                            .map(|value| {
94                                value
95                                    .as_f64()
96                                    .map(|number| number as f32)
97                                    .ok_or_else(|| "invalid numeric vector coordinate".to_string())
98                            })
99                            .collect::<Result<Vec<_>, _>>()?,
100                    ))
101                } else {
102                    Ok(Self::Json(crate::json::to_vec(value).unwrap_or_default()))
103                }
104            }
105            JsonValue::Object(map) => {
106                if map.len() == 1 {
107                    if let Some(JsonValue::String(encoded)) = map.get("$bytes") {
108                        if let Ok(bytes) = decode_base64(encoded) {
109                            return Ok(Self::Bytes(bytes));
110                        }
111                    }
112                    if let Some(value) = map.get("$ts") {
113                        if let Some(timestamp) = json_i64(value) {
114                            return Ok(Self::Timestamp(timestamp));
115                        }
116                    }
117                    if let Some(JsonValue::String(value)) = map.get("$uuid") {
118                        if let Ok(uuid) = crate::crypto::Uuid::parse_str(value) {
119                            return Ok(Self::Uuid(*uuid.as_bytes()));
120                        }
121                    }
122                    if let Some(JsonValue::String(encoded)) = map.get("$float") {
123                        return Ok(match encoded.as_str() {
124                            "NaN" => Self::Float64(f64::NAN),
125                            "Infinity" | "+Infinity" | "inf" | "+inf" => {
126                                Self::Float64(f64::INFINITY)
127                            }
128                            "-Infinity" | "-inf" => Self::Float64(f64::NEG_INFINITY),
129                            _ => Self::Json(crate::json::to_vec(value).unwrap_or_default()),
130                        });
131                    }
132                    if map.contains_key("$number") || map.contains_key("$decimalText") {
133                        return Err("superseded exact-number envelope".to_string());
134                    }
135                    if let Some(value) = map.get("$int") {
136                        return json_i64(value)
137                            .map(Self::Int64)
138                            .ok_or_else(|| "invalid $int exact-number envelope".to_string());
139                    }
140                    if let Some(value) = map.get("$uint") {
141                        if let JsonValue::String(value) = value {
142                            return value
143                                .parse::<u64>()
144                                .map(Self::UInt64)
145                                .map_err(|_| "invalid $uint exact-number envelope".to_string());
146                        }
147                        return Err("invalid $uint exact-number envelope".to_string());
148                    }
149                    if let Some(JsonValue::String(value)) = map.get("$decimal") {
150                        return Ok(Self::DecimalText(value.clone()));
151                    }
152                }
153                Ok(Self::Json(crate::json::to_vec(value).unwrap_or_default()))
154            }
155        }
156    }
157
158    /// Decode a general JSON payload while preserving unrecognized typed
159    /// envelopes as JSON. Row insertion historically accepts those objects
160    /// as document values rather than rejecting the request as a bind error.
161    pub fn decode_json_lossy(value: &crate::json::Value) -> Self {
162        Self::decode_json(value)
163            .unwrap_or_else(|_| Self::Json(crate::json::to_vec(value).unwrap_or_default()))
164    }
165}
166
167fn json_i64(value: &crate::json::Value) -> Option<i64> {
168    match value {
169        crate::json::Value::Integer(value) => Some(*value),
170        crate::json::Value::Number(value) => {
171            if value.is_finite()
172                && value.fract() == 0.0
173                && *value >= i64::MIN as f64
174                && *value <= i64::MAX as f64
175            {
176                Some(*value as i64)
177            } else {
178                None
179            }
180        }
181        crate::json::Value::String(value) => value.parse::<i64>().ok(),
182        _ => None,
183    }
184}
185
186fn decode_base64(input: &str) -> Result<Vec<u8>, String> {
187    let bytes = input.as_bytes();
188    if !bytes.len().is_multiple_of(4) {
189        return Err("base64 length must be a multiple of 4".to_string());
190    }
191    let mut output = Vec::with_capacity(bytes.len() / 4 * 3);
192    for chunk in bytes.chunks_exact(4) {
193        let padding = chunk.iter().rev().take_while(|&&byte| byte == b'=').count();
194        let a = base64_value(chunk[0])?;
195        let b = base64_value(chunk[1])?;
196        let c = if chunk[2] == b'=' {
197            0
198        } else {
199            base64_value(chunk[2])?
200        };
201        let d = if chunk[3] == b'=' {
202            0
203        } else {
204            base64_value(chunk[3])?
205        };
206        let decoded = ((a as u32) << 18) | ((b as u32) << 12) | ((c as u32) << 6) | d as u32;
207        output.push(((decoded >> 16) & 0xff) as u8);
208        if padding < 2 {
209            output.push(((decoded >> 8) & 0xff) as u8);
210        }
211        if padding < 1 {
212            output.push((decoded & 0xff) as u8);
213        }
214    }
215    Ok(output)
216}
217
218fn base64_value(byte: u8) -> Result<u8, String> {
219    match byte {
220        b'A'..=b'Z' => Ok(byte - b'A'),
221        b'a'..=b'z' => Ok(byte - b'a' + 26),
222        b'0'..=b'9' => Ok(byte - b'0' + 52),
223        b'+' => Ok(62),
224        b'/' => Ok(63),
225        b'=' => Ok(0),
226        _ => Err(format!("invalid base64 character: {}", byte as char)),
227    }
228}
229
230#[derive(Debug, Clone)]
231enum QuerySource {
232    Sql(String),
233    Prepared(PreparedId),
234}
235
236#[derive(Debug, Clone)]
237pub struct QueryRequest {
238    source: QuerySource,
239    params: Vec<ParamValue>,
240    commit_policy: Option<crate::replication::CommitPolicy>,
241}
242
243impl QueryRequest {
244    pub fn sql(sql: impl Into<String>, params: Vec<ParamValue>) -> Self {
245        Self {
246            source: QuerySource::Sql(sql.into()),
247            params,
248            commit_policy: None,
249        }
250    }
251
252    pub fn prepared(prepared_id: PreparedId, params: Vec<ParamValue>) -> Self {
253        Self {
254            source: QuerySource::Prepared(prepared_id),
255            params,
256            commit_policy: None,
257        }
258    }
259
260    pub fn with_commit_policy(mut self, commit_policy: crate::replication::CommitPolicy) -> Self {
261        self.commit_policy = Some(commit_policy);
262        self
263    }
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
267pub struct PreparedId(u64);
268
269impl PreparedId {
270    pub const fn from_raw(value: u64) -> Self {
271        Self(value)
272    }
273
274    pub const fn into_raw(self) -> u64 {
275        self.0
276    }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
280pub struct PreparedQuery {
281    pub id: PreparedId,
282    pub parameter_count: usize,
283}
284
285/// How a prepared shape names its parameter slots.
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287enum ParamBinding {
288    /// The parser emitted one `Expr::Parameter` per client `$N` placeholder.
289    UserPlaceholders,
290    /// The planner's shape cache turned every literal into a parameter slot.
291    /// This is what the legacy binary protocol has always prepared: `WHERE
292    /// id = 5` reports one parameter in PREPARED_OK and clients bind against
293    /// that count.
294    AutoParameterizedLiterals,
295}
296
297#[derive(Debug, Clone)]
298struct PreparedStatement {
299    sql: String,
300    shape: QueryExpr,
301    parameter_count: usize,
302    binding: ParamBinding,
303    ddl_epoch: u64,
304}
305
306/// Connection-owned prepared-statement state. A registry must not be shared
307/// between logical connections; IDs resolve only in the instance that minted them.
308pub struct PreparedRegistry {
309    statements: parking_lot::RwLock<BTreeMap<PreparedId, PreparedStatement>>,
310    next_id: AtomicU64,
311    #[cfg(test)]
312    parse_count: AtomicU64,
313    enabled: AtomicBool,
314    capacity: NonZeroUsize,
315}
316
317impl PreparedRegistry {
318    /// Build a connection-scoped registry bounded to 1,024 entries. Once
319    /// full, preparing a query evicts the oldest prepared ID.
320    pub fn new() -> Self {
321        Self::with_nonzero_capacity(
322            NonZeroUsize::new(DEFAULT_PREPARED_CAPACITY)
323                .expect("invariant: DEFAULT_PREPARED_CAPACITY is non-zero"),
324        )
325    }
326
327    /// Build a connection-scoped registry with oldest-ID eviction at `capacity`.
328    pub fn with_capacity(capacity: usize) -> RedDBResult<Self> {
329        let capacity = NonZeroUsize::new(capacity).ok_or_else(|| {
330            RedDBError::InvalidOperation(
331                "prepared registry capacity must be greater than zero".to_string(),
332            )
333        })?;
334        Ok(Self::with_nonzero_capacity(capacity))
335    }
336
337    fn with_nonzero_capacity(capacity: NonZeroUsize) -> Self {
338        Self {
339            statements: parking_lot::RwLock::new(BTreeMap::new()),
340            next_id: AtomicU64::new(1),
341            #[cfg(test)]
342            parse_count: AtomicU64::new(0),
343            enabled: AtomicBool::new(!prepared_disabled_from_env()),
344            capacity,
345        }
346    }
347
348    /// Prepare a statement whose parameters are the client's `$N`
349    /// placeholders (ADR 0015).
350    pub fn prepare(&self, runtime: &RedDBRuntime, sql: &str) -> RedDBResult<PreparedQuery> {
351        self.ensure_enabled()?;
352        #[cfg(test)]
353        self.parse_count.fetch_add(1, Ordering::Relaxed);
354        let shape = parse_multi(sql).map_err(|error| RedDBError::Query(error.to_string()))?;
355        let indices = user_params::collect_indices(&shape);
356        let parameter_count = indices.iter().copied().max().map_or(0, |index| index + 1);
357        user_params::validate(&indices, parameter_count)
358            .map_err(|error| RedDBError::Query(error.to_string()))?;
359        Ok(self.insert(
360            runtime,
361            sql,
362            shape,
363            parameter_count,
364            ParamBinding::UserPlaceholders,
365        ))
366    }
367
368    /// Prepare a statement whose parameters are the planner's
369    /// auto-parameterized literals instead of `$N` placeholders.
370    ///
371    /// The legacy binary protocol reports `WHERE id = 5` as one parameter in
372    /// PREPARED_OK and its clients bind positionally against that count, so
373    /// the wire prepares through here. `prepare` stays the entry point for
374    /// transports that speak ADR 0015 placeholders.
375    pub fn prepare_auto_parameterized(
376        &self,
377        runtime: &RedDBRuntime,
378        sql: &str,
379    ) -> RedDBResult<PreparedQuery> {
380        self.ensure_enabled()?;
381        let parsed = parse_multi(sql).map_err(|error| RedDBError::Query(error.to_string()))?;
382        // Runtime-side view rewrite runs at execute time, not prepare — view
383        // bodies may change between the two on another thread, and rewriting
384        // here would pin stale bodies into the shape.
385        let (shape, parameter_count) = match parameterize_query_expr(&parsed) {
386            Some(parameterized) => (parameterized.shape, parameterized.parameter_count),
387            None => (parsed, 0),
388        };
389        Ok(self.insert(
390            runtime,
391            sql,
392            shape,
393            parameter_count,
394            ParamBinding::AutoParameterizedLiterals,
395        ))
396    }
397
398    fn insert(
399        &self,
400        runtime: &RedDBRuntime,
401        sql: &str,
402        shape: QueryExpr,
403        parameter_count: usize,
404        binding: ParamBinding,
405    ) -> PreparedQuery {
406        let mut statements = self.statements.write();
407        let id = PreparedId(self.next_id.fetch_add(1, Ordering::Relaxed));
408        if statements.len() == self.capacity.get() {
409            statements.pop_first();
410        }
411        statements.insert(
412            id,
413            PreparedStatement {
414                sql: sql.to_string(),
415                shape,
416                parameter_count,
417                binding,
418                ddl_epoch: runtime.ddl_epoch(),
419            },
420        );
421        PreparedQuery {
422            id,
423            parameter_count,
424        }
425    }
426
427    /// One-way emergency kill switch. Existing entries stay allocated but
428    /// cannot be prepared or executed after this call.
429    pub fn disable(&self) {
430        self.enabled.store(false, Ordering::Release);
431    }
432
433    /// Whether prepared statements are still served on this connection.
434    /// Transports gate their handlers on this so the kill switch keeps its
435    /// own wire error text instead of inheriting an execution error's.
436    pub fn is_enabled(&self) -> bool {
437        self.enabled.load(Ordering::Acquire)
438    }
439
440    /// DDL epoch captured when `id` was prepared, or `None` when the entry is
441    /// gone. Transports check it themselves to keep the wire's
442    /// `prepared_needs_replan` precedence ahead of their arity check.
443    pub fn ddl_epoch(&self, id: PreparedId) -> Option<u64> {
444        self.statements
445            .read()
446            .get(&id)
447            .map(|statement| statement.ddl_epoch)
448    }
449
450    #[cfg(test)]
451    pub(crate) fn parse_count(&self) -> u64 {
452        self.parse_count.load(Ordering::Relaxed)
453    }
454
455    /// Drop a prepared entry — DEALLOCATE frees the shape, not just the
456    /// transport's wire-id mapping.
457    pub fn release(&self, id: PreparedId) {
458        self.statements.write().remove(&id);
459    }
460
461    fn get(&self, id: PreparedId) -> RedDBResult<PreparedStatement> {
462        self.ensure_enabled()?;
463        self.statements
464            .read()
465            .get(&id)
466            .cloned()
467            .ok_or_else(|| RedDBError::Query("prepared statement not found or expired".to_string()))
468    }
469
470    fn ensure_enabled(&self) -> RedDBResult<()> {
471        if self.enabled.load(Ordering::Acquire) {
472            Ok(())
473        } else {
474            Err(RedDBError::Query(
475                "prepared statements disabled".to_string(),
476            ))
477        }
478    }
479}
480
481/// `REDDB_DISABLE_PREPARED=1` forces clients onto the plain query path by
482/// making every registry operation fail. Read once per process: registries are
483/// per-connection and the binary listener builds one per text query, so this
484/// must not cost an environment lookup on the hot path.
485fn prepared_disabled_from_env() -> bool {
486    static FLAG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
487    *FLAG.get_or_init(|| {
488        std::env::var("REDDB_DISABLE_PREPARED")
489            .ok()
490            .is_some_and(|value| {
491                matches!(
492                    value.trim().to_ascii_lowercase().as_str(),
493                    "1" | "true" | "on" | "yes"
494                )
495            })
496    })
497}
498
499impl Default for PreparedRegistry {
500    fn default() -> Self {
501        Self::new()
502    }
503}
504
505#[derive(Debug, Clone)]
506enum BoundStatement {
507    Sql { sql: String, params: Vec<Value> },
508    // Boxed: a bound `QueryExpr` dwarfs the text variant.
509    Prepared { sql: String, expr: Box<QueryExpr> },
510}
511
512/// A request resolved to the statement that will run, before it runs.
513///
514/// Transports with a zero-copy fast path — the binary listener's direct scan —
515/// need the bound `QueryExpr` to test scan eligibility. Handing it out here
516/// keeps a single binder instead of forking one back into the transport.
517#[derive(Debug, Clone)]
518pub struct BoundRequest {
519    statement: BoundStatement,
520    commit_policy: Option<crate::replication::CommitPolicy>,
521}
522
523impl BoundRequest {
524    /// The bound expression of a prepared request, or `None` for text SQL,
525    /// which the runtime parses inside its own statement frame.
526    pub fn prepared_expr(&self) -> Option<&QueryExpr> {
527        match &self.statement {
528            BoundStatement::Prepared { expr, .. } => Some(expr.as_ref()),
529            BoundStatement::Sql { .. } => None,
530        }
531    }
532}
533
534pub struct QueryRequestExecutor<'a> {
535    runtime: &'a RedDBRuntime,
536    prepared: &'a PreparedRegistry,
537}
538
539impl<'a> QueryRequestExecutor<'a> {
540    pub fn new(runtime: &'a RedDBRuntime, prepared: &'a PreparedRegistry) -> Self {
541        Self { runtime, prepared }
542    }
543
544    /// Resolve a request to its bound statement without executing it.
545    pub fn bind(&self, request: QueryRequest) -> RedDBResult<BoundRequest> {
546        let commit_policy = request.commit_policy;
547        let params = request
548            .params
549            .into_iter()
550            .map(Value::from)
551            .collect::<Vec<_>>();
552        let statement = match request.source {
553            QuerySource::Sql(sql) => {
554                self.reject_weaker_commit_policy(&sql, commit_policy)?;
555                BoundStatement::Sql { sql, params }
556            }
557            QuerySource::Prepared(id) => {
558                let prepared = self.prepared.get(id)?;
559                if prepared.ddl_epoch != self.runtime.ddl_epoch() {
560                    return Err(RedDBError::Query("prepared_needs_replan".to_string()));
561                }
562                self.reject_weaker_commit_policy(&prepared.sql, commit_policy)?;
563                let expr = match prepared.binding {
564                    ParamBinding::UserPlaceholders => {
565                        user_params::bind(&prepared.shape, &params)
566                            .map_err(|error| RedDBError::Query(error.to_string()))?
567                    }
568                    // A shape with no slots is already executable. Statement
569                    // kinds the planner does not auto-parameterize (DML, DDL,
570                    // commands) land here, and the binder rejects those
571                    // outright rather than returning them unchanged.
572                    ParamBinding::AutoParameterizedLiterals if prepared.parameter_count == 0 => {
573                        prepared.shape.clone()
574                    }
575                    ParamBinding::AutoParameterizedLiterals => {
576                        bind_parameterized_query(&prepared.shape, &params, prepared.parameter_count)
577                            .ok_or_else(|| RedDBError::Query("prepared bind failed".to_string()))?
578                    }
579                };
580                BoundStatement::Prepared {
581                    sql: prepared.sql,
582                    expr: Box::new(expr),
583                }
584            }
585        };
586        Ok(BoundRequest {
587            statement,
588            commit_policy,
589        })
590    }
591
592    /// Execute an already-bound request and enforce its commit policy.
593    pub fn execute_bound(&self, bound: BoundRequest) -> RedDBResult<RuntimeQueryResult> {
594        let commit_policy = bound.commit_policy;
595        let result = match bound.statement {
596            BoundStatement::Sql { sql, params } => {
597                self.runtime.execute_query_with_params(&sql, &params)
598            }
599            BoundStatement::Prepared { sql, expr } => {
600                self.runtime.execute_prepared_query(&sql, *expr)
601            }
602        }?;
603        if matches!(result.statement_type, "insert" | "update" | "delete") {
604            self.runtime
605                .enforce_commit_policy_for_request(self.runtime.cdc_current_lsn(), commit_policy)?;
606        }
607        Ok(result)
608    }
609
610    pub fn execute(&self, request: QueryRequest) -> RedDBResult<RuntimeQueryResult> {
611        self.execute_bound(self.bind(request)?)
612    }
613
614    /// Refuse a per-request policy weaker than the resolved floor before the
615    /// write lands.
616    ///
617    /// Gated on mutating statements: a read never consulted the commit path
618    /// before this seam existed, and must not start failing because a client
619    /// attached a weak policy to a SELECT. Requests that carry no override
620    /// resolve to the floor by definition, so they skip the classifier
621    /// entirely and the hot text-query path stays free.
622    fn reject_weaker_commit_policy(
623        &self,
624        sql: &str,
625        commit_policy: Option<crate::replication::CommitPolicy>,
626    ) -> RedDBResult<()> {
627        if commit_policy.is_none() || !super::statement_frame::statement_is_write(sql) {
628            return Ok(());
629        }
630        self.runtime
631            .resolve_request_commit_policy(commit_policy)
632            .map(|_| ())
633            .map_err(super::impl_replication_commit::commit_policy_violation_error)
634    }
635}