cd /news/ai-tools/sql-like-queries-in-fsrs-plugin-for-… · home topics ai-tools article
[ARTICLE · art-18794] src=dev.to pub= topic=ai-tools verified=true sentiment=↑ positive

SQL-like Queries in FSRS Plugin for Obsidian

The FSRS plugin for Obsidian now supports SQL-like queries, allowing users to filter, sort, and select spaced repetition cards using a custom query language embedded in markdown blocks. The plugin processes queries entirely in Rust/WASM, with a hand-written parser that provides readable error messages, and can filter through 5,000 cards in under 0.1 seconds.

read4 min publishedMay 30, 2026

Spaced repetition in Obsidian usually works as "show all cards with due earlier than today." That's enough for simple cases, but once you have hundreds of notes, you want to filter, sort, and select.

My FSRS plugin now has a query language resembling SQL. It turns a markdown block into a live table that updates with every review.

fsrs-table SELECT file as "Note", r as "Retrievability", date_format(due, '%d.%m.%Y') as "Due" WHERE r < 0.7 ORDER BY r ASC LIMIT 20 → the table shows the 20 most "forgotten" cards, sorted by retrieval probability.

Initially I planned to offer table settings using standard SQL syntax. But pretty quickly the syntax became a real query language, and the implementation itself — an embedded lightweight DB.

High-level test coverage in TypeScript made it easy to iterate on functionality located in the WASM module via an AI agent.

When faced with dual-language testing (TypeScript + Rust), the artificial intelligence prefers to do the job properly rather than fake it.

After implementing the lexer → parser → AST → evaluator pipeline for numeric values, I extended it to strings, added filtering via WHERE, then functions.

Extending the syntax or adding a function came down to a single request to the agent — and a feasibility check.

fsrs-table

SELECT

AS

.WHERE

=

, !=

, <

, >

, <=

, >=

, AND

, OR

.ORDER BY

ASC

) or descending (DESC

).LIMIT

date_format()

due

date to any text format.Available fields:

Field (alias) Type Description
file
string path to the note
due
date next review date
stability (s)
number stability in days
difficulty (d)
number difficulty
retrievability (r)
number probability of recall (0…1)
reps
number total number of reviews
state
string New, Learning, Review, or Relearning
elapsed
number days since last review
scheduled
number scheduled interval in days

fsrs-table

Can't Do (and Shouldn't) JOIN

, aggregations (COUNT

, SUM

…).INSERT

, UPDATE

, DELETE

).LIMIT

doesn't short-circuit processing (to guarantee the first N rows by sort order, all cards must be evaluated).This is not a database — it's a filter + sort over a cached set of cards.

All query processing happens inside Rust/WASM:

SELECT

, WHERE

, LIMIT

, identifiers, operators).

// simplified: WHERE clause AST
pub enum Expression {
    Comparison {
        field: String,
        operator: ComparisonOp,
        value: Value,
    },
    Logical {
        left: Box<Expression>,
        operator: LogicalOp,  // AND or OR
        right: Box<Expression>,
    },
}

The parser is hand-written (not nom

/pest

) to keep full control over error messages. On an invalid query, the plugin shows a readable message: "Unknown field: retriv".

Why not SQLite?

SQLite would require WASM compilation (maybe possible) and an extra synchronization layer. My implementation is lighter, needs no external dependencies, and works exclusively with data already loaded in memory.

The card cache lives inside WASM. On the first vault scan, the plugin computes stability

, difficulty

, due

, and retrievability

for each card. Subsequent queries work off this cache.

On a vault with 5,000 cards, end-to-end from UI action to displayed table:

r

— another LIMIT

adds no gain, but 0.07 s is imperceptible to the user anyway.All fields (stability

, difficulty

, retrievability

) are computed on the fly from review history (stored in YAML frontmatter). Each answer recalculates only one card — cost < 0.01 s.

SELECT file, r as "Probability", date_format(due, '%d.%m')
WHERE r >= 0.3 AND r <= 0.7
ORDER BY r ASC
LIMIT 15
SELECT file, d as "Difficulty", s as "Stability (days)"
WHERE d > 5.0 AND state = "Review"
ORDER BY d DESC
SELECT file, date_format(due, '%d.%m.%Y')
WHERE due < '2026-06-01_00:00'
ORDER BY due ASC
SELECT file, reps
WHERE state = "New"

The realization that a table configuration method had turned into a full-fledged embedded database didn't come right away. Which suggests that's how the first DBs came to be — out of a need to solve simple practical problems.

The plugin is already available in the Obsidian community catalog. Install it, try it out, and write your own queries.

Or clone the plugin repository and check if you really can extend the SQL functionality with a single prompt to an agent.

Evgene Kopylov, 2026

── more in #ai-tools 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/sql-like-queries-in-…] indexed:0 read:4min 2026-05-30 ·