Back to Devoured - October 26, 2026
DEVOURED
DuckDB hf extension — read and write Hugging Face assets (GitHub Repo)

DuckDB hf extension — read and write Hugging Face assets (GitHub Repo)

Data GitHub
The new DuckDB hf extension enables direct SQL-based reads and writes to Hugging Face datasets and models using hf:// URLs.
What: Created by Hugging Face's Lysandre Debut, the extension supports caching, deduplication via Xet, and range requests, making it possible to query files stored on the Hub as if they were local tables.
Why it matters: This integrates the Hugging Face Hub directly into the analytical workflow, bypassing the need to download large files manually before processing them with DuckDB.
Takeaway: If you use DuckDB 2.0.0, you can install the extension via `CREATE OR REPLACE EXTENSION REPOSITORY hf_repo` and start querying with `SELECT * FROM 'hf://datasets/...'`.
Decoder
  • Xet: A storage protocol that enables content-defined deduplication for files, significantly reducing upload sizes for repeated content.
  • Range Request: An HTTP feature that allows a client to request only a specific part of a file, essential for scanning large remote datasets without downloading the whole file.
Original article

DuckDB hf extension — read and write Hugging Face assets with hf://

A native C++ DuckDB extension to read/write to hf:// buckets, datasets, etc.

  • Read: using the Hub's /resolve/ endpoints (range requests + a shared read-ahead cache);
  • Write: using the Hub's commit API (same as huggingface_hub) and Xet for efficient uploads via deduplication.
  • Targets DuckDB 2.0.0 (v2.0.0, Cyanoptera).
  • Published at hf.co/duckdb-extensions.
INSTALL hf FROM hf_repo; LOAD hf FROM hf_repo;   -- one-time setup: see Installation

SELECT * FROM 'hf://datasets/lhoestq/demo1/*.parquet';
SELECT * FROM hf_ls('hf://datasets/lhoestq/demo1');
COPY (SELECT * FROM range(10) t(i)) TO 'hf://datasets/me/demo/out.parquet';
COPY (SELECT * FROM read_parquet('hf://datasets/me/demo/*.parquet'))
  TO 'hf://datasets/me/demo/partitioned' (FORMAT PARQUET, PARTITION_BY (kind));

Installation

Make sure you are using DuckDB 2.0:

SELECT version();   -- v2.0.0        <- the identifier, and the directory name a repository serves;
                                     --   a pre-2.0 client answers v2.0.0-alpha47600 instead, and wants
                                     --   a different binary: one directory per identifier is the mapping
PRAGMA platform;    -- linux_arm64         <- one of linux_amd64, linux_arm64, osx_amd64, osx_arm64,
                                            --   windows_amd64, windows_amd64_mingw,
                                            --   linux_amd64_musl, linux_arm64_musl

Install the extension:

  • in DuckDB CLI:
SET allow_extension_repositories = 'allowed';   -- the gate for adding *and* installing from a repository
CREATE OR REPLACE EXTENSION REPOSITORY hf_repo
  WITH PREFIX 'https://hf.co/buckets/duckdb-extensions/builds/resolve';
INSTALL hf FROM hf_repo;
  • in Python:
import duckdb
con = duckdb.connect(config={"allow_extension_repositories": "allowed"})
con.execute("CREATE OR REPLACE EXTENSION REPOSITORY hf_repo WITH PREFIX "
            "'https://hf.co/buckets/duckdb-extensions/builds/resolve'")
con.execute("INSTALL hf FROM hf_repo");

Usage

Load and use

LOAD hf FROM hf_repo;
SELECT * FROM hf_ls('hf://datasets/lhoestq/demo1');

Reads and writes, by repo type

Reads work for all five types, with or without @revision:

SELECT * FROM read_parquet('hf://datasets/wikimedia/wikipedia/20231101.ab/*.parquet');
SELECT content FROM read_text('hf://models/google-bert/bert-base-uncased/config.json');
SELECT * FROM glob('hf://models/nvidia/**/*.safetensors');
SELECT * FROM hf_ls('hf://spaces/dalle-mini/dalle-mini');
SELECT content FROM read_text('hf://kernels/kernels-community/flash-attn3/README.md');
SELECT * FROM read_parquet('hf://buckets/lhoestq/playground/duckdb/*.parquet');

A bucket file is Xet content — no pointer, no branch, no commit — so a bucket write chunks the file, and uploads it:

COPY (SELECT * FROM large_table) TO 'hf://buckets/me/playground/export.parquet' (FORMAT PARQUET);
COPY (SELECT * FROM events) TO 'hf://buckets/me/playground/events/' (FORMAT PARQUET, PARTITION_BY (day));

hf_bucket_copy enables free file copies since files are pointers in Xet.

Authentication

  • with the hf CLI:
hf auth login
  • manually with a token:
CREATE SECRET (TYPE HUGGINGFACE, TOKEN 'hf_...');

Token resolution has the same order as huggingface_hub: a matching CREATE SECRET (TYPE HUGGINGFACE, …) → SET hf_token → HF_TOKEN → HF_TOKEN_PATH → $HF_HOME/token → ~/.cache/huggingface/token.

Settings

setting default meaning
hf_token `` API token (secrets and env vars take precedence as above)
hf_endpoint https://huggingface.co Hub endpoint
hf_cache_dir `` where to buffer files before uploading (falls back to temp_directory/TMPDIR)
hf_read_buffer_size 8388608 smallest read-ahead window fetched for a hf:// file (also one range request when a window is split)
hf_read_cache_size 268435456 fetched bytes one hf:// file may keep in memory (shared by all its readers)
hf_read_cache_total_size 1073741824 fetched bytes the process keeps over all files, and how that budget splits between files open at once
hf_read_max_parallel_requests 4 range requests one missing window is pulled with (1 disables the fan-out)
hf_max_per_page 0 (=1000) page size for tree listings
hf_commit_message Upload files with DuckDB commit message for writes
hf_create_repo false create the target repository when it does not exist
hf_xet_write true send file content through Xet (content-defined, deduplicated chunks). If the content store is unreachable the write falls back to a plain LFS upload, so a dead CAS costs time, not correctness. false forces LFS
hf_write_max_parallel_uploads 3 how many of a file's xorbs are on the wire at once (1 = sequential). Each one waiting costs up to 64 MiB in memory, which is also how far ahead of the network the chunker may run
hf_xet_compression lz4 chunk payloads inside an xorb: LZ4 frames, or none. Changes bytes on the wire and nothing else — an xorb's hash covers uncompressed content, so dedup, shards and other clients are unaffected. Incompressible chunks come out ~16 B larger (stored raw inside a frame, as reference clients do). Anything else, including bg4, is refused at the SET
hf_xet_upload_dedup true ask the content store whether it already holds a chunk before uploading it, and write a pointer instead of the bytes when it does. Costs one small request per 256 chunks (plus the file's first chunk); false uploads everything and produces the byte-identical shard an upload without the feature would have written

Table functions

function result
hf_ls(path, recursive := false) url, path, is_dir, size, sha256
hf_meta(path) url, exists, size, etag, repo_type, repository, revision
hf_upload(local_path, hf_path, message := …) url, size, sha256
hf_delete(path_or_list, missing_ok := false) url — one commit per repository in the list
hf_bucket_copy(source, target[, hash_wait := n]) source, url, path, size, xet_hash, status — points a bucket at content the Hub already holds; hash_wait (default 30s) is how long to wait for the Hub to finish hashing a file committed moments ago
hf_list_revisions(path, max_revisions := 20) repository, revision, title, author, date (limit is a reserved word)
hf_list_refs(path) repository, kind, name, ref, target_commit
hf_create_repo(path, private := true) url, created
hf_status() version, DuckDB version, endpoint, token source, whoami, transport, every read/write setting — never the token
hf_read_stats(reset := false) the read path's counters: bytes_fetched / <file size> is the amplification (a scan should be 1.0), evictions says the cache is too small, retained_bytes what it still holds
hf_read_cache_clear() cleared_bytes, cleared_files
hf_xet_upload(path[, scope := 'hf://…'][, publish := bool]) xorb, num_chunks, bytes, uncompressed_bytes, inserted, xet_hash, shard_bytes, shard_hash, shard_result, published, deduped_chunks, dedup_queries, dedup_hits, dedup_shard_failures — the Xet write path by hand; inserted/shard_result are the CAS's dedup answers, scope names the repository whose token the bytes go up against, publish registers it as a bucket file, and the last four are per file (they repeat on every xorb row; a fully deduplicated upload is one row with xorb NULL)
hf_xet_selftest() 1090 rows of (check_name, assert_passed, expected, got, …) — the Xet port replayed against reference vectors generated from xet-core. Offline, no token

Limitations

  • On a released 2.0.0 client, hf_status().duckdb_version answers v2.0.0-alpha49307.
  • Writes buffer to a local temp file first and upload + commit happen on Close().
  • Dedup is strong on content that repeats and weak on content that is merely similar.
  • hf_write_max_parallel_uploads is a fixed client-side policy.
  • Small files go as base64 in the commit; larger ones use git-LFS basic multipart.
  • A repository cannot be deleted from SQL.
  • The read cache is in-memory only and retained outside DuckDB's buffer manager.
  • Sequential whole-file reads (read_blob of a big file) are behind httpfs.
  • Every request goes through DuckDB's HTTPUtil and the transport must speak https.
  • hf_xet_write's fallback cannot be made to fail from SQL.
Explore all 69 articles in today's digest

Fresh dev news, AI-digested daily. Never miss a trend at devoured.fyi