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