Michael Collard33 downloadsRender SQLite queries as markdown tables via fenced code blocks.
An Obsidian plugin that runs SQLite queries against local databases and renders the results as tables in reading view. Write SQL in a fenced code block, get a live table.
Add a fenced sql (or sqlite) code block to any note:
```sql
db: /var/lib/jellyfin/data/jellyfin.db
SELECT name, department, salary
FROM employees
WHERE department = 'Engineering'
ORDER BY salary DESC;
```
Or use an ODBC-style connection string with an ObsSync block:
```ObsSync
DRIVER=sqlite;DATABASE=/var/lib/jellyfin/data/jellyfin.db;QUERY=SELECT name, department, salary FROM employees WHERE department = 'Engineering' ORDER BY salary DESC
```
Switch to reading view — the code block is replaced with a rendered table:
| name | department | salary |
|---|---|---|
| Eve Johnson | Engineering | 140000 |
| Carol Davis | Engineering | 130000 |
| Alice Chen | Engineering | 125000 |
| Hank Brown | Engineering | 115000 |
A footer row shows the database filename, when the data was retrieved, and the row count. Click 📂 or 🔄 in the footer to manually refresh.
sql / sqlitedb: /absolute/path/to/database.db
SELECT ...;
db: — path to the SQLite database file. Absolute paths or vault-relative paths.SELECT, WITH ... SELECT, or EXPLAIN QUERY PLAN.INSERT, UPDATE, DELETE, DROP, etc.) are blocked.ObsSync (ODBC-style)DRIVER=sqlite;DATABASE=/absolute/path/to/database.db;QUERY=SELECT ...
DRIVER= — optional, defaults to sqlite. Currently only sqlite is supported.DATABASE= — path to the SQLite database file. Absolute paths or vault-relative paths.QUERY= — must be last; its value extends to end of the block, so semicolons inside the SQL are safe.REFRESH: directive (see below).REFRESH:15m re-runs the query every N time units while the note is open.Add a /* REFRESH:... */ comment anywhere in the block to control when the query runs:
| Value | Behaviour |
|---|---|
| (omitted) | Auto — query runs on every render (default) |
REFRESH:AUTO |
Same as default |
REFRESH:MANUAL |
Show cache on render; live query only on 🔄 click |
REFRESH:15m |
Auto on render + re-query every 15 minutes |
Time units: ms · s / sec(s) / second(s) · m / min(s) / minute(s) · h / hr(s) / hour(s) · d / day(s). Decimal values (1.5h) are valid.
Examples:
```sql
db: /var/lib/jellyfin/data/jellyfin.db
SELECT Type, count(*) FROM BaseItems GROUP BY Type;
/* REFRESH:5m */
```
```sql
db: /path/to/large.db
SELECT * FROM expensive_view;
/* REFRESH:MANUAL */
```
After a successful query, the plugin writes a /* REFRESH:... | CACHE:{...} */ comment to the end of the code block. This is invisible in reading view but preserved in the markdown source:
```sql
db: /var/lib/jellyfin/data/jellyfin.db
SELECT Type, count(*) FROM BaseItems GROUP BY Type;
/* REFRESH:5m | CACHE:{"columns":["Type","count(*)"],"rows":[...],"dbName":"jellyfin.db","timestamp":"2026-06-08 14:00:00","rowCount":7} */
```
The cache is updated on each successful query. When the database is unreachable, the last cached result is displayed. If no REFRESH: directive is present, the comment is just /* CACHE:{...} */ (backward compatible).
npm install
npm run build
Copy main.js, manifest.json, and styles.css into your vault:
.obsidian/plugins/obs-sqlite-md/
├── main.js
├── manifest.json
└── styles.css
Enable the plugin in Settings → Community plugins.
npm run dev # watch mode — rebuilds on file changes
fs to read database files from disk. Absolute paths and vault-relative paths both work.main.js at build time. No separate .wasm file to manage.Uint8Array. Large databases (hundreds of MB) will use proportional memory.sql, sqlite, and ObsSync as code block languages.fs is required dynamically at runtime (never at module load), so the plugin loads safely on mobile without crashing.window.setTimeout chain; the chain stops automatically when el.isConnected is false (note closed or navigated away).ARCHITECTURE.md for full design details.MIT