1use super::store::CatalogStore;
2use crate::catalog::{
3 Catalog, CatalogError, CatalogResult, CatalogSchemaId, CatalogTable, SchemaChange, TableId,
4 TableIdentifier,
5};
6use crate::evolution::{
7 FieldTransform, apply_schema_changes, compile_column_evolution, schema_field_ids,
8};
9use crate::metadata::TableMetadata;
10use crate::snapshot::TableSnapshotCommitter;
11use crate::write::{TABLE_WRITE_PLAN_FORMAT, TABLE_WRITE_PLAN_VERSION};
12use crate::{
13 FieldId, ReadOnlyTableBuilder, Table, TableError, TableReaderBuilder, TableSchema,
14 TableWriteBuilder, TableWritePlan, TableWriterBuilder,
15};
16use cobble::{
17 ColumnFamilyOptions, Config, CoordinatorConfig, Db, DbCoordinator, VolumeDescriptor,
18 VolumeUsageKind,
19};
20use serde::{Deserialize, Serialize, de::DeserializeOwned};
21use sha2::{Digest, Sha256};
22use std::collections::{HashMap, HashSet};
23use std::fmt::Write as _;
24use std::path::Path;
25use std::sync::{Arc, Mutex, OnceLock, Weak};
26use url::Url;
27use uuid::Uuid;
28
29#[cfg(test)]
30#[path = "../../tests/unit/catalog_storage.rs"]
31mod storage_tests;
32
33impl From<cobble::Error> for CatalogError {
34 fn from(error: cobble::Error) -> Self {
35 Self::Backend(Box::new(error))
36 }
37}
38
39const CATALOG_FORMAT: &str = "cobble-table-catalog";
40const CATALOG_VERSION: u32 = 1;
41
42pub(crate) struct CatalogRuntimeContext {
46 config: Config,
47 storage_id: String,
48}
49
50impl CatalogRuntimeContext {
51 fn scoped_config(&self, runtime: Config, table_id: TableId) -> Config {
52 scoped_table_config(&self.config.volumes, &self.storage_id, table_id, runtime)
53 }
54}
55
56fn scoped_table_config(
57 shared_volumes: &[VolumeDescriptor],
58 storage_id: &str,
59 table_id: TableId,
60 mut runtime: Config,
61) -> Config {
62 let relative_root = format!("{storage_id}/tables/TABLE-{table_id}");
63 let mut volumes = shared_volumes
64 .iter()
65 .filter_map(|volume| shared_volume(volume, &relative_root))
66 .collect::<Vec<_>>();
67 volumes.extend(
68 runtime
69 .volumes
70 .iter()
71 .flat_map(|volume| runtime_volumes(volume, &relative_root)),
72 );
73 runtime.volumes = volumes;
74 runtime
75}
76
77fn shared_volume(source: &VolumeDescriptor, relative_root: &str) -> Option<VolumeDescriptor> {
78 plan_shared_volume(source).map(|mut volume| {
79 volume.base_dir = append_relative_path(&volume.base_dir, relative_root);
80 volume
81 })
82}
83
84fn plan_shared_volume(source: &VolumeDescriptor) -> Option<VolumeDescriptor> {
85 let mut volume = source.clone();
86 volume.kinds = 0;
87 for kind in [
88 VolumeUsageKind::Meta,
89 VolumeUsageKind::Snapshot,
90 VolumeUsageKind::Wal,
91 ] {
92 if source.supports(kind) {
93 volume.set_usage(kind);
94 }
95 }
96 (volume.kinds != 0).then_some(volume)
97}
98
99fn runtime_volumes(source: &VolumeDescriptor, relative_root: &str) -> Vec<VolumeDescriptor> {
100 let mut owned = source.clone();
101 owned.kinds = 0;
102 for kind in [
103 VolumeUsageKind::PrimaryDataPriorityHigh,
104 VolumeUsageKind::PrimaryDataPriorityMedium,
105 VolumeUsageKind::PrimaryDataPriorityLow,
106 VolumeUsageKind::Cache,
107 ] {
108 if source.supports(kind) {
109 owned.set_usage(kind);
110 }
111 }
112 let mut volumes = Vec::new();
113 if owned.kinds != 0 {
114 owned.base_dir = append_relative_path(&owned.base_dir, relative_root);
115 volumes.push(owned);
116 }
117 if source.supports(VolumeUsageKind::Readonly) {
118 let mut readonly = source.clone();
119 readonly.kinds = 0;
120 readonly.set_usage(VolumeUsageKind::Readonly);
121 volumes.push(readonly);
122 }
123 volumes
124}
125
126fn append_relative_path(base: &str, relative: &str) -> String {
127 if let Ok(mut url) = Url::parse(base) {
128 let parent = url.path().trim_end_matches('/');
129 let child = relative.trim_matches('/');
130 let path = match (parent, child) {
131 ("", child) => format!("/{child}"),
132 (parent, "") => parent.to_string(),
133 (parent, child) => format!("{parent}/{child}"),
134 };
135 url.set_path(&path);
136 return url.to_string();
137 }
138 Path::new(base)
139 .join(relative)
140 .to_string_lossy()
141 .into_owned()
142}
143
144#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct FileCatalogConfig {
150 storage_id: String,
151}
152
153impl FileCatalogConfig {
154 pub fn new(storage_id: impl Into<String>) -> Self {
155 Self {
156 storage_id: storage_id.into(),
157 }
158 }
159
160 pub fn storage_id(&self) -> &str {
161 &self.storage_id
162 }
163}
164
165#[derive(Clone, Debug, PartialEq, Eq)]
167pub struct ShardSchemaMapping {
168 table_id: TableId,
169 db_id: String,
170 catalog_schema_id: CatalogSchemaId,
171 core_schema_id: u64,
172}
173
174impl ShardSchemaMapping {
175 pub fn table_id(&self) -> TableId {
176 self.table_id
177 }
178
179 pub fn db_id(&self) -> &str {
180 &self.db_id
181 }
182
183 pub fn catalog_schema_id(&self) -> CatalogSchemaId {
184 self.catalog_schema_id
185 }
186
187 pub fn core_schema_id(&self) -> u64 {
188 self.core_schema_id
189 }
190}
191#[derive(Clone, Debug, Serialize, Deserialize)]
192struct CurrentPointer {
193 format: String,
194 version: u32,
195 generation: u64,
196}
197
198#[derive(Clone, Debug, Serialize, Deserialize)]
199struct CatalogManifest {
200 format: String,
201 version: u32,
202 generation: u64,
203 next_table_id: u32,
204 namespaces: Vec<NamespaceEntry>,
205}
206
207impl CatalogManifest {
208 fn empty() -> Self {
209 Self {
210 format: CATALOG_FORMAT.to_string(),
211 version: CATALOG_VERSION,
212 generation: 0,
213 next_table_id: 1,
214 namespaces: Vec::new(),
215 }
216 }
217}
218
219#[derive(Clone, Debug, Serialize, Deserialize)]
220struct NamespaceEntry {
221 namespace: Vec<String>,
222 namespace_id: Uuid,
223}
224
225#[derive(Clone, Debug, Serialize, Deserialize)]
226struct NamespaceManifest {
227 format: String,
228 version: u32,
229 generation: u64,
230 namespace: Vec<String>,
231 tables: Vec<TableEntry>,
232}
233
234#[derive(Clone, Debug, Serialize, Deserialize)]
235struct TableEntry {
236 name: String,
237 table_id: TableId,
238 catalog_schema_id: CatalogSchemaId,
239}
240
241#[derive(Clone, Debug, Serialize, Deserialize)]
242struct TableIdentity {
243 format: String,
244 version: u32,
245 table_id: TableId,
246 physical_name: String,
247}
248
249#[derive(Clone, Debug, Serialize, Deserialize)]
250struct TableSchemaRecord {
251 format: String,
252 version: u32,
253 table_id: TableId,
254 catalog_schema_id: CatalogSchemaId,
255 schema: TableSchema,
256 used_field_ids: Vec<FieldId>,
257 field_transforms: Vec<FieldTransform>,
258}
259
260#[derive(Clone, Debug, Serialize, Deserialize)]
261struct ShardSchemaMappingFile {
262 format: String,
263 version: u32,
264 table_id: TableId,
265 db_id: String,
266 catalog_schema_id: CatalogSchemaId,
267 core_schema_id: u64,
268}
269
270pub struct FileCatalog {
276 store: CatalogStore,
277 runtime_context: Arc<CatalogRuntimeContext>,
278 state: Mutex<CatalogManifest>,
279 operation_lock: Arc<Mutex<()>>,
280}
281
282impl FileCatalog {
283 pub fn open(config: &Config, catalog_config: FileCatalogConfig) -> CatalogResult<Self> {
284 let store = CatalogStore::open(config, catalog_config.storage_id())?;
285 let manifest = load_catalog_manifest(&store)?;
286 let operation_lock = process_operation_lock(catalog_config.storage_id())?;
287 let runtime_context = Arc::new(CatalogRuntimeContext {
288 config: config.clone(),
289 storage_id: catalog_config.storage_id().to_string(),
290 });
291 Ok(Self {
292 store,
293 runtime_context,
294 state: Mutex::new(manifest),
295 operation_lock,
296 })
297 }
298
299 pub fn materialize_table(
303 &self,
304 db: Arc<Db>,
305 identifier: &TableIdentifier,
306 ) -> CatalogResult<Table> {
307 let table = self.load_table(identifier)?;
308 let (physical_name, target) = materialize_loaded_table(&self.store, db.as_ref(), &table)?;
309 Table::from_metadata(db, physical_name, target).map_err(Into::into)
310 }
311
312 fn with_current<T>(
313 &self,
314 operation: impl FnOnce(&mut CatalogManifest) -> CatalogResult<T>,
315 ) -> CatalogResult<T> {
316 let _operation = lock(&self.operation_lock)?;
317 let mut manifest = lock(&self.state)?;
318 *manifest = load_catalog_manifest(&self.store)?;
319 operation(&mut manifest)
320 }
321
322 fn namespace_entry<'a>(
323 manifest: &'a CatalogManifest,
324 namespace: &[String],
325 ) -> Option<&'a NamespaceEntry> {
326 manifest
327 .namespaces
328 .iter()
329 .find(|entry| entry.namespace == namespace)
330 }
331
332 fn required_namespace<'a>(
333 manifest: &'a CatalogManifest,
334 namespace: &[String],
335 ) -> CatalogResult<&'a NamespaceEntry> {
336 Self::namespace_entry(manifest, namespace)
337 .ok_or_else(|| CatalogError::NamespaceNotFound(namespace.to_vec()))
338 }
339
340 fn load_namespace(&self, entry: &NamespaceEntry) -> CatalogResult<NamespaceManifest> {
341 let prefix = namespace_prefix(entry.namespace_id);
342 let current: CurrentPointer = read_json(&self.store, &format!("{prefix}/CURRENT"))?;
343 validate_header(¤t.format, current.version)?;
344 let manifest: NamespaceManifest = read_json(
345 &self.store,
346 &format!("{prefix}/NAMESPACE-{}", current.generation),
347 )?;
348 validate_header(&manifest.format, manifest.version)?;
349 if manifest.generation != current.generation || manifest.namespace != entry.namespace {
350 return Err(CatalogError::InvalidMetadata(
351 "namespace manifest does not match CURRENT or catalog entry".to_string(),
352 ));
353 }
354 Ok(manifest)
355 }
356
357 fn load_identity(&self, table_id: TableId) -> CatalogResult<TableIdentity> {
358 let identity: TableIdentity = read_json(&self.store, &table_identity_path(table_id))?;
359 validate_header(&identity.format, identity.version)?;
360 debug_assert_eq!(identity.table_id, table_id);
361 debug_assert_eq!(identity.physical_name, physical_table_name(table_id));
362 Ok(identity)
363 }
364
365 fn load_schema(
366 &self,
367 table_id: TableId,
368 catalog_schema_id: CatalogSchemaId,
369 ) -> CatalogResult<TableSchemaRecord> {
370 load_table_schema_record(&self.store, table_id, catalog_schema_id)
371 }
372
373 fn catalog_table(
374 &self,
375 identifier: TableIdentifier,
376 identity: TableIdentity,
377 schema: TableSchemaRecord,
378 ) -> CatalogTable {
379 CatalogTable {
380 identifier,
381 table_id: identity.table_id,
382 catalog_schema_id: schema.catalog_schema_id,
383 schema: schema.schema,
384 runtime_context: Arc::clone(&self.runtime_context),
385 }
386 }
387
388 fn commit_catalog(
389 &self,
390 current: &mut CatalogManifest,
391 next: CatalogManifest,
392 ) -> CatalogResult<()> {
393 write_json(&self.store, &format!("CATALOG-{}", next.generation), &next)?;
394 write_json(
395 &self.store,
396 "CURRENT",
397 &CurrentPointer {
398 format: CATALOG_FORMAT.to_string(),
399 version: CATALOG_VERSION,
400 generation: next.generation,
401 },
402 )?;
403 *current = next;
404 Ok(())
405 }
406
407 fn commit_namespace(
408 &self,
409 entry: &NamespaceEntry,
410 manifest: &NamespaceManifest,
411 ) -> CatalogResult<()> {
412 let prefix = namespace_prefix(entry.namespace_id);
413 write_json(
414 &self.store,
415 &format!("{prefix}/NAMESPACE-{}", manifest.generation),
416 manifest,
417 )?;
418 write_json(
419 &self.store,
420 &format!("{prefix}/CURRENT"),
421 &CurrentPointer {
422 format: CATALOG_FORMAT.to_string(),
423 version: CATALOG_VERSION,
424 generation: manifest.generation,
425 },
426 )
427 }
428}
429
430fn materialize_loaded_table(
431 store: &CatalogStore,
432 db: &Db,
433 table: &CatalogTable,
434) -> CatalogResult<(String, TableMetadata)> {
435 materialize_table_definition(
436 store,
437 db,
438 table.table_id,
439 table.catalog_schema_id,
440 &table.schema,
441 )
442}
443
444pub(crate) fn materialize_write_plan(
445 store_config: &Config,
446 db: &Db,
447 plan: &TableWritePlan,
448) -> crate::Result<(String, TableMetadata)> {
449 plan.validate()?;
450 let store = CatalogStore::open(store_config, &plan.storage_id)
451 .map_err(|error| TableError::internal(error.to_string()))?;
452 materialize_table_definition(
453 &store,
454 db,
455 plan.table_id,
456 plan.catalog_schema_id,
457 &plan.schema,
458 )
459 .map_err(|error| TableError::internal(error.to_string()))
460}
461
462fn materialize_table_definition(
463 store: &CatalogStore,
464 db: &Db,
465 table_id: TableId,
466 catalog_schema_id: CatalogSchemaId,
467 schema: &TableSchema,
468) -> CatalogResult<(String, TableMetadata)> {
469 let physical_name = physical_table_name(table_id);
470 let target = TableMetadata::compile_catalog(schema.clone(), table_id, catalog_schema_id)?;
471 let current = db.current_schema();
472 let materialized = if let Some(column_family_id) =
473 current.column_family_ids().get(&physical_name).copied()
474 {
475 let options = current.column_family_options_in_family(column_family_id);
476 let existing = options.metadata.as_ref().ok_or_else(|| {
477 TableError::InvalidSchema(format!(
478 "column family '{physical_name}' is not a catalog table"
479 ))
480 })?;
481 let existing = TableMetadata::from_value(existing)?;
482 let binding = existing.catalog_binding.ok_or_else(|| {
483 TableError::InvalidSchema(format!(
484 "column family '{physical_name}' is not a catalog table"
485 ))
486 })?;
487 if binding.table_id != table_id {
488 return Err(TableError::InvalidSchema(format!(
489 "column family '{physical_name}' belongs to another catalog table"
490 ))
491 .into());
492 }
493 if binding.catalog_schema_id > catalog_schema_id {
494 return Err(TableError::InvalidSchema(format!(
495 "catalog schema {} cannot replace newer materialized schema {}",
496 catalog_schema_id, binding.catalog_schema_id
497 ))
498 .into());
499 }
500 let source_record = load_table_schema_record(store, table_id, binding.catalog_schema_id)?;
501 let mut materialized = TableMetadata::compile_catalog(
502 source_record.schema,
503 table_id,
504 binding.catalog_schema_id,
505 )?;
506 if existing != materialized
507 || current.num_columns_in_family(column_family_id)
508 != Some(materialized.layout.value_columns.len().max(1))
509 {
510 return Err(TableError::InvalidSchema(
511 "materialized table metadata does not match the catalog".to_string(),
512 )
513 .into());
514 }
515 write_schema_mapping(
516 store,
517 shard_schema_mapping(table_id, db, binding.catalog_schema_id, current.version()),
518 )?;
519 let mut materialized_catalog_schema_id = binding.catalog_schema_id;
520 while materialized_catalog_schema_id < catalog_schema_id {
521 let next_catalog_schema_id =
522 materialized_catalog_schema_id.next().ok_or_else(|| {
523 CatalogError::InvalidSchemaEvolution(
524 "catalog schema id space exhausted".to_string(),
525 )
526 })?;
527 let next_record = load_table_schema_record(store, table_id, next_catalog_schema_id)?;
528 let next = TableMetadata::compile_catalog(
529 next_record.schema,
530 table_id,
531 next_catalog_schema_id,
532 )?;
533 let remap =
534 compile_column_evolution(&materialized, &next, &next_record.field_transforms)?;
535 let mut builder = db.update_schema();
536 builder.remap_columns(Some(physical_name.clone()), remap)?;
537 builder.set_column_family_options(
538 Some(physical_name.clone()),
539 ColumnFamilyOptions {
540 metadata: Some(next.to_value()?),
541 ..ColumnFamilyOptions::default()
542 },
543 )?;
544 let core_schema_id = builder.commit().version();
545 write_schema_mapping(
546 store,
547 shard_schema_mapping(table_id, db, next_catalog_schema_id, core_schema_id),
548 )?;
549 materialized = next;
550 materialized_catalog_schema_id = next_catalog_schema_id;
551 }
552 materialized
553 } else {
554 let mut builder = db.update_schema();
555 builder.ensure_column_family_exists(physical_name.clone())?;
556 for column in 0..target.layout.value_columns.len().max(1) {
557 builder.add_column(column, None, None, Some(physical_name.clone()))?;
558 }
559 builder.set_column_family_options(
560 Some(physical_name.clone()),
561 ColumnFamilyOptions {
562 metadata: Some(target.to_value()?),
563 ..ColumnFamilyOptions::default()
564 },
565 )?;
566 let core_schema_id = builder.commit().version();
567 write_schema_mapping(
568 store,
569 shard_schema_mapping(table_id, db, catalog_schema_id, core_schema_id),
570 )?;
571 return Ok((physical_name, target));
572 };
573 if materialized != target {
574 return Err(TableError::InvalidSchema(
575 "materialized table metadata does not match the requested catalog schema".to_string(),
576 )
577 .into());
578 }
579 Ok((physical_name, materialized))
580}
581
582fn shard_schema_mapping(
583 table_id: TableId,
584 db: &Db,
585 catalog_schema_id: CatalogSchemaId,
586 core_schema_id: u64,
587) -> ShardSchemaMappingFile {
588 ShardSchemaMappingFile {
589 format: CATALOG_FORMAT.to_string(),
590 version: CATALOG_VERSION,
591 table_id,
592 db_id: db.id().to_string(),
593 catalog_schema_id,
594 core_schema_id,
595 }
596}
597
598fn write_schema_mapping(
599 store: &CatalogStore,
600 mapping: ShardSchemaMappingFile,
601) -> CatalogResult<()> {
602 let path = schema_mapping_path(mapping.table_id, &mapping.db_id, mapping.catalog_schema_id);
603 if store.exists(&path)? {
604 let existing: ShardSchemaMappingFile = read_json(store, &path)?;
605 validate_header(&existing.format, existing.version)?;
606 validate_schema_mapping_key(
607 &existing,
608 mapping.table_id,
609 &mapping.db_id,
610 mapping.catalog_schema_id,
611 )?;
612 if existing.core_schema_id <= mapping.core_schema_id {
613 return Ok(());
614 }
615 }
616 write_json(store, &path, &mapping)
617}
618
619fn load_table_schema_record(
620 store: &CatalogStore,
621 table_id: TableId,
622 catalog_schema_id: CatalogSchemaId,
623) -> CatalogResult<TableSchemaRecord> {
624 let record: TableSchemaRecord =
625 read_json(store, &table_schema_path(table_id, catalog_schema_id))?;
626 validate_header(&record.format, record.version)?;
627 if record.table_id != table_id || record.catalog_schema_id != catalog_schema_id {
628 return Err(CatalogError::InvalidMetadata(
629 "table schema record does not match its lookup key".to_string(),
630 ));
631 }
632 #[cfg(debug_assertions)]
633 {
634 debug_assert!(record.schema.validate().is_ok());
635 debug_assert!(
636 record
637 .used_field_ids
638 .windows(2)
639 .all(|window| window[0] < window[1])
640 );
641 debug_assert!(
642 schema_field_ids(&record.schema)
643 .iter()
644 .all(|field_id| record.used_field_ids.binary_search(field_id).is_ok())
645 );
646 }
647 Ok(record)
648}
649
650impl CatalogTable {
651 #[cfg(feature = "ffi")]
653 #[doc(hidden)]
654 pub fn physical_name(&self) -> String {
655 physical_table_name(self.table_id)
656 }
657
658 pub fn materialize_table(&self, db: Arc<Db>) -> CatalogResult<Table> {
662 let store = CatalogStore::open(
663 &self.runtime_context.config,
664 &self.runtime_context.storage_id,
665 )?;
666 let (physical_name, target) = materialize_loaded_table(&store, db.as_ref(), self)?;
667 Table::from_metadata(db, physical_name, target).map_err(Into::into)
668 }
669
670 pub fn new_write_builder(&self) -> TableWriteBuilder {
672 TableWriteBuilder::new(self.clone(), self.runtime_context.config.total_buckets)
673 }
674
675 pub fn writer_builder(&self, runtime: Config) -> CatalogResult<TableWriterBuilder> {
677 self.new_write_builder()
678 .total_buckets(runtime.total_buckets)
679 .build()?
680 .writer_builder(runtime)
681 }
682
683 pub fn refresh_writer(&self, table: &mut Table) -> CatalogResult<bool> {
688 let physical_name = physical_table_name(self.table_id);
689 if table.name() != physical_name {
690 return Err(TableError::InvalidSchema(
691 "Table does not belong to this catalog table".to_string(),
692 )
693 .into());
694 }
695 materialize_loaded_table(
696 &CatalogStore::open(
697 &self.runtime_context.config,
698 &self.runtime_context.storage_id,
699 )?,
700 table.db(),
701 self,
702 )?;
703 table.refresh_schema().map_err(Into::into)
704 }
705
706 pub fn reader_builder(&self, runtime: Config) -> CatalogResult<TableReaderBuilder> {
708 let context = &self.runtime_context;
709 let config = context.scoped_config(runtime, self.table_id);
710 Ok(TableReaderBuilder::from_catalog(
711 config,
712 physical_table_name(self.table_id),
713 self.table_id,
714 ))
715 }
716
717 pub fn readonly_table_builder(&self, runtime: Config) -> CatalogResult<ReadOnlyTableBuilder> {
719 let context = &self.runtime_context;
720 let config = context.scoped_config(runtime, self.table_id);
721 Ok(ReadOnlyTableBuilder::from_catalog(
722 config,
723 physical_table_name(self.table_id),
724 self.table_id,
725 ))
726 }
727
728 pub fn snapshot_committer(
730 &self,
731 runtime: Config,
732 max_pending_commits: usize,
733 ) -> CatalogResult<TableSnapshotCommitter> {
734 let total_buckets = runtime.total_buckets;
735 let coordinator = Arc::new(self.coordinator(runtime)?);
736 Ok(TableSnapshotCommitter::new(
737 coordinator,
738 total_buckets,
739 max_pending_commits,
740 )?)
741 }
742
743 pub fn coordinator(&self, runtime: Config) -> CatalogResult<DbCoordinator> {
745 let context = &self.runtime_context;
746 let config = context.scoped_config(runtime, self.table_id);
747 Ok(DbCoordinator::open(CoordinatorConfig::from_config(
748 &config,
749 ))?)
750 }
751}
752
753pub(crate) fn build_write_plan(
754 table: CatalogTable,
755 total_buckets: u32,
756) -> CatalogResult<TableWritePlan> {
757 let context = &table.runtime_context;
758 let shared_volumes = context
759 .config
760 .volumes
761 .iter()
762 .filter_map(plan_shared_volume)
763 .map(|volume| volume.without_credentials())
764 .collect();
765 let plan = TableWritePlan {
766 format: TABLE_WRITE_PLAN_FORMAT.to_string(),
767 version: TABLE_WRITE_PLAN_VERSION,
768 identifier: table.identifier,
769 table_id: table.table_id,
770 catalog_schema_id: table.catalog_schema_id,
771 schema: table.schema,
772 storage_id: context.storage_id.clone(),
773 shared_volumes,
774 total_buckets,
775 auth_source: Some(context.config.clone()),
776 };
777 plan.validate()?;
778 Ok(plan)
779}
780
781pub(crate) fn writer_builder_from_write_plan(
782 plan: &TableWritePlan,
783 runtime: Config,
784) -> CatalogResult<TableWriterBuilder> {
785 plan.validate()?;
786 let credential_source = plan.auth_source.as_ref().unwrap_or(&runtime);
787 let shared_volumes = plan
788 .shared_volumes
789 .iter()
790 .map(|volume| volume.with_credentials_from(credential_source))
791 .collect::<Vec<_>>();
792 let store_config = Config {
793 volumes: shared_volumes.clone(),
794 ..Config::default()
795 };
796 let mut config = scoped_table_config(&shared_volumes, &plan.storage_id, plan.table_id, runtime);
797 config.total_buckets = plan.total_buckets;
798 Ok(TableWriterBuilder::from_write_plan(
799 config,
800 physical_table_name(plan.table_id),
801 plan.clone(),
802 store_config,
803 ))
804}
805
806impl FileCatalog {
807 pub fn load_shard_schema_mapping(
809 &self,
810 identifier: &TableIdentifier,
811 db_id: &str,
812 catalog_schema_id: CatalogSchemaId,
813 ) -> CatalogResult<ShardSchemaMapping> {
814 let table = self.load_table(identifier)?;
815 if catalog_schema_id > table.catalog_schema_id {
816 return Err(CatalogError::SchemaNotFound {
817 table: identifier.clone(),
818 catalog_schema_id,
819 });
820 }
821 let mapping: ShardSchemaMappingFile = read_json(
822 &self.store,
823 &schema_mapping_path(table.table_id, db_id, catalog_schema_id),
824 )?;
825 validate_header(&mapping.format, mapping.version)?;
826 validate_schema_mapping_key(&mapping, table.table_id, db_id, catalog_schema_id)?;
827 Ok(ShardSchemaMapping {
828 table_id: mapping.table_id,
829 db_id: mapping.db_id,
830 catalog_schema_id: mapping.catalog_schema_id,
831 core_schema_id: mapping.core_schema_id,
832 })
833 }
834}
835
836impl Catalog for FileCatalog {
837 fn create_namespace(&self, namespace: Vec<String>) -> CatalogResult<()> {
838 validate_namespace(&namespace)?;
839 self.with_current(|current| {
840 if Self::namespace_entry(current, &namespace).is_some() {
841 return Err(CatalogError::NamespaceAlreadyExists(namespace));
842 }
843 let entry = NamespaceEntry {
844 namespace: namespace.clone(),
845 namespace_id: Uuid::new_v4(),
846 };
847 self.commit_namespace(
848 &entry,
849 &NamespaceManifest {
850 format: CATALOG_FORMAT.to_string(),
851 version: CATALOG_VERSION,
852 generation: 1,
853 namespace,
854 tables: Vec::new(),
855 },
856 )?;
857 let mut next = current.clone();
858 next.generation += 1;
859 next.namespaces.push(entry);
860 next.namespaces
861 .sort_by(|left, right| left.namespace.cmp(&right.namespace));
862 self.commit_catalog(current, next)
863 })
864 }
865
866 fn list_namespaces(&self) -> CatalogResult<Vec<Vec<String>>> {
867 self.with_current(|current| {
868 Ok(current
869 .namespaces
870 .iter()
871 .map(|entry| entry.namespace.clone())
872 .collect())
873 })
874 }
875
876 fn drop_namespace(&self, namespace: &[String]) -> CatalogResult<()> {
877 validate_namespace(namespace)?;
878 self.with_current(|current| {
879 let entry = Self::required_namespace(current, namespace)?.clone();
880 if !self.load_namespace(&entry)?.tables.is_empty() {
881 return Err(CatalogError::NamespaceNotEmpty(namespace.to_vec()));
882 }
883 let mut next = current.clone();
884 next.generation += 1;
885 next.namespaces
886 .retain(|candidate| candidate.namespace != namespace);
887 self.commit_catalog(current, next)
888 })
889 }
890
891 fn create_table(
892 &self,
893 identifier: TableIdentifier,
894 schema: TableSchema,
895 ) -> CatalogResult<CatalogTable> {
896 validate_identifier(&identifier)?;
897 schema.validate()?;
898 self.with_current(|current| {
899 let namespace_entry =
900 Self::required_namespace(current, identifier.namespace())?.clone();
901 let mut namespace = self.load_namespace(&namespace_entry)?;
902 if namespace
903 .tables
904 .iter()
905 .any(|entry| entry.name == identifier.name())
906 {
907 return Err(CatalogError::TableAlreadyExists(identifier));
908 }
909 let table_id = TableId::new(current.next_table_id);
910 let mut next = current.clone();
911 next.generation += 1;
912 next.next_table_id = next.next_table_id.checked_add(1).ok_or_else(|| {
913 CatalogError::InvalidMetadata("table id space exhausted".to_string())
914 })?;
915 self.commit_catalog(current, next)?;
916 let identity = TableIdentity {
917 format: CATALOG_FORMAT.to_string(),
918 version: CATALOG_VERSION,
919 table_id,
920 physical_name: physical_table_name(table_id),
921 };
922 write_json(&self.store, &table_identity_path(table_id), &identity)?;
923 let schema = TableSchemaRecord {
924 format: CATALOG_FORMAT.to_string(),
925 version: CATALOG_VERSION,
926 table_id,
927 catalog_schema_id: CatalogSchemaId::INITIAL,
928 used_field_ids: sorted_field_ids(schema_field_ids(&schema)),
929 field_transforms: Vec::new(),
930 schema,
931 };
932 write_json(
933 &self.store,
934 &table_schema_path(table_id, schema.catalog_schema_id),
935 &schema,
936 )?;
937 namespace.generation += 1;
938 namespace.tables.push(TableEntry {
939 name: identifier.name().to_string(),
940 table_id,
941 catalog_schema_id: schema.catalog_schema_id,
942 });
943 namespace
944 .tables
945 .sort_by(|left, right| left.name.cmp(&right.name));
946 self.commit_namespace(&namespace_entry, &namespace)?;
947 Ok(self.catalog_table(identifier, identity, schema))
948 })
949 }
950
951 fn load_table(&self, identifier: &TableIdentifier) -> CatalogResult<CatalogTable> {
952 validate_identifier(identifier)?;
953 self.with_current(|current| {
954 let namespace_entry = Self::required_namespace(current, identifier.namespace())?;
955 let namespace = self.load_namespace(namespace_entry)?;
956 let entry = namespace
957 .tables
958 .iter()
959 .find(|entry| entry.name == identifier.name())
960 .ok_or_else(|| CatalogError::TableNotFound(identifier.clone()))?;
961 let identity = self.load_identity(entry.table_id)?;
962 let schema = self.load_schema(entry.table_id, entry.catalog_schema_id)?;
963 Ok(self.catalog_table(identifier.clone(), identity, schema))
964 })
965 }
966
967 fn load_table_schema(
968 &self,
969 identifier: &TableIdentifier,
970 catalog_schema_id: CatalogSchemaId,
971 ) -> CatalogResult<TableSchema> {
972 validate_identifier(identifier)?;
973 self.with_current(|current| {
974 let namespace_entry = Self::required_namespace(current, identifier.namespace())?;
975 let namespace = self.load_namespace(namespace_entry)?;
976 let entry = namespace
977 .tables
978 .iter()
979 .find(|entry| entry.name == identifier.name())
980 .ok_or_else(|| CatalogError::TableNotFound(identifier.clone()))?;
981 if catalog_schema_id > entry.catalog_schema_id {
982 return Err(CatalogError::SchemaNotFound {
983 table: identifier.clone(),
984 catalog_schema_id,
985 });
986 }
987 Ok(self.load_schema(entry.table_id, catalog_schema_id)?.schema)
988 })
989 }
990
991 fn evolve_schema(
992 &self,
993 identifier: &TableIdentifier,
994 changes: Vec<SchemaChange>,
995 ) -> CatalogResult<CatalogTable> {
996 validate_identifier(identifier)?;
997 self.with_current(|current| {
998 let namespace_entry =
999 Self::required_namespace(current, identifier.namespace())?.clone();
1000 let mut namespace = self.load_namespace(&namespace_entry)?;
1001 let entry_index = namespace
1002 .tables
1003 .iter()
1004 .position(|entry| entry.name == identifier.name())
1005 .ok_or_else(|| CatalogError::TableNotFound(identifier.clone()))?;
1006 let table_id = namespace.tables[entry_index].table_id;
1007 let current_catalog_schema_id = namespace.tables[entry_index].catalog_schema_id;
1008 let current_schema = self.load_schema(table_id, current_catalog_schema_id)?;
1009 let used_field_ids = current_schema
1010 .used_field_ids
1011 .iter()
1012 .copied()
1013 .collect::<HashSet<_>>();
1014 let (next_schema, used_field_ids, field_transforms) =
1015 apply_schema_changes(current_schema.schema, changes, used_field_ids)
1016 .map_err(|error| CatalogError::InvalidSchemaEvolution(error.to_string()))?;
1017 let next_catalog_schema_id = current_catalog_schema_id.next().ok_or_else(|| {
1018 CatalogError::InvalidSchemaEvolution("schema id space exhausted".to_string())
1019 })?;
1020 let record = TableSchemaRecord {
1021 format: CATALOG_FORMAT.to_string(),
1022 version: CATALOG_VERSION,
1023 table_id,
1024 catalog_schema_id: next_catalog_schema_id,
1025 schema: next_schema,
1026 used_field_ids: sorted_field_ids(used_field_ids),
1027 field_transforms,
1028 };
1029 write_json(
1030 &self.store,
1031 &table_schema_path(table_id, next_catalog_schema_id),
1032 &record,
1033 )?;
1034 namespace.tables[entry_index].catalog_schema_id = next_catalog_schema_id;
1035 namespace.generation += 1;
1036 self.commit_namespace(&namespace_entry, &namespace)?;
1037 Ok(self.catalog_table(identifier.clone(), self.load_identity(table_id)?, record))
1038 })
1039 }
1040
1041 fn list_tables(&self, namespace: &[String]) -> CatalogResult<Vec<TableIdentifier>> {
1042 validate_namespace(namespace)?;
1043 self.with_current(|current| {
1044 let namespace_entry = Self::required_namespace(current, namespace)?;
1045 Ok(self
1046 .load_namespace(namespace_entry)?
1047 .tables
1048 .into_iter()
1049 .map(|entry| TableIdentifier::new(namespace.to_vec(), entry.name))
1050 .collect())
1051 })
1052 }
1053
1054 fn table_exists(&self, identifier: &TableIdentifier) -> CatalogResult<bool> {
1055 validate_identifier(identifier)?;
1056 self.with_current(|current| {
1057 let Some(namespace_entry) = Self::namespace_entry(current, identifier.namespace())
1058 else {
1059 return Ok(false);
1060 };
1061 Ok(self
1062 .load_namespace(namespace_entry)?
1063 .tables
1064 .iter()
1065 .any(|entry| entry.name == identifier.name()))
1066 })
1067 }
1068
1069 fn rename_table(
1070 &self,
1071 identifier: &TableIdentifier,
1072 new_name: String,
1073 ) -> CatalogResult<CatalogTable> {
1074 validate_identifier(identifier)?;
1075 let new_identifier = identifier.renamed(new_name);
1076 validate_identifier(&new_identifier)?;
1077 self.with_current(|current| {
1078 let namespace_entry =
1079 Self::required_namespace(current, identifier.namespace())?.clone();
1080 let mut namespace = self.load_namespace(&namespace_entry)?;
1081 let source_index = namespace
1082 .tables
1083 .iter()
1084 .position(|entry| entry.name == identifier.name())
1085 .ok_or_else(|| CatalogError::TableNotFound(identifier.clone()))?;
1086 if namespace
1087 .tables
1088 .iter()
1089 .any(|entry| entry.name == new_identifier.name())
1090 {
1091 return Err(CatalogError::TableAlreadyExists(new_identifier));
1092 }
1093 let entry = &mut namespace.tables[source_index];
1094 let table_id = entry.table_id;
1095 let catalog_schema_id = entry.catalog_schema_id;
1096 entry.name = new_identifier.name().to_string();
1097 namespace.generation += 1;
1098 namespace
1099 .tables
1100 .sort_by(|left, right| left.name.cmp(&right.name));
1101 self.commit_namespace(&namespace_entry, &namespace)?;
1102 let identity = self.load_identity(table_id)?;
1103 let schema = self.load_schema(table_id, catalog_schema_id)?;
1104 Ok(self.catalog_table(new_identifier, identity, schema))
1105 })
1106 }
1107
1108 fn drop_table(&self, identifier: &TableIdentifier) -> CatalogResult<()> {
1109 validate_identifier(identifier)?;
1110 self.with_current(|current| {
1111 let namespace_entry =
1112 Self::required_namespace(current, identifier.namespace())?.clone();
1113 let mut namespace = self.load_namespace(&namespace_entry)?;
1114 let original_len = namespace.tables.len();
1115 namespace
1116 .tables
1117 .retain(|entry| entry.name != identifier.name());
1118 if namespace.tables.len() == original_len {
1119 return Err(CatalogError::TableNotFound(identifier.clone()));
1120 }
1121 namespace.generation += 1;
1122 self.commit_namespace(&namespace_entry, &namespace)
1123 })
1124 }
1125}
1126
1127fn load_catalog_manifest(store: &CatalogStore) -> CatalogResult<CatalogManifest> {
1128 if !store.exists("CURRENT")? {
1129 return Ok(CatalogManifest::empty());
1130 }
1131 let current: CurrentPointer = read_json(store, "CURRENT")?;
1132 validate_header(¤t.format, current.version)?;
1133 let manifest: CatalogManifest = read_json(store, &format!("CATALOG-{}", current.generation))?;
1134 validate_header(&manifest.format, manifest.version)?;
1135 if manifest.generation != current.generation {
1136 return Err(CatalogError::InvalidMetadata(
1137 "catalog generation does not match CURRENT".to_string(),
1138 ));
1139 }
1140 Ok(manifest)
1141}
1142
1143fn process_operation_lock(storage_id: &str) -> CatalogResult<Arc<Mutex<()>>> {
1144 static LOCKS: OnceLock<Mutex<HashMap<String, Weak<Mutex<()>>>>> = OnceLock::new();
1145 let mut locks = lock(LOCKS.get_or_init(|| Mutex::new(HashMap::new())))?;
1146 if let Some(existing) = locks.get(storage_id).and_then(Weak::upgrade) {
1147 return Ok(existing);
1148 }
1149 let operation_lock = Arc::new(Mutex::new(()));
1150 locks.insert(storage_id.to_string(), Arc::downgrade(&operation_lock));
1151 Ok(operation_lock)
1152}
1153
1154fn lock<T>(mutex: &Mutex<T>) -> CatalogResult<std::sync::MutexGuard<'_, T>> {
1155 mutex
1156 .lock()
1157 .map_err(|_| CatalogError::InvalidMetadata("catalog lock poisoned".to_string()))
1158}
1159
1160fn validate_header(format: &str, version: u32) -> CatalogResult<()> {
1161 if format != CATALOG_FORMAT || version != CATALOG_VERSION {
1162 return Err(CatalogError::InvalidMetadata(format!(
1163 "unsupported format/version: {format}/{version}"
1164 )));
1165 }
1166 Ok(())
1167}
1168
1169fn validate_namespace(namespace: &[String]) -> CatalogResult<()> {
1170 if namespace.is_empty() {
1171 return Err(CatalogError::InvalidIdentifier(
1172 "namespace must contain at least one component".to_string(),
1173 ));
1174 }
1175 for component in namespace {
1176 validate_name("namespace component", component)?;
1177 }
1178 Ok(())
1179}
1180
1181fn validate_identifier(identifier: &TableIdentifier) -> CatalogResult<()> {
1182 validate_namespace(identifier.namespace())?;
1183 validate_name("table name", identifier.name())
1184}
1185
1186fn validate_name(label: &str, value: &str) -> CatalogResult<()> {
1187 if value.is_empty() || value != value.trim() || value.chars().any(char::is_control) {
1188 return Err(CatalogError::InvalidIdentifier(format!(
1189 "invalid {label}: {value:?}"
1190 )));
1191 }
1192 Ok(())
1193}
1194
1195fn namespace_prefix(namespace_id: Uuid) -> String {
1196 format!("namespaces/{namespace_id}")
1197}
1198
1199fn table_identity_path(table_id: TableId) -> String {
1200 format!("tables/TABLE-{table_id}/IDENTITY")
1201}
1202
1203pub(crate) fn physical_table_name(table_id: TableId) -> String {
1204 format!("t{table_id}")
1205}
1206
1207fn table_schema_path(table_id: TableId, schema_id: CatalogSchemaId) -> String {
1208 format!(
1209 "tables/TABLE-{table_id}/schemas/SCHEMA-{}",
1210 schema_id.as_u32()
1211 )
1212}
1213
1214fn schema_mapping_path(
1215 table_id: TableId,
1216 db_id: &str,
1217 catalog_schema_id: CatalogSchemaId,
1218) -> String {
1219 let digest = Sha256::digest(db_id.as_bytes());
1220 let mut shard = String::with_capacity(digest.len() * 2);
1221 for byte in digest {
1222 write!(&mut shard, "{byte:02x}").expect("writing to a string cannot fail");
1223 }
1224 format!(
1225 "tables/TABLE-{table_id}/shards/{shard}/SCHEMA-{}",
1226 catalog_schema_id.as_u32()
1227 )
1228}
1229
1230fn validate_schema_mapping_key(
1231 mapping: &ShardSchemaMappingFile,
1232 table_id: TableId,
1233 db_id: &str,
1234 catalog_schema_id: CatalogSchemaId,
1235) -> CatalogResult<()> {
1236 if mapping.table_id != table_id
1237 || mapping.db_id != db_id
1238 || mapping.catalog_schema_id != catalog_schema_id
1239 {
1240 return Err(CatalogError::InvalidMetadata(
1241 "shard schema mapping does not match its lookup key".to_string(),
1242 ));
1243 }
1244 Ok(())
1245}
1246
1247fn sorted_field_ids(field_ids: HashSet<FieldId>) -> Vec<FieldId> {
1248 let mut field_ids = field_ids.into_iter().collect::<Vec<_>>();
1249 field_ids.sort_unstable();
1250 field_ids
1251}
1252
1253fn read_json<T: DeserializeOwned>(store: &CatalogStore, path: &str) -> CatalogResult<T> {
1254 let bytes = store.read(path)?;
1255 serde_json::from_slice(&bytes).map_err(|error| CatalogError::InvalidMetadata(error.to_string()))
1256}
1257
1258fn write_json<T: Serialize>(store: &CatalogStore, path: &str, value: &T) -> CatalogResult<()> {
1259 let bytes = serde_json::to_vec(value)
1260 .map_err(|error| CatalogError::InvalidMetadata(error.to_string()))?;
1261 store.write(path, &bytes)?;
1262 Ok(())
1263}