Skip to content

Configuration file (.dirsql.toml)

.dirsql.toml is a TOML file with one optional [dirsql] section, zero or more [[dirsql.extension]] entries, and zero or more [[table]] entries. An empty file is valid. A missing [dirsql] section behaves as an all-defaults one. Unknown keys are a parse error at every level (top level, [dirsql], [[table]], [[dirsql.extension]]) — a typo or a removed key fails loudly, naming the offending key, rather than silently no-opping.

The CLI loads a config only when you pass it with -c/--config; with none given it serves the baked-in default (a ./.dirsql.toml on disk is not auto-loaded). The SDKs load a config via the config constructor parameter.

Path resolution. Relative paths in the config ([[dirsql.extension]]path) resolve against the config file's parent directory. The index root is not a config concern — it is decided by the runner (the CLI's invocation directory, or an SDK's explicit root), never by the config file's location. See --config.

[dirsql] keys

KeyTypeDefaultDescription
ignorearray of strings[]Glob patterns matched against root-relative paths. Matched files are skipped entirely — excluded from the initial scan and from watch events.
pre-querystringnoneServer-wide command hook: the raw POST /query request body is passed to this command as {args}, and the plain-text SQL it prints is executed instead of parsing the body as {"sql": …}. CLI server only; the SDKs ignore it. Must be non-empty. See Command hooks.
post-querystringnoneServer-wide command hook: each successful POST /query result set is handed to this command (as a JSON array on stdin, and as {args} up to 96 KiB), and the JSON body it prints is returned instead of the bare row array. CLI server only; the SDKs ignore it. Must be non-empty. See Command hooks.
hook-timeoutinteger (seconds)30One global per-run timeout for every command hook — on-file, pre-query, and post-query alike. Positive whole seconds; zero and negative values are a config error. See Command hooks.

The top-level .dirsql/ directory under the root is always excluded from scanning, whether or not it appears in ignore — it is reserved for dirsql's own metadata (the persist cache lives there by default). Only the top-level .dirsql/ is reserved; a nested sub/.dirsql/ is an ordinary directory.

toml
[dirsql]
ignore = ["node_modules/**", ".git/**"]
hook-timeout = 300

Persistence is not a config key. Keep the SQLite index on disk between runs with the --persist [PATH] CLI flag — a machine-local operational choice that belongs to the runner, not to shareable config.

[[dirsql.extension]]

Each entry declares a SQLite extension to load at startup. Extensions are loaded onto the connection before any CREATE TABLE runs; loading is enabled only for the duration of each load and disabled again afterwards, so the SQL load_extension() function is never exposed to queries.

KeyRequiredDescription
pathyes (non-empty)The extension's shared library. Either a file path (.so / .dylib / .dll; relative paths resolve against the config file's parent directory) or a bare package name (no path separator, no loadable-file suffix). The package name is runtime-specific — see package-name resolution below.
entrypointnoInit-symbol override. When omitted, SQLite derives the entry point from the filename (sqlite3_<filename>_init); set this when that default does not match (e.g. sqlite-vec's entry point is sqlite3_vec_init).
toml
[[dirsql.extension]]
path       = "./ext/vec0.dylib"
entrypoint = "sqlite3_vec_init"

Package-name resolution

A path naming a package is resolved from the installed package in the runtime environment: the Python launcher and SDK use importlib, the Node launcher and SDK use require.resolve against node_modules. Resolution is file-first (a same-named local file wins) and errors when the package contains zero or multiple loadables for the current platform. The standalone Rust binary and the Rust SDK are file-path-only — they have no interpreter to resolve package names with.

The name must be what the runtime's resolver knows, which is not always the name you installed:

  • Python resolves the importable module name — underscores, not the pip distribution name. pip install sqlite-vec is loaded as path = "sqlite_vec"; path = "sqlite-vec" does not resolve.
  • Node resolves the package whose install actually contains the loadable. Meta-packages that split binaries per platform resolve via the platform package — e.g. npm install sqlite-vec is loaded as path = "sqlite-vec-linux-x64" (matching your platform), not path = "sqlite-vec", whose meta-package ships no loadable.

Extensions add functions callable in queries and in a table's DDL. An extension-backed virtual table cannot be declared as a [[table]]dirsql tables are per-file row tables, so a CREATE VIRTUAL TABLE DDL is rejected; call the extension's functions in queries instead.

[[table]]

Each entry maps a glob pattern to a SQL table. Every matched file produces rows whose columns come from filesystem facts — glob captures and virtual columns — plus, when on-file is set, the output of a per-file command.

KeyRequiredDescription
ddlyesA SQLite CREATE TABLE statement. The table name is parsed from it. Only columns declared here are populated; auto-injected facts not in the DDL are dropped.
globyesGlob pattern matched against root-relative paths. May contain {name} capture segments. Every table whose glob matches a file receives that file's rows — a file can populate multiple tables.
strictno (default false)When true, rows whose keys do not exactly match the declared columns are rejected with an error: extra keys error, and every declared column must be supplied (by the command/on-file output, a glob capture, or a stat column). When false, extra keys are dropped and missing columns become NULL.
on-filenoA command run once per matched file; its stdout (a JSON array of row objects) becomes the file's rows. Must be non-empty. See Command hooks.

Without on-file, a table produces exactly one row per matched file, built entirely from filesystem facts. Content interpretation (frontmatter, JSON fields, CSV parsing) is out of scope for plain config tables — use on-file, or a programmatic SDK table with an on_file callback.

toml
[[table]]
ddl  = "CREATE TABLE comments (thread_id TEXT, basename TEXT, mtime INTEGER)"
glob = "_comments/{thread_id}/*.jsonl"

[[table]]
ddl     = "CREATE TABLE papers (paper_id TEXT, title TEXT)"
glob    = "**/meta.json"
on-file = "uv run python extract_papers.py {path}"
strict  = true

on-file row mapping

The command prints a JSON array of objects; each object becomes one row. JSON values map to SQLite as: nullNULL; true/false1/0; an integral number → INTEGER, any other number → REAL; a string → TEXT; a nested array or object → its JSON text as TEXT.

Filesystem facts are still merged onto every on-file row; a column emitted by the command wins over a same-named fact. Output that is not a JSON array of objects is a per-file failure: the file is skipped with a stderr warning and the scan continues (see failure semantics).

Composing multiple configs

Pass -c/--config more than once to compose several config files — a shared team config plus local overrides, or a plugin's config alongside your own:

bash
dirsql -c ./.dirsql.toml -c ~/team/embeddings.toml -c ./local.toml

The configs load and merge in argv order:

  • [[table]], ignore, and [[dirsql.extension]] entries accumulate across all configs, in order.
  • Each config's on-file, pre-query, and post-query hooks run from that config file's own directory, under that config's own hook-timeout — so a relative command like on-file = "sh ./extract.sh" resolves against the config that declared it, wherever it lives.
  • pre-query / post-query hooks chain FIFO: the request body flows through each pre-query stage in order to the final SQL, and the result rows flow through each post-query stage to the response. See the hook contract.
  • Each config is validated on its own (the parse errors below apply per file). There is no cross-file merge validation, with one structural exception: two configs defining a table of the same name is an error, naming the table.

The index root is the invocation directory regardless of where any config lives. With no -c, the baked-in default is served (no ./.dirsql.toml auto-discovery); a single -c behaves exactly as before.

Parse errors

Loading fails (the CLI enters degraded mode; the SDKs raise/reject) when:

  • The TOML is malformed.
  • Any table contains an unknown key (top level, [dirsql], [[table]], or [[dirsql.extension]]). The error names the offending key.
  • A [[table]] entry omits ddl or glob.
  • A [[dirsql.extension]] entry omits path, or path is empty.
  • on-file, pre-query, or post-query is present but empty/whitespace.
  • hook-timeout is zero or negative.

Full example

toml
[dirsql]
ignore = ["node_modules/**", ".git/**", "dist/**"]
pre-query = "uv run python to_sql.py {args}"
post-query = "jq -c '{results: .}'"
hook-timeout = 120

[[dirsql.extension]]
path       = "sqlite_vec"            # Python module name; on Node use the
                                     # platform package, e.g. sqlite-vec-linux-x64
entrypoint = "sqlite3_vec_init"

[[table]]
ddl  = "CREATE TABLE comments (thread_id TEXT, basename TEXT, mtime INTEGER)"
glob = "_comments/{thread_id}/*.jsonl"

[[table]]
ddl  = "CREATE TABLE documents (path TEXT, basename TEXT, size INTEGER)"
glob = "**/index.md"

Released under the MIT License.