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