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#[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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287enum ParamBinding {
288 UserPlaceholders,
290 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
306pub 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 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 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 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 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 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 pub fn disable(&self) {
430 self.enabled.store(false, Ordering::Release);
431 }
432
433 pub fn is_enabled(&self) -> bool {
437 self.enabled.load(Ordering::Acquire)
438 }
439
440 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 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
481fn 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 Prepared { sql: String, expr: Box<QueryExpr> },
510}
511
512#[derive(Debug, Clone)]
518pub struct BoundRequest {
519 statement: BoundStatement,
520 commit_policy: Option<crate::replication::CommitPolicy>,
521}
522
523impl BoundRequest {
524 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 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, ¶ms)
566 .map_err(|error| RedDBError::Query(error.to_string()))?
567 }
568 ParamBinding::AutoParameterizedLiterals if prepared.parameter_count == 0 => {
573 prepared.shape.clone()
574 }
575 ParamBinding::AutoParameterizedLiterals => {
576 bind_parameterized_query(&prepared.shape, ¶ms, 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 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, ¶ms)
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 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}