1#[cfg(feature = "full")]
3use super::local_read::{query_resident_scoped, ReadScope};
4use super::{
5 local_read::{
6 query_resident_bound, query_store_stream, source::StoreView, LocalReadError,
7 StreamQueryResult,
8 },
9 scalar::ScalarResult,
10};
11use crate::{
12 catalog::{
13 codec::{deserialize_collection_contracts, serialize_collection_contracts},
14 CollectionContract,
15 },
16 storage::store_wal::StoreWalAction,
17};
18use reddb_file::VfsFile;
19use reddb_rql::ast::{QueryExpr, TxnControl};
20use reddb_types::Value;
21use std::collections::HashMap;
22#[cfg(feature = "full")]
23use std::{collections::HashSet, path::PathBuf};
24
25mod mutation;
26#[cfg(all(feature = "full", not(target_arch = "wasm32")))]
27pub mod service;
28mod strategy;
29#[cfg(feature = "full")]
30pub mod vault;
31
32#[cfg(feature = "full")]
33use strategy::ResidentStrategy;
34use strategy::{PagedStrategy, StorageStrategy};
35
36type Result<T> = std::result::Result<T, LocalReadError>;
37
38pub const MAX_QUERY_PARAMETERS: usize = 65_536;
40
41struct Transaction<T> {
42 store: T,
43 contracts: HashMap<String, CollectionContract>,
44 actions: Vec<StoreWalAction>,
45 catalog_changed: bool,
46 aborted: bool,
47}
48
49#[cfg(feature = "full")]
52pub struct LocalDatabase<F: VfsFile> {
53 database: Database<ResidentStrategy<F>>,
54}
55
56#[doc(hidden)]
59pub struct PagedLocalDatabase<F: VfsFile + 'static> {
60 database: Database<PagedStrategy<F>>,
61}
62
63struct Database<S: StorageStrategy> {
64 storage: S,
65 contracts: HashMap<String, CollectionContract>,
66 transaction: Option<Transaction<S::Transaction>>,
67}
68
69#[cfg(feature = "full")]
70impl<F: VfsFile> LocalDatabase<F> {
71 pub fn create(file: F, name: impl Into<PathBuf>, now_ms: u64) -> Result<Self> {
72 Ok(Self {
73 database: Database::create(ResidentStrategy::create(file, name, now_ms)?),
74 })
75 }
76
77 pub fn open(file: F, name: impl Into<PathBuf>) -> Result<Self> {
78 Ok(Self {
79 database: Database::open(ResidentStrategy::open(file, name)?)?,
80 })
81 }
82
83 pub fn query(&mut self, sql: &str, now_ms: u64) -> Result<ScalarResult> {
84 self.database.query(sql, now_ms)
85 }
86
87 pub fn check_healthy(&self) -> Result<()> {
89 self.database.check_healthy()
90 }
91
92 pub fn catalog(&self) -> Result<ScalarResult> {
94 self.database.catalog()
95 }
96
97 pub fn query_stream(
98 &self,
99 sql: &str,
100 parameters: &[Value],
101 visit: &mut dyn FnMut(&[Value]) -> Result<bool>,
102 ) -> Result<StreamQueryResult> {
103 self.database.query_stream(sql, parameters, visit)
104 }
105
106 pub fn read_query(
108 &self,
109 sql: &str,
110 parameters: &[Value],
111 scopes: &[ReadScope],
112 ) -> Result<ScalarResult> {
113 self.database.read_query(sql, parameters, scopes)
114 }
115
116 pub fn query_with_params(
117 &mut self,
118 sql: &str,
119 parameters: &[Value],
120 now_ms: u64,
121 ) -> Result<ScalarResult> {
122 self.database.query_with_params(sql, parameters, now_ms)
123 }
124
125 pub fn into_inner(self) -> F {
126 self.database.storage.into_inner()
127 }
128}
129
130impl<F: VfsFile + 'static> PagedLocalDatabase<F> {
131 pub fn create(file: F) -> Result<Self> {
132 Ok(Self {
133 database: Database::create(PagedStrategy::create(file)?),
134 })
135 }
136
137 pub fn open(file: F) -> Result<Self> {
138 Ok(Self {
139 database: Database::open(PagedStrategy::open(file)?)?,
140 })
141 }
142
143 pub fn query(&mut self, sql: &str, now_ms: u64) -> Result<ScalarResult> {
144 self.database.query(sql, now_ms)
145 }
146
147 pub fn query_with_params(
148 &mut self,
149 sql: &str,
150 parameters: &[Value],
151 now_ms: u64,
152 ) -> Result<ScalarResult> {
153 self.database.query_with_params(sql, parameters, now_ms)
154 }
155
156 pub fn catalog(&self) -> Result<ScalarResult> {
157 self.database.catalog()
158 }
159
160 pub fn query_stream(
161 &self,
162 sql: &str,
163 parameters: &[Value],
164 visit: &mut dyn FnMut(&[Value]) -> Result<bool>,
165 ) -> Result<StreamQueryResult> {
166 self.database.query_stream(sql, parameters, visit)
167 }
168
169 pub fn cache_pages(&self) -> usize {
170 self.database.storage.cache_pages()
171 }
172
173 pub fn into_inner(self) -> F {
174 self.database.storage.into_inner()
175 }
176}
177
178impl<S: StorageStrategy> Database<S> {
179 fn create(storage: S) -> Self {
180 Self {
181 storage,
182 contracts: HashMap::new(),
183 transaction: None,
184 }
185 }
186
187 fn open(storage: S) -> Result<Self> {
188 let bytes = storage.committed_view()?.aux_metadata()?;
189 let mut contracts = HashMap::new();
190 if !bytes.is_empty() {
191 for contract in deserialize_collection_contracts(&bytes)
192 .map_err(|error| LocalReadError::Catalog(error.to_string()))?
193 {
194 let name = contract.name.clone();
195 if contracts.insert(name.clone(), contract).is_some() {
196 return Err(LocalReadError::Catalog(format!(
197 "duplicate collection contract: {name}"
198 )));
199 }
200 }
201 }
202 Ok(Self {
203 storage,
204 contracts,
205 transaction: None,
206 })
207 }
208
209 fn query(&mut self, sql: &str, now_ms: u64) -> Result<ScalarResult> {
210 self.query_with_params(sql, &[], now_ms)
211 }
212
213 fn check_healthy(&self) -> Result<()> {
215 self.storage.committed_view()?;
216 Ok(())
217 }
218
219 fn catalog(&self) -> Result<ScalarResult> {
221 self.check_healthy()?;
222 if self
223 .transaction
224 .as_ref()
225 .is_some_and(|transaction| transaction.aborted)
226 {
227 return Err(query_error("transaction is aborted; ROLLBACK is required"));
228 }
229 let (_, contracts) = self.visible()?;
230 let mut tables: Vec<_> = contracts.values().collect();
231 tables.sort_by(|left, right| left.name.cmp(&right.name));
232 let mut rows = Vec::new();
233 for table in tables {
234 if table.declared_model != crate::catalog::CollectionModel::Table {
235 continue;
236 }
237 for column in crate::catalog::schema::resolved_contract_columns(table)
238 .map_err(|error| LocalReadError::Catalog(error.to_string()))?
239 {
240 rows.push(vec![
241 Value::text(table.name.as_str()),
242 Value::text(column.name),
243 Value::text(column.data_type_name),
244 Value::Boolean(!column.not_null),
245 ]);
246 }
247 }
248 Ok(ScalarResult {
249 columns: ["table_name", "column_name", "data_type", "nullable"]
250 .map(String::from)
251 .to_vec(),
252 rows,
253 })
254 }
255
256 #[cfg(feature = "full")]
258 fn read_query(
259 &self,
260 sql: &str,
261 parameters: &[Value],
262 scopes: &[ReadScope],
263 ) -> Result<ScalarResult> {
264 self.check_healthy()?;
265 if parameters.len() > MAX_QUERY_PARAMETERS || scopes.len() > 64 {
266 return Err(query_error("inspection arguments exceed their limit"));
267 }
268 if self
269 .transaction
270 .as_ref()
271 .is_some_and(|transaction| transaction.aborted)
272 {
273 return Err(query_error("transaction is aborted; ROLLBACK is required"));
274 }
275 let statement = reddb_rql::parse_frontend(sql)
276 .map_err(LocalReadError::Parse)?
277 .into_query_expr();
278 let statement =
279 super::parameters::bind(&statement, parameters).map_err(LocalReadError::Parameters)?;
280 let QueryExpr::Table(query) = statement else {
281 return Err(LocalReadError::Unsupported(
282 "inspection accepts table SELECT only",
283 ));
284 };
285 let mut tables = HashSet::new();
286 if scopes.iter().any(|scope| !tables.insert(&scope.table)) {
287 return Err(query_error("duplicate inspection table scope"));
288 }
289 let scope = scopes
290 .iter()
291 .find(|scope| scope.table == query.table)
292 .ok_or(LocalReadError::Unsupported(
293 "table is outside the inspection scope",
294 ))?;
295 let (resident, contracts) = self.visible()?;
296 query_resident_scoped(resident, contracts, query, scope)
297 }
298
299 fn query_stream(
300 &self,
301 sql: &str,
302 parameters: &[Value],
303 visit: &mut dyn FnMut(&[Value]) -> Result<bool>,
304 ) -> Result<StreamQueryResult> {
305 self.check_healthy()?;
306 if parameters.len() > MAX_QUERY_PARAMETERS {
307 return Err(query_error("local SQL accepts at most 65536 parameters"));
308 }
309 if self
310 .transaction
311 .as_ref()
312 .is_some_and(|transaction| transaction.aborted)
313 {
314 return Err(query_error("transaction is aborted; ROLLBACK is required"));
315 }
316 let statement = reddb_rql::parse_frontend(sql)
317 .map_err(LocalReadError::Parse)?
318 .into_query_expr();
319 let statement =
320 super::parameters::bind(&statement, parameters).map_err(LocalReadError::Parameters)?;
321 let QueryExpr::Table(query) = statement else {
322 return Err(LocalReadError::Unsupported(
323 "streaming execution requires SELECT",
324 ));
325 };
326 let (store, contracts) = self.visible()?;
327 query_store_stream(store, contracts, query, visit)
328 }
329
330 fn query_with_params(
331 &mut self,
332 sql: &str,
333 parameters: &[Value],
334 now_ms: u64,
335 ) -> Result<ScalarResult> {
336 let result = self.query_inner(sql, parameters, now_ms);
337 if result.is_err() {
338 if let Some(transaction) = &mut self.transaction {
339 transaction.aborted = true;
340 }
341 }
342 result
343 }
344
345 fn query_inner(
346 &mut self,
347 sql: &str,
348 parameters: &[Value],
349 now_ms: u64,
350 ) -> Result<ScalarResult> {
351 self.check_healthy()?;
353 if parameters.len() > MAX_QUERY_PARAMETERS {
354 return Err(query_error("local SQL accepts at most 65536 parameters"));
355 }
356 let statement = reddb_rql::parse_frontend(sql)
357 .map_err(LocalReadError::Parse)?
358 .into_query_expr();
359 let statement =
360 super::parameters::bind(&statement, parameters).map_err(LocalReadError::Parameters)?;
361 self.storage.admit(&statement)?;
362 if matches!(
363 statement,
364 QueryExpr::TransactionControl(TxnControl::Rollback)
365 ) {
366 if self.transaction.take().is_none() {
367 return Err(query_error("no active transaction"));
368 }
369 return Ok(affected(0));
370 }
371 if self
372 .transaction
373 .as_ref()
374 .is_some_and(|transaction| transaction.aborted)
375 {
376 return Err(query_error("transaction is aborted; ROLLBACK is required"));
377 }
378 match statement {
379 QueryExpr::TransactionControl(control) => self.control(control),
380 QueryExpr::Table(query) => {
381 let (resident, contracts) = self.visible()?;
382 query_resident_bound(resident, contracts, query)
383 }
384 QueryExpr::CreateTable(query) => self.create_table(&query, now_ms),
385 QueryExpr::Insert(_) | QueryExpr::Update(_) | QueryExpr::Delete(_) => {
386 let (resident, contracts) = self.visible()?;
387 let actions = mutation::plan(&statement, resident, contracts, now_ms)?;
388 let count = actions.len();
389 if let Some(transaction) = &mut self.transaction {
390 if transaction.actions.len().checked_add(count).is_none_or(|count| count > super::local_read::LOCAL_READ_ROWS_MAX) {
391 return Err(LocalReadError::ResourceLimit);
392 }
393 self.storage.apply_transaction(&transaction.store, &actions)?;
396 transaction.actions.extend(actions);
397 } else {
398 self.storage.publish_actions(&actions)?;
399 }
400 Ok(affected(count))
401 }
402 _ => Err(LocalReadError::Unsupported("local SQL supports table SELECT, CREATE TABLE, INSERT, UPDATE, DELETE and BEGIN/COMMIT/ROLLBACK")),
403 }
404 }
405
406 fn visible(&self) -> Result<(&dyn StoreView, &HashMap<String, CollectionContract>)> {
407 match &self.transaction {
408 Some(transaction) => Ok((&transaction.store, &transaction.contracts)),
409 None => Ok((self.storage.committed_view()?, &self.contracts)),
410 }
411 }
412
413 fn control(&mut self, control: TxnControl) -> Result<ScalarResult> {
414 match control {
415 TxnControl::Begin(None) => {
416 if self.transaction.is_some() {
417 return Err(query_error("a transaction is already active"));
418 }
419 self.transaction = Some(Transaction {
420 store: self.storage.begin_transaction()?,
421 contracts: self.contracts.clone(),
422 actions: Vec::new(),
423 catalog_changed: false,
424 aborted: false,
425 });
426 }
427 TxnControl::Commit => {
428 let transaction = self.transaction.as_ref().ok_or_else(|| query_error("no active transaction"))?;
429 let catalog = transaction
430 .catalog_changed
431 .then(|| catalog_bytes(&transaction.contracts));
432 self.storage.commit_transaction(
433 &transaction.store,
434 &transaction.actions,
435 catalog,
436 )?;
437 self.contracts = self.transaction.take().expect("active transaction committed").contracts;
438 }
439 _ => return Err(LocalReadError::Unsupported("local transactions support BEGIN, COMMIT and ROLLBACK without isolation options or savepoints")),
440 }
441 Ok(affected(0))
442 }
443
444 fn create_table(
445 &mut self,
446 query: &reddb_rql::ast::CreateTableQuery,
447 now_ms: u64,
448 ) -> Result<ScalarResult> {
449 mutation::admit_create(query)?;
450 let (exists, catalog_exists) = {
451 let (resident, contracts) = self.visible()?;
452 (
453 resident.contains_collection(&query.name)?,
454 contracts.contains_key(&query.name),
455 )
456 };
457 if exists || catalog_exists {
458 if query.if_not_exists && catalog_exists {
459 return Ok(affected(0));
460 }
461 return Err(query_error(format!(
462 "table '{}' already exists",
463 query.name
464 )));
465 }
466 let contract =
467 crate::catalog::ddl::collection_contract_from_create_table(query, u128::from(now_ms))
468 .map_err(query_error)?;
469 crate::catalog::schema::validate_contract_expressions(&contract)
470 .map_err(|error| query_error(error.message))?;
471 let action = StoreWalAction::CreateCollection {
472 name: query.name.clone(),
473 };
474 if let Some(transaction) = &mut self.transaction {
475 self.storage
476 .apply_transaction(&transaction.store, &[action])?;
477 transaction.contracts.insert(query.name.clone(), contract);
478 transaction.catalog_changed = true;
479 } else {
480 let mut contracts = self.contracts.clone();
481 contracts.insert(query.name.clone(), contract);
482 self.storage
483 .publish_catalog_actions(&[action], catalog_bytes(&contracts))?;
484 self.contracts = contracts;
485 }
486 Ok(affected(0))
487 }
488}
489
490fn affected(count: usize) -> ScalarResult {
491 ScalarResult {
492 columns: vec!["affectedRows".into()],
493 rows: vec![vec![Value::UnsignedInteger(
494 u64::try_from(count).expect("row count fits u64"),
495 )]],
496 }
497}
498fn query_error(message: impl Into<String>) -> LocalReadError {
499 LocalReadError::Query(message.into())
500}
501fn catalog_bytes(contracts: &HashMap<String, CollectionContract>) -> Vec<u8> {
502 let mut contracts: Vec<_> = contracts.values().cloned().collect();
503 contracts.sort_by(|left, right| left.name.cmp(&right.name));
504 serialize_collection_contracts(&contracts)
505}