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 index_growth_rows = entity_fields
1597 .iter()
1598 .map(|(_, fields)| fields.clone())
1599 .collect::<Vec<_>>();
1600 self.admit_non_evictable_growth(
1601 crate::storage::memory_pools::MemoryPool::IndexMemory,
1602 &format!("create index {}", query.name),
1603 crate::runtime::memory_admission::estimate_index_growth(
1604 &index_growth_rows,
1605 &query.columns,
1606 ),
1607 )?;
1608
1609 let indexed_count = self
1611 .inner
1612 .index_store
1613 .create_index(
1614 &query.name,
1615 &query.table,
1616 &query.columns,
1617 method_kind,
1618 query.unique,
1619 &entity_fields,
1620 )
1621 .map_err(RedDBError::Internal)?;
1622
1623 let analyzed = crate::storage::query::planner::stats_catalog::analyze_entity_fields(
1624 &query.table,
1625 &entity_fields,
1626 );
1627 crate::storage::query::planner::stats_catalog::persist_table_stats(&store, &analyzed);
1628 self.invalidate_plan_cache();
1629
1630 self.inner
1632 .index_store
1633 .register(super::index_store::RegisteredIndex {
1634 name: query.name.clone(),
1635 collection: query.table.clone(),
1636 columns: query.columns.clone(),
1637 method: method_kind,
1638 unique: query.unique,
1639 });
1640 self.persist_runtime_index_descriptor(super::index_store::RegisteredIndex {
1641 name: query.name.clone(),
1642 collection: query.table.clone(),
1643 columns: query.columns.clone(),
1644 method: method_kind,
1645 unique: query.unique,
1646 })?;
1647 self.schema_vocabulary_apply(crate::runtime::schema_vocabulary::DdlEvent::CreateIndex {
1651 collection: query.table.clone(),
1652 index: query.name.clone(),
1653 columns: query.columns.clone(),
1654 });
1655
1656 let method_str = format!("{}", query.method);
1657 let unique_str = if query.unique { "unique " } else { "" };
1658 let cols = query.columns.join(", ");
1659 let total_count = entity_fields.len();
1660 let coverage_detail =
1661 if matches!(method_kind, super::index_store::IndexMethodKind::H3 { .. })
1662 && total_count > 0
1663 && indexed_count == 0
1664 {
1665 format!(
1666 "{} of {} entities indexed — no indexable geo value in '{}'; expected {}",
1667 indexed_count,
1668 total_count,
1669 cols,
1670 crate::geo::RECOGNIZED_GEO_SHAPES
1671 )
1672 } else if matches!(method_kind, super::index_store::IndexMethodKind::H3 { .. }) {
1673 format!("{} of {} entities indexed", indexed_count, total_count)
1674 } else {
1675 format!("{} entities indexed", indexed_count)
1676 };
1677
1678 Ok(RuntimeQueryResult::ok_message(
1679 raw_query.to_string(),
1680 &format!(
1681 "{}index '{}' created on '{}' ({}) using {} ({})",
1682 unique_str, query.name, query.table, cols, method_str, coverage_detail
1683 ),
1684 "create",
1685 ))
1686 }
1687
1688 pub fn execute_drop_index(
1692 &self,
1693 raw_query: &str,
1694 query: &DropIndexQuery,
1695 ) -> RedDBResult<RuntimeQueryResult> {
1696 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1697 let store = self.inner.db.store();
1698
1699 if store.get_collection(&query.table).is_none() {
1701 if query.if_exists {
1702 return Ok(RuntimeQueryResult::ok_message(
1703 raw_query.to_string(),
1704 &format!("table '{}' does not exist", query.table),
1705 "drop",
1706 ));
1707 }
1708 return Err(RedDBError::NotFound(format!(
1709 "table '{}' not found",
1710 query.table
1711 )));
1712 }
1713
1714 self.inner.index_store.drop_index(&query.name, &query.table);
1716 self.persist_runtime_index_drop(&query.table, &query.name)?;
1717 self.invalidate_plan_cache();
1718 self.schema_vocabulary_apply(crate::runtime::schema_vocabulary::DdlEvent::DropIndex {
1720 collection: query.table.clone(),
1721 index: query.name.clone(),
1722 });
1723
1724 Ok(RuntimeQueryResult::ok_message(
1725 raw_query.to_string(),
1726 &format!("index '{}' dropped from '{}'", query.name, query.table),
1727 "drop",
1728 ))
1729 }
1730
1731 fn execute_drop_typed_collection(
1732 &self,
1733 raw_query: &str,
1734 name: &str,
1735 if_exists: bool,
1736 expected_model: CollectionModel,
1737 label: &str,
1738 ) -> RedDBResult<RuntimeQueryResult> {
1739 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1740 if is_system_schema_name(name) {
1741 return Err(RedDBError::Query("system schema is read-only".to_string()));
1742 }
1743 let store = self.inner.db.store();
1744 if store.get_collection(name).is_none() {
1745 if if_exists {
1746 return Ok(RuntimeQueryResult::ok_message(
1747 raw_query.to_string(),
1748 &format!("{label} '{name}' does not exist"),
1749 "drop",
1750 ));
1751 }
1752 return Err(RedDBError::NotFound(format!("{label} '{name}' not found")));
1753 }
1754
1755 let actual = self
1756 .inner
1757 .db
1758 .collection_contract(name)
1759 .map(|contract| contract.declared_model)
1760 .map(Ok)
1761 .unwrap_or_else(|| {
1762 polymorphic_resolver::resolve(name, &self.inner.db.catalog_model_snapshot())
1763 })?;
1764 polymorphic_resolver::ensure_model_match(expected_model, actual)?;
1765 self.drop_collection_storage(raw_query, name, label)
1766 }
1767
1768 pub fn execute_truncate(
1769 &self,
1770 raw_query: &str,
1771 query: &TruncateQuery,
1772 ) -> RedDBResult<RuntimeQueryResult> {
1773 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
1774 if is_system_schema_name(&query.name) {
1775 return Err(RedDBError::Query("system schema is read-only".to_string()));
1776 }
1777
1778 let label = query
1779 .model
1780 .map(polymorphic_resolver::model_name)
1781 .unwrap_or("collection");
1782 let store = self.inner.db.store();
1783 if store.get_collection(&query.name).is_none() {
1784 if query.if_exists {
1785 return Ok(RuntimeQueryResult::ok_message(
1786 raw_query.to_string(),
1787 &format!("{label} '{}' does not exist", query.name),
1788 "truncate",
1789 ));
1790 }
1791 return Err(RedDBError::NotFound(format!(
1792 "{label} '{}' not found",
1793 query.name
1794 )));
1795 }
1796
1797 let actual =
1798 polymorphic_resolver::resolve(&query.name, &self.inner.db.catalog_model_snapshot())?;
1799 if let Some(expected) = query.model {
1800 polymorphic_resolver::ensure_model_match(expected, actual)?;
1801 }
1802
1803 if actual == CollectionModel::Queue {
1804 return self.execute_queue_command(
1805 raw_query,
1806 &QueueCommand::Purge {
1807 queue: query.name.clone(),
1808 },
1809 );
1810 }
1811
1812 let affected = self.truncate_collection_entities(&query.name)?;
1814 crate::runtime::mutation::emit_truncate_event_for_collection(self, &query.name, affected)?;
1816 self.inner.db.invalidate_vector_index(&query.name);
1817 self.clear_table_planner_stats(&query.name);
1818 self.invalidate_result_cache();
1819
1820 Ok(RuntimeQueryResult::ok_message(
1821 raw_query.to_string(),
1822 &format!(
1823 "{affected} entities truncated from {label} '{}'",
1824 query.name
1825 ),
1826 "truncate",
1827 ))
1828 }
1829
1830 fn truncate_collection_entities(&self, name: &str) -> RedDBResult<u64> {
1831 let store = self.inner.db.store();
1832 let Some(manager) = store.get_collection(name) else {
1833 return Ok(0);
1834 };
1835 let entities = manager.query_all(|_| true);
1836 if entities.is_empty() {
1837 return Ok(0);
1838 }
1839
1840 for entity in &entities {
1841 let fields = entity_index_fields(&entity.data);
1842 self.inner
1843 .index_store
1844 .index_entity_delete(name, entity.id, &fields)
1845 .map_err(RedDBError::Internal)?;
1846 }
1847
1848 let ids = entities.iter().map(|entity| entity.id).collect::<Vec<_>>();
1849 let deleted_ids = store
1850 .delete_batch(name, &ids)
1851 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1852 for id in &deleted_ids {
1853 store.context_index().remove_entity(*id);
1854 }
1855 Ok(deleted_ids.len() as u64)
1856 }
1857
1858 fn drop_collection_storage(
1859 &self,
1860 raw_query: &str,
1861 name: &str,
1862 label: &str,
1863 ) -> RedDBResult<RuntimeQueryResult> {
1864 let store = self.inner.db.store();
1865
1866 let final_count = store
1869 .get_collection(name)
1870 .map(|manager| manager.query_all(|_| true).len() as u64)
1871 .unwrap_or(0);
1872 crate::runtime::mutation::emit_collection_dropped_event_for_collection(
1873 self,
1874 name,
1875 final_count,
1876 )?;
1877
1878 let orphaned_indices: Vec<String> = self
1879 .inner
1880 .index_store
1881 .list_indices(name)
1882 .into_iter()
1883 .map(|index| index.name)
1884 .collect();
1885 for index_name in &orphaned_indices {
1886 self.inner.index_store.drop_index(index_name, name);
1887 }
1888
1889 store
1890 .drop_collection(name)
1891 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1892 self.inner.db.invalidate_vector_index(name);
1893 self.inner.db.clear_collection_default_ttl_ms(name);
1894 self.inner
1895 .db
1896 .remove_collection_contract(name)
1897 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1898 self.clear_table_planner_stats(name);
1899 self.invalidate_result_cache();
1900 if let Some(store) = self.inner.auth_store.read().clone() {
1901 store.invalidate_visible_collections_cache();
1902 }
1903 self.inner
1904 .db
1905 .persist_metadata()
1906 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1907 self.schema_vocabulary_apply(
1908 crate::runtime::schema_vocabulary::DdlEvent::DropCollection {
1909 collection: name.to_string(),
1910 },
1911 );
1912
1913 Ok(RuntimeQueryResult::ok_message(
1914 raw_query.to_string(),
1915 &format!("{label} '{name}' dropped"),
1916 "drop",
1917 ))
1918 }
1919}
1920
1921pub(crate) fn is_system_schema_name(name: &str) -> bool {
1922 name == "red" || name.starts_with("red.") || name.starts_with("__red_schema_")
1923}
1924
1925fn entity_index_fields(data: &EntityData) -> Vec<(String, Value)> {
1926 match data {
1927 EntityData::Row(row) => {
1928 if let Some(ref named) = row.named {
1929 named
1930 .iter()
1931 .map(|(key, value)| (key.clone(), value.clone()))
1932 .collect()
1933 } else if let Some(ref schema) = row.schema {
1934 schema
1935 .iter()
1936 .zip(row.columns.iter())
1937 .map(|(key, value)| (key.clone(), value.clone()))
1938 .collect()
1939 } else {
1940 Vec::new()
1941 }
1942 }
1943 EntityData::Node(node) => node
1944 .properties
1945 .iter()
1946 .map(|(key, value)| (key.clone(), value.clone()))
1947 .collect(),
1948 _ => Vec::new(),
1949 }
1950}
1951
1952fn validate_ai_policy_modalities(policy: &crate::catalog::AiPolicy) -> RedDBResult<()> {
1958 use crate::runtime::ai::provider_capabilities::{Modality, Registry};
1959 let registry = Registry::new();
1960 let check = |provider: &str, model: &str, modality: Modality| -> RedDBResult<()> {
1961 registry
1962 .validate_policy_modality(provider, model, modality)
1963 .map_err(|err| RedDBError::Query(err.to_string()))
1964 };
1965 if let Some(embed) = &policy.embed {
1966 check(&embed.provider, &embed.model, Modality::Embed)?;
1967 }
1968 if let Some(moderate) = &policy.moderate {
1969 check(&moderate.provider, &moderate.model, Modality::Moderate)?;
1970 }
1971 if let Some(vision) = &policy.vision {
1972 check(&vision.provider, &vision.model, Modality::Vision)?;
1973 }
1974 Ok(())
1975}
1976
1977fn collection_contract_from_create_table(
1978 query: &CreateTableQuery,
1979) -> RedDBResult<crate::physical::CollectionContract> {
1980 let now = current_unix_ms();
1981 let mut declared_columns: Vec<crate::physical::DeclaredColumnContract> = query
1982 .columns
1983 .iter()
1984 .map(declared_column_contract_from_ddl)
1985 .collect();
1986 if query.timestamps {
1987 declared_columns.push(crate::physical::DeclaredColumnContract {
1991 name: "created_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 declared_columns.push(crate::physical::DeclaredColumnContract {
2004 name: "updated_at".to_string(),
2005 data_type: "BIGINT".to_string(),
2006 sql_type: Some(crate::storage::schema::SqlTypeName::simple("BIGINT")),
2007 not_null: true,
2008 default: None,
2009 compress: None,
2010 unique: false,
2011 primary_key: false,
2012 enum_variants: Vec::new(),
2013 array_element: None,
2014 decimal_precision: None,
2015 });
2016 }
2017 Ok(crate::physical::CollectionContract {
2018 name: query.name.clone(),
2019 declared_model: crate::catalog::CollectionModel::Table,
2020 schema_mode: crate::catalog::SchemaMode::SemiStructured,
2021 origin: crate::physical::ContractOrigin::Explicit,
2022 version: 1,
2023 created_at_unix_ms: now,
2024 updated_at_unix_ms: now,
2025 default_ttl_ms: query.default_ttl_ms,
2026 vector_dimension: None,
2027 vector_metric: None,
2028 context_index_fields: query.context_index_fields.clone(),
2029 declared_columns,
2030 table_def: Some(build_table_def_from_create_table(query)?),
2031 timestamps_enabled: query.timestamps,
2032 context_index_enabled: query.context_index_enabled
2033 || !query.context_index_fields.is_empty(),
2034 metrics_raw_retention_ms: None,
2035 metrics_rollup_policies: Vec::new(),
2036 metrics_tenant_identity: None,
2037 metrics_namespace: None,
2038 append_only: query.append_only,
2039 subscriptions: query.subscriptions.clone(),
2040 analytics_config: Vec::new(),
2041 session_key: None,
2042 session_gap_ms: None,
2043 retention_duration_ms: None,
2044 analytical_storage: None,
2045 ai_policy: query.ai_policy.clone(),
2046 })
2047}
2048
2049fn default_collection_contract_for_existing_table(
2050 name: &str,
2051) -> crate::physical::CollectionContract {
2052 let now = current_unix_ms();
2053 crate::physical::CollectionContract {
2054 name: name.to_string(),
2055 declared_model: crate::catalog::CollectionModel::Table,
2056 schema_mode: crate::catalog::SchemaMode::SemiStructured,
2057 origin: crate::physical::ContractOrigin::Explicit,
2058 version: 0,
2059 created_at_unix_ms: now,
2060 updated_at_unix_ms: now,
2061 default_ttl_ms: None,
2062 vector_dimension: None,
2063 vector_metric: None,
2064 context_index_fields: Vec::new(),
2065 declared_columns: Vec::new(),
2066 table_def: Some(crate::storage::schema::TableDef::new(name.to_string())),
2067 timestamps_enabled: false,
2068 context_index_enabled: false,
2069 metrics_raw_retention_ms: None,
2070 metrics_rollup_policies: Vec::new(),
2071 metrics_tenant_identity: None,
2072 metrics_namespace: None,
2073 append_only: false,
2074 subscriptions: Vec::new(),
2075 analytics_config: Vec::new(),
2076 session_key: None,
2077 session_gap_ms: None,
2078 retention_duration_ms: None,
2079 analytical_storage: None,
2080
2081 ai_policy: None,
2082 }
2083}
2084
2085fn keyed_collection_contract(
2086 name: &str,
2087 model: crate::catalog::CollectionModel,
2088 analytics_config: Vec<crate::catalog::AnalyticsViewDescriptor>,
2089) -> crate::physical::CollectionContract {
2090 let now = current_unix_ms();
2091 crate::physical::CollectionContract {
2092 name: name.to_string(),
2093 declared_model: model,
2094 schema_mode: crate::catalog::SchemaMode::Dynamic,
2095 origin: crate::physical::ContractOrigin::Explicit,
2096 version: 1,
2097 created_at_unix_ms: now,
2098 updated_at_unix_ms: now,
2099 default_ttl_ms: None,
2100 vector_dimension: None,
2101 vector_metric: None,
2102 context_index_fields: Vec::new(),
2103 declared_columns: Vec::new(),
2104 table_def: None,
2105 timestamps_enabled: false,
2106 context_index_enabled: false,
2107 metrics_raw_retention_ms: None,
2108 metrics_rollup_policies: Vec::new(),
2109 metrics_tenant_identity: None,
2110 metrics_namespace: None,
2111 append_only: false,
2112 subscriptions: Vec::new(),
2113 analytics_config,
2114 session_key: None,
2115 session_gap_ms: None,
2116 retention_duration_ms: None,
2117 analytical_storage: None,
2118
2119 ai_policy: None,
2120 }
2121}
2122
2123fn metrics_collection_contract(query: &CreateTableQuery) -> crate::physical::CollectionContract {
2124 let now = current_unix_ms();
2125 crate::physical::CollectionContract {
2126 name: query.name.clone(),
2127 declared_model: crate::catalog::CollectionModel::Metrics,
2128 schema_mode: crate::catalog::SchemaMode::SemiStructured,
2129 origin: crate::physical::ContractOrigin::Explicit,
2130 version: 1,
2131 created_at_unix_ms: now,
2132 updated_at_unix_ms: now,
2133 default_ttl_ms: query.default_ttl_ms,
2134 vector_dimension: None,
2135 vector_metric: None,
2136 context_index_fields: Vec::new(),
2137 declared_columns: Vec::new(),
2138 table_def: None,
2139 timestamps_enabled: false,
2140 context_index_enabled: false,
2141 metrics_raw_retention_ms: query.default_ttl_ms,
2142 metrics_rollup_policies: query.metrics_rollup_policies.clone(),
2143 metrics_tenant_identity: Some(
2144 query
2145 .tenant_by
2146 .clone()
2147 .unwrap_or_else(|| "current_tenant".to_string()),
2148 ),
2149 metrics_namespace: Some("default".to_string()),
2150 append_only: true,
2151 subscriptions: Vec::new(),
2152 analytics_config: Vec::new(),
2153 session_key: None,
2154 session_gap_ms: None,
2155 retention_duration_ms: None,
2156 analytical_storage: None,
2157
2158 ai_policy: None,
2159 }
2160}
2161
2162fn vector_collection_contract(query: &CreateVectorQuery) -> crate::physical::CollectionContract {
2163 let now = current_unix_ms();
2164 crate::physical::CollectionContract {
2165 name: query.name.clone(),
2166 declared_model: crate::catalog::CollectionModel::Vector,
2167 schema_mode: crate::catalog::SchemaMode::Dynamic,
2168 origin: crate::physical::ContractOrigin::Explicit,
2169 version: 1,
2170 created_at_unix_ms: now,
2171 updated_at_unix_ms: now,
2172 default_ttl_ms: None,
2173 vector_dimension: Some(query.dimension),
2174 vector_metric: Some(query.metric),
2175 context_index_fields: Vec::new(),
2176 declared_columns: Vec::new(),
2177 table_def: None,
2178 timestamps_enabled: false,
2179 context_index_enabled: false,
2180 metrics_raw_retention_ms: None,
2181 metrics_rollup_policies: Vec::new(),
2182 metrics_tenant_identity: None,
2183 metrics_namespace: None,
2184 append_only: false,
2185 subscriptions: Vec::new(),
2186 analytics_config: Vec::new(),
2187 session_key: None,
2188 session_gap_ms: None,
2189 retention_duration_ms: None,
2190 analytical_storage: None,
2191
2192 ai_policy: None,
2193 }
2194}
2195
2196fn declared_column_contract_from_ddl(
2197 column: &CreateColumnDef,
2198) -> crate::physical::DeclaredColumnContract {
2199 crate::physical::DeclaredColumnContract {
2200 name: column.name.clone(),
2201 data_type: column.data_type.clone(),
2202 sql_type: Some(column.sql_type.clone()),
2203 not_null: column.not_null,
2204 default: column.default.clone(),
2205 compress: column.compress,
2206 unique: column.unique,
2207 primary_key: column.primary_key,
2208 enum_variants: column.enum_variants.clone(),
2209 array_element: column.array_element.clone(),
2210 decimal_precision: column.decimal_precision,
2211 }
2212}
2213
2214fn apply_alter_operations_to_contract(
2215 contract: &mut crate::physical::CollectionContract,
2216 operations: &[AlterOperation],
2217) {
2218 if contract.table_def.is_none() {
2219 contract.table_def = Some(crate::storage::schema::TableDef::new(contract.name.clone()));
2220 }
2221 for operation in operations {
2222 match operation {
2223 AlterOperation::AddColumn(column) => {
2224 if !contract
2225 .declared_columns
2226 .iter()
2227 .any(|existing| existing.name == column.name)
2228 {
2229 contract
2230 .declared_columns
2231 .push(declared_column_contract_from_ddl(column));
2232 }
2233 if let Some(table_def) = contract.table_def.as_mut() {
2234 if table_def.get_column(&column.name).is_none() {
2235 if let Ok(column_def) = column_def_from_ddl(column) {
2236 if column.primary_key {
2237 table_def.primary_key.push(column.name.clone());
2238 table_def.constraints.push(
2239 crate::storage::schema::Constraint::new(
2240 format!("pk_{}", column.name),
2241 crate::storage::schema::ConstraintType::PrimaryKey,
2242 )
2243 .on_columns(vec![column.name.clone()]),
2244 );
2245 }
2246 if column.unique {
2247 table_def.constraints.push(
2248 crate::storage::schema::Constraint::new(
2249 format!("uniq_{}", column.name),
2250 crate::storage::schema::ConstraintType::Unique,
2251 )
2252 .on_columns(vec![column.name.clone()]),
2253 );
2254 }
2255 if column.not_null {
2256 table_def.constraints.push(
2257 crate::storage::schema::Constraint::new(
2258 format!("not_null_{}", column.name),
2259 crate::storage::schema::ConstraintType::NotNull,
2260 )
2261 .on_columns(vec![column.name.clone()]),
2262 );
2263 }
2264 table_def.columns.push(column_def);
2265 }
2266 }
2267 }
2268 }
2269 AlterOperation::DropColumn(name) => {
2270 contract
2271 .declared_columns
2272 .retain(|column| column.name != *name);
2273 if let Some(table_def) = contract.table_def.as_mut() {
2274 if let Some(index) = table_def.column_index(name) {
2275 table_def.columns.remove(index);
2276 }
2277 table_def.primary_key.retain(|column| column != name);
2278 table_def.constraints.retain(|constraint| {
2279 !constraint.columns.iter().any(|column| column == name)
2280 });
2281 table_def
2282 .indexes
2283 .retain(|index| !index.columns.iter().any(|column| column == name));
2284 }
2285 }
2286 AlterOperation::RenameColumn { from, to } => {
2287 if contract
2288 .declared_columns
2289 .iter()
2290 .any(|column| column.name == *to)
2291 {
2292 continue;
2293 }
2294 if let Some(column) = contract
2295 .declared_columns
2296 .iter_mut()
2297 .find(|column| column.name == *from)
2298 {
2299 column.name = to.clone();
2300 }
2301 if let Some(table_def) = contract.table_def.as_mut() {
2302 if let Some(column) = table_def
2303 .columns
2304 .iter_mut()
2305 .find(|column| column.name == *from)
2306 {
2307 column.name = to.clone();
2308 }
2309 for primary_key in &mut table_def.primary_key {
2310 if *primary_key == *from {
2311 *primary_key = to.clone();
2312 }
2313 }
2314 for constraint in &mut table_def.constraints {
2315 for column in &mut constraint.columns {
2316 if *column == *from {
2317 *column = to.clone();
2318 }
2319 }
2320 if let Some(ref_columns) = constraint.ref_columns.as_mut() {
2321 for column in ref_columns {
2322 if *column == *from {
2323 *column = to.clone();
2324 }
2325 }
2326 }
2327 }
2328 for index in &mut table_def.indexes {
2329 for column in &mut index.columns {
2330 if *column == *from {
2331 *column = to.clone();
2332 }
2333 }
2334 }
2335 }
2336 }
2337 AlterOperation::AttachPartition { .. } | AlterOperation::DetachPartition { .. } => {}
2340 AlterOperation::EnableRowLevelSecurity | AlterOperation::DisableRowLevelSecurity => {}
2344 AlterOperation::EnableTenancy { .. } | AlterOperation::DisableTenancy => {}
2347 AlterOperation::SetAppendOnly(on) => {
2348 contract.append_only = *on;
2349 }
2350 AlterOperation::SetVersioned(_) => {}
2353 AlterOperation::EnableEvents(subscription) => {
2354 let mut subscription = subscription.clone();
2355 subscription.source = contract.name.clone();
2356 subscription.enabled = true;
2357 if let Some(existing) = contract
2358 .subscriptions
2359 .iter_mut()
2360 .find(|existing| existing.target_queue == subscription.target_queue)
2361 {
2362 *existing = subscription;
2363 } else {
2364 contract.subscriptions.push(subscription);
2365 }
2366 }
2367 AlterOperation::DisableEvents => {
2368 for subscription in &mut contract.subscriptions {
2369 subscription.enabled = false;
2370 }
2371 }
2372 AlterOperation::AddSubscription { name, descriptor } => {
2373 let mut sub = descriptor.clone();
2374 sub.name = name.clone();
2375 sub.source = contract.name.clone();
2376 sub.enabled = true;
2377 if let Some(existing) = contract.subscriptions.iter_mut().find(|s| s.name == *name)
2378 {
2379 *existing = sub;
2380 } else {
2381 contract.subscriptions.push(sub);
2382 }
2383 }
2384 AlterOperation::DropSubscription { name } => {
2385 contract.subscriptions.retain(|s| s.name != *name);
2386 }
2387 AlterOperation::AddSigner { .. } | AlterOperation::RevokeSigner { .. } => {}
2392 AlterOperation::SetRetention { duration_ms } => {
2393 contract.retention_duration_ms = Some(*duration_ms);
2394 }
2395 AlterOperation::UnsetRetention => {
2396 contract.retention_duration_ms = None;
2397 }
2398 AlterOperation::AddAnalytics(views) => {
2402 for view in views {
2403 if !contract
2407 .analytics_config
2408 .iter()
2409 .any(|existing| existing.output == view.output)
2410 {
2411 contract.analytics_config.push(view.clone());
2412 }
2413 }
2414 }
2415 AlterOperation::DropAnalytics(output) => {
2416 contract
2417 .analytics_config
2418 .retain(|view| view.output != *output);
2419 }
2420 }
2421 }
2422}
2423
2424pub(crate) fn retention_timestamp_column_exists(
2429 contract: &crate::physical::CollectionContract,
2430) -> bool {
2431 if contract.timestamps_enabled {
2432 return true;
2433 }
2434 if matches!(
2435 contract.declared_model,
2436 crate::catalog::CollectionModel::TimeSeries | crate::catalog::CollectionModel::Metrics
2437 ) {
2438 return true;
2442 }
2443 contract
2444 .declared_columns
2445 .iter()
2446 .any(|column| is_temporal_data_type(&column.data_type))
2447}
2448
2449fn is_temporal_data_type(data_type: &str) -> bool {
2450 let upper = data_type.to_ascii_uppercase();
2451 matches!(
2452 upper.as_str(),
2453 "TIMESTAMP" | "TIMESTAMPMS" | "TIMESTAMP_MS" | "DATETIME" | "DATE"
2454 )
2455}
2456
2457fn validate_event_subscriptions(
2458 runtime: &RedDBRuntime,
2459 source: &str,
2460 subscriptions: &[crate::catalog::SubscriptionDescriptor],
2461) -> RedDBResult<()> {
2462 for subscription in subscriptions
2463 .iter()
2464 .filter(|subscription| subscription.enabled)
2465 {
2466 if subscription.all_tenants && crate::runtime::impl_core::current_tenant().is_some() {
2467 return Err(RedDBError::Query(
2468 "cross-tenant subscription requires cluster-admin capability (events:cluster_subscribe)".to_string(),
2469 ));
2470 }
2471 validate_subscription_auth(runtime, source, subscription)?;
2472 if subscription.target_queue == source
2473 || subscription_would_create_cycle(
2474 &runtime.inner.db,
2475 source,
2476 &subscription.target_queue,
2477 )
2478 {
2479 return Err(RedDBError::Query(
2480 "subscription would create cycle".to_string(),
2481 ));
2482 }
2483 audit_subscription_redact_gap(runtime, source, subscription);
2484 }
2485 Ok(())
2486}
2487
2488fn validate_subscription_auth(
2489 runtime: &RedDBRuntime,
2490 source: &str,
2491 subscription: &crate::catalog::SubscriptionDescriptor,
2492) -> RedDBResult<()> {
2493 let auth_store = match runtime.inner.auth_store.read().clone() {
2494 Some(store) => store,
2495 None => return Ok(()),
2496 };
2497 let (username, role) = match crate::runtime::impl_core::current_auth_identity() {
2498 Some(identity) => identity,
2499 None => return Ok(()),
2500 };
2501 let tenant = crate::runtime::impl_core::current_tenant();
2502 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
2503
2504 if auth_store.iam_authorization_enabled() {
2505 let ctx = crate::auth::policies::EvalContext {
2506 principal_tenant: tenant.clone(),
2507 current_tenant: tenant.clone(),
2508 peer_ip: None,
2509 mfa_present: false,
2510 now_ms: crate::auth::now_ms(),
2511 principal_is_admin_role: role == crate::auth::Role::Admin,
2512 principal_is_platform_scoped: principal.tenant.is_none(),
2513 };
2514 let mut source_resource = crate::auth::policies::ResourceRef::new("table", source);
2515 if let Some(t) = tenant.as_deref() {
2516 source_resource = source_resource.with_tenant(t.to_string());
2517 }
2518 if !auth_store.check_policy_authz_with_role(
2519 &principal,
2520 "select",
2521 &source_resource,
2522 &ctx,
2523 role,
2524 ) {
2525 return Err(RedDBError::Query(format!(
2526 "permission denied: principal=`{}` action=`select` resource=`{}:{}` denied by IAM policy",
2527 principal, source_resource.kind, source_resource.name
2528 )));
2529 }
2530
2531 let mut target_resource =
2532 crate::auth::policies::ResourceRef::new("queue", subscription.target_queue.clone());
2533 if let Some(t) = tenant.as_deref() {
2534 target_resource = target_resource.with_tenant(t.to_string());
2535 }
2536 if !auth_store.check_policy_authz_with_role(
2537 &principal,
2538 "write",
2539 &target_resource,
2540 &ctx,
2541 role,
2542 ) {
2543 return Err(RedDBError::Query(format!(
2544 "permission denied: principal=`{}` action=`write` resource=`{}:{}` denied by IAM policy",
2545 principal, target_resource.kind, target_resource.name
2546 )));
2547 }
2548 return Ok(());
2549 }
2550
2551 let ctx = crate::auth::privileges::AuthzContext {
2552 principal: &username,
2553 effective_role: role,
2554 tenant: tenant.as_deref(),
2555 };
2556 auth_store
2557 .check_grant(
2558 &ctx,
2559 crate::auth::privileges::Action::Select,
2560 &crate::auth::privileges::Resource::table_from_name(source),
2561 )
2562 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))?;
2563 auth_store
2564 .check_grant(
2565 &ctx,
2566 crate::auth::privileges::Action::Insert,
2567 &crate::auth::privileges::Resource::table_from_name(&subscription.target_queue),
2568 )
2569 .map_err(|err| RedDBError::Query(format!("permission denied: {err}")))?;
2570 Ok(())
2571}
2572
2573fn audit_subscription_redact_gap(
2574 runtime: &RedDBRuntime,
2575 source: &str,
2576 subscription: &crate::catalog::SubscriptionDescriptor,
2577) {
2578 let auth_store = match runtime.inner.auth_store.read().clone() {
2579 Some(store) if store.iam_authorization_enabled() => store,
2580 _ => return,
2581 };
2582 let (username, role) = match crate::runtime::impl_core::current_auth_identity() {
2583 Some(identity) => identity,
2584 None => return,
2585 };
2586 let tenant = crate::runtime::impl_core::current_tenant();
2587 let principal = crate::auth::UserId::from_parts(tenant.as_deref(), &username);
2588 let missing = subscription_redact_gap_columns(&auth_store, &principal, source, subscription);
2589 if missing.is_empty() {
2590 return;
2591 }
2592
2593 let columns = missing.into_iter().collect::<Vec<_>>().join(", ");
2594 tracing::warn!(
2595 target: "reddb::operator",
2596 "subscription_redact_gap: source={} target_queue={} columns=[{}]",
2597 source,
2598 subscription.target_queue,
2599 columns
2600 );
2601 let mut event = AuditEvent::builder("subscription_redact_gap")
2602 .principal(username)
2603 .source(AuditAuthSource::System)
2604 .resource(format!(
2605 "subscription:{}->{}",
2606 source, subscription.target_queue
2607 ))
2608 .outcome(Outcome::Success)
2609 .field(AuditFieldEscaper::field("source", source))
2610 .field(AuditFieldEscaper::field(
2611 "target_queue",
2612 subscription.target_queue.clone(),
2613 ))
2614 .field(AuditFieldEscaper::field(
2615 "subscription",
2616 subscription.name.clone(),
2617 ))
2618 .field(AuditFieldEscaper::field("columns", columns))
2619 .field(AuditFieldEscaper::field("role", role.as_str()));
2620 if let Some(t) = tenant {
2621 event = event.tenant(t);
2622 }
2623 runtime.inner.audit_log.record_event(event.build());
2624}
2625
2626fn subscription_redact_gap_columns(
2627 auth_store: &crate::auth::store::AuthStore,
2628 principal: &crate::auth::UserId,
2629 source: &str,
2630 subscription: &crate::catalog::SubscriptionDescriptor,
2631) -> BTreeSet<String> {
2632 let redacted: HashSet<String> = subscription
2633 .redact_fields
2634 .iter()
2635 .map(|field| field.to_ascii_lowercase())
2636 .collect();
2637 auth_store
2638 .effective_policies(principal)
2639 .iter()
2640 .flat_map(|policy| policy.statements.iter())
2641 .filter(|statement| statement.effect == crate::auth::policies::Effect::Deny)
2642 .filter(|statement| statement.actions.iter().any(action_pattern_matches_select))
2643 .flat_map(|statement| statement.resources.iter())
2644 .filter_map(|resource| denied_column_for_source(resource, source))
2645 .filter(|column| !redact_covers_column(&redacted, source, column))
2646 .collect()
2647}
2648
2649fn action_pattern_matches_select(pattern: &crate::auth::policies::ActionPattern) -> bool {
2650 match pattern {
2651 crate::auth::policies::ActionPattern::Wildcard => true,
2652 crate::auth::policies::ActionPattern::Exact(action) => action == "select",
2653 crate::auth::policies::ActionPattern::Prefix(prefix) => {
2654 "select".len() > prefix.len() + 1
2655 && "select".starts_with(prefix)
2656 && "select".as_bytes()[prefix.len()] == b':'
2657 }
2658 }
2659}
2660
2661fn denied_column_for_source(
2662 resource: &crate::auth::policies::ResourcePattern,
2663 source: &str,
2664) -> Option<String> {
2665 let crate::auth::policies::ResourcePattern::Exact { kind, name } = resource else {
2666 return None;
2667 };
2668 if kind != "column" {
2669 return None;
2670 }
2671 let column = crate::auth::ColumnRef::parse_resource_name(name).ok()?;
2672 (column.table_resource_name() == source).then_some(column.column)
2673}
2674
2675fn redact_covers_column(redacted: &HashSet<String>, source: &str, column: &str) -> bool {
2676 let column = column.to_ascii_lowercase();
2677 let qualified = format!("{}.{}", source.to_ascii_lowercase(), column);
2678 redacted.contains("*") || redacted.contains(&column) || redacted.contains(&qualified)
2679}
2680
2681fn subscription_would_create_cycle(
2682 db: &crate::storage::unified::devx::RedDB,
2683 source: &str,
2684 target: &str,
2685) -> bool {
2686 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
2687 for contract in db.collection_contracts() {
2688 for subscription in contract
2689 .subscriptions
2690 .into_iter()
2691 .filter(|subscription| subscription.enabled)
2692 {
2693 graph
2694 .entry(subscription.source)
2695 .or_default()
2696 .push(subscription.target_queue);
2697 }
2698 }
2699 graph
2700 .entry(source.to_string())
2701 .or_default()
2702 .push(target.to_string());
2703
2704 let mut stack = vec![target.to_string()];
2705 let mut seen = HashSet::new();
2706 while let Some(node) = stack.pop() {
2707 if node == source {
2708 return true;
2709 }
2710 if !seen.insert(node.clone()) {
2711 continue;
2712 }
2713 if let Some(next) = graph.get(&node) {
2714 stack.extend(next.iter().cloned());
2715 }
2716 }
2717 false
2718}
2719
2720pub(crate) fn ensure_event_target_queue_pub(
2721 runtime: &RedDBRuntime,
2722 queue: &str,
2723) -> RedDBResult<()> {
2724 ensure_event_target_queue(runtime, queue)
2725}
2726
2727fn ensure_event_target_queue(runtime: &RedDBRuntime, queue: &str) -> RedDBResult<()> {
2728 let store = runtime.inner.db.store();
2729 if store.get_collection(queue).is_some() {
2730 return Ok(());
2731 }
2732 store
2733 .create_collection(queue)
2734 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2735 runtime
2736 .inner
2737 .db
2738 .save_collection_contract(event_queue_collection_contract(queue))
2739 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2740 store.set_config_tree(
2741 &format!("queue.{queue}.mode"),
2742 &crate::serde_json::Value::String("fanout".to_string()),
2743 );
2744 Ok(())
2745}
2746
2747fn event_queue_collection_contract(queue: &str) -> crate::physical::CollectionContract {
2748 let now = current_unix_ms();
2749 crate::physical::CollectionContract {
2750 name: queue.to_string(),
2751 declared_model: crate::catalog::CollectionModel::Queue,
2752 schema_mode: crate::catalog::SchemaMode::Dynamic,
2753 origin: crate::physical::ContractOrigin::Implicit,
2754 version: 1,
2755 created_at_unix_ms: now,
2756 updated_at_unix_ms: now,
2757 default_ttl_ms: None,
2758 vector_dimension: None,
2759 vector_metric: None,
2760 context_index_fields: Vec::new(),
2761 declared_columns: Vec::new(),
2762 table_def: None,
2763 timestamps_enabled: false,
2764 context_index_enabled: false,
2765 metrics_raw_retention_ms: None,
2766 metrics_rollup_policies: Vec::new(),
2767 metrics_tenant_identity: None,
2768 metrics_namespace: None,
2769 append_only: true,
2770 subscriptions: Vec::new(),
2771 analytics_config: Vec::new(),
2772 session_key: None,
2773 session_gap_ms: None,
2774 retention_duration_ms: None,
2775 analytical_storage: None,
2776
2777 ai_policy: None,
2778 }
2779}
2780
2781fn build_table_def_from_create_table(
2782 query: &CreateTableQuery,
2783) -> RedDBResult<crate::storage::schema::TableDef> {
2784 let mut table = crate::storage::schema::TableDef::new(query.name.clone());
2785 for column in &query.columns {
2786 if column.primary_key {
2787 table.primary_key.push(column.name.clone());
2788 table.constraints.push(
2789 crate::storage::schema::Constraint::new(
2790 format!("pk_{}", column.name),
2791 crate::storage::schema::ConstraintType::PrimaryKey,
2792 )
2793 .on_columns(vec![column.name.clone()]),
2794 );
2795 }
2796 if column.unique {
2797 table.constraints.push(
2798 crate::storage::schema::Constraint::new(
2799 format!("uniq_{}", column.name),
2800 crate::storage::schema::ConstraintType::Unique,
2801 )
2802 .on_columns(vec![column.name.clone()]),
2803 );
2804 }
2805 if column.not_null {
2806 table.constraints.push(
2807 crate::storage::schema::Constraint::new(
2808 format!("not_null_{}", column.name),
2809 crate::storage::schema::ConstraintType::NotNull,
2810 )
2811 .on_columns(vec![column.name.clone()]),
2812 );
2813 }
2814 table.columns.push(column_def_from_ddl(column)?);
2815 }
2816 if query.timestamps {
2821 table.columns.push(
2822 crate::storage::schema::ColumnDef::new(
2823 "created_at".to_string(),
2824 crate::storage::schema::DataType::UnsignedInteger,
2825 )
2826 .not_null(),
2827 );
2828 table.columns.push(
2829 crate::storage::schema::ColumnDef::new(
2830 "updated_at".to_string(),
2831 crate::storage::schema::DataType::UnsignedInteger,
2832 )
2833 .not_null(),
2834 );
2835 table.constraints.push(
2836 crate::storage::schema::Constraint::new(
2837 "not_null_created_at".to_string(),
2838 crate::storage::schema::ConstraintType::NotNull,
2839 )
2840 .on_columns(vec!["created_at".to_string()]),
2841 );
2842 table.constraints.push(
2843 crate::storage::schema::Constraint::new(
2844 "not_null_updated_at".to_string(),
2845 crate::storage::schema::ConstraintType::NotNull,
2846 )
2847 .on_columns(vec!["updated_at".to_string()]),
2848 );
2849 }
2850 table
2851 .validate()
2852 .map_err(|err| RedDBError::Query(format!("invalid table definition: {err}")))?;
2853 Ok(table)
2854}
2855
2856fn column_def_from_ddl(column: &CreateColumnDef) -> RedDBResult<crate::storage::schema::ColumnDef> {
2857 let data_type = resolve_declared_data_type(&column.data_type)
2858 .map_err(|err| RedDBError::Query(err.to_string()))?;
2859 let mut column_def = crate::storage::schema::ColumnDef::new(column.name.clone(), data_type);
2860 if column.not_null {
2861 column_def = column_def.not_null();
2862 }
2863 if let Some(default) = &column.default {
2864 column_def = column_def.with_default(default.as_bytes().to_vec());
2865 }
2866 if column.compress.unwrap_or(0) > 0 {
2867 column_def = column_def.compressed();
2868 }
2869 if !column.enum_variants.is_empty() {
2870 column_def = column_def.with_variants(column.enum_variants.clone());
2871 }
2872 if let Some(precision) = column.decimal_precision {
2873 column_def = column_def.with_precision(precision);
2874 }
2875 if let Some(element_type) = &column.array_element {
2876 column_def = column_def.with_element_type(
2877 resolve_declared_data_type(element_type)
2878 .map_err(|err| RedDBError::Query(err.to_string()))?,
2879 );
2880 }
2881 column_def = column_def.with_metadata("ddl_data_type", column.data_type.clone());
2882 if column.unique {
2883 column_def = column_def.with_metadata("unique", "true");
2884 }
2885 if column.primary_key {
2886 column_def = column_def.with_metadata("primary_key", "true");
2887 }
2888 Ok(column_def)
2889}
2890
2891fn current_unix_ms() -> u128 {
2892 std::time::SystemTime::now()
2893 .duration_since(std::time::UNIX_EPOCH)
2894 .unwrap_or_default()
2895 .as_millis()
2896}
2897
2898#[cfg(test)]
2899mod tests {
2900 use crate::auth::policies::{ActionPattern, Effect, Policy, ResourcePattern, Statement};
2901 use crate::auth::store::{AuthStore, PrincipalRef};
2902 use crate::auth::UserId;
2903 use crate::auth::{AuthConfig, Role};
2904 use crate::runtime::impl_core::{clear_current_auth_identity, set_current_auth_identity};
2905 use crate::storage::schema::Value;
2906 use crate::{RedDBOptions, RedDBRuntime};
2907 use std::sync::Arc;
2908
2909 fn make_allow_policy(id: &str, action: &str, collection: &str) -> Policy {
2910 Policy {
2911 id: id.to_string(),
2912 version: 1,
2913 tenant: None,
2914 created_at: 0,
2915 updated_at: 0,
2916 statements: vec![Statement {
2917 sid: None,
2918 effect: Effect::Allow,
2919 actions: vec![ActionPattern::Exact(action.to_string())],
2920 resources: vec![ResourcePattern::Exact {
2921 kind: "collection".to_string(),
2922 name: collection.to_string(),
2923 }],
2924 condition: None,
2925 }],
2926 }
2927 }
2928
2929 fn wire_auth_store(rt: &RedDBRuntime) -> Arc<AuthStore> {
2930 let store = Arc::new(AuthStore::new(AuthConfig::default()));
2931 *rt.inner.auth_store.write() = Some(store.clone());
2932 store
2933 }
2934
2935 #[test]
2936 fn drop_denied_without_iam_policy() {
2937 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2938 rt.execute_query("CREATE TABLE foo (id INT)").unwrap();
2939 let store = wire_auth_store(&rt);
2940 let select_only = Policy {
2942 id: "select-only".to_string(),
2943 version: 1,
2944 tenant: None,
2945 created_at: 0,
2946 updated_at: 0,
2947 statements: vec![Statement {
2948 sid: None,
2949 effect: Effect::Allow,
2950 actions: vec![ActionPattern::Exact("select".to_string())],
2951 resources: vec![ResourcePattern::Wildcard],
2952 condition: None,
2953 }],
2954 };
2955 store.put_policy_internal(select_only).unwrap();
2956 let alice = UserId::from_parts(None, "alice");
2957 store
2958 .attach_policy(PrincipalRef::User(alice), "select-only")
2959 .unwrap();
2960 set_current_auth_identity("alice".to_string(), Role::Write);
2961 let err = rt.execute_query("DROP TABLE foo").unwrap_err();
2962 clear_current_auth_identity();
2963 assert!(
2964 format!("{err}").contains("denied by IAM policy"),
2965 "got: {err}"
2966 );
2967 }
2968
2969 #[test]
2970 fn drop_allowed_with_explicit_iam_policy() {
2971 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2972 rt.execute_query("CREATE TABLE bar (id INT)").unwrap();
2973 let store = wire_auth_store(&rt);
2974 let policy = make_allow_policy("allow-drop-bar", "drop", "bar");
2975 store.put_policy_internal(policy).unwrap();
2976 let bob = UserId::from_parts(None, "bob");
2977 store
2978 .attach_policy(PrincipalRef::User(bob), "allow-drop-bar")
2979 .unwrap();
2980 set_current_auth_identity("bob".to_string(), Role::Write);
2981 rt.execute_query("DROP TABLE bar").unwrap();
2982 clear_current_auth_identity();
2983 }
2984
2985 #[test]
2986 fn drop_allowed_with_wildcard_iam_policy() {
2987 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
2988 rt.execute_query("CREATE TABLE baz (id INT)").unwrap();
2989 let store = wire_auth_store(&rt);
2990 let policy = Policy {
2991 id: "allow-drop-all".to_string(),
2992 version: 1,
2993 tenant: None,
2994 created_at: 0,
2995 updated_at: 0,
2996 statements: vec![Statement {
2997 sid: None,
2998 effect: Effect::Allow,
2999 actions: vec![ActionPattern::Exact("drop".to_string())],
3000 resources: vec![ResourcePattern::Wildcard],
3001 condition: None,
3002 }],
3003 };
3004 store.put_policy_internal(policy).unwrap();
3005 let carl = UserId::from_parts(None, "carl");
3006 store
3007 .attach_policy(PrincipalRef::User(carl), "allow-drop-all")
3008 .unwrap();
3009 set_current_auth_identity("carl".to_string(), Role::Write);
3010 rt.execute_query("DROP TABLE baz").unwrap();
3011 clear_current_auth_identity();
3012 }
3013
3014 #[test]
3015 fn truncate_denied_without_iam_policy() {
3016 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3017 rt.execute_query("CREATE TABLE qux (id INT)").unwrap();
3018 let store = wire_auth_store(&rt);
3019 store
3026 .set_enforcement_mode(crate::auth::enforcement_mode::PolicyEnforcementMode::PolicyOnly);
3027 let select_only = Policy {
3029 id: "select-only-2".to_string(),
3030 version: 1,
3031 tenant: None,
3032 created_at: 0,
3033 updated_at: 0,
3034 statements: vec![Statement {
3035 sid: None,
3036 effect: Effect::Allow,
3037 actions: vec![ActionPattern::Exact("select".to_string())],
3038 resources: vec![ResourcePattern::Wildcard],
3039 condition: None,
3040 }],
3041 };
3042 store.put_policy_internal(select_only).unwrap();
3043 let dana = UserId::from_parts(None, "dana");
3044 store
3045 .attach_policy(PrincipalRef::User(dana), "select-only-2")
3046 .unwrap();
3047 set_current_auth_identity("dana".to_string(), Role::Write);
3048 let err = rt.execute_query("TRUNCATE TABLE qux").unwrap_err();
3049 clear_current_auth_identity();
3050 assert!(
3051 format!("{err}").contains("denied by IAM policy"),
3052 "got: {err}"
3053 );
3054 }
3055
3056 #[test]
3057 fn truncate_table_clears_rows_and_preserves_schema_and_indexes() {
3058 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3059 rt.execute_query("CREATE TABLE users (id INT, name TEXT)")
3060 .unwrap();
3061 rt.execute_query("INSERT INTO users (id, name) VALUES (1, 'ana'), (2, 'bob')")
3062 .unwrap();
3063 rt.execute_query("CREATE INDEX idx_users_id ON users (id) USING HASH")
3064 .unwrap();
3065
3066 let truncated = rt.execute_query("TRUNCATE TABLE users").unwrap();
3067 assert_eq!(truncated.statement_type, "truncate");
3068 assert_eq!(truncated.affected_rows, 0);
3069
3070 let empty = rt.execute_query("SELECT id FROM users").unwrap();
3071 assert!(empty.result.records.is_empty());
3072
3073 rt.execute_query("INSERT INTO users (id, name) VALUES (3, 'cy')")
3074 .unwrap();
3075 let selected = rt
3076 .execute_query("SELECT name FROM users WHERE id = 3")
3077 .unwrap();
3078 let name = selected.result.records[0].get("name").unwrap();
3079 assert_eq!(name, &Value::text("cy"));
3080 assert!(rt.db().collection_contract("users").is_some());
3081 assert!(rt
3082 .inner
3083 .index_store
3084 .list_indices("users")
3085 .iter()
3086 .any(|index| index.name == "idx_users_id"));
3087 }
3088
3089 #[test]
3090 fn truncate_collection_is_polymorphic_and_typed_mismatch_fails() {
3091 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3092 rt.execute_query("CREATE QUEUE tasks").unwrap();
3093 rt.execute_query("QUEUE PUSH tasks {'job':'a'}").unwrap();
3094
3095 let err = rt.execute_query("TRUNCATE TABLE tasks").unwrap_err();
3096 assert!(format!("{err}").contains("model mismatch: expected table, got queue"));
3097
3098 rt.execute_query("TRUNCATE COLLECTION tasks").unwrap();
3099 let len = rt.execute_query("QUEUE LEN tasks").unwrap();
3100 assert_eq!(
3101 len.result.records[0].get("len"),
3102 Some(&Value::UnsignedInteger(0))
3103 );
3104 }
3105
3106 #[test]
3107 fn truncate_system_schema_is_read_only() {
3108 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3109 let err = rt
3110 .execute_query("TRUNCATE COLLECTION red.collections")
3111 .unwrap_err();
3112 assert!(format!("{err}").contains("system schema is read-only"));
3113 }
3114
3115 fn queue_payloads(rt: &RedDBRuntime, queue: &str) -> Vec<crate::json::Value> {
3118 let result = rt
3119 .execute_query(&format!("QUEUE PEEK {queue} 100"))
3120 .expect("peek queue");
3121 result
3122 .result
3123 .records
3124 .iter()
3125 .map(
3126 |record| match record.get("payload").expect("payload column") {
3127 Value::Json(bytes) => crate::json::from_slice(bytes).expect("json payload"),
3128 other => panic!("expected JSON queue payload, got {other:?}"),
3129 },
3130 )
3131 .collect()
3132 }
3133
3134 #[test]
3137 fn truncate_event_enabled_table_emits_single_truncate_event() {
3138 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3139 rt.execute_query("CREATE TABLE users (id INT, name TEXT) WITH EVENTS TO users_events")
3140 .unwrap();
3141 rt.execute_query(
3142 "INSERT INTO users (id, name) VALUES (1, 'alice'), (2, 'bob'), (3, 'carol')",
3143 )
3144 .unwrap();
3145
3146 rt.execute_query("QUEUE POP users_events COUNT 10").unwrap();
3148
3149 rt.execute_query("TRUNCATE TABLE users").unwrap();
3150
3151 let events = queue_payloads(&rt, "users_events");
3152 assert_eq!(
3154 events.len(),
3155 1,
3156 "expected 1 truncate event, got {}",
3157 events.len()
3158 );
3159 let ev = events[0].as_object().expect("event is object");
3160 assert_eq!(
3161 ev.get("op").and_then(crate::json::Value::as_str),
3162 Some("truncate")
3163 );
3164 assert_eq!(
3165 ev.get("collection").and_then(crate::json::Value::as_str),
3166 Some("users")
3167 );
3168 assert_eq!(
3169 ev.get("entities_count")
3170 .and_then(crate::json::Value::as_u64),
3171 Some(3)
3172 );
3173 assert!(ev.get("ts").and_then(crate::json::Value::as_u64).is_some());
3174 assert!(ev.get("lsn").and_then(crate::json::Value::as_u64).is_some());
3175 assert!(ev
3176 .get("event_id")
3177 .and_then(crate::json::Value::as_str)
3178 .is_some_and(|s| !s.is_empty()));
3179 }
3180
3181 #[test]
3183 fn truncate_no_events_collection_emits_nothing() {
3184 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3185 rt.execute_query("CREATE TABLE plain (id INT, val TEXT)")
3186 .unwrap();
3187 rt.execute_query("INSERT INTO plain (id, val) VALUES (1, 'a'), (2, 'b')")
3188 .unwrap();
3189 rt.execute_query("TRUNCATE TABLE plain").unwrap();
3191 let rows = rt.execute_query("SELECT id FROM plain").unwrap();
3193 assert!(rows.result.records.is_empty());
3194 }
3195
3196 #[test]
3200 fn drop_event_enabled_table_emits_single_collection_dropped_event() {
3201 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3202 rt.execute_query("CREATE TABLE users (id INT, name TEXT) WITH EVENTS TO users_events")
3203 .unwrap();
3204 rt.execute_query("INSERT INTO users (id, name) VALUES (1, 'alice'), (2, 'bob')")
3205 .unwrap();
3206
3207 rt.execute_query("QUEUE POP users_events COUNT 10").unwrap();
3209
3210 rt.execute_query("DROP TABLE users").unwrap();
3211
3212 let events = queue_payloads(&rt, "users_events");
3214 assert_eq!(
3215 events.len(),
3216 1,
3217 "expected 1 collection_dropped event, got {}",
3218 events.len()
3219 );
3220 let ev = events[0].as_object().expect("event is object");
3221 assert_eq!(
3222 ev.get("op").and_then(crate::json::Value::as_str),
3223 Some("collection_dropped")
3224 );
3225 assert_eq!(
3226 ev.get("collection").and_then(crate::json::Value::as_str),
3227 Some("users")
3228 );
3229 assert_eq!(
3230 ev.get("final_entities_count")
3231 .and_then(crate::json::Value::as_u64),
3232 Some(2)
3233 );
3234 assert!(ev.get("ts").and_then(crate::json::Value::as_u64).is_some());
3235 assert!(ev.get("lsn").and_then(crate::json::Value::as_u64).is_some());
3236 assert!(ev
3237 .get("event_id")
3238 .and_then(crate::json::Value::as_str)
3239 .is_some_and(|s| !s.is_empty()));
3240
3241 let err = rt.execute_query("SELECT id FROM users").unwrap_err();
3243 assert!(
3244 format!("{err}").contains("users"),
3245 "expected not-found error"
3246 );
3247 }
3248
3249 #[test]
3252 fn drop_no_events_collection_emits_nothing() {
3253 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3254 rt.execute_query("CREATE TABLE plain (id INT, val TEXT)")
3255 .unwrap();
3256 rt.execute_query("INSERT INTO plain (id, val) VALUES (1, 'a')")
3257 .unwrap();
3258 rt.execute_query("DROP TABLE plain").unwrap();
3259 let err = rt.execute_query("SELECT id FROM plain").unwrap_err();
3261 assert!(format!("{err}").contains("plain"));
3262 }
3263
3264 #[test]
3268 fn ops_filter_insert_only_ignores_update_and_delete() {
3269 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3270 rt.execute_query(
3271 "CREATE TABLE items (id INT, val TEXT) WITH EVENTS (INSERT) TO items_events",
3272 )
3273 .unwrap();
3274 rt.execute_query("INSERT INTO items (id, val) VALUES (1, 'a')")
3275 .unwrap();
3276 rt.execute_query("UPDATE items SET val = 'b' WHERE id = 1")
3277 .unwrap();
3278 rt.execute_query("DELETE FROM items WHERE id = 1").unwrap();
3279
3280 let events = queue_payloads(&rt, "items_events");
3281 assert_eq!(
3283 events.len(),
3284 1,
3285 "expected 1 insert event, got {}",
3286 events.len()
3287 );
3288 assert_eq!(
3289 events[0]
3290 .as_object()
3291 .unwrap()
3292 .get("op")
3293 .and_then(crate::json::Value::as_str),
3294 Some("insert")
3295 );
3296 }
3297
3298 #[test]
3300 fn where_filter_skips_rows_that_do_not_match() {
3301 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3302 rt.execute_query(
3303 "CREATE TABLE users (id INT, status TEXT) WITH EVENTS WHERE status = 'active' TO users_events",
3304 )
3305 .unwrap();
3306
3307 rt.execute_query("INSERT INTO users (id, status) VALUES (1, 'active')")
3309 .unwrap();
3310 rt.execute_query("INSERT INTO users (id, status) VALUES (2, 'inactive')")
3312 .unwrap();
3313
3314 let events = queue_payloads(&rt, "users_events");
3315 assert_eq!(
3316 events.len(),
3317 1,
3318 "expected 1 event (only active), got {}",
3319 events.len()
3320 );
3321 let ev = events[0].as_object().unwrap();
3322 assert_eq!(
3323 ev.get("op").and_then(crate::json::Value::as_str),
3324 Some("insert")
3325 );
3326 let after = ev.get("after").unwrap().as_object().unwrap();
3327 assert_eq!(
3328 after.get("status").and_then(crate::json::Value::as_str),
3329 Some("active")
3330 );
3331 }
3332
3333 #[test]
3335 fn ops_filter_and_where_filter_combined() {
3336 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3337 rt.execute_query(
3338 "CREATE TABLE items (id INT, status TEXT) WITH EVENTS (INSERT, UPDATE) WHERE status = 'active' TO items_events",
3339 )
3340 .unwrap();
3341
3342 rt.execute_query("INSERT INTO items (id, status) VALUES (1, 'active')")
3344 .unwrap();
3345 rt.execute_query("INSERT INTO items (id, status) VALUES (2, 'inactive')")
3347 .unwrap();
3348 rt.execute_query("UPDATE items SET status = 'inactive' WHERE id = 1")
3350 .unwrap();
3351 rt.execute_query("DELETE FROM items WHERE id = 2").unwrap();
3353
3354 let events = queue_payloads(&rt, "items_events");
3355 assert_eq!(
3357 events.len(),
3358 1,
3359 "expected 1 event, got {}: {events:?}",
3360 events.len()
3361 );
3362 assert_eq!(
3363 events[0]
3364 .as_object()
3365 .unwrap()
3366 .get("op")
3367 .and_then(crate::json::Value::as_str),
3368 Some("insert")
3369 );
3370 }
3371
3372 #[test]
3374 fn where_filter_on_delete_checks_before_state() {
3375 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3376 rt.execute_query(
3377 "CREATE TABLE users (id INT, status TEXT) WITH EVENTS (DELETE) WHERE status = 'active' TO users_events",
3378 )
3379 .unwrap();
3380
3381 rt.execute_query("INSERT INTO users (id, status) VALUES (1, 'active'), (2, 'inactive')")
3382 .unwrap();
3383
3384 rt.execute_query("DELETE FROM users WHERE id = 1").unwrap();
3386 rt.execute_query("DELETE FROM users WHERE id = 2").unwrap();
3388
3389 let events = queue_payloads(&rt, "users_events");
3390 assert_eq!(
3391 events.len(),
3392 1,
3393 "expected 1 delete event, got {}",
3394 events.len()
3395 );
3396 let ev = events[0].as_object().unwrap();
3397 assert_eq!(
3398 ev.get("op").and_then(crate::json::Value::as_str),
3399 Some("delete")
3400 );
3401 }
3402
3403 #[test]
3407 fn alter_add_column_on_event_enabled_table_succeeds() {
3408 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3409 rt.execute_query("CREATE TABLE users (id INT, name TEXT) WITH EVENTS TO users_events")
3410 .unwrap();
3411 rt.execute_query("ALTER TABLE users ADD COLUMN phone TEXT")
3413 .unwrap();
3414 let contract = rt.db().collection_contract("users").unwrap();
3416 assert!(
3417 contract.declared_columns.iter().any(|c| c.name == "phone"),
3418 "phone column should be in contract"
3419 );
3420 assert!(
3422 contract.subscriptions.iter().any(|s| s.enabled),
3423 "subscription should remain enabled"
3424 );
3425 }
3426
3427 #[test]
3430 fn alter_drop_column_and_rls_on_event_enabled_table_succeeds() {
3431 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3432 rt.execute_query(
3433 "CREATE TABLE items (id INT, secret TEXT, status TEXT) WITH EVENTS TO items_events",
3434 )
3435 .unwrap();
3436 rt.execute_query("ALTER TABLE items DROP COLUMN secret")
3438 .unwrap();
3439 let contract = rt.db().collection_contract("items").unwrap();
3440 assert!(
3441 !contract.declared_columns.iter().any(|c| c.name == "secret"),
3442 "secret column should be removed"
3443 );
3444 rt.execute_query("ALTER TABLE items ENABLE ROW LEVEL SECURITY")
3446 .unwrap();
3447 assert!(
3449 contract.subscriptions.iter().any(|s| s.enabled),
3450 "subscription should remain enabled"
3451 );
3452 }
3453
3454 #[test]
3460 fn create_vector_marks_collection_as_turbo_baseline() {
3461 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3462 rt.execute_query("CREATE VECTOR embeddings DIM 4").unwrap();
3463 let store = rt.db().store();
3464 assert!(
3465 crate::runtime::vector_turbo_kind::is_turbo(&store, "embeddings"),
3466 "new vector collections must be turbo-marked baseline"
3467 );
3468 assert!(
3469 rt.db().turbo_state("embeddings").is_some(),
3470 "turbo_state must materialise after CREATE VECTOR"
3471 );
3472 }
3473
3474 #[test]
3478 fn create_table_does_not_mark_turbo() {
3479 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3480 rt.execute_query("CREATE TABLE plain (id INT)").unwrap();
3481 let store = rt.db().store();
3482 assert!(
3483 !crate::runtime::vector_turbo_kind::is_turbo(&store, "plain"),
3484 "non-vector collections must not gain the turbo marker"
3485 );
3486 assert!(rt.db().turbo_state("plain").is_none());
3487 }
3488
3489 #[test]
3492 fn create_collection_kind_vector_turbo_still_marked() {
3493 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3494 rt.execute_query("CREATE COLLECTION turbo_v KIND vector.turbo DIM 4")
3495 .unwrap();
3496 let store = rt.db().store();
3497 assert!(crate::runtime::vector_turbo_kind::is_turbo(
3498 &store, "turbo_v"
3499 ));
3500 assert!(rt.db().turbo_state("turbo_v").is_some());
3501 }
3502
3503 #[test]
3506 fn create_table_persists_and_introspects_ai_policy() {
3507 use crate::catalog::{ModerateDegradedMode, ModerateRejectAction};
3508 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3509 rt.execute_query(
3510 "CREATE TABLE posts (id INT, title TEXT, body TEXT, photo TEXT) WITH ( \
3511 EMBED (fields = ('title', 'body'), provider = 'openai', model = 'text-embedding-3-small'), \
3512 MODERATE (fields = ('body'), provider = 'openai', model = 'omni-moderation-latest', sync = true, degraded = closed, on_reject = flag), \
3513 VISION (image_field = 'photo', outputs = ('caption'), provider = 'openai', model = 'gpt-4o') \
3514 )",
3515 )
3516 .expect("create table with ai policy");
3517
3518 let contracts = rt.db().collection_contracts();
3519 let contract = contracts
3520 .iter()
3521 .find(|c| c.name == "posts")
3522 .expect("contract persisted");
3523 let policy = contract.ai_policy.as_ref().expect("ai policy persisted");
3524
3525 let embed = policy.embed.as_ref().expect("embed");
3526 assert_eq!(embed.fields, vec!["title".to_string(), "body".to_string()]);
3527 assert_eq!(embed.model, "text-embedding-3-small");
3528
3529 let moderate = policy.moderate.as_ref().expect("moderate");
3530 assert!(moderate.sync_gate);
3531 assert_eq!(moderate.degraded_mode, ModerateDegradedMode::Closed);
3532 assert_eq!(moderate.reject_action, ModerateRejectAction::Flag);
3533
3534 let vision = policy.vision.as_ref().expect("vision");
3535 assert_eq!(vision.image_field, "photo");
3536 assert_eq!(vision.model, "gpt-4o");
3537 }
3538
3539 #[test]
3543 fn create_table_rejects_ai_policy_incapable_provider() {
3544 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3545 let err = rt
3546 .execute_query(
3547 "CREATE TABLE bad (id INT, body TEXT) WITH ( \
3548 EMBED (fields = ('body'), provider = 'anthropic', model = 'claude-3-5-sonnet') \
3549 )",
3550 )
3551 .unwrap_err();
3552 assert!(
3553 err.to_string()
3554 .contains("cannot serve the 'embed' modality"),
3555 "{err}"
3556 );
3557 assert!(
3558 rt.db().store().get_collection("bad").is_none(),
3559 "rejected DDL must not create the collection"
3560 );
3561 }
3562}