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 store = self.inner.db.store();
1976 let Some(manager) = store.get_collection(name) else {
1977 return Ok(0);
1978 };
1979 let entities = manager.query_all(|_| true);
1980 if entities.is_empty() {
1981 return Ok(0);
1982 }
1983
1984 for entity in &entities {
1985 let fields = entity_index_fields(&entity.data);
1986 self.inner
1987 .index_store
1988 .index_entity_delete(name, entity.id, &fields)
1989 .map_err(RedDBError::Internal)?;
1990 }
1991
1992 let ids = entities.iter().map(|entity| entity.id).collect::<Vec<_>>();
1993 let deleted_ids = store
1994 .delete_batch(name, &ids)
1995 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1996 for id in &deleted_ids {
1997 store.context_index().remove_entity(*id);
1998 }
1999 Ok(deleted_ids.len() as u64)
2000 }
2001
2002 fn drop_collection_storage(
2003 &self,
2004 raw_query: &str,
2005 name: &str,
2006 label: &str,
2007 ) -> RedDBResult<RuntimeQueryResult> {
2008 let store = self.inner.db.store();
2009
2010 let final_count = store
2013 .get_collection(name)
2014 .map(|manager| manager.query_all(|_| true).len() as u64)
2015 .unwrap_or(0);
2016 crate::runtime::mutation::emit_collection_dropped_event_for_collection(
2017 self,
2018 name,
2019 final_count,
2020 )?;
2021
2022 let topology_lock = self.inner.index_store.collection_topology_lock(name);
2023 let _topology_guard = topology_lock.write();
2024 let orphaned_indices: Vec<String> = self
2025 .inner
2026 .index_store
2027 .list_indices(name)
2028 .into_iter()
2029 .map(|index| index.name)
2030 .collect();
2031 for index_name in &orphaned_indices {
2032 self.inner.index_store.drop_index(index_name, name);
2033 }
2034
2035 store
2036 .drop_collection(name)
2037 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2038 self.inner.db.invalidate_vector_index(name);
2039 self.inner.db.clear_collection_default_ttl_ms(name);
2040 self.inner
2041 .db
2042 .remove_collection_contract(name)
2043 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2044 self.clear_table_planner_stats(name);
2045 self.invalidate_result_cache();
2046 if let Some(store) = self.inner.auth_store.read().clone() {
2047 store.invalidate_visible_collections_cache();
2048 }
2049 self.schema_vocabulary_apply(
2053 crate::runtime::schema_vocabulary::DdlEvent::DropCollection {
2054 collection: name.to_string(),
2055 },
2056 );
2057
2058 Ok(RuntimeQueryResult::ok_message(
2059 raw_query.to_string(),
2060 &format!("{label} '{name}' dropped"),
2061 "drop",
2062 ))
2063 }
2064}
2065
2066pub(crate) fn is_system_schema_name(name: &str) -> bool {
2067 name == "red" || name.starts_with("red.") || name.starts_with("__red_schema_")
2068}
2069
2070fn entity_index_fields(data: &EntityData) -> Vec<(String, Value)> {
2071 match data {
2072 EntityData::Row(row) => {
2073 if let Some(ref named) = row.named {
2074 named
2075 .iter()
2076 .map(|(key, value)| (key.clone(), value.clone()))
2077 .collect()
2078 } else if let Some(ref schema) = row.schema {
2079 schema
2080 .iter()
2081 .zip(row.columns.iter())
2082 .map(|(key, value)| (key.clone(), value.clone()))
2083 .collect()
2084 } else {
2085 Vec::new()
2086 }
2087 }
2088 EntityData::Node(node) => node
2089 .properties
2090 .iter()
2091 .map(|(key, value)| (key.clone(), value.clone()))
2092 .collect(),
2093 _ => Vec::new(),
2094 }
2095}
2096
2097fn validate_ai_policy_modalities(policy: &crate::catalog::AiPolicy) -> RedDBResult<()> {
2103 use crate::runtime::ai::provider_capabilities::{Modality, Registry};
2104 let registry = Registry::new();
2105 let check = |provider: &str, model: &str, modality: Modality| -> RedDBResult<()> {
2106 registry
2107 .validate_policy_modality(provider, model, modality)
2108 .map_err(|err| RedDBError::Query(err.to_string()))
2109 };
2110 if let Some(embed) = &policy.embed {
2111 check(&embed.provider, &embed.model, Modality::Embed)?;
2112 }
2113 if let Some(moderate) = &policy.moderate {
2114 check(&moderate.provider, &moderate.model, Modality::Moderate)?;
2115 }
2116 if let Some(vision) = &policy.vision {
2117 check(&vision.provider, &vision.model, Modality::Vision)?;
2118 }
2119 Ok(())
2120}
2121
2122fn collection_contract_from_create_table(
2123 query: &CreateTableQuery,
2124) -> RedDBResult<crate::physical::CollectionContract> {
2125 reddb_engine::catalog::ddl::collection_contract_from_create_table(query, current_unix_ms())
2126 .map_err(RedDBError::Query)
2127}
2128
2129fn default_collection_contract_for_existing_table(
2130 name: &str,
2131) -> crate::physical::CollectionContract {
2132 let now = current_unix_ms();
2133 crate::physical::CollectionContract {
2134 name: name.to_string(),
2135 declared_model: crate::catalog::CollectionModel::Table,
2136 schema_mode: crate::catalog::SchemaMode::SemiStructured,
2137 origin: crate::physical::ContractOrigin::Explicit,
2138 version: 0,
2139 created_at_unix_ms: now,
2140 updated_at_unix_ms: now,
2141 default_ttl_ms: None,
2142 vector_dimension: None,
2143 vector_metric: None,
2144 context_index_fields: Vec::new(),
2145 declared_columns: Vec::new(),
2146 table_def: Some(reddb_types::TableDef::new(name.to_string())),
2147 timestamps_enabled: false,
2148 context_index_enabled: false,
2149 metrics_raw_retention_ms: None,
2150 metrics_rollup_policies: Vec::new(),
2151 metrics_tenant_identity: None,
2152 metrics_namespace: None,
2153 append_only: false,
2154 subscriptions: Vec::new(),
2155 analytics_config: Vec::new(),
2156 session_key: None,
2157 session_gap_ms: None,
2158 retention_duration_ms: None,
2159 analytical_storage: None,
2160
2161 ai_policy: None,
2162 }
2163}
2164
2165fn keyed_collection_contract(
2166 name: &str,
2167 model: crate::catalog::CollectionModel,
2168 analytics_config: Vec<crate::catalog::AnalyticsViewDescriptor>,
2169) -> crate::physical::CollectionContract {
2170 let now = current_unix_ms();
2171 crate::physical::CollectionContract {
2172 name: name.to_string(),
2173 declared_model: model,
2174 schema_mode: crate::catalog::SchemaMode::Dynamic,
2175 origin: crate::physical::ContractOrigin::Explicit,
2176 version: 1,
2177 created_at_unix_ms: now,
2178 updated_at_unix_ms: now,
2179 default_ttl_ms: None,
2180 vector_dimension: None,
2181 vector_metric: None,
2182 context_index_fields: Vec::new(),
2183 declared_columns: Vec::new(),
2184 table_def: None,
2185 timestamps_enabled: false,
2186 context_index_enabled: false,
2187 metrics_raw_retention_ms: None,
2188 metrics_rollup_policies: Vec::new(),
2189 metrics_tenant_identity: None,
2190 metrics_namespace: None,
2191 append_only: false,
2192 subscriptions: Vec::new(),
2193 analytics_config,
2194 session_key: None,
2195 session_gap_ms: None,
2196 retention_duration_ms: None,
2197 analytical_storage: None,
2198
2199 ai_policy: None,
2200 }
2201}
2202
2203fn metrics_collection_contract(query: &CreateTableQuery) -> crate::physical::CollectionContract {
2204 let now = current_unix_ms();
2205 crate::physical::CollectionContract {
2206 name: query.name.clone(),
2207 declared_model: crate::catalog::CollectionModel::Metrics,
2208 schema_mode: crate::catalog::SchemaMode::SemiStructured,
2209 origin: crate::physical::ContractOrigin::Explicit,
2210 version: 1,
2211 created_at_unix_ms: now,
2212 updated_at_unix_ms: now,
2213 default_ttl_ms: query.default_ttl_ms,
2214 vector_dimension: None,
2215 vector_metric: None,
2216 context_index_fields: Vec::new(),
2217 declared_columns: Vec::new(),
2218 table_def: None,
2219 timestamps_enabled: false,
2220 context_index_enabled: false,
2221 metrics_raw_retention_ms: query.default_ttl_ms,
2222 metrics_rollup_policies: query.metrics_rollup_policies.clone(),
2223 metrics_tenant_identity: Some(
2224 query
2225 .tenant_by
2226 .clone()
2227 .unwrap_or_else(|| "current_tenant".to_string()),
2228 ),
2229 metrics_namespace: Some("default".to_string()),
2230 append_only: true,
2231 subscriptions: Vec::new(),
2232 analytics_config: Vec::new(),
2233 session_key: None,
2234 session_gap_ms: None,
2235 retention_duration_ms: None,
2236 analytical_storage: None,
2237
2238 ai_policy: None,
2239 }
2240}
2241
2242fn vector_collection_contract(query: &CreateVectorQuery) -> crate::physical::CollectionContract {
2243 let now = current_unix_ms();
2244 crate::physical::CollectionContract {
2245 name: query.name.clone(),
2246 declared_model: crate::catalog::CollectionModel::Vector,
2247 schema_mode: crate::catalog::SchemaMode::Dynamic,
2248 origin: crate::physical::ContractOrigin::Explicit,
2249 version: 1,
2250 created_at_unix_ms: now,
2251 updated_at_unix_ms: now,
2252 default_ttl_ms: None,
2253 vector_dimension: Some(query.dimension),
2254 vector_metric: Some(query.metric),
2255 context_index_fields: Vec::new(),
2256 declared_columns: Vec::new(),
2257 table_def: None,
2258 timestamps_enabled: false,
2259 context_index_enabled: false,
2260 metrics_raw_retention_ms: None,
2261 metrics_rollup_policies: Vec::new(),
2262 metrics_tenant_identity: None,
2263 metrics_namespace: None,
2264 append_only: false,
2265 subscriptions: Vec::new(),
2266 analytics_config: Vec::new(),
2267 session_key: None,
2268 session_gap_ms: None,
2269 retention_duration_ms: None,
2270 analytical_storage: None,
2271
2272 ai_policy: None,
2273 }
2274}
2275
2276fn apply_alter_operations_to_contract(
2277 contract: &mut crate::physical::CollectionContract,
2278 operations: &[AlterOperation],
2279) {
2280 if contract.table_def.is_none() {
2281 contract.table_def = Some(reddb_types::TableDef::new(contract.name.clone()));
2282 }
2283 for operation in operations {
2284 match operation {
2285 AlterOperation::AddColumn(column) => {
2286 if !contract
2287 .declared_columns
2288 .iter()
2289 .any(|existing| existing.name == column.name)
2290 {
2291 contract
2292 .declared_columns
2293 .push(declared_column_contract_from_ddl(column));
2294 }
2295 if let Some(table_def) = contract.table_def.as_mut() {
2296 if table_def.get_column(&column.name).is_none() {
2297 if let Ok(column_def) = column_def_from_ddl(column) {
2298 if column.primary_key {
2299 table_def.primary_key.push(column.name.clone());
2300 table_def.constraints.push(
2301 reddb_types::Constraint::new(
2302 format!("pk_{}", column.name),
2303 reddb_types::ConstraintType::PrimaryKey,
2304 )
2305 .on_columns(vec![column.name.clone()]),
2306 );
2307 }
2308 if column.unique {
2309 table_def.constraints.push(
2310 reddb_types::Constraint::new(
2311 format!("uniq_{}", column.name),
2312 reddb_types::ConstraintType::Unique,
2313 )
2314 .on_columns(vec![column.name.clone()]),
2315 );
2316 }
2317 if column.not_null {
2318 table_def.constraints.push(
2319 reddb_types::Constraint::new(
2320 format!("not_null_{}", column.name),
2321 reddb_types::ConstraintType::NotNull,
2322 )
2323 .on_columns(vec![column.name.clone()]),
2324 );
2325 }
2326 table_def.columns.push(column_def);
2327 }
2328 }
2329 }
2330 }
2331 AlterOperation::DropColumn(name) => {
2332 contract
2333 .declared_columns
2334 .retain(|column| column.name != *name);
2335 if let Some(table_def) = contract.table_def.as_mut() {
2336 if let Some(index) = table_def.column_index(name) {
2337 table_def.columns.remove(index);
2338 }
2339 table_def.primary_key.retain(|column| column != name);
2340 table_def.constraints.retain(|constraint| {
2341 !constraint.columns.iter().any(|column| column == name)
2342 });
2343 table_def
2344 .indexes
2345 .retain(|index| !index.columns.iter().any(|column| column == name));
2346 }
2347 }
2348 AlterOperation::RenameColumn { from, to } => {
2349 if contract
2350 .declared_columns
2351 .iter()
2352 .any(|column| column.name == *to)
2353 {
2354 continue;
2355 }
2356 if let Some(column) = contract
2357 .declared_columns
2358 .iter_mut()
2359 .find(|column| column.name == *from)
2360 {
2361 column.name = to.clone();
2362 }
2363 if let Some(table_def) = contract.table_def.as_mut() {
2364 if let Some(column) = table_def
2365 .columns
2366 .iter_mut()
2367 .find(|column| column.name == *from)
2368 {
2369 column.name = to.clone();
2370 }
2371 for primary_key in &mut table_def.primary_key {
2372 if *primary_key == *from {
2373 *primary_key = to.clone();
2374 }
2375 }
2376 for constraint in &mut table_def.constraints {
2377 for column in &mut constraint.columns {
2378 if *column == *from {
2379 *column = to.clone();
2380 }
2381 }
2382 if let Some(ref_columns) = constraint.ref_columns.as_mut() {
2383 for column in ref_columns {
2384 if *column == *from {
2385 *column = to.clone();
2386 }
2387 }
2388 }
2389 }
2390 for index in &mut table_def.indexes {
2391 for column in &mut index.columns {
2392 if *column == *from {
2393 *column = to.clone();
2394 }
2395 }
2396 }
2397 }
2398 }
2399 AlterOperation::AttachPartition { .. } | AlterOperation::DetachPartition { .. } => {}
2402 AlterOperation::EnableRowLevelSecurity | AlterOperation::DisableRowLevelSecurity => {}
2406 AlterOperation::EnableTenancy { .. } | AlterOperation::DisableTenancy => {}
2409 AlterOperation::SetAppendOnly(on) => {
2410 contract.append_only = *on;
2411 }
2412 AlterOperation::SetVersioned(_) => {}
2415 AlterOperation::EnableEvents(subscription) => {
2416 let mut subscription = subscription.clone();
2417 subscription.source = contract.name.clone();
2418 subscription.enabled = true;
2419 if let Some(existing) = contract
2420 .subscriptions
2421 .iter_mut()
2422 .find(|existing| existing.target_queue == subscription.target_queue)
2423 {
2424 *existing = subscription;
2425 } else {
2426 contract.subscriptions.push(subscription);
2427 }
2428 }
2429 AlterOperation::DisableEvents => {
2430 for subscription in &mut contract.subscriptions {
2431 subscription.enabled = false;
2432 }
2433 }
2434 AlterOperation::AddSubscription { name, descriptor } => {
2435 let mut sub = descriptor.clone();
2436 sub.name = name.clone();
2437 sub.source = contract.name.clone();
2438 sub.enabled = true;
2439 if let Some(existing) = contract.subscriptions.iter_mut().find(|s| s.name == *name)
2440 {
2441 *existing = sub;
2442 } else {
2443 contract.subscriptions.push(sub);
2444 }
2445 }
2446 AlterOperation::DropSubscription { name } => {
2447 contract.subscriptions.retain(|s| s.name != *name);
2448 }
2449 AlterOperation::AddSigner { .. } | AlterOperation::RevokeSigner { .. } => {}
2454 AlterOperation::SetRetention { duration_ms } => {
2455 contract.retention_duration_ms = Some(*duration_ms);
2456 }
2457 AlterOperation::UnsetRetention => {
2458 contract.retention_duration_ms = None;
2459 }
2460 AlterOperation::AddAnalytics(views) => {
2464 for view in views {
2465 if !contract
2469 .analytics_config
2470 .iter()
2471 .any(|existing| existing.output == view.output)
2472 {
2473 contract.analytics_config.push(view.clone());
2474 }
2475 }
2476 }
2477 AlterOperation::DropAnalytics(output) => {
2478 contract
2479 .analytics_config
2480 .retain(|view| view.output != *output);
2481 }
2482 }
2483 }
2484}
2485
2486pub(crate) fn retention_timestamp_column_exists(
2491 contract: &crate::physical::CollectionContract,
2492) -> bool {
2493 if contract.timestamps_enabled {
2494 return true;
2495 }
2496 if matches!(
2497 contract.declared_model,
2498 crate::catalog::CollectionModel::TimeSeries | crate::catalog::CollectionModel::Metrics
2499 ) {
2500 return true;
2504 }
2505 contract
2506 .declared_columns
2507 .iter()
2508 .any(|column| is_temporal_data_type(&column.data_type))
2509}
2510
2511fn is_temporal_data_type(data_type: &str) -> bool {
2512 let upper = data_type.to_ascii_uppercase();
2513 matches!(
2514 upper.as_str(),
2515 "TIMESTAMP" | "TIMESTAMPMS" | "TIMESTAMP_MS" | "DATETIME" | "DATE"
2516 )
2517}
2518
2519fn validate_event_subscriptions(
2520 runtime: &RedDBRuntime,
2521 source: &str,
2522 subscriptions: &[crate::catalog::SubscriptionDescriptor],
2523) -> RedDBResult<()> {
2524 for subscription in subscriptions
2525 .iter()
2526 .filter(|subscription| subscription.enabled)
2527 {
2528 if subscription.all_tenants && crate::runtime::impl_core::current_tenant().is_some() {
2529 return Err(RedDBError::Query(
2530 "cross-tenant subscription requires cluster-admin capability (events:cluster_subscribe)".to_string(),
2531 ));
2532 }
2533 validate_subscription_auth(runtime, source, subscription)?;
2534 if subscription.target_queue == source
2535 || subscription_would_create_cycle(
2536 &runtime.inner.db,
2537 source,
2538 &subscription.target_queue,
2539 )
2540 {
2541 return Err(RedDBError::Query(
2542 "subscription would create cycle".to_string(),
2543 ));
2544 }
2545 audit_subscription_redact_gap(runtime, source, subscription);
2546 }
2547 Ok(())
2548}
2549
2550fn validate_subscription_auth(
2551 runtime: &RedDBRuntime,
2552 source: &str,
2553 subscription: &crate::catalog::SubscriptionDescriptor,
2554) -> RedDBResult<()> {
2555 let auth_store = match runtime.inner.auth_store.read().clone() {
2556 Some(store) => store,
2557 None => return Ok(()),
2558 };
2559 let (username, role) = match crate::runtime::impl_core::current_auth_identity() {
2560 Some(identity) => identity,
2561 None => return Ok(()),
2562 };
2563 let tenant = crate::runtime::impl_core::current_tenant();
2564 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
2565
2566 if auth_store.iam_authorization_enabled() {
2567 let ctx = crate::auth::policies::EvalContext {
2568 principal_tenant: tenant.clone(),
2569 current_tenant: tenant.clone(),
2570 peer_ip: None,
2571 mfa_present: false,
2572 now_ms: crate::auth::now_ms(),
2573 principal_is_admin_role: role == crate::auth::Role::Admin,
2574 principal_is_platform_scoped: principal.tenant.is_none(),
2575 };
2576 let mut source_resource = crate::auth::policies::ResourceRef::new("table", source);
2577 if let Some(t) = tenant.as_deref() {
2578 source_resource = source_resource.with_tenant(t.to_string());
2579 }
2580 if !auth_store.check_policy_authz_with_role(
2581 &principal,
2582 "select",
2583 &source_resource,
2584 &ctx,
2585 role,
2586 ) {
2587 return Err(RedDBError::Query(format!(
2588 "permission denied: principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
2589 principal, source_resource.kind, source_resource.name
2590 )));
2591 }
2592
2593 let mut target_resource =
2594 crate::auth::policies::ResourceRef::new("queue", subscription.target_queue.clone());
2595 if let Some(t) = tenant.as_deref() {
2596 target_resource = target_resource.with_tenant(t.to_string());
2597 }
2598 if !auth_store.check_policy_authz_with_role(
2599 &principal,
2600 "write",
2601 &target_resource,
2602 &ctx,
2603 role,
2604 ) {
2605 return Err(RedDBError::Query(format!(
2606 "permission denied: principal=`{}` action=`write` resource=`{}:{}` denied by IAM policy",
2607 principal, target_resource.kind, target_resource.name
2608 )));
2609 }
2610 return Ok(());
2611 }
2612
2613 let ctx = crate::auth::privileges::AuthzContext {
2614 principal: &username,
2615 effective_role: role,
2616 tenant: tenant.as_deref(),
2617 };
2618 auth_store
2619 .check_grant(
2620 &ctx,
2621 crate::auth::privileges::Action::Select,
2622 &crate::auth::privileges::Resource::table_from_name(source),
2623 )
2624 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))?;
2625 auth_store
2626 .check_grant(
2627 &ctx,
2628 crate::auth::privileges::Action::Insert,
2629 &crate::auth::privileges::Resource::table_from_name(&subscription.target_queue),
2630 )
2631 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))?;
2632 Ok(())
2633}
2634
2635fn audit_subscription_redact_gap(
2636 runtime: &RedDBRuntime,
2637 source: &str,
2638 subscription: &crate::catalog::SubscriptionDescriptor,
2639) {
2640 let auth_store = match runtime.inner.auth_store.read().clone() {
2641 Some(store) if store.iam_authorization_enabled() => store,
2642 _ => return,
2643 };
2644 let (username, role) = match crate::runtime::impl_core::current_auth_identity() {
2645 Some(identity) => identity,
2646 None => return,
2647 };
2648 let tenant = crate::runtime::impl_core::current_tenant();
2649 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
2650 let missing = subscription_redact_gap_columns(&auth_store, &principal, source, subscription);
2651 if missing.is_empty() {
2652 return;
2653 }
2654
2655 let columns = missing.into_iter().collect::<Vec<_>>().join(", ");
2656 tracing::warn!(
2657 target: "reddb::operator",
2658 "subscription_redact_gap: source={} target_queue={} columns=[{}]",
2659 source,
2660 subscription.target_queue,
2661 columns
2662 );
2663 let mut event = AuditEvent::builder("subscription_redact_gap")
2664 .principal(username)
2665 .source(AuditAuthSource::System)
2666 .resource(format!(
2667 "subscription:{}->{}",
2668 source, subscription.target_queue
2669 ))
2670 .outcome(Outcome::Success)
2671 .field(AuditFieldEscaper::field("source", source))
2672 .field(AuditFieldEscaper::field(
2673 "target_queue",
2674 subscription.target_queue.clone(),
2675 ))
2676 .field(AuditFieldEscaper::field(
2677 "subscription",
2678 subscription.name.clone(),
2679 ))
2680 .field(AuditFieldEscaper::field("columns", columns))
2681 .field(AuditFieldEscaper::field("role", role.as_str()));
2682 if let Some(t) = tenant {
2683 event = event.tenant(t);
2684 }
2685 runtime.inner.audit_log.record_event(event.build());
2686}
2687
2688fn subscription_redact_gap_columns(
2689 auth_store: &crate::auth::store::AuthStore,
2690 principal: &crate::auth::UserId,
2691 source: &str,
2692 subscription: &crate::catalog::SubscriptionDescriptor,
2693) -> BTreeSet<String> {
2694 let redacted: HashSet<String> = subscription
2695 .redact_fields
2696 .iter()
2697 .map(|field| field.to_ascii_lowercase())
2698 .collect();
2699 auth_store
2700 .effective_policies(principal)
2701 .iter()
2702 .flat_map(|policy| policy.statements.iter())
2703 .filter(|statement| statement.effect == crate::auth::policies::Effect::Deny)
2704 .filter(|statement| statement.actions.iter().any(action_pattern_matches_select))
2705 .flat_map(|statement| statement.resources.iter())
2706 .filter_map(|resource| denied_column_for_source(resource, source))
2707 .filter(|column| !redact_covers_column(&redacted, source, column))
2708 .collect()
2709}
2710
2711fn action_pattern_matches_select(pattern: &crate::auth::policies::ActionPattern) -> bool {
2712 match pattern {
2713 crate::auth::policies::ActionPattern::Wildcard => true,
2714 crate::auth::policies::ActionPattern::Exact(action) => action == "select",
2715 crate::auth::policies::ActionPattern::Prefix(prefix) => {
2716 "select".len() > prefix.len() + 1
2717 && "select".starts_with(prefix)
2718 && "select".as_bytes()[prefix.len()] == b':'
2719 }
2720 }
2721}
2722
2723fn denied_column_for_source(
2724 resource: &crate::auth::policies::ResourcePattern,
2725 source: &str,
2726) -> Option<String> {
2727 let crate::auth::policies::ResourcePattern::Exact { kind, name } = resource else {
2728 return None;
2729 };
2730 if kind != "column" {
2731 return None;
2732 }
2733 let column = crate::auth::ColumnRef::parse_resource_name(name).ok()?;
2734 (column.table_resource_name() == source).then_some(column.column)
2735}
2736
2737fn redact_covers_column(redacted: &HashSet<String>, source: &str, column: &str) -> bool {
2738 let column = column.to_ascii_lowercase();
2739 let qualified = format!("{}.{}", source.to_ascii_lowercase(), column);
2740 redacted.contains("*") || redacted.contains(&column) || redacted.contains(&qualified)
2741}
2742
2743fn subscription_would_create_cycle(
2744 db: &crate::storage::unified::devx::RedDB,
2745 source: &str,
2746 target: &str,
2747) -> bool {
2748 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
2749 for contract in db.collection_contracts() {
2750 for subscription in contract
2751 .subscriptions
2752 .into_iter()
2753 .filter(|subscription| subscription.enabled)
2754 {
2755 graph
2756 .entry(subscription.source)
2757 .or_default()
2758 .push(subscription.target_queue);
2759 }
2760 }
2761 graph
2762 .entry(source.to_string())
2763 .or_default()
2764 .push(target.to_string());
2765
2766 let mut stack = vec![target.to_string()];
2767 let mut seen = HashSet::new();
2768 while let Some(node) = stack.pop() {
2769 if node == source {
2770 return true;
2771 }
2772 if !seen.insert(node.clone()) {
2773 continue;
2774 }
2775 if let Some(next) = graph.get(&node) {
2776 stack.extend(next.iter().cloned());
2777 }
2778 }
2779 false
2780}
2781
2782pub(crate) fn ensure_event_target_queue_pub(
2783 runtime: &RedDBRuntime,
2784 queue: &str,
2785) -> RedDBResult<()> {
2786 ensure_event_target_queue(runtime, queue)
2787}
2788
2789fn ensure_event_target_queue(runtime: &RedDBRuntime, queue: &str) -> RedDBResult<()> {
2790 let store = runtime.inner.db.store();
2791 if store.get_collection(queue).is_some() {
2792 return Ok(());
2793 }
2794 store
2795 .create_collection(queue)
2796 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2797 crate::runtime::queue_storage_index::prepare_queue(runtime, queue)?;
2798 runtime
2799 .inner
2800 .db
2801 .save_collection_contract(event_queue_collection_contract(queue))
2802 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2803 store.set_config_tree(
2804 &format!("queue.{queue}.mode"),
2805 &crate::serde_json::Value::String("fanout".to_string()),
2806 );
2807 Ok(())
2808}
2809
2810fn event_queue_collection_contract(queue: &str) -> crate::physical::CollectionContract {
2811 let now = current_unix_ms();
2812 crate::physical::CollectionContract {
2813 name: queue.to_string(),
2814 declared_model: crate::catalog::CollectionModel::Queue,
2815 schema_mode: crate::catalog::SchemaMode::Dynamic,
2816 origin: crate::physical::ContractOrigin::Implicit,
2817 version: 1,
2818 created_at_unix_ms: now,
2819 updated_at_unix_ms: now,
2820 default_ttl_ms: None,
2821 vector_dimension: None,
2822 vector_metric: None,
2823 context_index_fields: Vec::new(),
2824 declared_columns: Vec::new(),
2825 table_def: None,
2826 timestamps_enabled: false,
2827 context_index_enabled: false,
2828 metrics_raw_retention_ms: None,
2829 metrics_rollup_policies: Vec::new(),
2830 metrics_tenant_identity: None,
2831 metrics_namespace: None,
2832 append_only: true,
2833 subscriptions: Vec::new(),
2834 analytics_config: Vec::new(),
2835 session_key: None,
2836 session_gap_ms: None,
2837 retention_duration_ms: None,
2838 analytical_storage: None,
2839
2840 ai_policy: None,
2841 }
2842}
2843
2844fn column_def_from_ddl(column: &CreateColumnDef) -> RedDBResult<reddb_types::ColumnDef> {
2845 reddb_engine::catalog::ddl::column_def_from_ddl(column).map_err(RedDBError::Query)
2846}
2847
2848fn current_unix_ms() -> u128 {
2849 std::time::SystemTime::now()
2850 .duration_since(std::time::UNIX_EPOCH)
2851 .unwrap_or_default()
2852 .as_millis()
2853}
2854
2855#[cfg(test)]
2856mod tests {
2857 use crate::auth::policies::{ActionPattern, Effect, Policy, ResourcePattern, Statement};
2858 use crate::auth::store::{AuthStore, PrincipalRef};
2859 use crate::auth::UserId;
2860 use crate::auth::{AuthConfig, Role};
2861 use crate::runtime::impl_core::{clear_current_auth_identity, set_current_auth_identity};
2862 use crate::{RedDBOptions, RedDBRuntime};
2863 use reddb_types::Value;
2864 use std::sync::Arc;
2865
2866 fn make_allow_policy(id: &str, action: &str, collection: &str) -> Policy {
2867 Policy {
2868 id: id.to_string(),
2869 version: 1,
2870 tenant: None,
2871 created_at: 0,
2872 updated_at: 0,
2873 statements: vec![Statement {
2874 sid: None,
2875 effect: Effect::Allow,
2876 actions: vec![ActionPattern::Exact(action.to_string())],
2877 resources: vec![ResourcePattern::Exact {
2878 kind: "collection".to_string(),
2879 name: collection.to_string(),
2880 }],
2881 condition: None,
2882 }],
2883 }
2884 }
2885
2886 fn wire_auth_store(rt: &RedDBRuntime) -> Arc<AuthStore> {
2887 let store = Arc::new(AuthStore::new(AuthConfig::default()));
2888 *rt.inner.auth_store.write() = Some(store.clone());
2889 store
2890 }
2891
2892 #[test]
2893 fn drop_denied_without_iam_policy() {
2894 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2895 rt.execute_query("CREATE TABLE foo (id INT)").unwrap();
2896 let store = wire_auth_store(&rt);
2897 let select_only = Policy {
2899 id: "select-only".to_string(),
2900 version: 1,
2901 tenant: None,
2902 created_at: 0,
2903 updated_at: 0,
2904 statements: vec![Statement {
2905 sid: None,
2906 effect: Effect::Allow,
2907 actions: vec![ActionPattern::Exact("select".to_string())],
2908 resources: vec![ResourcePattern::Wildcard],
2909 condition: None,
2910 }],
2911 };
2912 store.put_policy_internal(select_only).unwrap();
2913 let alice = UserId::from_parts(None, "alice");
2914 store
2915 .attach_policy(PrincipalRef::User(alice), "select-only")
2916 .unwrap();
2917 set_current_auth_identity("alice".to_string(), Role::Write);
2918 let err = rt.execute_query("DROP TABLE foo").unwrap_err();
2919 clear_current_auth_identity();
2920 assert!(
2921 format!("{err}").contains("denied by IAM policy"),
2922 "got: {err}"
2923 );
2924 }
2925
2926 #[test]
2927 fn drop_allowed_with_explicit_iam_policy() {
2928 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2929 rt.execute_query("CREATE TABLE bar (id INT)").unwrap();
2930 let store = wire_auth_store(&rt);
2931 let policy = make_allow_policy("allow-drop-bar", "drop", "bar");
2932 store.put_policy_internal(policy).unwrap();
2933 let bob = UserId::from_parts(None, "bob");
2934 store
2935 .attach_policy(PrincipalRef::User(bob), "allow-drop-bar")
2936 .unwrap();
2937 set_current_auth_identity("bob".to_string(), Role::Write);
2938 rt.execute_query("DROP TABLE bar").unwrap();
2939 clear_current_auth_identity();
2940 }
2941
2942 #[test]
2943 fn drop_allowed_with_wildcard_iam_policy() {
2944 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2945 rt.execute_query("CREATE TABLE baz (id INT)").unwrap();
2946 let store = wire_auth_store(&rt);
2947 let policy = Policy {
2948 id: "allow-drop-all".to_string(),
2949 version: 1,
2950 tenant: None,
2951 created_at: 0,
2952 updated_at: 0,
2953 statements: vec![Statement {
2954 sid: None,
2955 effect: Effect::Allow,
2956 actions: vec![ActionPattern::Exact("drop".to_string())],
2957 resources: vec![ResourcePattern::Wildcard],
2958 condition: None,
2959 }],
2960 };
2961 store.put_policy_internal(policy).unwrap();
2962 let carl = UserId::from_parts(None, "carl");
2963 store
2964 .attach_policy(PrincipalRef::User(carl), "allow-drop-all")
2965 .unwrap();
2966 set_current_auth_identity("carl".to_string(), Role::Write);
2967 rt.execute_query("DROP TABLE baz").unwrap();
2968 clear_current_auth_identity();
2969 }
2970
2971 #[test]
2972 fn truncate_denied_without_iam_policy() {
2973 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2974 rt.execute_query("CREATE TABLE qux (id INT)").unwrap();
2975 let store = wire_auth_store(&rt);
2976 store
2983 .set_enforcement_mode(crate::auth::enforcement_mode::PolicyEnforcementMode::PolicyOnly);
2984 let select_only = Policy {
2986 id: "select-only-2".to_string(),
2987 version: 1,
2988 tenant: None,
2989 created_at: 0,
2990 updated_at: 0,
2991 statements: vec![Statement {
2992 sid: None,
2993 effect: Effect::Allow,
2994 actions: vec![ActionPattern::Exact("select".to_string())],
2995 resources: vec![ResourcePattern::Wildcard],
2996 condition: None,
2997 }],
2998 };
2999 store.put_policy_internal(select_only).unwrap();
3000 let dana = UserId::from_parts(None, "dana");
3001 store
3002 .attach_policy(PrincipalRef::User(dana), "select-only-2")
3003 .unwrap();
3004 set_current_auth_identity("dana".to_string(), Role::Write);
3005 let err = rt.execute_query("TRUNCATE TABLE qux").unwrap_err();
3006 clear_current_auth_identity();
3007 assert!(
3008 format!("{err}").contains("denied by IAM policy"),
3009 "got: {err}"
3010 );
3011 }
3012
3013 #[test]
3014 fn truncate_table_clears_rows_and_preserves_schema_and_indexes() {
3015 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3016 rt.execute_query("CREATE TABLE users (id INT, name TEXT)")
3017 .unwrap();
3018 rt.execute_query("INSERT INTO users (id, name) VALUES (1, 'ana'), (2, 'bob')")
3019 .unwrap();
3020 rt.execute_query("CREATE INDEX idx_users_id ON users (id) USING HASH")
3021 .unwrap();
3022
3023 let truncated = rt.execute_query("TRUNCATE TABLE users").unwrap();
3024 assert_eq!(truncated.statement_type, "truncate");
3025 assert_eq!(truncated.affected_rows, 0);
3026
3027 let empty = rt.execute_query("SELECT id FROM users").unwrap();
3028 assert!(empty.result.records.is_empty());
3029
3030 rt.execute_query("INSERT INTO users (id, name) VALUES (3, 'cy')")
3031 .unwrap();
3032 let selected = rt
3033 .execute_query("SELECT name FROM users WHERE id = 3")
3034 .unwrap();
3035 let name = selected.result.records[0].get("name").unwrap();
3036 assert_eq!(name, &Value::text("cy"));
3037 assert!(rt.db().collection_contract("users").is_some());
3038 assert!(rt
3039 .inner
3040 .index_store
3041 .list_indices("users")
3042 .iter()
3043 .any(|index| index.name == "idx_users_id"));
3044 }
3045
3046 #[test]
3047 fn truncate_collection_is_polymorphic_and_typed_mismatch_fails() {
3048 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3049 rt.execute_query("CREATE QUEUE tasks").unwrap();
3050 rt.execute_query("QUEUE PUSH tasks {'job':'a'}").unwrap();
3051
3052 let err = rt.execute_query("TRUNCATE TABLE tasks").unwrap_err();
3053 assert!(format!("{err}").contains("model mismatch: expected table, got queue"));
3054
3055 rt.execute_query("TRUNCATE COLLECTION tasks").unwrap();
3056 let len = rt.execute_query("QUEUE LEN tasks").unwrap();
3057 assert_eq!(
3058 len.result.records[0].get("len"),
3059 Some(&Value::UnsignedInteger(0))
3060 );
3061 }
3062
3063 #[test]
3064 fn truncate_system_schema_is_read_only() {
3065 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3066 let err = rt
3067 .execute_query("TRUNCATE COLLECTION red.collections")
3068 .unwrap_err();
3069 assert!(format!("{err}").contains("system schema is read-only"));
3070 }
3071
3072 fn queue_payloads(rt: &RedDBRuntime, queue: &str) -> Vec<crate::json::Value> {
3075 let result = rt
3076 .execute_query(&format!("QUEUE PEEK {queue} 100"))
3077 .expect("peek queue");
3078 result
3079 .result
3080 .records
3081 .iter()
3082 .map(
3083 |record| match record.get("payload").expect("payload column") {
3084 Value::Json(bytes) => crate::json::from_slice(bytes).expect("json payload"),
3085 other => panic!("expected JSON queue payload, got {other:?}"),
3086 },
3087 )
3088 .collect()
3089 }
3090
3091 #[test]
3094 fn truncate_event_enabled_table_emits_single_truncate_event() {
3095 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3096 rt.execute_query("CREATE TABLE users (id INT, name TEXT) WITH EVENTS TO users_events")
3097 .unwrap();
3098 rt.execute_query(
3099 "INSERT INTO users (id, name) VALUES (1, 'alice'), (2, 'bob'), (3, 'carol')",
3100 )
3101 .unwrap();
3102
3103 rt.execute_query("QUEUE POP users_events COUNT 10").unwrap();
3105
3106 rt.execute_query("TRUNCATE TABLE users").unwrap();
3107
3108 let events = queue_payloads(&rt, "users_events");
3109 assert_eq!(
3111 events.len(),
3112 1,
3113 "expected 1 truncate event, got {}",
3114 events.len()
3115 );
3116 let ev = events[0].as_object().expect("event is object");
3117 assert_eq!(
3118 ev.get("op").and_then(crate::json::Value::as_str),
3119 Some("truncate")
3120 );
3121 assert_eq!(
3122 ev.get("collection").and_then(crate::json::Value::as_str),
3123 Some("users")
3124 );
3125 assert_eq!(
3126 ev.get("entities_count")
3127 .and_then(crate::json::Value::as_u64),
3128 Some(3)
3129 );
3130 assert!(ev.get("ts").and_then(crate::json::Value::as_u64).is_some());
3131 assert!(ev.get("lsn").and_then(crate::json::Value::as_u64).is_some());
3132 assert!(ev
3133 .get("event_id")
3134 .and_then(crate::json::Value::as_str)
3135 .is_some_and(|s| !s.is_empty()));
3136 }
3137
3138 #[test]
3140 fn truncate_no_events_collection_emits_nothing() {
3141 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3142 rt.execute_query("CREATE TABLE plain (id INT, val TEXT)")
3143 .unwrap();
3144 rt.execute_query("INSERT INTO plain (id, val) VALUES (1, 'a'), (2, 'b')")
3145 .unwrap();
3146 rt.execute_query("TRUNCATE TABLE plain").unwrap();
3148 let rows = rt.execute_query("SELECT id FROM plain").unwrap();
3150 assert!(rows.result.records.is_empty());
3151 }
3152
3153 #[test]
3157 fn drop_event_enabled_table_emits_single_collection_dropped_event() {
3158 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3159 rt.execute_query("CREATE TABLE users (id INT, name TEXT) WITH EVENTS TO users_events")
3160 .unwrap();
3161 rt.execute_query("INSERT INTO users (id, name) VALUES (1, 'alice'), (2, 'bob')")
3162 .unwrap();
3163
3164 rt.execute_query("QUEUE POP users_events COUNT 10").unwrap();
3166
3167 rt.execute_query("DROP TABLE users").unwrap();
3168
3169 let events = queue_payloads(&rt, "users_events");
3171 assert_eq!(
3172 events.len(),
3173 1,
3174 "expected 1 collection_dropped event, got {}",
3175 events.len()
3176 );
3177 let ev = events[0].as_object().expect("event is object");
3178 assert_eq!(
3179 ev.get("op").and_then(crate::json::Value::as_str),
3180 Some("collection_dropped")
3181 );
3182 assert_eq!(
3183 ev.get("collection").and_then(crate::json::Value::as_str),
3184 Some("users")
3185 );
3186 assert_eq!(
3187 ev.get("final_entities_count")
3188 .and_then(crate::json::Value::as_u64),
3189 Some(2)
3190 );
3191 assert!(ev.get("ts").and_then(crate::json::Value::as_u64).is_some());
3192 assert!(ev.get("lsn").and_then(crate::json::Value::as_u64).is_some());
3193 assert!(ev
3194 .get("event_id")
3195 .and_then(crate::json::Value::as_str)
3196 .is_some_and(|s| !s.is_empty()));
3197
3198 let err = rt.execute_query("SELECT id FROM users").unwrap_err();
3200 assert!(
3201 format!("{err}").contains("users"),
3202 "expected not-found error"
3203 );
3204 }
3205
3206 #[test]
3209 fn drop_no_events_collection_emits_nothing() {
3210 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3211 rt.execute_query("CREATE TABLE plain (id INT, val TEXT)")
3212 .unwrap();
3213 rt.execute_query("INSERT INTO plain (id, val) VALUES (1, 'a')")
3214 .unwrap();
3215 rt.execute_query("DROP TABLE plain").unwrap();
3216 let err = rt.execute_query("SELECT id FROM plain").unwrap_err();
3218 assert!(format!("{err}").contains("plain"));
3219 }
3220
3221 #[test]
3225 fn ops_filter_insert_only_ignores_update_and_delete() {
3226 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3227 rt.execute_query(
3228 "CREATE TABLE items (id INT, val TEXT) WITH EVENTS (INSERT) TO items_events",
3229 )
3230 .unwrap();
3231 rt.execute_query("INSERT INTO items (id, val) VALUES (1, 'a')")
3232 .unwrap();
3233 rt.execute_query("UPDATE items SET val = 'b' WHERE id = 1")
3234 .unwrap();
3235 rt.execute_query("DELETE FROM items WHERE id = 1").unwrap();
3236
3237 let events = queue_payloads(&rt, "items_events");
3238 assert_eq!(
3240 events.len(),
3241 1,
3242 "expected 1 insert event, got {}",
3243 events.len()
3244 );
3245 assert_eq!(
3246 events[0]
3247 .as_object()
3248 .unwrap()
3249 .get("op")
3250 .and_then(crate::json::Value::as_str),
3251 Some("insert")
3252 );
3253 }
3254
3255 #[test]
3257 fn where_filter_skips_rows_that_do_not_match() {
3258 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3259 rt.execute_query(
3260 "CREATE TABLE users (id INT, status TEXT) WITH EVENTS WHERE status = 'active' TO users_events",
3261 )
3262 .unwrap();
3263
3264 rt.execute_query("INSERT INTO users (id, status) VALUES (1, 'active')")
3266 .unwrap();
3267 rt.execute_query("INSERT INTO users (id, status) VALUES (2, 'inactive')")
3269 .unwrap();
3270
3271 let events = queue_payloads(&rt, "users_events");
3272 assert_eq!(
3273 events.len(),
3274 1,
3275 "expected 1 event (only active), got {}",
3276 events.len()
3277 );
3278 let ev = events[0].as_object().unwrap();
3279 assert_eq!(
3280 ev.get("op").and_then(crate::json::Value::as_str),
3281 Some("insert")
3282 );
3283 let after = ev.get("after").unwrap().as_object().unwrap();
3284 assert_eq!(
3285 after.get("status").and_then(crate::json::Value::as_str),
3286 Some("active")
3287 );
3288 }
3289
3290 #[test]
3292 fn ops_filter_and_where_filter_combined() {
3293 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3294 rt.execute_query(
3295 "CREATE TABLE items (id INT, status TEXT) WITH EVENTS (INSERT, UPDATE) WHERE status = 'active' TO items_events",
3296 )
3297 .unwrap();
3298
3299 rt.execute_query("INSERT INTO items (id, status) VALUES (1, 'active')")
3301 .unwrap();
3302 rt.execute_query("INSERT INTO items (id, status) VALUES (2, 'inactive')")
3304 .unwrap();
3305 rt.execute_query("UPDATE items SET status = 'inactive' WHERE id = 1")
3307 .unwrap();
3308 rt.execute_query("DELETE FROM items WHERE id = 2").unwrap();
3310
3311 let events = queue_payloads(&rt, "items_events");
3312 assert_eq!(
3314 events.len(),
3315 1,
3316 "expected 1 event, got {}: {events:?}",
3317 events.len()
3318 );
3319 assert_eq!(
3320 events[0]
3321 .as_object()
3322 .unwrap()
3323 .get("op")
3324 .and_then(crate::json::Value::as_str),
3325 Some("insert")
3326 );
3327 }
3328
3329 #[test]
3331 fn where_filter_on_delete_checks_before_state() {
3332 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3333 rt.execute_query(
3334 "CREATE TABLE users (id INT, status TEXT) WITH EVENTS (DELETE) WHERE status = 'active' TO users_events",
3335 )
3336 .unwrap();
3337
3338 rt.execute_query("INSERT INTO users (id, status) VALUES (1, 'active'), (2, 'inactive')")
3339 .unwrap();
3340
3341 rt.execute_query("DELETE FROM users WHERE id = 1").unwrap();
3343 rt.execute_query("DELETE FROM users WHERE id = 2").unwrap();
3345
3346 let events = queue_payloads(&rt, "users_events");
3347 assert_eq!(
3348 events.len(),
3349 1,
3350 "expected 1 delete event, got {}",
3351 events.len()
3352 );
3353 let ev = events[0].as_object().unwrap();
3354 assert_eq!(
3355 ev.get("op").and_then(crate::json::Value::as_str),
3356 Some("delete")
3357 );
3358 }
3359
3360 #[test]
3364 fn alter_add_column_on_event_enabled_table_succeeds() {
3365 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3366 rt.execute_query("CREATE TABLE users (id INT, name TEXT) WITH EVENTS TO users_events")
3367 .unwrap();
3368 rt.execute_query("ALTER TABLE users ADD COLUMN phone TEXT")
3370 .unwrap();
3371 let contract = rt.db().collection_contract("users").unwrap();
3373 assert!(
3374 contract.declared_columns.iter().any(|c| c.name == "phone"),
3375 "phone column should be in contract"
3376 );
3377 assert!(
3379 contract.subscriptions.iter().any(|s| s.enabled),
3380 "subscription should remain enabled"
3381 );
3382 }
3383
3384 #[test]
3387 fn alter_drop_column_and_rls_on_event_enabled_table_succeeds() {
3388 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3389 rt.execute_query(
3390 "CREATE TABLE items (id INT, secret TEXT, status TEXT) WITH EVENTS TO items_events",
3391 )
3392 .unwrap();
3393 rt.execute_query("ALTER TABLE items DROP COLUMN secret")
3395 .unwrap();
3396 let contract = rt.db().collection_contract("items").unwrap();
3397 assert!(
3398 !contract.declared_columns.iter().any(|c| c.name == "secret"),
3399 "secret column should be removed"
3400 );
3401 rt.execute_query("ALTER TABLE items ENABLE ROW LEVEL SECURITY")
3403 .unwrap();
3404 assert!(
3406 contract.subscriptions.iter().any(|s| s.enabled),
3407 "subscription should remain enabled"
3408 );
3409 }
3410
3411 #[test]
3417 fn create_vector_marks_collection_as_turbo_baseline() {
3418 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3419 rt.execute_query("CREATE VECTOR embeddings DIM 4").unwrap();
3420 let store = rt.db().store();
3421 assert!(
3422 crate::runtime::vector_turbo_kind::is_turbo(&store, "embeddings"),
3423 "new vector collections must be turbo-marked baseline"
3424 );
3425 assert!(
3426 rt.db().turbo_state("embeddings").is_some(),
3427 "turbo_state must materialise after CREATE VECTOR"
3428 );
3429 }
3430
3431 #[test]
3435 fn create_table_does_not_mark_turbo() {
3436 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3437 rt.execute_query("CREATE TABLE plain (id INT)").unwrap();
3438 let store = rt.db().store();
3439 assert!(
3440 !crate::runtime::vector_turbo_kind::is_turbo(&store, "plain"),
3441 "non-vector collections must not gain the turbo marker"
3442 );
3443 assert!(rt.db().turbo_state("plain").is_none());
3444 }
3445
3446 #[test]
3449 fn create_collection_kind_vector_turbo_still_marked() {
3450 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3451 rt.execute_query("CREATE COLLECTION turbo_v KIND vector.turbo DIM 4")
3452 .unwrap();
3453 let store = rt.db().store();
3454 assert!(crate::runtime::vector_turbo_kind::is_turbo(
3455 &store, "turbo_v"
3456 ));
3457 assert!(rt.db().turbo_state("turbo_v").is_some());
3458 }
3459
3460 #[test]
3463 fn create_table_persists_and_introspects_ai_policy() {
3464 use crate::catalog::{ModerateDegradedMode, ModerateRejectAction};
3465 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3466 rt.execute_query(
3467 "CREATE TABLE posts (id INT, title TEXT, body TEXT, photo TEXT) WITH ( \
3468 EMBED (fields = ('title', 'body'), provider = 'openai', model = 'text-embedding-3-small'), \
3469 MODERATE (fields = ('body'), provider = 'openai', model = 'omni-moderation-latest', sync = true, degraded = closed, on_reject = flag), \
3470 VISION (image_field = 'photo', outputs = ('caption'), provider = 'openai', model = 'gpt-4o') \
3471 )",
3472 )
3473 .expect("create table with ai policy");
3474
3475 let contracts = rt.db().collection_contracts();
3476 let contract = contracts
3477 .iter()
3478 .find(|c| c.name == "posts")
3479 .expect("contract persisted");
3480 let policy = contract.ai_policy.as_ref().expect("ai policy persisted");
3481
3482 let embed = policy.embed.as_ref().expect("embed");
3483 assert_eq!(embed.fields, vec!["title".to_string(), "body".to_string()]);
3484 assert_eq!(embed.model, "text-embedding-3-small");
3485
3486 let moderate = policy.moderate.as_ref().expect("moderate");
3487 assert!(moderate.sync_gate);
3488 assert_eq!(moderate.degraded_mode, ModerateDegradedMode::Closed);
3489 assert_eq!(moderate.reject_action, ModerateRejectAction::Flag);
3490
3491 let vision = policy.vision.as_ref().expect("vision");
3492 assert_eq!(vision.image_field, "photo");
3493 assert_eq!(vision.model, "gpt-4o");
3494 }
3495
3496 #[test]
3500 fn create_table_rejects_ai_policy_incapable_provider() {
3501 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3502 let err = rt
3503 .execute_query(
3504 "CREATE TABLE bad (id INT, body TEXT) WITH ( \
3505 EMBED (fields = ('body'), provider = 'anthropic', model = 'claude-3-5-sonnet') \
3506 )",
3507 )
3508 .unwrap_err();
3509 assert!(
3510 err.to_string()
3511 .contains("cannot serve the 'embed' modality"),
3512 "{err}"
3513 );
3514 assert!(
3515 rt.db().store().get_collection("bad").is_none(),
3516 "rejected DDL must not create the collection"
3517 );
3518 }
3519}