Skip to main content

alopex_embedded/
catalog_api.rs

1//! Catalog API 向けの公開型定義。
2
3use 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/// Catalog 情報(公開 API 返却用)。
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct CatalogInfo {
23    /// Catalog 名。
24    pub name: String,
25    /// コメント。
26    pub comment: Option<String>,
27    /// ストレージルート。
28    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/// Namespace 情報(公開 API 返却用)。
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct NamespaceInfo {
44    /// Namespace 名。
45    pub name: String,
46    /// 所属 Catalog 名。
47    pub catalog_name: String,
48    /// コメント。
49    pub comment: Option<String>,
50    /// ストレージルート。
51    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/// カラム情報(公開 API 返却用)。
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct ColumnInfo {
68    /// カラム名。
69    pub name: String,
70    /// データ型(例: "INTEGER", "TEXT", "VECTOR(128, COSINE)")。
71    pub data_type: String,
72    /// NULL 許可。
73    pub nullable: bool,
74    /// 主キーの一部かどうか。
75    pub is_primary_key: bool,
76    /// コメント。
77    pub comment: Option<String>,
78}
79
80/// ストレージ設定情報(TableInfo 返却用)。
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct StorageInfo {
83    /// ストレージ種別("row" | "columnar")。
84    pub storage_type: String,
85    /// 圧縮方式("none" | "lz4" | "zstd")。
86    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/// テーブル情報(公開 API 返却用)。
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub struct TableInfo {
119    /// テーブル名。
120    pub name: String,
121    /// 所属 Catalog 名。
122    pub catalog_name: String,
123    /// 所属 Namespace 名。
124    pub namespace_name: String,
125    /// テーブル ID。
126    pub table_id: u32,
127    /// テーブル種別。
128    pub table_type: TableType,
129    /// カラム情報。
130    pub columns: Vec<ColumnInfo>,
131    /// 主キー。
132    pub primary_key: Option<Vec<String>>,
133    /// ストレージロケーション。
134    pub storage_location: Option<String>,
135    /// データソース形式。
136    pub data_source_format: DataSourceFormat,
137    /// ストレージ設定。
138    pub storage_options: StorageInfo,
139    /// コメント。
140    pub comment: Option<String>,
141    /// カスタムプロパティ。
142    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/// インデックス情報(公開 API 返却用)。
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct IndexInfo {
195    /// インデックス名。
196    pub name: String,
197    /// インデックス ID。
198    pub index_id: u32,
199    /// 所属 Catalog 名。
200    pub catalog_name: String,
201    /// 所属 Namespace 名。
202    pub namespace_name: String,
203    /// 対象テーブル名。
204    pub table_name: String,
205    /// 対象カラム名。
206    pub columns: Vec<String>,
207    /// インデックス方式("btree" | "hnsw")。
208    pub method: String,
209    /// ユニーク制約。
210    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/// Catalog 作成リクエスト。
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct CreateCatalogRequest {
241    /// Catalog 名。
242    pub name: String,
243    /// コメント。
244    pub comment: Option<String>,
245    /// ストレージルート。
246    pub storage_root: Option<String>,
247}
248
249impl CreateCatalogRequest {
250    /// 必須フィールドを指定して作成する。
251    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    /// コメントを指定する。
260    pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
261        self.comment = Some(comment.into());
262        self
263    }
264
265    /// ストレージルートを指定する。
266    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    /// 必須フィールドを検証して返す。
272    pub fn build(self) -> Result<Self> {
273        validate_required(&self.name, "catalog 名")?;
274        Ok(self)
275    }
276}
277
278/// Namespace 作成リクエスト。
279#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct CreateNamespaceRequest {
281    /// 所属 Catalog 名。
282    pub catalog_name: String,
283    /// Namespace 名。
284    pub name: String,
285    /// コメント。
286    pub comment: Option<String>,
287    /// ストレージルート。
288    pub storage_root: Option<String>,
289}
290
291impl CreateNamespaceRequest {
292    /// 必須フィールドを指定して作成する。
293    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    /// コメントを指定する。
303    pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
304        self.comment = Some(comment.into());
305        self
306    }
307
308    /// ストレージルートを指定する。
309    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    /// 必須フィールドを検証して返す。
315    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/// テーブル作成リクエスト。
323#[derive(Debug, Clone)]
324pub struct CreateTableRequest {
325    /// Catalog 名(既定: "default")。
326    pub catalog_name: String,
327    /// Namespace 名(既定: "default")。
328    pub namespace_name: String,
329    /// テーブル名。
330    pub name: String,
331    /// スキーマ。
332    pub schema: Option<Vec<ColumnDefinition>>,
333    /// テーブル種別(既定: Managed)。
334    pub table_type: TableType,
335    /// データソース形式(None の場合は Alopex)。
336    pub data_source_format: Option<DataSourceFormat>,
337    /// 主キー。
338    pub primary_key: Option<Vec<String>>,
339    /// ストレージルート。
340    pub storage_root: Option<String>,
341    /// ストレージオプション。
342    pub storage_options: Option<StorageOptions>,
343    /// コメント。
344    pub comment: Option<String>,
345    /// カスタムプロパティ(None の場合は空の HashMap)。
346    pub properties: Option<HashMap<String, String>>,
347}
348
349impl CreateTableRequest {
350    /// 必須フィールドを指定して作成する。
351    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    /// Catalog 名を指定する。
368    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    /// Namespace 名を指定する。
374    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    /// スキーマを指定する。
380    pub fn with_schema(mut self, schema: Vec<ColumnDefinition>) -> Self {
381        self.schema = Some(schema);
382        self
383    }
384
385    /// テーブル種別を指定する。
386    pub fn with_table_type(mut self, table_type: TableType) -> Self {
387        self.table_type = table_type;
388        self
389    }
390
391    /// データソース形式を指定する。
392    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    /// 主キーを指定する。
398    pub fn with_primary_key(mut self, primary_key: Vec<String>) -> Self {
399        self.primary_key = Some(primary_key);
400        self
401    }
402
403    /// ストレージルートを指定する。
404    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    /// ストレージオプションを指定する。
410    pub fn with_storage_options(mut self, storage_options: StorageOptions) -> Self {
411        self.storage_options = Some(storage_options);
412        self
413    }
414
415    /// コメントを指定する。
416    pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
417        self.comment = Some(comment.into());
418        self
419    }
420
421    /// カスタムプロパティを指定する。
422    pub fn with_properties(mut self, properties: HashMap<String, String>) -> Self {
423        self.properties = Some(properties);
424        self
425    }
426
427    /// 必須フィールドを検証して返す。
428    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/// カラム定義。
451#[derive(Debug, Clone)]
452pub struct ColumnDefinition {
453    /// カラム名。
454    pub name: String,
455    /// データ型。
456    pub data_type: DataType,
457    /// NULL 許可(既定: true)。
458    pub nullable: bool,
459    /// コメント。
460    pub comment: Option<String>,
461}
462
463impl ColumnDefinition {
464    /// 必須フィールドを指定して作成する。
465    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    /// NULL 許可を指定する。
475    pub fn with_nullable(mut self, nullable: bool) -> Self {
476        self.nullable = nullable;
477        self
478    }
479
480    /// コメントを指定する。
481    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    /// Catalog 一覧を取得する。
489    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    /// Catalog を取得する。
499    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    /// Namespace 一覧を取得する。
508    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    /// Namespace を取得する。
519    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    /// テーブル一覧を取得する。
531    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    /// デフォルト catalog/namespace のテーブル一覧を取得する。
552    pub fn list_tables_simple(&self) -> Result<Vec<TableInfo>> {
553        self.list_tables("default", "default")
554    }
555
556    /// テーブル情報を取得する。
557    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    /// デフォルト catalog/namespace のテーブル情報を取得する。
588    pub fn get_table_info_simple(&self, table_name: &str) -> Result<TableInfo> {
589        self.get_table_info("default", "default", table_name)
590    }
591
592    /// テーブル情報をキャッシュから取得する(キャッシュミス時は DB ルックアップ)。
593    ///
594    /// パフォーマンス最適化のため、キャッシュを使用してテーブルメタデータを取得する。
595    /// DDL 操作(テーブル作成/削除など)が発生するとキャッシュは自動的に無効化される。
596    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        // Check cache first
603        if let Some(cached) = self.get_cached_table_info(catalog_name, namespace_name, table_name) {
604            return Ok(cached);
605        }
606
607        // Cache miss: fetch from database and cache the result
608        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    /// インデックス一覧を取得する。
618    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    /// デフォルト catalog/namespace のインデックス一覧を取得する。
635    pub fn list_indexes_simple(&self, table_name: &str) -> Result<Vec<IndexInfo>> {
636        self.list_indexes("default", "default", table_name)
637    }
638
639    /// インデックス情報を取得する。
640    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    /// デフォルト catalog/namespace のインデックス情報を取得する。
662    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    /// Catalog を作成する。
667    ///
668    /// # Examples
669    ///
670    /// ```
671    /// use alopex_embedded::{CreateCatalogRequest, Database};
672    ///
673    /// let db = Database::new();
674    /// let catalog = db.create_catalog(CreateCatalogRequest::new("main")).unwrap();
675    /// assert_eq!(catalog.name, "main");
676    /// ```
677    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    /// Catalog を削除する。
696    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    /// Namespace を作成する。
725    ///
726    /// # Examples
727    ///
728    /// ```
729    /// use alopex_embedded::{CreateCatalogRequest, CreateNamespaceRequest, Database};
730    ///
731    /// let db = Database::new();
732    /// db.create_catalog(CreateCatalogRequest::new("main")).unwrap();
733    /// let namespace = db
734    ///     .create_namespace(CreateNamespaceRequest::new("main", "analytics"))
735    ///     .unwrap();
736    /// assert_eq!(namespace.catalog_name, "main");
737    /// assert_eq!(namespace.name, "analytics");
738    /// ```
739    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    /// Namespace を削除する。
772    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    /// テーブルを作成する。
818    ///
819    /// # Examples
820    ///
821    /// ```
822    /// use alopex_embedded::{
823    ///     ColumnDefinition, CreateCatalogRequest, CreateNamespaceRequest, CreateTableRequest,
824    ///     Database,
825    /// };
826    /// use alopex_sql::ast::ddl::DataType;
827    ///
828    /// let db = Database::new();
829    /// db.create_catalog(CreateCatalogRequest::new("default")).unwrap();
830    /// db.create_namespace(CreateNamespaceRequest::new("default", "default"))
831    ///     .unwrap();
832    ///
833    /// let schema = vec![ColumnDefinition::new("id", DataType::Integer)];
834    /// let table = db
835    ///     .create_table(CreateTableRequest::new("users").with_schema(schema))
836    ///     .unwrap();
837    /// assert_eq!(table.name, "users");
838    /// ```
839    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); // Release lock before invalidating cache
896        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    /// デフォルト catalog/namespace のテーブルを作成する。
904    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    /// テーブルを削除する。
913    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); // Release lock before invalidating cache
937        self.invalidate_table_info_cache();
938        Ok(())
939    }
940
941    /// デフォルト catalog/namespace のテーブルを削除する。
942    pub fn delete_table_simple(&self, name: &str) -> Result<()> {
943        self.delete_table("default", "default", name)
944    }
945}
946
947impl<'a> Transaction<'a> {
948    /// Catalog 一覧を取得する(オーバーレイ反映)。
949    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    /// Catalog を取得する(オーバーレイ反映)。
959    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    /// Namespace 一覧を取得する(オーバーレイ反映)。
968    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    /// Namespace を取得する(オーバーレイ反映)。
979    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    /// テーブル一覧を取得する(オーバーレイ反映)。
991    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    /// テーブル情報を取得する(オーバーレイ反映)。
1017    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    /// Catalog を作成する(オーバーレイ反映)。
1053    ///
1054    /// # Examples
1055    ///
1056    /// ```
1057    /// use alopex_embedded::{CreateCatalogRequest, Database, TxnMode};
1058    ///
1059    /// let db = Database::new();
1060    /// let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
1061    /// let catalog = txn.create_catalog(CreateCatalogRequest::new("main")).unwrap();
1062    /// assert_eq!(catalog.name, "main");
1063    /// ```
1064    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    /// Catalog を削除する(オーバーレイ反映)。
1086    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    /// Namespace を作成する(オーバーレイ反映)。
1117    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    /// Namespace を削除する(オーバーレイ反映)。
1149    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    /// テーブルを作成する(オーバーレイ反映)。
1188    ///
1189    /// # Examples
1190    ///
1191    /// ```
1192    /// use alopex_embedded::{
1193    ///     ColumnDefinition, CreateCatalogRequest, CreateNamespaceRequest, CreateTableRequest,
1194    ///     Database, TxnMode,
1195    /// };
1196    /// use alopex_sql::ast::ddl::DataType;
1197    ///
1198    /// let db = Database::new();
1199    /// let mut txn = db.begin(TxnMode::ReadWrite).unwrap();
1200    /// txn.create_catalog(CreateCatalogRequest::new("main")).unwrap();
1201    /// txn.create_namespace(CreateNamespaceRequest::new("main", "default"))
1202    ///     .unwrap();
1203    ///
1204    /// let schema = vec![ColumnDefinition::new("id", DataType::Integer)];
1205    /// let table = txn
1206    ///     .create_table(
1207    ///         CreateTableRequest::new("events")
1208    ///             .with_catalog_name("main")
1209    ///             .with_namespace_name("default")
1210    ///             .with_schema(schema),
1211    ///     )
1212    ///     .unwrap();
1213    /// assert_eq!(table.name, "events");
1214    /// ```
1215    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    /// テーブルを削除する(オーバーレイ反映)。
1283    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
1570/// The only catalog representation accepted in a schema manifest.  It is a
1571/// versioned structural document, never user-supplied SQL or Rust-private
1572/// catalog persistence bytes.
1573pub 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/// Stable, versioned public representation of the catalog carried by a
1578/// schema manifest.  The snapshot is additive: an existing object must be
1579/// identical, while a missing object is created atomically with the reported
1580/// catalog version.  Destructive schema replacement is intentionally outside
1581/// this Phase 1 control path.
1582#[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/// Catalog metadata in [`CatalogManifestDelta`].
1609#[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/// Namespace metadata in [`CatalogManifestDelta`].
1638#[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/// Table definition in [`CatalogManifestDelta`].
1670#[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/// Column definition in [`CatalogManifestTable`].  SQL default expressions
1740/// are deliberately excluded because the persistent catalog does not retain
1741/// them as a portable catalog contract.
1742#[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/// Index definition in [`CatalogManifestDelta`].
1973#[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    /// Exports the currently visible SQL catalog as a versioned manifest
2062    /// document.  This captures catalog metadata only; it does not transport
2063    /// user SQL or table data.
2064    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    /// Returns the catalog version that was atomically recorded with the last
2140    /// successfully applied schema manifest.  `None` is an untouched local
2141    /// catalog, which compatibility checks treat as version zero.
2142    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    /// Applies a committed management manifest to this member's local SQL
2162    /// catalog.  The returned evidence is suitable for
2163    /// `SchemaApplyEvidenceAdapter`; it reports `Applied` only after the
2164    /// catalog overlay and manifest version commit together.
2165    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}