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