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