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