1use super::*;
9use crate::catalog::CollectionModel;
10use crate::runtime::audit_log::{AuditAuthSource, AuditEvent, AuditFieldEscaper, Outcome};
11use crate::runtime::ddl::polymorphic_resolver;
12use reddb_engine::catalog::ddl::declared_column_contract_from_ddl;
13use reddb_rql::analyzer::analyze_create_table;
14use reddb_rql::ast::CreateColumnDef;
15use reddb_rql::ast::{
16 CreateVcsRefQuery, DropForkQuery, DropVcsRefQuery, ForkStoreQuery, VcsRefKind,
17};
18use std::collections::{BTreeSet, HashMap, HashSet};
19
20fn operational_wal_retention_floor(durable_lsn: u64) -> u64 {
21 if durable_lsn == 0 {
22 0
23 } else {
24 1
25 }
26}
27
28fn validate_store_fork_lsn(
29 fork_lsn: u64,
30 retention_floor: u64,
31 durable_lsn: u64,
32) -> RedDBResult<()> {
33 if fork_lsn < retention_floor {
34 return Err(RedDBError::Query(format!(
35 "cannot fork store at LSN {fork_lsn}: below operational WAL retention floor \
36 {retention_floor}; restore from backup for older recovery points"
37 )));
38 }
39 if fork_lsn > durable_lsn {
40 return Err(RedDBError::Query(format!(
41 "cannot fork store at LSN {fork_lsn}: current durable LSN is {durable_lsn}; \
42 restore from backup for recovery points outside the retained WAL window"
43 )));
44 }
45 Ok(())
46}
47
48fn vault_master_key_ref(collection: &str) -> String {
49 format!("red.vault.{collection}.master_key")
50}
51
52impl RedDBRuntime {
53 pub fn execute_create_table(
59 &self,
60 raw_query: &str,
61 query: &CreateTableQuery,
62 ) -> RedDBResult<RuntimeQueryResult> {
63 if query.collection_model != CollectionModel::Table {
64 if query
65 .columns
66 .iter()
67 .any(|column| column.generated.is_some() || column.check.is_some())
68 {
69 return Err(RedDBError::Query(
70 "schema expressions currently require TABLE collections".to_string(),
71 ));
72 }
73 return self.execute_create_keyed_collection(raw_query, query);
74 }
75 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
76 if is_system_schema_name(&query.name) {
77 return Err(RedDBError::Query("system schema is read-only".to_string()));
78 }
79 let store = self.inner.db.store();
80 analyze_create_table(query).map_err(|err| RedDBError::Query(err.to_string()))?;
81 if let Some(policy) = &query.ai_policy {
85 validate_ai_policy_modalities(policy)?;
86 }
87 crate::reserved_fields::ensure_no_reserved_public_item_fields(
88 query.columns.iter().map(|column| column.name.as_str()),
89 &format!("table '{}'", query.name),
90 )?;
91 let exists = store.get_collection(&query.name).is_some();
93 if exists {
94 if query.if_not_exists {
95 return Ok(RuntimeQueryResult::ok_message(
96 raw_query.to_string(),
97 &format!("table '{}' already exists", query.name),
98 "create",
99 ));
100 }
101 return Err(RedDBError::Query(format!(
102 "table '{}' already exists",
103 query.name
104 )));
105 }
106
107 let contract = collection_contract_from_create_table(query)?;
110 crate::application::collection_contract_enforcer::validate_contract_expressions(&contract)?;
111 validate_event_subscriptions(self, &query.name, &contract.subscriptions)?;
112 store
114 .create_collection(&query.name)
115 .map_err(|err| RedDBError::Internal(err.to_string()))?;
116 for subscription in &contract.subscriptions {
117 ensure_event_target_queue(self, &subscription.target_queue)?;
118 }
119 if let Some(default_ttl_ms) = query.default_ttl_ms {
120 self.inner
121 .db
122 .set_collection_default_ttl_ms(&query.name, default_ttl_ms);
123 }
124 self.inner
125 .db
126 .save_collection_contract(contract)
127 .map_err(|err| RedDBError::Internal(err.to_string()))?;
128 if let Some(tenant_id) = crate::runtime::impl_core::current_tenant() {
129 store.set_config_tree(
130 &format!("red.collection_tenants.{}", query.name),
131 &crate::serde_json::Value::String(tenant_id),
132 );
133 }
134 self.inner
135 .db
136 .persist_metadata()
137 .map_err(|err| RedDBError::Internal(err.to_string()))?;
138 self.refresh_table_planner_stats(&query.name);
139 self.invalidate_result_cache();
140 let columns: Vec<String> = query.columns.iter().map(|col| col.name.clone()).collect();
143 self.schema_vocabulary_apply(
144 crate::runtime::schema_vocabulary::DdlEvent::CreateCollection {
145 collection: query.name.clone(),
146 columns,
147 type_tags: Vec::new(),
148 description: None,
149 },
150 );
151 if let Some(spec) = &query.partition_by {
158 let kind_str = match spec.kind {
159 reddb_rql::ast::PartitionKind::Range => "range",
160 reddb_rql::ast::PartitionKind::List => "list",
161 reddb_rql::ast::PartitionKind::Hash => "hash",
162 };
163 store.set_config_tree(
164 &format!("partition.{}.by", query.name),
165 &crate::serde_json::Value::String(kind_str.to_string()),
166 );
167 store.set_config_tree(
168 &format!("partition.{}.column", query.name),
169 &crate::serde_json::Value::String(spec.column.clone()),
170 );
171 }
172
173 if let Some(col) = &query.tenant_by {
184 store.set_config_tree(
185 &format!("tenant_tables.{}.column", query.name),
186 &crate::serde_json::Value::String(col.clone()),
187 );
188 self.register_tenant_table(&query.name, col);
189 }
190
191 let ttl_suffix = query
192 .default_ttl_ms
193 .map(|ttl_ms| format!(" with default TTL {}ms", ttl_ms))
194 .unwrap_or_default();
195
196 let tenant_suffix = query
197 .tenant_by
198 .as_ref()
199 .map(|col| format!(" (tenant-scoped by {col})"))
200 .unwrap_or_default();
201
202 Ok(RuntimeQueryResult::ok_message(
203 raw_query.to_string(),
204 &format!(
205 "table '{}' created{}{}",
206 query.name, ttl_suffix, tenant_suffix
207 ),
208 "create",
209 ))
210 }
211
212 fn execute_create_keyed_collection(
213 &self,
214 raw_query: &str,
215 query: &CreateTableQuery,
216 ) -> RedDBResult<RuntimeQueryResult> {
217 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
218 if is_system_schema_name(&query.name) {
219 return Err(RedDBError::Query("system schema is read-only".to_string()));
220 }
221 let store = self.inner.db.store();
222 let label = polymorphic_resolver::model_name(query.collection_model);
223 if store.get_collection(&query.name).is_some() {
224 if query.if_not_exists {
225 return Ok(RuntimeQueryResult::ok_message(
226 raw_query.to_string(),
227 &format!("{label} '{}' already exists", query.name),
228 "create",
229 ));
230 }
231 return Err(RedDBError::Query(format!(
232 "{label} '{}' already exists",
233 query.name
234 )));
235 }
236
237 if query.collection_model == CollectionModel::Vault {
238 self.reject_vault_transaction()?;
239 self.provision_vault_key_material(&query.name, query.vault_own_master_key)?;
240 }
241 store
242 .create_collection(&query.name)
243 .map_err(|err| RedDBError::Internal(err.to_string()))?;
244 if query.collection_model == CollectionModel::Vault {
245 let key_scope = if query.vault_own_master_key {
246 "own"
247 } else {
248 "cluster"
249 };
250 store.set_config_tree(
251 &format!("red.vault.{}.key_scope", query.name),
252 &crate::serde_json::Value::String(key_scope.to_string()),
253 );
254 store.set_config_tree(
255 &format!("red.vault.{}.status", query.name),
256 &crate::serde_json::Value::String("sealed".to_string()),
257 );
258 }
259 if query.collection_model == CollectionModel::Metrics {
260 for spec in &query.metrics_rollup_policies {
261 let policy = crate::storage::timeseries::retention::DownsamplePolicy::parse(spec)
262 .ok_or_else(|| {
263 RedDBError::Query(format!("invalid metrics rollup policy '{}'", spec))
264 })?;
265 if policy.source != "raw" {
266 return Err(RedDBError::Query(format!(
267 "invalid metrics rollup policy '{}': metrics v0 rollups must use raw as source",
268 spec
269 )));
270 }
271 if !matches!(
272 policy.aggregation.as_str(),
273 "avg" | "sum" | "min" | "max" | "count"
274 ) {
275 return Err(RedDBError::Query(format!(
276 "invalid metrics rollup policy '{}': supported aggregations are avg, sum, min, max, count",
277 spec
278 )));
279 }
280 }
281 if let Some(raw_retention_ms) = query.default_ttl_ms {
282 self.inner
283 .db
284 .set_collection_default_ttl_ms(&query.name, raw_retention_ms);
285 store.set_config_tree(
286 &format!("red.metrics.{}.raw_retention_ms", query.name),
287 &crate::serde_json::Value::Number(raw_retention_ms as f64),
288 );
289 }
290 let tenant_identity = query
291 .tenant_by
292 .clone()
293 .unwrap_or_else(|| "current_tenant".to_string());
294 store.set_config_tree(
295 &format!("red.metrics.{}.tenant_identity", query.name),
296 &crate::serde_json::Value::String(tenant_identity),
297 );
298 store.set_config_tree(
299 &format!("red.metrics.{}.namespace", query.name),
300 &crate::serde_json::Value::String("default".to_string()),
301 );
302 if !query.metrics_rollup_policies.is_empty() {
303 store.set_config_tree(
304 &format!("red.metrics.{}.rollup_policies", query.name),
305 &crate::serde_json::Value::Array(
306 query
307 .metrics_rollup_policies
308 .iter()
309 .cloned()
310 .map(crate::serde_json::Value::String)
311 .collect(),
312 ),
313 );
314 }
315 }
316 let contract = if query.collection_model == CollectionModel::Metrics {
317 metrics_collection_contract(query)
318 } else {
319 keyed_collection_contract(
320 &query.name,
321 query.collection_model,
322 query.analytics_config.clone(),
323 )
324 };
325 self.inner
326 .db
327 .save_collection_contract(contract)
328 .map_err(|err| RedDBError::Internal(err.to_string()))?;
329 if let Some(tenant_id) = crate::runtime::impl_core::current_tenant() {
330 store.set_config_tree(
331 &format!("red.collection_tenants.{}", query.name),
332 &crate::serde_json::Value::String(tenant_id),
333 );
334 }
335 self.inner
336 .db
337 .persist_metadata()
338 .map_err(|err| RedDBError::Internal(err.to_string()))?;
339 self.invalidate_result_cache();
340
341 Ok(RuntimeQueryResult::ok_message(
342 raw_query.to_string(),
343 &format!("{label} '{}' created", query.name),
344 "create",
345 ))
346 }
347
348 pub fn ensure_system_graph_with_analytics(
357 &self,
358 name: &str,
359 outputs: &[crate::catalog::AnalyticsOutput],
360 ) -> RedDBResult<()> {
361 let store = self.inner.db.store();
362 if store.get_collection(name).is_some() {
363 return Ok(());
364 }
365 let analytics_config = outputs
366 .iter()
367 .map(|output| crate::catalog::AnalyticsViewDescriptor {
368 output: *output,
369 algorithm: None,
370 resolution: None,
371 max_iterations: None,
372 tolerance: None,
373 })
374 .collect();
375 store
376 .create_collection(name)
377 .map_err(|err| RedDBError::Internal(err.to_string()))?;
378 let contract = keyed_collection_contract(
379 name,
380 crate::catalog::CollectionModel::Graph,
381 analytics_config,
382 );
383 self.inner
384 .db
385 .save_collection_contract(contract)
386 .map_err(|err| RedDBError::Internal(err.to_string()))?;
387 self.inner
388 .db
389 .persist_metadata()
390 .map_err(|err| RedDBError::Internal(err.to_string()))?;
391 self.invalidate_result_cache_process_only();
392 Ok(())
393 }
394
395 pub fn execute_create_collection(
396 &self,
397 raw_query: &str,
398 query: &CreateCollectionQuery,
399 ) -> RedDBResult<RuntimeQueryResult> {
400 let model = match query.kind.as_str() {
401 "graph" => CollectionModel::Graph,
402 "document" => CollectionModel::Document,
403 "metrics" => CollectionModel::Metrics,
404 "vector.turbo" => {
405 let dimension = query.vector_dimension.ok_or_else(|| {
406 RedDBError::Query(
407 "CREATE COLLECTION KIND vector.turbo requires DIM".to_string(),
408 )
409 })?;
410 let create = CreateVectorQuery {
411 name: query.name.clone(),
412 dimension,
413 metric: query
414 .vector_metric
415 .unwrap_or(crate::storage::engine::distance::DistanceMetric::Cosine),
416 if_not_exists: query.if_not_exists,
417 };
418 let result = self.execute_create_vector(raw_query, &create)?;
419 let store = self.inner.db.store();
426 crate::runtime::vector_turbo_kind::mark_as_turbo(&store, &query.name);
427 self.inner
428 .db
429 .persist_metadata()
430 .map_err(|err| RedDBError::Internal(err.to_string()))?;
431 let _ = self.inner.db.turbo_state(&query.name);
436 return Ok(result);
437 }
438 "blockchain" => CollectionModel::Table,
443 other => {
444 return Err(RedDBError::Query(format!(
445 "NOT_YET_SUPPORTED: CREATE COLLECTION KIND {other} is not implemented"
446 )));
447 }
448 };
449 let create = CreateTableQuery {
450 collection_model: model,
451 name: query.name.clone(),
452 columns: Vec::new(),
453 unique_constraints: Vec::new(),
454 if_not_exists: query.if_not_exists,
455 default_ttl_ms: None,
456 metrics_rollup_policies: Vec::new(),
457 context_index_fields: Vec::new(),
458 context_index_enabled: false,
459 timestamps: false,
460 partition_by: None,
461 tenant_by: None,
462 append_only: false,
463 subscriptions: Vec::new(),
464 analytics_config: Vec::new(),
465 vault_own_master_key: false,
466 ai_policy: None,
467 };
468 let result = self.execute_create_table(raw_query, &create)?;
469 if query.kind == "blockchain" {
470 self.install_blockchain_kind(&query.name)?;
471 }
472 if !query.allowed_signers.is_empty() {
477 let actor = crate::runtime::impl_core::current_user_projected()
478 .unwrap_or_else(|| "@system/create-collection".to_string());
479 crate::runtime::signed_writes_kind::install(
480 &self.inner.db.store(),
481 &query.name,
482 &query.allowed_signers,
483 &actor,
484 );
485 }
486 Ok(result)
487 }
488
489 fn install_blockchain_kind(&self, name: &str) -> RedDBResult<()> {
493 use crate::runtime::blockchain_kind;
494 use crate::storage::unified::{EntityData, EntityId, EntityKind, RowData, UnifiedEntity};
495 use std::sync::Arc;
496
497 let store = self.inner.db.store();
498 blockchain_kind::mark_as_chain(&store, name);
499
500 let existing_tip = blockchain_kind::chain_tip(&store, name);
501 if existing_tip.height.is_some() {
502 return Ok(());
503 }
504
505 let fields = blockchain_kind::genesis_fields(blockchain_kind::now_ms());
506 let named: std::collections::HashMap<String, reddb_types::Value> =
507 fields.into_iter().collect();
508 let entity = UnifiedEntity::new(
509 EntityId::new(0),
510 EntityKind::TableRow {
511 table: Arc::from(name),
512 row_id: 0,
513 },
514 EntityData::Row(RowData {
515 columns: Vec::new(),
516 named: Some(named),
517 schema: None,
518 }),
519 );
520 store
521 .insert_auto(name, entity)
522 .map_err(|err| RedDBError::Internal(err.to_string()))?;
523 if let Some(tip) = blockchain_kind::chain_tip_full(&store, name) {
526 self.inner
527 .chain_tip_cache
528 .lock()
529 .insert(name.to_string(), tip);
530 }
531 Ok(())
532 }
533
534 pub fn execute_create_vector(
535 &self,
536 raw_query: &str,
537 query: &CreateVectorQuery,
538 ) -> RedDBResult<RuntimeQueryResult> {
539 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
540 if is_system_schema_name(&query.name) {
541 return Err(RedDBError::Query("system schema is read-only".to_string()));
542 }
543 let store = self.inner.db.store();
544 if store.get_collection(&query.name).is_some() {
545 if query.if_not_exists {
546 return Ok(RuntimeQueryResult::ok_message(
547 raw_query.to_string(),
548 &format!("vector '{}' already exists", query.name),
549 "create",
550 ));
551 }
552 return Err(RedDBError::Query(format!(
553 "vector '{}' already exists",
554 query.name
555 )));
556 }
557
558 store
559 .create_collection(&query.name)
560 .map_err(|err| RedDBError::Internal(err.to_string()))?;
561 self.inner
562 .db
563 .save_collection_contract(vector_collection_contract(query))
564 .map_err(|err| RedDBError::Internal(err.to_string()))?;
565 if let Some(tenant_id) = crate::runtime::impl_core::current_tenant() {
566 store.set_config_tree(
567 &format!("red.collection_tenants.{}", query.name),
568 &crate::serde_json::Value::String(tenant_id),
569 );
570 }
571 crate::runtime::vector_turbo_kind::mark_as_turbo(&store, &query.name);
580 self.inner
581 .db
582 .persist_metadata()
583 .map_err(|err| RedDBError::Internal(err.to_string()))?;
584 let _ = self.inner.db.turbo_state(&query.name);
589 self.invalidate_result_cache();
590
591 Ok(RuntimeQueryResult::ok_message(
592 raw_query.to_string(),
593 &format!("vector '{}' created", query.name),
594 "create",
595 ))
596 }
597
598 fn provision_vault_key_material(
599 &self,
600 collection: &str,
601 own_master_key: bool,
602 ) -> RedDBResult<()> {
603 let auth_store = self.inner.auth_store.read().clone().ok_or_else(|| {
604 RedDBError::Query("CREATE VAULT requires an enabled, unsealed vault".to_string())
605 })?;
606 if !auth_store.is_vault_backed() {
607 return Err(RedDBError::Query(
608 "CREATE VAULT requires an enabled, unsealed vault".to_string(),
609 ));
610 }
611
612 auth_store
613 .ensure_vault_secret_key_result()
614 .map_err(|err| RedDBError::Query(err.to_string()))?;
615
616 if own_master_key
617 && auth_store
618 .vault_kv_get(&vault_master_key_ref(collection))
619 .is_none()
620 {
621 let key = crate::auth::store::random_bytes(32);
622 auth_store
623 .vault_kv_try_set(vault_master_key_ref(collection), hex::encode(key))
624 .map_err(|err| RedDBError::Query(err.to_string()))?;
625 }
626
627 Ok(())
628 }
629
630 pub fn execute_drop_table(
634 &self,
635 raw_query: &str,
636 query: &DropTableQuery,
637 ) -> RedDBResult<RuntimeQueryResult> {
638 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
639 let store = self.inner.db.store();
640
641 if is_system_schema_name(&query.name) {
642 return Err(RedDBError::Query("system schema is read-only".to_string()));
643 }
644
645 let exists = store.get_collection(&query.name).is_some();
646 if !exists {
647 if query.if_exists {
648 return Ok(RuntimeQueryResult::ok_message(
649 raw_query.to_string(),
650 &format!("table '{}' does not exist", query.name),
651 "drop",
652 ));
653 }
654 return Err(RedDBError::NotFound(format!(
655 "table '{}' not found",
656 query.name
657 )));
658 }
659 let actual =
660 polymorphic_resolver::resolve(&query.name, &self.inner.db.catalog_model_snapshot())?;
661 polymorphic_resolver::ensure_model_match(CollectionModel::Table, actual)?;
662
663 let final_count = store
666 .get_collection(&query.name)
667 .map(|manager| manager.query_all(|_| true).len() as u64)
668 .unwrap_or(0);
669 crate::runtime::mutation::emit_collection_dropped_event_for_collection(
670 self,
671 &query.name,
672 final_count,
673 )?;
674
675 let topology_lock = self.inner.index_store.collection_topology_lock(&query.name);
676 let _topology_guard = topology_lock.write();
677 let orphaned_indices: Vec<String> = self
678 .inner
679 .index_store
680 .list_indices(&query.name)
681 .into_iter()
682 .map(|index| index.name)
683 .collect();
684 for name in &orphaned_indices {
685 self.inner.index_store.drop_index(name, &query.name);
686 }
687
688 store
689 .drop_collection(&query.name)
690 .map_err(|err| RedDBError::Internal(err.to_string()))?;
691 self.inner.db.invalidate_vector_index(&query.name);
692 self.inner.db.clear_collection_default_ttl_ms(&query.name);
693 self.inner
694 .db
695 .remove_collection_contract(&query.name)
696 .map_err(|err| RedDBError::Internal(err.to_string()))?;
697 self.clear_table_planner_stats(&query.name);
698 self.invalidate_result_cache();
699 if let Some(store) = self.inner.auth_store.read().clone() {
703 store.invalidate_visible_collections_cache();
704 }
705 self.inner
706 .db
707 .persist_metadata()
708 .map_err(|err| RedDBError::Internal(err.to_string()))?;
709 self.schema_vocabulary_apply(
715 crate::runtime::schema_vocabulary::DdlEvent::DropCollection {
716 collection: query.name.clone(),
717 },
718 );
719
720 Ok(RuntimeQueryResult::ok_message(
721 raw_query.to_string(),
722 &format!("table '{}' dropped", query.name),
723 "drop",
724 ))
725 }
726
727 pub fn execute_drop_graph(
728 &self,
729 raw_query: &str,
730 query: &DropGraphQuery,
731 ) -> RedDBResult<RuntimeQueryResult> {
732 self.execute_drop_typed_collection(
733 raw_query,
734 &query.name,
735 query.if_exists,
736 CollectionModel::Graph,
737 "graph",
738 )
739 }
740
741 pub fn execute_drop_vector(
742 &self,
743 raw_query: &str,
744 query: &DropVectorQuery,
745 ) -> RedDBResult<RuntimeQueryResult> {
746 self.execute_drop_typed_collection(
747 raw_query,
748 &query.name,
749 query.if_exists,
750 CollectionModel::Vector,
751 "vector",
752 )
753 }
754
755 pub fn execute_drop_document(
756 &self,
757 raw_query: &str,
758 query: &DropDocumentQuery,
759 ) -> RedDBResult<RuntimeQueryResult> {
760 self.execute_drop_typed_collection(
761 raw_query,
762 &query.name,
763 query.if_exists,
764 CollectionModel::Document,
765 "document",
766 )
767 }
768
769 pub fn execute_drop_kv(
770 &self,
771 raw_query: &str,
772 query: &DropKvQuery,
773 ) -> RedDBResult<RuntimeQueryResult> {
774 let label = polymorphic_resolver::model_name(query.model);
775 self.execute_drop_typed_collection(
776 raw_query,
777 &query.name,
778 query.if_exists,
779 query.model,
780 label,
781 )
782 }
783
784 pub fn execute_drop_collection(
785 &self,
786 raw_query: &str,
787 query: &DropCollectionQuery,
788 ) -> RedDBResult<RuntimeQueryResult> {
789 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
790 if is_system_schema_name(&query.name) {
791 return Err(RedDBError::Query("system schema is read-only".to_string()));
792 }
793 let store = self.inner.db.store();
794 if store.get_collection(&query.name).is_none() {
795 if query.if_exists {
796 return Ok(RuntimeQueryResult::ok_message(
797 raw_query.to_string(),
798 &format!("collection '{}' does not exist", query.name),
799 "drop",
800 ));
801 }
802 return Err(RedDBError::NotFound(format!(
803 "collection '{}' not found",
804 query.name
805 )));
806 }
807
808 let actual =
809 polymorphic_resolver::resolve(&query.name, &self.inner.db.catalog_model_snapshot())?;
810 if let Some(expected) = query.model {
811 polymorphic_resolver::ensure_model_match(expected, actual)?;
812 }
813
814 match actual {
815 CollectionModel::Table => self.execute_drop_table(
816 raw_query,
817 &DropTableQuery {
818 name: query.name.clone(),
819 if_exists: query.if_exists,
820 },
821 ),
822 CollectionModel::TimeSeries => self.execute_drop_timeseries(
823 raw_query,
824 &DropTimeSeriesQuery {
825 name: query.name.clone(),
826 if_exists: query.if_exists,
827 },
828 ),
829 CollectionModel::Queue => self.execute_drop_queue(
830 raw_query,
831 &DropQueueQuery {
832 name: query.name.clone(),
833 if_exists: query.if_exists,
834 },
835 ),
836 CollectionModel::Graph => self.execute_drop_graph(
837 raw_query,
838 &DropGraphQuery {
839 name: query.name.clone(),
840 if_exists: query.if_exists,
841 },
842 ),
843 CollectionModel::Vector => self.execute_drop_vector(
844 raw_query,
845 &DropVectorQuery {
846 name: query.name.clone(),
847 if_exists: query.if_exists,
848 },
849 ),
850 CollectionModel::Document => self.execute_drop_document(
851 raw_query,
852 &DropDocumentQuery {
853 name: query.name.clone(),
854 if_exists: query.if_exists,
855 },
856 ),
857 CollectionModel::Kv => self.execute_drop_kv(
858 raw_query,
859 &DropKvQuery {
860 name: query.name.clone(),
861 if_exists: query.if_exists,
862 model: CollectionModel::Kv,
863 },
864 ),
865 CollectionModel::Config => self.execute_drop_kv(
866 raw_query,
867 &DropKvQuery {
868 name: query.name.clone(),
869 if_exists: query.if_exists,
870 model: CollectionModel::Config,
871 },
872 ),
873 CollectionModel::Vault => self.execute_drop_kv(
874 raw_query,
875 &DropKvQuery {
876 name: query.name.clone(),
877 if_exists: query.if_exists,
878 model: CollectionModel::Vault,
879 },
880 ),
881 CollectionModel::Hll => self.execute_probabilistic_command(
882 raw_query,
883 &ProbabilisticCommand::DropHll {
884 name: query.name.clone(),
885 if_exists: query.if_exists,
886 },
887 ),
888 CollectionModel::Sketch => self.execute_probabilistic_command(
889 raw_query,
890 &ProbabilisticCommand::DropSketch {
891 name: query.name.clone(),
892 if_exists: query.if_exists,
893 },
894 ),
895 CollectionModel::Filter => self.execute_probabilistic_command(
896 raw_query,
897 &ProbabilisticCommand::DropFilter {
898 name: query.name.clone(),
899 if_exists: query.if_exists,
900 },
901 ),
902 CollectionModel::Metrics => self.execute_drop_typed_collection(
903 raw_query,
904 &query.name,
905 query.if_exists,
906 CollectionModel::Metrics,
907 "metrics",
908 ),
909 CollectionModel::Mixed => self.execute_drop_typed_collection(
910 raw_query,
911 &query.name,
912 query.if_exists,
913 CollectionModel::Mixed,
914 "collection",
915 ),
916 }
917 }
918
919 fn require_graph_for_analytics(&self, name: &str) -> RedDBResult<()> {
925 let is_graph = self
926 .inner
927 .db
928 .collection_contract(name)
929 .map(|c| c.declared_model == crate::catalog::CollectionModel::Graph)
930 .unwrap_or(false);
931 if !is_graph {
932 return Err(RedDBError::Query(format!(
933 "ALTER GRAPH ... ANALYTICS: '{name}' is not a graph collection"
934 )));
935 }
936 Ok(())
937 }
938
939 pub fn execute_alter_table(
945 &self,
946 raw_query: &str,
947 query: &AlterTableQuery,
948 ) -> RedDBResult<RuntimeQueryResult> {
949 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
950 let expression_contract =
951 self.db()
952 .collection_contract(&query.name)
953 .is_some_and(|contract| {
954 crate::application::collection_contract_enforcer::has_contract_expressions(
955 &contract,
956 )
957 });
958 if query.operations.iter().any(|operation| match operation {
959 AlterOperation::AddColumn(column) => {
960 expression_contract || column.generated.is_some() || column.check.is_some()
961 }
962 AlterOperation::DropColumn(_) | AlterOperation::RenameColumn { .. } => {
963 expression_contract
964 }
965 _ => false,
966 }) {
967 return Err(RedDBError::Query("ALTER of expression schemas requires validated backfill; create a new collection and import explicitly".to_string()));
968 }
969
970 let store = self.inner.db.store();
971
972 if store.get_collection(&query.name).is_none() {
974 return Err(RedDBError::NotFound(format!(
975 "table '{}' not found",
976 query.name
977 )));
978 }
979
980 let mut messages = Vec::new();
981
982 let fields_added: Vec<String> = query
984 .operations
985 .iter()
986 .filter_map(|op| {
987 if let AlterOperation::AddColumn(col) = op {
988 Some(col.name.clone())
989 } else {
990 None
991 }
992 })
993 .collect();
994 let fields_removed: Vec<String> = query
995 .operations
996 .iter()
997 .filter_map(|op| {
998 if let AlterOperation::DropColumn(name) = op {
999 Some(name.clone())
1000 } else {
1001 None
1002 }
1003 })
1004 .collect();
1005
1006 for op in &query.operations {
1007 match op {
1008 AlterOperation::AddColumn(col) => {
1009 messages.push(format!("column '{}' added", col.name));
1011 }
1012 AlterOperation::DropColumn(name) => {
1013 messages.push(format!("column '{}' dropped", name));
1014 }
1015 AlterOperation::RenameColumn { from, to } => {
1016 messages.push(format!("column '{}' renamed to '{}'", from, to));
1017 }
1018 AlterOperation::AttachPartition { child, bound } => {
1019 store.set_config_tree(
1023 &format!("partition.{}.children.{}", query.name, child),
1024 &crate::serde_json::Value::String(bound.clone()),
1025 );
1026 messages.push(format!(
1027 "partition '{child}' attached to '{}' ({bound})",
1028 query.name
1029 ));
1030 }
1031 AlterOperation::DetachPartition { child } => {
1032 store.set_config_tree(
1033 &format!("partition.{}.children.{}", query.name, child),
1034 &crate::serde_json::Value::Null,
1035 );
1036 messages.push(format!(
1037 "partition '{child}' detached from '{}'",
1038 query.name
1039 ));
1040 }
1041 AlterOperation::EnableRowLevelSecurity => {
1042 self.inner
1043 .rls_enabled_tables
1044 .write()
1045 .insert(query.name.clone());
1046 store.set_config_tree(
1048 &format!("rls.enabled.{}", query.name),
1049 &crate::serde_json::Value::Bool(true),
1050 );
1051 self.invalidate_plan_cache();
1052 messages.push(format!("row level security enabled on '{}'", query.name));
1053 self.invalidate_result_cache();
1054 }
1055 AlterOperation::DisableRowLevelSecurity => {
1056 self.inner.rls_enabled_tables.write().remove(&query.name);
1057 store.set_config_tree(
1058 &format!("rls.enabled.{}", query.name),
1059 &crate::serde_json::Value::Null,
1060 );
1061 self.invalidate_plan_cache();
1062 messages.push(format!("row level security disabled on '{}'", query.name));
1063 self.invalidate_result_cache();
1064 }
1065 AlterOperation::EnableTenancy { column } => {
1067 store.set_config_tree(
1068 &format!("tenant_tables.{}.column", query.name),
1069 &crate::serde_json::Value::String(column.clone()),
1070 );
1071 self.register_tenant_table(&query.name, column);
1072 self.invalidate_plan_cache();
1073 messages.push(format!(
1074 "tenancy enabled on '{}' by column '{column}'",
1075 query.name
1076 ));
1077 }
1078 AlterOperation::DisableTenancy => {
1079 store.set_config_tree(
1080 &format!("tenant_tables.{}.column", query.name),
1081 &crate::serde_json::Value::Null,
1082 );
1083 self.unregister_tenant_table(&query.name);
1084 self.invalidate_plan_cache();
1085 messages.push(format!("tenancy disabled on '{}'", query.name));
1086 }
1087 AlterOperation::SetAppendOnly(on) => {
1088 messages.push(format!(
1093 "append_only {} on '{}'",
1094 if *on { "enabled" } else { "disabled" },
1095 query.name
1096 ));
1097 }
1098 AlterOperation::SetVersioned(on) => {
1099 self.vcs_set_versioned(&query.name, *on)?;
1106 messages.push(format!(
1107 "versioned {} on '{}'",
1108 if *on { "enabled" } else { "disabled" },
1109 query.name
1110 ));
1111 }
1112 AlterOperation::EnableEvents(subscription) => {
1113 let mut subscription = subscription.clone();
1114 subscription.source = query.name.clone();
1115 validate_event_subscriptions(
1116 self,
1117 &query.name,
1118 std::slice::from_ref(&subscription),
1119 )?;
1120 ensure_event_target_queue(self, &subscription.target_queue)?;
1121 messages.push(format!(
1122 "events enabled on '{}' to '{}'",
1123 query.name, subscription.target_queue
1124 ));
1125 }
1126 AlterOperation::DisableEvents => {
1127 messages.push(format!("events disabled on '{}'", query.name));
1128 }
1129 AlterOperation::AddSubscription { name, descriptor } => {
1130 let mut sub = descriptor.clone();
1131 sub.name = name.clone();
1132 sub.source = query.name.clone();
1133 validate_event_subscriptions(self, &query.name, std::slice::from_ref(&sub))?;
1134 ensure_event_target_queue(self, &sub.target_queue)?;
1135 messages.push(format!(
1136 "subscription '{}' added on '{}' to '{}'",
1137 name, query.name, sub.target_queue
1138 ));
1139 }
1140 AlterOperation::DropSubscription { name } => {
1141 messages.push(format!(
1142 "subscription '{}' dropped on '{}'",
1143 name, query.name
1144 ));
1145 }
1146 AlterOperation::AddSigner { pubkey } => {
1147 if !crate::runtime::signed_writes_kind::is_signed(&store, &query.name) {
1154 return Err(RedDBError::Query(format!(
1155 "ALTER COLLECTION ADD SIGNER: '{}' has no signer registry; \
1156 recreate it with CREATE COLLECTION ... SIGNED_BY (...)",
1157 query.name
1158 )));
1159 }
1160 let actor = crate::runtime::impl_core::current_user_projected()
1161 .unwrap_or_else(|| "@system/alter".to_string());
1162 let changed = crate::runtime::signed_writes_kind::add_signer(
1163 &store,
1164 &query.name,
1165 *pubkey,
1166 &actor,
1167 );
1168 messages.push(format!(
1169 "signer {} on '{}'",
1170 if changed { "added" } else { "already present" },
1171 query.name
1172 ));
1173 }
1174 AlterOperation::RevokeSigner { pubkey } => {
1175 if !crate::runtime::signed_writes_kind::is_signed(&store, &query.name) {
1176 return Err(RedDBError::Query(format!(
1177 "ALTER COLLECTION REVOKE SIGNER: '{}' has no signer registry",
1178 query.name
1179 )));
1180 }
1181 let actor = crate::runtime::impl_core::current_user_projected()
1182 .unwrap_or_else(|| "@system/alter".to_string());
1183 let changed = crate::runtime::signed_writes_kind::revoke_signer(
1184 &store,
1185 &query.name,
1186 pubkey,
1187 &actor,
1188 );
1189 messages.push(format!(
1190 "signer {} on '{}'",
1191 if changed {
1192 "revoked"
1193 } else {
1194 "already revoked"
1195 },
1196 query.name
1197 ));
1198 }
1199 AlterOperation::SetRetention { duration_ms } => {
1200 let existing = self.inner.db.collection_contract(&query.name);
1206 let has_ts_column = existing
1207 .as_ref()
1208 .map(retention_timestamp_column_exists)
1209 .unwrap_or(false);
1210 if !has_ts_column {
1211 return Err(RedDBError::Query(format!(
1212 "ALTER COLLECTION SET RETENTION: '{}' has no timestamp \
1213 column — declare a TIMESTAMP/TIMESTAMPMS/DATETIME column \
1214 or enable WITH timestamps = true before setting a \
1215 retention policy",
1216 query.name
1217 )));
1218 }
1219 messages.push(format!(
1220 "retention set to {duration_ms} ms on '{}'",
1221 query.name
1222 ));
1223 }
1224 AlterOperation::UnsetRetention => {
1225 messages.push(format!("retention cleared on '{}'", query.name));
1226 }
1227 AlterOperation::AddAnalytics(views) => {
1228 self.require_graph_for_analytics(&query.name)?;
1233 let existing = self.inner.db.collection_contract(&query.name);
1234 let enabled: std::collections::BTreeSet<crate::catalog::AnalyticsOutput> =
1235 existing
1236 .as_ref()
1237 .map(|c| c.analytics_config.iter().map(|v| v.output).collect())
1238 .unwrap_or_default();
1239 for view in views {
1240 if enabled.contains(&view.output) {
1241 messages.push(format!(
1244 "analytics '{}' already enabled on '{}'",
1245 view.output.as_str(),
1246 query.name
1247 ));
1248 } else {
1249 messages.push(format!(
1250 "analytics '{}' enabled on '{}'",
1251 view.output.as_str(),
1252 query.name
1253 ));
1254 }
1255 }
1256 }
1257 AlterOperation::DropAnalytics(output) => {
1258 self.require_graph_for_analytics(&query.name)?;
1259 let enabled = self
1260 .inner
1261 .db
1262 .collection_contract(&query.name)
1263 .map(|c| c.analytics_config.iter().any(|v| v.output == *output))
1264 .unwrap_or(false);
1265 if !enabled {
1266 return Err(RedDBError::Query(format!(
1269 "ALTER GRAPH DROP ANALYTICS: analytics output '{}' is not enabled on graph '{}'",
1270 output.as_str(),
1271 query.name
1272 )));
1273 }
1274 messages.push(format!(
1275 "analytics '{}' disabled on '{}'",
1276 output.as_str(),
1277 query.name
1278 ));
1279 }
1280 }
1281 }
1282
1283 let mut contract = self
1284 .inner
1285 .db
1286 .collection_contract(&query.name)
1287 .unwrap_or_else(|| default_collection_contract_for_existing_table(&query.name));
1288 apply_alter_operations_to_contract(&mut contract, &query.operations);
1289 contract.version = contract.version.saturating_add(1);
1290 contract.updated_at_unix_ms = current_unix_ms();
1291 self.inner
1292 .db
1293 .save_collection_contract(contract)
1294 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1295 if !fields_added.is_empty() || !fields_removed.is_empty() {
1299 let sub_names: Vec<String> = self
1300 .inner
1301 .db
1302 .collection_contract(&query.name)
1303 .map(|c| {
1304 c.subscriptions
1305 .iter()
1306 .filter(|s| s.enabled)
1307 .map(|s| s.name.clone())
1308 .collect()
1309 })
1310 .unwrap_or_default();
1311 if !sub_names.is_empty() {
1312 crate::telemetry::operator_event::OperatorEvent::SubscriptionSchemaChange {
1313 collection: query.name.clone(),
1314 subscription_names: sub_names.join(", "),
1315 fields_added: fields_added.join(", "),
1316 fields_removed: fields_removed.join(", "),
1317 lsn: self.cdc_current_lsn(),
1318 }
1319 .emit_global();
1320 }
1321 }
1322
1323 self.clear_table_planner_stats(&query.name);
1324 self.invalidate_result_cache();
1325 let post_alter_columns: Vec<String> = self
1330 .inner
1331 .db
1332 .collection_contract(&query.name)
1333 .map(|contract| {
1334 contract
1335 .declared_columns
1336 .iter()
1337 .map(|col| col.name.clone())
1338 .collect()
1339 })
1340 .unwrap_or_default();
1341 self.schema_vocabulary_apply(
1342 crate::runtime::schema_vocabulary::DdlEvent::AlterCollection {
1343 collection: query.name.clone(),
1344 columns: post_alter_columns,
1345 type_tags: Vec::new(),
1346 description: None,
1347 },
1348 );
1349
1350 let message = if messages.is_empty() {
1351 format!("table '{}' altered (no operations)", query.name)
1352 } else {
1353 format!("table '{}' altered: {}", query.name, messages.join(", "))
1354 };
1355
1356 Ok(RuntimeQueryResult::ok_message(
1357 raw_query.to_string(),
1358 &message,
1359 "alter",
1360 ))
1361 }
1362
1363 pub fn execute_create_vcs_ref(
1364 &self,
1365 raw_query: &str,
1366 query: &CreateVcsRefQuery,
1367 ) -> RedDBResult<RuntimeQueryResult> {
1368 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1369 let connection_id = crate::runtime::impl_core::current_connection_id();
1370 match query.kind {
1371 VcsRefKind::Branch => {
1372 self.vcs_branch_create(crate::application::vcs::CreateBranchInput {
1373 name: query.name.clone(),
1374 from: query.target.clone(),
1375 connection_id,
1376 })?;
1377 }
1378 VcsRefKind::Tag => {
1379 let target = match &query.target {
1380 Some(target) => target.clone(),
1381 None => self
1382 .vcs_status(crate::application::vcs::StatusInput { connection_id })?
1383 .head_commit
1384 .filter(|head| !head.is_empty())
1385 .ok_or_else(|| {
1386 RedDBError::InvalidConfig(
1387 "cannot create tag: HEAD has no commits".to_string(),
1388 )
1389 })?,
1390 };
1391 self.vcs_tag_create(crate::application::vcs::CreateTagInput {
1392 name: query.name.clone(),
1393 target,
1394 annotation: None,
1395 })?;
1396 }
1397 }
1398 let kind = match query.kind {
1399 VcsRefKind::Branch => "branch",
1400 VcsRefKind::Tag => "tag",
1401 };
1402 Ok(RuntimeQueryResult::ok_message(
1403 raw_query.to_string(),
1404 &format!("{kind} '{}' created", query.name),
1405 "create",
1406 ))
1407 }
1408
1409 pub fn execute_drop_vcs_ref(
1410 &self,
1411 raw_query: &str,
1412 query: &DropVcsRefQuery,
1413 ) -> RedDBResult<RuntimeQueryResult> {
1414 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1415 match query.kind {
1416 VcsRefKind::Branch => self.vcs_branch_delete(&query.name)?,
1417 VcsRefKind::Tag => self.vcs_tag_delete(&query.name)?,
1418 }
1419 let kind = match query.kind {
1420 VcsRefKind::Branch => "branch",
1421 VcsRefKind::Tag => "tag",
1422 };
1423 Ok(RuntimeQueryResult::ok_message(
1424 raw_query.to_string(),
1425 &format!("{kind} '{}' dropped", query.name),
1426 "drop",
1427 ))
1428 }
1429
1430 pub fn execute_fork_store(
1431 &self,
1432 raw_query: &str,
1433 query: &ForkStoreQuery,
1434 ) -> RedDBResult<RuntimeQueryResult> {
1435 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1436 let path = self.inner.db.path().ok_or_else(|| {
1437 RedDBError::Query("FORK STORE requires a persistent store".to_string())
1438 })?;
1439 let durable_lsn = self.cdc_current_lsn();
1440 let fork_lsn = query.at_lsn.unwrap_or(durable_lsn);
1441 let retention_floor = operational_wal_retention_floor(durable_lsn);
1442 validate_store_fork_lsn(fork_lsn, retention_floor, durable_lsn)?;
1443
1444 let manifest = reddb_file::OperationalManifest::for_db_path(path);
1445 let mut collections = self.inner.db.store().list_collections();
1446 collections.sort();
1447 manifest.recover_or_bootstrap(&collections).map_err(|err| {
1448 RedDBError::Query(format!("failed to prepare store fork manifest: {err}"))
1449 })?;
1450 manifest
1451 .create_fork(&query.name, fork_lsn)
1452 .map_err(|err| RedDBError::Query(format!("failed to create store fork: {err}")))?;
1453 Ok(RuntimeQueryResult::ok_message(
1454 raw_query.to_string(),
1455 &format!("store fork '{}' created at LSN {fork_lsn}", query.name),
1456 "fork_store",
1457 ))
1458 }
1459
1460 pub fn execute_drop_fork(
1461 &self,
1462 raw_query: &str,
1463 query: &DropForkQuery,
1464 ) -> RedDBResult<RuntimeQueryResult> {
1465 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1466 let path = self.inner.db.path().ok_or_else(|| {
1467 RedDBError::Query("DROP FORK requires a persistent store".to_string())
1468 })?;
1469 let manifest = reddb_file::OperationalManifest::for_db_path(path);
1470 let dropped = manifest
1471 .drop_fork(&query.name)
1472 .map_err(|err| RedDBError::Query(format!("failed to drop store fork: {err}")))?;
1473 if !dropped && !query.if_exists {
1474 return Err(RedDBError::NotFound(format!("store fork '{}'", query.name)));
1475 }
1476 Ok(RuntimeQueryResult::ok_message(
1477 raw_query.to_string(),
1478 &format!("store fork '{}' dropped", query.name),
1479 "drop_fork",
1480 ))
1481 }
1482
1483 pub fn execute_promote_fork(
1484 &self,
1485 raw_query: &str,
1486 query: &PromoteForkQuery,
1487 ) -> RedDBResult<RuntimeQueryResult> {
1488 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1489 let path = self.inner.db.path().ok_or_else(|| {
1490 RedDBError::Query("PROMOTE FORK requires a persistent store".to_string())
1491 })?;
1492 self.flush()?;
1493 let manifest = reddb_file::OperationalManifest::for_db_path(path);
1494 let outcome = manifest
1495 .promote_fork(&query.name)
1496 .map_err(|err| RedDBError::Query(format!("failed to promote store fork: {err}")))?;
1497 let outcome =
1498 outcome.ok_or_else(|| RedDBError::NotFound(format!("store fork '{}'", query.name)))?;
1499 Ok(RuntimeQueryResult::ok_message(
1500 raw_query.to_string(),
1501 &format!(
1502 "store fork '{}' promoted at LSN {}; retired parent archived at {}",
1503 outcome.name,
1504 outcome.fork_lsn,
1505 outcome.archived_parent.store_identity()
1506 ),
1507 "promote_fork",
1508 ))
1509 }
1510
1511 pub fn execute_explain_alter(
1518 &self,
1519 raw_query: &str,
1520 query: &ExplainAlterQuery,
1521 ) -> RedDBResult<RuntimeQueryResult> {
1522 let analyzed = analyze_create_table(&query.target)
1526 .map_err(|err| RedDBError::Query(err.to_string()))?;
1527
1528 let current_contract = self.inner.db.collection_contract(&query.target.name);
1529
1530 let current_unique = current_contract
1533 .as_ref()
1534 .map(|contract| {
1535 contract
1536 .table_def
1537 .iter()
1538 .flat_map(|table| &table.constraints)
1539 .filter(|constraint| {
1540 constraint.constraint_type == reddb_types::ConstraintType::Unique
1541 && !contract.declared_columns.iter().any(|column| {
1542 column.unique
1543 && constraint.columns == [column.name.clone()]
1544 && constraint.name == format!("uniq_{}", column.name)
1545 })
1546 })
1547 .map(|constraint| (constraint.name.clone(), constraint.columns.clone()))
1548 .collect::<BTreeSet<_>>()
1549 })
1550 .unwrap_or_default();
1551 let target_unique = analyzed
1552 .unique_constraints
1553 .into_iter()
1554 .map(|constraint| (constraint.name, constraint.columns))
1555 .collect::<BTreeSet<_>>();
1556 if current_unique != target_unique {
1557 return Err(RedDBError::Query("NOT_YET_SUPPORTED: EXPLAIN ALTER cannot plan changes to table-level UNIQUE constraints".to_string()));
1558 }
1559
1560 let current_columns: Vec<crate::physical::DeclaredColumnContract> = current_contract
1561 .as_ref()
1562 .map(|c| c.declared_columns.clone())
1563 .unwrap_or_default();
1564
1565 let diff = super::schema_diff::compute_column_diff(
1566 &query.target.name,
1567 ¤t_columns,
1568 &query.target.columns,
1569 );
1570
1571 let rendered = match query.format {
1572 ExplainFormat::Sql => super::schema_diff::format_as_sql(&diff),
1573 ExplainFormat::Json => super::schema_diff::format_as_json(&diff),
1574 };
1575
1576 let format_label = match query.format {
1577 ExplainFormat::Sql => "sql",
1578 ExplainFormat::Json => "json",
1579 };
1580
1581 let columns = vec![
1582 "table".to_string(),
1583 "format".to_string(),
1584 "diff".to_string(),
1585 ];
1586 let row = vec![
1587 ("table".to_string(), Value::text(query.target.name.clone())),
1588 ("format".to_string(), Value::text(format_label.to_string())),
1589 ("diff".to_string(), Value::text(rendered)),
1590 ];
1591
1592 Ok(RuntimeQueryResult::ok_records(
1593 raw_query.to_string(),
1594 columns,
1595 vec![row],
1596 "explain",
1597 ))
1598 }
1599
1600 pub fn execute_create_index(
1605 &self,
1606 raw_query: &str,
1607 query: &CreateIndexQuery,
1608 ) -> RedDBResult<RuntimeQueryResult> {
1609 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1610 let topology_lock = self
1611 .inner
1612 .index_store
1613 .collection_topology_lock(&query.table);
1614 let _topology_guard = topology_lock.write();
1615 let store = self.inner.db.store();
1616
1617 let manager = store
1619 .get_collection(&query.table)
1620 .ok_or_else(|| RedDBError::NotFound(format!("table '{}' not found", query.table)))?;
1621
1622 if query.if_not_exists
1623 && self
1624 .inner
1625 .index_store
1626 .list_indices(&query.table)
1627 .iter()
1628 .any(|index| index.name == query.name)
1629 {
1630 return Ok(RuntimeQueryResult::ok_message(
1631 raw_query.to_string(),
1632 &format!(
1633 "index '{}' already exists on '{}'; skipped",
1634 query.name, query.table
1635 ),
1636 "create",
1637 ));
1638 }
1639
1640 let method_kind = match query.method {
1641 IndexMethod::Hash => super::index_store::IndexMethodKind::Hash,
1642 IndexMethod::BTree => super::index_store::IndexMethodKind::BTree,
1643 IndexMethod::Bitmap => super::index_store::IndexMethodKind::Bitmap,
1644 IndexMethod::Spatial => super::index_store::IndexMethodKind::default_spatial(),
1647 IndexMethod::H3 { resolution } => {
1648 super::index_store::IndexMethodKind::H3 { resolution }
1649 }
1650 };
1651
1652 let entities = manager.query_all(|_| true);
1662 let mut entity_fields: Vec<(crate::storage::unified::EntityId, Vec<(String, Value)>)> =
1663 entities
1664 .iter()
1665 .map(|e| {
1666 let fields = match &e.data {
1667 crate::storage::EntityData::Row(row) => {
1668 if let Some(ref named) = row.named {
1669 named.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
1670 } else if let Some(ref schema) = row.schema {
1671 schema
1675 .iter()
1676 .zip(row.columns.iter())
1677 .map(|(k, v)| (k.clone(), v.clone()))
1678 .collect()
1679 } else {
1680 Vec::new()
1681 }
1682 }
1683 crate::storage::EntityData::Node(node) => node
1684 .properties
1685 .iter()
1686 .map(|(k, v)| (k.clone(), v.clone()))
1687 .collect(),
1688 crate::storage::EntityData::TimeSeries(ts) => ts
1689 .fields
1690 .iter()
1691 .map(|(k, v)| (k.clone(), v.clone()))
1692 .collect(),
1693 _ => Vec::new(),
1694 };
1695 (e.id, fields)
1696 })
1697 .collect();
1698
1699 if query.unique
1700 && matches!(
1701 method_kind,
1702 super::index_store::IndexMethodKind::Hash
1703 | super::index_store::IndexMethodKind::BTree
1704 )
1705 {
1706 let snapshots = self.snapshot_manager();
1711 let own_xids = self.own_transaction_xids();
1712 let mut live = entities.iter().map(|entity| {
1713 snapshots.row_reserves_unique_key(entity.xmin, entity.xmax, &own_xids)
1714 });
1715 entity_fields.retain(|_| live.next().unwrap_or(false));
1716 if let Some((earlier, later)) =
1717 super::index_store::first_duplicate_unique_key(&query.columns, &entity_fields)
1718 {
1719 return Err(RedDBError::Query(format!(
1720 "cannot create unique index '{}' on '{}': rows {} and {} share the same ({}) key",
1721 query.name,
1722 query.table,
1723 earlier.raw(),
1724 later.raw(),
1725 query.columns.join(", ")
1726 )));
1727 }
1728 }
1729
1730 let index_growth_rows = entity_fields
1731 .iter()
1732 .map(|(_, fields)| fields.clone())
1733 .collect::<Vec<_>>();
1734 let _reservation = self.admit_non_evictable_growth(
1735 crate::storage::memory_pools::MemoryPool::IndexMemory,
1736 &format!("create index {}", query.name),
1737 crate::runtime::memory_admission::estimate_index_growth(
1738 &index_growth_rows,
1739 &query.columns,
1740 ),
1741 )?;
1742
1743 let indexed_count = self
1745 .inner
1746 .index_store
1747 .create_index(
1748 &query.name,
1749 &query.table,
1750 &query.columns,
1751 method_kind,
1752 query.unique,
1753 &entity_fields,
1754 )
1755 .map_err(RedDBError::Internal)?;
1756
1757 let analyzed = crate::storage::query::planner::stats_catalog::analyze_entity_fields(
1758 &query.table,
1759 &entity_fields,
1760 );
1761 crate::storage::query::planner::stats_catalog::persist_table_stats(&store, &analyzed);
1762 self.invalidate_plan_cache();
1763
1764 self.inner
1766 .index_store
1767 .register(super::index_store::RegisteredIndex {
1768 name: query.name.clone(),
1769 collection: query.table.clone(),
1770 columns: query.columns.clone(),
1771 method: method_kind,
1772 unique: query.unique,
1773 });
1774 self.persist_runtime_index_descriptor(super::index_store::RegisteredIndex {
1775 name: query.name.clone(),
1776 collection: query.table.clone(),
1777 columns: query.columns.clone(),
1778 method: method_kind,
1779 unique: query.unique,
1780 })?;
1781 self.schema_vocabulary_apply(crate::runtime::schema_vocabulary::DdlEvent::CreateIndex {
1785 collection: query.table.clone(),
1786 index: query.name.clone(),
1787 columns: query.columns.clone(),
1788 });
1789
1790 let method_str = format!("{}", query.method);
1791 let unique_str = if query.unique { "unique " } else { "" };
1792 let cols = query.columns.join(", ");
1793 let total_count = entity_fields.len();
1794 let coverage_detail =
1795 if matches!(method_kind, super::index_store::IndexMethodKind::H3 { .. })
1796 && total_count > 0
1797 && indexed_count == 0
1798 {
1799 format!(
1800 "{} of {} entities indexed — no indexable geo value in '{}'; expected {}",
1801 indexed_count,
1802 total_count,
1803 cols,
1804 crate::geo::RECOGNIZED_GEO_SHAPES
1805 )
1806 } else if matches!(method_kind, super::index_store::IndexMethodKind::H3 { .. }) {
1807 format!("{} of {} entities indexed", indexed_count, total_count)
1808 } else {
1809 format!("{} entities indexed", indexed_count)
1810 };
1811
1812 Ok(RuntimeQueryResult::ok_message(
1813 raw_query.to_string(),
1814 &format!(
1815 "{}index '{}' created on '{}' ({}) using {} ({})",
1816 unique_str, query.name, query.table, cols, method_str, coverage_detail
1817 ),
1818 "create",
1819 ))
1820 }
1821
1822 pub fn execute_drop_index(
1826 &self,
1827 raw_query: &str,
1828 query: &DropIndexQuery,
1829 ) -> RedDBResult<RuntimeQueryResult> {
1830 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1831 let topology_lock = self
1832 .inner
1833 .index_store
1834 .collection_topology_lock(&query.table);
1835 let _topology_guard = topology_lock.write();
1836 let store = self.inner.db.store();
1837
1838 if store.get_collection(&query.table).is_none() {
1840 if query.if_exists {
1841 return Ok(RuntimeQueryResult::ok_message(
1842 raw_query.to_string(),
1843 &format!("table '{}' does not exist", query.table),
1844 "drop",
1845 ));
1846 }
1847 return Err(RedDBError::NotFound(format!(
1848 "table '{}' not found",
1849 query.table
1850 )));
1851 }
1852
1853 self.inner.index_store.drop_index(&query.name, &query.table);
1855 self.persist_runtime_index_drop(&query.table, &query.name)?;
1856 self.invalidate_plan_cache();
1857 self.schema_vocabulary_apply(crate::runtime::schema_vocabulary::DdlEvent::DropIndex {
1859 collection: query.table.clone(),
1860 index: query.name.clone(),
1861 });
1862
1863 Ok(RuntimeQueryResult::ok_message(
1864 raw_query.to_string(),
1865 &format!("index '{}' dropped from '{}'", query.name, query.table),
1866 "drop",
1867 ))
1868 }
1869
1870 fn execute_drop_typed_collection(
1871 &self,
1872 raw_query: &str,
1873 name: &str,
1874 if_exists: bool,
1875 expected_model: CollectionModel,
1876 label: &str,
1877 ) -> RedDBResult<RuntimeQueryResult> {
1878 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1879 if is_system_schema_name(name) {
1880 return Err(RedDBError::Query("system schema is read-only".to_string()));
1881 }
1882 let store = self.inner.db.store();
1883 if store.get_collection(name).is_none() {
1884 if if_exists {
1885 return Ok(RuntimeQueryResult::ok_message(
1886 raw_query.to_string(),
1887 &format!("{label} '{name}' does not exist"),
1888 "drop",
1889 ));
1890 }
1891 return Err(RedDBError::NotFound(format!("{label} '{name}' not found")));
1892 }
1893
1894 let actual = self
1895 .inner
1896 .db
1897 .collection_contract(name)
1898 .map(|contract| contract.declared_model)
1899 .map(Ok)
1900 .unwrap_or_else(|| {
1901 polymorphic_resolver::resolve(name, &self.inner.db.catalog_model_snapshot())
1902 })?;
1903 polymorphic_resolver::ensure_model_match(expected_model, actual)?;
1904 self.drop_collection_storage(raw_query, name, label)
1905 }
1906
1907 pub fn execute_truncate(
1908 &self,
1909 raw_query: &str,
1910 query: &TruncateQuery,
1911 ) -> RedDBResult<RuntimeQueryResult> {
1912 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1913 if is_system_schema_name(&query.name) {
1914 return Err(RedDBError::Query("system schema is read-only".to_string()));
1915 }
1916
1917 let label = query
1918 .model
1919 .map(polymorphic_resolver::model_name)
1920 .unwrap_or("collection");
1921 let store = self.inner.db.store();
1922 if store.get_collection(&query.name).is_none() {
1923 if query.if_exists {
1924 return Ok(RuntimeQueryResult::ok_message(
1925 raw_query.to_string(),
1926 &format!("{label} '{}' does not exist", query.name),
1927 "truncate",
1928 ));
1929 }
1930 return Err(RedDBError::NotFound(format!(
1931 "{label} '{}' not found",
1932 query.name
1933 )));
1934 }
1935
1936 let actual =
1937 polymorphic_resolver::resolve(&query.name, &self.inner.db.catalog_model_snapshot())?;
1938 if let Some(expected) = query.model {
1939 polymorphic_resolver::ensure_model_match(expected, actual)?;
1940 }
1941
1942 if actual == CollectionModel::Queue {
1943 return self.execute_queue_command(
1944 raw_query,
1945 &QueueCommand::Purge {
1946 queue: query.name.clone(),
1947 },
1948 );
1949 }
1950
1951 let affected = self.truncate_collection_entities(&query.name)?;
1953 crate::runtime::mutation::emit_truncate_event_for_collection(self, &query.name, affected)?;
1955 self.inner.db.invalidate_vector_index(&query.name);
1956 self.clear_table_planner_stats(&query.name);
1957 self.invalidate_result_cache();
1958
1959 Ok(RuntimeQueryResult::ok_message(
1960 raw_query.to_string(),
1961 &format!(
1962 "{affected} entities truncated from {label} '{}'",
1963 query.name
1964 ),
1965 "truncate",
1966 ))
1967 }
1968
1969 fn truncate_collection_entities(&self, name: &str) -> RedDBResult<u64> {
1970 let store = self.inner.db.store();
1971 let Some(manager) = store.get_collection(name) else {
1972 return Ok(0);
1973 };
1974 let entities = manager.query_all(|_| true);
1975 if entities.is_empty() {
1976 return Ok(0);
1977 }
1978
1979 for entity in &entities {
1980 let fields = entity_index_fields(&entity.data);
1981 self.inner
1982 .index_store
1983 .index_entity_delete(name, entity.id, &fields)
1984 .map_err(RedDBError::Internal)?;
1985 }
1986
1987 let ids = entities.iter().map(|entity| entity.id).collect::<Vec<_>>();
1988 let deleted_ids = store
1989 .delete_batch(name, &ids)
1990 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1991 for id in &deleted_ids {
1992 store.context_index().remove_entity(*id);
1993 }
1994 Ok(deleted_ids.len() as u64)
1995 }
1996
1997 fn drop_collection_storage(
1998 &self,
1999 raw_query: &str,
2000 name: &str,
2001 label: &str,
2002 ) -> RedDBResult<RuntimeQueryResult> {
2003 let store = self.inner.db.store();
2004
2005 let final_count = store
2008 .get_collection(name)
2009 .map(|manager| manager.query_all(|_| true).len() as u64)
2010 .unwrap_or(0);
2011 crate::runtime::mutation::emit_collection_dropped_event_for_collection(
2012 self,
2013 name,
2014 final_count,
2015 )?;
2016
2017 let topology_lock = self.inner.index_store.collection_topology_lock(name);
2018 let _topology_guard = topology_lock.write();
2019 let orphaned_indices: Vec<String> = self
2020 .inner
2021 .index_store
2022 .list_indices(name)
2023 .into_iter()
2024 .map(|index| index.name)
2025 .collect();
2026 for index_name in &orphaned_indices {
2027 self.inner.index_store.drop_index(index_name, name);
2028 }
2029
2030 store
2031 .drop_collection(name)
2032 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2033 self.inner.db.invalidate_vector_index(name);
2034 self.inner.db.clear_collection_default_ttl_ms(name);
2035 self.inner
2036 .db
2037 .remove_collection_contract(name)
2038 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2039 self.clear_table_planner_stats(name);
2040 self.invalidate_result_cache();
2041 if let Some(store) = self.inner.auth_store.read().clone() {
2042 store.invalidate_visible_collections_cache();
2043 }
2044 self.inner
2045 .db
2046 .persist_metadata()
2047 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2048 self.schema_vocabulary_apply(
2049 crate::runtime::schema_vocabulary::DdlEvent::DropCollection {
2050 collection: name.to_string(),
2051 },
2052 );
2053
2054 Ok(RuntimeQueryResult::ok_message(
2055 raw_query.to_string(),
2056 &format!("{label} '{name}' dropped"),
2057 "drop",
2058 ))
2059 }
2060}
2061
2062pub(crate) fn is_system_schema_name(name: &str) -> bool {
2063 name == "red" || name.starts_with("red.") || name.starts_with("__red_schema_")
2064}
2065
2066fn entity_index_fields(data: &EntityData) -> Vec<(String, Value)> {
2067 match data {
2068 EntityData::Row(row) => {
2069 if let Some(ref named) = row.named {
2070 named
2071 .iter()
2072 .map(|(key, value)| (key.clone(), value.clone()))
2073 .collect()
2074 } else if let Some(ref schema) = row.schema {
2075 schema
2076 .iter()
2077 .zip(row.columns.iter())
2078 .map(|(key, value)| (key.clone(), value.clone()))
2079 .collect()
2080 } else {
2081 Vec::new()
2082 }
2083 }
2084 EntityData::Node(node) => node
2085 .properties
2086 .iter()
2087 .map(|(key, value)| (key.clone(), value.clone()))
2088 .collect(),
2089 _ => Vec::new(),
2090 }
2091}
2092
2093fn validate_ai_policy_modalities(policy: &crate::catalog::AiPolicy) -> RedDBResult<()> {
2099 use crate::runtime::ai::provider_capabilities::{Modality, Registry};
2100 let registry = Registry::new();
2101 let check = |provider: &str, model: &str, modality: Modality| -> RedDBResult<()> {
2102 registry
2103 .validate_policy_modality(provider, model, modality)
2104 .map_err(|err| RedDBError::Query(err.to_string()))
2105 };
2106 if let Some(embed) = &policy.embed {
2107 check(&embed.provider, &embed.model, Modality::Embed)?;
2108 }
2109 if let Some(moderate) = &policy.moderate {
2110 check(&moderate.provider, &moderate.model, Modality::Moderate)?;
2111 }
2112 if let Some(vision) = &policy.vision {
2113 check(&vision.provider, &vision.model, Modality::Vision)?;
2114 }
2115 Ok(())
2116}
2117
2118fn collection_contract_from_create_table(
2119 query: &CreateTableQuery,
2120) -> RedDBResult<crate::physical::CollectionContract> {
2121 reddb_engine::catalog::ddl::collection_contract_from_create_table(query, current_unix_ms())
2122 .map_err(RedDBError::Query)
2123}
2124
2125fn default_collection_contract_for_existing_table(
2126 name: &str,
2127) -> crate::physical::CollectionContract {
2128 let now = current_unix_ms();
2129 crate::physical::CollectionContract {
2130 name: name.to_string(),
2131 declared_model: crate::catalog::CollectionModel::Table,
2132 schema_mode: crate::catalog::SchemaMode::SemiStructured,
2133 origin: crate::physical::ContractOrigin::Explicit,
2134 version: 0,
2135 created_at_unix_ms: now,
2136 updated_at_unix_ms: now,
2137 default_ttl_ms: None,
2138 vector_dimension: None,
2139 vector_metric: None,
2140 context_index_fields: Vec::new(),
2141 declared_columns: Vec::new(),
2142 table_def: Some(reddb_types::TableDef::new(name.to_string())),
2143 timestamps_enabled: false,
2144 context_index_enabled: false,
2145 metrics_raw_retention_ms: None,
2146 metrics_rollup_policies: Vec::new(),
2147 metrics_tenant_identity: None,
2148 metrics_namespace: None,
2149 append_only: false,
2150 subscriptions: Vec::new(),
2151 analytics_config: Vec::new(),
2152 session_key: None,
2153 session_gap_ms: None,
2154 retention_duration_ms: None,
2155 analytical_storage: None,
2156
2157 ai_policy: None,
2158 }
2159}
2160
2161fn keyed_collection_contract(
2162 name: &str,
2163 model: crate::catalog::CollectionModel,
2164 analytics_config: Vec<crate::catalog::AnalyticsViewDescriptor>,
2165) -> crate::physical::CollectionContract {
2166 let now = current_unix_ms();
2167 crate::physical::CollectionContract {
2168 name: name.to_string(),
2169 declared_model: model,
2170 schema_mode: crate::catalog::SchemaMode::Dynamic,
2171 origin: crate::physical::ContractOrigin::Explicit,
2172 version: 1,
2173 created_at_unix_ms: now,
2174 updated_at_unix_ms: now,
2175 default_ttl_ms: None,
2176 vector_dimension: None,
2177 vector_metric: None,
2178 context_index_fields: Vec::new(),
2179 declared_columns: Vec::new(),
2180 table_def: None,
2181 timestamps_enabled: false,
2182 context_index_enabled: false,
2183 metrics_raw_retention_ms: None,
2184 metrics_rollup_policies: Vec::new(),
2185 metrics_tenant_identity: None,
2186 metrics_namespace: None,
2187 append_only: false,
2188 subscriptions: Vec::new(),
2189 analytics_config,
2190 session_key: None,
2191 session_gap_ms: None,
2192 retention_duration_ms: None,
2193 analytical_storage: None,
2194
2195 ai_policy: None,
2196 }
2197}
2198
2199fn metrics_collection_contract(query: &CreateTableQuery) -> crate::physical::CollectionContract {
2200 let now = current_unix_ms();
2201 crate::physical::CollectionContract {
2202 name: query.name.clone(),
2203 declared_model: crate::catalog::CollectionModel::Metrics,
2204 schema_mode: crate::catalog::SchemaMode::SemiStructured,
2205 origin: crate::physical::ContractOrigin::Explicit,
2206 version: 1,
2207 created_at_unix_ms: now,
2208 updated_at_unix_ms: now,
2209 default_ttl_ms: query.default_ttl_ms,
2210 vector_dimension: None,
2211 vector_metric: None,
2212 context_index_fields: Vec::new(),
2213 declared_columns: Vec::new(),
2214 table_def: None,
2215 timestamps_enabled: false,
2216 context_index_enabled: false,
2217 metrics_raw_retention_ms: query.default_ttl_ms,
2218 metrics_rollup_policies: query.metrics_rollup_policies.clone(),
2219 metrics_tenant_identity: Some(
2220 query
2221 .tenant_by
2222 .clone()
2223 .unwrap_or_else(|| "current_tenant".to_string()),
2224 ),
2225 metrics_namespace: Some("default".to_string()),
2226 append_only: true,
2227 subscriptions: Vec::new(),
2228 analytics_config: Vec::new(),
2229 session_key: None,
2230 session_gap_ms: None,
2231 retention_duration_ms: None,
2232 analytical_storage: None,
2233
2234 ai_policy: None,
2235 }
2236}
2237
2238fn vector_collection_contract(query: &CreateVectorQuery) -> crate::physical::CollectionContract {
2239 let now = current_unix_ms();
2240 crate::physical::CollectionContract {
2241 name: query.name.clone(),
2242 declared_model: crate::catalog::CollectionModel::Vector,
2243 schema_mode: crate::catalog::SchemaMode::Dynamic,
2244 origin: crate::physical::ContractOrigin::Explicit,
2245 version: 1,
2246 created_at_unix_ms: now,
2247 updated_at_unix_ms: now,
2248 default_ttl_ms: None,
2249 vector_dimension: Some(query.dimension),
2250 vector_metric: Some(query.metric),
2251 context_index_fields: Vec::new(),
2252 declared_columns: Vec::new(),
2253 table_def: None,
2254 timestamps_enabled: false,
2255 context_index_enabled: false,
2256 metrics_raw_retention_ms: None,
2257 metrics_rollup_policies: Vec::new(),
2258 metrics_tenant_identity: None,
2259 metrics_namespace: None,
2260 append_only: false,
2261 subscriptions: Vec::new(),
2262 analytics_config: Vec::new(),
2263 session_key: None,
2264 session_gap_ms: None,
2265 retention_duration_ms: None,
2266 analytical_storage: None,
2267
2268 ai_policy: None,
2269 }
2270}
2271
2272fn apply_alter_operations_to_contract(
2273 contract: &mut crate::physical::CollectionContract,
2274 operations: &[AlterOperation],
2275) {
2276 if contract.table_def.is_none() {
2277 contract.table_def = Some(reddb_types::TableDef::new(contract.name.clone()));
2278 }
2279 for operation in operations {
2280 match operation {
2281 AlterOperation::AddColumn(column) => {
2282 if !contract
2283 .declared_columns
2284 .iter()
2285 .any(|existing| existing.name == column.name)
2286 {
2287 contract
2288 .declared_columns
2289 .push(declared_column_contract_from_ddl(column));
2290 }
2291 if let Some(table_def) = contract.table_def.as_mut() {
2292 if table_def.get_column(&column.name).is_none() {
2293 if let Ok(column_def) = column_def_from_ddl(column) {
2294 if column.primary_key {
2295 table_def.primary_key.push(column.name.clone());
2296 table_def.constraints.push(
2297 reddb_types::Constraint::new(
2298 format!("pk_{}", column.name),
2299 reddb_types::ConstraintType::PrimaryKey,
2300 )
2301 .on_columns(vec![column.name.clone()]),
2302 );
2303 }
2304 if column.unique {
2305 table_def.constraints.push(
2306 reddb_types::Constraint::new(
2307 format!("uniq_{}", column.name),
2308 reddb_types::ConstraintType::Unique,
2309 )
2310 .on_columns(vec![column.name.clone()]),
2311 );
2312 }
2313 if column.not_null {
2314 table_def.constraints.push(
2315 reddb_types::Constraint::new(
2316 format!("not_null_{}", column.name),
2317 reddb_types::ConstraintType::NotNull,
2318 )
2319 .on_columns(vec![column.name.clone()]),
2320 );
2321 }
2322 table_def.columns.push(column_def);
2323 }
2324 }
2325 }
2326 }
2327 AlterOperation::DropColumn(name) => {
2328 contract
2329 .declared_columns
2330 .retain(|column| column.name != *name);
2331 if let Some(table_def) = contract.table_def.as_mut() {
2332 if let Some(index) = table_def.column_index(name) {
2333 table_def.columns.remove(index);
2334 }
2335 table_def.primary_key.retain(|column| column != name);
2336 table_def.constraints.retain(|constraint| {
2337 !constraint.columns.iter().any(|column| column == name)
2338 });
2339 table_def
2340 .indexes
2341 .retain(|index| !index.columns.iter().any(|column| column == name));
2342 }
2343 }
2344 AlterOperation::RenameColumn { from, to } => {
2345 if contract
2346 .declared_columns
2347 .iter()
2348 .any(|column| column.name == *to)
2349 {
2350 continue;
2351 }
2352 if let Some(column) = contract
2353 .declared_columns
2354 .iter_mut()
2355 .find(|column| column.name == *from)
2356 {
2357 column.name = to.clone();
2358 }
2359 if let Some(table_def) = contract.table_def.as_mut() {
2360 if let Some(column) = table_def
2361 .columns
2362 .iter_mut()
2363 .find(|column| column.name == *from)
2364 {
2365 column.name = to.clone();
2366 }
2367 for primary_key in &mut table_def.primary_key {
2368 if *primary_key == *from {
2369 *primary_key = to.clone();
2370 }
2371 }
2372 for constraint in &mut table_def.constraints {
2373 for column in &mut constraint.columns {
2374 if *column == *from {
2375 *column = to.clone();
2376 }
2377 }
2378 if let Some(ref_columns) = constraint.ref_columns.as_mut() {
2379 for column in ref_columns {
2380 if *column == *from {
2381 *column = to.clone();
2382 }
2383 }
2384 }
2385 }
2386 for index in &mut table_def.indexes {
2387 for column in &mut index.columns {
2388 if *column == *from {
2389 *column = to.clone();
2390 }
2391 }
2392 }
2393 }
2394 }
2395 AlterOperation::AttachPartition { .. } | AlterOperation::DetachPartition { .. } => {}
2398 AlterOperation::EnableRowLevelSecurity | AlterOperation::DisableRowLevelSecurity => {}
2402 AlterOperation::EnableTenancy { .. } | AlterOperation::DisableTenancy => {}
2405 AlterOperation::SetAppendOnly(on) => {
2406 contract.append_only = *on;
2407 }
2408 AlterOperation::SetVersioned(_) => {}
2411 AlterOperation::EnableEvents(subscription) => {
2412 let mut subscription = subscription.clone();
2413 subscription.source = contract.name.clone();
2414 subscription.enabled = true;
2415 if let Some(existing) = contract
2416 .subscriptions
2417 .iter_mut()
2418 .find(|existing| existing.target_queue == subscription.target_queue)
2419 {
2420 *existing = subscription;
2421 } else {
2422 contract.subscriptions.push(subscription);
2423 }
2424 }
2425 AlterOperation::DisableEvents => {
2426 for subscription in &mut contract.subscriptions {
2427 subscription.enabled = false;
2428 }
2429 }
2430 AlterOperation::AddSubscription { name, descriptor } => {
2431 let mut sub = descriptor.clone();
2432 sub.name = name.clone();
2433 sub.source = contract.name.clone();
2434 sub.enabled = true;
2435 if let Some(existing) = contract.subscriptions.iter_mut().find(|s| s.name == *name)
2436 {
2437 *existing = sub;
2438 } else {
2439 contract.subscriptions.push(sub);
2440 }
2441 }
2442 AlterOperation::DropSubscription { name } => {
2443 contract.subscriptions.retain(|s| s.name != *name);
2444 }
2445 AlterOperation::AddSigner { .. } | AlterOperation::RevokeSigner { .. } => {}
2450 AlterOperation::SetRetention { duration_ms } => {
2451 contract.retention_duration_ms = Some(*duration_ms);
2452 }
2453 AlterOperation::UnsetRetention => {
2454 contract.retention_duration_ms = None;
2455 }
2456 AlterOperation::AddAnalytics(views) => {
2460 for view in views {
2461 if !contract
2465 .analytics_config
2466 .iter()
2467 .any(|existing| existing.output == view.output)
2468 {
2469 contract.analytics_config.push(view.clone());
2470 }
2471 }
2472 }
2473 AlterOperation::DropAnalytics(output) => {
2474 contract
2475 .analytics_config
2476 .retain(|view| view.output != *output);
2477 }
2478 }
2479 }
2480}
2481
2482pub(crate) fn retention_timestamp_column_exists(
2487 contract: &crate::physical::CollectionContract,
2488) -> bool {
2489 if contract.timestamps_enabled {
2490 return true;
2491 }
2492 if matches!(
2493 contract.declared_model,
2494 crate::catalog::CollectionModel::TimeSeries | crate::catalog::CollectionModel::Metrics
2495 ) {
2496 return true;
2500 }
2501 contract
2502 .declared_columns
2503 .iter()
2504 .any(|column| is_temporal_data_type(&column.data_type))
2505}
2506
2507fn is_temporal_data_type(data_type: &str) -> bool {
2508 let upper = data_type.to_ascii_uppercase();
2509 matches!(
2510 upper.as_str(),
2511 "TIMESTAMP" | "TIMESTAMPMS" | "TIMESTAMP_MS" | "DATETIME" | "DATE"
2512 )
2513}
2514
2515fn validate_event_subscriptions(
2516 runtime: &RedDBRuntime,
2517 source: &str,
2518 subscriptions: &[crate::catalog::SubscriptionDescriptor],
2519) -> RedDBResult<()> {
2520 for subscription in subscriptions
2521 .iter()
2522 .filter(|subscription| subscription.enabled)
2523 {
2524 if subscription.all_tenants && crate::runtime::impl_core::current_tenant().is_some() {
2525 return Err(RedDBError::Query(
2526 "cross-tenant subscription requires cluster-admin capability (events:cluster_subscribe)".to_string(),
2527 ));
2528 }
2529 validate_subscription_auth(runtime, source, subscription)?;
2530 if subscription.target_queue == source
2531 || subscription_would_create_cycle(
2532 &runtime.inner.db,
2533 source,
2534 &subscription.target_queue,
2535 )
2536 {
2537 return Err(RedDBError::Query(
2538 "subscription would create cycle".to_string(),
2539 ));
2540 }
2541 audit_subscription_redact_gap(runtime, source, subscription);
2542 }
2543 Ok(())
2544}
2545
2546fn validate_subscription_auth(
2547 runtime: &RedDBRuntime,
2548 source: &str,
2549 subscription: &crate::catalog::SubscriptionDescriptor,
2550) -> RedDBResult<()> {
2551 let auth_store = match runtime.inner.auth_store.read().clone() {
2552 Some(store) => store,
2553 None => return Ok(()),
2554 };
2555 let (username, role) = match crate::runtime::impl_core::current_auth_identity() {
2556 Some(identity) => identity,
2557 None => return Ok(()),
2558 };
2559 let tenant = crate::runtime::impl_core::current_tenant();
2560 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
2561
2562 if auth_store.iam_authorization_enabled() {
2563 let ctx = crate::auth::policies::EvalContext {
2564 principal_tenant: tenant.clone(),
2565 current_tenant: tenant.clone(),
2566 peer_ip: None,
2567 mfa_present: false,
2568 now_ms: crate::auth::now_ms(),
2569 principal_is_admin_role: role == crate::auth::Role::Admin,
2570 principal_is_platform_scoped: principal.tenant.is_none(),
2571 };
2572 let mut source_resource = crate::auth::policies::ResourceRef::new("table", source);
2573 if let Some(t) = tenant.as_deref() {
2574 source_resource = source_resource.with_tenant(t.to_string());
2575 }
2576 if !auth_store.check_policy_authz_with_role(
2577 &principal,
2578 "select",
2579 &source_resource,
2580 &ctx,
2581 role,
2582 ) {
2583 return Err(RedDBError::Query(format!(
2584 "permission denied: principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
2585 principal, source_resource.kind, source_resource.name
2586 )));
2587 }
2588
2589 let mut target_resource =
2590 crate::auth::policies::ResourceRef::new("queue", subscription.target_queue.clone());
2591 if let Some(t) = tenant.as_deref() {
2592 target_resource = target_resource.with_tenant(t.to_string());
2593 }
2594 if !auth_store.check_policy_authz_with_role(
2595 &principal,
2596 "write",
2597 &target_resource,
2598 &ctx,
2599 role,
2600 ) {
2601 return Err(RedDBError::Query(format!(
2602 "permission denied: principal=`{}` action=`write` resource=`{}:{}` denied by IAM policy",
2603 principal, target_resource.kind, target_resource.name
2604 )));
2605 }
2606 return Ok(());
2607 }
2608
2609 let ctx = crate::auth::privileges::AuthzContext {
2610 principal: &username,
2611 effective_role: role,
2612 tenant: tenant.as_deref(),
2613 };
2614 auth_store
2615 .check_grant(
2616 &ctx,
2617 crate::auth::privileges::Action::Select,
2618 &crate::auth::privileges::Resource::table_from_name(source),
2619 )
2620 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))?;
2621 auth_store
2622 .check_grant(
2623 &ctx,
2624 crate::auth::privileges::Action::Insert,
2625 &crate::auth::privileges::Resource::table_from_name(&subscription.target_queue),
2626 )
2627 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))?;
2628 Ok(())
2629}
2630
2631fn audit_subscription_redact_gap(
2632 runtime: &RedDBRuntime,
2633 source: &str,
2634 subscription: &crate::catalog::SubscriptionDescriptor,
2635) {
2636 let auth_store = match runtime.inner.auth_store.read().clone() {
2637 Some(store) if store.iam_authorization_enabled() => store,
2638 _ => return,
2639 };
2640 let (username, role) = match crate::runtime::impl_core::current_auth_identity() {
2641 Some(identity) => identity,
2642 None => return,
2643 };
2644 let tenant = crate::runtime::impl_core::current_tenant();
2645 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
2646 let missing = subscription_redact_gap_columns(&auth_store, &principal, source, subscription);
2647 if missing.is_empty() {
2648 return;
2649 }
2650
2651 let columns = missing.into_iter().collect::<Vec<_>>().join(", ");
2652 tracing::warn!(
2653 target: "reddb::operator",
2654 "subscription_redact_gap: source={} target_queue={} columns=[{}]",
2655 source,
2656 subscription.target_queue,
2657 columns
2658 );
2659 let mut event = AuditEvent::builder("subscription_redact_gap")
2660 .principal(username)
2661 .source(AuditAuthSource::System)
2662 .resource(format!(
2663 "subscription:{}->{}",
2664 source, subscription.target_queue
2665 ))
2666 .outcome(Outcome::Success)
2667 .field(AuditFieldEscaper::field("source", source))
2668 .field(AuditFieldEscaper::field(
2669 "target_queue",
2670 subscription.target_queue.clone(),
2671 ))
2672 .field(AuditFieldEscaper::field(
2673 "subscription",
2674 subscription.name.clone(),
2675 ))
2676 .field(AuditFieldEscaper::field("columns", columns))
2677 .field(AuditFieldEscaper::field("role", role.as_str()));
2678 if let Some(t) = tenant {
2679 event = event.tenant(t);
2680 }
2681 runtime.inner.audit_log.record_event(event.build());
2682}
2683
2684fn subscription_redact_gap_columns(
2685 auth_store: &crate::auth::store::AuthStore,
2686 principal: &crate::auth::UserId,
2687 source: &str,
2688 subscription: &crate::catalog::SubscriptionDescriptor,
2689) -> BTreeSet<String> {
2690 let redacted: HashSet<String> = subscription
2691 .redact_fields
2692 .iter()
2693 .map(|field| field.to_ascii_lowercase())
2694 .collect();
2695 auth_store
2696 .effective_policies(principal)
2697 .iter()
2698 .flat_map(|policy| policy.statements.iter())
2699 .filter(|statement| statement.effect == crate::auth::policies::Effect::Deny)
2700 .filter(|statement| statement.actions.iter().any(action_pattern_matches_select))
2701 .flat_map(|statement| statement.resources.iter())
2702 .filter_map(|resource| denied_column_for_source(resource, source))
2703 .filter(|column| !redact_covers_column(&redacted, source, column))
2704 .collect()
2705}
2706
2707fn action_pattern_matches_select(pattern: &crate::auth::policies::ActionPattern) -> bool {
2708 match pattern {
2709 crate::auth::policies::ActionPattern::Wildcard => true,
2710 crate::auth::policies::ActionPattern::Exact(action) => action == "select",
2711 crate::auth::policies::ActionPattern::Prefix(prefix) => {
2712 "select".len() > prefix.len() + 1
2713 && "select".starts_with(prefix)
2714 && "select".as_bytes()[prefix.len()] == b':'
2715 }
2716 }
2717}
2718
2719fn denied_column_for_source(
2720 resource: &crate::auth::policies::ResourcePattern,
2721 source: &str,
2722) -> Option<String> {
2723 let crate::auth::policies::ResourcePattern::Exact { kind, name } = resource else {
2724 return None;
2725 };
2726 if kind != "column" {
2727 return None;
2728 }
2729 let column = crate::auth::ColumnRef::parse_resource_name(name).ok()?;
2730 (column.table_resource_name() == source).then_some(column.column)
2731}
2732
2733fn redact_covers_column(redacted: &HashSet<String>, source: &str, column: &str) -> bool {
2734 let column = column.to_ascii_lowercase();
2735 let qualified = format!("{}.{}", source.to_ascii_lowercase(), column);
2736 redacted.contains("*") || redacted.contains(&column) || redacted.contains(&qualified)
2737}
2738
2739fn subscription_would_create_cycle(
2740 db: &crate::storage::unified::devx::RedDB,
2741 source: &str,
2742 target: &str,
2743) -> bool {
2744 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
2745 for contract in db.collection_contracts() {
2746 for subscription in contract
2747 .subscriptions
2748 .into_iter()
2749 .filter(|subscription| subscription.enabled)
2750 {
2751 graph
2752 .entry(subscription.source)
2753 .or_default()
2754 .push(subscription.target_queue);
2755 }
2756 }
2757 graph
2758 .entry(source.to_string())
2759 .or_default()
2760 .push(target.to_string());
2761
2762 let mut stack = vec![target.to_string()];
2763 let mut seen = HashSet::new();
2764 while let Some(node) = stack.pop() {
2765 if node == source {
2766 return true;
2767 }
2768 if !seen.insert(node.clone()) {
2769 continue;
2770 }
2771 if let Some(next) = graph.get(&node) {
2772 stack.extend(next.iter().cloned());
2773 }
2774 }
2775 false
2776}
2777
2778pub(crate) fn ensure_event_target_queue_pub(
2779 runtime: &RedDBRuntime,
2780 queue: &str,
2781) -> RedDBResult<()> {
2782 ensure_event_target_queue(runtime, queue)
2783}
2784
2785fn ensure_event_target_queue(runtime: &RedDBRuntime, queue: &str) -> RedDBResult<()> {
2786 let store = runtime.inner.db.store();
2787 if store.get_collection(queue).is_some() {
2788 return Ok(());
2789 }
2790 store
2791 .create_collection(queue)
2792 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2793 runtime
2794 .inner
2795 .db
2796 .save_collection_contract(event_queue_collection_contract(queue))
2797 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2798 store.set_config_tree(
2799 &format!("queue.{queue}.mode"),
2800 &crate::serde_json::Value::String("fanout".to_string()),
2801 );
2802 Ok(())
2803}
2804
2805fn event_queue_collection_contract(queue: &str) -> crate::physical::CollectionContract {
2806 let now = current_unix_ms();
2807 crate::physical::CollectionContract {
2808 name: queue.to_string(),
2809 declared_model: crate::catalog::CollectionModel::Queue,
2810 schema_mode: crate::catalog::SchemaMode::Dynamic,
2811 origin: crate::physical::ContractOrigin::Implicit,
2812 version: 1,
2813 created_at_unix_ms: now,
2814 updated_at_unix_ms: now,
2815 default_ttl_ms: None,
2816 vector_dimension: None,
2817 vector_metric: None,
2818 context_index_fields: Vec::new(),
2819 declared_columns: Vec::new(),
2820 table_def: None,
2821 timestamps_enabled: false,
2822 context_index_enabled: false,
2823 metrics_raw_retention_ms: None,
2824 metrics_rollup_policies: Vec::new(),
2825 metrics_tenant_identity: None,
2826 metrics_namespace: None,
2827 append_only: true,
2828 subscriptions: Vec::new(),
2829 analytics_config: Vec::new(),
2830 session_key: None,
2831 session_gap_ms: None,
2832 retention_duration_ms: None,
2833 analytical_storage: None,
2834
2835 ai_policy: None,
2836 }
2837}
2838
2839fn column_def_from_ddl(column: &CreateColumnDef) -> RedDBResult<reddb_types::ColumnDef> {
2840 reddb_engine::catalog::ddl::column_def_from_ddl(column).map_err(RedDBError::Query)
2841}
2842
2843fn current_unix_ms() -> u128 {
2844 std::time::SystemTime::now()
2845 .duration_since(std::time::UNIX_EPOCH)
2846 .unwrap_or_default()
2847 .as_millis()
2848}
2849
2850#[cfg(test)]
2851mod tests {
2852 use crate::auth::policies::{ActionPattern, Effect, Policy, ResourcePattern, Statement};
2853 use crate::auth::store::{AuthStore, PrincipalRef};
2854 use crate::auth::UserId;
2855 use crate::auth::{AuthConfig, Role};
2856 use crate::runtime::impl_core::{clear_current_auth_identity, set_current_auth_identity};
2857 use crate::{RedDBOptions, RedDBRuntime};
2858 use reddb_types::Value;
2859 use std::sync::Arc;
2860
2861 fn make_allow_policy(id: &str, action: &str, collection: &str) -> Policy {
2862 Policy {
2863 id: id.to_string(),
2864 version: 1,
2865 tenant: None,
2866 created_at: 0,
2867 updated_at: 0,
2868 statements: vec![Statement {
2869 sid: None,
2870 effect: Effect::Allow,
2871 actions: vec![ActionPattern::Exact(action.to_string())],
2872 resources: vec![ResourcePattern::Exact {
2873 kind: "collection".to_string(),
2874 name: collection.to_string(),
2875 }],
2876 condition: None,
2877 }],
2878 }
2879 }
2880
2881 fn wire_auth_store(rt: &RedDBRuntime) -> Arc<AuthStore> {
2882 let store = Arc::new(AuthStore::new(AuthConfig::default()));
2883 *rt.inner.auth_store.write() = Some(store.clone());
2884 store
2885 }
2886
2887 #[test]
2888 fn drop_denied_without_iam_policy() {
2889 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2890 rt.execute_query("CREATE TABLE foo (id INT)").unwrap();
2891 let store = wire_auth_store(&rt);
2892 let select_only = Policy {
2894 id: "select-only".to_string(),
2895 version: 1,
2896 tenant: None,
2897 created_at: 0,
2898 updated_at: 0,
2899 statements: vec![Statement {
2900 sid: None,
2901 effect: Effect::Allow,
2902 actions: vec![ActionPattern::Exact("select".to_string())],
2903 resources: vec![ResourcePattern::Wildcard],
2904 condition: None,
2905 }],
2906 };
2907 store.put_policy_internal(select_only).unwrap();
2908 let alice = UserId::from_parts(None, "alice");
2909 store
2910 .attach_policy(PrincipalRef::User(alice), "select-only")
2911 .unwrap();
2912 set_current_auth_identity("alice".to_string(), Role::Write);
2913 let err = rt.execute_query("DROP TABLE foo").unwrap_err();
2914 clear_current_auth_identity();
2915 assert!(
2916 format!("{err}").contains("denied by IAM policy"),
2917 "got: {err}"
2918 );
2919 }
2920
2921 #[test]
2922 fn drop_allowed_with_explicit_iam_policy() {
2923 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2924 rt.execute_query("CREATE TABLE bar (id INT)").unwrap();
2925 let store = wire_auth_store(&rt);
2926 let policy = make_allow_policy("allow-drop-bar", "drop", "bar");
2927 store.put_policy_internal(policy).unwrap();
2928 let bob = UserId::from_parts(None, "bob");
2929 store
2930 .attach_policy(PrincipalRef::User(bob), "allow-drop-bar")
2931 .unwrap();
2932 set_current_auth_identity("bob".to_string(), Role::Write);
2933 rt.execute_query("DROP TABLE bar").unwrap();
2934 clear_current_auth_identity();
2935 }
2936
2937 #[test]
2938 fn drop_allowed_with_wildcard_iam_policy() {
2939 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2940 rt.execute_query("CREATE TABLE baz (id INT)").unwrap();
2941 let store = wire_auth_store(&rt);
2942 let policy = Policy {
2943 id: "allow-drop-all".to_string(),
2944 version: 1,
2945 tenant: None,
2946 created_at: 0,
2947 updated_at: 0,
2948 statements: vec![Statement {
2949 sid: None,
2950 effect: Effect::Allow,
2951 actions: vec![ActionPattern::Exact("drop".to_string())],
2952 resources: vec![ResourcePattern::Wildcard],
2953 condition: None,
2954 }],
2955 };
2956 store.put_policy_internal(policy).unwrap();
2957 let carl = UserId::from_parts(None, "carl");
2958 store
2959 .attach_policy(PrincipalRef::User(carl), "allow-drop-all")
2960 .unwrap();
2961 set_current_auth_identity("carl".to_string(), Role::Write);
2962 rt.execute_query("DROP TABLE baz").unwrap();
2963 clear_current_auth_identity();
2964 }
2965
2966 #[test]
2967 fn truncate_denied_without_iam_policy() {
2968 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2969 rt.execute_query("CREATE TABLE qux (id INT)").unwrap();
2970 let store = wire_auth_store(&rt);
2971 store
2978 .set_enforcement_mode(crate::auth::enforcement_mode::PolicyEnforcementMode::PolicyOnly);
2979 let select_only = Policy {
2981 id: "select-only-2".to_string(),
2982 version: 1,
2983 tenant: None,
2984 created_at: 0,
2985 updated_at: 0,
2986 statements: vec![Statement {
2987 sid: None,
2988 effect: Effect::Allow,
2989 actions: vec![ActionPattern::Exact("select".to_string())],
2990 resources: vec![ResourcePattern::Wildcard],
2991 condition: None,
2992 }],
2993 };
2994 store.put_policy_internal(select_only).unwrap();
2995 let dana = UserId::from_parts(None, "dana");
2996 store
2997 .attach_policy(PrincipalRef::User(dana), "select-only-2")
2998 .unwrap();
2999 set_current_auth_identity("dana".to_string(), Role::Write);
3000 let err = rt.execute_query("TRUNCATE TABLE qux").unwrap_err();
3001 clear_current_auth_identity();
3002 assert!(
3003 format!("{err}").contains("denied by IAM policy"),
3004 "got: {err}"
3005 );
3006 }
3007
3008 #[test]
3009 fn truncate_table_clears_rows_and_preserves_schema_and_indexes() {
3010 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3011 rt.execute_query("CREATE TABLE users (id INT, name TEXT)")
3012 .unwrap();
3013 rt.execute_query("INSERT INTO users (id, name) VALUES (1, 'ana'), (2, 'bob')")
3014 .unwrap();
3015 rt.execute_query("CREATE INDEX idx_users_id ON users (id) USING HASH")
3016 .unwrap();
3017
3018 let truncated = rt.execute_query("TRUNCATE TABLE users").unwrap();
3019 assert_eq!(truncated.statement_type, "truncate");
3020 assert_eq!(truncated.affected_rows, 0);
3021
3022 let empty = rt.execute_query("SELECT id FROM users").unwrap();
3023 assert!(empty.result.records.is_empty());
3024
3025 rt.execute_query("INSERT INTO users (id, name) VALUES (3, 'cy')")
3026 .unwrap();
3027 let selected = rt
3028 .execute_query("SELECT name FROM users WHERE id = 3")
3029 .unwrap();
3030 let name = selected.result.records[0].get("name").unwrap();
3031 assert_eq!(name, &Value::text("cy"));
3032 assert!(rt.db().collection_contract("users").is_some());
3033 assert!(rt
3034 .inner
3035 .index_store
3036 .list_indices("users")
3037 .iter()
3038 .any(|index| index.name == "idx_users_id"));
3039 }
3040
3041 #[test]
3042 fn truncate_collection_is_polymorphic_and_typed_mismatch_fails() {
3043 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3044 rt.execute_query("CREATE QUEUE tasks").unwrap();
3045 rt.execute_query("QUEUE PUSH tasks {'job':'a'}").unwrap();
3046
3047 let err = rt.execute_query("TRUNCATE TABLE tasks").unwrap_err();
3048 assert!(format!("{err}").contains("model mismatch: expected table, got queue"));
3049
3050 rt.execute_query("TRUNCATE COLLECTION tasks").unwrap();
3051 let len = rt.execute_query("QUEUE LEN tasks").unwrap();
3052 assert_eq!(
3053 len.result.records[0].get("len"),
3054 Some(&Value::UnsignedInteger(0))
3055 );
3056 }
3057
3058 #[test]
3059 fn truncate_system_schema_is_read_only() {
3060 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3061 let err = rt
3062 .execute_query("TRUNCATE COLLECTION red.collections")
3063 .unwrap_err();
3064 assert!(format!("{err}").contains("system schema is read-only"));
3065 }
3066
3067 fn queue_payloads(rt: &RedDBRuntime, queue: &str) -> Vec<crate::json::Value> {
3070 let result = rt
3071 .execute_query(&format!("QUEUE PEEK {queue} 100"))
3072 .expect("peek queue");
3073 result
3074 .result
3075 .records
3076 .iter()
3077 .map(
3078 |record| match record.get("payload").expect("payload column") {
3079 Value::Json(bytes) => crate::json::from_slice(bytes).expect("json payload"),
3080 other => panic!("expected JSON queue payload, got {other:?}"),
3081 },
3082 )
3083 .collect()
3084 }
3085
3086 #[test]
3089 fn truncate_event_enabled_table_emits_single_truncate_event() {
3090 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3091 rt.execute_query("CREATE TABLE users (id INT, name TEXT) WITH EVENTS TO users_events")
3092 .unwrap();
3093 rt.execute_query(
3094 "INSERT INTO users (id, name) VALUES (1, 'alice'), (2, 'bob'), (3, 'carol')",
3095 )
3096 .unwrap();
3097
3098 rt.execute_query("QUEUE POP users_events COUNT 10").unwrap();
3100
3101 rt.execute_query("TRUNCATE TABLE users").unwrap();
3102
3103 let events = queue_payloads(&rt, "users_events");
3104 assert_eq!(
3106 events.len(),
3107 1,
3108 "expected 1 truncate event, got {}",
3109 events.len()
3110 );
3111 let ev = events[0].as_object().expect("event is object");
3112 assert_eq!(
3113 ev.get("op").and_then(crate::json::Value::as_str),
3114 Some("truncate")
3115 );
3116 assert_eq!(
3117 ev.get("collection").and_then(crate::json::Value::as_str),
3118 Some("users")
3119 );
3120 assert_eq!(
3121 ev.get("entities_count")
3122 .and_then(crate::json::Value::as_u64),
3123 Some(3)
3124 );
3125 assert!(ev.get("ts").and_then(crate::json::Value::as_u64).is_some());
3126 assert!(ev.get("lsn").and_then(crate::json::Value::as_u64).is_some());
3127 assert!(ev
3128 .get("event_id")
3129 .and_then(crate::json::Value::as_str)
3130 .is_some_and(|s| !s.is_empty()));
3131 }
3132
3133 #[test]
3135 fn truncate_no_events_collection_emits_nothing() {
3136 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3137 rt.execute_query("CREATE TABLE plain (id INT, val TEXT)")
3138 .unwrap();
3139 rt.execute_query("INSERT INTO plain (id, val) VALUES (1, 'a'), (2, 'b')")
3140 .unwrap();
3141 rt.execute_query("TRUNCATE TABLE plain").unwrap();
3143 let rows = rt.execute_query("SELECT id FROM plain").unwrap();
3145 assert!(rows.result.records.is_empty());
3146 }
3147
3148 #[test]
3152 fn drop_event_enabled_table_emits_single_collection_dropped_event() {
3153 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3154 rt.execute_query("CREATE TABLE users (id INT, name TEXT) WITH EVENTS TO users_events")
3155 .unwrap();
3156 rt.execute_query("INSERT INTO users (id, name) VALUES (1, 'alice'), (2, 'bob')")
3157 .unwrap();
3158
3159 rt.execute_query("QUEUE POP users_events COUNT 10").unwrap();
3161
3162 rt.execute_query("DROP TABLE users").unwrap();
3163
3164 let events = queue_payloads(&rt, "users_events");
3166 assert_eq!(
3167 events.len(),
3168 1,
3169 "expected 1 collection_dropped event, got {}",
3170 events.len()
3171 );
3172 let ev = events[0].as_object().expect("event is object");
3173 assert_eq!(
3174 ev.get("op").and_then(crate::json::Value::as_str),
3175 Some("collection_dropped")
3176 );
3177 assert_eq!(
3178 ev.get("collection").and_then(crate::json::Value::as_str),
3179 Some("users")
3180 );
3181 assert_eq!(
3182 ev.get("final_entities_count")
3183 .and_then(crate::json::Value::as_u64),
3184 Some(2)
3185 );
3186 assert!(ev.get("ts").and_then(crate::json::Value::as_u64).is_some());
3187 assert!(ev.get("lsn").and_then(crate::json::Value::as_u64).is_some());
3188 assert!(ev
3189 .get("event_id")
3190 .and_then(crate::json::Value::as_str)
3191 .is_some_and(|s| !s.is_empty()));
3192
3193 let err = rt.execute_query("SELECT id FROM users").unwrap_err();
3195 assert!(
3196 format!("{err}").contains("users"),
3197 "expected not-found error"
3198 );
3199 }
3200
3201 #[test]
3204 fn drop_no_events_collection_emits_nothing() {
3205 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3206 rt.execute_query("CREATE TABLE plain (id INT, val TEXT)")
3207 .unwrap();
3208 rt.execute_query("INSERT INTO plain (id, val) VALUES (1, 'a')")
3209 .unwrap();
3210 rt.execute_query("DROP TABLE plain").unwrap();
3211 let err = rt.execute_query("SELECT id FROM plain").unwrap_err();
3213 assert!(format!("{err}").contains("plain"));
3214 }
3215
3216 #[test]
3220 fn ops_filter_insert_only_ignores_update_and_delete() {
3221 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3222 rt.execute_query(
3223 "CREATE TABLE items (id INT, val TEXT) WITH EVENTS (INSERT) TO items_events",
3224 )
3225 .unwrap();
3226 rt.execute_query("INSERT INTO items (id, val) VALUES (1, 'a')")
3227 .unwrap();
3228 rt.execute_query("UPDATE items SET val = 'b' WHERE id = 1")
3229 .unwrap();
3230 rt.execute_query("DELETE FROM items WHERE id = 1").unwrap();
3231
3232 let events = queue_payloads(&rt, "items_events");
3233 assert_eq!(
3235 events.len(),
3236 1,
3237 "expected 1 insert event, got {}",
3238 events.len()
3239 );
3240 assert_eq!(
3241 events[0]
3242 .as_object()
3243 .unwrap()
3244 .get("op")
3245 .and_then(crate::json::Value::as_str),
3246 Some("insert")
3247 );
3248 }
3249
3250 #[test]
3252 fn where_filter_skips_rows_that_do_not_match() {
3253 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3254 rt.execute_query(
3255 "CREATE TABLE users (id INT, status TEXT) WITH EVENTS WHERE status = 'active' TO users_events",
3256 )
3257 .unwrap();
3258
3259 rt.execute_query("INSERT INTO users (id, status) VALUES (1, 'active')")
3261 .unwrap();
3262 rt.execute_query("INSERT INTO users (id, status) VALUES (2, 'inactive')")
3264 .unwrap();
3265
3266 let events = queue_payloads(&rt, "users_events");
3267 assert_eq!(
3268 events.len(),
3269 1,
3270 "expected 1 event (only active), got {}",
3271 events.len()
3272 );
3273 let ev = events[0].as_object().unwrap();
3274 assert_eq!(
3275 ev.get("op").and_then(crate::json::Value::as_str),
3276 Some("insert")
3277 );
3278 let after = ev.get("after").unwrap().as_object().unwrap();
3279 assert_eq!(
3280 after.get("status").and_then(crate::json::Value::as_str),
3281 Some("active")
3282 );
3283 }
3284
3285 #[test]
3287 fn ops_filter_and_where_filter_combined() {
3288 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3289 rt.execute_query(
3290 "CREATE TABLE items (id INT, status TEXT) WITH EVENTS (INSERT, UPDATE) WHERE status = 'active' TO items_events",
3291 )
3292 .unwrap();
3293
3294 rt.execute_query("INSERT INTO items (id, status) VALUES (1, 'active')")
3296 .unwrap();
3297 rt.execute_query("INSERT INTO items (id, status) VALUES (2, 'inactive')")
3299 .unwrap();
3300 rt.execute_query("UPDATE items SET status = 'inactive' WHERE id = 1")
3302 .unwrap();
3303 rt.execute_query("DELETE FROM items WHERE id = 2").unwrap();
3305
3306 let events = queue_payloads(&rt, "items_events");
3307 assert_eq!(
3309 events.len(),
3310 1,
3311 "expected 1 event, got {}: {events:?}",
3312 events.len()
3313 );
3314 assert_eq!(
3315 events[0]
3316 .as_object()
3317 .unwrap()
3318 .get("op")
3319 .and_then(crate::json::Value::as_str),
3320 Some("insert")
3321 );
3322 }
3323
3324 #[test]
3326 fn where_filter_on_delete_checks_before_state() {
3327 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3328 rt.execute_query(
3329 "CREATE TABLE users (id INT, status TEXT) WITH EVENTS (DELETE) WHERE status = 'active' TO users_events",
3330 )
3331 .unwrap();
3332
3333 rt.execute_query("INSERT INTO users (id, status) VALUES (1, 'active'), (2, 'inactive')")
3334 .unwrap();
3335
3336 rt.execute_query("DELETE FROM users WHERE id = 1").unwrap();
3338 rt.execute_query("DELETE FROM users WHERE id = 2").unwrap();
3340
3341 let events = queue_payloads(&rt, "users_events");
3342 assert_eq!(
3343 events.len(),
3344 1,
3345 "expected 1 delete event, got {}",
3346 events.len()
3347 );
3348 let ev = events[0].as_object().unwrap();
3349 assert_eq!(
3350 ev.get("op").and_then(crate::json::Value::as_str),
3351 Some("delete")
3352 );
3353 }
3354
3355 #[test]
3359 fn alter_add_column_on_event_enabled_table_succeeds() {
3360 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3361 rt.execute_query("CREATE TABLE users (id INT, name TEXT) WITH EVENTS TO users_events")
3362 .unwrap();
3363 rt.execute_query("ALTER TABLE users ADD COLUMN phone TEXT")
3365 .unwrap();
3366 let contract = rt.db().collection_contract("users").unwrap();
3368 assert!(
3369 contract.declared_columns.iter().any(|c| c.name == "phone"),
3370 "phone column should be in contract"
3371 );
3372 assert!(
3374 contract.subscriptions.iter().any(|s| s.enabled),
3375 "subscription should remain enabled"
3376 );
3377 }
3378
3379 #[test]
3382 fn alter_drop_column_and_rls_on_event_enabled_table_succeeds() {
3383 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3384 rt.execute_query(
3385 "CREATE TABLE items (id INT, secret TEXT, status TEXT) WITH EVENTS TO items_events",
3386 )
3387 .unwrap();
3388 rt.execute_query("ALTER TABLE items DROP COLUMN secret")
3390 .unwrap();
3391 let contract = rt.db().collection_contract("items").unwrap();
3392 assert!(
3393 !contract.declared_columns.iter().any(|c| c.name == "secret"),
3394 "secret column should be removed"
3395 );
3396 rt.execute_query("ALTER TABLE items ENABLE ROW LEVEL SECURITY")
3398 .unwrap();
3399 assert!(
3401 contract.subscriptions.iter().any(|s| s.enabled),
3402 "subscription should remain enabled"
3403 );
3404 }
3405
3406 #[test]
3412 fn create_vector_marks_collection_as_turbo_baseline() {
3413 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3414 rt.execute_query("CREATE VECTOR embeddings DIM 4").unwrap();
3415 let store = rt.db().store();
3416 assert!(
3417 crate::runtime::vector_turbo_kind::is_turbo(&store, "embeddings"),
3418 "new vector collections must be turbo-marked baseline"
3419 );
3420 assert!(
3421 rt.db().turbo_state("embeddings").is_some(),
3422 "turbo_state must materialise after CREATE VECTOR"
3423 );
3424 }
3425
3426 #[test]
3430 fn create_table_does_not_mark_turbo() {
3431 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3432 rt.execute_query("CREATE TABLE plain (id INT)").unwrap();
3433 let store = rt.db().store();
3434 assert!(
3435 !crate::runtime::vector_turbo_kind::is_turbo(&store, "plain"),
3436 "non-vector collections must not gain the turbo marker"
3437 );
3438 assert!(rt.db().turbo_state("plain").is_none());
3439 }
3440
3441 #[test]
3444 fn create_collection_kind_vector_turbo_still_marked() {
3445 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3446 rt.execute_query("CREATE COLLECTION turbo_v KIND vector.turbo DIM 4")
3447 .unwrap();
3448 let store = rt.db().store();
3449 assert!(crate::runtime::vector_turbo_kind::is_turbo(
3450 &store, "turbo_v"
3451 ));
3452 assert!(rt.db().turbo_state("turbo_v").is_some());
3453 }
3454
3455 #[test]
3458 fn create_table_persists_and_introspects_ai_policy() {
3459 use crate::catalog::{ModerateDegradedMode, ModerateRejectAction};
3460 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3461 rt.execute_query(
3462 "CREATE TABLE posts (id INT, title TEXT, body TEXT, photo TEXT) WITH ( \
3463 EMBED (fields = ('title', 'body'), provider = 'openai', model = 'text-embedding-3-small'), \
3464 MODERATE (fields = ('body'), provider = 'openai', model = 'omni-moderation-latest', sync = true, degraded = closed, on_reject = flag), \
3465 VISION (image_field = 'photo', outputs = ('caption'), provider = 'openai', model = 'gpt-4o') \
3466 )",
3467 )
3468 .expect("create table with ai policy");
3469
3470 let contracts = rt.db().collection_contracts();
3471 let contract = contracts
3472 .iter()
3473 .find(|c| c.name == "posts")
3474 .expect("contract persisted");
3475 let policy = contract.ai_policy.as_ref().expect("ai policy persisted");
3476
3477 let embed = policy.embed.as_ref().expect("embed");
3478 assert_eq!(embed.fields, vec!["title".to_string(), "body".to_string()]);
3479 assert_eq!(embed.model, "text-embedding-3-small");
3480
3481 let moderate = policy.moderate.as_ref().expect("moderate");
3482 assert!(moderate.sync_gate);
3483 assert_eq!(moderate.degraded_mode, ModerateDegradedMode::Closed);
3484 assert_eq!(moderate.reject_action, ModerateRejectAction::Flag);
3485
3486 let vision = policy.vision.as_ref().expect("vision");
3487 assert_eq!(vision.image_field, "photo");
3488 assert_eq!(vision.model, "gpt-4o");
3489 }
3490
3491 #[test]
3495 fn create_table_rejects_ai_policy_incapable_provider() {
3496 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3497 let err = rt
3498 .execute_query(
3499 "CREATE TABLE bad (id INT, body TEXT) WITH ( \
3500 EMBED (fields = ('body'), provider = 'anthropic', model = 'claude-3-5-sonnet') \
3501 )",
3502 )
3503 .unwrap_err();
3504 assert!(
3505 err.to_string()
3506 .contains("cannot serve the 'embed' modality"),
3507 "{err}"
3508 );
3509 assert!(
3510 rt.db().store().get_collection("bad").is_none(),
3511 "rejected DDL must not create the collection"
3512 );
3513 }
3514}