| <!-- | |
| ~ Licensed to the Apache Software Foundation (ASF) under one | |
| ~ or more contributor license agreements. See the NOTICE file | |
| ~ distributed with this work for additional information | |
| ~ regarding copyright ownership. The ASF licenses this file | |
| ~ to you under the Apache License, Version 2.0 (the | |
| ~ "License"); you may not use this file except in compliance | |
| ~ with the License. You may obtain a copy of the License at | |
| ~ | |
| ~ http://www.apache.org/licenses/LICENSE-2.0 | |
| ~ | |
| ~ Unless required by applicable law or agreed to in writing, | |
| ~ software distributed under the License is distributed on an | |
| ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| ~ KIND, either express or implied. See the License for the | |
| ~ specific language governing permissions and limitations | |
| ~ under the License. | |
| --> | |
| # SQL Catalog | |
| `SqlCatalog` implements the Iceberg `Catalog` API on top of a relational | |
| database. Its on-disk schema is compatible with the Apache Iceberg Java | |
| `JdbcCatalog`: two tables, `iceberg_tables` and `iceberg_namespace_properties`, | |
| scoped by a catalog name so multiple catalogs can share one database. | |
| ## Design | |
| `SqlCatalog` owns the Iceberg catalog behavior. It validates namespaces, reads | |
| and writes table metadata files, and performs optimistic-concurrency commits. | |
| Database access is delegated to a small storage interface: | |
| ``` | |
| Application | |
| | | |
| v | |
| SqlCatalog | |
| | | |
| | CatalogStore API | |
| v | |
| CatalogStore implementation | |
| | | |
| v | |
| SQL database | |
| - iceberg_tables | |
| - iceberg_namespace_properties | |
| ``` | |
| `CatalogStore` (see [`catalog_store.h`](catalog_store.h)) exposes typed row | |
| operations such as `InsertTable`, `GetTableMetadataLocation`, | |
| `UpdateTableMetadataLocation(expected_current)`, namespace-property CRUD, and | |
| `RunInTransaction`. It exposes no SQL strings or driver-specific types. | |
| The project provides built-in `CatalogStore` implementations for SQLite, | |
| PostgreSQL, and MySQL. They are implemented with | |
| [sqlpp23](https://github.com/rbock/sqlpp23), and the shared query code lives in | |
| `catalog_store_sqlpp23_internal.h`. Users can also provide their own | |
| `CatalogStore` implementation for another database, driver, or connection pool. | |
| sqlpp23 is a build-time-only dependency for the built-in stores. It is compiled | |
| into the connector translation units and does not appear in the installed | |
| interface, so downstream consumers only need the native client libraries. | |
| > The built-in sqlpp23 connectors require CMake >= 3.28 and C++23; sqlpp23 is | |
| > fetched automatically via `FetchContent` when at least one built-in connector | |
| > is enabled. A SQL catalog backed only by a user-supplied `CatalogStore` does not | |
| > need sqlpp23. | |
| ## Out-of-the-box usage | |
| Enable the SQL catalog and any built-in connectors at configure time. Built-in | |
| connectors pull in their native client libraries via sqlpp23: | |
| | CMake option | Default | sqlpp23 target | Native dependency | | |
| |-----------------------------|---------|-----------------------|------------------------| | |
| | `ICEBERG_BUILD_SQL_CATALOG` | `OFF` | - | - | | |
| | `ICEBERG_SQL_SQLITE` | `OFF` | `sqlpp23::sqlite3` | SQLite3 | | |
| | `ICEBERG_SQL_POSTGRESQL` | `OFF` | `sqlpp23::postgresql` | libpq (PostgreSQL) | | |
| | `ICEBERG_SQL_MYSQL` | `OFF` | `sqlpp23::mysql` | libmysqlclient (MySQL) | | |
| ```bash | |
| cmake -S . -B build -DICEBERG_BUILD_SQL_CATALOG=ON -DICEBERG_SQL_SQLITE=ON | |
| ``` | |
| ```cpp | |
| #include "iceberg/catalog/sql/sql_catalog.h" | |
| using iceberg::sql::SqlCatalog; | |
| using iceberg::sql::SqlCatalogConfig; | |
| SqlCatalogConfig config{ | |
| .name = "prod", | |
| .uri = "/var/lib/iceberg/catalog.db", // SQLite file path | |
| .warehouse_location = "s3://my-bucket/warehouse", | |
| }; | |
| auto catalog = SqlCatalog::MakeSqliteCatalog(config, file_io).value(); | |
| // catalog->CreateNamespace(...), CreateTable(...), LoadTable(...), ... | |
| ``` | |
| `MakePostgreSqlCatalog` and `MakeMySqlCatalog` use the same | |
| `[scheme://][user[:password]@]host[:port][/database]` URI form. Connector | |
| factories are always declared in the public headers; if a connector was not | |
| built, its factory returns `ErrorKind::kNotSupported`. Each enabled factory | |
| creates the schema if it does not yet exist. | |
| The PostgreSQL and MySQL stores use a single sqlpp23 connection when | |
| `max_connections <= 1` and a bounded sqlpp23 connection pool otherwise. | |
| Transaction bodies reuse the same leased connection for every store operation | |
| issued inside `RunInTransaction`. The SQLite store ignores `max_connections` and | |
| always uses a single connection: a file database only allows one writer (a pool | |
| of write connections would just hit `SQLITE_BUSY`) and a `:memory:` database is | |
| private to each connection. | |
| The backing schema follows the Java/Rust-compatible `iceberg_tables` layout, | |
| including the optional `iceberg_type` column. New table rows write | |
| `iceberg_type = 'TABLE'` as the record type; existing rows with `NULL` remain | |
| readable for compatibility. | |
| ## Bring your own store | |
| To use a database, driver, or connection pool that is not built in, implement | |
| `CatalogStore` and inject it. No catalog code changes are required: | |
| ```cpp | |
| class MyCatalogStore : public iceberg::sql::CatalogStore { | |
| public: | |
| iceberg::Status Initialize() override { /* CREATE TABLE IF NOT EXISTS ... */ } | |
| iceberg::Result<std::optional<std::string>> GetTableMetadataLocation( | |
| std::string_view ns, std::string_view name) override { /* ... */ } | |
| iceberg::Status InsertTable(std::string_view ns, std::string_view name, | |
| std::string_view metadata_location) override { /* ... */ } | |
| // ... the remaining CatalogStore operations ... | |
| iceberg::Status RunInTransaction( | |
| const std::function<iceberg::Status()>& body) override { /* ... */ } | |
| }; | |
| auto store = std::make_shared<MyCatalogStore>(/* ... */); | |
| auto catalog = SqlCatalog::Make(config, file_io, std::move(store)).value(); | |
| ``` | |
| ### Implementation contract | |
| - **Catalog scope**: a store instance is bound to one catalog name; every row it | |
| reads or writes must be scoped by that name. | |
| - **Namespace identifiers**: namespace levels must not be empty and must not | |
| contain `.` because the backing schema stores a namespace as a dot-joined | |
| string. | |
| - **Table rows**: new table inserts should write `iceberg_type = 'TABLE'` as | |
| the record type. | |
| Reads should treat both `TABLE` and `NULL` as table rows so older databases | |
| remain readable. | |
| - **Unique violations**: `InsertTable`, `InsertNamespaceProperty`, and | |
| `RenameTable` must report a primary-key collision as `ErrorKind::kAlreadyExists`. | |
| The catalog relies on this as the authoritative signal for concurrent creates. | |
| - **Affected rows**: `UpdateTableMetadataLocation` performs the optimistic | |
| compare-and-set; it must return the number of rows updated (0 on a stale base). | |
| - **Atomicity**: `RunInTransaction` must commit on success and roll back on any | |
| error so the database is left unchanged. | |
| - **Threading**: a store may be called from multiple threads; serialize | |
| internally or use one connection per concurrent operation. The built-in | |
| PostgreSQL and MySQL stores use a bounded sqlpp23 connection pool when | |
| `max_connections > 1`; the SQLite store always uses a single connection. |