1use std::collections::{BTreeMap, BTreeSet, HashMap};
4
5use alopex_cluster::{NodeId, SchemaApplyEvidence, SchemaApplyState, SchemaManifest};
6use alopex_core::{KVStore, KVTransaction};
7use alopex_sql::ast::ddl::{DataType, IndexMethod, VectorMetric};
8use alopex_sql::catalog::persistent::{CatalogMeta, IndexFqn, NamespaceMeta, TableFqn};
9use alopex_sql::catalog::{
10 Catalog, CatalogOverlay, ColumnMetadata, Compression, IndexMetadata, RowIdMode, StorageOptions,
11 StorageType, TableMetadata,
12};
13use alopex_sql::planner::types::ResolvedType;
14use alopex_sql::{DataSourceFormat, TableType};
15use serde::{Deserialize, Serialize};
16use sha2::Digest;
17
18use crate::{Database, Error, Result, Transaction, TxnMode};
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct CatalogInfo {
23 pub name: String,
25 pub comment: Option<String>,
27 pub storage_root: Option<String>,
29}
30
31impl From<CatalogMeta> for CatalogInfo {
32 fn from(value: CatalogMeta) -> Self {
33 Self {
34 name: value.name,
35 comment: value.comment,
36 storage_root: value.storage_root,
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct NamespaceInfo {
44 pub name: String,
46 pub catalog_name: String,
48 pub comment: Option<String>,
50 pub storage_root: Option<String>,
52}
53
54impl From<NamespaceMeta> for NamespaceInfo {
55 fn from(value: NamespaceMeta) -> Self {
56 Self {
57 name: value.name,
58 catalog_name: value.catalog_name,
59 comment: value.comment,
60 storage_root: value.storage_root,
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct ColumnInfo {
68 pub name: String,
70 pub data_type: String,
72 pub nullable: bool,
74 pub is_primary_key: bool,
76 pub comment: Option<String>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct StorageInfo {
83 pub storage_type: String,
85 pub compression: String,
87}
88
89impl Default for StorageInfo {
90 fn default() -> Self {
91 Self {
92 storage_type: "row".to_string(),
93 compression: "none".to_string(),
94 }
95 }
96}
97
98impl From<&StorageOptions> for StorageInfo {
99 fn from(value: &StorageOptions) -> Self {
100 let storage_type = match value.storage_type {
101 StorageType::Row => "row",
102 StorageType::Columnar => "columnar",
103 };
104 let compression = match value.compression {
105 Compression::None => "none",
106 Compression::Lz4 => "lz4",
107 Compression::Zstd => "zstd",
108 };
109 Self {
110 storage_type: storage_type.to_string(),
111 compression: compression.to_string(),
112 }
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct TableInfo {
119 pub name: String,
121 pub catalog_name: String,
123 pub namespace_name: String,
125 pub table_id: u32,
127 pub table_type: TableType,
129 pub columns: Vec<ColumnInfo>,
131 pub primary_key: Option<Vec<String>>,
133 pub storage_location: Option<String>,
135 pub data_source_format: DataSourceFormat,
137 pub storage_options: StorageInfo,
139 pub comment: Option<String>,
141 pub properties: HashMap<String, String>,
143}
144
145impl From<&TableMetadata> for TableInfo {
146 fn from(value: &TableMetadata) -> Self {
147 let primary_key = value.primary_key.clone();
148 let columns = value
149 .columns
150 .iter()
151 .map(|column| ColumnInfo {
152 name: column.name.clone(),
153 data_type: resolved_type_to_string(&column.data_type),
154 nullable: !column.not_null,
155 is_primary_key: column.primary_key
156 || primary_key
157 .as_ref()
158 .map(|keys| keys.iter().any(|name| name == &column.name))
159 .unwrap_or(false),
160 comment: None,
161 })
162 .collect();
163 let storage_options = if value.storage_options == StorageOptions::default() {
164 StorageInfo::default()
165 } else {
166 StorageInfo::from(&value.storage_options)
167 };
168
169 Self {
170 name: value.name.clone(),
171 catalog_name: value.catalog_name.clone(),
172 namespace_name: value.namespace_name.clone(),
173 table_id: value.table_id,
174 table_type: value.table_type,
175 columns,
176 primary_key,
177 storage_location: value.storage_location.clone(),
178 data_source_format: value.data_source_format,
179 storage_options,
180 comment: value.comment.clone(),
181 properties: value.properties.clone(),
182 }
183 }
184}
185
186impl From<TableMetadata> for TableInfo {
187 fn from(value: TableMetadata) -> Self {
188 Self::from(&value)
189 }
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct IndexInfo {
195 pub name: String,
197 pub index_id: u32,
199 pub catalog_name: String,
201 pub namespace_name: String,
203 pub table_name: String,
205 pub columns: Vec<String>,
207 pub method: String,
209 pub is_unique: bool,
211}
212
213impl From<&IndexMetadata> for IndexInfo {
214 fn from(value: &IndexMetadata) -> Self {
215 let method = match value.method {
216 Some(IndexMethod::BTree) | None => "btree",
217 Some(IndexMethod::Hnsw) => "hnsw",
218 };
219 Self {
220 name: value.name.clone(),
221 index_id: value.index_id,
222 catalog_name: value.catalog_name.clone(),
223 namespace_name: value.namespace_name.clone(),
224 table_name: value.table.clone(),
225 columns: value.columns.clone(),
226 method: method.to_string(),
227 is_unique: value.unique,
228 }
229 }
230}
231
232impl From<IndexMetadata> for IndexInfo {
233 fn from(value: IndexMetadata) -> Self {
234 Self::from(&value)
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct CreateCatalogRequest {
241 pub name: String,
243 pub comment: Option<String>,
245 pub storage_root: Option<String>,
247}
248
249impl CreateCatalogRequest {
250 pub fn new(name: impl Into<String>) -> Self {
252 Self {
253 name: name.into(),
254 comment: None,
255 storage_root: None,
256 }
257 }
258
259 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
261 self.comment = Some(comment.into());
262 self
263 }
264
265 pub fn with_storage_root(mut self, storage_root: impl Into<String>) -> Self {
267 self.storage_root = Some(storage_root.into());
268 self
269 }
270
271 pub fn build(self) -> Result<Self> {
273 validate_required(&self.name, "catalog 名")?;
274 Ok(self)
275 }
276}
277
278#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct CreateNamespaceRequest {
281 pub catalog_name: String,
283 pub name: String,
285 pub comment: Option<String>,
287 pub storage_root: Option<String>,
289}
290
291impl CreateNamespaceRequest {
292 pub fn new(catalog_name: impl Into<String>, name: impl Into<String>) -> Self {
294 Self {
295 catalog_name: catalog_name.into(),
296 name: name.into(),
297 comment: None,
298 storage_root: None,
299 }
300 }
301
302 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
304 self.comment = Some(comment.into());
305 self
306 }
307
308 pub fn with_storage_root(mut self, storage_root: impl Into<String>) -> Self {
310 self.storage_root = Some(storage_root.into());
311 self
312 }
313
314 pub fn build(self) -> Result<Self> {
316 validate_required(&self.catalog_name, "catalog 名")?;
317 validate_required(&self.name, "namespace 名")?;
318 Ok(self)
319 }
320}
321
322#[derive(Debug, Clone)]
324pub struct CreateTableRequest {
325 pub catalog_name: String,
327 pub namespace_name: String,
329 pub name: String,
331 pub schema: Option<Vec<ColumnDefinition>>,
333 pub table_type: TableType,
335 pub data_source_format: Option<DataSourceFormat>,
337 pub primary_key: Option<Vec<String>>,
339 pub storage_root: Option<String>,
341 pub storage_options: Option<StorageOptions>,
343 pub comment: Option<String>,
345 pub properties: Option<HashMap<String, String>>,
347}
348
349impl CreateTableRequest {
350 pub fn new(name: impl Into<String>) -> Self {
352 Self {
353 catalog_name: "default".to_string(),
354 namespace_name: "default".to_string(),
355 name: name.into(),
356 schema: None,
357 table_type: TableType::Managed,
358 data_source_format: None,
359 primary_key: None,
360 storage_root: None,
361 storage_options: None,
362 comment: None,
363 properties: None,
364 }
365 }
366
367 pub fn with_catalog_name(mut self, catalog_name: impl Into<String>) -> Self {
369 self.catalog_name = catalog_name.into();
370 self
371 }
372
373 pub fn with_namespace_name(mut self, namespace_name: impl Into<String>) -> Self {
375 self.namespace_name = namespace_name.into();
376 self
377 }
378
379 pub fn with_schema(mut self, schema: Vec<ColumnDefinition>) -> Self {
381 self.schema = Some(schema);
382 self
383 }
384
385 pub fn with_table_type(mut self, table_type: TableType) -> Self {
387 self.table_type = table_type;
388 self
389 }
390
391 pub fn with_data_source_format(mut self, data_source_format: DataSourceFormat) -> Self {
393 self.data_source_format = Some(data_source_format);
394 self
395 }
396
397 pub fn with_primary_key(mut self, primary_key: Vec<String>) -> Self {
399 self.primary_key = Some(primary_key);
400 self
401 }
402
403 pub fn with_storage_root(mut self, storage_root: impl Into<String>) -> Self {
405 self.storage_root = Some(storage_root.into());
406 self
407 }
408
409 pub fn with_storage_options(mut self, storage_options: StorageOptions) -> Self {
411 self.storage_options = Some(storage_options);
412 self
413 }
414
415 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
417 self.comment = Some(comment.into());
418 self
419 }
420
421 pub fn with_properties(mut self, properties: HashMap<String, String>) -> Self {
423 self.properties = Some(properties);
424 self
425 }
426
427 pub fn build(mut self) -> Result<Self> {
429 validate_required(&self.catalog_name, "catalog 名")?;
430 validate_required(&self.namespace_name, "namespace 名")?;
431 validate_required(&self.name, "table 名")?;
432
433 if self.table_type == TableType::Managed && self.schema.is_none() {
434 return Err(Error::SchemaRequired);
435 }
436 if self.table_type == TableType::External && self.storage_root.is_none() {
437 return Err(Error::StorageRootRequired);
438 }
439
440 if self.data_source_format.is_none() {
441 self.data_source_format = Some(DataSourceFormat::Alopex);
442 }
443 if self.properties.is_none() {
444 self.properties = Some(HashMap::new());
445 }
446 Ok(self)
447 }
448}
449
450#[derive(Debug, Clone)]
452pub struct ColumnDefinition {
453 pub name: String,
455 pub data_type: DataType,
457 pub nullable: bool,
459 pub comment: Option<String>,
461}
462
463impl ColumnDefinition {
464 pub fn new(name: impl Into<String>, data_type: DataType) -> Self {
466 Self {
467 name: name.into(),
468 data_type,
469 nullable: true,
470 comment: None,
471 }
472 }
473
474 pub fn with_nullable(mut self, nullable: bool) -> Self {
476 self.nullable = nullable;
477 self
478 }
479
480 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
482 self.comment = Some(comment.into());
483 self
484 }
485}
486
487impl Database {
488 pub fn list_catalogs(&self) -> Result<Vec<CatalogInfo>> {
490 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
491 Ok(catalog
492 .list_catalogs()
493 .into_iter()
494 .map(CatalogInfo::from)
495 .collect())
496 }
497
498 pub fn get_catalog(&self, name: &str) -> Result<CatalogInfo> {
500 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
501 let meta = catalog
502 .get_catalog(name)
503 .ok_or_else(|| Error::CatalogNotFound(name.to_string()))?;
504 Ok(meta.into())
505 }
506
507 pub fn list_namespaces(&self, catalog_name: &str) -> Result<Vec<NamespaceInfo>> {
509 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
510 ensure_catalog_exists(&*catalog, catalog_name)?;
511 Ok(catalog
512 .list_namespaces(catalog_name)
513 .into_iter()
514 .map(NamespaceInfo::from)
515 .collect())
516 }
517
518 pub fn get_namespace(&self, catalog_name: &str, namespace_name: &str) -> Result<NamespaceInfo> {
520 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
521 ensure_catalog_exists(&*catalog, catalog_name)?;
522 let meta = catalog
523 .get_namespace(catalog_name, namespace_name)
524 .ok_or_else(|| {
525 Error::NamespaceNotFound(catalog_name.to_string(), namespace_name.to_string())
526 })?;
527 Ok(meta.into())
528 }
529
530 pub fn list_tables(&self, catalog_name: &str, namespace_name: &str) -> Result<Vec<TableInfo>> {
532 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
533 ensure_namespace_exists(&*catalog, catalog_name, namespace_name)?;
534 let namespace = catalog
535 .get_namespace(catalog_name, namespace_name)
536 .ok_or_else(|| {
537 Error::NamespaceNotFound(catalog_name.to_string(), namespace_name.to_string())
538 })?;
539
540 let overlay = CatalogOverlay::new();
541 let tables = catalog.list_tables_in_txn(catalog_name, namespace_name, &overlay);
542 Ok(tables
543 .into_iter()
544 .map(|table| {
545 let info = TableInfo::from(table);
546 apply_storage_location(info, namespace.storage_root.as_deref())
547 })
548 .collect())
549 }
550
551 pub fn list_tables_simple(&self) -> Result<Vec<TableInfo>> {
553 self.list_tables("default", "default")
554 }
555
556 pub fn get_table_info(
558 &self,
559 catalog_name: &str,
560 namespace_name: &str,
561 table_name: &str,
562 ) -> Result<TableInfo> {
563 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
564 ensure_namespace_exists(&*catalog, catalog_name, namespace_name)?;
565 let namespace = catalog
566 .get_namespace(catalog_name, namespace_name)
567 .ok_or_else(|| {
568 Error::NamespaceNotFound(catalog_name.to_string(), namespace_name.to_string())
569 })?;
570
571 let overlay = CatalogOverlay::new();
572 let tables = catalog.list_tables_in_txn(catalog_name, namespace_name, &overlay);
573 let table = tables
574 .into_iter()
575 .find(|table| table.name == table_name)
576 .ok_or_else(|| {
577 Error::TableNotFound(table_full_name(catalog_name, namespace_name, table_name))
578 })?;
579
580 let info = TableInfo::from(table);
581 Ok(apply_storage_location(
582 info,
583 namespace.storage_root.as_deref(),
584 ))
585 }
586
587 pub fn get_table_info_simple(&self, table_name: &str) -> Result<TableInfo> {
589 self.get_table_info("default", "default", table_name)
590 }
591
592 pub fn get_table_info_cached(
597 &self,
598 catalog_name: &str,
599 namespace_name: &str,
600 table_name: &str,
601 ) -> Result<crate::CachedTableInfo> {
602 if let Some(cached) = self.get_cached_table_info(catalog_name, namespace_name, table_name) {
604 return Ok(cached);
605 }
606
607 let info = self.get_table_info(catalog_name, namespace_name, table_name)?;
609 let cached = crate::CachedTableInfo {
610 storage_location: info.storage_location.clone(),
611 format: format!("{:?}", info.data_source_format).to_uppercase(),
612 };
613 self.cache_table_info(catalog_name, namespace_name, table_name, cached.clone());
614 Ok(cached)
615 }
616
617 pub fn list_indexes(
619 &self,
620 catalog_name: &str,
621 namespace_name: &str,
622 table_name: &str,
623 ) -> Result<Vec<IndexInfo>> {
624 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
625 ensure_namespace_exists(&*catalog, catalog_name, namespace_name)?;
626 ensure_table_exists(&*catalog, catalog_name, namespace_name, table_name)?;
627
628 let overlay = CatalogOverlay::new();
629 let fqn = TableFqn::new(catalog_name, namespace_name, table_name);
630 let indexes = catalog.list_indexes_in_txn(&fqn, &overlay);
631 Ok(indexes.into_iter().map(IndexInfo::from).collect())
632 }
633
634 pub fn list_indexes_simple(&self, table_name: &str) -> Result<Vec<IndexInfo>> {
636 self.list_indexes("default", "default", table_name)
637 }
638
639 pub fn get_index_info(
641 &self,
642 catalog_name: &str,
643 namespace_name: &str,
644 table_name: &str,
645 index_name: &str,
646 ) -> Result<IndexInfo> {
647 let indexes = self.list_indexes(catalog_name, namespace_name, table_name)?;
648 indexes
649 .into_iter()
650 .find(|index| index.name == index_name)
651 .ok_or_else(|| {
652 Error::IndexNotFound(index_full_name(
653 catalog_name,
654 namespace_name,
655 table_name,
656 index_name,
657 ))
658 })
659 }
660
661 pub fn get_index_info_simple(&self, table_name: &str, index_name: &str) -> Result<IndexInfo> {
663 self.get_index_info("default", "default", table_name, index_name)
664 }
665
666 pub fn create_catalog(&self, request: CreateCatalogRequest) -> Result<CatalogInfo> {
678 let request = request.build()?;
679 let mut catalog = self.sql_catalog.write().expect("catalog lock poisoned");
680 if catalog.get_catalog(&request.name).is_some() {
681 return Err(Error::CatalogAlreadyExists(request.name));
682 }
683 let meta = CatalogMeta {
684 name: request.name,
685 comment: request.comment,
686 storage_root: request.storage_root,
687 };
688 catalog
689 .create_catalog(meta.clone())
690 .map_err(|err| Error::Sql(err.into()))?;
691 self.invalidate_table_info_cache();
692 Ok(meta.into())
693 }
694
695 pub fn delete_catalog(&self, name: &str, force: bool) -> Result<()> {
697 if name == "default" {
698 return Err(Error::CannotDeleteDefault("catalog".to_string()));
699 }
700 let mut catalog = self.sql_catalog.write().expect("catalog lock poisoned");
701 ensure_catalog_exists(&*catalog, name)?;
702
703 if !force {
704 let namespaces = catalog.list_namespaces(name);
705 let has_non_default = namespaces.iter().any(|ns| ns.name != "default");
706 let has_tables = namespaces.iter().any(|ns| {
707 let overlay = CatalogOverlay::new();
708 !catalog
709 .list_tables_in_txn(name, &ns.name, &overlay)
710 .is_empty()
711 });
712 if has_non_default || has_tables {
713 return Err(Error::CatalogNotEmpty(name.to_string()));
714 }
715 }
716
717 catalog
718 .delete_catalog(name)
719 .map_err(|err| Error::Sql(err.into()))?;
720 self.invalidate_table_info_cache();
721 Ok(())
722 }
723
724 pub fn create_namespace(&self, request: CreateNamespaceRequest) -> Result<NamespaceInfo> {
740 let request = request.build()?;
741 let mut catalog = self.sql_catalog.write().expect("catalog lock poisoned");
742 let catalog_meta = catalog
743 .get_catalog(&request.catalog_name)
744 .ok_or_else(|| Error::CatalogNotFound(request.catalog_name.clone()))?;
745 if catalog
746 .get_namespace(&request.catalog_name, &request.name)
747 .is_some()
748 {
749 return Err(Error::NamespaceAlreadyExists(
750 request.catalog_name,
751 request.name,
752 ));
753 }
754
755 let storage_root = request
756 .storage_root
757 .or_else(|| catalog_meta.storage_root.clone());
758 let meta = NamespaceMeta {
759 name: request.name,
760 catalog_name: request.catalog_name,
761 comment: request.comment,
762 storage_root,
763 };
764 catalog
765 .create_namespace(meta.clone())
766 .map_err(|err| Error::Sql(err.into()))?;
767 self.invalidate_table_info_cache();
768 Ok(meta.into())
769 }
770
771 pub fn delete_namespace(
773 &self,
774 catalog_name: &str,
775 namespace_name: &str,
776 force: bool,
777 ) -> Result<()> {
778 if namespace_name == "default" {
779 return Err(Error::CannotDeleteDefault("namespace".to_string()));
780 }
781 let mut catalog = self.sql_catalog.write().expect("catalog lock poisoned");
782 ensure_namespace_exists(&*catalog, catalog_name, namespace_name)?;
783
784 let overlay = CatalogOverlay::new();
785 let tables = catalog.list_tables_in_txn(catalog_name, namespace_name, &overlay);
786 if !force && !tables.is_empty() {
787 return Err(Error::NamespaceNotEmpty(
788 catalog_name.to_string(),
789 namespace_name.to_string(),
790 ));
791 }
792
793 if force {
794 let store = catalog.store().clone();
795 let mut txn = store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
796 for table in &tables {
797 catalog
798 .persist_drop_table(&mut txn, &TableFqn::from(table))
799 .map_err(|err| Error::Sql(err.into()))?;
800 }
801 txn.commit_self().map_err(Error::Core)?;
802
803 let mut overlay = CatalogOverlay::new();
804 for table in tables {
805 overlay.drop_table(&TableFqn::from(&table));
806 }
807 catalog.apply_overlay(overlay);
808 }
809
810 catalog
811 .delete_namespace(catalog_name, namespace_name)
812 .map_err(|err| Error::Sql(err.into()))?;
813 self.invalidate_table_info_cache();
814 Ok(())
815 }
816
817 pub fn create_table(&self, request: CreateTableRequest) -> Result<TableInfo> {
840 let request = request.build()?;
841 let mut catalog = self.sql_catalog.write().expect("catalog lock poisoned");
842
843 ensure_namespace_exists(&*catalog, &request.catalog_name, &request.namespace_name)?;
844 ensure_table_absent(
845 &*catalog,
846 &request.catalog_name,
847 &request.namespace_name,
848 &request.name,
849 )?;
850
851 if request.table_type == TableType::Managed && request.storage_root.is_some() {
852 eprintln!("警告: managed テーブルの storage_root は無視されます");
853 }
854
855 let table_id = catalog.next_table_id();
856 let primary_key = request.primary_key.clone();
857 let columns = build_columns(request.schema.clone(), primary_key.as_ref())?;
858
859 let storage_options = request.storage_options.unwrap_or_else(|| StorageOptions {
860 compression: Compression::None,
861 ..StorageOptions::default()
862 });
863
864 let namespace = catalog.get_namespace(&request.catalog_name, &request.namespace_name);
865 let storage_location = resolve_storage_location(
866 &request.table_type,
867 request.storage_root.as_deref(),
868 namespace.as_ref(),
869 &request.name,
870 )?;
871
872 let mut table = TableMetadata::new(&request.name, columns).with_table_id(table_id);
873 table.catalog_name = request.catalog_name.clone();
874 table.namespace_name = request.namespace_name.clone();
875 table.primary_key = primary_key;
876 table.storage_options = storage_options;
877 table.table_type = request.table_type;
878 table.data_source_format = request
879 .data_source_format
880 .unwrap_or(DataSourceFormat::Alopex);
881 table.storage_location = storage_location;
882 table.comment = request.comment;
883 table.properties = request.properties.unwrap_or_default();
884
885 let store = catalog.store().clone();
886 let mut txn = store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
887 catalog
888 .persist_create_table(&mut txn, &table)
889 .map_err(|err| Error::Sql(err.into()))?;
890 txn.commit_self().map_err(Error::Core)?;
891
892 let mut overlay = CatalogOverlay::new();
893 overlay.add_table(TableFqn::from(&table), table.clone());
894 catalog.apply_overlay(overlay);
895 drop(catalog); self.invalidate_table_info_cache();
897
898 let info = TableInfo::from(table);
899 let namespace_root = namespace.and_then(|ns| ns.storage_root);
900 Ok(apply_storage_location(info, namespace_root.as_deref()))
901 }
902
903 pub fn create_table_simple(
905 &self,
906 name: &str,
907 schema: Vec<ColumnDefinition>,
908 ) -> Result<TableInfo> {
909 self.create_table(CreateTableRequest::new(name).with_schema(schema))
910 }
911
912 pub fn delete_table(
914 &self,
915 catalog_name: &str,
916 namespace_name: &str,
917 table_name: &str,
918 ) -> Result<()> {
919 let mut catalog = self.sql_catalog.write().expect("catalog lock poisoned");
920 ensure_namespace_exists(&*catalog, catalog_name, namespace_name)?;
921 let table = find_table_metadata(&*catalog, catalog_name, namespace_name, table_name)?
922 .ok_or_else(|| {
923 Error::TableNotFound(table_full_name(catalog_name, namespace_name, table_name))
924 })?;
925
926 let store = catalog.store().clone();
927 let mut txn = store.begin(TxnMode::ReadWrite).map_err(Error::Core)?;
928 catalog
929 .persist_drop_table(&mut txn, &TableFqn::from(&table))
930 .map_err(|err| Error::Sql(err.into()))?;
931 txn.commit_self().map_err(Error::Core)?;
932
933 let mut overlay = CatalogOverlay::new();
934 overlay.drop_table(&TableFqn::from(&table));
935 catalog.apply_overlay(overlay);
936 drop(catalog); self.invalidate_table_info_cache();
938 Ok(())
939 }
940
941 pub fn delete_table_simple(&self, name: &str) -> Result<()> {
943 self.delete_table("default", "default", name)
944 }
945}
946
947impl<'a> Transaction<'a> {
948 pub fn list_catalogs(&self) -> Result<Vec<CatalogInfo>> {
950 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
951 Ok(catalog
952 .list_catalogs_in_txn(self.catalog_overlay())
953 .into_iter()
954 .map(CatalogInfo::from)
955 .collect())
956 }
957
958 pub fn get_catalog(&self, name: &str) -> Result<CatalogInfo> {
960 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
961 let meta = catalog
962 .get_catalog_in_txn(name, self.catalog_overlay())
963 .ok_or_else(|| Error::CatalogNotFound(name.to_string()))?;
964 Ok(meta.clone().into())
965 }
966
967 pub fn list_namespaces(&self, catalog_name: &str) -> Result<Vec<NamespaceInfo>> {
969 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
970 ensure_catalog_exists_in_txn(&*catalog, self.catalog_overlay(), catalog_name)?;
971 Ok(catalog
972 .list_namespaces_in_txn(catalog_name, self.catalog_overlay())
973 .into_iter()
974 .map(NamespaceInfo::from)
975 .collect())
976 }
977
978 pub fn get_namespace(&self, catalog_name: &str, namespace_name: &str) -> Result<NamespaceInfo> {
980 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
981 ensure_catalog_exists_in_txn(&*catalog, self.catalog_overlay(), catalog_name)?;
982 let meta = catalog
983 .get_namespace_in_txn(catalog_name, namespace_name, self.catalog_overlay())
984 .ok_or_else(|| {
985 Error::NamespaceNotFound(catalog_name.to_string(), namespace_name.to_string())
986 })?;
987 Ok(meta.clone().into())
988 }
989
990 pub fn list_tables(&self, catalog_name: &str, namespace_name: &str) -> Result<Vec<TableInfo>> {
992 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
993 ensure_namespace_exists_in_txn(
994 &*catalog,
995 self.catalog_overlay(),
996 catalog_name,
997 namespace_name,
998 )?;
999 let namespace = catalog
1000 .get_namespace_in_txn(catalog_name, namespace_name, self.catalog_overlay())
1001 .cloned()
1002 .ok_or_else(|| {
1003 Error::NamespaceNotFound(catalog_name.to_string(), namespace_name.to_string())
1004 })?;
1005 let tables =
1006 catalog.list_tables_in_txn(catalog_name, namespace_name, self.catalog_overlay());
1007 Ok(tables
1008 .into_iter()
1009 .map(|table| {
1010 let info = TableInfo::from(table);
1011 apply_storage_location(info, namespace.storage_root.as_deref())
1012 })
1013 .collect())
1014 }
1015
1016 pub fn get_table_info(
1018 &self,
1019 catalog_name: &str,
1020 namespace_name: &str,
1021 table_name: &str,
1022 ) -> Result<TableInfo> {
1023 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
1024 ensure_namespace_exists_in_txn(
1025 &*catalog,
1026 self.catalog_overlay(),
1027 catalog_name,
1028 namespace_name,
1029 )?;
1030 let namespace = catalog
1031 .get_namespace_in_txn(catalog_name, namespace_name, self.catalog_overlay())
1032 .cloned()
1033 .ok_or_else(|| {
1034 Error::NamespaceNotFound(catalog_name.to_string(), namespace_name.to_string())
1035 })?;
1036
1037 let tables =
1038 catalog.list_tables_in_txn(catalog_name, namespace_name, self.catalog_overlay());
1039 let table = tables
1040 .into_iter()
1041 .find(|table| table.name == table_name)
1042 .ok_or_else(|| {
1043 Error::TableNotFound(table_full_name(catalog_name, namespace_name, table_name))
1044 })?;
1045 let info = TableInfo::from(table);
1046 Ok(apply_storage_location(
1047 info,
1048 namespace.storage_root.as_deref(),
1049 ))
1050 }
1051
1052 pub fn create_catalog(&mut self, request: CreateCatalogRequest) -> Result<CatalogInfo> {
1065 ensure_write_mode(self)?;
1066 let request = request.build()?;
1067 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
1068 if catalog
1069 .get_catalog_in_txn(&request.name, self.catalog_overlay())
1070 .is_some()
1071 {
1072 return Err(Error::CatalogAlreadyExists(request.name));
1073 }
1074
1075 let meta = CatalogMeta {
1076 name: request.name,
1077 comment: request.comment,
1078 storage_root: request.storage_root,
1079 };
1080 self.catalog_overlay_mut().add_catalog(meta.clone());
1081 self.catalog_modified = true;
1082 Ok(meta.into())
1083 }
1084
1085 pub fn delete_catalog(&mut self, name: &str, force: bool) -> Result<()> {
1087 ensure_write_mode(self)?;
1088 if name == "default" {
1089 return Err(Error::CannotDeleteDefault("catalog".to_string()));
1090 }
1091 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
1092 ensure_catalog_exists_in_txn(&*catalog, self.catalog_overlay(), name)?;
1093
1094 if !force {
1095 let namespaces = catalog.list_namespaces_in_txn(name, self.catalog_overlay());
1096 let has_non_default = namespaces.iter().any(|ns| ns.name != "default");
1097 let has_tables = namespaces.iter().any(|ns| {
1098 !catalog
1099 .list_tables_in_txn(name, &ns.name, self.catalog_overlay())
1100 .is_empty()
1101 });
1102 if has_non_default || has_tables {
1103 return Err(Error::CatalogNotEmpty(name.to_string()));
1104 }
1105 }
1106
1107 if force {
1108 self.catalog_overlay_mut().drop_cascade_catalog(name);
1109 } else {
1110 self.catalog_overlay_mut().drop_catalog(name);
1111 }
1112 self.catalog_modified = true;
1113 Ok(())
1114 }
1115
1116 pub fn create_namespace(&mut self, request: CreateNamespaceRequest) -> Result<NamespaceInfo> {
1118 ensure_write_mode(self)?;
1119 let request = request.build()?;
1120 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
1121 let catalog_meta = catalog
1122 .get_catalog_in_txn(&request.catalog_name, self.catalog_overlay())
1123 .ok_or_else(|| Error::CatalogNotFound(request.catalog_name.clone()))?;
1124 if catalog
1125 .get_namespace_in_txn(&request.catalog_name, &request.name, self.catalog_overlay())
1126 .is_some()
1127 {
1128 return Err(Error::NamespaceAlreadyExists(
1129 request.catalog_name,
1130 request.name,
1131 ));
1132 }
1133
1134 let storage_root = request
1135 .storage_root
1136 .or_else(|| catalog_meta.storage_root.clone());
1137 let meta = NamespaceMeta {
1138 name: request.name,
1139 catalog_name: request.catalog_name,
1140 comment: request.comment,
1141 storage_root,
1142 };
1143 self.catalog_overlay_mut().add_namespace(meta.clone());
1144 self.catalog_modified = true;
1145 Ok(meta.into())
1146 }
1147
1148 pub fn delete_namespace(
1150 &mut self,
1151 catalog_name: &str,
1152 namespace_name: &str,
1153 force: bool,
1154 ) -> Result<()> {
1155 ensure_write_mode(self)?;
1156 if namespace_name == "default" {
1157 return Err(Error::CannotDeleteDefault("namespace".to_string()));
1158 }
1159 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
1160 ensure_namespace_exists_in_txn(
1161 &*catalog,
1162 self.catalog_overlay(),
1163 catalog_name,
1164 namespace_name,
1165 )?;
1166
1167 let tables =
1168 catalog.list_tables_in_txn(catalog_name, namespace_name, self.catalog_overlay());
1169 if !force && !tables.is_empty() {
1170 return Err(Error::NamespaceNotEmpty(
1171 catalog_name.to_string(),
1172 namespace_name.to_string(),
1173 ));
1174 }
1175
1176 if force {
1177 self.catalog_overlay_mut()
1178 .drop_cascade_namespace(catalog_name, namespace_name);
1179 } else {
1180 self.catalog_overlay_mut()
1181 .drop_namespace(catalog_name, namespace_name);
1182 }
1183 self.catalog_modified = true;
1184 Ok(())
1185 }
1186
1187 pub fn create_table(&mut self, request: CreateTableRequest) -> Result<TableInfo> {
1216 ensure_write_mode(self)?;
1217 let request = request.build()?;
1218
1219 let mut catalog = self.db.sql_catalog.write().expect("catalog lock poisoned");
1220 ensure_namespace_exists_in_txn(
1221 &*catalog,
1222 self.catalog_overlay(),
1223 &request.catalog_name,
1224 &request.namespace_name,
1225 )?;
1226 ensure_table_absent_in_txn(
1227 &*catalog,
1228 self.catalog_overlay(),
1229 &request.catalog_name,
1230 &request.namespace_name,
1231 &request.name,
1232 )?;
1233
1234 if request.table_type == TableType::Managed && request.storage_root.is_some() {
1235 eprintln!("警告: managed テーブルの storage_root は無視されます");
1236 }
1237
1238 let table_id = catalog.next_table_id();
1239 let primary_key = request.primary_key.clone();
1240 let columns = build_columns(request.schema.clone(), primary_key.as_ref())?;
1241
1242 let storage_options = request.storage_options.unwrap_or_else(|| StorageOptions {
1243 compression: Compression::None,
1244 ..StorageOptions::default()
1245 });
1246
1247 let namespace = catalog
1248 .get_namespace_in_txn(
1249 &request.catalog_name,
1250 &request.namespace_name,
1251 self.catalog_overlay(),
1252 )
1253 .cloned();
1254 let storage_location = resolve_storage_location(
1255 &request.table_type,
1256 request.storage_root.as_deref(),
1257 namespace.as_ref(),
1258 &request.name,
1259 )?;
1260
1261 let mut table = TableMetadata::new(&request.name, columns).with_table_id(table_id);
1262 table.catalog_name = request.catalog_name.clone();
1263 table.namespace_name = request.namespace_name.clone();
1264 table.primary_key = primary_key;
1265 table.storage_options = storage_options;
1266 table.table_type = request.table_type;
1267 table.data_source_format = request
1268 .data_source_format
1269 .unwrap_or(DataSourceFormat::Alopex);
1270 table.storage_location = storage_location;
1271 table.comment = request.comment;
1272 table.properties = request.properties.unwrap_or_default();
1273
1274 self.catalog_overlay_mut()
1275 .add_table(TableFqn::from(&table), table.clone());
1276 self.catalog_modified = true;
1277 let info = TableInfo::from(table);
1278 let namespace_root = namespace.and_then(|ns| ns.storage_root);
1279 Ok(apply_storage_location(info, namespace_root.as_deref()))
1280 }
1281
1282 pub fn delete_table(
1284 &mut self,
1285 catalog_name: &str,
1286 namespace_name: &str,
1287 table_name: &str,
1288 ) -> Result<()> {
1289 ensure_write_mode(self)?;
1290 let catalog = self.db.sql_catalog.read().expect("catalog lock poisoned");
1291 ensure_namespace_exists_in_txn(
1292 &*catalog,
1293 self.catalog_overlay(),
1294 catalog_name,
1295 namespace_name,
1296 )?;
1297 let table = find_table_metadata_in_txn(
1298 &*catalog,
1299 self.catalog_overlay(),
1300 catalog_name,
1301 namespace_name,
1302 table_name,
1303 )?
1304 .ok_or_else(|| {
1305 Error::TableNotFound(table_full_name(catalog_name, namespace_name, table_name))
1306 })?;
1307
1308 self.catalog_overlay_mut()
1309 .drop_table(&TableFqn::from(&table));
1310 self.catalog_modified = true;
1311 Ok(())
1312 }
1313}
1314
1315fn validate_required(value: &str, label: &str) -> Result<()> {
1316 if value.trim().is_empty() {
1317 return Err(Error::Core(alopex_core::Error::InvalidFormat(format!(
1318 "{label}が未指定です"
1319 ))));
1320 }
1321 Ok(())
1322}
1323
1324fn ensure_catalog_exists<S: alopex_core::kv::KVStore>(
1325 catalog: &alopex_sql::catalog::PersistentCatalog<S>,
1326 name: &str,
1327) -> Result<()> {
1328 if catalog.get_catalog(name).is_none() {
1329 return Err(Error::CatalogNotFound(name.to_string()));
1330 }
1331 Ok(())
1332}
1333
1334fn ensure_catalog_exists_in_txn<S: alopex_core::kv::KVStore>(
1335 catalog: &alopex_sql::catalog::PersistentCatalog<S>,
1336 overlay: &CatalogOverlay,
1337 name: &str,
1338) -> Result<()> {
1339 if catalog.get_catalog_in_txn(name, overlay).is_none() {
1340 return Err(Error::CatalogNotFound(name.to_string()));
1341 }
1342 Ok(())
1343}
1344
1345fn ensure_namespace_exists<S: alopex_core::kv::KVStore>(
1346 catalog: &alopex_sql::catalog::PersistentCatalog<S>,
1347 catalog_name: &str,
1348 namespace_name: &str,
1349) -> Result<()> {
1350 ensure_catalog_exists(catalog, catalog_name)?;
1351 if catalog
1352 .get_namespace(catalog_name, namespace_name)
1353 .is_none()
1354 {
1355 return Err(Error::NamespaceNotFound(
1356 catalog_name.to_string(),
1357 namespace_name.to_string(),
1358 ));
1359 }
1360 Ok(())
1361}
1362
1363fn ensure_namespace_exists_in_txn<S: alopex_core::kv::KVStore>(
1364 catalog: &alopex_sql::catalog::PersistentCatalog<S>,
1365 overlay: &CatalogOverlay,
1366 catalog_name: &str,
1367 namespace_name: &str,
1368) -> Result<()> {
1369 ensure_catalog_exists_in_txn(catalog, overlay, catalog_name)?;
1370 if catalog
1371 .get_namespace_in_txn(catalog_name, namespace_name, overlay)
1372 .is_none()
1373 {
1374 return Err(Error::NamespaceNotFound(
1375 catalog_name.to_string(),
1376 namespace_name.to_string(),
1377 ));
1378 }
1379 Ok(())
1380}
1381
1382fn ensure_table_exists<S: alopex_core::kv::KVStore>(
1383 catalog: &alopex_sql::catalog::PersistentCatalog<S>,
1384 catalog_name: &str,
1385 namespace_name: &str,
1386 table_name: &str,
1387) -> Result<()> {
1388 let Some(table) = find_table_metadata(catalog, catalog_name, namespace_name, table_name)?
1389 else {
1390 return Err(Error::TableNotFound(table_full_name(
1391 catalog_name,
1392 namespace_name,
1393 table_name,
1394 )));
1395 };
1396 let _ = table;
1397 Ok(())
1398}
1399
1400fn ensure_table_absent<S: alopex_core::kv::KVStore>(
1401 catalog: &alopex_sql::catalog::PersistentCatalog<S>,
1402 catalog_name: &str,
1403 namespace_name: &str,
1404 table_name: &str,
1405) -> Result<()> {
1406 if find_table_metadata(catalog, catalog_name, namespace_name, table_name)?.is_some() {
1407 return Err(Error::TableAlreadyExists(table_full_name(
1408 catalog_name,
1409 namespace_name,
1410 table_name,
1411 )));
1412 }
1413 Ok(())
1414}
1415
1416fn ensure_table_absent_in_txn<S: alopex_core::kv::KVStore>(
1417 catalog: &alopex_sql::catalog::PersistentCatalog<S>,
1418 overlay: &CatalogOverlay,
1419 catalog_name: &str,
1420 namespace_name: &str,
1421 table_name: &str,
1422) -> Result<()> {
1423 if find_table_metadata_in_txn(catalog, overlay, catalog_name, namespace_name, table_name)?
1424 .is_some()
1425 {
1426 return Err(Error::TableAlreadyExists(table_full_name(
1427 catalog_name,
1428 namespace_name,
1429 table_name,
1430 )));
1431 }
1432 Ok(())
1433}
1434
1435fn find_table_metadata<S: alopex_core::kv::KVStore>(
1436 catalog: &alopex_sql::catalog::PersistentCatalog<S>,
1437 catalog_name: &str,
1438 namespace_name: &str,
1439 table_name: &str,
1440) -> Result<Option<TableMetadata>> {
1441 let overlay = CatalogOverlay::new();
1442 let tables = catalog.list_tables_in_txn(catalog_name, namespace_name, &overlay);
1443 Ok(tables.into_iter().find(|table| table.name == table_name))
1444}
1445
1446fn find_table_metadata_in_txn<S: alopex_core::kv::KVStore>(
1447 catalog: &alopex_sql::catalog::PersistentCatalog<S>,
1448 overlay: &CatalogOverlay,
1449 catalog_name: &str,
1450 namespace_name: &str,
1451 table_name: &str,
1452) -> Result<Option<TableMetadata>> {
1453 let tables = catalog.list_tables_in_txn(catalog_name, namespace_name, overlay);
1454 Ok(tables.into_iter().find(|table| table.name == table_name))
1455}
1456
1457fn table_full_name(catalog_name: &str, namespace_name: &str, table_name: &str) -> String {
1458 format!("{catalog_name}.{namespace_name}.{table_name}")
1459}
1460
1461fn index_full_name(
1462 catalog_name: &str,
1463 namespace_name: &str,
1464 table_name: &str,
1465 index_name: &str,
1466) -> String {
1467 format!("{catalog_name}.{namespace_name}.{table_name}.{index_name}")
1468}
1469
1470fn apply_storage_location(mut info: TableInfo, namespace_root: Option<&str>) -> TableInfo {
1471 if info.storage_location.is_none() && info.table_type == TableType::Managed {
1472 if let Some(root) = namespace_root {
1473 info.storage_location = Some(format!("{root}/{}", info.name));
1474 }
1475 }
1476 info
1477}
1478
1479fn resolve_storage_location(
1480 table_type: &TableType,
1481 request_storage_root: Option<&str>,
1482 namespace: Option<&NamespaceMeta>,
1483 table_name: &str,
1484) -> Result<Option<String>> {
1485 match table_type {
1486 TableType::Managed => Ok(namespace
1487 .and_then(|ns| ns.storage_root.as_deref())
1488 .map(|root| format!("{root}/{table_name}"))),
1489 TableType::External => {
1490 let storage_root = request_storage_root
1491 .map(|root| root.to_string())
1492 .ok_or(Error::StorageRootRequired)?;
1493 Ok(Some(storage_root))
1494 }
1495 }
1496}
1497
1498fn build_columns(
1499 schema: Option<Vec<ColumnDefinition>>,
1500 primary_key: Option<&Vec<String>>,
1501) -> Result<Vec<ColumnMetadata>> {
1502 let Some(schema) = schema else {
1503 return Ok(Vec::new());
1504 };
1505
1506 let mut columns = Vec::with_capacity(schema.len());
1507 for definition in schema {
1508 validate_required(&definition.name, "column 名")?;
1509 let mut column = ColumnMetadata::new(
1510 definition.name.clone(),
1511 ResolvedType::from_ast(&definition.data_type),
1512 )
1513 .with_not_null(!definition.nullable);
1514 if primary_key
1515 .map(|keys| keys.iter().any(|key| key == &definition.name))
1516 .unwrap_or(false)
1517 {
1518 column = column.with_primary_key(true).with_not_null(true);
1519 }
1520 columns.push(column);
1521 }
1522
1523 if let Some(keys) = primary_key {
1524 let missing: Vec<String> = keys
1525 .iter()
1526 .filter(|key| !columns.iter().any(|col| col.name == **key))
1527 .cloned()
1528 .collect();
1529 if !missing.is_empty() {
1530 return Err(Error::Core(alopex_core::Error::InvalidFormat(format!(
1531 "主キーが見つかりません: {}",
1532 missing.join(", ")
1533 ))));
1534 }
1535 }
1536
1537 Ok(columns)
1538}
1539
1540fn ensure_write_mode(txn: &Transaction<'_>) -> Result<()> {
1541 let mode = txn.txn_mode()?;
1542 if mode != TxnMode::ReadWrite {
1543 return Err(Error::TxnReadOnly);
1544 }
1545 Ok(())
1546}
1547
1548fn resolved_type_to_string(resolved_type: &ResolvedType) -> String {
1549 match resolved_type {
1550 ResolvedType::Integer => "INTEGER".to_string(),
1551 ResolvedType::BigInt => "BIGINT".to_string(),
1552 ResolvedType::Float => "FLOAT".to_string(),
1553 ResolvedType::Double => "DOUBLE".to_string(),
1554 ResolvedType::Text => "TEXT".to_string(),
1555 ResolvedType::Blob => "BLOB".to_string(),
1556 ResolvedType::Boolean => "BOOLEAN".to_string(),
1557 ResolvedType::Timestamp => "TIMESTAMP".to_string(),
1558 ResolvedType::Vector { dimension, metric } => {
1559 let metric = match metric {
1560 VectorMetric::Cosine => "COSINE",
1561 VectorMetric::L2 => "L2",
1562 VectorMetric::Inner => "INNER",
1563 };
1564 format!("VECTOR({dimension}, {metric})")
1565 }
1566 ResolvedType::Null => "NULL".to_string(),
1567 }
1568}
1569
1570pub const CATALOG_MANIFEST_DELTA_FORMAT: &str = "alopex.catalog.snapshot.v1";
1574
1575const CATALOG_MANIFEST_VERSION_KEY: &[u8] = b"__alopex/schema-manifest-version/v1";
1576
1577#[allow(missing_docs)]
1583#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1584pub struct CatalogManifestDelta {
1585 pub format_version: u32,
1586 pub catalog_version: u64,
1587 #[serde(default)]
1588 pub catalogs: Vec<CatalogManifestCatalog>,
1589 #[serde(default)]
1590 pub namespaces: Vec<CatalogManifestNamespace>,
1591 pub tables: Vec<CatalogManifestTable>,
1592 pub indexes: Vec<CatalogManifestIndex>,
1593}
1594
1595#[allow(missing_docs)]
1596impl CatalogManifestDelta {
1597 pub const FORMAT_VERSION: u32 = 1;
1598
1599 pub fn encode(&self) -> std::result::Result<Vec<u8>, serde_json::Error> {
1600 serde_json::to_vec(self)
1601 }
1602
1603 pub fn decode(bytes: &[u8]) -> std::result::Result<Self, serde_json::Error> {
1604 serde_json::from_slice(bytes)
1605 }
1606}
1607
1608#[allow(missing_docs)]
1610#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1611pub struct CatalogManifestCatalog {
1612 pub name: String,
1613 pub comment: Option<String>,
1614 pub storage_root: Option<String>,
1615}
1616
1617impl From<&CatalogMeta> for CatalogManifestCatalog {
1618 fn from(meta: &CatalogMeta) -> Self {
1619 Self {
1620 name: meta.name.clone(),
1621 comment: meta.comment.clone(),
1622 storage_root: meta.storage_root.clone(),
1623 }
1624 }
1625}
1626
1627impl From<&CatalogManifestCatalog> for CatalogMeta {
1628 fn from(manifest: &CatalogManifestCatalog) -> Self {
1629 Self {
1630 name: manifest.name.clone(),
1631 comment: manifest.comment.clone(),
1632 storage_root: manifest.storage_root.clone(),
1633 }
1634 }
1635}
1636
1637#[allow(missing_docs)]
1639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1640pub struct CatalogManifestNamespace {
1641 pub catalog_name: String,
1642 pub name: String,
1643 pub comment: Option<String>,
1644 pub storage_root: Option<String>,
1645}
1646
1647impl From<&NamespaceMeta> for CatalogManifestNamespace {
1648 fn from(meta: &NamespaceMeta) -> Self {
1649 Self {
1650 catalog_name: meta.catalog_name.clone(),
1651 name: meta.name.clone(),
1652 comment: meta.comment.clone(),
1653 storage_root: meta.storage_root.clone(),
1654 }
1655 }
1656}
1657
1658impl From<&CatalogManifestNamespace> for NamespaceMeta {
1659 fn from(manifest: &CatalogManifestNamespace) -> Self {
1660 Self {
1661 catalog_name: manifest.catalog_name.clone(),
1662 name: manifest.name.clone(),
1663 comment: manifest.comment.clone(),
1664 storage_root: manifest.storage_root.clone(),
1665 }
1666 }
1667}
1668
1669#[allow(missing_docs)]
1671#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1672pub struct CatalogManifestTable {
1673 pub table_id: u32,
1674 pub catalog_name: String,
1675 pub namespace_name: String,
1676 pub name: String,
1677 pub table_type: CatalogManifestTableType,
1678 pub data_source_format: CatalogManifestDataSourceFormat,
1679 pub columns: Vec<CatalogManifestColumn>,
1680 pub primary_key: Option<Vec<String>>,
1681 pub storage: CatalogManifestStorage,
1682 pub storage_location: Option<String>,
1683 pub comment: Option<String>,
1684 pub properties: BTreeMap<String, String>,
1685}
1686
1687impl CatalogManifestTable {
1688 fn fqn(&self) -> TableFqn {
1689 TableFqn::new(&self.catalog_name, &self.namespace_name, &self.name)
1690 }
1691
1692 fn to_metadata(&self) -> TableMetadata {
1693 let columns = self
1694 .columns
1695 .iter()
1696 .map(CatalogManifestColumn::to_metadata)
1697 .collect();
1698 let mut table = TableMetadata::new(self.name.clone(), columns).with_table_id(self.table_id);
1699 table.catalog_name = self.catalog_name.clone();
1700 table.namespace_name = self.namespace_name.clone();
1701 table.table_type = self.table_type.into();
1702 table.data_source_format = self.data_source_format.into();
1703 table.primary_key = self.primary_key.clone();
1704 table.storage_options = self.storage.to_options();
1705 table.storage_location = self.storage_location.clone();
1706 table.comment = self.comment.clone();
1707 table.properties = self.properties.clone().into_iter().collect();
1708 table
1709 }
1710}
1711
1712impl From<&TableMetadata> for CatalogManifestTable {
1713 fn from(table: &TableMetadata) -> Self {
1714 Self {
1715 table_id: table.table_id,
1716 catalog_name: table.catalog_name.clone(),
1717 namespace_name: table.namespace_name.clone(),
1718 name: table.name.clone(),
1719 table_type: table.table_type.into(),
1720 data_source_format: table.data_source_format.into(),
1721 columns: table
1722 .columns
1723 .iter()
1724 .map(CatalogManifestColumn::from)
1725 .collect(),
1726 primary_key: table.primary_key.clone(),
1727 storage: CatalogManifestStorage::from(&table.storage_options),
1728 storage_location: table.storage_location.clone(),
1729 comment: table.comment.clone(),
1730 properties: table
1731 .properties
1732 .iter()
1733 .map(|(key, value)| (key.clone(), value.clone()))
1734 .collect(),
1735 }
1736 }
1737}
1738
1739#[allow(missing_docs)]
1743#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1744pub struct CatalogManifestColumn {
1745 pub name: String,
1746 pub data_type: CatalogManifestDataType,
1747 pub not_null: bool,
1748 pub primary_key: bool,
1749 pub unique: bool,
1750}
1751
1752impl CatalogManifestColumn {
1753 fn to_metadata(&self) -> ColumnMetadata {
1754 ColumnMetadata::new(self.name.clone(), self.data_type.clone().into())
1755 .with_not_null(self.not_null)
1756 .with_primary_key(self.primary_key)
1757 .with_unique(self.unique)
1758 }
1759}
1760
1761impl From<&ColumnMetadata> for CatalogManifestColumn {
1762 fn from(column: &ColumnMetadata) -> Self {
1763 Self {
1764 name: column.name.clone(),
1765 data_type: (&column.data_type).into(),
1766 not_null: column.not_null,
1767 primary_key: column.primary_key,
1768 unique: column.unique,
1769 }
1770 }
1771}
1772
1773#[allow(missing_docs)]
1774#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1775#[serde(rename_all = "snake_case")]
1776pub enum CatalogManifestTableType {
1777 Managed,
1778 External,
1779}
1780
1781impl From<TableType> for CatalogManifestTableType {
1782 fn from(value: TableType) -> Self {
1783 match value {
1784 TableType::Managed => Self::Managed,
1785 TableType::External => Self::External,
1786 }
1787 }
1788}
1789
1790impl From<CatalogManifestTableType> for TableType {
1791 fn from(value: CatalogManifestTableType) -> Self {
1792 match value {
1793 CatalogManifestTableType::Managed => Self::Managed,
1794 CatalogManifestTableType::External => Self::External,
1795 }
1796 }
1797}
1798
1799#[allow(missing_docs)]
1800#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1801#[serde(rename_all = "snake_case")]
1802pub enum CatalogManifestDataSourceFormat {
1803 Alopex,
1804 Parquet,
1805 Delta,
1806}
1807
1808impl From<DataSourceFormat> for CatalogManifestDataSourceFormat {
1809 fn from(value: DataSourceFormat) -> Self {
1810 match value {
1811 DataSourceFormat::Alopex => Self::Alopex,
1812 DataSourceFormat::Parquet => Self::Parquet,
1813 DataSourceFormat::Delta => Self::Delta,
1814 }
1815 }
1816}
1817
1818impl From<CatalogManifestDataSourceFormat> for DataSourceFormat {
1819 fn from(value: CatalogManifestDataSourceFormat) -> Self {
1820 match value {
1821 CatalogManifestDataSourceFormat::Alopex => Self::Alopex,
1822 CatalogManifestDataSourceFormat::Parquet => Self::Parquet,
1823 CatalogManifestDataSourceFormat::Delta => Self::Delta,
1824 }
1825 }
1826}
1827
1828#[allow(missing_docs)]
1829#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1830#[serde(rename_all = "snake_case", tag = "kind")]
1831pub enum CatalogManifestDataType {
1832 Integer,
1833 BigInt,
1834 Float,
1835 Double,
1836 Text,
1837 Blob,
1838 Boolean,
1839 Timestamp,
1840 Vector {
1841 dimension: u32,
1842 metric: CatalogManifestVectorMetric,
1843 },
1844 Null,
1845}
1846
1847impl From<&ResolvedType> for CatalogManifestDataType {
1848 fn from(value: &ResolvedType) -> Self {
1849 match value {
1850 ResolvedType::Integer => Self::Integer,
1851 ResolvedType::BigInt => Self::BigInt,
1852 ResolvedType::Float => Self::Float,
1853 ResolvedType::Double => Self::Double,
1854 ResolvedType::Text => Self::Text,
1855 ResolvedType::Blob => Self::Blob,
1856 ResolvedType::Boolean => Self::Boolean,
1857 ResolvedType::Timestamp => Self::Timestamp,
1858 ResolvedType::Vector { dimension, metric } => Self::Vector {
1859 dimension: *dimension,
1860 metric: (*metric).into(),
1861 },
1862 ResolvedType::Null => Self::Null,
1863 }
1864 }
1865}
1866
1867impl From<CatalogManifestDataType> for ResolvedType {
1868 fn from(value: CatalogManifestDataType) -> Self {
1869 match value {
1870 CatalogManifestDataType::Integer => Self::Integer,
1871 CatalogManifestDataType::BigInt => Self::BigInt,
1872 CatalogManifestDataType::Float => Self::Float,
1873 CatalogManifestDataType::Double => Self::Double,
1874 CatalogManifestDataType::Text => Self::Text,
1875 CatalogManifestDataType::Blob => Self::Blob,
1876 CatalogManifestDataType::Boolean => Self::Boolean,
1877 CatalogManifestDataType::Timestamp => Self::Timestamp,
1878 CatalogManifestDataType::Vector { dimension, metric } => Self::Vector {
1879 dimension,
1880 metric: metric.into(),
1881 },
1882 CatalogManifestDataType::Null => Self::Null,
1883 }
1884 }
1885}
1886
1887#[allow(missing_docs)]
1888#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1889#[serde(rename_all = "snake_case")]
1890pub enum CatalogManifestVectorMetric {
1891 Cosine,
1892 L2,
1893 Inner,
1894}
1895
1896impl From<VectorMetric> for CatalogManifestVectorMetric {
1897 fn from(value: VectorMetric) -> Self {
1898 match value {
1899 VectorMetric::Cosine => Self::Cosine,
1900 VectorMetric::L2 => Self::L2,
1901 VectorMetric::Inner => Self::Inner,
1902 }
1903 }
1904}
1905
1906impl From<CatalogManifestVectorMetric> for VectorMetric {
1907 fn from(value: CatalogManifestVectorMetric) -> Self {
1908 match value {
1909 CatalogManifestVectorMetric::Cosine => Self::Cosine,
1910 CatalogManifestVectorMetric::L2 => Self::L2,
1911 CatalogManifestVectorMetric::Inner => Self::Inner,
1912 }
1913 }
1914}
1915
1916#[allow(missing_docs)]
1917#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1918pub struct CatalogManifestStorage {
1919 pub storage_type: CatalogManifestStorageType,
1920 pub compression: CatalogManifestCompression,
1921 pub row_group_size: u32,
1922 pub row_id_mode: CatalogManifestRowIdMode,
1923}
1924
1925impl CatalogManifestStorage {
1926 fn to_options(&self) -> StorageOptions {
1927 StorageOptions {
1928 storage_type: self.storage_type.into(),
1929 compression: self.compression.into(),
1930 row_group_size: self.row_group_size,
1931 row_id_mode: self.row_id_mode.into(),
1932 }
1933 }
1934}
1935
1936impl From<&StorageOptions> for CatalogManifestStorage {
1937 fn from(value: &StorageOptions) -> Self {
1938 Self {
1939 storage_type: value.storage_type.into(),
1940 compression: value.compression.into(),
1941 row_group_size: value.row_group_size,
1942 row_id_mode: value.row_id_mode.into(),
1943 }
1944 }
1945}
1946
1947macro_rules! catalog_manifest_enum {
1948 ($manifest:ident, $native:ident, { $($variant:ident),+ $(,)? }) => {
1949 #[allow(missing_docs)]
1950 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1951 #[serde(rename_all = "snake_case")]
1952 pub enum $manifest { $($variant),+ }
1953
1954 impl From<$native> for $manifest {
1955 fn from(value: $native) -> Self {
1956 match value { $($native::$variant => Self::$variant),+ }
1957 }
1958 }
1959
1960 impl From<$manifest> for $native {
1961 fn from(value: $manifest) -> Self {
1962 match value { $($manifest::$variant => Self::$variant),+ }
1963 }
1964 }
1965 };
1966}
1967
1968catalog_manifest_enum!(CatalogManifestStorageType, StorageType, { Row, Columnar });
1969catalog_manifest_enum!(CatalogManifestCompression, Compression, { None, Lz4, Zstd });
1970catalog_manifest_enum!(CatalogManifestRowIdMode, RowIdMode, { None, Direct });
1971
1972#[allow(missing_docs)]
1974#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1975pub struct CatalogManifestIndex {
1976 pub index_id: u32,
1977 pub catalog_name: String,
1978 pub namespace_name: String,
1979 pub name: String,
1980 pub table: String,
1981 pub columns: Vec<String>,
1982 pub column_indices: Vec<usize>,
1983 pub unique: bool,
1984 pub method: Option<CatalogManifestIndexMethod>,
1985 pub options: Vec<(String, String)>,
1986}
1987
1988impl CatalogManifestIndex {
1989 fn fqn(&self) -> IndexFqn {
1990 IndexFqn::new(
1991 &self.catalog_name,
1992 &self.namespace_name,
1993 &self.table,
1994 &self.name,
1995 )
1996 }
1997
1998 fn to_metadata(&self) -> IndexMetadata {
1999 let mut index = IndexMetadata::new(
2000 self.index_id,
2001 self.name.clone(),
2002 self.table.clone(),
2003 self.columns.clone(),
2004 )
2005 .with_column_indices(self.column_indices.clone())
2006 .with_unique(self.unique)
2007 .with_options(self.options.clone());
2008 index.catalog_name = self.catalog_name.clone();
2009 index.namespace_name = self.namespace_name.clone();
2010 if let Some(method) = self.method {
2011 index = index.with_method(method.into());
2012 }
2013 index
2014 }
2015}
2016
2017impl From<&IndexMetadata> for CatalogManifestIndex {
2018 fn from(index: &IndexMetadata) -> Self {
2019 Self {
2020 index_id: index.index_id,
2021 catalog_name: index.catalog_name.clone(),
2022 namespace_name: index.namespace_name.clone(),
2023 name: index.name.clone(),
2024 table: index.table.clone(),
2025 columns: index.columns.clone(),
2026 column_indices: index.column_indices.clone(),
2027 unique: index.unique,
2028 method: index.method.map(Into::into),
2029 options: index.options.clone(),
2030 }
2031 }
2032}
2033
2034#[allow(missing_docs)]
2035#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2036#[serde(rename_all = "snake_case")]
2037pub enum CatalogManifestIndexMethod {
2038 BTree,
2039 Hnsw,
2040}
2041
2042impl From<IndexMethod> for CatalogManifestIndexMethod {
2043 fn from(value: IndexMethod) -> Self {
2044 match value {
2045 IndexMethod::BTree => Self::BTree,
2046 IndexMethod::Hnsw => Self::Hnsw,
2047 }
2048 }
2049}
2050
2051impl From<CatalogManifestIndexMethod> for IndexMethod {
2052 fn from(value: CatalogManifestIndexMethod) -> Self {
2053 match value {
2054 CatalogManifestIndexMethod::BTree => Self::BTree,
2055 CatalogManifestIndexMethod::Hnsw => Self::Hnsw,
2056 }
2057 }
2058}
2059
2060impl Database {
2061 pub fn export_catalog_manifest_delta(&self, catalog_version: u64) -> Result<Vec<u8>> {
2065 let catalog = self.sql_catalog.read().expect("catalog lock poisoned");
2066 let mut catalogs = catalog
2067 .list_catalogs()
2068 .iter()
2069 .map(CatalogManifestCatalog::from)
2070 .collect::<Vec<_>>();
2071 catalogs.sort_by(|left, right| left.name.cmp(&right.name));
2072 let mut namespaces = catalog
2073 .list_catalogs()
2074 .iter()
2075 .flat_map(|meta| {
2076 catalog
2077 .list_namespaces(&meta.name)
2078 .iter()
2079 .map(CatalogManifestNamespace::from)
2080 .collect::<Vec<_>>()
2081 })
2082 .collect::<Vec<_>>();
2083 namespaces.sort_by(|left, right| {
2084 (&left.catalog_name, &left.name).cmp(&(&right.catalog_name, &right.name))
2085 });
2086 let mut tables = catalog
2087 .list_tables()
2088 .iter()
2089 .map(CatalogManifestTable::from)
2090 .collect::<Vec<_>>();
2091 tables.sort_by(|left, right| {
2092 (&left.catalog_name, &left.namespace_name, &left.name).cmp(&(
2093 &right.catalog_name,
2094 &right.namespace_name,
2095 &right.name,
2096 ))
2097 });
2098 let mut indexes = catalog
2099 .list_tables()
2100 .iter()
2101 .flat_map(|table| {
2102 catalog
2103 .get_indexes_for_table(&table.name)
2104 .into_iter()
2105 .filter(|index| {
2106 index.catalog_name == table.catalog_name
2107 && index.namespace_name == table.namespace_name
2108 })
2109 .map(CatalogManifestIndex::from)
2110 .collect::<Vec<_>>()
2111 })
2112 .collect::<Vec<_>>();
2113 indexes.sort_by(|left, right| {
2114 (
2115 &left.catalog_name,
2116 &left.namespace_name,
2117 &left.table,
2118 &left.name,
2119 )
2120 .cmp(&(
2121 &right.catalog_name,
2122 &right.namespace_name,
2123 &right.table,
2124 &right.name,
2125 ))
2126 });
2127 CatalogManifestDelta {
2128 format_version: CatalogManifestDelta::FORMAT_VERSION,
2129 catalog_version,
2130 catalogs,
2131 namespaces,
2132 tables,
2133 indexes,
2134 }
2135 .encode()
2136 .map_err(catalog_manifest_encoding_error)
2137 }
2138
2139 pub fn catalog_manifest_version(&self) -> Result<Option<u64>> {
2143 let mut txn = self.store.begin(TxnMode::ReadOnly).map_err(Error::Core)?;
2144 let stored = txn
2145 .get(&CATALOG_MANIFEST_VERSION_KEY.to_vec())
2146 .map_err(Error::Core)?;
2147 txn.rollback_self().map_err(Error::Core)?;
2148 match stored {
2149 None => Ok(None),
2150 Some(bytes) if bytes.len() == std::mem::size_of::<u64>() => {
2151 let mut encoded = [0u8; std::mem::size_of::<u64>()];
2152 encoded.copy_from_slice(&bytes);
2153 Ok(Some(u64::from_be_bytes(encoded)))
2154 }
2155 Some(_) => Err(Error::Core(alopex_core::Error::InvalidFormat(
2156 "invalid stored schema manifest catalog version".to_string(),
2157 ))),
2158 }
2159 }
2160
2161 pub fn apply_schema_manifest(
2166 &self,
2167 member: impl Into<NodeId>,
2168 manifest: &SchemaManifest,
2169 ) -> SchemaApplyEvidence {
2170 let member = member.into();
2171 if manifest.catalog_delta_format != CATALOG_MANIFEST_DELTA_FORMAT {
2172 return apply_evidence(
2173 manifest,
2174 member,
2175 SchemaApplyState::Incompatible,
2176 None,
2177 false,
2178 "unsupported catalog manifest format",
2179 );
2180 }
2181 let actual_checksum = format!("{:x}", sha2::Sha256::digest(&manifest.catalog_delta));
2182 if actual_checksum != manifest.checksum {
2183 return apply_evidence(
2184 manifest,
2185 member,
2186 SchemaApplyState::Failed,
2187 None,
2188 false,
2189 "catalog manifest checksum mismatch",
2190 );
2191 }
2192 let delta = match CatalogManifestDelta::decode(&manifest.catalog_delta) {
2193 Ok(delta) => delta,
2194 Err(_) => {
2195 return apply_evidence(
2196 manifest,
2197 member,
2198 SchemaApplyState::Incompatible,
2199 None,
2200 false,
2201 "catalog manifest payload is not a supported structural document",
2202 );
2203 }
2204 };
2205 if let Err(detail) = validate_catalog_manifest_delta(&delta) {
2206 return apply_evidence(
2207 manifest,
2208 member,
2209 SchemaApplyState::Incompatible,
2210 None,
2211 false,
2212 detail,
2213 );
2214 }
2215 if delta.catalog_version != manifest.schema_version {
2216 return apply_evidence(
2217 manifest,
2218 member,
2219 SchemaApplyState::Incompatible,
2220 None,
2221 false,
2222 "catalog document version does not match the committed schema version",
2223 );
2224 }
2225 let current_version = match self.catalog_manifest_version() {
2226 Ok(version) => version.unwrap_or(0),
2227 Err(_) => {
2228 return apply_evidence(
2229 manifest,
2230 member,
2231 SchemaApplyState::Failed,
2232 None,
2233 false,
2234 "local catalog manifest version could not be read",
2235 );
2236 }
2237 };
2238 if current_version < manifest.compatibility.minimum_catalog_version
2239 || current_version > manifest.compatibility.maximum_catalog_version
2240 {
2241 return apply_evidence(
2242 manifest,
2243 member,
2244 SchemaApplyState::Incompatible,
2245 Some(current_version),
2246 false,
2247 "local catalog version is outside the manifest compatibility range",
2248 );
2249 }
2250 match self.apply_catalog_manifest_delta(&delta) {
2251 Ok(()) => apply_evidence(
2252 manifest,
2253 member,
2254 SchemaApplyState::Applied,
2255 Some(delta.catalog_version),
2256 true,
2257 "",
2258 ),
2259 Err(detail) => apply_evidence(
2260 manifest,
2261 member,
2262 SchemaApplyState::Failed,
2263 Some(current_version),
2264 false,
2265 detail,
2266 ),
2267 }
2268 }
2269
2270 fn apply_catalog_manifest_delta(
2271 &self,
2272 delta: &CatalogManifestDelta,
2273 ) -> std::result::Result<(), String> {
2274 let mut txn = self
2275 .store
2276 .begin(TxnMode::ReadWrite)
2277 .map_err(|error| error.to_string())?;
2278 let mut catalog = self
2279 .sql_catalog
2280 .write()
2281 .map_err(|_| "catalog lock poisoned".to_string())?;
2282 let mut overlay = CatalogOverlay::new();
2283
2284 for manifest_catalog in &delta.catalogs {
2285 match catalog.get_catalog(&manifest_catalog.name) {
2286 Some(existing) if CatalogManifestCatalog::from(&existing) == *manifest_catalog => {}
2287 Some(_) => {
2288 return Err(format!(
2289 "local catalog {} differs from the manifest",
2290 manifest_catalog.name
2291 ));
2292 }
2293 None => overlay.add_catalog(manifest_catalog.into()),
2294 }
2295 }
2296 for manifest_namespace in &delta.namespaces {
2297 match catalog.get_namespace(&manifest_namespace.catalog_name, &manifest_namespace.name)
2298 {
2299 Some(existing)
2300 if CatalogManifestNamespace::from(&existing) == *manifest_namespace => {}
2301 Some(_) => {
2302 return Err(format!(
2303 "local namespace {}.{} differs from the manifest",
2304 manifest_namespace.catalog_name, manifest_namespace.name
2305 ));
2306 }
2307 None => overlay.add_namespace(manifest_namespace.into()),
2308 }
2309 }
2310
2311 for table in &delta.tables {
2312 let existing = catalog.list_tables().into_iter().find(|candidate| {
2313 candidate.name == table.name
2314 && candidate.catalog_name == table.catalog_name
2315 && candidate.namespace_name == table.namespace_name
2316 });
2317 match existing {
2318 Some(existing) if CatalogManifestTable::from(&existing) == *table => {}
2319 Some(_) => {
2320 return Err(format!(
2321 "local table {}.{}.{} differs from the manifest",
2322 table.catalog_name, table.namespace_name, table.name
2323 ));
2324 }
2325 None => overlay.add_table(table.fqn(), table.to_metadata()),
2326 }
2327 }
2328
2329 for index in &delta.indexes {
2330 let existing = catalog.get_index(&index.name).filter(|candidate| {
2331 candidate.catalog_name == index.catalog_name
2332 && candidate.namespace_name == index.namespace_name
2333 && candidate.table == index.table
2334 });
2335 match existing {
2336 Some(existing) if CatalogManifestIndex::from(existing) == *index => {}
2337 Some(_) => {
2338 return Err(format!(
2339 "local index {}.{}.{} differs from the manifest",
2340 index.catalog_name, index.namespace_name, index.name
2341 ));
2342 }
2343 None => overlay.add_index(index.fqn(), index.to_metadata()),
2344 }
2345 }
2346
2347 catalog
2348 .persist_overlay(&mut txn, &overlay)
2349 .map_err(|error| error.to_string())?;
2350 txn.put(
2351 CATALOG_MANIFEST_VERSION_KEY.to_vec(),
2352 delta.catalog_version.to_be_bytes().to_vec(),
2353 )
2354 .map_err(|error| error.to_string())?;
2355 txn.commit_self().map_err(|error| error.to_string())?;
2356 catalog.apply_overlay(overlay);
2357 drop(catalog);
2358 self.invalidate_table_info_cache();
2359 self.hnsw_cache
2360 .write()
2361 .map_err(|_| "HNSW cache lock poisoned".to_string())?
2362 .clear();
2363 Ok(())
2364 }
2365}
2366
2367fn catalog_manifest_encoding_error(error: serde_json::Error) -> Error {
2368 Error::Core(alopex_core::Error::InvalidFormat(format!(
2369 "could not encode catalog manifest: {error}"
2370 )))
2371}
2372
2373fn validate_catalog_manifest_delta(
2374 delta: &CatalogManifestDelta,
2375) -> std::result::Result<(), &'static str> {
2376 if delta.format_version != CatalogManifestDelta::FORMAT_VERSION {
2377 return Err("unsupported catalog manifest document version");
2378 }
2379 let mut catalogs = BTreeSet::new();
2380 if delta
2381 .catalogs
2382 .iter()
2383 .any(|catalog| catalog.name.trim().is_empty() || !catalogs.insert(catalog.name.as_str()))
2384 {
2385 return Err("catalog manifest contains an invalid or duplicate catalog");
2386 }
2387 let mut namespaces = BTreeSet::new();
2388 if delta.namespaces.iter().any(|namespace| {
2389 namespace.catalog_name.trim().is_empty()
2390 || namespace.name.trim().is_empty()
2391 || !namespaces.insert((namespace.catalog_name.as_str(), namespace.name.as_str()))
2392 }) {
2393 return Err("catalog manifest contains an invalid or duplicate namespace");
2394 }
2395 let mut tables = BTreeSet::new();
2396 for table in &delta.tables {
2397 if table.name.trim().is_empty()
2398 || table.catalog_name.trim().is_empty()
2399 || table.namespace_name.trim().is_empty()
2400 || table.columns.is_empty()
2401 || !tables.insert((
2402 table.catalog_name.as_str(),
2403 table.namespace_name.as_str(),
2404 table.name.as_str(),
2405 ))
2406 {
2407 return Err("catalog manifest contains an invalid or duplicate table");
2408 }
2409 let mut columns = BTreeSet::new();
2410 if table
2411 .columns
2412 .iter()
2413 .any(|column| column.name.trim().is_empty() || !columns.insert(column.name.as_str()))
2414 {
2415 return Err("catalog manifest contains an invalid or duplicate column");
2416 }
2417 }
2418 let mut indexes = BTreeSet::new();
2419 for index in &delta.indexes {
2420 let table_key = (
2421 index.catalog_name.as_str(),
2422 index.namespace_name.as_str(),
2423 index.table.as_str(),
2424 );
2425 if index.name.trim().is_empty()
2426 || index.columns.is_empty()
2427 || !tables.contains(&table_key)
2428 || !indexes.insert((
2429 index.catalog_name.as_str(),
2430 index.namespace_name.as_str(),
2431 index.table.as_str(),
2432 index.name.as_str(),
2433 ))
2434 {
2435 return Err("catalog manifest contains an invalid, duplicate, or orphaned index");
2436 }
2437 }
2438 Ok(())
2439}
2440
2441fn apply_evidence(
2442 manifest: &SchemaManifest,
2443 member: NodeId,
2444 state: SchemaApplyState,
2445 catalog_version: Option<u64>,
2446 compatibility_verified: bool,
2447 detail: impl Into<String>,
2448) -> SchemaApplyEvidence {
2449 let detail = detail.into();
2450 SchemaApplyEvidence {
2451 manifest_id: manifest.id.clone(),
2452 member,
2453 state,
2454 catalog_version,
2455 checksum: (state == SchemaApplyState::Applied).then(|| manifest.checksum.clone()),
2456 compatibility_verified,
2457 failure_detail: (!detail.is_empty()).then_some(detail),
2458 }
2459}
2460
2461#[cfg(test)]
2462mod tests {
2463 use super::*;
2464 use crate::{Database, TxnMode};
2465 use alopex_sql::catalog::{ColumnMetadata, RowIdMode};
2466 use alopex_sql::ExecutionResult;
2467
2468 #[test]
2469 fn storage_info_default_is_row_none() {
2470 let info = StorageInfo::default();
2471 assert_eq!(info.storage_type, "row");
2472 assert_eq!(info.compression, "none");
2473 }
2474
2475 #[test]
2476 fn column_definition_defaults_to_nullable() {
2477 let column = ColumnDefinition::new("id", DataType::Integer);
2478 assert!(column.nullable);
2479 assert!(column.comment.is_none());
2480
2481 let column = column.with_nullable(false).with_comment("ID");
2482 assert!(!column.nullable);
2483 assert_eq!(column.comment.as_deref(), Some("ID"));
2484 }
2485
2486 #[test]
2487 fn create_catalog_request_builder_validates_name() {
2488 let err = CreateCatalogRequest::new("").build().unwrap_err();
2489 assert!(matches!(err, Error::Core(_)));
2490
2491 let request = CreateCatalogRequest::new("main")
2492 .with_comment("メイン")
2493 .with_storage_root("/data")
2494 .build()
2495 .unwrap();
2496 assert_eq!(request.name, "main");
2497 assert_eq!(request.comment.as_deref(), Some("メイン"));
2498 assert_eq!(request.storage_root.as_deref(), Some("/data"));
2499 }
2500
2501 #[test]
2502 fn create_namespace_request_builder_validates_fields() {
2503 let err = CreateNamespaceRequest::new("", "default")
2504 .build()
2505 .unwrap_err();
2506 assert!(matches!(err, Error::Core(_)));
2507
2508 let request = CreateNamespaceRequest::new("main", "analytics")
2509 .with_comment("分析")
2510 .build()
2511 .unwrap();
2512 assert_eq!(request.catalog_name, "main");
2513 assert_eq!(request.name, "analytics");
2514 assert_eq!(request.comment.as_deref(), Some("分析"));
2515 }
2516
2517 #[test]
2518 fn create_table_request_defaults_and_validation() {
2519 let schema = vec![ColumnDefinition::new("id", DataType::Integer)];
2520
2521 let request = CreateTableRequest::new("users")
2522 .with_schema(schema.clone())
2523 .build()
2524 .unwrap();
2525 assert_eq!(request.catalog_name, "default");
2526 assert_eq!(request.namespace_name, "default");
2527 assert_eq!(request.table_type, TableType::Managed);
2528 assert_eq!(request.data_source_format, Some(DataSourceFormat::Alopex));
2529 assert_eq!(request.properties.as_ref().unwrap().len(), 0);
2530
2531 let err = CreateTableRequest::new("users").build().unwrap_err();
2532 assert!(matches!(err, Error::SchemaRequired));
2533
2534 let err = CreateTableRequest::new("ext")
2535 .with_table_type(TableType::External)
2536 .build()
2537 .unwrap_err();
2538 assert!(matches!(err, Error::StorageRootRequired));
2539
2540 let request = CreateTableRequest::new("ext")
2541 .with_table_type(TableType::External)
2542 .with_storage_root("/external")
2543 .build()
2544 .unwrap();
2545 assert_eq!(request.storage_root.as_deref(), Some("/external"));
2546 assert_eq!(request.data_source_format, Some(DataSourceFormat::Alopex));
2547 assert!(request.properties.as_ref().unwrap().is_empty());
2548 }
2549
2550 #[test]
2551 fn table_info_converts_from_metadata() {
2552 let mut table = TableMetadata::new(
2553 "users",
2554 vec![
2555 ColumnMetadata::new("id", ResolvedType::Integer).with_primary_key(true),
2556 ColumnMetadata::new("name", ResolvedType::Text),
2557 ],
2558 )
2559 .with_table_id(42);
2560 table.catalog_name = "main".to_string();
2561 table.namespace_name = "default".to_string();
2562 table.primary_key = Some(vec!["id".to_string()]);
2563 table.storage_options = StorageOptions {
2564 storage_type: StorageType::Columnar,
2565 compression: Compression::Zstd,
2566 row_group_size: 1024,
2567 row_id_mode: RowIdMode::Direct,
2568 };
2569
2570 let info = TableInfo::from(table);
2571 assert_eq!(info.name, "users");
2572 assert_eq!(info.table_id, 42);
2573 assert_eq!(info.catalog_name, "main");
2574 assert_eq!(info.namespace_name, "default");
2575 assert_eq!(info.columns.len(), 2);
2576 assert_eq!(info.columns[0].data_type, "INTEGER");
2577 assert!(info.columns[0].is_primary_key);
2578 assert_eq!(info.storage_options.storage_type, "columnar");
2579 assert_eq!(info.storage_options.compression, "zstd");
2580 }
2581
2582 #[test]
2583 fn table_info_defaults_storage_options_to_row_none() {
2584 let table = TableMetadata::new(
2585 "logs",
2586 vec![ColumnMetadata::new("id", ResolvedType::Integer)],
2587 );
2588 let info = TableInfo::from(table);
2589 assert_eq!(info.storage_options.storage_type, "row");
2590 assert_eq!(info.storage_options.compression, "none");
2591 }
2592
2593 #[test]
2594 fn index_info_converts_from_metadata() {
2595 let mut index = IndexMetadata::new(1, "idx_users_id", "users", vec!["id".to_string()])
2596 .with_unique(true)
2597 .with_method(IndexMethod::Hnsw);
2598 index.catalog_name = "main".to_string();
2599 index.namespace_name = "default".to_string();
2600
2601 let info = IndexInfo::from(index);
2602 assert_eq!(info.name, "idx_users_id");
2603 assert_eq!(info.table_name, "users");
2604 assert_eq!(info.method, "hnsw");
2605 assert!(info.is_unique);
2606 }
2607
2608 fn ensure_default_catalog_and_namespace(db: &Database) {
2609 let _ = db.create_catalog(CreateCatalogRequest::new("default"));
2610 let _ = db.create_namespace(CreateNamespaceRequest::new("default", "default"));
2611 }
2612
2613 fn manifest_from_delta(delta: Vec<u8>, version: u64) -> SchemaManifest {
2614 SchemaManifest {
2615 id: alopex_cluster::SchemaManifestId::new("manifest-1"),
2616 parent_id: None,
2617 schema_version: version,
2618 catalog_delta_format: CATALOG_MANIFEST_DELTA_FORMAT.to_string(),
2619 checksum: format!("{:x}", sha2::Sha256::digest(&delta)),
2620 catalog_delta: delta,
2621 compatibility: alopex_cluster::SchemaCompatibility {
2622 minimum_catalog_version: 0,
2623 maximum_catalog_version: version,
2624 },
2625 owner: alopex_cluster::NodeId::new("node-a"),
2626 created_at_epoch: 3,
2627 }
2628 }
2629
2630 #[test]
2631 fn verified_manifest_apply_makes_sql_catalog_and_reported_version_agree() {
2632 let source = Database::new();
2633 ensure_default_catalog_and_namespace(&source);
2634 source
2635 .execute_sql("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);")
2636 .unwrap();
2637 source
2638 .execute_sql("CREATE INDEX idx_users_name ON users (name);")
2639 .unwrap();
2640 let manifest = manifest_from_delta(source.export_catalog_manifest_delta(7).unwrap(), 7);
2641
2642 let target = Database::new();
2643 let evidence = target.apply_schema_manifest("node-b", &manifest);
2644
2645 assert_eq!(evidence.state, SchemaApplyState::Applied);
2646 assert_eq!(evidence.catalog_version, Some(7));
2647 assert_eq!(
2648 evidence.checksum.as_deref(),
2649 Some(manifest.checksum.as_str())
2650 );
2651 assert!(evidence.compatibility_verified);
2652 assert_eq!(target.catalog_manifest_version().unwrap(), Some(7));
2653 assert!(matches!(
2654 target
2655 .execute_sql("INSERT INTO users (id, name) VALUES (1, 'alice');")
2656 .unwrap(),
2657 ExecutionResult::RowsAffected(1)
2658 ));
2659 assert_eq!(
2660 target.get_table_info_simple("users").unwrap().table_id,
2661 source.get_table_info_simple("users").unwrap().table_id
2662 );
2663 let indexes = target.list_indexes_simple("users").unwrap();
2664 assert!(indexes.iter().any(|index| index.name == "idx_users_name"));
2665 }
2666
2667 #[test]
2668 fn corrupted_or_incompatible_catalog_never_returns_applied_evidence() {
2669 let source = Database::new();
2670 ensure_default_catalog_and_namespace(&source);
2671 source
2672 .execute_sql("CREATE TABLE users (id INTEGER PRIMARY KEY);")
2673 .unwrap();
2674 let mut corrupt = manifest_from_delta(source.export_catalog_manifest_delta(4).unwrap(), 4);
2675 corrupt.checksum = "wrong".to_string();
2676 let target = Database::new();
2677 let evidence = target.apply_schema_manifest("node-b", &corrupt);
2678 assert_eq!(evidence.state, SchemaApplyState::Failed);
2679 assert_eq!(target.catalog_manifest_version().unwrap(), None);
2680 assert!(target
2681 .execute_sql("INSERT INTO users (id) VALUES (1);")
2682 .is_err());
2683
2684 let manifest = manifest_from_delta(source.export_catalog_manifest_delta(4).unwrap(), 4);
2685 let mismatched = Database::new();
2686 ensure_default_catalog_and_namespace(&mismatched);
2687 mismatched
2688 .execute_sql("CREATE TABLE users (id TEXT PRIMARY KEY);")
2689 .unwrap();
2690 let evidence = mismatched.apply_schema_manifest("node-b", &manifest);
2691 assert_eq!(evidence.state, SchemaApplyState::Failed);
2692 assert_eq!(mismatched.catalog_manifest_version().unwrap(), None);
2693 }
2694
2695 #[test]
2696 fn database_catalog_and_namespace_crud() {
2697 let db = Database::new();
2698
2699 let catalog = db
2700 .create_catalog(CreateCatalogRequest::new("main"))
2701 .unwrap();
2702 assert_eq!(catalog.name, "main");
2703
2704 let namespace = db
2705 .create_namespace(CreateNamespaceRequest::new("main", "analytics"))
2706 .unwrap();
2707 assert_eq!(namespace.catalog_name, "main");
2708 assert_eq!(namespace.name, "analytics");
2709
2710 let list = db.list_namespaces("main").unwrap();
2711 assert_eq!(list.len(), 1);
2712
2713 let err = db.delete_catalog("main", false).unwrap_err();
2714 assert!(matches!(err, Error::CatalogNotEmpty(_)));
2715
2716 db.delete_catalog("main", true).unwrap();
2717
2718 let err = db.get_catalog("main").unwrap_err();
2719 assert!(matches!(err, Error::CatalogNotFound(_)));
2720 }
2721
2722 #[test]
2723 fn cannot_delete_default_catalog_or_namespace() {
2724 let db = Database::new();
2725 ensure_default_catalog_and_namespace(&db);
2726
2727 let err = db.delete_catalog("default", true).unwrap_err();
2728 assert!(matches!(err, Error::CannotDeleteDefault(_)));
2729
2730 let err = db.delete_namespace("default", "default", true).unwrap_err();
2731 assert!(matches!(err, Error::CannotDeleteDefault(_)));
2732
2733 let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
2734 let err = txn.delete_catalog("default", true).unwrap_err();
2735 assert!(matches!(err, Error::CannotDeleteDefault(_)));
2736
2737 let err = txn
2738 .delete_namespace("default", "default", true)
2739 .unwrap_err();
2740 assert!(matches!(err, Error::CannotDeleteDefault(_)));
2741 }
2742
2743 #[test]
2744 fn database_table_crud_and_simple_helpers() {
2745 let db = Database::new();
2746 ensure_default_catalog_and_namespace(&db);
2747
2748 let schema = vec![ColumnDefinition::new("id", DataType::Integer)];
2749 let info = db.create_table_simple("users", schema).unwrap();
2750 assert_eq!(info.catalog_name, "default");
2751 assert_eq!(info.namespace_name, "default");
2752 assert_eq!(info.table_type, TableType::Managed);
2753 assert_eq!(info.data_source_format, DataSourceFormat::Alopex);
2754 assert_eq!(info.storage_options.storage_type, "row");
2755 assert_eq!(info.storage_options.compression, "none");
2756
2757 let tables = db.list_tables_simple().unwrap();
2758 assert_eq!(tables.len(), 1);
2759
2760 let info = db.get_table_info_simple("users").unwrap();
2761 assert_eq!(info.name, "users");
2762
2763 let err = db
2764 .create_table_simple(
2765 "users",
2766 vec![ColumnDefinition::new("id", DataType::Integer)],
2767 )
2768 .unwrap_err();
2769 assert!(matches!(err, Error::TableAlreadyExists(_)));
2770
2771 db.delete_table_simple("users").unwrap();
2772 assert!(db.list_tables_simple().unwrap().is_empty());
2773 }
2774
2775 #[test]
2776 fn database_index_read_helpers() {
2777 let db = Database::new();
2778 ensure_default_catalog_and_namespace(&db);
2779
2780 let schema = vec![ColumnDefinition::new("id", DataType::Integer)];
2781 db.create_table_simple("users", schema).unwrap();
2782
2783 let result = db
2784 .execute_sql("CREATE INDEX idx_users_id ON users (id);")
2785 .unwrap();
2786 assert!(matches!(result, ExecutionResult::Success));
2787
2788 let indexes = db.list_indexes_simple("users").unwrap();
2789 assert_eq!(indexes.len(), 1);
2790 assert_eq!(indexes[0].name, "idx_users_id");
2791 assert_eq!(indexes[0].method, "btree");
2792
2793 let index = db.get_index_info_simple("users", "idx_users_id").unwrap();
2794 assert_eq!(index.table_name, "users");
2795 }
2796
2797 #[test]
2798 fn transaction_overlay_visibility_and_commit() {
2799 let db = Database::new();
2800 let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
2801
2802 txn.create_catalog(CreateCatalogRequest::new("main"))
2803 .unwrap();
2804 txn.create_namespace(CreateNamespaceRequest::new("main", "default"))
2805 .unwrap();
2806
2807 let schema = vec![ColumnDefinition::new("id", DataType::Integer)];
2808 txn.create_table(
2809 CreateTableRequest::new("events")
2810 .with_catalog_name("main")
2811 .with_namespace_name("default")
2812 .with_schema(schema),
2813 )
2814 .unwrap();
2815
2816 let tables = txn.list_tables("main", "default").unwrap();
2817 assert_eq!(tables.len(), 1);
2818
2819 txn.commit().unwrap();
2820
2821 let info = db.get_table_info("main", "default", "events").unwrap();
2822 assert_eq!(info.name, "events");
2823 }
2824
2825 #[test]
2826 fn transaction_commit_persists_overlay_to_store() {
2827 let db = Database::new();
2828 let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
2829
2830 txn.create_catalog(CreateCatalogRequest::new("main"))
2831 .unwrap();
2832 txn.create_namespace(CreateNamespaceRequest::new("main", "default"))
2833 .unwrap();
2834
2835 let schema = vec![ColumnDefinition::new("id", DataType::Integer)];
2836 txn.create_table(
2837 CreateTableRequest::new("events")
2838 .with_catalog_name("main")
2839 .with_namespace_name("default")
2840 .with_schema(schema),
2841 )
2842 .unwrap();
2843
2844 txn.commit().unwrap();
2845
2846 let reloaded = alopex_sql::catalog::PersistentCatalog::load(db.store.clone()).unwrap();
2847 assert!(reloaded.get_catalog("main").is_some());
2848 assert!(reloaded.get_namespace("main", "default").is_some());
2849 assert!(reloaded.table_exists("events"));
2850 }
2851
2852 #[test]
2853 fn transaction_rollback_discards_overlay() {
2854 let db = Database::new();
2855 let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
2856
2857 txn.create_catalog(CreateCatalogRequest::new("main"))
2858 .unwrap();
2859 txn.create_namespace(CreateNamespaceRequest::new("main", "default"))
2860 .unwrap();
2861
2862 let schema = vec![ColumnDefinition::new("id", DataType::Integer)];
2863 txn.create_table(
2864 CreateTableRequest::new("staging")
2865 .with_catalog_name("main")
2866 .with_namespace_name("default")
2867 .with_schema(schema),
2868 )
2869 .unwrap();
2870
2871 txn.rollback().unwrap();
2872
2873 let err = db.get_table_info("main", "default", "staging").unwrap_err();
2874 assert!(matches!(err, Error::CatalogNotFound(_)));
2875 }
2876
2877 #[test]
2878 fn transaction_readonly_rejects_ddl() {
2879 let db = Database::new();
2880 let mut txn = db.begin(TxnMode::ReadOnly).unwrap();
2881 let err = txn
2882 .create_catalog(CreateCatalogRequest::new("main"))
2883 .unwrap_err();
2884 assert!(matches!(err, Error::TxnReadOnly));
2885 }
2886}