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