DuckDB Obsidian

February 28, 2026 · View on GitHub

A DuckDB extension for querying an Obsidian vault's Markdown notes directly from SQL.

SELECT basename, first_header, properties->'$.tags'
FROM obsidian_notes()
ORDER BY basename;

Loading the Extension

1. Install DuckDB

Download DuckDB for your platform from duckdb.org/docs/installation.

2. Download the extension

Download the .duckdb_extension file for your platform from the GitHub Releases page. Choose the file matching your DuckDB version and OS/architecture.

3. Load the extension

Since the extension is not signed by DuckDB, launch DuckDB with unsigned extensions allowed:

duckdb -unsigned

Then load the extension by its local path:

LOAD '/path/to/obsidian.duckdb_extension';

The extension also auto-loads the built-in json extension, which is required for properties column operations.

Setting the working directory

obsidian_notes() without arguments scans the current working directory. Launch DuckDB from your vault directory, or use the path argument:

cd /path/to/your/vault
duckdb -unsigned
LOAD '/path/to/obsidian.duckdb_extension';
SELECT basename, first_header FROM obsidian_notes() LIMIT 10;

Table Function

obsidian_notes()                        -- scan vault at current working directory
obsidian_notes('/path/to/vault')        -- scan vault at the given path

The function requires the path to be a valid Obsidian vault (must contain a .obsidian directory). Hidden directories (.obsidian, .git, etc.) are skipped during file discovery. All .md files in all non-hidden subdirectories are included.

Available Columns

ColumnTypeDescription
filenameVARCHARFile name including the .md extension (e.g. note.md)
basenameVARCHARFile name without the extension (e.g. note)
filepathVARCHARFull absolute path to the file
relative_pathVARCHARPath relative to the vault root (e.g. subdir/note.md)
first_headerVARCHARText of the first H1 heading; NULL if the note has no H1
headersSTRUCT(level INT, text VARCHAR)[]All headings in document order, each with their level (1–6) and text
propertiesJSONYAML frontmatter serialized as a JSON object; NULL if no frontmatter
internal_linksSTRUCT(target VARCHAR, display_name VARCHAR, header VARCHAR, block_ref VARCHAR)[]All [[wiki-links]] found in the note body and frontmatter
FieldDescription
targetThe linked note name (e.g. note_a from [[note_a]])
display_nameAlias text, or NULL (e.g. My Alias from [[note_a|My Alias]])
headerLinked heading, or NULL (e.g. Section from [[note_a#Section]])
block_refLinked block ID, or NULL (e.g. abc123 from [[note_a^abc123]])

Query Examples

List all notes with a computed title (frontmatter title → first H1 → filename):

SELECT
    basename,
    COALESCE(
        json_extract_string(properties, '$.title'),
        first_header,
        basename
    ) AS title
FROM obsidian_notes()
ORDER BY title;

Find notes by tag (frontmatter tags array):

SELECT basename
FROM obsidian_notes()
WHERE json_contains(properties->'$.tags', '"projects"')
ORDER BY basename;

Extract a specific frontmatter field:

SELECT basename, json_extract_string(properties, '$.author') AS author
FROM obsidian_notes()
WHERE properties IS NOT NULL;

Find notes containing an H2 heading:

SELECT basename
FROM obsidian_notes()
WHERE list_contains(list_transform(headers, h -> h.level), 2)
ORDER BY basename;

Find all notes linking to a specific note:

SELECT basename
FROM obsidian_notes()
WHERE list_contains(list_transform(internal_links, l -> l.target), 'some_note')
ORDER BY basename;

Build a link graph (which note links to which):

SELECT
    basename AS source,
    unnest(list_transform(internal_links, l -> l.target)) AS target
FROM obsidian_notes()
WHERE len(internal_links) > 0
ORDER BY source, target;

Find orphan notes (not linked to by any other note):

WITH links AS (
    SELECT unnest(list_transform(internal_links, l -> l.target)) AS target
    FROM obsidian_notes()
)
SELECT basename
FROM obsidian_notes()
WHERE basename NOT IN (SELECT target FROM links)
ORDER BY basename;

Development

Prerequisites

Install vcpkg (used for cmark-gfm, ryml, and other dependencies):

git clone https://github.com/Microsoft/vcpkg.git ~/.vcpkg
~/.vcpkg/bootstrap-vcpkg.sh

Install Ninja (recommended for faster builds):

brew install ninja ccache   # macOS

Set required environment variables (add to your shell profile or prefix each command):

export GEN=ninja
export VCPKG_TOOLCHAIN_PATH=~/.vcpkg/scripts/buildsystems/vcpkg.cmake

Building

make          # release build
make debug    # debug build

Output binaries:

  • ./build/release/duckdb — DuckDB shell with the extension preloaded
  • ./build/release/test/unittest — test runner
  • ./build/release/extension/obsidian/obsidian.duckdb_extension — loadable extension file

Running Tests

Always run make before make test — they are separate steps.

make test        # run all SQL logic tests against release build
make test_debug  # run tests against debug build

Run a single test file:

./build/release/test/unittest --test-dir test/sql obsidian.test

Other Commands

make format   # format C++ source files
make clean    # remove build artifacts