DuckDB APFS Extension

April 27, 2026 · View on GitHub

Query your macOS file system with SQL. This DuckDB extension provides Windows Everything-like file search and analysis capabilities for macOS, powered by Spotlight index and POSIX fts(3) directory traversal.

Platform: macOS only (requires APFS/HFS+ and Spotlight service)

Functions

apfs_scan(path [, mode] [, since=TIMESTAMP])

Recursively scan a directory, returning file metadata as a table (11 columns).

ParameterTypeDefaultDescription
pathVARCHAR(required)Directory to scan
modeVARCHAR'fts''fts' (POSIX traversal, precise) or 'spotlight' (macOS index, fast)
sinceTIMESTAMP(optional)Incremental scan — only files modified after this timestamp (spotlight mode only)

apfs_search(keyword)

Instant file name search across the entire disk via Spotlight index.

ParameterTypeDescription
keywordVARCHARFile name search keyword (case-insensitive, supports wildcards)

apfs_has_changes(path, since)

Scalar function that checks whether any file in a directory has been modified since a given timestamp.

ParameterTypeDescription
pathVARCHARDirectory path
sinceTIMESTAMPCheck for changes after this timestamp
ReturnsBOOLEANtrue if changes exist, false otherwise

Return Columns

All three functions share the same 11-column schema:

ColumnTypeDescription
pathVARCHARFull file path
nameVARCHARFile name
extensionVARCHARFile extension (without .)
sizeBIGINTFile size in bytes
file_typeVARCHARfile / directory / symlink / other
modified_atTIMESTAMPLast modified time
created_atTIMESTAMPCreation time
accessed_atTIMESTAMPLast accessed time
permissionsVARCHARPermission string (e.g., rwxr-xr-x)
ownerVARCHAROwner username
depthINTEGERDepth relative to scan root

Quick Start

-- Instant search (Spotlight, milliseconds)
SELECT * FROM apfs_search('report') LIMIT 20;

-- Scan a directory
SELECT * FROM apfs_scan('/Users/max/Documents', 'fts');

-- Full disk scan via Spotlight (seconds)
CREATE TABLE files AS SELECT * FROM apfs_scan('/', 'spotlight');

-- Top 20 extensions by disk usage
SELECT extension, COUNT(*) AS cnt,
       SUM(size) / 1024 / 1024 / 1024 AS gb
FROM files
WHERE file_type = 'file'
GROUP BY extension
ORDER BY gb DESC
LIMIT 20;

-- Find large video files (> 1 GB)
SELECT name, path, size / 1024 / 1024 / 1024 AS gb
FROM files
WHERE extension IN ('mp4', 'mkv', 'avi', 'mov')
  AND size > 1024 * 1024 * 1024
ORDER BY size DESC;

-- Find potential duplicate files
SELECT name, size, COUNT(*) AS copies
FROM files
WHERE file_type = 'file'
GROUP BY name, size
HAVING copies > 1
ORDER BY size DESC;

-- Incremental scan: only recently modified files
SELECT * FROM apfs_scan('/', 'spotlight',
    since=TIMESTAMP '2026-04-01 00:00:00');

-- Check if a directory has changed since last scan
SELECT apfs_has_changes('/Users/max/Documents',
    TIMESTAMP '2026-04-01 00:00:00');

Scan Modes

ModeEngineSpeedCoveragePermission
ftsPOSIX fts(3)30-120s (full disk)All accessible filesFull Disk Access needed for protected dirs
spotlightmacOS MDQuery1-3s (full disk)Indexed files onlyNo extra permission needed

Recommendation: Use spotlight for fast full-disk scans. Use fts when you need 100% coverage or when scanning non-indexed directories like /tmp.

Building

Prerequisites

  • macOS 10.15+
  • Xcode Command Line Tools (xcode-select --install)
  • CMake 3.5+

Build from Source

git clone --recurse-submodules https://github.com/<your-username>/duckdb-apfs.git
cd duckdb-apfs

# Release build (recommended for actual use)
make

# Debug build (for development)
make debug

# Faster build with Ninja
GEN=ninja make

Build Output

BuildDuckDB CLILoadable Extension
Releasebuild/release/duckdbbuild/release/extension/apfs/apfs.duckdb_extension
Debugbuild/debug/duckdbbuild/debug/extension/apfs/apfs.duckdb_extension

The CLI binary has the extension statically linked — just run ./build/release/duckdb and start querying.

Running Tests

# Run all apfs tests (8 test files, 74 assertions)
make test

# Or run directly
build/debug/test/unittest "test/sql/apfs/*"

# Run a single test
build/debug/test/unittest "test/sql/apfs/test_apfs_scan_fts.test"

Loading as Dynamic Extension

If you have a separate DuckDB installation:

SET allow_unsigned_extensions = true;
LOAD '/path/to/apfs.duckdb_extension';

Note: The extension version must match the DuckDB version exactly.

Project Structure

duckdb-apfs/
├── src/                          # C++ extension source
│   ├── apfs_extension.cpp        # Entry point, function registration
│   ├── apfs_fts_scanner.cpp      # POSIX fts(3) traversal engine
│   ├── apfs_spotlight_scanner.cpp # Spotlight MDQuery engine + apfs_search
│   ├── apfs_has_changes.cpp      # Change detection scalar function
│   ├── apfs_utils.cpp            # Utilities (UTF-8, permissions, owner)
│   └── include/                  # Header files
├── test/sql/apfs/                # 8 sqllogictest test files
├── docs/                         # Design doc, build guide
├── duckdb/                       # DuckDB source (git submodule)
├── extension-ci-tools/           # CI tools (git submodule)
├── CMakeLists.txt
├── Makefile
└── extension_config.cmake

macOS Permissions

FunctionPermission Required
apfs_search()None
apfs_scan(path, 'spotlight')None
apfs_scan(path, 'fts')Full Disk Access (for protected directories)
apfs_has_changes()None

To grant Full Disk Access: System Settings → Privacy & Security → Full Disk Access → add your terminal app.

License

MIT