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