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