Configuration
ftm-lakehouse can be configured via environment variables or YAML configuration files.
Environment Variables
Core Settings
| Variable | Description | Default |
|---|---|---|
LAKEHOUSE_URI |
Base path to lakehouse storage | ./data |
LAKEHOUSE_JOURNAL_URI |
SQLAlchemy URI for statement journal | sqlite:///:memory: |
LAKEHOUSE_GRACE_PERIOD_DAYS |
Default tombstone grace period used by maintenance optimize (rows with deleted_at older than this are physically dropped in the merge step) |
30 |
LAKEHOUSE_MAX_BUFFER_ROWS |
Hard cap on rows held in an in-memory EntityBuffer before a flush is required. Bulk-import paths that hit the cap raise BufferFullError and the caller flushes + retries. |
1_000_000 |
LAKEHOUSE_JOURNAL_POOL_SIZE |
Postgres journal connections kept warm between writers. ADBC ships no pool of its own and a cold connection costs ~60ms, which the journal would pay per writer; a checkout is ~0.3ms including the liveness ping that keeps a connection the server has since dropped (idle_session_timeout, a pgbouncer reap, a failover, a restart) from reaching a writer. This bounds only what is kept idle – writers beyond it open their own connection rather than queueing, so peak connections follow write concurrency either way. It applies per cached dataset journal, so a worker writing many datasets holds up to this many idle connections for each of them: that is the figure to size against postgres max_connections. Set to 0 to pool nothing. |
5 |
LAKEHOUSE_LOCK_MAX_RETRIES |
Retry bound for every wait on the dataset write fence: acquiring the exclusive maintenance lock (.LOCK), an append backing off while .LOCK is held, and maintenance draining in-flight append markers (.LOCK-APPENDS/). Retry n sleeps n + jitter seconds, so the total wait is roughly N²/2 seconds; the default gives up after ~4.5 minutes with a RuntimeError instead of waiting forever. Stale locks or markers from crashed writers need a manual ftm-lakehouse maintenance unlock. |
22 |
LAKEHOUSE_DUCKDB_MEMORY_LIMIT |
Per-DuckDB-connection RAM ceiling. Queries exceeding it spill to disk rather than growing toward all available RAM. Format follows DuckDB's SET memory_limit (e.g. 8GB, 512MB). |
8GB |
LAKEHOUSE_DUCKDB_TEMP_DIRECTORY |
Spill-to-disk path for queries that overflow LAKEHOUSE_DUCKDB_MEMORY_LIMIT |
Defaults under the OS temp directory. Point at a fast, capacity-controlled volume for heavy workloads. Set to a real volume if a container uses /tmp as tmpfs to avoid RAM spilling. Set empty to use default. |
LAKEHOUSE_DUCKDB_EXTENSION_DIRECTORY |
Directory DuckDB loads its extensions from (and auto-installs into when one is missing). Unset = $HOME/.duckdb/extensions, which fails in containers running without a writable HOME (Failed to create directory "/.duckdb"). The shipped Docker image pre-installs the delta extension at build time and sets this to /opt/duckdb/extensions, so runtime needs neither a writable HOME nor network access. |
(unset) |
LAKEHOUSE_ON_ZFS |
Enable ZFS dataset creation for local storage | false |
LAKEHOUSE_ZFS_POOL |
ZFS dataset path for the lakehouse root (e.g. zpools/tank/lakehouse). Transport / agent settings (ZFS_SOCKET, ZFS_OWNER, ...) belong to the zfs-agent package -- see ZFS Integration |
(required when ON_ZFS is enabled) |
LAKEHOUSE_API_KEY / LAKEHOUSE_API_SECRET |
Client-side auth headers attached to outgoing lakehouse-API requests (authenticate through the reverse proxy in front of the API server) | (unset) |
LAKEHOUSE_PUBLIC_URL_PREFIX |
Public URL prefix for blob URLs (supports a ${dataset} placeholder) |
(unset) |
LOG_LEVEL |
Logging level (DEBUG, INFO, WARNING, ERROR) | INFO |
DEBUG |
Enable debug mode | false |
There is deliberately no environment setting for the shard count: it is per-dataset configuration (shards in config.yml, default 0 = single shard). Placement is enforced on write – ParquetStore.append derives each row's shard from its entity_id against the count the store resolved – so a process with a different environment can't mis-shard an existing dataset. Huge datasets should configure 8 or more at creation; changing it later means rewriting every partition with maintenance shard – see Sharding.
Basic Usage
# Local filesystem
export LAKEHOUSE_URI=./my_lakehouse
# S3 storage
export LAKEHOUSE_URI=s3://my-bucket/lakehouse
export AWS_ACCESS_KEY_ID=your_key
export AWS_SECRET_ACCESS_KEY=your_secret
# With persistent journal (for production)
export LAKEHOUSE_JOURNAL_URI=postgresql://user:pass@localhost/journal
Dataset Configuration
Each dataset can have its own config.yml file that follows the ftmq.model.Dataset specification:
name: my_dataset # also known as "foreign_id"
title: An Awesome Dataset
shards: 0 # entity-id hash shards; configure 8+ for huge datasets at creation
compression: zst # compress exported artifacts (gz / zst; unset = uncompressed)
description: >
A detailed description of this dataset,
its sources, and contents.
updated_at: 2024-09-25
category: leak # or: sanctions, pep, etc.
publisher:
name: Data and Research Center – DARC
url: https://dataresearchcenter.org
Write it with ftm-lakehouse -d my_dataset configure -c config.yml (or update_dataset() from Python). Both merge – keys absent from the yaml keep their current value – and keep a versioned snapshot of each write.
Storage Backends
Local Filesystem
Amazon S3
export LAKEHOUSE_URI=s3://bucket-name/prefix
export AWS_ACCESS_KEY_ID=your_key
export AWS_SECRET_ACCESS_KEY=your_secret
export AWS_REGION=us-east-1
S3-Compatible (MinIO, etc.)
export LAKEHOUSE_URI=s3://bucket-name/prefix
export AWS_ACCESS_KEY_ID=your_key
export AWS_SECRET_ACCESS_KEY=your_secret
export AWS_ENDPOINT_URL=https://minlake.example.com
Google Cloud Storage
Requires extra install: pip install gcsfs
export LAKEHOUSE_URI=gs://bucket-name/prefix
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
Azure Blob Storage
Requires extra install: pip install adlfs
export LAKEHOUSE_URI=az://container-name/prefix
export AZURE_STORAGE_ACCOUNT_NAME=your_account
export AZURE_STORAGE_ACCOUNT_KEY=your_key
Or using connection string:
export LAKEHOUSE_URI=az://container-name/prefix
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net"
Or using SAS token:
export LAKEHOUSE_URI=az://container-name/prefix
export AZURE_STORAGE_ACCOUNT_NAME=your_account
export AZURE_STORAGE_SAS_TOKEN="?sv=2021-06-08&ss=b&srt=sco&sp=rwdlacyx..."
Or using Azure AD / Service Principal:
export LAKEHOUSE_URI=az://container-name/prefix
export AZURE_STORAGE_ACCOUNT_NAME=your_account
export AZURE_STORAGE_TENANT_ID=your_tenant_id
export AZURE_STORAGE_CLIENT_ID=your_client_id
export AZURE_STORAGE_CLIENT_SECRET=your_client_secret
Journal Database
The statement journal buffers writes before flushing to Delta Lake storage. For production use, configure a persistent database:
SQLite (File-based)
PostgreSQL
In-Memory (for debugging / testing)
Warning
The in-memory journal is lost when the process exits. Use a persistent database for production workloads.
Python Configuration
You can also configure programmatically:
from ftm_lakehouse import get_entities, get_lakehouse
# Get lakehouse with custom URI
lake = get_lakehouse(uri="s3://my-bucket/lakehouse")
# Repositories per dataset (uri derived from the catalog)
entities = get_entities("my_dataset", lake.dataset_uri("my_dataset"))
Multi-Dataset Configuration
A lakehouse can contain multiple datasets, each with different configurations:
lakehouse/
dataset_a/
config.yml # Dataset A config
archive/
...
dataset_b/
config.yml # Dataset B config (could point to remote storage)
...
A dataset can reference remote storage while appearing in a local catalog:
# lakehouse/remote_dataset/config.yml
name: remote_dataset
title: Remote Dataset
# This dataset's data lives in S3
storage:
uri: s3://remote-bucket/dataset
Catalog
The catalog is the storage root itself – any directory under the lakehouse uri
that contains a config.yml is a dataset. get_lakehouse().list_datasets()
enumerates them; there is no catalog-level configuration file.