H5DB Developer Guide
July 19, 2026 · View on GitHub
This guide covers the essential workflows for developing the h5db DuckDB extension.
Table of Contents
- Prerequisites
- Initial Setup
- Building the Project
- Running Tests
- Working with Python Scripts
- Development Workflow
- Project Structure
- Troubleshooting
Prerequisites
Before you begin, ensure you have the following installed:
- C++ compiler: GCC 9+ or Clang 10+
- CMake: 3.15+
- Git: For cloning repositories
- Python 3: For test data generation
- ninja-build: Fast build system (recommended)
- ccache: Compiler cache for faster rebuilds (recommended)
Installing Prerequisites (Ubuntu/Debian)
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
git \
python3 \
python3-venv \
ninja-build \
ccache
Initial Setup
1. Clone the Repository
git clone <repository-url>
cd h5db
2. Initialize Git Submodules
The project includes DuckDB as a git submodule:
git submodule update --init --recursive
3. Set Up VCPKG (Dependency Management)
VCPKG is used to manage HDF5 and other dependencies.
If you don't have vcpkg installed:
# Clone vcpkg (recommended: same parent directory as h5db)
cd ..
git clone https://github.com/Microsoft/vcpkg.git
cd vcpkg
./bootstrap-vcpkg.sh
# Set the toolchain path environment variable
export VCPKG_TOOLCHAIN_PATH=`pwd`/scripts/buildsystems/vcpkg.cmake
# Go back to h5db directory
cd ../h5db
Update the .env file:
The project includes a .env file that needs to be configured with your vcpkg path:
# Edit .env to set your vcpkg path
vim .env
# Example content:
# export VCPKG_TOOLCHAIN_PATH=/home/yourusername/personal/vcpkg/scripts/buildsystems/vcpkg.cmake
# export GEN=ninja
Or create it from scratch:
cat > .env << EOF
# H5DB Development Environment Variables
# VCPKG Toolchain Path - Required for dependency management
export VCPKG_TOOLCHAIN_PATH=/path/to/vcpkg/scripts/buildsystems/vcpkg.cmake
# Build optimizations - Use ninja and ccache for fast builds
export GEN=ninja
# Optional: Increase ccache size if needed (default is 5GB)
# export CCACHE_MAXSIZE=10G
EOF
Replace /path/to/vcpkg with your actual vcpkg installation path. If you followed the steps above and installed vcpkg in the parent directory:
# From h5db directory, automatically set the path
export VCPKG_TOOLCHAIN_PATH=`cd ../vcpkg && pwd`/scripts/buildsystems/vcpkg.cmake
echo "export VCPKG_TOOLCHAIN_PATH=$VCPKG_TOOLCHAIN_PATH" >> .env
echo "export GEN=ninja" >> .env
4. Set Up Python Virtual Environment
The project includes a Python virtual environment for running test data generation scripts and the SFTP interaction harness:
# The venv is already created, but if you need to recreate it:
python3 -m venv venv
source venv/bin/activate
pip install h5py numpy paramiko
deactivate
Note: The venv is configured to automatically source .env when activated.
Building the Project
Quick Start
The simplest way to build:
# Activate virtual environment (includes all tools and environment variables)
source venv/bin/activate
# Build the project (uses ninja and ccache automatically)
make -j8
Build Outputs
After a successful build, you'll find:
build/release/
├── duckdb # DuckDB CLI with h5db extension loaded
├── test/unittest # Test runner
└── extension/h5db/
└── h5db.duckdb_extension # Loadable extension binary
Build Variants
# Release build (default, optimized)
source venv/bin/activate && make
# Debug build (with debug symbols, no optimization)
source venv/bin/activate && make debug
# Release with debug info
source venv/bin/activate && make reldebug
First Build vs. Incremental Builds
First build:
- Downloads and compiles all dependencies (HDF5, zlib, etc.)
- Compiles DuckDB and all extensions
- Takes ~10-15 minutes with ninja
Incremental builds (after making changes):
- Only recompiles changed files (thanks to ccache)
- Takes 10-60 seconds depending on changes
- Extension-only changes: ~10-30 seconds
Clean Build
If you need to start fresh:
# Clean all build artifacts
make clean
# Then rebuild
source venv/bin/activate && make -j8
Running Tests
Test Structure
The project uses DuckDB's SQLLogicTest framework. Tests are located in:
test/
├── sql/ # SQLLogicTest files
└── data/
├── *.h5 # HDF5 test files
└── *.py # Scripts to generate test data
test/sql/large/ # Large SQLLogicTest files (slow)
Running All Tests
# Full suite: local tests + rewritten remote HTTP/SFTP suites
make test
# Run the local SQLLogicTests directly
./build/release/test/unittest "test/sql/*" "~test/sql/remote/*"
# Optional: skip slow tests
./build/release/test/unittest "test/sql/*" "~test/sql/large/*" "~test/sql/remote/*"
If you run tests directly, ensure test data exists first:
./test/data/ensure_test_data.sh
Running Specific Tests
# Run a specific local test file directly
./build/release/test/unittest "test/sql/<testfile>.test"
# Run the rewritten remote HTTP suite (includes test/sql/remote/*.test)
make test_remote_http
# Run the rewritten remote SFTP suite + dedicated interaction harness
make test_remote_sftp
Running Individual Test Cases
You can filter tests by pattern:
# Run tests matching a pattern
./build/release/test/unittest "test/sql/<testfile>.test" -testcase="*pattern*"
Using the Makefile Test Target
The Makefile provides a convenient test target:
# Full suite: local tests + remote HTTP/SFTP suites
make test
make test also ensures all HDF5 test data is present by running
test/data/ensure_test_data.sh before executing the tests.
It then runs the local SQLLogicTests, the rewritten remote HTTP suite (including test/sql/remote/*.test), and the
rewritten remote SFTP suite plus its dedicated interaction harness.
Running Remote HTTP Test Suite
To run the suite against rewritten remote URLs (using the local range-capable HTTP server):
make test_remote_http
This target also runs test/sql/remote/*.test (auth, retries, redirects, timeout, truncation, corruption,
cache behavior, and simulated server/drop failures).
Running Remote SFTP Test Suite
To run the suite against rewritten SFTP URLs (using the local rooted SFTP server):
make test_remote_sftp
This target rewrites the main SQL suite against sftp:// URLs, starts the local SFTP test server, and then runs the
dedicated interaction harness in test/scripts/run_sftp_interaction_tests.py. The runner will use the repo venv when
present and otherwise falls back to python3/python, installing paramiko if needed.
For targeted SFTP work, run the harness scripts directly:
# Run a subset of rewritten SQLLogicTests through SFTP and skip the interaction harness
bash test/scripts/run_sftp_tests.sh --test-glob 'glob/*.test' --skip-interaction-tests
# Run selected interaction tests
./venv/bin/python test/scripts/run_sftp_interaction_tests.py \
--duckdb-bin ./build/release/duckdb \
SFTPInteractionTests.test_sftp_glob_skips_unlistable_literal_directory_component
SFTP Interrupt and Cleanup Behavior
The native SFTP backend keeps libssh2 sessions in nonblocking mode. Remote SSH/SFTP operations wait in short slices and
check DuckDB's query interrupt state between waits, so user interrupts can stop connection setup, authentication,
directory listing, file reads, and normal cleanup. The intentional exception is getaddrinfo(...), which remains a
regular local resolver call to avoid adding resolver-specific machinery.
Cleanup is graceful by default: h5db tries to close SFTP handles, shut down the SFTP subsystem, send SSH disconnect, and free the libssh2 session through the normal libssh2 APIs. Cleanup switches to bounded abortive behavior when either the connection has already seen an unreliable remote-connection event or DuckDB reports the query context as interrupted. Once that condition is observed, all cleanup steps in the current cleanup cascade share one 1s grace period. If graceful cleanup still cannot make progress, h5db shuts down the socket transport and releases local ownership of the SFTP/session objects rather than letting cleanup block the query indefinitely.
Windows CI currently skips this SQL glob test:
test/sql/glob/h5_glob_symlink.testContains full file-symlink glob coverage forh5_tree,h5_ls, andh5_read, including direct and globbed access totest/data/glob_symlink/link_file.h5. Windows CI showed that directH5Fopenwith HDF5 1.14.6 fails when the final path component is a file symlink, with HDF5 reporting anH5FD__sec2_openfailure anderrno = 22. The full test is markedrequire notwindows. Windows keeps narrower coverage intest/sql/glob/h5_glob_symlink_windows.test, which exercises symlink directories in intermediate path components without opening a file symlink as the final component.
Working with Python Scripts
The project uses Python scripts to generate HDF5 test data files. Always use the virtual environment to ensure correct dependencies.
Activating the Virtual Environment
# Activate venv (automatically sources .env)
source venv/bin/activate
# Your prompt will change to show (venv)
(venv) user@host:~/h5db$
# Deactivate when done
deactivate
Running Python Scripts
Method 1: With activated venv
source venv/bin/activate
cd test/data
python create_rse_edge_cases.py
deactivate
Method 2: Direct execution (recommended)
# Run directly without activating venv
./venv/bin/python test/data/create_rse_edge_cases.py
Method 3: From within test/data directory
cd test/data
../../venv/bin/python create_rse_edge_cases.py
cd ../..
Regenerating Test Data
If you modify a test data generation script or need to recreate test files:
# Regenerate the entire test-data tree
./test/data/generate_all_test_data.sh
# Or regenerate only the fixture touched by your change
cd test/data
../../venv/bin/python create_unsupported_types_test.py
cd ../..
test/data/ensure_test_data.sh only generates missing files. If you change a generator script, rerun that specific
generator or generate_all_test_data.sh.
Installing Additional Python Packages
source venv/bin/activate
pip install <package-name>
deactivate
Development Workflow
Typical Development Cycle
- Make code changes in
src/ - Rebuild the extension:
source venv/bin/activate && make -j8 - Run tests to verify:
./build/release/test/unittest "test/sql/*" "~test/sql/remote/*"# Optional: skip slow tests ./build/release/test/unittest "test/sql/*" "~test/sql/large/*" "~test/sql/remote/*" - Interactive testing with DuckDB CLI:
./build/release/duckdb D SELECT * FROM h5_read('test/data/simple.h5', '/integers'); D SELECT * FROM h5_ls('test/data/links.h5');
Code Formatting
The project uses automated code formatting to maintain consistency.
The formatting targets use tools installed in the repo virtual environment, so activate venv before running
make format-check or make format-fix.
One-time setup (already configured):
./scripts/setup-dev-env.sh # Install formatting tools in venv
./scripts/install-pre-commit-hook.sh # Optional: auto-check on commit
Before committing:
# Check formatting (fast)
source venv/bin/activate && make format-check
# Auto-fix formatting issues
source venv/bin/activate && make format-fix
What gets formatted:
- C/C++ code:
clang-format(uses DuckDB's .clang-format rules) - Python code:
black - CMake files:
cmake-format
Pre-commit hook (if installed):
- Automatically runs
make format-checkbefore each commit - Blocks commits with formatting issues
- To bypass temporarily:
git commit --no-verify - If the hook fails due to missing tools, run
./scripts/setup-dev-env.sh
Thread Safety Considerations
IMPORTANT: The HDF5 library is not thread-safe. To prevent race conditions and crashes when DuckDB parallelizes query execution, all HDF5 API calls are protected by a global mutex (hdf5_global_mutex).
What this means for developers:
-
When adding new HDF5 function calls, always protect them with the mutex:
// Lock for all HDF5 operations (not thread-safe) std::lock_guard<std::recursive_mutex> lock(hdf5_global_mutex); // Now safe to call HDF5 API hid_t file_id = H5Fopen(...); -
Protected locations (current shape):
h5_tree: file opens, resumable namespace-iteration slices, object resolution, and any projected dataset metadata or attribute reads; the mutex is released before each produced batch is returned to DuckDBh5_ls: file opens, group validation, immediate-child iteration, and any projected child metadata or attribute reads; scalarh5_lsreads its complete result while holding the mutexh5_read: schema determination, dataset opens, and scan-time HDF5 readsh5_attributes: attribute schema reads and attribute value reads
-
Performance implications:
- The mutex serializes all HDF5 operations across threads
- This prevents crashes but may reduce parallelism
- This is necessary for correctness with the current HDF5 build
-
Future optimizations (if needed):
- Use thread-safe HDF5 builds (requires specific compile flags)
- Implement fine-grained locking per file handle
- Consider read-write locks for concurrent reads
Historical context: A critical segmentation fault bug (BUG_UNION_ALL_SEGFAULT.md) was discovered when DuckDB created 12 threads for parallel execution of UNION ALL queries. The crash occurred in H5C_protect during parallel H5ReadInit calls. The global mutex fix resolved this issue.
Adding New Tests
-
Create test data (if needed):
# Create a Python script in test/data/ vim test/data/create_mytest.py # Generate the HDF5 file ./venv/bin/python test/data/create_mytest.py -
Add test cases to an existing
.testfile or create a new one:vim test/sql/mytest.test -
Run the new tests:
./build/release/test/unittest "test/sql/mytest.test"
Debugging
Using the DuckDB CLI for interactive debugging:
./build/release/duckdb
# Turn on profiling
D .timer on
# Run queries and inspect results
D SELECT * FROM h5_tree('test/data/simple.h5');
D SELECT * FROM h5_ls('test/data/links.h5');
D SELECT * FROM h5_read('test/data/simple.h5', '/integers');
Debugging with GDB:
# Build with debug symbols
source venv/bin/activate && make debug
# Run under GDB
gdb --args ./build/debug/test/unittest "test/sql/*" "~test/sql/remote/*"
Viewing test file contents:
# Check what's in an HDF5 file
./build/release/duckdb -c "SELECT * FROM h5_tree('test/data/simple.h5');"
# Check one group's immediate children
./build/release/duckdb -c "SELECT * FROM h5_ls('test/data/links.h5');"
# Check structure
./build/release/duckdb -c "SELECT path, type, dtype, shape FROM h5_tree('test/data/simple.h5');"
Project Structure
h5db/
├── .env # Build environment configuration
├── Makefile # Main build file
├── CMakeLists.txt # CMake configuration
├── vcpkg.json # Dependency specification
├── extension_config.cmake # Extension-specific CMake config
│
├── src/ # Source code
│ ├── h5db_extension.cpp # Extension entry point
│ ├── h5_read_table.cpp # table h5_read implementation
│ ├── h5_read_scalar.cpp # scalar h5_read implementation
│ ├── h5_read_shared.cpp # shared h5_read dataset helpers
│ ├── h5_remote_backend.cpp # DuckDB-FS and SFTP remote backends
│ ├── h5_remote_vfd.cpp # HDF5 remote VFD glue
│ ├── h5_sftp_secrets.cpp # DuckDB TYPE sftp secret registration
│ ├── h5_attr.cpp # h5_attr projected-attribute marker
│ ├── h5_tree.cpp # h5_tree implementation
│ ├── h5_ls.cpp # h5_ls table/scalar implementations
│ ├── h5_tree_shared.cpp # shared h5_tree/h5_ls metadata helpers
│ ├── h5_attributes.cpp # h5_attributes implementation
│ └── h5_common.cpp # Shared HDF5 helpers
│
├── test/ # Test suite
│ ├── sql/ # SQLLogicTest files
│ └── data/ # Test data files
│ ├── *.h5 # HDF5 test files
│ └── *.py # Data generation scripts
│
├── docs/ # Documentation
│ ├── README.md # Documentation index
│ ├── USER_GUIDE.md # Practical user guide
│ ├── API.md # Public API reference
│ ├── RSE_USAGE.md # RSE user guide
│ ├── developer/ # Contributor docs
│ └── internals/ # Internal design notes
│
├── duckdb/ # DuckDB submodule
├── extension-ci-tools/ # CI/CD tooling
└── venv/ # Python virtual environment
Key Files
.env: Environment configuration (VCPKG path, build settings)src/h5_read_table.cpp: Tableh5_read, run-encoded scanner, shared chunk-cache coordination, and multi-file scan wrappersrc/h5_read_scalar.cpp: Scalarh5_read, including runtime-typed dataset materialization intoVARIANTsrc/h5_read_shared.cpp: Dataset opening, contextual errors, checked sizing, and string decoding shared by bothh5_readformssrc/h5_read_table.cppintentionally does not currently register DuckDBget_partition_data. An earlier partition-based implementation was removed because early-stopping plans could abandon a partition while still blocking shared cache progress. Reintroduce it only with a design that does not make cache progress depend on a local scan state returning after it has produced a chunk.src/h5_remote_backend.cpp: DuckDB-backed remote access plussftp://backendsrc/h5_remote_vfd.cpp: HDF5 VFD integration for remote filessrc/h5_sftp_secrets.cpp: registration and validation for DuckDBTYPE sftpsecretssrc/h5_attr.cpp:h5_attr(...)projected-attribute marker registrationsrc/h5_tree.cpp: recursive namespace listingsrc/h5_ls.cpp: immediate-child listing (h5_lstable and scalar forms)src/h5_tree_shared.cpp: shared row resolution, metadata, and projected-attribute helpers forh5_tree/h5_lssrc/h5_attributes.cpp: Attribute readersrc/h5_common.cpp: Shared HDF5 helperstest/sql/*.test: SQLLogicTest test files (regular)test/sql/remote/*.test: remote-only SQLLogicTests (auth, retries, redirects, transport faults)test/sql/large/*.test: large SQLLogicTests (slow)test/scripts/run_remote_tests.sh: rewritten remote HTTP suite harnesstest/scripts/run_sftp_tests.sh: rewritten remote SFTP suite harnesstest/scripts/run_sftp_interaction_tests.py: dedicated SFTP interaction/auth/cache harnesstest/scripts/sftp_test_server_lib.py: local rooted SFTP test server implementationvcpkg.json: Dependencies (hdf5,libssh2)
Troubleshooting
SWMR Notes
Observed SWMR open behavior can vary by HDF5 build/version. In this repo’s current setup (HDF5 1.14.6 via vcpkg),
opening with H5F_ACC_SWMR_READ succeeds even for files not written in SWMR mode. Other builds may reject SWMR opens
unless the file is marked by a SWMR writer, so don’t assume identical behavior across environments.
Build Issues
Problem: CMake can't find HDF5
Solution: Ensure VCPKG_TOOLCHAIN_PATH is set correctly in .env:
source venv/bin/activate
echo $VCPKG_TOOLCHAIN_PATH
# Should print: /path/to/vcpkg/scripts/buildsystems/vcpkg.cmake
Problem: Build fails with "generator does not match"
Solution: Clean and rebuild:
make clean
source venv/bin/activate && make -j8
Problem: Undefined reference errors
Solution: HDF5 libraries may not be linked. Check CMakeLists.txt includes:
target_link_libraries(... ${HDF5_C_LIBRARIES})
Test Issues
Problem: Tests fail with "File not found"
Solution: Ensure test data exists, then run tests from project root:
cd /path/to/h5db
./test/data/ensure_test_data.sh
./build/release/test/unittest "test/sql/*" "~test/sql/remote/*"
Problem: Python script fails with "ModuleNotFoundError"
Solution: Use the venv Python:
./venv/bin/python test/data/script.py
# NOT: python3 test/data/script.py
Problem: HDF5 test file is outdated
Solution: Regenerate all test data:
./test/data/generate_all_test_data.sh
Environment Issues
Problem: Commands fail with "command not found"
Solution: Activate the virtual environment first:
source venv/bin/activate
make -j8
Problem: make format-fix fails because black, clang-format, or cmake-format is missing
Solution: Install the dev tools if needed, then activate the virtual environment:
./scripts/setup-dev-env.sh
source venv/bin/activate
make format-fix
Problem: VCPKG dependencies not found
Solution: Ensure vcpkg is bootstrapped and path is set:
cd /path/to/vcpkg
./bootstrap-vcpkg.sh
# Set the path using pwd
export VCPKG_TOOLCHAIN_PATH=`pwd`/scripts/buildsystems/vcpkg.cmake
# Verify it's set
echo $VCPKG_TOOLCHAIN_PATH
Quick Reference
Common Commands
# Build
source venv/bin/activate && make -j8
# Test all suites
make test
# Test local SQLLogicTests only
./build/release/test/unittest "test/sql/*" "~test/sql/remote/*"
# Optional: skip slow tests
./build/release/test/unittest "test/sql/*" "~test/sql/large/*" "~test/sql/remote/*"
# Test rewritten remote HTTP suite
make test_remote_http
# Test rewritten remote SFTP suite + interaction harness
make test_remote_sftp
# Ensure test data exists (generate if missing)
./test/data/ensure_test_data.sh
# Test specific file
./build/release/test/unittest "test/sql/<testfile>.test"
# Run DuckDB CLI
./build/release/duckdb
# Run Python script
./venv/bin/python test/data/<script>.py
# Clean build
make clean
# Regenerate test data
./test/data/generate_all_test_data.sh
Environment Variables
These are set in .env and automatically loaded when you activate the virtual environment:
VCPKG_TOOLCHAIN_PATH: Path to vcpkg CMake toolchainGEN: Build generator (ninja for fast builds)
Example .env content:
export VCPKG_TOOLCHAIN_PATH=/home/yourusername/personal/vcpkg/scripts/buildsystems/vcpkg.cmake
export GEN=ninja
To set the path dynamically (from vcpkg directory):
cd /path/to/vcpkg
export VCPKG_TOOLCHAIN_PATH=`pwd`/scripts/buildsystems/vcpkg.cmake
Note: When you run source venv/bin/activate, the .env file is automatically sourced, so you get both the Python environment and all build variables.
Build Targets
make # Release build
make debug # Debug build
make test # Run tests (generates data if missing)
make clean # Clean build artifacts
Additional Resources
- Main README:
README.md- Project overview and usage - API Reference:
../API.md- Complete function reference - RSE Documentation:
../RSE_USAGE.md- Run-Start Encoding guide
For questions or issues, please check existing documentation or open an issue on GitHub.