1use std::collections::{HashMap, HashSet};
19use std::str::FromStr;
20use std::sync::Arc;
21use std::time::Duration;
22
23use async_trait::async_trait;
24use iceberg::encryption::kms::{KeyManagementClient, KmsClientFactory};
25use iceberg::io::{FileIO, FileIOBuilder, StorageFactory};
26use iceberg::spec::{TableMetadata, TableMetadataBuilder};
27use iceberg::table::Table;
28use iceberg::{
29 Catalog, CatalogBuilder, Error, ErrorKind, MetadataLocation, Namespace, NamespaceIdent, Result,
30 Runtime, TableCommit, TableCreation, TableIdent,
31};
32use sqlx::any::{AnyPoolOptions, AnyQueryResult, AnyRow, install_default_drivers};
33use sqlx::{Any, AnyPool, Column, Executor, Row, Transaction};
34
35use crate::error::{
36 from_sqlx_error, no_such_namespace_err, no_such_table_err, table_already_exists_err,
37};
38
39pub const SQL_CATALOG_PROP_URI: &str = "uri";
41pub const SQL_CATALOG_PROP_WAREHOUSE: &str = "warehouse";
43pub const SQL_CATALOG_PROP_BIND_STYLE: &str = "sql.bind-style";
45const SQL_CATALOG_PROP_BIND_STYLE_LEGACY: &str = "sql_bind_style";
48pub const SQL_CATALOG_PROP_SCHEMA_VERSION: &str = "sql.schema-version";
59
60static CATALOG_TABLE_NAME: &str = "iceberg_tables";
61static CATALOG_FIELD_CATALOG_NAME: &str = "catalog_name";
62static CATALOG_FIELD_TABLE_NAME: &str = "table_name";
63static CATALOG_FIELD_TABLE_NAMESPACE: &str = "table_namespace";
64static CATALOG_FIELD_METADATA_LOCATION_PROP: &str = "metadata_location";
65static CATALOG_FIELD_PREVIOUS_METADATA_LOCATION_PROP: &str = "previous_metadata_location";
66static CATALOG_FIELD_RECORD_TYPE: &str = "iceberg_type";
67static CATALOG_FIELD_TABLE_RECORD_TYPE: &str = "TABLE";
68
69static NAMESPACE_TABLE_NAME: &str = "iceberg_namespace_properties";
70static NAMESPACE_FIELD_NAME: &str = "namespace";
71static NAMESPACE_FIELD_PROPERTY_KEY: &str = "property_key";
72static NAMESPACE_FIELD_PROPERTY_VALUE: &str = "property_value";
73
74static NAMESPACE_LOCATION_PROPERTY_KEY: &str = "location";
75
76static MAX_CONNECTIONS: u32 = 10; static IDLE_TIMEOUT: u64 = 10; static TEST_BEFORE_ACQUIRE: bool = true; fn parse_pool_property<T>(
81 props: &HashMap<String, String>,
82 property: &'static str,
83 default: T,
84) -> Result<T>
85where
86 T: FromStr,
87 T::Err: std::error::Error + Send + Sync + 'static,
88{
89 props.get(property).map_or(Ok(default), |value| {
90 value.parse().map_err(|error| {
91 Error::new(
92 ErrorKind::DataInvalid,
93 "Failed to parse SQL catalog pool property",
94 )
95 .with_context("property", property)
96 .with_context("value", value)
97 .with_source(error)
98 })
99 })
100}
101
102#[derive(Debug)]
104pub struct SqlCatalogBuilder {
105 config: SqlCatalogConfig,
106 storage_factory: Option<Arc<dyn StorageFactory>>,
107 kms_client_factory: Option<Arc<dyn KmsClientFactory>>,
108 runtime: Option<Runtime>,
109}
110
111impl Default for SqlCatalogBuilder {
112 fn default() -> Self {
113 Self {
114 config: SqlCatalogConfig {
115 uri: "".to_string(),
116 name: "".to_string(),
117 warehouse_location: "".to_string(),
118 sql_bind_style: SqlBindStyle::DollarNumeric,
119 schema_version: None,
120 props: HashMap::new(),
121 },
122 storage_factory: None,
123 kms_client_factory: None,
124 runtime: None,
125 }
126 }
127}
128
129impl SqlCatalogBuilder {
130 pub fn uri(mut self, uri: impl Into<String>) -> Self {
135 self.config.uri = uri.into();
136 self
137 }
138
139 pub fn warehouse_location(mut self, location: impl Into<String>) -> Self {
144 self.config.warehouse_location = location.into();
145 self
146 }
147
148 pub fn sql_bind_style(mut self, sql_bind_style: SqlBindStyle) -> Self {
153 self.config.sql_bind_style = sql_bind_style;
154 self
155 }
156
157 pub fn props(mut self, props: HashMap<String, String>) -> Self {
162 for (k, v) in props {
163 self.config.props.insert(k, v);
164 }
165 self
166 }
167
168 pub fn prop(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
175 self.config.props.insert(key.into(), value.into());
176 self
177 }
178}
179
180impl CatalogBuilder for SqlCatalogBuilder {
181 type C = SqlCatalog;
182
183 fn with_storage_factory(mut self, storage_factory: Arc<dyn StorageFactory>) -> Self {
184 self.storage_factory = Some(storage_factory);
185 self
186 }
187
188 fn with_kms_client_factory(mut self, kms_client_factory: Arc<dyn KmsClientFactory>) -> Self {
189 self.kms_client_factory = Some(kms_client_factory);
190 self
191 }
192
193 fn with_runtime(mut self, runtime: Runtime) -> Self {
194 self.runtime = Some(runtime);
195 self
196 }
197
198 fn load(
199 mut self,
200 name: impl Into<String>,
201 props: HashMap<String, String>,
202 ) -> impl Future<Output = Result<Self::C>> + Send {
203 for (k, v) in props {
204 self.config.props.insert(k, v);
205 }
206
207 if let Some(uri) = self.config.props.remove(SQL_CATALOG_PROP_URI) {
208 self.config.uri = uri;
209 }
210 if let Some(warehouse_location) = self.config.props.remove(SQL_CATALOG_PROP_WAREHOUSE) {
211 self.config.warehouse_location = warehouse_location;
212 }
213
214 let name = name.into();
215
216 let mut valid_sql_bind_style = true;
217
218 let sql_bind_style = self
220 .config
221 .props
222 .remove(SQL_CATALOG_PROP_BIND_STYLE)
223 .or_else(|| self.config.props.remove(SQL_CATALOG_PROP_BIND_STYLE_LEGACY));
224
225 if let Some(sql_bind_style) = sql_bind_style {
227 if let Ok(sql_bind_style) = SqlBindStyle::from_str(&sql_bind_style) {
228 self.config.sql_bind_style = sql_bind_style;
229 } else {
230 valid_sql_bind_style = false;
231 }
232 }
233
234 let mut valid_schema_version = true;
237 if let Some(schema_version) = self.config.props.remove(SQL_CATALOG_PROP_SCHEMA_VERSION) {
238 match SchemaVersion::from_str(&schema_version) {
239 Ok(schema_version) => self.config.schema_version = Some(schema_version),
240 Err(_) => valid_schema_version = false,
241 }
242 }
243
244 let valid_name = !name.trim().is_empty();
245
246 async move {
247 if !valid_name {
248 Err(Error::new(
249 ErrorKind::DataInvalid,
250 "Catalog name cannot be empty",
251 ))
252 } else if !valid_sql_bind_style {
253 Err(Error::new(
254 ErrorKind::DataInvalid,
255 format!(
256 "`{}` values are valid only if they're `{}` or `{}`",
257 SQL_CATALOG_PROP_BIND_STYLE,
258 SqlBindStyle::DollarNumeric,
259 SqlBindStyle::QMark
260 ),
261 ))
262 } else if !valid_schema_version {
263 Err(Error::new(
264 ErrorKind::DataInvalid,
265 format!(
266 "`{}` values are valid only if they're `{}` or `{}`",
267 SQL_CATALOG_PROP_SCHEMA_VERSION,
268 SchemaVersion::V0,
269 SchemaVersion::V1
270 ),
271 ))
272 } else {
273 self.config.name = name;
274 let runtime = match self.runtime {
275 Some(rt) => rt,
276 None => Runtime::try_current()?,
277 };
278 let kms_client = match self.kms_client_factory {
279 Some(factory) => Some(factory.create_kms_client(&self.config.props).await?),
280 None => None,
281 };
282 SqlCatalog::new(self.config, self.storage_factory, runtime, kms_client).await
283 }
284 }
285 }
286}
287
288#[derive(Debug)]
297struct SqlCatalogConfig {
298 uri: String,
299 name: String,
300 warehouse_location: String,
301 sql_bind_style: SqlBindStyle,
302 schema_version: Option<SchemaVersion>,
303 props: HashMap<String, String>,
304}
305
306#[derive(Debug)]
307pub struct SqlCatalog {
312 name: String,
313 connection: AnyPool,
314 warehouse_location: String,
315 fileio: FileIO,
316 sql_bind_style: SqlBindStyle,
317 runtime: Runtime,
318 kms_client: Option<Arc<dyn KeyManagementClient>>,
319 schema_version: SchemaVersion,
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, strum::EnumString, strum::Display)]
323#[strum(ascii_case_insensitive)]
324pub enum SchemaVersion {
326 V0,
328 V1,
330}
331
332impl SchemaVersion {
333 async fn detect(pool: &AnyPool) -> Result<Self> {
335 let catalog_table_description = pool
336 .describe(&format!("SELECT * FROM {CATALOG_TABLE_NAME}"))
337 .await
338 .map_err(from_sqlx_error)?;
339
340 let has_type_column = catalog_table_description.columns().iter().any(|column| {
341 column
342 .name()
343 .eq_ignore_ascii_case(CATALOG_FIELD_RECORD_TYPE)
344 });
345
346 Ok(if has_type_column {
347 SchemaVersion::V1
348 } else {
349 SchemaVersion::V0
350 })
351 }
352
353 fn record_type_filter(self) -> String {
359 match self {
360 SchemaVersion::V1 => format!(
361 "AND ({CATALOG_FIELD_RECORD_TYPE} = '{CATALOG_FIELD_TABLE_RECORD_TYPE}' \
362 OR {CATALOG_FIELD_RECORD_TYPE} IS NULL)"
363 ),
364 SchemaVersion::V0 => String::new(),
365 }
366 }
367
368 fn migration_sql(self) -> Option<String> {
372 match self {
373 SchemaVersion::V1 => Some(format!(
374 "ALTER TABLE {CATALOG_TABLE_NAME} ADD COLUMN {CATALOG_FIELD_RECORD_TYPE} VARCHAR(5)"
375 )),
376 SchemaVersion::V0 => None,
377 }
378 }
379}
380
381#[derive(Debug, PartialEq, strum::EnumString, strum::Display)]
382pub enum SqlBindStyle {
384 DollarNumeric,
386 QMark,
388}
389
390impl SqlCatalog {
391 async fn new(
393 config: SqlCatalogConfig,
394 storage_factory: Option<Arc<dyn StorageFactory>>,
395 runtime: Runtime,
396 kms_client: Option<Arc<dyn KeyManagementClient>>,
397 ) -> Result<Self> {
398 let factory = storage_factory.ok_or_else(|| {
399 Error::new(
400 ErrorKind::Unexpected,
401 "StorageFactory must be provided for SqlCatalog. Use `with_storage_factory` to configure it.",
402 )
403 })?;
404 let fileio = FileIOBuilder::new(factory)
407 .with_props(config.props.clone())
408 .build();
409
410 install_default_drivers();
411 let max_connections =
412 parse_pool_property(&config.props, "pool.max-connections", MAX_CONNECTIONS)?;
413 let idle_timeout = parse_pool_property(&config.props, "pool.idle-timeout", IDLE_TIMEOUT)?;
414 let test_before_acquire = parse_pool_property(
415 &config.props,
416 "pool.test-before-acquire",
417 TEST_BEFORE_ACQUIRE,
418 )?;
419
420 let pool = AnyPoolOptions::new()
421 .max_connections(max_connections)
422 .idle_timeout(Duration::from_secs(idle_timeout))
423 .test_before_acquire(test_before_acquire)
424 .connect(&config.uri)
425 .await
426 .map_err(from_sqlx_error)?;
427
428 sqlx::query(&format!(
429 "CREATE TABLE IF NOT EXISTS {CATALOG_TABLE_NAME} (
430 {CATALOG_FIELD_CATALOG_NAME} VARCHAR(255) NOT NULL,
431 {CATALOG_FIELD_TABLE_NAMESPACE} VARCHAR(255) NOT NULL,
432 {CATALOG_FIELD_TABLE_NAME} VARCHAR(255) NOT NULL,
433 {CATALOG_FIELD_METADATA_LOCATION_PROP} VARCHAR(1000),
434 {CATALOG_FIELD_PREVIOUS_METADATA_LOCATION_PROP} VARCHAR(1000),
435 {CATALOG_FIELD_RECORD_TYPE} VARCHAR(5),
436 PRIMARY KEY ({CATALOG_FIELD_CATALOG_NAME}, {CATALOG_FIELD_TABLE_NAMESPACE}, {CATALOG_FIELD_TABLE_NAME}))"
437 ))
438 .execute(&pool)
439 .await
440 .map_err(from_sqlx_error)?;
441
442 sqlx::query(&format!(
443 "CREATE TABLE IF NOT EXISTS {NAMESPACE_TABLE_NAME} (
444 {CATALOG_FIELD_CATALOG_NAME} VARCHAR(255) NOT NULL,
445 {NAMESPACE_FIELD_NAME} VARCHAR(255) NOT NULL,
446 {NAMESPACE_FIELD_PROPERTY_KEY} VARCHAR(255),
447 {NAMESPACE_FIELD_PROPERTY_VALUE} VARCHAR(1000),
448 PRIMARY KEY ({CATALOG_FIELD_CATALOG_NAME}, {NAMESPACE_FIELD_NAME}, {NAMESPACE_FIELD_PROPERTY_KEY}))"
449 ))
450 .execute(&pool)
451 .await
452 .map_err(from_sqlx_error)?;
453
454 let detected_schema_version = SchemaVersion::detect(&pool).await?;
455 let expected_schema_version = config.schema_version;
456
457 let schema_version = match (detected_schema_version, expected_schema_version) {
459 (SchemaVersion::V1, Some(SchemaVersion::V1) | None) => {
460 tracing::debug!(
461 "detected {CATALOG_TABLE_NAME} schema {} which already supports views",
462 detected_schema_version,
463 );
464 SchemaVersion::V1
465 }
466 (SchemaVersion::V0, Some(expected_schema_version @ SchemaVersion::V1)) => {
467 tracing::warn!(
468 "table {CATALOG_TABLE_NAME} has inferred schema {} but expected schema {}, performing migration",
469 detected_schema_version,
470 expected_schema_version,
471 );
472 if let Some(migration_sql) = SchemaVersion::V1.migration_sql() {
473 sqlx::query(&migration_sql)
474 .execute(&pool)
475 .await
476 .map_err(from_sqlx_error)?;
477 }
478 SchemaVersion::V1
479 }
480 (SchemaVersion::V0, Some(SchemaVersion::V0) | None) => {
481 tracing::warn!(
482 "table {CATALOG_TABLE_NAME} has inferred schema {}; SQL catalog is initialized without view support, table creation, and table registration. \
483 To auto-migrate the database schema, set {}=V1",
484 detected_schema_version,
485 SQL_CATALOG_PROP_SCHEMA_VERSION,
486 );
487 SchemaVersion::V0
488 }
489 (SchemaVersion::V1, Some(expected_schema_version @ SchemaVersion::V0)) => {
490 tracing::warn!(
491 "ignoring expected schema {} for table {CATALOG_TABLE_NAME}: the table is \
492 already at schema {}, and downgrade migration is not supported",
493 expected_schema_version,
494 detected_schema_version,
495 );
496 SchemaVersion::V1
497 }
498 };
499
500 Ok(SqlCatalog {
501 name: config.name.to_owned(),
502 connection: pool,
503 warehouse_location: config.warehouse_location,
504 fileio,
505 sql_bind_style: config.sql_bind_style,
506 runtime,
507 kms_client,
508 schema_version,
509 })
510 }
511
512 fn replace_placeholders(&self, query: &str) -> String {
514 match self.sql_bind_style {
515 SqlBindStyle::DollarNumeric => {
516 let mut count = 1;
517 query
518 .chars()
519 .fold(String::with_capacity(query.len()), |mut acc, c| {
520 if c == '?' {
521 acc.push('$');
522 acc.push_str(&count.to_string());
523 count += 1;
524 } else {
525 acc.push(c);
526 }
527 acc
528 })
529 }
530 _ => query.to_owned(),
531 }
532 }
533
534 async fn fetch_rows(&self, query: &str, args: Vec<Option<&str>>) -> Result<Vec<AnyRow>> {
536 let query_with_placeholders = self.replace_placeholders(query);
537
538 let mut sqlx_query = sqlx::query(&query_with_placeholders);
539 for arg in args {
540 sqlx_query = sqlx_query.bind(arg);
541 }
542
543 sqlx_query
544 .fetch_all(&self.connection)
545 .await
546 .map_err(from_sqlx_error)
547 }
548
549 async fn execute(
551 &self,
552 query: &str,
553 args: Vec<Option<&str>>,
554 transaction: Option<&mut Transaction<'_, Any>>,
555 ) -> Result<AnyQueryResult> {
556 let query_with_placeholders = self.replace_placeholders(query);
557
558 let mut sqlx_query = sqlx::query(&query_with_placeholders);
559 for arg in args {
560 sqlx_query = sqlx_query.bind(arg);
561 }
562
563 match transaction {
564 Some(t) => sqlx_query.execute(&mut **t).await.map_err(from_sqlx_error),
565 None => {
566 let mut tx = self.connection.begin().await.map_err(from_sqlx_error)?;
567 let result = sqlx_query
568 .execute(&mut *tx)
569 .await
570 .map_err(from_sqlx_error)?;
571 tx.commit().await.map_err(from_sqlx_error)?;
572 Ok(result)
573 }
574 }
575 }
576}
577
578#[async_trait]
579impl Catalog for SqlCatalog {
580 async fn list_namespaces(
581 &self,
582 parent: Option<&NamespaceIdent>,
583 ) -> Result<Vec<NamespaceIdent>> {
584 let all_namespaces_stmt = format!(
586 "SELECT {CATALOG_FIELD_TABLE_NAMESPACE}
587 FROM {CATALOG_TABLE_NAME}
588 WHERE {CATALOG_FIELD_CATALOG_NAME} = ?
589 UNION
590 SELECT {NAMESPACE_FIELD_NAME}
591 FROM {NAMESPACE_TABLE_NAME}
592 WHERE {CATALOG_FIELD_CATALOG_NAME} = ?"
593 );
594
595 let namespace_rows = self
596 .fetch_rows(&all_namespaces_stmt, vec![
597 Some(&self.name),
598 Some(&self.name),
599 ])
600 .await?;
601
602 let mut namespaces = HashSet::<NamespaceIdent>::with_capacity(namespace_rows.len());
603
604 if let Some(parent) = parent {
605 if self.namespace_exists(parent).await? {
606 let parent_str = parent.join(".");
607
608 for row in namespace_rows.iter() {
609 let nsp = row.try_get::<String, _>(0).map_err(from_sqlx_error)?;
610 if nsp != parent_str && nsp.starts_with(&parent_str) {
612 namespaces.insert(NamespaceIdent::from_strs(nsp.split("."))?);
613 }
614 }
615
616 Ok(namespaces.into_iter().collect::<Vec<NamespaceIdent>>())
617 } else {
618 no_such_namespace_err(parent)
619 }
620 } else {
621 for row in namespace_rows.iter() {
622 let nsp = row.try_get::<String, _>(0).map_err(from_sqlx_error)?;
623 let mut levels = nsp.split(".").collect::<Vec<&str>>();
624 if !levels.is_empty() {
625 let first_level = levels.drain(..1).collect::<Vec<&str>>();
626 namespaces.insert(NamespaceIdent::from_strs(first_level)?);
627 }
628 }
629
630 Ok(namespaces.into_iter().collect::<Vec<NamespaceIdent>>())
631 }
632 }
633
634 async fn create_namespace(
635 &self,
636 namespace: &NamespaceIdent,
637 properties: HashMap<String, String>,
638 ) -> Result<Namespace> {
639 let exists = self.namespace_exists(namespace).await?;
640
641 if exists {
642 return Err(Error::new(
643 ErrorKind::NamespaceAlreadyExists,
644 format!("Namespace {namespace:?} already exists"),
645 ));
646 }
647
648 let namespace_str = namespace.join(".");
649 let insert = format!(
650 "INSERT INTO {NAMESPACE_TABLE_NAME} ({CATALOG_FIELD_CATALOG_NAME}, {NAMESPACE_FIELD_NAME}, {NAMESPACE_FIELD_PROPERTY_KEY}, {NAMESPACE_FIELD_PROPERTY_VALUE})
651 VALUES (?, ?, ?, ?)");
652 if !properties.is_empty() {
653 let mut insert_properties = properties.clone();
654 insert_properties.insert("exists".to_string(), "true".to_string());
655
656 let mut query_args = Vec::with_capacity(insert_properties.len() * 4);
657 let mut insert_stmt = insert.clone();
658 for (index, (key, value)) in insert_properties.iter().enumerate() {
659 query_args.extend_from_slice(&[
660 Some(self.name.as_str()),
661 Some(namespace_str.as_str()),
662 Some(key.as_str()),
663 Some(value.as_str()),
664 ]);
665 if index > 0 {
666 insert_stmt.push_str(", (?, ?, ?, ?)");
667 }
668 }
669
670 self.execute(&insert_stmt, query_args, None).await?;
671
672 Ok(Namespace::with_properties(
673 namespace.clone(),
674 insert_properties,
675 ))
676 } else {
677 self.execute(
679 &insert,
680 vec![
681 Some(&self.name),
682 Some(&namespace_str),
683 Some("exists"),
684 Some("true"),
685 ],
686 None,
687 )
688 .await?;
689 Ok(Namespace::with_properties(namespace.clone(), properties))
690 }
691 }
692
693 async fn get_namespace(&self, namespace: &NamespaceIdent) -> Result<Namespace> {
694 let exists = self.namespace_exists(namespace).await?;
695 if exists {
696 let namespace_props = self
697 .fetch_rows(
698 &format!(
699 "SELECT
700 {NAMESPACE_FIELD_NAME},
701 {NAMESPACE_FIELD_PROPERTY_KEY},
702 {NAMESPACE_FIELD_PROPERTY_VALUE}
703 FROM {NAMESPACE_TABLE_NAME}
704 WHERE {CATALOG_FIELD_CATALOG_NAME} = ?
705 AND {NAMESPACE_FIELD_NAME} = ?"
706 ),
707 vec![Some(&self.name), Some(&namespace.join("."))],
708 )
709 .await?;
710
711 let mut properties = HashMap::with_capacity(namespace_props.len());
712
713 for row in namespace_props {
714 let key = row
715 .try_get::<String, _>(NAMESPACE_FIELD_PROPERTY_KEY)
716 .map_err(from_sqlx_error)?;
717 let value = row
718 .try_get::<String, _>(NAMESPACE_FIELD_PROPERTY_VALUE)
719 .map_err(from_sqlx_error)?;
720
721 properties.insert(key, value);
722 }
723
724 Ok(Namespace::with_properties(namespace.clone(), properties))
725 } else {
726 no_such_namespace_err(namespace)
727 }
728 }
729
730 async fn namespace_exists(&self, namespace: &NamespaceIdent) -> Result<bool> {
731 let namespace_str = namespace.join(".");
732
733 let table_namespaces = self
734 .fetch_rows(
735 &format!(
736 "SELECT 1 FROM {CATALOG_TABLE_NAME}
737 WHERE {CATALOG_FIELD_CATALOG_NAME} = ?
738 AND {CATALOG_FIELD_TABLE_NAMESPACE} = ?
739 LIMIT 1"
740 ),
741 vec![Some(&self.name), Some(&namespace_str)],
742 )
743 .await?;
744
745 if !table_namespaces.is_empty() {
746 Ok(true)
747 } else {
748 let namespaces = self
749 .fetch_rows(
750 &format!(
751 "SELECT 1 FROM {NAMESPACE_TABLE_NAME}
752 WHERE {CATALOG_FIELD_CATALOG_NAME} = ?
753 AND {NAMESPACE_FIELD_NAME} = ?
754 LIMIT 1"
755 ),
756 vec![Some(&self.name), Some(&namespace_str)],
757 )
758 .await?;
759 if !namespaces.is_empty() {
760 Ok(true)
761 } else {
762 Ok(false)
763 }
764 }
765 }
766
767 async fn update_namespace(
768 &self,
769 namespace: &NamespaceIdent,
770 properties: HashMap<String, String>,
771 ) -> Result<()> {
772 let exists = self.namespace_exists(namespace).await?;
773 if exists {
774 let existing_properties = self.get_namespace(namespace).await?.properties().clone();
775 let namespace_str = namespace.join(".");
776
777 let mut updates = vec![];
778 let mut inserts = vec![];
779
780 for (key, value) in properties.iter() {
781 if existing_properties.contains_key(key) {
782 if existing_properties.get(key) != Some(value) {
783 updates.push((key, value));
784 }
785 } else {
786 inserts.push((key, value));
787 }
788 }
789
790 let mut tx = self.connection.begin().await.map_err(from_sqlx_error)?;
791 let update_stmt = format!(
792 "UPDATE {NAMESPACE_TABLE_NAME} SET {NAMESPACE_FIELD_PROPERTY_VALUE} = ?
793 WHERE {CATALOG_FIELD_CATALOG_NAME} = ?
794 AND {NAMESPACE_FIELD_NAME} = ?
795 AND {NAMESPACE_FIELD_PROPERTY_KEY} = ?"
796 );
797
798 let insert_stmt = format!(
799 "INSERT INTO {NAMESPACE_TABLE_NAME} ({CATALOG_FIELD_CATALOG_NAME}, {NAMESPACE_FIELD_NAME}, {NAMESPACE_FIELD_PROPERTY_KEY}, {NAMESPACE_FIELD_PROPERTY_VALUE})
800 VALUES (?, ?, ?, ?)"
801 );
802
803 for (key, value) in updates {
804 self.execute(
805 &update_stmt,
806 vec![
807 Some(value),
808 Some(&self.name),
809 Some(&namespace_str),
810 Some(key),
811 ],
812 Some(&mut tx),
813 )
814 .await?;
815 }
816
817 for (key, value) in inserts {
818 self.execute(
819 &insert_stmt,
820 vec![
821 Some(&self.name),
822 Some(&namespace_str),
823 Some(key),
824 Some(value),
825 ],
826 Some(&mut tx),
827 )
828 .await?;
829 }
830
831 let _ = tx.commit().await.map_err(from_sqlx_error)?;
832
833 Ok(())
834 } else {
835 no_such_namespace_err(namespace)
836 }
837 }
838
839 async fn drop_namespace(&self, namespace: &NamespaceIdent) -> Result<()> {
840 let exists = self.namespace_exists(namespace).await?;
841 if exists {
842 let tables = self.list_tables(namespace).await?;
844 if !tables.is_empty() {
845 return Err(Error::new(
846 ErrorKind::Unexpected,
847 format!(
848 "Namespace {:?} is not empty. {} tables exist.",
849 namespace,
850 tables.len()
851 ),
852 ));
853 }
854
855 self.execute(
856 &format!(
857 "DELETE FROM {NAMESPACE_TABLE_NAME}
858 WHERE {NAMESPACE_FIELD_NAME} = ?
859 AND {CATALOG_FIELD_CATALOG_NAME} = ?"
860 ),
861 vec![Some(&namespace.join(".")), Some(&self.name)],
862 None,
863 )
864 .await?;
865
866 Ok(())
867 } else {
868 no_such_namespace_err(namespace)
869 }
870 }
871
872 async fn list_tables(&self, namespace: &NamespaceIdent) -> Result<Vec<TableIdent>> {
873 let exists = self.namespace_exists(namespace).await?;
874 if exists {
875 let rows = self
876 .fetch_rows(
877 &format!(
878 "SELECT {CATALOG_FIELD_TABLE_NAME},
879 {CATALOG_FIELD_TABLE_NAMESPACE}
880 FROM {CATALOG_TABLE_NAME}
881 WHERE {CATALOG_FIELD_TABLE_NAMESPACE} = ?
882 AND {CATALOG_FIELD_CATALOG_NAME} = ?
883 {}",
884 self.schema_version.record_type_filter()
885 ),
886 vec![Some(&namespace.join(".")), Some(&self.name)],
887 )
888 .await?;
889
890 let mut tables = HashSet::<TableIdent>::with_capacity(rows.len());
891
892 for row in rows.iter() {
893 let tbl = row
894 .try_get::<String, _>(CATALOG_FIELD_TABLE_NAME)
895 .map_err(from_sqlx_error)?;
896 let ns_strs = row
897 .try_get::<String, _>(CATALOG_FIELD_TABLE_NAMESPACE)
898 .map_err(from_sqlx_error)?;
899 let ns = NamespaceIdent::from_strs(ns_strs.split("."))?;
900 tables.insert(TableIdent::new(ns, tbl));
901 }
902
903 Ok(tables.into_iter().collect::<Vec<TableIdent>>())
904 } else {
905 no_such_namespace_err(namespace)
906 }
907 }
908
909 async fn table_exists(&self, identifier: &TableIdent) -> Result<bool> {
910 let namespace = identifier.namespace().join(".");
911 let table_name = identifier.name();
912 let table_counts = self
913 .fetch_rows(
914 &format!(
915 "SELECT 1
916 FROM {CATALOG_TABLE_NAME}
917 WHERE {CATALOG_FIELD_TABLE_NAMESPACE} = ?
918 AND {CATALOG_FIELD_CATALOG_NAME} = ?
919 AND {CATALOG_FIELD_TABLE_NAME} = ?
920 {}",
921 self.schema_version.record_type_filter()
922 ),
923 vec![Some(&namespace), Some(&self.name), Some(table_name)],
924 )
925 .await?;
926
927 if !table_counts.is_empty() {
928 Ok(true)
929 } else {
930 Ok(false)
931 }
932 }
933
934 async fn drop_table(&self, identifier: &TableIdent) -> Result<()> {
935 if !self.table_exists(identifier).await? {
936 return no_such_table_err(identifier);
937 }
938
939 self.execute(
940 &format!(
941 "DELETE FROM {CATALOG_TABLE_NAME}
942 WHERE {CATALOG_FIELD_CATALOG_NAME} = ?
943 AND {CATALOG_FIELD_TABLE_NAME} = ?
944 AND {CATALOG_FIELD_TABLE_NAMESPACE} = ?
945 {}",
946 self.schema_version.record_type_filter()
947 ),
948 vec![
949 Some(&self.name),
950 Some(identifier.name()),
951 Some(&identifier.namespace().join(".")),
952 ],
953 None,
954 )
955 .await?;
956
957 Ok(())
958 }
959
960 async fn purge_table(&self, table: &TableIdent) -> Result<()> {
961 let table_info = self.load_table(table).await?;
962 self.drop_table(table).await?;
963 iceberg::drop_table_data(&table_info).await
964 }
965
966 async fn load_table(&self, identifier: &TableIdent) -> Result<Table> {
967 if !self.table_exists(identifier).await? {
968 return no_such_table_err(identifier);
969 }
970
971 let rows = self
972 .fetch_rows(
973 &format!(
974 "SELECT {CATALOG_FIELD_METADATA_LOCATION_PROP}
975 FROM {CATALOG_TABLE_NAME}
976 WHERE {CATALOG_FIELD_CATALOG_NAME} = ?
977 AND {CATALOG_FIELD_TABLE_NAME} = ?
978 AND {CATALOG_FIELD_TABLE_NAMESPACE} = ?
979 {}",
980 self.schema_version.record_type_filter()
981 ),
982 vec![
983 Some(&self.name),
984 Some(identifier.name()),
985 Some(&identifier.namespace().join(".")),
986 ],
987 )
988 .await?;
989
990 if rows.is_empty() {
991 return no_such_table_err(identifier);
992 }
993
994 let row = &rows[0];
995 let tbl_metadata_location = row
996 .try_get::<String, _>(CATALOG_FIELD_METADATA_LOCATION_PROP)
997 .map_err(from_sqlx_error)?;
998
999 let metadata = TableMetadata::read_from(&self.fileio, &tbl_metadata_location).await?;
1000
1001 let mut builder = Table::builder()
1002 .file_io(self.fileio.clone())
1003 .identifier(identifier.clone())
1004 .metadata_location(tbl_metadata_location)
1005 .metadata(metadata)
1006 .runtime(self.runtime.clone());
1007 if let Some(kms_client) = self.kms_client.clone() {
1008 builder = builder.kms_client(kms_client);
1009 }
1010 Ok(builder.build()?)
1011 }
1012
1013 async fn create_table(
1014 &self,
1015 namespace: &NamespaceIdent,
1016 creation: TableCreation,
1017 ) -> Result<Table> {
1018 if self.schema_version != SchemaVersion::V1 {
1019 return Err(Error::new(
1020 ErrorKind::FeatureUnsupported,
1021 format!(
1022 "Table creation is not supported for SQL catalog schema version {}",
1023 self.schema_version
1024 ),
1025 ));
1026 }
1027
1028 if !self.namespace_exists(namespace).await? {
1029 return no_such_namespace_err(namespace);
1030 }
1031
1032 let tbl_name = creation.name.clone();
1033 let tbl_ident = TableIdent::new(namespace.clone(), tbl_name.clone());
1034
1035 if self.table_exists(&tbl_ident).await? {
1036 return table_already_exists_err(&tbl_ident);
1037 }
1038
1039 let tbl_creation = if creation.location.is_some() {
1040 creation
1041 } else {
1042 let nsp_properties = self.get_namespace(namespace).await?.properties().clone();
1045 let nsp_location = match nsp_properties.get(NAMESPACE_LOCATION_PROPERTY_KEY) {
1046 Some(location) => location.clone(),
1047 None => {
1048 format!(
1049 "{}/{}",
1050 self.warehouse_location.clone(),
1051 namespace.join("/")
1052 )
1053 }
1054 };
1055
1056 let tbl_location = format!("{}/{}", nsp_location, tbl_ident.name());
1057
1058 TableCreation {
1059 location: Some(tbl_location),
1060 ..creation
1061 }
1062 };
1063
1064 let tbl_metadata = TableMetadataBuilder::from_table_creation(tbl_creation)?
1065 .build()?
1066 .metadata;
1067 let tbl_metadata_location = MetadataLocation::try_new_with_metadata(&tbl_metadata)?;
1068
1069 tbl_metadata
1070 .write_to(&self.fileio, &tbl_metadata_location)
1071 .await?;
1072
1073 let tbl_metadata_location_str = tbl_metadata_location.to_string();
1074 self.execute(&format!(
1075 "INSERT INTO {CATALOG_TABLE_NAME}
1076 ({CATALOG_FIELD_CATALOG_NAME}, {CATALOG_FIELD_TABLE_NAMESPACE}, {CATALOG_FIELD_TABLE_NAME}, {CATALOG_FIELD_METADATA_LOCATION_PROP}, {CATALOG_FIELD_RECORD_TYPE})
1077 VALUES (?, ?, ?, ?, ?)
1078 "), vec![Some(&self.name), Some(&namespace.join(".")), Some(&tbl_name.clone()), Some(&tbl_metadata_location_str), Some(CATALOG_FIELD_TABLE_RECORD_TYPE)], None).await?;
1079
1080 let mut builder = Table::builder()
1081 .file_io(self.fileio.clone())
1082 .metadata_location(tbl_metadata_location_str)
1083 .identifier(tbl_ident)
1084 .metadata(tbl_metadata)
1085 .runtime(self.runtime.clone());
1086 if let Some(kms_client) = self.kms_client.clone() {
1087 builder = builder.kms_client(kms_client);
1088 }
1089 Ok(builder.build()?)
1090 }
1091
1092 async fn rename_table(&self, src: &TableIdent, dest: &TableIdent) -> Result<()> {
1093 if src == dest {
1094 return Ok(());
1095 }
1096
1097 if !self.table_exists(src).await? {
1098 return no_such_table_err(src);
1099 }
1100
1101 if !self.namespace_exists(dest.namespace()).await? {
1102 return no_such_namespace_err(dest.namespace());
1103 }
1104
1105 if self.table_exists(dest).await? {
1106 return table_already_exists_err(dest);
1107 }
1108
1109 self.execute(
1110 &format!(
1111 "UPDATE {CATALOG_TABLE_NAME}
1112 SET {CATALOG_FIELD_TABLE_NAME} = ?, {CATALOG_FIELD_TABLE_NAMESPACE} = ?
1113 WHERE {CATALOG_FIELD_CATALOG_NAME} = ?
1114 AND {CATALOG_FIELD_TABLE_NAME} = ?
1115 AND {CATALOG_FIELD_TABLE_NAMESPACE} = ?
1116 {}",
1117 self.schema_version.record_type_filter()
1118 ),
1119 vec![
1120 Some(dest.name()),
1121 Some(&dest.namespace().join(".")),
1122 Some(&self.name),
1123 Some(src.name()),
1124 Some(&src.namespace().join(".")),
1125 ],
1126 None,
1127 )
1128 .await?;
1129
1130 Ok(())
1131 }
1132
1133 async fn register_table(
1134 &self,
1135 table_ident: &TableIdent,
1136 metadata_location: String,
1137 ) -> Result<Table> {
1138 if self.schema_version != SchemaVersion::V1 {
1139 return Err(Error::new(
1140 ErrorKind::FeatureUnsupported,
1141 format!(
1142 "Table registration is not supported for SQL catalog schema version {}",
1143 self.schema_version
1144 ),
1145 ));
1146 }
1147
1148 if self.table_exists(table_ident).await? {
1149 return table_already_exists_err(table_ident);
1150 }
1151
1152 let metadata = TableMetadata::read_from(&self.fileio, &metadata_location).await?;
1153
1154 let namespace = table_ident.namespace();
1155 let tbl_name = table_ident.name().to_string();
1156
1157 self.execute(&format!(
1158 "INSERT INTO {CATALOG_TABLE_NAME}
1159 ({CATALOG_FIELD_CATALOG_NAME}, {CATALOG_FIELD_TABLE_NAMESPACE}, {CATALOG_FIELD_TABLE_NAME}, {CATALOG_FIELD_METADATA_LOCATION_PROP}, {CATALOG_FIELD_RECORD_TYPE})
1160 VALUES (?, ?, ?, ?, ?)
1161 "), vec![Some(&self.name), Some(&namespace.join(".")), Some(&tbl_name), Some(&metadata_location), Some(CATALOG_FIELD_TABLE_RECORD_TYPE)], None).await?;
1162
1163 let mut builder = Table::builder()
1164 .identifier(table_ident.clone())
1165 .metadata_location(metadata_location)
1166 .metadata(metadata)
1167 .file_io(self.fileio.clone())
1168 .runtime(self.runtime.clone());
1169 if let Some(kms_client) = self.kms_client.clone() {
1170 builder = builder.kms_client(kms_client);
1171 }
1172 Ok(builder.build()?)
1173 }
1174
1175 async fn update_table(&self, commit: TableCommit) -> Result<Table> {
1177 let table_ident = commit.identifier().clone();
1178 let current_table = self.load_table(&table_ident).await?;
1179 let current_metadata_location = current_table.metadata_location_result()?.to_string();
1180
1181 let staged_table = commit.apply(current_table)?;
1182 let staged_metadata_location_str = staged_table.metadata_location_result()?;
1183 let staged_metadata_location = MetadataLocation::from_str(staged_metadata_location_str)?;
1184
1185 staged_table
1186 .metadata()
1187 .write_to(staged_table.file_io(), &staged_metadata_location)
1188 .await?;
1189
1190 let staged_metadata_location_str = staged_metadata_location.to_string();
1191 let update_result = self
1192 .execute(
1193 &format!(
1194 "UPDATE {CATALOG_TABLE_NAME}
1195 SET {CATALOG_FIELD_METADATA_LOCATION_PROP} = ?, {CATALOG_FIELD_PREVIOUS_METADATA_LOCATION_PROP} = ?
1196 WHERE {CATALOG_FIELD_CATALOG_NAME} = ?
1197 AND {CATALOG_FIELD_TABLE_NAME} = ?
1198 AND {CATALOG_FIELD_TABLE_NAMESPACE} = ?
1199 {}
1200 AND {CATALOG_FIELD_METADATA_LOCATION_PROP} = ?",
1201 self.schema_version.record_type_filter()
1202 ),
1203 vec![
1204 Some(&staged_metadata_location_str),
1205 Some(current_metadata_location.as_str()),
1206 Some(&self.name),
1207 Some(table_ident.name()),
1208 Some(&table_ident.namespace().join(".")),
1209 Some(current_metadata_location.as_str()),
1210 ],
1211 None,
1212 )
1213 .await?;
1214
1215 if update_result.rows_affected() == 0 {
1216 return Err(Error::new(
1217 ErrorKind::CatalogCommitConflicts,
1218 format!("Commit conflicted for table: {table_ident}"),
1219 )
1220 .with_retryable(true));
1221 }
1222
1223 Ok(staged_table)
1224 }
1225}
1226
1227#[cfg(test)]
1228mod tests {
1229 use std::collections::{HashMap, HashSet};
1230 use std::hash::Hash;
1231 use std::sync::Arc;
1232
1233 use iceberg::io::LocalFsStorageFactory;
1234 use iceberg::spec::{NestedField, PartitionSpec, PrimitiveType, Schema, SortOrder, Type};
1235 use iceberg::table::Table;
1236 use iceberg::{
1237 Catalog, CatalogBuilder, ErrorKind, Namespace, NamespaceIdent, TableCreation, TableIdent,
1238 };
1239 use itertools::Itertools;
1240 use regex::Regex;
1241 use sqlx::any::install_default_drivers;
1242 use sqlx::migrate::MigrateDatabase;
1243 use sqlx::{Column, Executor};
1244 use tempfile::TempDir;
1245
1246 use crate::catalog::{
1247 CATALOG_FIELD_RECORD_TYPE, CATALOG_TABLE_NAME, NAMESPACE_LOCATION_PROPERTY_KEY,
1248 NAMESPACE_TABLE_NAME, SQL_CATALOG_PROP_BIND_STYLE, SQL_CATALOG_PROP_BIND_STYLE_LEGACY,
1249 SQL_CATALOG_PROP_SCHEMA_VERSION, SQL_CATALOG_PROP_URI, SQL_CATALOG_PROP_WAREHOUSE,
1250 };
1251 use crate::{SchemaVersion, SqlBindStyle, SqlCatalog, SqlCatalogBuilder};
1252
1253 const UUID_REGEX_STR: &str = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
1254
1255 fn temp_path() -> String {
1256 let temp_dir = TempDir::new().unwrap();
1257 temp_dir.path().to_str().unwrap().to_string()
1258 }
1259
1260 fn to_set<T: Eq + Hash>(vec: Vec<T>) -> HashSet<T> {
1261 HashSet::from_iter(vec)
1262 }
1263
1264 fn default_properties() -> HashMap<String, String> {
1265 HashMap::from([("exists".to_string(), "true".to_string())])
1266 }
1267
1268 async fn new_sql_catalog(
1270 warehouse_location: String,
1271 name: Option<impl ToString>,
1272 ) -> impl Catalog {
1273 let name = if let Some(name) = name {
1274 name.to_string()
1275 } else {
1276 "iceberg".to_string()
1277 };
1278 let sql_lite_uri = format!("sqlite:{}", temp_path());
1279 sqlx::Sqlite::create_database(&sql_lite_uri).await.unwrap();
1280
1281 let props = HashMap::from_iter([
1282 (SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri.to_string()),
1283 (SQL_CATALOG_PROP_WAREHOUSE.to_string(), warehouse_location),
1284 (
1285 SQL_CATALOG_PROP_BIND_STYLE.to_string(),
1286 SqlBindStyle::DollarNumeric.to_string(),
1287 ),
1288 ]);
1289 SqlCatalogBuilder::default()
1290 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1291 .load(&name, props)
1292 .await
1293 .unwrap()
1294 }
1295
1296 async fn create_namespace<C: Catalog>(catalog: &C, namespace_ident: &NamespaceIdent) {
1297 let _ = catalog
1298 .create_namespace(namespace_ident, HashMap::new())
1299 .await
1300 .unwrap();
1301 }
1302
1303 async fn create_namespaces<C: Catalog>(catalog: &C, namespace_idents: &Vec<&NamespaceIdent>) {
1304 for namespace_ident in namespace_idents {
1305 let _ = create_namespace(catalog, namespace_ident).await;
1306 }
1307 }
1308
1309 fn simple_table_schema() -> Schema {
1310 Schema::builder()
1311 .with_fields(vec![
1312 NestedField::required(1, "foo", Type::Primitive(PrimitiveType::Int)).into(),
1313 ])
1314 .build()
1315 .unwrap()
1316 }
1317
1318 async fn create_table<C: Catalog>(catalog: &C, table_ident: &TableIdent) {
1319 let _ = catalog
1320 .create_table(
1321 &table_ident.namespace,
1322 TableCreation::builder()
1323 .name(table_ident.name().into())
1324 .schema(simple_table_schema())
1325 .location(temp_path())
1326 .build(),
1327 )
1328 .await
1329 .unwrap();
1330 }
1331
1332 async fn create_tables<C: Catalog>(catalog: &C, table_idents: Vec<&TableIdent>) {
1333 for table_ident in table_idents {
1334 create_table(catalog, table_ident).await;
1335 }
1336 }
1337
1338 fn assert_table_eq(table: &Table, expected_table_ident: &TableIdent, expected_schema: &Schema) {
1339 assert_eq!(table.identifier(), expected_table_ident);
1340
1341 let metadata = table.metadata();
1342
1343 assert_eq!(metadata.current_schema().as_ref(), expected_schema);
1344
1345 let expected_partition_spec = PartitionSpec::builder(expected_schema.clone())
1346 .with_spec_id(0)
1347 .build()
1348 .unwrap();
1349
1350 assert_eq!(
1351 metadata
1352 .partition_specs_iter()
1353 .map(|p| p.as_ref())
1354 .collect_vec(),
1355 vec![&expected_partition_spec]
1356 );
1357
1358 let expected_sorted_order = SortOrder::builder()
1359 .with_order_id(0)
1360 .with_fields(vec![])
1361 .build(expected_schema)
1362 .unwrap();
1363
1364 assert_eq!(
1365 metadata
1366 .sort_orders_iter()
1367 .map(|s| s.as_ref())
1368 .collect_vec(),
1369 vec![&expected_sorted_order]
1370 );
1371
1372 assert_eq!(metadata.properties(), &HashMap::new());
1373
1374 assert!(!table.readonly());
1375 }
1376
1377 fn assert_table_metadata_location_matches(table: &Table, regex_str: &str) {
1378 let actual = table.metadata_location().unwrap().to_string();
1379 let regex = Regex::new(regex_str).unwrap();
1380 assert!(regex.is_match(&actual))
1381 }
1382
1383 #[tokio::test]
1384 async fn test_initialized() {
1385 let warehouse_loc = temp_path();
1386 new_sql_catalog(warehouse_loc.clone(), Some("iceberg")).await;
1387 new_sql_catalog(warehouse_loc.clone(), Some("iceberg")).await;
1389 new_sql_catalog(warehouse_loc.clone(), Some("iceberg")).await;
1390 }
1391
1392 async fn new_commit_error_catalog() -> SqlCatalog {
1393 let sql_lite_uri = format!("sqlite:{}", temp_path());
1394 sqlx::Sqlite::create_database(&sql_lite_uri).await.unwrap();
1395 let catalog = SqlCatalogBuilder::default()
1396 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1397 .prop("pool.max-connections", "1")
1398 .load(
1399 "iceberg",
1400 HashMap::from_iter([
1401 (SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri),
1402 (SQL_CATALOG_PROP_WAREHOUSE.to_string(), temp_path()),
1403 ]),
1404 )
1405 .await
1406 .unwrap();
1407
1408 catalog
1409 .connection
1410 .execute("PRAGMA foreign_keys = ON")
1411 .await
1412 .unwrap();
1413 catalog
1415 .connection
1416 .execute("CREATE TABLE parent(id INTEGER PRIMARY KEY)")
1417 .await
1418 .unwrap();
1419 catalog
1420 .connection
1421 .execute(
1422 "CREATE TABLE child(parent_id INTEGER REFERENCES parent(id) \
1423 DEFERRABLE INITIALLY DEFERRED)",
1424 )
1425 .await
1426 .unwrap();
1427
1428 catalog
1429 }
1430
1431 #[tokio::test]
1432 async fn test_execute_returns_commit_error() {
1433 let catalog = new_commit_error_catalog().await;
1434
1435 let trigger = format!(
1438 "CREATE TRIGGER fail_namespace_commit
1439 AFTER INSERT ON {NAMESPACE_TABLE_NAME}
1440 BEGIN INSERT INTO child VALUES (1); END"
1441 );
1442 catalog.connection.execute(trigger.as_str()).await.unwrap();
1443
1444 let failed_namespace = NamespaceIdent::new("failed".into());
1445 let error = catalog
1446 .create_namespace(&failed_namespace, HashMap::new())
1447 .await
1448 .unwrap_err();
1449 assert_eq!(error.kind(), ErrorKind::Unexpected);
1450 assert!(!catalog.namespace_exists(&failed_namespace).await.unwrap());
1451
1452 catalog
1454 .connection
1455 .execute("INSERT INTO parent VALUES (1)")
1456 .await
1457 .unwrap();
1458 let committed_namespace = NamespaceIdent::new("committed".into());
1459 catalog
1460 .create_namespace(&committed_namespace, HashMap::new())
1461 .await
1462 .unwrap();
1463 assert!(
1464 catalog
1465 .namespace_exists(&committed_namespace)
1466 .await
1467 .unwrap()
1468 );
1469 }
1470
1471 #[tokio::test]
1474 async fn test_storage_props_propagate_to_file_io() {
1475 let sql_lite_uri = format!("sqlite:{}", temp_path());
1476 sqlx::Sqlite::create_database(&sql_lite_uri).await.unwrap();
1477 let warehouse_location = temp_path();
1478
1479 let catalog = SqlCatalogBuilder::default()
1480 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1481 .load(
1482 "iceberg",
1483 HashMap::from_iter([
1484 (SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri),
1485 (SQL_CATALOG_PROP_WAREHOUSE.to_string(), warehouse_location),
1486 ("s3.region".to_string(), "us-east-1".to_string()),
1487 ("hf.token".to_string(), "hf_test_token".to_string()),
1488 ]),
1489 )
1490 .await
1491 .unwrap();
1492
1493 let props = catalog.fileio.config().props();
1494 assert_eq!(props.get("s3.region"), Some(&"us-east-1".to_string()));
1495 assert_eq!(props.get("hf.token"), Some(&"hf_test_token".to_string()));
1496 }
1497
1498 #[tokio::test]
1499 async fn test_builder_method() {
1500 let sql_lite_uri = format!("sqlite:{}", temp_path());
1501 sqlx::Sqlite::create_database(&sql_lite_uri).await.unwrap();
1502 let warehouse_location = temp_path();
1503
1504 let catalog = SqlCatalogBuilder::default()
1505 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1506 .uri(sql_lite_uri.to_string())
1507 .warehouse_location(warehouse_location.clone())
1508 .sql_bind_style(SqlBindStyle::QMark)
1509 .load("iceberg", HashMap::default())
1510 .await;
1511 assert!(catalog.is_ok());
1512
1513 let catalog = catalog.unwrap();
1514 assert!(catalog.warehouse_location == warehouse_location);
1515 assert!(catalog.sql_bind_style == SqlBindStyle::QMark);
1516 }
1517
1518 #[tokio::test]
1521 async fn test_builder_props_non_existent_path_fails() {
1522 let sql_lite_uri = format!("sqlite:{}", temp_path());
1523 let sql_lite_uri2 = format!("sqlite:{}", temp_path());
1524 sqlx::Sqlite::create_database(&sql_lite_uri).await.unwrap();
1525 let warehouse_location = temp_path();
1526
1527 let catalog = SqlCatalogBuilder::default()
1528 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1529 .uri(sql_lite_uri)
1530 .warehouse_location(warehouse_location)
1531 .load(
1532 "iceberg",
1533 HashMap::from_iter([(SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri2)]),
1534 )
1535 .await;
1536 assert!(catalog.is_err());
1537 }
1538
1539 #[tokio::test]
1543 async fn test_builder_props_set_valid_uri() {
1544 let sql_lite_uri = format!("sqlite:{}", temp_path());
1545 let sql_lite_uri2 = format!("sqlite:{}", temp_path());
1546 sqlx::Sqlite::create_database(&sql_lite_uri).await.unwrap();
1547 let warehouse_location = temp_path();
1548
1549 let catalog = SqlCatalogBuilder::default()
1550 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1551 .uri(sql_lite_uri2)
1552 .warehouse_location(warehouse_location)
1553 .load(
1554 "iceberg",
1555 HashMap::from_iter([(SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri.clone())]),
1556 )
1557 .await;
1558 assert!(catalog.is_ok());
1559 }
1560
1561 #[tokio::test]
1563 async fn test_builder_props_take_precedence() {
1564 let sql_lite_uri = format!("sqlite:{}", temp_path());
1565 sqlx::Sqlite::create_database(&sql_lite_uri).await.unwrap();
1566 let warehouse_location = temp_path();
1567 let warehouse_location2 = temp_path();
1568
1569 let catalog = SqlCatalogBuilder::default()
1570 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1571 .warehouse_location(warehouse_location2)
1572 .sql_bind_style(SqlBindStyle::DollarNumeric)
1573 .load(
1574 "iceberg",
1575 HashMap::from_iter([
1576 (SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri),
1577 (
1578 SQL_CATALOG_PROP_WAREHOUSE.to_string(),
1579 warehouse_location.clone(),
1580 ),
1581 (
1582 SQL_CATALOG_PROP_BIND_STYLE.to_string(),
1583 SqlBindStyle::QMark.to_string(),
1584 ),
1585 ]),
1586 )
1587 .await;
1588
1589 assert!(catalog.is_ok());
1590
1591 let catalog = catalog.unwrap();
1592 assert!(catalog.warehouse_location == warehouse_location);
1593 assert!(catalog.sql_bind_style == SqlBindStyle::QMark);
1594 }
1595
1596 #[tokio::test]
1598 async fn test_builder_props_take_precedence_props() {
1599 let sql_lite_uri = format!("sqlite:{}", temp_path());
1600 let sql_lite_uri2 = format!("sqlite:{}", temp_path());
1601 sqlx::Sqlite::create_database(&sql_lite_uri).await.unwrap();
1602 let warehouse_location = temp_path();
1603 let warehouse_location2 = temp_path();
1604
1605 let props = HashMap::from_iter([
1606 (SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri.clone()),
1607 (
1608 SQL_CATALOG_PROP_WAREHOUSE.to_string(),
1609 warehouse_location.clone(),
1610 ),
1611 (
1612 SQL_CATALOG_PROP_BIND_STYLE.to_string(),
1613 SqlBindStyle::QMark.to_string(),
1614 ),
1615 ]);
1616 let props2 = HashMap::from_iter([
1617 (SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri2.clone()),
1618 (
1619 SQL_CATALOG_PROP_WAREHOUSE.to_string(),
1620 warehouse_location2.clone(),
1621 ),
1622 (
1623 SQL_CATALOG_PROP_BIND_STYLE.to_string(),
1624 SqlBindStyle::DollarNumeric.to_string(),
1625 ),
1626 ]);
1627
1628 let catalog = SqlCatalogBuilder::default()
1629 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1630 .props(props2)
1631 .load("iceberg", props)
1632 .await;
1633
1634 assert!(catalog.is_ok());
1635
1636 let catalog = catalog.unwrap();
1637 assert!(catalog.warehouse_location == warehouse_location);
1638 assert!(catalog.sql_bind_style == SqlBindStyle::QMark);
1639 }
1640
1641 #[tokio::test]
1643 async fn test_builder_props_take_precedence_prop() {
1644 let sql_lite_uri = format!("sqlite:{}", temp_path());
1645 let sql_lite_uri2 = format!("sqlite:{}", temp_path());
1646 sqlx::Sqlite::create_database(&sql_lite_uri).await.unwrap();
1647 let warehouse_location = temp_path();
1648 let warehouse_location2 = temp_path();
1649
1650 let props = HashMap::from_iter([
1651 (SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri.clone()),
1652 (
1653 SQL_CATALOG_PROP_WAREHOUSE.to_string(),
1654 warehouse_location.clone(),
1655 ),
1656 (
1657 SQL_CATALOG_PROP_BIND_STYLE.to_string(),
1658 SqlBindStyle::QMark.to_string(),
1659 ),
1660 ]);
1661
1662 let catalog = SqlCatalogBuilder::default()
1663 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1664 .prop(SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri2)
1665 .prop(SQL_CATALOG_PROP_WAREHOUSE.to_string(), warehouse_location2)
1666 .prop(
1667 SQL_CATALOG_PROP_BIND_STYLE.to_string(),
1668 SqlBindStyle::DollarNumeric.to_string(),
1669 )
1670 .load("iceberg", props)
1671 .await;
1672
1673 assert!(catalog.is_ok());
1674
1675 let catalog = catalog.unwrap();
1676 assert!(catalog.warehouse_location == warehouse_location);
1677 assert!(catalog.sql_bind_style == SqlBindStyle::QMark);
1678 }
1679
1680 #[tokio::test]
1682 async fn test_builder_props_invalid_bind_style_fails() {
1683 let sql_lite_uri = format!("sqlite:{}", temp_path());
1684 sqlx::Sqlite::create_database(&sql_lite_uri).await.unwrap();
1685 let warehouse_location = temp_path();
1686
1687 let catalog = SqlCatalogBuilder::default()
1688 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1689 .load(
1690 "iceberg",
1691 HashMap::from_iter([
1692 (SQL_CATALOG_PROP_URI.to_string(), sql_lite_uri),
1693 (SQL_CATALOG_PROP_WAREHOUSE.to_string(), warehouse_location),
1694 (SQL_CATALOG_PROP_BIND_STYLE.to_string(), "AAA".to_string()),
1695 ]),
1696 )
1697 .await;
1698
1699 assert!(catalog.is_err());
1700 }
1701
1702 #[tokio::test]
1703 async fn test_builder_props_invalid_pool_property_fails() {
1704 for property in [
1705 "pool.max-connections",
1706 "pool.idle-timeout",
1707 "pool.test-before-acquire",
1708 ] {
1709 let error = SqlCatalogBuilder::default()
1710 .with_storage_factory(Arc::new(LocalFsStorageFactory))
1711 .prop(property, "invalid")
1712 .load("iceberg", HashMap::new())
1713 .await
1714 .unwrap_err();
1715
1716 assert_eq!(error.kind(), ErrorKind::DataInvalid);
1717 assert!(error.to_string().contains(property));
1718 assert!(error.to_string().contains("invalid"));
1719 }
1720 }
1721
1722 #[tokio::test]
1723 async fn test_list_namespaces_returns_empty_vector() {
1724 let warehouse_loc = temp_path();
1725 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1726
1727 assert_eq!(catalog.list_namespaces(None).await.unwrap(), vec![]);
1728 }
1729
1730 #[tokio::test]
1731 async fn test_list_namespaces_returns_empty_different_name() {
1732 let warehouse_loc = temp_path();
1733 let catalog = new_sql_catalog(warehouse_loc.clone(), Some("iceberg")).await;
1734 let namespace_ident_1 = NamespaceIdent::new("a".into());
1735 let namespace_ident_2 = NamespaceIdent::new("b".into());
1736 create_namespaces(&catalog, &vec![&namespace_ident_1, &namespace_ident_2]).await;
1737 assert_eq!(
1738 to_set(catalog.list_namespaces(None).await.unwrap()),
1739 to_set(vec![namespace_ident_1, namespace_ident_2])
1740 );
1741
1742 let catalog2 = new_sql_catalog(warehouse_loc, Some("test")).await;
1743 assert_eq!(catalog2.list_namespaces(None).await.unwrap(), vec![]);
1744 }
1745
1746 #[tokio::test]
1747 async fn test_list_namespaces_returns_only_top_level_namespaces() {
1748 let warehouse_loc = temp_path();
1749 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1750 let namespace_ident_1 = NamespaceIdent::new("a".into());
1751 let namespace_ident_2 = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
1752 let namespace_ident_3 = NamespaceIdent::new("b".into());
1753 create_namespaces(&catalog, &vec![
1754 &namespace_ident_1,
1755 &namespace_ident_2,
1756 &namespace_ident_3,
1757 ])
1758 .await;
1759
1760 assert_eq!(
1761 to_set(catalog.list_namespaces(None).await.unwrap()),
1762 to_set(vec![namespace_ident_1, namespace_ident_3])
1763 );
1764 }
1765
1766 #[tokio::test]
1767 async fn test_list_namespaces_returns_no_namespaces_under_parent() {
1768 let warehouse_loc = temp_path();
1769 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1770 let namespace_ident_1 = NamespaceIdent::new("a".into());
1771 let namespace_ident_2 = NamespaceIdent::new("b".into());
1772 create_namespaces(&catalog, &vec![&namespace_ident_1, &namespace_ident_2]).await;
1773
1774 assert_eq!(
1775 catalog
1776 .list_namespaces(Some(&namespace_ident_1))
1777 .await
1778 .unwrap(),
1779 vec![]
1780 );
1781 }
1782
1783 #[tokio::test]
1784 async fn test_list_namespaces_returns_namespace_under_parent() {
1785 let warehouse_loc = temp_path();
1786 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1787 let namespace_ident_1 = NamespaceIdent::new("a".into());
1788 let namespace_ident_2 = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
1789 let namespace_ident_3 = NamespaceIdent::new("c".into());
1790 create_namespaces(&catalog, &vec![
1791 &namespace_ident_1,
1792 &namespace_ident_2,
1793 &namespace_ident_3,
1794 ])
1795 .await;
1796
1797 assert_eq!(
1798 to_set(catalog.list_namespaces(None).await.unwrap()),
1799 to_set(vec![namespace_ident_1.clone(), namespace_ident_3])
1800 );
1801
1802 assert_eq!(
1803 catalog
1804 .list_namespaces(Some(&namespace_ident_1))
1805 .await
1806 .unwrap(),
1807 vec![NamespaceIdent::from_strs(vec!["a", "b"]).unwrap()]
1808 );
1809 }
1810
1811 #[tokio::test]
1812 async fn test_list_namespaces_returns_multiple_namespaces_under_parent() {
1813 let warehouse_loc = temp_path();
1814 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1815 let namespace_ident_1 = NamespaceIdent::new("a".to_string());
1816 let namespace_ident_2 = NamespaceIdent::from_strs(vec!["a", "a"]).unwrap();
1817 let namespace_ident_3 = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
1818 let namespace_ident_4 = NamespaceIdent::from_strs(vec!["a", "c"]).unwrap();
1819 let namespace_ident_5 = NamespaceIdent::new("b".into());
1820 create_namespaces(&catalog, &vec![
1821 &namespace_ident_1,
1822 &namespace_ident_2,
1823 &namespace_ident_3,
1824 &namespace_ident_4,
1825 &namespace_ident_5,
1826 ])
1827 .await;
1828
1829 assert_eq!(
1830 to_set(
1831 catalog
1832 .list_namespaces(Some(&namespace_ident_1))
1833 .await
1834 .unwrap()
1835 ),
1836 to_set(vec![
1837 NamespaceIdent::from_strs(vec!["a", "a"]).unwrap(),
1838 NamespaceIdent::from_strs(vec!["a", "b"]).unwrap(),
1839 NamespaceIdent::from_strs(vec!["a", "c"]).unwrap(),
1840 ])
1841 );
1842 }
1843
1844 #[tokio::test]
1845 async fn test_namespace_exists_returns_false() {
1846 let warehouse_loc = temp_path();
1847 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1848 let namespace_ident = NamespaceIdent::new("a".into());
1849 create_namespace(&catalog, &namespace_ident).await;
1850
1851 assert!(
1852 !catalog
1853 .namespace_exists(&NamespaceIdent::new("b".into()))
1854 .await
1855 .unwrap()
1856 );
1857 }
1858
1859 #[tokio::test]
1860 async fn test_namespace_exists_returns_true() {
1861 let warehouse_loc = temp_path();
1862 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1863 let namespace_ident = NamespaceIdent::new("a".into());
1864 create_namespace(&catalog, &namespace_ident).await;
1865
1866 assert!(catalog.namespace_exists(&namespace_ident).await.unwrap());
1867 }
1868
1869 #[tokio::test]
1870 async fn test_create_namespace_with_properties() {
1871 let warehouse_loc = temp_path();
1872 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1873 let namespace_ident = NamespaceIdent::new("abc".into());
1874
1875 let mut properties = default_properties();
1876 properties.insert("k".into(), "v".into());
1877
1878 assert_eq!(
1879 catalog
1880 .create_namespace(&namespace_ident, properties.clone())
1881 .await
1882 .unwrap(),
1883 Namespace::with_properties(namespace_ident.clone(), properties.clone())
1884 );
1885
1886 assert_eq!(
1887 catalog.get_namespace(&namespace_ident).await.unwrap(),
1888 Namespace::with_properties(namespace_ident, properties)
1889 );
1890 }
1891
1892 #[tokio::test]
1893 async fn test_create_nested_namespace() {
1894 let warehouse_loc = temp_path();
1895 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1896 let parent_namespace_ident = NamespaceIdent::new("a".into());
1897 create_namespace(&catalog, &parent_namespace_ident).await;
1898
1899 let child_namespace_ident = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
1900
1901 assert_eq!(
1902 catalog
1903 .create_namespace(&child_namespace_ident, HashMap::new())
1904 .await
1905 .unwrap(),
1906 Namespace::new(child_namespace_ident.clone())
1907 );
1908
1909 assert_eq!(
1910 catalog.get_namespace(&child_namespace_ident).await.unwrap(),
1911 Namespace::with_properties(child_namespace_ident, default_properties())
1912 );
1913 }
1914
1915 #[tokio::test]
1916 async fn test_create_deeply_nested_namespace() {
1917 let warehouse_loc = temp_path();
1918 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1919 let namespace_ident_a = NamespaceIdent::new("a".into());
1920 let namespace_ident_a_b = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
1921 create_namespaces(&catalog, &vec![&namespace_ident_a, &namespace_ident_a_b]).await;
1922
1923 let namespace_ident_a_b_c = NamespaceIdent::from_strs(vec!["a", "b", "c"]).unwrap();
1924
1925 assert_eq!(
1926 catalog
1927 .create_namespace(&namespace_ident_a_b_c, HashMap::new())
1928 .await
1929 .unwrap(),
1930 Namespace::new(namespace_ident_a_b_c.clone())
1931 );
1932
1933 assert_eq!(
1934 catalog.get_namespace(&namespace_ident_a_b_c).await.unwrap(),
1935 Namespace::with_properties(namespace_ident_a_b_c, default_properties())
1936 );
1937 }
1938
1939 #[tokio::test]
1940 async fn test_update_namespace_noop() {
1941 let warehouse_loc = temp_path();
1942 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1943 let namespace_ident = NamespaceIdent::new("a".into());
1944 create_namespace(&catalog, &namespace_ident).await;
1945
1946 catalog
1947 .update_namespace(&namespace_ident, HashMap::new())
1948 .await
1949 .unwrap();
1950
1951 assert_eq!(
1952 *catalog
1953 .get_namespace(&namespace_ident)
1954 .await
1955 .unwrap()
1956 .properties(),
1957 HashMap::from_iter([("exists".to_string(), "true".to_string())])
1958 )
1959 }
1960
1961 #[tokio::test]
1962 async fn test_update_nested_namespace() {
1963 let warehouse_loc = temp_path();
1964 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1965 let namespace_ident = NamespaceIdent::from_strs(["a", "b"]).unwrap();
1966 create_namespace(&catalog, &namespace_ident).await;
1967
1968 let mut props = HashMap::from_iter([
1969 ("prop1".to_string(), "val1".to_string()),
1970 ("prop2".into(), "val2".into()),
1971 ]);
1972
1973 catalog
1974 .update_namespace(&namespace_ident, props.clone())
1975 .await
1976 .unwrap();
1977
1978 props.insert("exists".into(), "true".into());
1979
1980 assert_eq!(
1981 *catalog
1982 .get_namespace(&namespace_ident)
1983 .await
1984 .unwrap()
1985 .properties(),
1986 props
1987 )
1988 }
1989
1990 #[tokio::test]
1991 async fn test_update_namespace_errors_if_nested_namespace_doesnt_exist() {
1992 let warehouse_loc = temp_path();
1993 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
1994 let namespace_ident = NamespaceIdent::from_strs(["a", "b"]).unwrap();
1995
1996 let props = HashMap::from_iter([
1997 ("prop1".to_string(), "val1".to_string()),
1998 ("prop2".into(), "val2".into()),
1999 ]);
2000
2001 let err = catalog
2002 .update_namespace(&namespace_ident, props)
2003 .await
2004 .unwrap_err();
2005
2006 assert_eq!(
2007 err.message(),
2008 format!("No such namespace: {namespace_ident:?}")
2009 );
2010 }
2011
2012 #[tokio::test]
2013 async fn test_drop_nested_namespace() {
2014 let warehouse_loc = temp_path();
2015 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
2016 let namespace_ident_a = NamespaceIdent::new("a".into());
2017 let namespace_ident_a_b = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
2018 create_namespaces(&catalog, &vec![&namespace_ident_a, &namespace_ident_a_b]).await;
2019
2020 catalog.drop_namespace(&namespace_ident_a_b).await.unwrap();
2021
2022 assert!(
2023 !catalog
2024 .namespace_exists(&namespace_ident_a_b)
2025 .await
2026 .unwrap()
2027 );
2028
2029 assert!(catalog.namespace_exists(&namespace_ident_a).await.unwrap());
2030 }
2031
2032 #[tokio::test]
2033 async fn test_drop_deeply_nested_namespace() {
2034 let warehouse_loc = temp_path();
2035 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
2036 let namespace_ident_a = NamespaceIdent::new("a".into());
2037 let namespace_ident_a_b = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
2038 let namespace_ident_a_b_c = NamespaceIdent::from_strs(vec!["a", "b", "c"]).unwrap();
2039 create_namespaces(&catalog, &vec![
2040 &namespace_ident_a,
2041 &namespace_ident_a_b,
2042 &namespace_ident_a_b_c,
2043 ])
2044 .await;
2045
2046 catalog
2047 .drop_namespace(&namespace_ident_a_b_c)
2048 .await
2049 .unwrap();
2050
2051 assert!(
2052 !catalog
2053 .namespace_exists(&namespace_ident_a_b_c)
2054 .await
2055 .unwrap()
2056 );
2057
2058 assert!(
2059 catalog
2060 .namespace_exists(&namespace_ident_a_b)
2061 .await
2062 .unwrap()
2063 );
2064
2065 assert!(catalog.namespace_exists(&namespace_ident_a).await.unwrap());
2066 }
2067
2068 #[tokio::test]
2069 async fn test_drop_namespace_throws_error_if_nested_namespace_doesnt_exist() {
2070 let warehouse_loc = temp_path();
2071 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
2072 create_namespace(&catalog, &NamespaceIdent::new("a".into())).await;
2073
2074 let non_existent_namespace_ident =
2075 NamespaceIdent::from_vec(vec!["a".into(), "b".into()]).unwrap();
2076 assert_eq!(
2077 catalog
2078 .drop_namespace(&non_existent_namespace_ident)
2079 .await
2080 .unwrap_err()
2081 .to_string(),
2082 format!("NamespaceNotFound => No such namespace: {non_existent_namespace_ident:?}")
2083 )
2084 }
2085
2086 #[tokio::test]
2087 async fn test_dropping_a_namespace_does_not_drop_namespaces_nested_under_that_one() {
2088 let warehouse_loc = temp_path();
2089 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
2090 let namespace_ident_a = NamespaceIdent::new("a".into());
2091 let namespace_ident_a_b = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
2092 create_namespaces(&catalog, &vec![&namespace_ident_a, &namespace_ident_a_b]).await;
2093
2094 catalog.drop_namespace(&namespace_ident_a).await.unwrap();
2095
2096 assert!(!catalog.namespace_exists(&namespace_ident_a).await.unwrap());
2097
2098 assert!(
2099 catalog
2100 .namespace_exists(&namespace_ident_a_b)
2101 .await
2102 .unwrap()
2103 );
2104 }
2105
2106 #[tokio::test]
2107 async fn test_create_table_falls_back_to_namespace_location_if_table_location_is_missing() {
2108 let warehouse_loc = temp_path();
2109 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
2110
2111 let namespace_ident = NamespaceIdent::new("a".into());
2112 let mut namespace_properties = HashMap::new();
2113 let namespace_location = temp_path();
2114 namespace_properties.insert(
2115 NAMESPACE_LOCATION_PROPERTY_KEY.to_string(),
2116 namespace_location.to_string(),
2117 );
2118 catalog
2119 .create_namespace(&namespace_ident, namespace_properties)
2120 .await
2121 .unwrap();
2122
2123 let table_name = "tbl1";
2124 let expected_table_ident = TableIdent::new(namespace_ident.clone(), table_name.into());
2125 let expected_table_metadata_location_regex =
2126 format!("^{namespace_location}/tbl1/metadata/00000-{UUID_REGEX_STR}.metadata.json$",);
2127
2128 let table = catalog
2129 .create_table(
2130 &namespace_ident,
2131 TableCreation::builder()
2132 .name(table_name.into())
2133 .schema(simple_table_schema())
2134 .build(),
2136 )
2137 .await
2138 .unwrap();
2139 assert_table_eq(&table, &expected_table_ident, &simple_table_schema());
2140 assert_table_metadata_location_matches(&table, &expected_table_metadata_location_regex);
2141
2142 let table = catalog.load_table(&expected_table_ident).await.unwrap();
2143 assert_table_eq(&table, &expected_table_ident, &simple_table_schema());
2144 assert_table_metadata_location_matches(&table, &expected_table_metadata_location_regex);
2145 }
2146
2147 #[tokio::test]
2148 async fn test_create_table_in_nested_namespace_falls_back_to_nested_namespace_location_if_table_location_is_missing()
2149 {
2150 let warehouse_loc = temp_path();
2151 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
2152
2153 let namespace_ident = NamespaceIdent::new("a".into());
2154 let mut namespace_properties = HashMap::new();
2155 let namespace_location = temp_path();
2156 namespace_properties.insert(
2157 NAMESPACE_LOCATION_PROPERTY_KEY.to_string(),
2158 namespace_location.to_string(),
2159 );
2160 catalog
2161 .create_namespace(&namespace_ident, namespace_properties)
2162 .await
2163 .unwrap();
2164
2165 let nested_namespace_ident = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
2166 let mut nested_namespace_properties = HashMap::new();
2167 let nested_namespace_location = temp_path();
2168 nested_namespace_properties.insert(
2169 NAMESPACE_LOCATION_PROPERTY_KEY.to_string(),
2170 nested_namespace_location.to_string(),
2171 );
2172 catalog
2173 .create_namespace(&nested_namespace_ident, nested_namespace_properties)
2174 .await
2175 .unwrap();
2176
2177 let table_name = "tbl1";
2178 let expected_table_ident =
2179 TableIdent::new(nested_namespace_ident.clone(), table_name.into());
2180 let expected_table_metadata_location_regex = format!(
2181 "^{nested_namespace_location}/tbl1/metadata/00000-{UUID_REGEX_STR}.metadata.json$",
2182 );
2183
2184 let table = catalog
2185 .create_table(
2186 &nested_namespace_ident,
2187 TableCreation::builder()
2188 .name(table_name.into())
2189 .schema(simple_table_schema())
2190 .build(),
2192 )
2193 .await
2194 .unwrap();
2195 assert_table_eq(&table, &expected_table_ident, &simple_table_schema());
2196 assert_table_metadata_location_matches(&table, &expected_table_metadata_location_regex);
2197
2198 let table = catalog.load_table(&expected_table_ident).await.unwrap();
2199 assert_table_eq(&table, &expected_table_ident, &simple_table_schema());
2200 assert_table_metadata_location_matches(&table, &expected_table_metadata_location_regex);
2201 }
2202
2203 #[tokio::test]
2204 async fn test_create_table_falls_back_to_warehouse_location_if_both_table_location_and_namespace_location_are_missing()
2205 {
2206 let warehouse_loc = temp_path();
2207 let catalog = new_sql_catalog(warehouse_loc.clone(), Some("iceberg")).await;
2208
2209 let namespace_ident = NamespaceIdent::new("a".into());
2210 let namespace_properties = HashMap::new();
2212 catalog
2213 .create_namespace(&namespace_ident, namespace_properties)
2214 .await
2215 .unwrap();
2216
2217 let table_name = "tbl1";
2218 let expected_table_ident = TableIdent::new(namespace_ident.clone(), table_name.into());
2219 let expected_table_metadata_location_regex =
2220 format!("^{warehouse_loc}/a/tbl1/metadata/00000-{UUID_REGEX_STR}.metadata.json$");
2221
2222 let table = catalog
2223 .create_table(
2224 &namespace_ident,
2225 TableCreation::builder()
2226 .name(table_name.into())
2227 .schema(simple_table_schema())
2228 .build(),
2230 )
2231 .await
2232 .unwrap();
2233 assert_table_eq(&table, &expected_table_ident, &simple_table_schema());
2234 assert_table_metadata_location_matches(&table, &expected_table_metadata_location_regex);
2235
2236 let table = catalog.load_table(&expected_table_ident).await.unwrap();
2237 assert_table_eq(&table, &expected_table_ident, &simple_table_schema());
2238 assert_table_metadata_location_matches(&table, &expected_table_metadata_location_regex);
2239 }
2240
2241 #[tokio::test]
2242 async fn test_create_table_in_nested_namespace_falls_back_to_warehouse_location_if_both_table_location_and_namespace_location_are_missing()
2243 {
2244 let warehouse_loc = temp_path();
2245 let catalog = new_sql_catalog(warehouse_loc.clone(), Some("iceberg")).await;
2246
2247 let namespace_ident = NamespaceIdent::new("a".into());
2248 create_namespace(&catalog, &namespace_ident).await;
2249
2250 let nested_namespace_ident = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
2251 create_namespace(&catalog, &nested_namespace_ident).await;
2252
2253 let table_name = "tbl1";
2254 let expected_table_ident =
2255 TableIdent::new(nested_namespace_ident.clone(), table_name.into());
2256 let expected_table_metadata_location_regex =
2257 format!("^{warehouse_loc}/a/b/tbl1/metadata/00000-{UUID_REGEX_STR}.metadata.json$");
2258
2259 let table = catalog
2260 .create_table(
2261 &nested_namespace_ident,
2262 TableCreation::builder()
2263 .name(table_name.into())
2264 .schema(simple_table_schema())
2265 .build(),
2267 )
2268 .await
2269 .unwrap();
2270 assert_table_eq(&table, &expected_table_ident, &simple_table_schema());
2271 assert_table_metadata_location_matches(&table, &expected_table_metadata_location_regex);
2272
2273 let table = catalog.load_table(&expected_table_ident).await.unwrap();
2274 assert_table_eq(&table, &expected_table_ident, &simple_table_schema());
2275 assert_table_metadata_location_matches(&table, &expected_table_metadata_location_regex);
2276 }
2277
2278 #[tokio::test]
2279 async fn test_create_table_throws_error_if_table_with_same_name_already_exists() {
2280 let warehouse_loc = temp_path();
2281 let catalog = new_sql_catalog(warehouse_loc.clone(), Some("iceberg")).await;
2282 let namespace_ident = NamespaceIdent::new("a".into());
2283 create_namespace(&catalog, &namespace_ident).await;
2284 let table_name = "tbl1";
2285 let table_ident = TableIdent::new(namespace_ident.clone(), table_name.into());
2286 create_table(&catalog, &table_ident).await;
2287
2288 let tmp_dir = TempDir::new().unwrap();
2289 let location = tmp_dir.path().to_str().unwrap().to_string();
2290
2291 assert_eq!(
2292 catalog
2293 .create_table(
2294 &namespace_ident,
2295 TableCreation::builder()
2296 .name(table_name.into())
2297 .schema(simple_table_schema())
2298 .location(location)
2299 .build()
2300 )
2301 .await
2302 .unwrap_err()
2303 .to_string(),
2304 format!(
2305 "TableAlreadyExists => Table {:?} already exists.",
2306 &table_ident
2307 )
2308 );
2309 }
2310
2311 #[tokio::test]
2312 async fn test_rename_table_src_table_is_same_as_dst_table() {
2313 let warehouse_loc = temp_path();
2314 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
2315 let namespace_ident = NamespaceIdent::new("n1".into());
2316 create_namespace(&catalog, &namespace_ident).await;
2317 let table_ident = TableIdent::new(namespace_ident.clone(), "tbl".into());
2318 create_table(&catalog, &table_ident).await;
2319
2320 catalog
2321 .rename_table(&table_ident, &table_ident)
2322 .await
2323 .unwrap();
2324
2325 assert_eq!(catalog.list_tables(&namespace_ident).await.unwrap(), vec![
2326 table_ident
2327 ],);
2328 }
2329
2330 #[tokio::test]
2331 async fn test_rename_table_across_nested_namespaces() {
2332 let warehouse_loc = temp_path();
2333 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
2334 let namespace_ident_a = NamespaceIdent::new("a".into());
2335 let namespace_ident_a_b = NamespaceIdent::from_strs(vec!["a", "b"]).unwrap();
2336 let namespace_ident_a_b_c = NamespaceIdent::from_strs(vec!["a", "b", "c"]).unwrap();
2337 create_namespaces(&catalog, &vec![
2338 &namespace_ident_a,
2339 &namespace_ident_a_b,
2340 &namespace_ident_a_b_c,
2341 ])
2342 .await;
2343
2344 let src_table_ident = TableIdent::new(namespace_ident_a_b_c.clone(), "tbl1".into());
2345 create_tables(&catalog, vec![&src_table_ident]).await;
2346
2347 let dst_table_ident = TableIdent::new(namespace_ident_a_b.clone(), "tbl1".into());
2348 catalog
2349 .rename_table(&src_table_ident, &dst_table_ident)
2350 .await
2351 .unwrap();
2352
2353 assert!(!catalog.table_exists(&src_table_ident).await.unwrap());
2354
2355 assert!(catalog.table_exists(&dst_table_ident).await.unwrap());
2356 }
2357
2358 #[tokio::test]
2359 async fn test_rename_table_throws_error_if_dst_namespace_doesnt_exist() {
2360 let warehouse_loc = temp_path();
2361 let catalog = new_sql_catalog(warehouse_loc, Some("iceberg")).await;
2362 let src_namespace_ident = NamespaceIdent::new("n1".into());
2363 let src_table_ident = TableIdent::new(src_namespace_ident.clone(), "tbl1".into());
2364 create_namespace(&catalog, &src_namespace_ident).await;
2365 create_table(&catalog, &src_table_ident).await;
2366
2367 let non_existent_dst_namespace_ident = NamespaceIdent::new("n2".into());
2368 let dst_table_ident =
2369 TableIdent::new(non_existent_dst_namespace_ident.clone(), "tbl1".into());
2370 assert_eq!(
2371 catalog
2372 .rename_table(&src_table_ident, &dst_table_ident)
2373 .await
2374 .unwrap_err()
2375 .to_string(),
2376 format!("NamespaceNotFound => No such namespace: {non_existent_dst_namespace_ident:?}"),
2377 );
2378 }
2379
2380 async fn create_v0_sqlite_db() -> (String, TempDir) {
2383 let temp_dir = TempDir::new().unwrap();
2384 let uri = format!(
2385 "sqlite:{}",
2386 temp_dir.path().join("catalog.db").to_str().unwrap()
2387 );
2388 sqlx::Sqlite::create_database(&uri).await.unwrap();
2389 let pool = sqlx::AnyPool::connect(&uri).await.unwrap();
2390 sqlx::query(
2391 "CREATE TABLE iceberg_tables (
2392 catalog_name VARCHAR(255) NOT NULL,
2393 table_namespace VARCHAR(255) NOT NULL,
2394 table_name VARCHAR(255) NOT NULL,
2395 metadata_location VARCHAR(1000),
2396 previous_metadata_location VARCHAR(1000),
2397 PRIMARY KEY (catalog_name, table_namespace, table_name)
2398 )",
2399 )
2400 .execute(&pool)
2401 .await
2402 .unwrap();
2403 sqlx::query(
2404 "INSERT INTO iceberg_tables
2405 (catalog_name, table_namespace, table_name, metadata_location)
2406 VALUES ('iceberg', 'test_namespace', 'existing_test_table', '/tmp/fake-location')",
2407 )
2408 .execute(&pool)
2409 .await
2410 .unwrap();
2411 pool.close().await;
2412 (uri, temp_dir)
2413 }
2414
2415 async fn create_v1_sqlite_db() -> (String, TempDir) {
2418 let temp_dir = TempDir::new().unwrap();
2419 let uri = format!(
2420 "sqlite:{}",
2421 temp_dir.path().join("catalog.db").to_str().unwrap()
2422 );
2423 sqlx::Sqlite::create_database(&uri).await.unwrap();
2424 let pool = sqlx::AnyPool::connect(&uri).await.unwrap();
2425 sqlx::query(
2426 "CREATE TABLE iceberg_tables (
2427 catalog_name VARCHAR(255) NOT NULL,
2428 table_namespace VARCHAR(255) NOT NULL,
2429 table_name VARCHAR(255) NOT NULL,
2430 metadata_location VARCHAR(1000),
2431 previous_metadata_location VARCHAR(1000),
2432 iceberg_type VARCHAR(5),
2433 PRIMARY KEY (catalog_name, table_namespace, table_name)
2434 )",
2435 )
2436 .execute(&pool)
2437 .await
2438 .unwrap();
2439 sqlx::query(
2440 "INSERT INTO iceberg_tables
2441 (catalog_name, table_namespace, table_name, metadata_location, iceberg_type)
2442 VALUES ('iceberg', 'test_namespace', 'existing_test_table', '/tmp/fake-location', 'TABLE')",
2443 )
2444 .execute(&pool)
2445 .await
2446 .unwrap();
2447 pool.close().await;
2448 (uri, temp_dir)
2449 }
2450
2451 fn catalog_props(
2454 uri: &str,
2455 warehouse: &TempDir,
2456 schema_version: Option<SchemaVersion>,
2457 ) -> HashMap<String, String> {
2458 let mut props = HashMap::from_iter([
2459 (SQL_CATALOG_PROP_URI.to_string(), uri.to_string()),
2460 (
2461 SQL_CATALOG_PROP_WAREHOUSE.to_string(),
2462 warehouse.path().to_str().unwrap().to_string(),
2463 ),
2464 (
2465 SQL_CATALOG_PROP_BIND_STYLE.to_string(),
2466 SqlBindStyle::QMark.to_string(),
2467 ),
2468 ]);
2469 if let Some(schema_version) = schema_version {
2470 props.insert(
2471 SQL_CATALOG_PROP_SCHEMA_VERSION.to_string(),
2472 schema_version.to_string(),
2473 );
2474 }
2475 props
2476 }
2477
2478 async fn record_type_column_exists(uri: &str) -> bool {
2482 let probe_pool = sqlx::AnyPool::connect(uri).await.unwrap();
2483 let column_exists = probe_pool
2484 .describe(&format!("SELECT * FROM {CATALOG_TABLE_NAME}"))
2485 .await
2486 .expect("connection and query should succeed")
2487 .columns()
2488 .iter()
2489 .any(|column| {
2490 column
2491 .name()
2492 .eq_ignore_ascii_case(CATALOG_FIELD_RECORD_TYPE)
2493 });
2494 probe_pool.close().await;
2495 column_exists
2496 }
2497
2498 #[tokio::test]
2499 async fn test_detect_schema_version() {
2500 install_default_drivers();
2501
2502 let detected_schema = {
2503 let (uri, _temp_dir) = create_v0_sqlite_db().await;
2504 let pool = sqlx::AnyPool::connect(&uri).await.unwrap();
2505 let detected_schema = SchemaVersion::detect(&pool).await.unwrap();
2506 pool.close().await;
2507 detected_schema
2508 };
2509 assert_eq!(
2510 detected_schema,
2511 SchemaVersion::V0,
2512 "a catalog table without an iceberg_type column should be V0",
2513 );
2514
2515 let detected_schema = {
2516 let (uri, _temp_dir) = create_v1_sqlite_db().await;
2517 let pool = sqlx::AnyPool::connect(&uri).await.unwrap();
2518 let detected_schema = SchemaVersion::detect(&pool).await.unwrap();
2519 pool.close().await;
2520 detected_schema
2521 };
2522 assert_eq!(
2523 detected_schema,
2524 SchemaVersion::V1,
2525 "a catalog table with an iceberg_type column should be V1",
2526 );
2527 }
2528
2529 #[tokio::test]
2530 async fn test_detect_schema_version_surfaces_errors() {
2531 install_default_drivers();
2532
2533 let temp_dir = TempDir::new().unwrap();
2534 let uri = format!(
2535 "sqlite:{}",
2536 temp_dir.path().join("catalog.db").to_str().unwrap()
2537 );
2538 sqlx::Sqlite::create_database(&uri).await.unwrap();
2539 let pool = sqlx::AnyPool::connect(&uri).await.unwrap();
2540
2541 let err = SchemaVersion::detect(&pool)
2543 .await
2544 .expect_err("detection should fail rather than report V0");
2545 pool.close().await;
2546
2547 assert!(
2548 err.to_string().contains("iceberg_tables"),
2549 "error should name the table it failed to introspect, got: {err}"
2550 );
2551 }
2552
2553 #[tokio::test]
2554 async fn test_v0_schema_migration() {
2555 install_default_drivers();
2556
2557 let (uri, temp_dir) = create_v0_sqlite_db().await;
2558
2559 let props = HashMap::from_iter([
2561 (SQL_CATALOG_PROP_URI.to_string(), uri.clone()),
2562 (
2563 SQL_CATALOG_PROP_WAREHOUSE.to_string(),
2564 temp_dir.path().to_str().unwrap().to_string(),
2565 ),
2566 (
2567 SQL_CATALOG_PROP_BIND_STYLE.to_string(),
2568 SqlBindStyle::QMark.to_string(),
2569 ),
2570 (
2571 SQL_CATALOG_PROP_SCHEMA_VERSION.to_string(),
2572 SchemaVersion::V1.to_string(),
2573 ),
2574 ]);
2575 let catalog = SqlCatalogBuilder::default()
2576 .with_storage_factory(Arc::new(LocalFsStorageFactory))
2577 .load("iceberg", props)
2578 .await
2579 .expect("should open V0 catalog and migrate schema when sql.schema-version=V1");
2580
2581 let namespace = NamespaceIdent::from_strs(["test_namespace"]).unwrap();
2583 let tables = catalog.list_tables(&namespace).await.unwrap();
2584 assert_eq!(tables.len(), 1);
2585 assert_eq!(tables[0].name(), "existing_test_table");
2586
2587 assert!(
2588 record_type_column_exists(&uri).await,
2589 "iceberg_type column should exist when sql.schema-version=V1 was set",
2590 );
2591 }
2592
2593 #[tokio::test]
2594 async fn test_v0_schema_no_migration_without_property() {
2595 install_default_drivers();
2596
2597 let (uri, temp_dir) = create_v0_sqlite_db().await;
2598
2599 let props = HashMap::from_iter([
2601 (SQL_CATALOG_PROP_URI.to_string(), uri.clone()),
2602 (
2603 SQL_CATALOG_PROP_WAREHOUSE.to_string(),
2604 temp_dir.path().to_str().unwrap().to_string(),
2605 ),
2606 (
2607 SQL_CATALOG_PROP_BIND_STYLE.to_string(),
2608 SqlBindStyle::QMark.to_string(),
2609 ),
2610 ]);
2611 let catalog = SqlCatalogBuilder::default()
2612 .with_storage_factory(Arc::new(LocalFsStorageFactory))
2613 .load("iceberg", props)
2614 .await
2615 .expect("should open V0 catalog without migrating");
2616
2617 assert_eq!(catalog.schema_version, SchemaVersion::V0);
2618
2619 let namespace = NamespaceIdent::from_strs(["test_namespace"]).unwrap();
2621 let tables = catalog.list_tables(&namespace).await.unwrap();
2622 assert_eq!(tables.len(), 1);
2623 assert_eq!(tables[0].name(), "existing_test_table");
2624
2625 assert!(
2626 !record_type_column_exists(&uri).await,
2627 "iceberg_type column should not exist when sql.schema-version=V1 was not set"
2628 );
2629 }
2630
2631 #[tokio::test]
2632 async fn test_explicit_v0_schema_version_is_honored() {
2633 install_default_drivers();
2634
2635 let (uri, temp_dir) = create_v0_sqlite_db().await;
2636
2637 let catalog = SqlCatalogBuilder::default()
2638 .with_storage_factory(Arc::new(LocalFsStorageFactory))
2639 .load(
2640 "iceberg",
2641 catalog_props(&uri, &temp_dir, Some(SchemaVersion::V0)),
2642 )
2643 .await
2644 .expect("requesting V0 against a V0 catalog table should succeed");
2645
2646 assert_eq!(catalog.schema_version, SchemaVersion::V0);
2647
2648 let namespace = NamespaceIdent::from_strs(["test_namespace"]).unwrap();
2649 let tables = catalog.list_tables(&namespace).await.unwrap();
2650 assert_eq!(tables.len(), 1);
2651 assert_eq!(tables[0].name(), "existing_test_table");
2652
2653 assert!(
2654 !record_type_column_exists(&uri).await,
2655 "iceberg_type column should not be added when V0 was requested"
2656 );
2657 }
2658
2659 #[tokio::test]
2660 async fn test_v0_schema_version_is_ignored_on_v1_table() {
2661 install_default_drivers();
2662
2663 let (uri, temp_dir) = create_v1_sqlite_db().await;
2664
2665 let catalog = SqlCatalogBuilder::default()
2666 .with_storage_factory(Arc::new(LocalFsStorageFactory))
2667 .load(
2668 "iceberg",
2669 catalog_props(&uri, &temp_dir, Some(SchemaVersion::V0)),
2670 )
2671 .await
2672 .expect("requesting V0 against a V1 catalog table should succeed");
2673
2674 assert_eq!(catalog.schema_version, SchemaVersion::V1);
2675
2676 let namespace = NamespaceIdent::from_strs(["test_namespace"]).unwrap();
2677 let tables = catalog.list_tables(&namespace).await.unwrap();
2678 assert_eq!(tables.len(), 1);
2679 assert_eq!(tables[0].name(), "existing_test_table");
2680 }
2681
2682 #[tokio::test]
2683 async fn test_v0_schema_version_on_empty_database_yields_v1() {
2684 install_default_drivers();
2685
2686 let temp_dir = TempDir::new().unwrap();
2687 let uri = format!(
2688 "sqlite:{}",
2689 temp_dir.path().join("catalog.db").to_str().unwrap()
2690 );
2691 sqlx::Sqlite::create_database(&uri).await.unwrap();
2692
2693 let catalog = SqlCatalogBuilder::default()
2694 .with_storage_factory(Arc::new(LocalFsStorageFactory))
2695 .load(
2696 "iceberg",
2697 catalog_props(&uri, &temp_dir, Some(SchemaVersion::V0)),
2698 )
2699 .await
2700 .expect("requesting V0 against an empty database should succeed");
2701
2702 assert_eq!(catalog.schema_version, SchemaVersion::V1);
2703 }
2704
2705 #[tokio::test]
2706 async fn test_create_table_unsupported_on_v0_schema() {
2707 install_default_drivers();
2708
2709 let (uri, temp_dir) = create_v0_sqlite_db().await;
2710
2711 let catalog = SqlCatalogBuilder::default()
2712 .with_storage_factory(Arc::new(LocalFsStorageFactory))
2713 .load("iceberg", catalog_props(&uri, &temp_dir, None))
2714 .await
2715 .expect("should open V0 catalog without migrating");
2716 assert_eq!(catalog.schema_version, SchemaVersion::V0);
2717
2718 let namespace = NamespaceIdent::from_strs(["test_namespace"]).unwrap();
2721 let err = catalog
2722 .create_table(
2723 &namespace,
2724 TableCreation::builder()
2725 .name("created_test_table".to_string())
2726 .schema(simple_table_schema())
2727 .location(temp_path())
2728 .build(),
2729 )
2730 .await
2731 .expect_err("table creation should be rejected on a V0 catalog table");
2732
2733 assert_eq!(err.kind(), ErrorKind::FeatureUnsupported);
2734 assert!(
2735 err.to_string().contains("Table creation is not supported"),
2736 "error should explain that table creation is unsupported, got: {err}"
2737 );
2738
2739 let tables = catalog.list_tables(&namespace).await.unwrap();
2740 assert_eq!(
2741 tables.len(),
2742 1,
2743 "only the pre-existing row should be present, got: {tables:?}"
2744 );
2745 }
2746
2747 #[tokio::test]
2748 async fn test_register_table_unsupported_on_v0_schema() {
2749 install_default_drivers();
2750
2751 let (uri, temp_dir) = create_v0_sqlite_db().await;
2752
2753 let catalog = SqlCatalogBuilder::default()
2754 .with_storage_factory(Arc::new(LocalFsStorageFactory))
2755 .load("iceberg", catalog_props(&uri, &temp_dir, None))
2756 .await
2757 .expect("should open V0 catalog without migrating");
2758 assert_eq!(catalog.schema_version, SchemaVersion::V0);
2759
2760 let namespace = NamespaceIdent::from_strs(["test_namespace"]).unwrap();
2761 let table_ident = TableIdent::new(namespace.clone(), "registered_test_table".to_string());
2762 let err = catalog
2764 .register_table(
2765 &table_ident,
2766 "/tmp/does-not-exist/metadata.json".to_string(),
2767 )
2768 .await
2769 .expect_err("table registration should be rejected on a V0 catalog table");
2770
2771 assert_eq!(err.kind(), ErrorKind::FeatureUnsupported);
2772 assert!(
2773 err.to_string()
2774 .contains("Table registration is not supported"),
2775 "error should explain that table registration is unsupported, got: {err}"
2776 );
2777
2778 let tables = catalog.list_tables(&namespace).await.unwrap();
2779 assert_eq!(
2780 tables.len(),
2781 1,
2782 "only the pre-existing row should be present, got: {tables:?}"
2783 );
2784 }
2785
2786 #[tokio::test]
2787 async fn test_create_table_supported_after_v0_migration() {
2788 install_default_drivers();
2789
2790 let (uri, temp_dir) = create_v0_sqlite_db().await;
2791
2792 let catalog = SqlCatalogBuilder::default()
2793 .with_storage_factory(Arc::new(LocalFsStorageFactory))
2794 .load(
2795 "iceberg",
2796 catalog_props(&uri, &temp_dir, Some(SchemaVersion::V1)),
2797 )
2798 .await
2799 .expect("should open V0 catalog and migrate schema when sql.schema-version=V1");
2800 assert_eq!(catalog.schema_version, SchemaVersion::V1);
2801
2802 let namespace = NamespaceIdent::from_strs(["test_namespace"]).unwrap();
2803 catalog
2804 .create_table(
2805 &namespace,
2806 TableCreation::builder()
2807 .name("created_test_table".to_string())
2808 .schema(simple_table_schema())
2809 .location(temp_path())
2810 .build(),
2811 )
2812 .await
2813 .expect("table creation should succeed once the schema is migrated to V1");
2814
2815 let table_names = catalog
2816 .list_tables(&namespace)
2817 .await
2818 .unwrap()
2819 .iter()
2820 .map(|table_ident| table_ident.name().to_string())
2821 .sorted()
2822 .collect_vec();
2823 assert_eq!(
2824 table_names,
2825 vec!["created_test_table", "existing_test_table"],
2826 "the migrated pre-existing row and the new table should both be listed"
2827 );
2828 }
2829
2830 #[tokio::test]
2831 async fn test_invalid_schema_version_is_rejected() {
2832 install_default_drivers();
2833
2834 let (uri, temp_dir) = create_v0_sqlite_db().await;
2835
2836 let props = HashMap::from_iter([
2839 (SQL_CATALOG_PROP_URI.to_string(), uri),
2840 (
2841 SQL_CATALOG_PROP_WAREHOUSE.to_string(),
2842 temp_dir.path().to_str().unwrap().to_string(),
2843 ),
2844 (
2845 SQL_CATALOG_PROP_BIND_STYLE.to_string(),
2846 SqlBindStyle::QMark.to_string(),
2847 ),
2848 (
2849 SQL_CATALOG_PROP_SCHEMA_VERSION.to_string(),
2850 "v2".to_string(),
2851 ),
2852 ]);
2853 let result = SqlCatalogBuilder::default()
2854 .with_storage_factory(Arc::new(LocalFsStorageFactory))
2855 .load("iceberg", props)
2856 .await;
2857
2858 let err = result.expect_err("an invalid sql.schema-version should be rejected");
2859 assert_eq!(err.kind(), ErrorKind::DataInvalid);
2860 }
2861
2862 #[tokio::test]
2863 async fn test_legacy_bind_style_key_is_accepted() {
2864 install_default_drivers();
2865
2866 let (uri, temp_dir) = create_v0_sqlite_db().await;
2867
2868 let props = HashMap::from_iter([
2870 (SQL_CATALOG_PROP_URI.to_string(), uri),
2871 (
2872 SQL_CATALOG_PROP_WAREHOUSE.to_string(),
2873 temp_dir.path().to_str().unwrap().to_string(),
2874 ),
2875 (
2876 SQL_CATALOG_PROP_BIND_STYLE_LEGACY.to_string(),
2877 SqlBindStyle::QMark.to_string(),
2878 ),
2879 ]);
2880 let catalog = SqlCatalogBuilder::default()
2881 .with_storage_factory(Arc::new(LocalFsStorageFactory))
2882 .load("iceberg", props)
2883 .await
2884 .expect("legacy sql_bind_style key should still be accepted");
2885
2886 assert_eq!(catalog.sql_bind_style, SqlBindStyle::QMark);
2887 }
2888}