1use super::{
3 local_read::{query_resident_bound, query_resident_scoped, LocalReadError, ReadScope},
4 scalar::ScalarResult,
5};
6use crate::{
7 catalog::{
8 codec::{deserialize_collection_contracts, serialize_collection_contracts},
9 CollectionContract,
10 },
11 storage::{
12 manager::ManagerConfig, resident_file::EmbeddedResidentStore,
13 resident_store::ResidentStore, store_wal::StoreWalAction,
14 },
15};
16use reddb_file::VfsFile;
17use reddb_rql::ast::{QueryExpr, TxnControl};
18use reddb_types::Value;
19use std::{
20 collections::{HashMap, HashSet},
21 path::PathBuf,
22};
23
24mod mutation;
25
26type Result<T> = std::result::Result<T, LocalReadError>;
27
28pub const MAX_QUERY_PARAMETERS: usize = 65_536;
30
31struct Transaction {
32 resident: ResidentStore,
33 contracts: HashMap<String, CollectionContract>,
34 actions: Vec<StoreWalAction>,
35 catalog_changed: bool,
36 aborted: bool,
37}
38
39pub struct LocalDatabase<F: VfsFile> {
42 store: EmbeddedResidentStore<F>,
43 contracts: HashMap<String, CollectionContract>,
44 transaction: Option<Transaction>,
45}
46
47impl<F: VfsFile> LocalDatabase<F> {
48 pub fn create(file: F, name: impl Into<PathBuf>, now_ms: u64) -> Result<Self> {
49 Ok(Self {
50 store: EmbeddedResidentStore::create(
51 file,
52 name,
53 u128::from(now_ms),
54 ManagerConfig::default(),
55 false,
56 0,
57 )?,
58 contracts: HashMap::new(),
59 transaction: None,
60 })
61 }
62
63 pub fn open(file: F, name: impl Into<PathBuf>) -> Result<Self> {
64 let store = EmbeddedResidentStore::open(file, name, ManagerConfig::default(), false, 0)?;
65 let bytes = store.committed_resident()?.aux_metadata();
66 let mut contracts = HashMap::new();
67 if !bytes.is_empty() {
68 for contract in deserialize_collection_contracts(&bytes)
69 .map_err(|error| LocalReadError::Catalog(error.to_string()))?
70 {
71 let name = contract.name.clone();
72 if contracts.insert(name.clone(), contract).is_some() {
73 return Err(LocalReadError::Catalog(format!(
74 "duplicate collection contract: {name}"
75 )));
76 }
77 }
78 }
79 Ok(Self {
80 store,
81 contracts,
82 transaction: None,
83 })
84 }
85
86 pub fn query(&mut self, sql: &str, now_ms: u64) -> Result<ScalarResult> {
87 self.query_with_params(sql, &[], now_ms)
88 }
89
90 pub fn check_healthy(&self) -> Result<()> {
92 self.store.committed_resident()?;
93 Ok(())
94 }
95
96 pub fn catalog(&self) -> Result<ScalarResult> {
98 self.check_healthy()?;
99 if self
100 .transaction
101 .as_ref()
102 .is_some_and(|transaction| transaction.aborted)
103 {
104 return Err(query_error("transaction is aborted; ROLLBACK is required"));
105 }
106 let (_, contracts) = self.visible()?;
107 let mut tables: Vec<_> = contracts.values().collect();
108 tables.sort_by(|left, right| left.name.cmp(&right.name));
109 let mut rows = Vec::new();
110 for table in tables {
111 for column in crate::catalog::schema::resolved_contract_columns(table)
112 .map_err(|error| LocalReadError::Catalog(error.to_string()))?
113 {
114 rows.push(vec![
115 Value::text(table.name.as_str()),
116 Value::text(column.name),
117 Value::text(column.data_type_name),
118 Value::Boolean(!column.not_null),
119 ]);
120 }
121 }
122 Ok(ScalarResult {
123 columns: ["table_name", "column_name", "data_type", "nullable"]
124 .map(String::from)
125 .to_vec(),
126 rows,
127 })
128 }
129
130 pub fn read_query(
132 &self,
133 sql: &str,
134 parameters: &[Value],
135 scopes: &[ReadScope],
136 ) -> Result<ScalarResult> {
137 self.check_healthy()?;
138 if parameters.len() > MAX_QUERY_PARAMETERS || scopes.len() > 64 {
139 return Err(query_error("inspection arguments exceed their limit"));
140 }
141 if self
142 .transaction
143 .as_ref()
144 .is_some_and(|transaction| transaction.aborted)
145 {
146 return Err(query_error("transaction is aborted; ROLLBACK is required"));
147 }
148 let statement = reddb_rql::parse_frontend(sql)
149 .map_err(LocalReadError::Parse)?
150 .into_query_expr();
151 let statement =
152 super::parameters::bind(&statement, parameters).map_err(LocalReadError::Parameters)?;
153 let QueryExpr::Table(query) = statement else {
154 return Err(LocalReadError::Unsupported(
155 "inspection accepts table SELECT only",
156 ));
157 };
158 let mut tables = HashSet::new();
159 if scopes.iter().any(|scope| !tables.insert(&scope.table)) {
160 return Err(query_error("duplicate inspection table scope"));
161 }
162 let scope = scopes
163 .iter()
164 .find(|scope| scope.table == query.table)
165 .ok_or(LocalReadError::Unsupported(
166 "table is outside the inspection scope",
167 ))?;
168 let (resident, contracts) = self.visible()?;
169 query_resident_scoped(resident, contracts, query, scope)
170 }
171
172 pub fn query_with_params(
173 &mut self,
174 sql: &str,
175 parameters: &[Value],
176 now_ms: u64,
177 ) -> Result<ScalarResult> {
178 let result = self.query_inner(sql, parameters, now_ms);
179 if result.is_err() {
180 if let Some(transaction) = &mut self.transaction {
181 transaction.aborted = true;
182 }
183 }
184 result
185 }
186
187 fn query_inner(
188 &mut self,
189 sql: &str,
190 parameters: &[Value],
191 now_ms: u64,
192 ) -> Result<ScalarResult> {
193 self.check_healthy()?;
195 if parameters.len() > MAX_QUERY_PARAMETERS {
196 return Err(query_error("local SQL accepts at most 65536 parameters"));
197 }
198 let statement = reddb_rql::parse_frontend(sql)
199 .map_err(LocalReadError::Parse)?
200 .into_query_expr();
201 let statement =
202 super::parameters::bind(&statement, parameters).map_err(LocalReadError::Parameters)?;
203 if matches!(
204 statement,
205 QueryExpr::TransactionControl(TxnControl::Rollback)
206 ) {
207 if self.transaction.take().is_none() {
208 return Err(query_error("no active transaction"));
209 }
210 return Ok(affected(0));
211 }
212 if self
213 .transaction
214 .as_ref()
215 .is_some_and(|transaction| transaction.aborted)
216 {
217 return Err(query_error("transaction is aborted; ROLLBACK is required"));
218 }
219 match statement {
220 QueryExpr::TransactionControl(control) => self.control(control),
221 QueryExpr::Table(query) => {
222 let (resident, contracts) = self.visible()?;
223 query_resident_bound(resident, contracts, query)
224 }
225 QueryExpr::CreateTable(query) => self.create_table(&query, now_ms),
226 QueryExpr::Insert(_) | QueryExpr::Update(_) | QueryExpr::Delete(_) => {
227 let (resident, contracts) = self.visible()?;
228 let actions = mutation::plan(&statement, resident, contracts, now_ms)?;
229 let count = actions.len();
230 if let Some(transaction) = &mut self.transaction {
231 if transaction.actions.len().checked_add(count).is_none_or(|count| count > super::local_read::LOCAL_READ_ROWS_MAX) {
232 return Err(LocalReadError::ResourceLimit);
233 }
234 apply(&transaction.resident, &actions)?;
237 transaction.actions.extend(actions);
238 } else {
239 self.store.publish_actions(&actions)?;
240 }
241 Ok(affected(count))
242 }
243 _ => Err(LocalReadError::Unsupported("local SQL supports table SELECT, CREATE TABLE, INSERT, UPDATE, DELETE and BEGIN/COMMIT/ROLLBACK")),
244 }
245 }
246
247 fn visible(&self) -> Result<(&ResidentStore, &HashMap<String, CollectionContract>)> {
248 match &self.transaction {
249 Some(transaction) => Ok((&transaction.resident, &transaction.contracts)),
250 None => Ok((self.store.committed_resident()?, &self.contracts)),
251 }
252 }
253
254 fn control(&mut self, control: TxnControl) -> Result<ScalarResult> {
255 match control {
256 TxnControl::Begin(None) => {
257 if self.transaction.is_some() {
258 return Err(query_error("a transaction is already active"));
259 }
260 self.transaction = Some(Transaction {
261 resident: copy_resident(self.store.committed_resident()?)?,
262 contracts: self.contracts.clone(),
263 actions: Vec::new(),
264 catalog_changed: false,
265 aborted: false,
266 });
267 }
268 TxnControl::Commit => {
269 let transaction = self.transaction.as_ref().ok_or_else(|| query_error("no active transaction"))?;
270 if transaction.catalog_changed {
271 self.store.publish_snapshot(&snapshot(&transaction.resident))?;
272 } else {
273 self.store.publish_actions(&transaction.actions)?;
274 }
275 self.contracts = self.transaction.take().expect("active transaction committed").contracts;
276 }
277 _ => return Err(LocalReadError::Unsupported("local transactions support BEGIN, COMMIT and ROLLBACK without isolation options or savepoints")),
278 }
279 Ok(affected(0))
280 }
281
282 fn create_table(
283 &mut self,
284 query: &reddb_rql::ast::CreateTableQuery,
285 now_ms: u64,
286 ) -> Result<ScalarResult> {
287 mutation::admit_create(query)?;
288 let (exists, catalog_exists) = {
289 let (resident, contracts) = self.visible()?;
290 (
291 resident.get_collection(&query.name).is_some(),
292 contracts.contains_key(&query.name),
293 )
294 };
295 if exists || catalog_exists {
296 if query.if_not_exists && catalog_exists {
297 return Ok(affected(0));
298 }
299 return Err(query_error(format!(
300 "table '{}' already exists",
301 query.name
302 )));
303 }
304 let contract =
305 crate::catalog::ddl::collection_contract_from_create_table(query, u128::from(now_ms))
306 .map_err(query_error)?;
307 crate::catalog::schema::validate_contract_expressions(&contract)
308 .map_err(|error| query_error(error.message))?;
309 let action = StoreWalAction::CreateCollection {
310 name: query.name.clone(),
311 };
312 if let Some(transaction) = &mut self.transaction {
313 apply(&transaction.resident, &[action])?;
314 transaction.contracts.insert(query.name.clone(), contract);
315 set_contracts(&transaction.resident, &transaction.contracts);
316 transaction.catalog_changed = true;
317 } else {
318 let (resident, contracts) = self.visible()?;
319 let staged = copy_resident(resident)?;
320 let mut contracts = contracts.clone();
321 apply(&staged, &[action])?;
322 contracts.insert(query.name.clone(), contract);
323 set_contracts(&staged, &contracts);
324 self.store.publish_snapshot(&snapshot(&staged))?;
325 self.contracts = contracts;
326 }
327 Ok(affected(0))
328 }
329
330 pub fn into_inner(self) -> F {
331 self.store.into_inner()
332 }
333}
334
335fn affected(count: usize) -> ScalarResult {
336 ScalarResult {
337 columns: vec!["affectedRows".into()],
338 rows: vec![vec![Value::UnsignedInteger(
339 u64::try_from(count).expect("row count fits u64"),
340 )]],
341 }
342}
343fn query_error(message: impl Into<String>) -> LocalReadError {
344 LocalReadError::Query(message.into())
345}
346fn snapshot(resident: &ResidentStore) -> Vec<u8> {
347 resident.snapshot_bytes(&HashSet::new(), &HashSet::new(), || {})
348}
349fn copy_resident(resident: &ResidentStore) -> Result<ResidentStore> {
350 ResidentStore::from_snapshot(&snapshot(resident), &ManagerConfig::default(), false, 0)
351 .map(|(resident, _)| resident)
352 .map_err(|error| query_error(error.to_string()))
353}
354fn apply(resident: &ResidentStore, actions: &[StoreWalAction]) -> Result<()> {
355 for action in actions {
356 resident.apply_replayed_action(
357 action,
358 reddb_file::STORE_VERSION_CURRENT,
359 &ManagerConfig::default(),
360 )?;
361 }
362 Ok(())
363}
364fn set_contracts(resident: &ResidentStore, contracts: &HashMap<String, CollectionContract>) {
365 let mut contracts: Vec<_> = contracts.values().cloned().collect();
366 contracts.sort_by(|left, right| left.name.cmp(&right.name));
367 resident.set_aux_metadata(serialize_collection_contracts(&contracts));
368}