dirsql 0.3.70 → 0.3.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/explanation.md +5 -0
- package/docs/howto/columns-from-paths.md +63 -0
- package/docs/howto/define-tables.md +70 -0
- package/docs/howto/embed.md +196 -0
- package/docs/howto/extract-from-contents.md +90 -0
- package/docs/howto/load-extension.md +91 -0
- package/docs/howto/persist.md +52 -0
- package/docs/howto/react-to-changes.md +67 -0
- package/docs/howto/search-by-meaning.md +148 -0
- package/docs/howto/skip-files.md +60 -0
- package/docs/reference/cli.md +132 -0
- package/docs/reference/columns.md +68 -0
- package/docs/reference/config.md +166 -0
- package/docs/reference/hooks.md +141 -0
- package/docs/reference/http-api.md +120 -0
- package/docs/reference/sdk.md +357 -0
- package/package.json +11 -11
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Search documents by meaning
|
|
2
|
+
|
|
3
|
+
Ask a question in plain language and get the closest documents back — even
|
|
4
|
+
when they share no keywords with it. Three pieces you already have compose
|
|
5
|
+
into semantic search: a SQLite vector extension for the distance math, an
|
|
6
|
+
[`on-file`](../reference/hooks.md#on-file) command to embed each file at
|
|
7
|
+
index time, and a [`pre-query`](../reference/hooks.md#pre-query) command to
|
|
8
|
+
embed each question at query time.
|
|
9
|
+
|
|
10
|
+
## How the pieces fit
|
|
11
|
+
|
|
12
|
+
1. **[`[[dirsql.extension]]`](../reference/config.md#dirsql-extension)**
|
|
13
|
+
loads [`sqlite-vec`](https://github.com/asg017/sqlite-vec), making
|
|
14
|
+
`vec_distance_cosine()` callable in queries.
|
|
15
|
+
2. **`on-file`** runs an embedding command once per matched file; the
|
|
16
|
+
vector lands in an `embedding` column next to the text.
|
|
17
|
+
3. **`pre-query`** receives each raw `POST /query` body, embeds the
|
|
18
|
+
question, and prints the nearest-neighbor SQL to run.
|
|
19
|
+
|
|
20
|
+
`dirsql` never sees a model — both hooks are commands you own, so any
|
|
21
|
+
embedding model works. This guide uses
|
|
22
|
+
[`model2vec`](https://github.com/MinishLab/model2vec), a small, fast,
|
|
23
|
+
CPU-only embedding library (its `potion-base-8M` model downloads ~30 MB
|
|
24
|
+
from Hugging Face on first use).
|
|
25
|
+
|
|
26
|
+
## 1. The embedding scripts
|
|
27
|
+
|
|
28
|
+
Suppose short notes live in `notes/*.md`:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
notes/pasta.md # boiling spaghetti, olive oil, garlic
|
|
32
|
+
notes/branches.md # git feature branches and pull requests
|
|
33
|
+
notes/tomatoes.md # planting tomato seedlings after the last frost
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Next to them, `embed.py` turns one file into one row carrying its text and
|
|
37
|
+
its embedding (a JSON array, stored as TEXT — `sqlite-vec` accepts JSON
|
|
38
|
+
vectors directly):
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
"""Embed one file's text; print a dirsql row array on stdout."""
|
|
42
|
+
import json
|
|
43
|
+
import sys
|
|
44
|
+
|
|
45
|
+
from model2vec import StaticModel
|
|
46
|
+
|
|
47
|
+
path = sys.argv[1]
|
|
48
|
+
text = open(path, encoding="utf-8").read()
|
|
49
|
+
model = StaticModel.from_pretrained("minishlab/potion-base-8M")
|
|
50
|
+
vector = model.encode([text])[0]
|
|
51
|
+
print(json.dumps([{"text": text, "embedding": json.dumps([round(float(x), 6) for x in vector])}]))
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
And `search.py` turns a `{"q": "..."}` request body into SQL, embedding the
|
|
55
|
+
question with the same model:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
"""Turn a {"q": "..."} request body into a nearest-neighbor SQL query."""
|
|
59
|
+
import json
|
|
60
|
+
import sys
|
|
61
|
+
|
|
62
|
+
from model2vec import StaticModel
|
|
63
|
+
|
|
64
|
+
body = json.loads(sys.argv[1])
|
|
65
|
+
model = StaticModel.from_pretrained("minishlab/potion-base-8M")
|
|
66
|
+
vector = model.encode([body["q"]])[0]
|
|
67
|
+
needle = json.dumps([round(float(x), 6) for x in vector])
|
|
68
|
+
print(
|
|
69
|
+
"SELECT _path, ROUND(vec_distance_cosine(embedding, '%s'), 3) AS distance "
|
|
70
|
+
"FROM notes ORDER BY distance LIMIT 3" % needle
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
::: warning The hook owns SQL safety
|
|
75
|
+
Whatever SQL `pre-query` prints is executed as-is. Here the interpolated
|
|
76
|
+
value is a numeric vector the script itself produced; never splice raw
|
|
77
|
+
request text into SQL. See [`pre-query`](../reference/hooks.md#pre-query).
|
|
78
|
+
:::
|
|
79
|
+
|
|
80
|
+
## 2. Wire them up in `.dirsql.toml`
|
|
81
|
+
|
|
82
|
+
```toml
|
|
83
|
+
[dirsql]
|
|
84
|
+
pre-query = "uv run --with model2vec python search.py {args}"
|
|
85
|
+
hook-timeout = 300 # headroom for the first-run model download
|
|
86
|
+
|
|
87
|
+
[[dirsql.extension]]
|
|
88
|
+
path = "sqlite_vec" # Python module name; see note below
|
|
89
|
+
entrypoint = "sqlite3_vec_init"
|
|
90
|
+
|
|
91
|
+
[[table]]
|
|
92
|
+
ddl = "CREATE TABLE notes (_path TEXT, text TEXT, embedding TEXT)"
|
|
93
|
+
glob = "notes/*.md"
|
|
94
|
+
on-file = "uv run --with model2vec python embed.py {path}"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The extension is named by package: the Python launcher resolves the
|
|
98
|
+
installed `sqlite_vec` module to its bundled loadable. Naming rules per
|
|
99
|
+
runtime — and the literal-path alternative that works everywhere — are in
|
|
100
|
+
[Load a SQLite extension](./load-extension.md).
|
|
101
|
+
|
|
102
|
+
## 3. Start the server and ask questions
|
|
103
|
+
|
|
104
|
+
Launch with `sqlite-vec` available to the launcher's environment:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
uvx --with sqlite-vec dirsql
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
The initial scan runs `embed.py` once per note. Then ask:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
curl -s http://localhost:7117/query \
|
|
114
|
+
-H 'content-type: application/json' \
|
|
115
|
+
-d '{"q": "how do I cook pasta?"}'
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
[{"_path":"notes/pasta.md","distance":0.315},{"_path":"notes/tomatoes.md","distance":0.881},{"_path":"notes/branches.md","distance":0.92}]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
curl -s http://localhost:7117/query \
|
|
124
|
+
-H 'content-type: application/json' \
|
|
125
|
+
-d '{"q": "reviewing code on github"}'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
[{"_path":"notes/branches.md","distance":0.51},{"_path":"notes/pasta.md","distance":1.033},{"_path":"notes/tomatoes.md","distance":1.074}]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Neither question shares a keyword with its top note — "cook" appears
|
|
133
|
+
nowhere in `pasta.md`, "github" nowhere in `branches.md`. The distance
|
|
134
|
+
ranking is doing the work.
|
|
135
|
+
|
|
136
|
+
Because `pre-query` is set, the request body is *not* the usual
|
|
137
|
+
`{"sql": …}` — the raw body goes to your script, which decides what SQL
|
|
138
|
+
runs ([hook interactions](../reference/http-api.md#hook-interactions)).
|
|
139
|
+
|
|
140
|
+
## Recomputing vs. caching
|
|
141
|
+
|
|
142
|
+
Embeddings are recomputed on every startup, because the database is a
|
|
143
|
+
derived, ephemeral view of the files
|
|
144
|
+
([how `dirsql` thinks](../explanation.md)). Once the model is cached this
|
|
145
|
+
is fast for small trees; for large ones, enable persistence so unchanged
|
|
146
|
+
files keep their stored embeddings across restarts —
|
|
147
|
+
[Keep the index across restarts](./persist.md). Editing a note re-embeds
|
|
148
|
+
just that file, automatically.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Skip files you don't want indexed
|
|
2
|
+
|
|
3
|
+
Drafts, build output, and editor droppings don't belong in your tables.
|
|
4
|
+
[`ignore`](../reference/config.md#dirsql-keys) globs exclude files entirely —
|
|
5
|
+
from the initial scan and from watch events alike.
|
|
6
|
+
|
|
7
|
+
## 1. Add `ignore` patterns
|
|
8
|
+
|
|
9
|
+
Suppose finished notes live in `notes/`, but drafts and scratch files hide
|
|
10
|
+
among them:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
notes/final.md
|
|
14
|
+
notes/drafts/wip.md
|
|
15
|
+
notes/scratch.md.tmp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Exclude the noise in `.dirsql.toml`:
|
|
19
|
+
|
|
20
|
+
```toml
|
|
21
|
+
[dirsql]
|
|
22
|
+
ignore = ["notes/drafts/**", "**/*.tmp"]
|
|
23
|
+
|
|
24
|
+
[[table]]
|
|
25
|
+
ddl = "CREATE TABLE notes (_path TEXT)"
|
|
26
|
+
glob = "notes/**/*"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Patterns match against root-relative paths, the same way table globs do. An
|
|
30
|
+
ignored file never reaches any table — even one whose glob would match it.
|
|
31
|
+
|
|
32
|
+
## 2. Confirm what made it in
|
|
33
|
+
|
|
34
|
+
Start the server (`npx dirsql` / `uvx dirsql`) and check:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
curl -s http://localhost:7117/query \
|
|
38
|
+
-H 'content-type: application/json' \
|
|
39
|
+
-d '{"sql":"SELECT _path FROM notes ORDER BY _path"}'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
[{"_path":"notes/final.md"}]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Notes
|
|
47
|
+
|
|
48
|
+
- `ignore` lives in a config file, so it needs one:
|
|
49
|
+
[zero-config mode](../reference/cli.md#zero-config-mode) indexes
|
|
50
|
+
everything with no ignores.
|
|
51
|
+
- The top-level `.dirsql/` directory is always excluded, ignore list or
|
|
52
|
+
not — it is reserved for `dirsql`'s own metadata
|
|
53
|
+
([config reference](../reference/config.md#dirsql-keys)).
|
|
54
|
+
- Narrow table globs are the other half of the story: a file matching no
|
|
55
|
+
table's glob contributes no rows either. Use `ignore` for things that
|
|
56
|
+
should never be looked at; use precise globs to shape what each table
|
|
57
|
+
sees ([Define tables for your files](./define-tables.md)).
|
|
58
|
+
- Embedding `dirsql` instead? The SDK constructor takes the same patterns
|
|
59
|
+
via its `ignore` parameter
|
|
60
|
+
([SDK reference](../reference/sdk.md#constructor)).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# CLI
|
|
2
|
+
|
|
3
|
+
The `dirsql` binary has two modes:
|
|
4
|
+
|
|
5
|
+
| Invocation | Behavior |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `dirsql` (no subcommand) | Start a long-lived HTTP server exposing a SQL view of a directory. See [HTTP API](./http-api.md). |
|
|
8
|
+
| `dirsql init` | Generate a starter `.dirsql.toml` by running `claude` over a directory. |
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
::: code-group
|
|
13
|
+
|
|
14
|
+
```bash [npm]
|
|
15
|
+
npx dirsql
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```bash [PyPI]
|
|
19
|
+
uvx dirsql
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash [Cargo]
|
|
23
|
+
# The `cli` feature is opt-in; this installs the binary only.
|
|
24
|
+
cargo install dirsql --features cli
|
|
25
|
+
dirsql
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
:::
|
|
29
|
+
|
|
30
|
+
The npm launcher requires **Node ≥ 20.11**.
|
|
31
|
+
|
|
32
|
+
## Server mode
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
dirsql
|
|
36
|
+
# Running at localhost:7117
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
On startup the server prints `Running at <host>:<port>` to stdout. It runs
|
|
40
|
+
until it receives `SIGINT` (Ctrl-C) or `SIGTERM`, then drains in-flight
|
|
41
|
+
requests, closes open `/events` streams, and exits.
|
|
42
|
+
|
|
43
|
+
### Flags
|
|
44
|
+
|
|
45
|
+
| Flag | Default | Description |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `--config <path>` | `./.dirsql.toml` | Path to the [config file](./config.md). The index is rooted at the directory containing this file (unless the config sets `[dirsql].root`). When the file does not exist, the server runs in [zero-config mode](#zero-config-mode). |
|
|
48
|
+
| `--host <addr>` | `localhost` | Bind address. |
|
|
49
|
+
| `--port <n>` | `7117` | TCP port to bind. |
|
|
50
|
+
| `--extension <path>` | none | Load a SQLite extension by literal path, overriding the config's `[[dirsql.extension]]` entries. Repeatable. Format: `<path>` or `<path>::<entrypoint>`. Internal plumbing for the pip/npm launchers, which resolve package-name extensions and pass the resolved paths here — not intended for direct use. When any `--extension` is present, the config file's own extension entries are not loaded. |
|
|
51
|
+
| `--version` | | Print the version and exit. |
|
|
52
|
+
| `--help` | | Print usage and exit. |
|
|
53
|
+
|
|
54
|
+
### Defaults
|
|
55
|
+
|
|
56
|
+
- Per-query timeout: **30 seconds**. A query exceeding it returns
|
|
57
|
+
`408 Request Timeout`.
|
|
58
|
+
- Command hooks (`on-file`, `pre-query`, `post-query`) default to a
|
|
59
|
+
**30-second** timeout each, overridable with the config key
|
|
60
|
+
[`[dirsql].hook-timeout`](./config.md#dirsql-keys).
|
|
61
|
+
|
|
62
|
+
### Zero-config mode
|
|
63
|
+
|
|
64
|
+
When the config file named by `--config` does not exist, the server indexes
|
|
65
|
+
the directory that would have contained it (the current directory for the
|
|
66
|
+
default `./.dirsql.toml`) with a single table named `files`:
|
|
67
|
+
|
|
68
|
+
- Glob: `**/*` — every file under the root, at any depth, no ignores.
|
|
69
|
+
- One row per file, with all seven
|
|
70
|
+
[virtual columns](./columns.md): `_path`, `_basename`, `_dir`, `_ext`,
|
|
71
|
+
`_size`, `_mtime`, `_ctime`.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
curl -s localhost:7117/query -H 'content-type: application/json' \
|
|
75
|
+
-d '{"sql":"SELECT _basename, _size FROM files ORDER BY _size DESC LIMIT 5"}'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
A config file, when present, fully overrules this default. A *missing*
|
|
79
|
+
config is not an error; only a config that exists but fails to load is (see
|
|
80
|
+
below).
|
|
81
|
+
|
|
82
|
+
### Degraded mode
|
|
83
|
+
|
|
84
|
+
When the config file exists but cannot be resolved or loaded (unreadable
|
|
85
|
+
path, invalid TOML, schema errors), the server still starts and binds, but
|
|
86
|
+
every request to `/query` and `/events` returns `503 Service Unavailable`
|
|
87
|
+
with a JSON body describing the failure:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{"error": "failed to load config: ..."}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Exit codes
|
|
94
|
+
|
|
95
|
+
| Code | Meaning |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `0` | Clean shutdown after `SIGINT` / `SIGTERM`. |
|
|
98
|
+
| `1` | Failed to bind `host:port`, or an error during shutdown. |
|
|
99
|
+
|
|
100
|
+
## `dirsql init`
|
|
101
|
+
|
|
102
|
+
Generates a `.dirsql.toml` by running the `claude` CLI over the target
|
|
103
|
+
directory. The generated config contains only filesystem-fact tables
|
|
104
|
+
(`[[table]]` entries whose columns come from [glob captures and virtual
|
|
105
|
+
columns](./columns.md)) — never content-derived columns.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
dirsql init
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Flags
|
|
112
|
+
|
|
113
|
+
| Flag | Default | Description |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `--root <path>` | current directory | Directory to scan. |
|
|
116
|
+
| `--output <path>` | `<root>/.dirsql.toml` | Where to write the generated config. |
|
|
117
|
+
| `--force` | off | Overwrite the output file if it already exists. |
|
|
118
|
+
|
|
119
|
+
### Requirements and failure modes
|
|
120
|
+
|
|
121
|
+
`init` requires `claude` on `PATH`, signed in; there is no separate API key.
|
|
122
|
+
All failures exit `1` with a message on stderr:
|
|
123
|
+
|
|
124
|
+
| Condition | Behavior |
|
|
125
|
+
|---|---|
|
|
126
|
+
| Output file exists and `--force` not passed | Fails before invoking `claude` (no LLM call is made). |
|
|
127
|
+
| `claude` not found on `PATH` | Fails with a pointer to the Claude Code install docs. |
|
|
128
|
+
| `claude` exits non-zero | Fails with `claude`'s stderr; no partial config is written. |
|
|
129
|
+
| `claude` produces non-UTF-8 output | Fails; nothing is written. |
|
|
130
|
+
|
|
131
|
+
On success, `claude`'s stdout is written verbatim to the output path and
|
|
132
|
+
`init` exits `0`.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Virtual columns and glob captures
|
|
2
|
+
|
|
3
|
+
Every table — config-defined or programmatic — gets filesystem facts merged
|
|
4
|
+
onto its rows automatically: seven reserved **virtual columns** derived from
|
|
5
|
+
the file's path and stat metadata, plus one column per **`{name}` capture**
|
|
6
|
+
in the table's glob.
|
|
7
|
+
|
|
8
|
+
Facts are **opt-in by DDL**: only facts whose name appears as a column in
|
|
9
|
+
the table's `CREATE TABLE` are populated; the rest are silently dropped.
|
|
10
|
+
Declaring them requires nothing else.
|
|
11
|
+
|
|
12
|
+
## Virtual columns
|
|
13
|
+
|
|
14
|
+
| Column | Type | Value |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `_path` | TEXT | The file's path relative to the scan root (e.g. `posts/hello.md`). |
|
|
17
|
+
| `_basename` | TEXT | The filename, including extension (`hello.md`). |
|
|
18
|
+
| `_dir` | TEXT | The parent directory relative to the root (`posts`); the empty string for files directly under the root. |
|
|
19
|
+
| `_ext` | TEXT | The file extension without the leading dot (`md`). Original case is preserved — `Photo.JPG` yields `JPG`; use `LOWER(_ext)` for case-insensitive matching. `NULL` when the file has no extension. |
|
|
20
|
+
| `_size` | INTEGER | File size in bytes. |
|
|
21
|
+
| `_mtime` | INTEGER | Last-modified time, Unix seconds. |
|
|
22
|
+
| `_ctime` | INTEGER | Creation (birth) time, Unix seconds. `NULL` when the platform or filesystem cannot supply it. |
|
|
23
|
+
|
|
24
|
+
A fact that cannot be computed (an unreadable file's `_size`/`_mtime`/
|
|
25
|
+
`_ctime`, a missing extension's `_ext`) is absent from the row: `NULL` in
|
|
26
|
+
the default relaxed mode, a missing-column error for a
|
|
27
|
+
[`strict`](./config.md#table) table that declares it.
|
|
28
|
+
|
|
29
|
+
```sql
|
|
30
|
+
SELECT _basename, _size
|
|
31
|
+
FROM posts
|
|
32
|
+
WHERE _mtime > strftime('%s', '2024-01-01')
|
|
33
|
+
ORDER BY _mtime DESC;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Glob captures
|
|
37
|
+
|
|
38
|
+
A `{name}` segment in a table's glob captures part of each matched path as
|
|
39
|
+
a TEXT column named `name`:
|
|
40
|
+
|
|
41
|
+
```toml
|
|
42
|
+
[[table]]
|
|
43
|
+
ddl = "CREATE TABLE comments (thread_id TEXT, _basename TEXT, _mtime INTEGER)"
|
|
44
|
+
glob = "_comments/{thread_id}/*.jsonl"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
A file at `_comments/abc123/2024-05-05.jsonl` produces a row with
|
|
48
|
+
`thread_id = "abc123"`.
|
|
49
|
+
|
|
50
|
+
- A capture name must be a valid identifier: a letter or underscore
|
|
51
|
+
followed by letters, digits, or underscores (`[a-zA-Z_][a-zA-Z0-9_]*`).
|
|
52
|
+
- A capture matches **within a single path segment** — one or more
|
|
53
|
+
characters, never a `/`. For matching purposes, `{name}` behaves like
|
|
54
|
+
`*`.
|
|
55
|
+
- A glob may contain multiple captures (`{year}/{month}/*.jpg`).
|
|
56
|
+
- Like virtual columns, a capture populates a column only when the DDL
|
|
57
|
+
declares a column of the same name.
|
|
58
|
+
|
|
59
|
+
## Precedence
|
|
60
|
+
|
|
61
|
+
Values produced by a table's own row source — an `on-file` command's JSON
|
|
62
|
+
output or an SDK `extract` callback's return value — **win** over
|
|
63
|
+
auto-injected facts of the same name. An extract that explicitly emits
|
|
64
|
+
`_path` is honored.
|
|
65
|
+
|
|
66
|
+
Injection order per row: virtual columns first, then glob captures, then
|
|
67
|
+
the row source's own values, each layer overwriting the previous, all
|
|
68
|
+
filtered to the DDL's declared columns.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# Configuration file (`.dirsql.toml`)
|
|
2
|
+
|
|
3
|
+
`.dirsql.toml` is a TOML file with one optional `[dirsql]` section, zero or
|
|
4
|
+
more `[[dirsql.extension]]` entries, and zero or more `[[table]]` entries.
|
|
5
|
+
An empty file is valid. A missing `[dirsql]` section behaves as an
|
|
6
|
+
all-defaults one. Unknown keys are ignored.
|
|
7
|
+
|
|
8
|
+
The [CLI](./cli.md) loads `./.dirsql.toml` by default (`--config <path>`
|
|
9
|
+
overrides). The [SDKs](./sdk.md) load a config via the `config` constructor
|
|
10
|
+
parameter.
|
|
11
|
+
|
|
12
|
+
**Path resolution.** Relative paths in the config (`root`, `persist_path`,
|
|
13
|
+
`[[dirsql.extension]]` `path`) resolve against the config file's parent
|
|
14
|
+
directory. The scan root defaults to that same directory when `root` is not
|
|
15
|
+
set.
|
|
16
|
+
|
|
17
|
+
## `[dirsql]` keys
|
|
18
|
+
|
|
19
|
+
| Key | Type | Default | Description |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| `root` | string | config file's parent directory | Directory to index. Relative values resolve against the config file's parent. An explicit `root` passed to an SDK constructor overrides this (a warning is emitted on stderr). |
|
|
22
|
+
| `ignore` | array of strings | `[]` | Glob patterns matched against root-relative paths. Matched files are skipped entirely — excluded from the initial scan and from watch events. |
|
|
23
|
+
| `persist` | boolean | `false` | Keep the SQLite index on disk between runs. When `false`, the index is ephemeral: rebuilt from your files on every startup and discarded on exit. |
|
|
24
|
+
| `persist_path` | string | `<root>/.dirsql/cache.db` | Location of the on-disk cache. Relative values resolve against the config file's parent. Ignored unless `persist = true`. |
|
|
25
|
+
| `pre-query` | string | none | Server-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](./hooks.md#pre-query). |
|
|
26
|
+
| `post-query` | string | none | Server-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](./hooks.md#post-query). |
|
|
27
|
+
| `hook-timeout` | integer (seconds) | `30` | One 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](./hooks.md#timeout). |
|
|
28
|
+
|
|
29
|
+
The top-level `.dirsql/` directory under the root is always excluded from
|
|
30
|
+
scanning, whether or not it appears in `ignore` — it is reserved for
|
|
31
|
+
`dirsql`'s own metadata (the persist cache lives there by default). Only the
|
|
32
|
+
top-level `.dirsql/` is reserved; a nested `sub/.dirsql/` is an ordinary
|
|
33
|
+
directory.
|
|
34
|
+
|
|
35
|
+
```toml
|
|
36
|
+
[dirsql]
|
|
37
|
+
root = "../data"
|
|
38
|
+
ignore = ["node_modules/**", ".git/**"]
|
|
39
|
+
persist = true
|
|
40
|
+
persist_path = ".dirsql/cache.db" # the default; shown for illustration
|
|
41
|
+
hook-timeout = 300
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## `[[dirsql.extension]]`
|
|
45
|
+
|
|
46
|
+
Each entry declares a SQLite extension to load at startup. Extensions are
|
|
47
|
+
loaded onto the connection before any `CREATE TABLE` runs; loading is
|
|
48
|
+
enabled only for the duration of each load and disabled again afterwards, so
|
|
49
|
+
the SQL `load_extension()` function is never exposed to queries.
|
|
50
|
+
|
|
51
|
+
| Key | Required | Description |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `path` | yes (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](#package-name-resolution) below. |
|
|
54
|
+
| `entrypoint` | no | Init-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`). |
|
|
55
|
+
|
|
56
|
+
```toml
|
|
57
|
+
[[dirsql.extension]]
|
|
58
|
+
path = "./ext/vec0.dylib"
|
|
59
|
+
entrypoint = "sqlite3_vec_init"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Package-name resolution
|
|
63
|
+
|
|
64
|
+
A `path` naming a package is resolved from the
|
|
65
|
+
installed package in the runtime environment: the Python launcher and SDK
|
|
66
|
+
use `importlib`, the Node launcher and SDK use `require.resolve` against
|
|
67
|
+
`node_modules`. Resolution is file-first (a same-named local file wins) and
|
|
68
|
+
errors when the package contains zero or multiple loadables for the current
|
|
69
|
+
platform. The **standalone Rust binary and the Rust SDK are
|
|
70
|
+
file-path-only** — they have no interpreter to resolve package names with.
|
|
71
|
+
|
|
72
|
+
The name must be what the runtime's resolver knows, which is not always the
|
|
73
|
+
name you installed:
|
|
74
|
+
|
|
75
|
+
- **Python** resolves the **importable module name** — underscores, not the
|
|
76
|
+
pip distribution name. `pip install sqlite-vec` is loaded as
|
|
77
|
+
`path = "sqlite_vec"`; `path = "sqlite-vec"` does not resolve.
|
|
78
|
+
- **Node** resolves the package whose install actually **contains the
|
|
79
|
+
loadable**. Meta-packages that split binaries per platform resolve via the
|
|
80
|
+
platform package — e.g. `npm install sqlite-vec` is loaded as
|
|
81
|
+
`path = "sqlite-vec-linux-x64"` (matching your platform), not
|
|
82
|
+
`path = "sqlite-vec"`, whose meta-package ships no loadable.
|
|
83
|
+
|
|
84
|
+
Extensions add **functions** callable in queries and in a table's DDL. An
|
|
85
|
+
extension-backed **virtual table** cannot be declared as a `[[table]]` —
|
|
86
|
+
`dirsql` tables are per-file row tables, so a `CREATE VIRTUAL TABLE` DDL is
|
|
87
|
+
rejected; call the extension's functions in queries instead.
|
|
88
|
+
|
|
89
|
+
## `[[table]]`
|
|
90
|
+
|
|
91
|
+
Each entry maps a glob pattern to a SQL table. Every matched file produces
|
|
92
|
+
rows whose columns come from filesystem facts — [glob captures and virtual
|
|
93
|
+
columns](./columns.md) — plus, when `on-file` is set, the output of a
|
|
94
|
+
per-file command.
|
|
95
|
+
|
|
96
|
+
| Key | Required | Description |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `ddl` | yes | A 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. |
|
|
99
|
+
| `glob` | yes | Glob pattern matched against root-relative paths. May contain `{name}` [capture segments](./columns.md#glob-captures). First matching table wins when a file matches several globs. |
|
|
100
|
+
| `strict` | no (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/extract output, a glob capture, or a virtual column). When `false`, extra keys are dropped and missing columns become `NULL`. |
|
|
101
|
+
| `on-file` | no | A 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](./hooks.md#on-file). |
|
|
102
|
+
|
|
103
|
+
Without `on-file`, a table produces exactly one row per matched file, built
|
|
104
|
+
entirely from filesystem facts. Content interpretation (frontmatter, JSON
|
|
105
|
+
fields, CSV parsing) is out of scope for plain config tables — use
|
|
106
|
+
`on-file`, or a programmatic [SDK table](./sdk.md#table) with an `extract`
|
|
107
|
+
callback.
|
|
108
|
+
|
|
109
|
+
```toml
|
|
110
|
+
[[table]]
|
|
111
|
+
ddl = "CREATE TABLE comments (thread_id TEXT, _basename TEXT, _mtime INTEGER)"
|
|
112
|
+
glob = "_comments/{thread_id}/*.jsonl"
|
|
113
|
+
|
|
114
|
+
[[table]]
|
|
115
|
+
ddl = "CREATE TABLE papers (paper_id TEXT, title TEXT)"
|
|
116
|
+
glob = "**/meta.json"
|
|
117
|
+
on-file = "uv run python extract_papers.py {path}"
|
|
118
|
+
strict = true
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `on-file` row mapping
|
|
122
|
+
|
|
123
|
+
The command prints a JSON array of objects; each object becomes one row.
|
|
124
|
+
JSON values map to SQLite as: `null` → `NULL`; `true`/`false` → `1`/`0`; an
|
|
125
|
+
integral number → `INTEGER`, any other number → `REAL`; a string → `TEXT`; a
|
|
126
|
+
nested array or object → its JSON text as `TEXT`.
|
|
127
|
+
|
|
128
|
+
Filesystem facts are still merged onto every `on-file` row; a column emitted
|
|
129
|
+
by the command wins over a same-named fact. Output that is not a JSON array
|
|
130
|
+
of objects is a per-file failure: the file is skipped with a stderr warning
|
|
131
|
+
and the scan continues (see [failure semantics](./hooks.md#failure-semantics)).
|
|
132
|
+
|
|
133
|
+
## Parse errors
|
|
134
|
+
|
|
135
|
+
Loading fails (the CLI enters [degraded mode](./cli.md#degraded-mode); the
|
|
136
|
+
SDKs raise/reject) when:
|
|
137
|
+
|
|
138
|
+
- The TOML is malformed.
|
|
139
|
+
- A `[[table]]` entry omits `ddl` or `glob`.
|
|
140
|
+
- A `[[dirsql.extension]]` entry omits `path`, or `path` is empty.
|
|
141
|
+
- `on-file`, `pre-query`, or `post-query` is present but empty/whitespace.
|
|
142
|
+
- `hook-timeout` is zero or negative.
|
|
143
|
+
|
|
144
|
+
## Full example
|
|
145
|
+
|
|
146
|
+
```toml
|
|
147
|
+
[dirsql]
|
|
148
|
+
ignore = ["node_modules/**", ".git/**", "dist/**"]
|
|
149
|
+
persist = true
|
|
150
|
+
pre-query = "uv run python to_sql.py {args}"
|
|
151
|
+
post-query = "jq -c '{results: .}'"
|
|
152
|
+
hook-timeout = 120
|
|
153
|
+
|
|
154
|
+
[[dirsql.extension]]
|
|
155
|
+
path = "sqlite_vec" # Python module name; on Node use the
|
|
156
|
+
# platform package, e.g. sqlite-vec-linux-x64
|
|
157
|
+
entrypoint = "sqlite3_vec_init"
|
|
158
|
+
|
|
159
|
+
[[table]]
|
|
160
|
+
ddl = "CREATE TABLE comments (thread_id TEXT, _basename TEXT, _mtime INTEGER)"
|
|
161
|
+
glob = "_comments/{thread_id}/*.jsonl"
|
|
162
|
+
|
|
163
|
+
[[table]]
|
|
164
|
+
ddl = "CREATE TABLE documents (_path TEXT, _basename TEXT, _size INTEGER)"
|
|
165
|
+
glob = "**/index.md"
|
|
166
|
+
```
|