3030#include < Databases/DatabaseFactory.h>
3131#include < Databases/DataLake/UnityCatalog.h>
3232#include < Databases/DataLake/RestCatalog.h>
33+ #include < Databases/DataLake/UnityV2Catalog.h>
3334#include < Databases/DataLake/GlueCatalog.h>
3435#include < Databases/DataLake/PaimonRestCatalog.h>
3536#if USE_AWS_S3 && USE_SSL
@@ -71,6 +72,7 @@ namespace DatabaseDataLakeSetting
7172 extern const DatabaseDataLakeSettingsDatabaseDataLakeCatalogType catalog_type;
7273 extern const DatabaseDataLakeSettingsString warehouse;
7374 extern const DatabaseDataLakeSettingsString catalog_credential;
75+ extern const DatabaseDataLakeSettingsBool use_unity_catalog_v2;
7476 extern const DatabaseDataLakeSettingsString auth_header;
7577 extern const DatabaseDataLakeSettingsString auth_scope;
7678 extern const DatabaseDataLakeSettingsString storage_endpoint;
@@ -506,11 +508,28 @@ void DatabaseDataLake::initialize() const
506508 }
507509 case DB ::DatabaseDataLakeCatalogType::UNITY :
508510 {
509- catalog_impl = std::make_shared<DataLake::UnityCatalog>(
511+ if (!settings[DatabaseDataLakeSetting::use_unity_catalog_v2])
512+ {
513+ catalog_impl = std::make_shared<DataLake::UnityCatalog>(
514+ settings[DatabaseDataLakeSetting::warehouse].value ,
515+ url,
516+ settings[DatabaseDataLakeSetting::catalog_credential].value ,
517+ settings[DatabaseDataLakeSetting::namespaces].value ,
518+ Context::getGlobalContextInstance ());
519+ break ;
520+ }
521+
522+ // / Databricks OIDC expects `all-apis`; the default `auth_scope` value targets Iceberg REST catalogs.
523+ const std::string unity_auth_scope = settings[DatabaseDataLakeSetting::auth_scope].changed
524+ ? settings[DatabaseDataLakeSetting::auth_scope].value
525+ : " all-apis" ;
526+ catalog_impl = std::make_shared<DataLake::UnityV2Catalog>(
510527 settings[DatabaseDataLakeSetting::warehouse].value ,
511528 url,
512529 settings[DatabaseDataLakeSetting::catalog_credential].value ,
513- settings[DatabaseDataLakeSetting::namespaces].value ,
530+ unity_auth_scope,
531+ settings[DatabaseDataLakeSetting::auth_header].value ,
532+ settings[DatabaseDataLakeSetting::oauth_server_uri].value ,
514533 Context::getGlobalContextInstance ());
515534 break ;
516535 }
@@ -658,15 +677,23 @@ void DatabaseDataLake::resetCatalog(String reason) const
658677
659678StorageObjectStorageConfigurationPtr DatabaseDataLake::getConfiguration (
660679 DatabaseDataLakeStorageType type,
661- DataLakeStorageSettingsPtr storage_settings) const
680+ DataLakeStorageSettingsPtr storage_settings,
681+ DataLake::DataLakeTableFormat table_format) const
662682{
663683 // / TODO: add tests for azure, local storage types.
664684
665685 const auto settings_version = database_settings.get ();
666686 const DatabaseDataLakeSettings & settings = *settings_version;
667687
668688 auto catalog = getCatalog ();
669- switch (catalog->getCatalogType ())
689+ auto catalog_type = catalog->getCatalogType ();
690+
691+ // / `UnityV2Catalog` serves both formats. Its Iceberg tables need the same configuration as an
692+ // / Iceberg REST catalog, so route them to that arm; the legacy catalog never reports Iceberg.
693+ if (catalog_type == DatabaseDataLakeCatalogType::UNITY && table_format == DataLake::DataLakeTableFormat::ICEBERG )
694+ catalog_type = DatabaseDataLakeCatalogType::ICEBERG_REST ;
695+
696+ switch (catalog_type)
670697 {
671698 case DatabaseDataLakeCatalogType::ICEBERG_ONELAKE :
672699 {
@@ -1011,7 +1038,7 @@ StoragePtr DatabaseDataLake::tryGetTableImpl(const String & name, ContextPtr con
10111038 }
10121039 }
10131040
1014- const auto configuration = getConfiguration (storage_type, storage_settings);
1041+ const auto configuration = getConfiguration (storage_type, storage_settings, table_metadata. getTableFormat () );
10151042
10161043 // / HACK: Hacky-hack to enable lazy load
10171044 ContextMutablePtr context_copy = Context::createCopy (context_);
@@ -1093,7 +1120,7 @@ StoragePtr DatabaseDataLake::tryGetTableImpl(const String & name, ContextPtr con
10931120 return std::nullopt ;
10941121 if (!with_vended_credentials && !catalog_manages_provider_chain)
10951122 return std::nullopt ;
1096- return catalog->getCredentialsConfigurationCallback (storage_id, DataLake::getForwardedAuthToken (context_));
1123+ return catalog->getCredentialsConfigurationCallback (storage_id, table_metadata, DataLake::getForwardedAuthToken (context_));
10971124 };
10981125
10991126 const auto catalog_uuid = table_metadata.getTableUUID ();
@@ -1421,7 +1448,7 @@ DatabaseTablesIteratorPtr DatabaseDataLake::getTablesIteratorWithHint(
14211448DatabaseTablesIteratorPtr DatabaseDataLake::getTablesIteratorImpl (
14221449 ContextPtr context_,
14231450 const FilterByNameFunction & filter_by_table_name,
1424- bool skip_not_loaded,
1451+ bool /* skip_not_loaded*/ ,
14251452 const TablesFilter & tables_filter,
14261453 bool keep_unresolved_tables) const
14271454{
@@ -1470,13 +1497,15 @@ DatabaseTablesIteratorPtr DatabaseDataLake::getTablesIteratorImpl(
14701497 futures.emplace_back (promises.back ()->get_future ());
14711498
14721499 pool.scheduleOrThrow (
1473- [this , table_name, skip_not_loaded, context_, keep_unresolved_tables, promise=promises.back ()]() mutable
1500+ [this , table_name, context_, keep_unresolved_tables, promise=promises.back ()]() mutable
14741501 {
14751502 StoragePtr storage = nullptr ;
14761503 try
14771504 {
14781505 LOG_INFO (log, " Get table information for table {}" , table_name);
1479- storage = tryGetTableImpl (table_name, context_, false , skip_not_loaded);
1506+ // / Tables marked unreadable at metadata time are skipped, not an error;
1507+ // / direct access still reports the reason.
1508+ storage = tryGetTableImpl (table_name, context_, /* lightweight*/ false , /* ignore_if_not_iceberg*/ true );
14801509 }
14811510 catch (...)
14821511 {
@@ -1668,6 +1697,10 @@ void DatabaseDataLake::applySettingsChanges(const SettingsChanges & settings_cha
16681697 auto new_settings = std::make_unique<DatabaseDataLakeSettings>(*current_settings);
16691698 new_settings->applyChanges (settings_changes);
16701699
1700+ // / Switching the Unity implementation replaces the catalog object rather than altering it in place.
1701+ const bool implementation_changed = (*new_settings)[DatabaseDataLakeSetting::use_unity_catalog_v2].value
1702+ != (*current_settings)[DatabaseDataLakeSetting::use_unity_catalog_v2].value ;
1703+
16711704 ASTPtr new_engine_definition;
16721705 {
16731706 std::lock_guard lock (mutex);
@@ -1695,7 +1728,17 @@ void DatabaseDataLake::applySettingsChanges(const SettingsChanges & settings_cha
16951728 storage->set (storage->settings , storage_settings_ast);
16961729 }
16971730
1731+ // / `use_unity_catalog_v2` is consumed by the database, not by the catalog object, so it is
1732+ // / never handed to the catalog. Re-applying the current value is a metadata-only no-op.
1733+ SettingsChanges catalog_settings_changes;
1734+ for (const auto & change : settings_changes)
1735+ {
1736+ if (change.name != DatabaseDataLakeSettings::getSettingName (DatabaseDataLakeSetting::use_unity_catalog_v2))
1737+ catalog_settings_changes.push_back (change);
1738+ }
1739+
16981740 std::shared_ptr<DataLake::ICatalog> local_catalog_snapshot;
1741+ if (!implementation_changed)
16991742 {
17001743 std::lock_guard lock (catalog_mutex);
17011744 local_catalog_snapshot = catalog_impl;
@@ -1704,8 +1747,9 @@ void DatabaseDataLake::applySettingsChanges(const SettingsChanges & settings_cha
17041747 // / Prepare the new catalog state without publishing it: validation, the eager token
17051748 // / fetch and the config reload may throw, and then nothing has changed yet.
17061749 DataLake::ICatalog::PreparedSettingsChangesPtr prepared_catalog_changes;
1707- if (local_catalog_snapshot)
1708- prepared_catalog_changes = local_catalog_snapshot->prepareSettingsChanges (settings_changes, DataLake::getForwardedAuthToken (query_context));
1750+ const bool alter_catalog = local_catalog_snapshot && !catalog_settings_changes.empty ();
1751+ if (alter_catalog)
1752+ prepared_catalog_changes = local_catalog_snapshot->prepareSettingsChanges (catalog_settings_changes, DataLake::getForwardedAuthToken (query_context));
17091753
17101754 // / Persist the new metadata before publishing anything: if the write fails, the live
17111755 // / state is untouched and matches the old metadata on disk. The create query is built
@@ -1717,7 +1761,7 @@ void DatabaseDataLake::applySettingsChanges(const SettingsChanges & settings_cha
17171761 DatabaseCatalog::instance ().updateMetadataFile (getDatabaseName (), new_create_query);
17181762
17191763 // / Publish. Nothing below throws.
1720- if (local_catalog_snapshot )
1764+ if (alter_catalog )
17211765 local_catalog_snapshot->commitSettingsChanges (std::move (prepared_catalog_changes));
17221766 database_settings.set (std::move (new_settings));
17231767 {
@@ -1726,9 +1770,9 @@ void DatabaseDataLake::applySettingsChanges(const SettingsChanges & settings_cha
17261770 }
17271771 if (!local_catalog_snapshot)
17281772 {
1729- // / The catalog was not built when the ALTER started. If a concurrent query
1730- // / built it meanwhile, it used the old settings: drop it so the next access
1731- // / rebuilds it with the new ones. Also clear a recorded construction failure
1773+ // / The catalog was not built when the ALTER started, or the implementation was switched.
1774+ // / If a concurrent query built it meanwhile, it used the old settings: drop it so the next
1775+ // / access rebuilds it with the new ones. Also clear a recorded construction failure
17321776 // / (e.g. credentials lost on RESTORE) for the same reason.
17331777 std::lock_guard lock (catalog_mutex);
17341778 resetCatalog (/* reason */ " " );
@@ -1813,6 +1857,10 @@ ASTPtr DatabaseDataLake::getCreateTableQueryImpl(
18131857 storage->engine ->setKind (ASTFunction::Kind::TABLE_ENGINE );
18141858 if (!table_metadata.isDefaultReadableTable ())
18151859 storage->engine ->name = DataLake::FAKE_TABLE_ENGINE_NAME_FOR_UNREADABLE_TABLES ;
1860+ else if (table_metadata.getTableFormat () != DataLake::DataLakeTableFormat::UNKNOWN )
1861+ // / Mixed-format catalogs (e.g. `UnityV2Catalog`) set the format per table; single-format
1862+ // / catalogs leave it UNKNOWN and keep the database-level table engine name.
1863+ storage->engine ->name = String (catalog->getTableEngineName (table_metadata));
18161864
18171865 storage->settings = {};
18181866 if (partition_by)
@@ -2197,9 +2245,10 @@ The following settings are supported:
21972245
21982246| Setting | Description |
21992247|-------------------------|-----------------------------------------------------------------------------------------|
2200- | `catalog_type` | Type of catalog: `glue`, `unity` (Delta), `rest` (Iceberg), `hive`, `onelake` (Iceberg), `delta_sharing` (Iceberg, flat namespaces), `horizon` (Snowflake Horizon Iceberg REST) |
2248+ | `catalog_type` | Type of catalog: `glue`, `unity` (Delta, or Delta and Iceberg with `use_unity_catalog_v2` ), `rest` (Iceberg), `hive`, `onelake` (Iceberg), `delta_sharing` (Iceberg, flat namespaces), `horizon` (Snowflake Horizon Iceberg REST) |
22012249| `warehouse` | The warehouse/database name to use in the catalog. |
22022250| `catalog_credential` | Authentication credential for the catalog (e.g., API key or token) |
2251+ | `use_unity_catalog_v2` | For `catalog_type = 'unity'`: use the new implementation, which serves both Delta Lake and Iceberg tables. Default: `false`. Set it on `CREATE`, or change it for an existing database with `ALTER DATABASE ... MODIFY SETTING`. |
22032252| `auth_header` | Custom HTTP header for authentication with the catalog service |
22042253| `auth_scope` | OAuth2 scope for authentication (if using OAuth) |
22052254| `storage_endpoint` | Endpoint URL for the underlying storage |
@@ -2325,6 +2374,22 @@ Some catalogs support only one of the two modes:
23252374See below sections for examples of using the `DataLakeCatalog` engine:
23262375
23272376* [Unity Catalog](/guides/use-cases/data-warehousing/unity-catalog)
2377+ * Unity Catalog with Delta Lake and Iceberg tables
2378+ With `use_unity_catalog_v2 = 1`, the `unity` catalog serves both Delta Lake and Iceberg tables,
2379+ detecting the format of each table.
2380+ ```sql
2381+ CREATE DATABASE database_name
2382+ ENGINE = DataLakeCatalog('https://<workspace>.cloud.databricks.com/api/2.1/unity-catalog')
2383+ SETTINGS
2384+ catalog_type = 'unity',
2385+ use_unity_catalog_v2 = 1,
2386+ warehouse = 'my_catalog',
2387+ catalog_credential = '<client_id>:<client_secret>';
2388+ SHOW TABLES FROM database_name;
2389+ SELECT count() FROM database_name.`schema_name.table_name`;
2390+ ```
2391+ `catalog_credential` accepts an OAuth service principal as `<client_id>:<client_secret>`,
2392+ a personal access token (`dapi...`), or a pre-obtained OAuth token.
23282393* [Glue Catalog](/guides/use-cases/data-warehousing/glue-catalog)
23292394* OneLake Catalog
23302395 Can be used by enabling `allow_experimental_database_iceberg` or `allow_database_iceberg`.
0 commit comments