blob: 3dedd93258a178582529818d7a5aad5c3dfd92c8 [file] [view]
<!--
~ 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. The SQL catalog is currently wired into the CMake build only;
> the Meson build does not build or install it yet.
## 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.