{"slug": "sql-like-queries-in-fsrs-plugin-for-obsidian", "title": "SQL-like Queries in FSRS Plugin for Obsidian", "summary": "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.", "body_md": "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.\n\nMy [FSRS](https://community.obsidian.md/plugins/fsrs) plugin now has a query language resembling SQL. It turns a markdown block into a live table that updates with every review.\n\nfsrs-table\nSELECT file as \"Note\",\n       r as \"Retrievability\",\n       date_format(due, '%d.%m.%Y') as \"Due\"\nWHERE r < 0.7\nORDER BY r ASC\nLIMIT 20\n→ the table shows the 20 most \"forgotten\" cards, sorted by retrieval probability.\n\nInitially 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.\n\nHigh-level test coverage in TypeScript made it easy to iterate on functionality located in the WASM module via an AI agent.\n\nWhen faced with dual-language testing (TypeScript + Rust), the artificial intelligence prefers to do the job properly rather than fake it.\n\nAfter implementing the **lexer → parser → AST → evaluator** pipeline for numeric values, I extended it to strings, added filtering via WHERE, then functions.\n\nExtending the syntax or adding a function came down to a single request to the agent — and a feasibility check.\n\n`fsrs-table`\n\n`SELECT`\n\n`AS`\n\n.`WHERE`\n\n`=`\n\n, `!=`\n\n, `<`\n\n, `>`\n\n, `<=`\n\n, `>=`\n\n, `AND`\n\n, `OR`\n\n.`ORDER BY`\n\n`ASC`\n\n) or descending (`DESC`\n\n).`LIMIT`\n\n`date_format()`\n\n`due`\n\ndate to any text format.**Available fields:**\n\n| Field (alias) | Type | Description |\n|---|---|---|\n`file` |\nstring | path to the note |\n`due` |\ndate | next review date |\n`stability` (s) |\nnumber | stability in days |\n`difficulty` (d) |\nnumber | difficulty |\n`retrievability` (r) |\nnumber | probability of recall (0…1) |\n`reps` |\nnumber | total number of reviews |\n`state` |\nstring | New, Learning, Review, or Relearning |\n`elapsed` |\nnumber | days since last review |\n`scheduled` |\nnumber | scheduled interval in days |\n\n`fsrs-table`\n\nCan't Do (and Shouldn't)\n`JOIN`\n\n, aggregations (`COUNT`\n\n, `SUM`\n\n…).`INSERT`\n\n, `UPDATE`\n\n, `DELETE`\n\n).`LIMIT`\n\ndoesn'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.\n\nAll query processing happens inside **Rust/WASM**:\n\n`SELECT`\n\n, `WHERE`\n\n, `LIMIT`\n\n, identifiers, operators).\n\n```\n// simplified: WHERE clause AST\npub enum Expression {\n    Comparison {\n        field: String,\n        operator: ComparisonOp,\n        value: Value,\n    },\n    Logical {\n        left: Box<Expression>,\n        operator: LogicalOp,  // AND or OR\n        right: Box<Expression>,\n    },\n}\n```\n\nThe parser is hand-written (not `nom`\n\n/`pest`\n\n) to keep full control over error messages. On an invalid query, the plugin shows a readable message: \"Unknown field: retriv\".\n\n**Why not SQLite?**\n\nSQLite 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.\n\nThe card cache lives inside WASM. On the first vault scan, the plugin computes `stability`\n\n, `difficulty`\n\n, `due`\n\n, and `retrievability`\n\nfor each card. Subsequent queries work off this cache.\n\nOn a vault with **5,000** cards, end-to-end from UI action to displayed table:\n\n`r`\n\n— another `LIMIT`\n\nadds no gain, but 0.07 s is imperceptible to the user anyway.All fields (`stability`\n\n, `difficulty`\n\n, `retrievability`\n\n) are computed on the fly from review history (stored in YAML frontmatter). Each answer recalculates only one card — cost < 0.01 s.\n\n```\nSELECT file, r as \"Probability\", date_format(due, '%d.%m')\nWHERE r >= 0.3 AND r <= 0.7\nORDER BY r ASC\nLIMIT 15\nSELECT file, d as \"Difficulty\", s as \"Stability (days)\"\nWHERE d > 5.0 AND state = \"Review\"\nORDER BY d DESC\nSELECT file, date_format(due, '%d.%m.%Y')\nWHERE due < '2026-06-01_00:00'\nORDER BY due ASC\nSELECT file, reps\nWHERE state = \"New\"\n```\n\nThe 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.\n\nThe plugin is already available in the Obsidian community catalog. Install it, try it out, and write your own queries.\n\nOr clone the plugin [repository](https://github.com/Evgene-Kopylov/fsrs_plugin) and check if you really can extend the SQL functionality with a single prompt to an agent.\n\n*Evgene Kopylov, 2026*", "url": "https://wpnews.pro/news/sql-like-queries-in-fsrs-plugin-for-obsidian", "canonical_source": "https://dev.to/evgenekopylov/sql-like-queries-in-fsrs-plugin-for-obsidian-5ac8", "published_at": "2026-05-30 21:38:10+00:00", "updated_at": "2026-05-30 22:11:53.554261+00:00", "lang": "en", "topics": ["ai-tools", "ai-products", "artificial-intelligence"], "entities": ["FSRS", "Obsidian", "TypeScript", "Rust", "WASM"], "alternates": {"html": "https://wpnews.pro/news/sql-like-queries-in-fsrs-plugin-for-obsidian", "markdown": "https://wpnews.pro/news/sql-like-queries-in-fsrs-plugin-for-obsidian.md", "text": "https://wpnews.pro/news/sql-like-queries-in-fsrs-plugin-for-obsidian.txt", "jsonld": "https://wpnews.pro/news/sql-like-queries-in-fsrs-plugin-for-obsidian.jsonld"}}