dirsql 0.4.68 → 0.4.70

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.
@@ -116,8 +116,8 @@ templates, with the trigger mistakes that fail silently, are in
116
116
 
117
117
  ## Going further
118
118
 
119
- - The parser mechanics — placeholders, stdout protocol, per-file failure
120
- isolation — are the [`on-file` hook contract](../reference/hooks.md#on-file);
119
+ - The parser mechanics — placeholders, stdout protocol, failure semantics —
120
+ are the [`on-file` hook contract](../reference/hooks.md#on-file);
121
121
  [Extract rows from file contents](./extract-from-contents.md) is the fuller
122
122
  recipe.
123
123
  - Your directory layout encodes data (authors, dates, IDs)? Split the path in
@@ -2,8 +2,9 @@
2
2
 
3
3
  Paths and stat metadata only get you so far — when the columns you want live
4
4
  *inside* the files (JSON fields, frontmatter, log lines), add an
5
- [`on-file`](../reference/config.md#table) command: it runs once per matched
6
- file and its stdout becomes the file's rows.
5
+ [`on-file`](../reference/config.md#table) command: it runs once for the
6
+ table, with every matched file as an argument, and the JSON array it prints
7
+ becomes the table's rows.
7
8
 
8
9
  ## 1. Point a command at the files
9
10
 
@@ -13,8 +14,8 @@ Suppose each book is a JSON file:
13
14
  {"title": "Middlemarch", "author": "George Eliot", "year": 1871}
14
15
  ```
15
16
 
16
- Any program that reads a file and prints a **JSON array of row objects** on
17
- stdout works. With [`jq`](https://jqlang.org/):
17
+ Any program that reads the files named by its arguments and prints a **JSON
18
+ array of row objects** on stdout works. With [`jq`](https://jqlang.org/):
18
19
 
19
20
  ```toml
20
21
  [[table]]
@@ -27,8 +28,8 @@ on-file = "jq -c -n '[inputs | {title, author, year}]'"
27
28
  The command runs once for the table, with every matched file's absolute path
28
29
  appended as an argument, and prints one array for all of them — the
29
30
  [command hook contract](../reference/hooks.md#on-file), which also covers
30
- the argv splitting, working directory, stdout protocol, and timeout shared
31
- by every hook.
31
+ the argv splitting, working directory, and stdout protocol shared by every
32
+ hook.
32
33
 
33
34
  ## 2. Query the extracted columns
34
35
 
@@ -68,13 +69,21 @@ dirsql query "SELECT event, user FROM events" -c ./.dirsql.toml
68
69
  [{"event":"login","user":"alice"},{"event":"logout","user":"alice"},{"event":"login","user":"bob"}]
69
70
  ```
70
71
 
71
- ## When a file fails
72
+ ## When the command fails
72
73
 
73
- A file whose command errors (or prints something that isn't a JSON array of
74
- objects) contributes no rows: `dirsql` warns on stderr and the scan
75
- continues — one bad file never takes down the index. Details in
76
- [failure semantics](../reference/hooks.md#failure-semantics); the JSON to
77
- SQLite value mapping is under
74
+ The command runs once for the whole table, so its failure is the table's: a
75
+ non-zero exit, no output, or output that is not a JSON array of objects fails
76
+ the build, and `dirsql query` exits `1` with the command's stderr tail on
77
+ stderr and nothing on stdout.
78
+
79
+ ```
80
+ dirsql query: failed to load config: table `books`: on-file command failed: command `jq -c -n '[inputs | {title, author, year}]'` failed (exit 5): jq: error (at /home/me/library/books/broken.json:1): Cannot index string with string "title"
81
+ ```
82
+
83
+ A file the command cannot parse is the command's to handle — skip it, or
84
+ emit a row with `NULL`s — because dirsql has no per-file view of the rows.
85
+ Details in [failure semantics](../reference/hooks.md#failure-semantics); the
86
+ JSON to SQLite value mapping is under
78
87
  [`on-file` row mapping](../reference/config.md#on-file-row-mapping).
79
88
 
80
89
  ## Going further
@@ -82,8 +91,9 @@ SQLite value mapping is under
82
91
  - Just want to read a field or two out of some JSON files? SQLite's JSON
83
92
  operators work directly on a path-table's `content` column, with no config
84
93
  and no command — [Query JSON file contents](./query-json.md).
85
- - The command re-runs on every startup and on every change to a matched
86
- file. If it is expensive, [keep the index across restarts](./persist.md).
94
+ - The command re-runs on every startup and, over all of the table's files, on
95
+ every change under the table's glob. If it is expensive,
96
+ [keep the index across restarts](./persist.md).
87
97
  - Embedding `dirsql` in a program instead? The SDK's `on_file` callback
88
98
  fills the same role in-process — see
89
99
  [Embed `dirsql` in your application](./embed.md).
@@ -66,12 +66,17 @@ dirsql query "SELECT title, author FROM './posts/*.md' ORDER BY title" \
66
66
  Now the parser's output *is* the table. `--on-file` runs the command once,
67
67
  with every matched file's absolute path appended as an argument, under the
68
68
  shared [`on-file` hook contract](../reference/hooks.md#on-file) (argv
69
- splitting, timeout, and failure semantics all come from there). The stat
70
- columns are no longer reachable — a parser that wants the path emits it, since
71
- it already has the path. See
69
+ splitting and failure semantics come from there). It runs in the directory
70
+ you ran `dirsql` from, which is why `python3 extract.py` finds the script. The
71
+ stat columns are no longer reachable — a parser that wants the path emits it,
72
+ since it already has the path. See
72
73
  [Parsing rows with `--on-file`](../reference/path-tables.md#parsing-rows-with-on-file)
73
74
  for the full behavior.
74
75
 
76
+ One run, one process: on a thousand files the parser starts once, not a
77
+ thousand times, so a Python or Node parser costs its startup once per query.
78
+ That is the shape to write — loop over `sys.argv[1:]`, collect, print once.
79
+
75
80
  `--on-file` applies to every path-table in the query and may be given at most
76
81
  once. It is a `query`-only flag: there is no config file involved yet, so it is
77
82
  the fastest way to see whether a parser produces the rows you expect.
@@ -114,12 +119,74 @@ gives every path-table one parser. In both spellings the table's columns are
114
119
  exactly what the parser emits: `dirsql` merges no filesystem facts back on. A
115
120
  row that needs the file's `path` emits it (the parser has the path).
116
121
 
122
+ ## The two shapes
123
+
124
+ Every parser is one of two shapes. The array length is the row count either
125
+ way; what differs is how many files feed it.
126
+
127
+ **One file, many rows.** A lab notebook is a single Markdown file of dated
128
+ `## YYYY-MM-DD` sections, and the question is about the sections. Name the one
129
+ file as the path-table; the parser receives one path and prints one row per
130
+ section. `notebook.py`:
131
+
132
+ ```python
133
+ import json, re, sys
134
+
135
+ rows = []
136
+ for path in sys.argv[1:]:
137
+ text = open(path, encoding="utf-8").read()
138
+ for date, body in re.findall(r"^## (\d{4}-\d\d-\d\d)\n(.*?)(?=^## |\Z)", text, re.S | re.M):
139
+ rows.append({"date": date, "words": len(body.split())})
140
+ print(json.dumps(rows))
141
+ ```
142
+
143
+ ```bash
144
+ dirsql query "SELECT date, words FROM './notebook.md' ORDER BY date DESC LIMIT 2" \
145
+ --on-file 'python3 notebook.py'
146
+ ```
147
+
148
+ ```json
149
+ [{"date":"2026-09-23","words":10},{"date":"2026-09-22","words":3}]
150
+ ```
151
+
152
+ **Many files, many rows.** A tree of JSONL session logs under
153
+ `projects/<name>/`, and the question is how many messages each project has per
154
+ role. The parser receives every log's path and prints one row per message,
155
+ carrying the project name it derived from the path, because dirsql will not
156
+ attach it. `messages.py`:
157
+
158
+ ```python
159
+ import json, os, sys
160
+
161
+ rows = []
162
+ for path in sys.argv[1:]:
163
+ project = os.path.relpath(path, "projects").split(os.sep)[0]
164
+ for line in open(path, encoding="utf-8"):
165
+ entry = json.loads(line)
166
+ if entry["type"] in ("user", "assistant"):
167
+ rows.append({"project": project, "role": entry["type"], "text": entry["message"]["content"]})
168
+ print(json.dumps(rows))
169
+ ```
170
+
171
+ ```bash
172
+ dirsql query "SELECT project, role, count(*) AS n FROM './projects/**/*.jsonl' GROUP BY project, role ORDER BY project, role" \
173
+ --on-file 'python3 messages.py'
174
+ ```
175
+
176
+ ```json
177
+ [{"project":"alpha","role":"assistant","n":2},{"project":"alpha","role":"user","n":1},{"project":"beta","role":"user","n":2}]
178
+ ```
179
+
180
+ The same two scripts work unchanged as a `[[table]]`'s `on-file` key. The
181
+ loop is the same in both shapes, so a parser written for one file already
182
+ handles a glob.
183
+
117
184
  ## Going further
118
185
 
119
- - The full stat-vs-parsed behavior, failure isolation, and skip rules:
186
+ - The full stat-vs-parsed behavior, failure semantics, and skip rules:
120
187
  [Parsing rows with `--on-file`](../reference/path-tables.md#parsing-rows-with-on-file).
121
188
  - Starting from a declared table instead of a path-table?
122
189
  [Extract rows from file contents](./extract-from-contents.md) covers the
123
190
  config-first path.
124
- - One row per record *within* a file (JSONL, multiple frontmatter blocks): the
125
- parser prints one object per row — the array length is the row count.
191
+ - A very large table may reach the parser in several invocations; see
192
+ [Argument-list limits](../reference/hooks.md#argument-list-limits).
@@ -4,8 +4,8 @@ By default the database is ephemeral: rebuilt from your files on every
4
4
  startup and discarded on exit. The [`--persist [PATH]`](../reference/cli.md#dirsql-server)
5
5
  flag keeps the SQLite index on disk instead, so a restart only re-parses
6
6
  files that actually changed — the difference between seconds and
7
- milliseconds on large trees, and between re-running and skipping expensive
8
- [`on-file`](./extract-from-contents.md) commands.
7
+ milliseconds on large trees, and between re-running and skipping an expensive
8
+ declared-table [`on-file`](./extract-from-contents.md) command.
9
9
 
10
10
  Whether and where to cache is a machine-local operational choice — it
11
11
  belongs to the command you run, not to the shared `.dirsql.toml`. That is
@@ -58,15 +58,11 @@ exactly what a non-persistent startup does.
58
58
  A run that changes nothing writes nothing: the cache file is read and left
59
59
  exactly as it was.
60
60
 
61
- This covers both kinds of table. Declared tables keep their indexed rows, and
62
- a [path-table](../reference/path-tables.md) parsed with `--on-file` keeps each
63
- file's parser output — so the second run over an unchanged tree spawns no
64
- parser process at all. A path-table's cached rows are keyed by the tree, the
65
- glob, the parser command and the `dirsql` version together, so changing any of
66
- them parses afresh rather than serving rows the current command never
67
- produced. A *stat* path-table caches nothing, because there is nothing to
68
- save: its columns are the metadata the scan already collected, and `content`
69
- is read live by design.
61
+ This covers declared tables, which keep their indexed rows. A
62
+ [path-table](../reference/path-tables.md) is not cached: a *stat* path-table
63
+ has nothing to save, since its columns are the metadata the scan already
64
+ collected and `content` is read live by design, and a path-table parsed with
65
+ `--on-file` runs its parser on every start.
70
66
 
71
67
  Persistence is a startup-time optimization, not a change in meaning: the
72
68
  database remains a derived view of your files, and queries return the same
@@ -120,6 +120,25 @@ ORDER BY vec_distance_cosine(emb, embed('local private models'))
120
120
  LIMIT 10
121
121
  ```
122
122
 
123
+ A field that needs real parsing — frontmatter, a log line — comes out with a
124
+ [parser attached](../reference/path-tables.md#parsing-rows-with-on-file) by
125
+ `--on-file`; the plugin's `embed()` then works on the parser's columns, since a
126
+ discovered plugin and an inline parser compose. With `title.py` printing one
127
+ `{"path", "title"}` row per note from its frontmatter:
128
+
129
+ ```bash
130
+ uvx --with dirsql-plugin-embeddings dirsql "
131
+ SELECT title,
132
+ vec_distance_cosine(embed(title), embed('how do I cook pasta?')) AS distance
133
+ FROM './notes/*.md'
134
+ ORDER BY distance
135
+ LIMIT 3" --on-file 'python3 title.py'
136
+ ```
137
+
138
+ ```json
139
+ [{"title":"Boiling spaghetti","distance":0.491},{"title":"Planting tomatoes","distance":0.836},{"title":"Feature branches","distance":1.064}]
140
+ ```
141
+
123
142
  The same projection works as an `on-file` hook feeding the indexed shape:
124
143
  parse the field in the hook, store it as a column, and the trigger embeds it
125
144
  once instead of on every query.
@@ -223,11 +223,7 @@ every statement, so it is reported once and exits `1` before the first prompt.
223
223
  | Code | Meaning |
224
224
  |---|---|
225
225
  | `0` | Clean EOF (Ctrl-D, `exit`, `quit`, or the end of a piped script) — **including when statements failed**. Matches interactive `sqlite3`; use [`dirsql query`](#dirsql-query) when a script needs a statement's exit status. |
226
- | `1` | The index could not be built (a bad `-c`, an unresolvable `--on-file`), or stdin could not be read. Nothing was executed. |
227
-
228
- `23` (partial scan) is not produced here: skipped files are reported before the
229
- first prompt, and a session's exit code describes the session rather than one
230
- scan.
226
+ | `1` | The index could not be built (a bad `-c`, an unresolvable `--on-file`, a failed `on-file` command), or stdin could not be read. Nothing was executed. |
231
227
 
232
228
  ## `dirsql server`
233
229
 
@@ -365,8 +361,9 @@ uses**, so behavior is identical to `POST /query` by construction:
365
361
  #### `--on-file <command>`
366
362
 
367
363
  Attach a parser to every [path-table](./path-tables.md#parsing-rows-with-on-file)
368
- in the query, so each matched file yields the rows the command prints (a JSON
369
- array of row objects) instead of the stat columns:
364
+ in the query: the command runs once per path-table, with every matched file's
365
+ path as a trailing argument, and the JSON array of row objects it prints
366
+ replaces the stat columns:
370
367
 
371
368
  ```sh
372
369
  dirsql query "SELECT title, author FROM './posts/*.md'" \
@@ -375,17 +372,21 @@ dirsql query "SELECT title, author FROM './posts/*.md'" \
375
372
 
376
373
  The command follows the [`on-file` hook contract](./hooks.md#on-file) — argv
377
374
  splitting, the `{root}` placeholder, every matched path as a trailing
378
- argument, the failure semantics, and the timeout. The parser's output is the whole schema; the stat columns are not
379
- reachable on a parsed path-table. `--on-file` may be given **at most once** (a
380
- repeat is an error pointing at config files) and never touches config-declared
381
- tables. It is a `query`-only flag — server mode rejects it as an unknown
375
+ argument, and the failure semantics — with one difference from the config
376
+ key: there is no config file, so the command runs in the index root (the
377
+ directory `dirsql query` was run in). The parser's output is the whole
378
+ schema; the stat columns are not reachable on a parsed path-table.
379
+ `--on-file` may be given **at most once** (a repeat is an error pointing at
380
+ config files) and never touches config-declared tables. It is a `query`-only flag — server mode rejects it as an unknown
382
381
  argument. The command string is copy-paste identical to a `[[table]]`
383
382
  `on-file` key, so an inline parser graduates to a config file unchanged — see
384
383
  [Parse your files into columns](../howto/parse-files-into-columns.md).
385
384
 
386
385
  Errors print the same diagnostic the HTTP `{"error": …}` body carries —
387
- config failures, SQL errors, rejected reads, hook failures, timeouts — to
388
- stderr, with exit code `1`.
386
+ config failures, SQL errors, rejected reads, hook failures — to stderr, with
387
+ exit code `1`. A failed `on-file` command is a hook failure: nothing is
388
+ printed on stdout, and the diagnostic carries the command's exit status and
389
+ the tail of its stderr.
389
390
 
390
391
  #### `--format {auto,table,json}`
391
392
 
@@ -398,7 +399,7 @@ takes, with the same `auto` default. A one-shot query is usually piped, so
398
399
  | Code | Meaning |
399
400
  |---|---|
400
401
  | `0` | Query succeeded; rows printed on stdout. |
401
- | `1` | Any failure: config, SQL, rejected read, hook, or timeout. The diagnostic is on stderr. |
402
+ | `1` | Any failure: config, SQL, rejected read, or a failed `on-file` command. The diagnostic is on stderr. |
402
403
 
403
404
  ## `dirsql init`
404
405
 
@@ -467,9 +468,9 @@ fragment is a launcher error naming the package — never a silent skip.
467
468
  ## Progress reporting
468
469
 
469
470
  Building the index over a large tree is not instant: the walk visits every
470
- file, then each matched file costs one `on-file` round trip plus whatever the
471
- table's `ddl` fires on insert. On a big corpus that is minutes. dirsql reports
472
- the two phases on **stderr** while they run:
471
+ file, then each table runs its `on-file` command over all of its files, plus
472
+ whatever the table's `ddl` fires on insert. On a big corpus that is minutes.
473
+ dirsql reports the two phases on **stderr** while they run:
473
474
 
474
475
  ```
475
476
  dirsql: scanning 128413 files
@@ -330,9 +330,9 @@ integral number → `INTEGER`, any other number → `REAL`; a string → `TEXT`;
330
330
  nested array or object → its JSON text as `TEXT`.
331
331
 
332
332
  A row's columns are exactly the keys the command emits, narrowed to the DDL;
333
- dirsql merges nothing else in. Output that is not a JSON array of objects is a
334
- per-file failure: the file is skipped with a stderr warning and the scan
335
- continues (see [failure semantics](./hooks.md#failure-semantics)).
333
+ dirsql merges nothing else in. Output that is not a JSON array of objects fails
334
+ the table, and a failed table fails the build (see
335
+ [failure semantics](./hooks.md#failure-semantics)).
336
336
 
337
337
  ## Composing multiple configs
338
338
 
@@ -37,10 +37,13 @@ The available placeholders are listed under the
37
37
 
38
38
  ### Working directory and environment
39
39
 
40
- The command runs in the **config file's directory**, so relative paths in
41
- the command resolve predictably regardless of where `dirsql` was launched.
42
- It inherits `dirsql`'s environment, so tools like `uvx --with …` / `npx …`
43
- resolve their dependencies as usual.
40
+ A command named by a `[[table]]` runs in the **config file's directory**, so
41
+ relative paths in the command resolve predictably regardless of where
42
+ `dirsql` was launched. A command named by the `--on-file` flag has no config
43
+ file: it runs in the **index root**, the directory `dirsql query` was run in,
44
+ which is also the directory a `./` path-table is relative to. In both forms
45
+ `{root}` is the index root. The command inherits `dirsql`'s environment, so
46
+ tools like `uvx --with …` / `npx …` resolve their dependencies as usual.
44
47
 
45
48
  ### stdout protocol
46
49
 
@@ -69,8 +72,8 @@ on-file = "timeout 30 my-extractor"
69
72
  ```
70
73
 
71
74
  When the wrapper kills an overrunning command, the run exits non-zero and
72
- the ordinary [failure semantics](#failure-semantics) apply — the file is
73
- skipped, the scan continues.
75
+ the ordinary [failure semantics](#failure-semantics) apply — the table's
76
+ command failed, so the build fails.
74
77
 
75
78
  ::: warning Windows
76
79
  Windows's built-in `timeout` command is a *sleep*, not a bound — it cannot
@@ -94,20 +97,39 @@ A hook run fails when the command:
94
97
  - exits zero but prints no non-empty stdout line,
95
98
  - or prints output that does not parse as a JSON array of row objects.
96
99
 
97
- What a failure *means*: **per-file isolation.** The file contributes no rows
98
- and is reported as skipped; the scan indexes every other file and commits.
99
- The CLI names up to ten skipped files on stderr, then `... and N more`, and
100
- exits `23` — distinct from `0` (clean) and `1` (the run failed), so a caller
101
- can tell a partial index from a complete one. A row the table rejects under
102
- `strict` counts as the same kind of failure.
100
+ What a failure *means*: the command ran once for the whole table, so its
101
+ failure is the table's, and a table that cannot be filled fails the build.
102
+ The error names the table and carries the command's exit status and stderr
103
+ tail; the CLI prints it and exits `1`. A row the table rejects under `strict`
104
+ counts as the same kind of failure.
103
105
 
104
106
  ## `on-file` contract
105
107
 
106
- Runs once per file matched by the table's `glob`, at initial scan and on
107
- every watched change. The command reads the file itself and prints a JSON
108
- array of row objects; see [`[[table]]`](./config.md#table) for the
108
+ Runs **once per table**, with every file matched by the table's `glob`
109
+ appended to the command as a trailing argument, and prints **one JSON array
110
+ of row objects** for the whole table. That is the whole contract. The command
111
+ reads the files itself; see [`[[table]]`](./config.md#table) for the
109
112
  row-mapping rules.
110
113
 
114
+ One process over all the files is the shape to write: a loop over the
115
+ arguments that collects rows and prints them once at the end. Parsing is the
116
+ command's cost, not dirsql's — dirsql spawns the command, waits, and reads
117
+ its stdout. A table over no matched files spawns nothing.
118
+
119
+ ```python
120
+ import json, sys
121
+
122
+ rows = []
123
+ for path in sys.argv[1:]:
124
+ rows.extend(parse(path))
125
+ print(json.dumps(rows))
126
+ ```
127
+
128
+ There is no row-to-file attribution: dirsql does not know which file a row
129
+ came from, and a row that needs the path carries it as a column the command
130
+ emitted. In watch mode a change to any file under the table's glob re-runs
131
+ the command over all of the table's files and replaces the table's rows.
132
+
111
133
  The same command is attachable two ways, over the same contract: the
112
134
  `on-file` config key on a `[[table]]`, and the
113
135
  [`--on-file <command>`](./cli.md#on-file-command) flag on `dirsql query`,
@@ -126,5 +148,20 @@ the path or stat metadata emits it (it has the paths).
126
148
  The matched files' **absolute** paths are not placeholders: they are appended
127
149
  to the command as trailing arguments, after everything written in the
128
150
  command, so `on-file = "extract.py"` receives them as `sys.argv[1:]` — one run
129
- per table, self-sufficient from any working directory. A command that still
130
- spells `{path}` is rejected at startup.
151
+ per table, self-sufficient from any working directory. A tool that needs a
152
+ path anywhere other than last gets a wrapper script. A command that spells
153
+ `{path}` is rejected at startup:
154
+
155
+ ```
156
+ on-file command `python3 extract.py {path}` uses `{path}`, but an on-file command now runs once per table with every matched path appended as trailing arguments and prints one JSON array of row objects. Remove `{path}` and read the paths from the command's arguments.
157
+ ```
158
+
159
+ ### Argument-list limits
160
+
161
+ Every platform caps the bytes one process may receive as arguments. When a
162
+ table's paths outgrow that cap, dirsql splits them into the fewest
163
+ consecutive runs that fit, runs the command once per run, and concatenates
164
+ the arrays it prints, in order. The command cannot tell: each run is an
165
+ ordinary invocation with a subset of the paths. A command must therefore not
166
+ assume one invocation sees every path — a count, a cross-file join, or a
167
+ dedupe over `sys.argv[1:]` is per run, not per table. Do that work in SQL.
@@ -79,6 +79,25 @@ SELECT * FROM ./;
79
79
  -- hint: paths used as table names must be quoted; did you mean "./"?
80
80
  ```
81
81
 
82
+ ### Symlinks
83
+
84
+ Symlinks are followed the way `bash -O globstar` follows them. A symlinked
85
+ file is a file: `'./*'` lists it, and so does naming it. A symlinked directory
86
+ is entered by any component of the path except `**`, which never walks into
87
+ one. A broken link lists nothing.
88
+
89
+ With `linkdir -> real`:
90
+
91
+ | You write | dirsql scans |
92
+ | --- | --- |
93
+ | `'./*/*'` | `linkdir/r.md` and `real/r.md` |
94
+ | `'./**'` | everything under `real/`, nothing under `linkdir/` |
95
+ | `'./**/*.md'` | `linkdir/r.md` too: `**` may stop on `linkdir` for `*.md` to enter |
96
+ | `'./linkdir/**'` | everything under `linkdir/` |
97
+
98
+ Each link entered uses up one component of the path, so a symlink cycle is
99
+ followed at most once per component and never hangs a scan.
100
+
82
101
  ### Paths outside the index root
83
102
 
84
103
  Three other prefixes resolve, with their usual shell meanings:
@@ -153,24 +172,31 @@ dirsql query "SELECT title, author FROM './posts/*.md'" \
153
172
  The command runs once, with every matched file's absolute path appended as an
154
173
  argument, and prints one JSON array of row objects, exactly like a declared
155
174
  table's [`on-file` hook](/reference/hooks) — same argv splitting, same `{root}`
156
- placeholder, same timeout. Its output *is* the table:
175
+ placeholder, same [argument-list splitting](/reference/hooks#argument-list-limits)
176
+ over a very large table. Its output *is* the table:
157
177
 
158
178
  - **The parser supplies the whole schema.** Columns are inferred from the keys
159
179
  across the emitted rows. The stat columns (`path`, `size`, …) are **not**
160
180
  reachable on a parsed path-table — a parser that wants the path emits it (it
161
181
  has the paths). The two modes stay cleanly separate.
162
- - **Failures are isolated per file.** A file whose parser fails (spawn, non-zero
163
- exit, timeout, or no output) or whose output is not a JSON array of rows
164
- contributes no rows; a one-line warning naming the file goes to stderr and the
165
- scan continues. The schema is inferred from the files that did parse.
182
+ - **The parser runs from the index root.** There is no config file to anchor
183
+ it, so the command's working directory is the directory `dirsql query` was
184
+ run in — the same directory the `./` path is relative to — and `{root}`
185
+ names it. `python3 extract.py` finds `extract.py` in that directory,
186
+ whatever directory the path-table scans.
187
+ - **A parser failure is the query's failure.** A run that fails (spawn,
188
+ non-zero exit, or no output) or whose output is not a JSON array of row
189
+ objects fails the query with the command's stderr tail; there is no partial
190
+ table, and the CLI exits `1`. A run that succeeds but prints an empty array
191
+ fails too, because there are no keys to infer columns from:
192
+
193
+ ```
194
+ dirsql_parsed: parser produced no rows for `posts/*.md`; cannot infer a schema
195
+ ```
166
196
  - **The skip rules still apply.** A parsed scan honors the same `node_modules`
167
197
  /`.git`/`ignore` rules a stat scan does (see below).
168
- - **`--persist` skips the parser for unchanged files.** With a
169
- [persistent cache](/howto/persist), each file's parser output is stored
170
- against its stat metadata, so a later run over an unchanged tree serves the
171
- rows from the cache and spawns no process. Change the file, the glob, the
172
- parser command, or the dirsql version and that file (or that whole table) is
173
- parsed again.
198
+ - **The parser runs on every start.** A parsed path-table is not kept in the
199
+ [persistent cache](/howto/persist); declared tables are.
174
200
 
175
201
  `--on-file` applies to **every** path-table in the query and may be given **at
176
202
  most once**. For different parsers per file set, define named tables in a
@@ -184,12 +210,14 @@ A path-table is scanned when the statement runs, so it always reflects the
184
210
  filesystem as it is *now* — unlike declared tables, which are indexed on build
185
211
  and updated by the watcher. A file created a moment ago shows up immediately.
186
212
 
187
- The scan is live all the way down to `content`: a file's body is read when the
188
- query names the `content` column, not when the row is discovered. A file
189
- deleted *after* the scan finds it but *before* its `content` is read yields
190
- `NULL` content — the same NULL an unreadable or non-UTF-8 file gives — rather
191
- than failing the query. This is an accepted consequence of reading live, not a
192
- bug to design around.
213
+ The scan is live all the way down to `content`: a file's body is read only
214
+ when the query names the `content` column, not when the row is discovered. A
215
+ query that names it has the bodies of the rows it selects read all at once,
216
+ several files at a time, before the first row comes back. A file deleted
217
+ *after* the scan finds it but *before* its `content` is read yields `NULL`
218
+ content — the same NULL an unreadable or non-UTF-8 file gives — rather than
219
+ failing the query. This is an accepted consequence of reading live, not a bug
220
+ to design around.
193
221
 
194
222
  The table itself is per-connection: it lives in `temp`, so it cannot leak into
195
223
  `sqlite_master` or survive a restart. Under `--persist` a *parsed* table's rows
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dirsql",
3
- "version": "0.4.68",
3
+ "version": "0.4.70",
4
4
  "description": "Ephemeral SQL index over a local directory",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/thekevinscott/dirsql",
@@ -221,10 +221,10 @@
221
221
  ]
222
222
  },
223
223
  "optionalDependencies": {
224
- "@dirsql/lib-linux-x64-gnu": "0.4.68",
225
- "@dirsql/lib-linux-arm64-gnu": "0.4.68",
226
- "@dirsql/lib-darwin-x64": "0.4.68",
227
- "@dirsql/lib-darwin-arm64": "0.4.68",
228
- "@dirsql/lib-win32-x64-msvc": "0.4.68"
224
+ "@dirsql/lib-linux-x64-gnu": "0.4.70",
225
+ "@dirsql/lib-linux-arm64-gnu": "0.4.70",
226
+ "@dirsql/lib-darwin-x64": "0.4.70",
227
+ "@dirsql/lib-darwin-arm64": "0.4.70",
228
+ "@dirsql/lib-win32-x64-msvc": "0.4.70"
229
229
  }
230
230
  }