duckdb_ledger

February 6, 2026 · View on GitHub

A DuckDB extension that exposes ledger-cli journal files as SQL virtual tables. Query your plain-text accounting data with the full power of SQL.

Quick Start

# Install DuckDB if you don't have it
brew install duckdb   # macOS

# Install the extension (one-time)
duckdb -unsigned -c "
  INSTALL '/path/to/duckdb_ledger/build/release/extension/duckdb_ledger/duckdb_ledger.duckdb_extension';
"

# Query your journal
duckdb -unsigned -c "
  LOAD duckdb_ledger;
  SELECT payee, sum(amount) as total
  FROM ledger_postings('~/finances/main.journal')
  WHERE account LIKE 'Expenses:%'
  GROUP BY payee ORDER BY total DESC;
"

Usage

The -unsigned flag is required because this is a locally-built extension without an official signature.

Interactive session

duckdb -unsigned
LOAD duckdb_ledger;

-- Now query any ledger journal file
SELECT * FROM ledger_postings('~/finances/main.journal') LIMIT 10;
SELECT * FROM ledger_transactions('~/finances/main.journal') LIMIT 10;

One-liner from the command line

duckdb -unsigned -c "
  LOAD duckdb_ledger;
  SELECT date, payee, account, amount
  FROM ledger_postings('~/finances/main.journal')
  WHERE date >= '2024-01-01';
"

Export to CSV or Parquet

duckdb -unsigned -c "
  LOAD duckdb_ledger;
  COPY (SELECT * FROM ledger_postings('~/finances/main.journal'))
  TO 'postings.parquet' (FORMAT PARQUET);
"

Table Functions

ledger_postings(filepath)

Returns one row per posting. This is the most granular view of your journal.

ColumnTypeDescription
txn_idINTEGERAuto-incrementing transaction ID
dateDATETransaction date
aux_dateDATEAuxiliary/effective date (nullable)
statusVARCHARcleared, pending, or uncleared
codeVARCHARCheck number or reference code (nullable)
payeeVARCHARPayee/description
txn_noteVARCHARTransaction-level note (nullable)
uuidVARCHARUUID tag if present (nullable)
accountVARCHARFull account name (e.g., Expenses:Food)
amountDECIMAL(18,8)Posting amount (balancing amounts computed automatically)
commodityVARCHARCurrency/commodity symbol (e.g., $, EUR)
is_virtualBOOLEANTrue for virtual postings (Account) and [Account]
is_balance_assignmentBOOLEANTrue for balance assignments (= \$500)
cost_amountDECIMAL(18,8)Cost amount for @/@@ postings (nullable)
cost_commodityVARCHARCost commodity (nullable)
cost_is_totalBOOLEANTrue if cost was specified with @@ (total cost)
post_noteVARCHARPosting-level note (nullable)
source_fileVARCHARSource journal file path
line_numberBIGINTLine number in source file

ledger_transactions(filepath)

Returns one row per transaction. Useful for transaction-level analysis.

ColumnTypeDescription
txn_idINTEGERAuto-incrementing transaction ID
dateDATETransaction date
aux_dateDATEAuxiliary/effective date (nullable)
statusVARCHARcleared, pending, or uncleared
codeVARCHARCheck number or reference code (nullable)
payeeVARCHARPayee/description
noteVARCHARTransaction note (nullable)
uuidVARCHARUUID tag if present (nullable)
num_postingsINTEGERNumber of postings in this transaction
source_fileVARCHARSource journal file path
line_numberBIGINTLine number in source file
metadataVARCHARJSON object of all tags/metadata (nullable)

Examples

Monthly expense breakdown

SELECT
    date_trunc('month', date) AS month,
    split_part(account, ':', 2) AS category,
    sum(amount) AS total
FROM ledger_postings('finances.journal')
WHERE account LIKE 'Expenses:%'
GROUP BY month, category
ORDER BY month DESC, total DESC;

Top payees by spend

SELECT payee, sum(amount) AS total_spent
FROM ledger_postings('finances.journal')
WHERE account LIKE 'Expenses:%'
GROUP BY payee
ORDER BY total_spent DESC
LIMIT 20;

Account balances

SELECT account, commodity, sum(amount) AS balance
FROM ledger_postings('finances.journal')
GROUP BY account, commodity
HAVING sum(amount) != 0
ORDER BY account;

Uncleared transactions

SELECT date, payee, sum(amount) AS total
FROM ledger_postings('finances.journal')
WHERE status = 'uncleared' AND account LIKE 'Expenses:%'
GROUP BY txn_id, date, payee
ORDER BY date DESC;

Monthly income vs expenses

SELECT
    date_trunc('month', date) AS month,
    sum(CASE WHEN account LIKE 'Income:%' THEN -amount ELSE 0 END) AS income,
    sum(CASE WHEN account LIKE 'Expenses:%' THEN amount ELSE 0 END) AS expenses,
    sum(CASE WHEN account LIKE 'Income:%' THEN -amount ELSE 0 END)
      - sum(CASE WHEN account LIKE 'Expenses:%' THEN amount ELSE 0 END) AS savings
FROM ledger_postings('finances.journal')
GROUP BY month
ORDER BY month DESC;

Multi-currency holdings

SELECT account, commodity, sum(amount) AS balance
FROM ledger_postings('finances.journal')
WHERE account LIKE 'Assets:%'
GROUP BY account, commodity
HAVING sum(amount) != 0
ORDER BY account, commodity;

Spending trend (rolling 30-day average)

WITH daily AS (
    SELECT date, sum(amount) AS daily_total
    FROM ledger_postings('finances.journal')
    WHERE account LIKE 'Expenses:%'
    GROUP BY date
)
SELECT date, daily_total,
    avg(daily_total) OVER (ORDER BY date ROWS BETWEEN 29 PRECEDING AND CURRENT ROW) AS rolling_avg
FROM daily
ORDER BY date DESC
LIMIT 90;

Export to CSV or Parquet

COPY (
    SELECT date, payee, account, amount, commodity
    FROM ledger_postings('finances.journal')
) TO 'postings.csv' (HEADER);

COPY (
    SELECT * FROM ledger_postings('finances.journal')
) TO 'postings.parquet' (FORMAT PARQUET);

How It Works

The extension uses libledger to parse journal files. On each query, the journal is parsed during the bind phase and all data is materialized into DuckDB vectors. Balancing posting amounts are computed automatically via ledger's finalize() so every posting has a non-null amount.

Include directives (include other.journal) are resolved by ledger's parser, so you can query multi-file journals by pointing at the top-level file.

Claude Code Skill

This repo includes a /ledger-query skill for Claude Code that lets your coding agent query your financial data using natural language.

Install the skill

ln -s /path/to/duckdb_ledger/skills/ledger-query ~/.claude/skills/ledger-query

Use it

In any Claude Code session:

/ledger-query how much did I spend on groceries last month?
/ledger-query what are my account balances?
/ledger-query show monthly income vs expenses for 2024

Or just ask about your finances naturally — Claude will invoke the skill automatically based on context.

Building from Source

Prerequisites

macOS:

brew install boost gmp mpfr cmake ninja

Ubuntu/Debian:

sudo apt install libboost-all-dev libgmp-dev libmpfr-dev cmake ninja-build

Build

git clone --recursive https://github.com/your-username/duckdb_ledger.git
cd duckdb_ledger
GEN=ninja make release

This produces:

  • ./build/release/duckdb — DuckDB binary with the extension statically linked
  • ./build/release/extension/duckdb_ledger/duckdb_ledger.duckdb_extension — loadable extension for system DuckDB

Install into system DuckDB

duckdb -unsigned -c "INSTALL '$(pwd)/build/release/extension/duckdb_ledger/duckdb_ledger.duckdb_extension';"

After this, LOAD duckdb_ledger works in any duckdb -unsigned session.

Run tests

./build/release/test/unittest "test/sql/*"