dirsql 0.4.107 → 0.4.109
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/docs/getting-started.md +3 -9
- package/docs/howto/columns-from-paths.md +3 -5
- package/docs/howto/define-tables.md +2 -4
- package/docs/howto/extract-from-contents.md +12 -12
- package/docs/howto/parse-files-into-columns.md +8 -14
- package/docs/howto/react-to-changes.md +1 -1
- package/docs/howto/skip-files.md +1 -1
- package/docs/howto/write-a-plugin.md +4 -7
- package/docs/reference/cli.md +2 -2
- package/docs/reference/config.md +5 -3
- package/docs/reference/glob.md +9 -1
- package/docs/reference/hooks.md +18 -19
- package/docs/reference/path-tables.md +5 -6
- package/package.json +6 -6
package/docs/getting-started.md
CHANGED
|
@@ -152,22 +152,16 @@ That parser is the point: a named table's columns are exactly what its
|
|
|
152
152
|
`on-file` command emits. `dirsql` adds nothing on its own — so this is where
|
|
153
153
|
you pull a value out of each file. Still inside `my-notes`, write a tiny
|
|
154
154
|
parser that reads a note's title line (the `# Heading`) and its author (the
|
|
155
|
-
folder name), and prints them as a JSON row:
|
|
155
|
+
folder name), and prints them as a JSON row, one object per line:
|
|
156
156
|
|
|
157
157
|
```bash
|
|
158
158
|
cat > note.sh <<'EOF'
|
|
159
159
|
#!/usr/bin/env sh
|
|
160
|
-
printf '['
|
|
161
|
-
sep=''
|
|
162
160
|
for f; do
|
|
163
161
|
title=$(sed -n 's/^# //p' "$f" | head -n1)
|
|
164
162
|
author=$(basename "$(dirname "$f")")
|
|
165
|
-
|
|
166
|
-
"$(jq -Rn --arg t "$title" '$t')" \
|
|
167
|
-
"$(jq -Rn --arg a "$author" '$a')"
|
|
168
|
-
sep=','
|
|
163
|
+
jq -cn --arg t "$title" --arg a "$author" '{title: $t, author: $a}'
|
|
169
164
|
done
|
|
170
|
-
printf ']'
|
|
171
165
|
EOF
|
|
172
166
|
```
|
|
173
167
|
|
|
@@ -189,7 +183,7 @@ Three keys define the table:
|
|
|
189
183
|
`notes/`, relative to the directory the config sits in.
|
|
190
184
|
- `ddl` is ordinary `CREATE TABLE` SQL naming the columns you want to keep.
|
|
191
185
|
- `on-file` is the command run once for the table, with every matched file's
|
|
192
|
-
path appended as an argument; the JSON
|
|
186
|
+
path appended as an argument; the JSON objects it prints, one per line, are the table's rows.
|
|
193
187
|
The columns are exactly what it emits — `title` from the heading, `author`
|
|
194
188
|
from the folder.
|
|
195
189
|
|
|
@@ -17,19 +17,17 @@ photos/2025/01/snow.jpg
|
|
|
17
17
|
|
|
18
18
|
Write a small parser, `pathcols.py`, that turns each path into a row. It
|
|
19
19
|
receives the matched files' absolute paths as its arguments and prints one
|
|
20
|
-
JSON
|
|
20
|
+
JSON object per line, one row each:
|
|
21
21
|
|
|
22
22
|
```python
|
|
23
23
|
#!/usr/bin/env python3
|
|
24
24
|
import json, os, sys
|
|
25
25
|
|
|
26
|
-
rows = []
|
|
27
26
|
for path in sys.argv[1:]:
|
|
28
27
|
parts = path.split(os.sep)
|
|
29
28
|
# .../photos/<year>/<month>/<file>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
print(json.dumps(rows))
|
|
29
|
+
print(json.dumps({"year": parts[-3], "month": parts[-2],
|
|
30
|
+
"basename": os.path.basename(path)}))
|
|
33
31
|
```
|
|
34
32
|
|
|
35
33
|
Point a table at it in `.dirsql.toml`:
|
|
@@ -11,18 +11,16 @@ Suppose your blog posts live under `posts/`, one markdown file each. A named
|
|
|
11
11
|
table needs three keys: a `glob` that selects the files, a `ddl` that names
|
|
12
12
|
the columns, and an [`on-file`](../reference/config.md#table) hook that emits
|
|
13
13
|
the table's rows. Put a small parser next to the config — `extract.py`, which
|
|
14
|
-
reads each post's title line and prints one JSON
|
|
14
|
+
reads each post's title line and prints one JSON row object per line:
|
|
15
15
|
|
|
16
16
|
```python
|
|
17
17
|
#!/usr/bin/env python3
|
|
18
18
|
import json, os, sys
|
|
19
19
|
|
|
20
|
-
rows = []
|
|
21
20
|
for path in sys.argv[1:]:
|
|
22
21
|
text = open(path, encoding="utf-8").read()
|
|
23
22
|
title = next((l[2:].strip() for l in text.splitlines() if l.startswith("# ")), None)
|
|
24
|
-
|
|
25
|
-
print(json.dumps(rows))
|
|
23
|
+
print(json.dumps({"title": title, "slug": os.path.basename(path)[:-3]}))
|
|
26
24
|
```
|
|
27
25
|
|
|
28
26
|
Then declare the table in `.dirsql.toml`:
|
|
@@ -3,8 +3,8 @@
|
|
|
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
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
|
|
7
|
-
|
|
6
|
+
table, with every matched file as an argument, and the JSON objects it prints,
|
|
7
|
+
one per line, become the table's rows.
|
|
8
8
|
|
|
9
9
|
## 1. Point a command at the files
|
|
10
10
|
|
|
@@ -14,19 +14,19 @@ Suppose each book is a JSON file:
|
|
|
14
14
|
{"title": "Middlemarch", "author": "George Eliot", "year": 1871}
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
Any program that reads the files named by its arguments and prints
|
|
18
|
-
|
|
17
|
+
Any program that reads the files named by its arguments and prints **one JSON
|
|
18
|
+
object per line** on stdout works. With [`jq`](https://jqlang.org/):
|
|
19
19
|
|
|
20
20
|
```toml
|
|
21
21
|
[[table]]
|
|
22
22
|
name = "books"
|
|
23
23
|
ddl = "CREATE TABLE books (title TEXT, author TEXT, year INTEGER)"
|
|
24
24
|
glob = "books/*.json"
|
|
25
|
-
on-file = "jq -c
|
|
25
|
+
on-file = "jq -c '{title, author, year}'"
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
The command runs once for the table, with every matched file's absolute path
|
|
29
|
-
appended as an argument, and prints one
|
|
29
|
+
appended as an argument, and prints one object per line for all of them — the
|
|
30
30
|
[command hook contract](../reference/hooks.md#on-file), which also covers
|
|
31
31
|
the argv splitting, working directory, and stdout protocol shared by every
|
|
32
32
|
hook.
|
|
@@ -50,15 +50,15 @@ The table's columns are exactly what the command emits, narrowed to the DDL —
|
|
|
50
50
|
|
|
51
51
|
## Multiple rows per file
|
|
52
52
|
|
|
53
|
-
Each
|
|
54
|
-
|
|
53
|
+
Each printed line is one row. A JSONL file is already one row per line, so
|
|
54
|
+
project the fields you want:
|
|
55
55
|
|
|
56
56
|
```toml
|
|
57
57
|
[[table]]
|
|
58
58
|
name = "events"
|
|
59
59
|
ddl = "CREATE TABLE events (event TEXT, user TEXT)"
|
|
60
60
|
glob = "logs/*.jsonl"
|
|
61
|
-
on-file = "jq -c
|
|
61
|
+
on-file = "jq -c '{event, user}'"
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
```bash
|
|
@@ -72,12 +72,12 @@ dirsql query "SELECT event, user FROM events" -c ./.dirsql.toml
|
|
|
72
72
|
## When the command fails
|
|
73
73
|
|
|
74
74
|
The command runs once for the whole table, so its failure is the table's: a
|
|
75
|
-
non-zero exit,
|
|
76
|
-
the build, and `dirsql query` exits `1` with the command's stderr tail on
|
|
75
|
+
non-zero exit, or output that is not one JSON object per line (an array
|
|
76
|
+
is rejected) fails the build, and `dirsql query` exits `1` with the command's stderr tail on
|
|
77
77
|
stderr and nothing on stdout.
|
|
78
78
|
|
|
79
79
|
```
|
|
80
|
-
dirsql query: failed to load config: table `books`: on-file command failed: command `jq -c
|
|
80
|
+
dirsql query: failed to load config: table `books`: on-file command failed: command `jq -c '{title, author, year}'` failed (exit 5): jq: error (at /home/me/library/books/broken.json:1): Cannot index string with string "title"
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
A file the command cannot parse is the command's to handle — skip it, or
|
|
@@ -32,15 +32,14 @@ To get them, you need a parser.
|
|
|
32
32
|
|
|
33
33
|
## 2. Attach a parser with `--on-file`
|
|
34
34
|
|
|
35
|
-
Any program that reads the files named by its arguments and prints
|
|
36
|
-
|
|
35
|
+
Any program that reads the files named by its arguments and prints **one JSON
|
|
36
|
+
object per line** on stdout is a parser. Here is a small one,
|
|
37
37
|
`extract.py`, that reads each post's frontmatter:
|
|
38
38
|
|
|
39
39
|
```python
|
|
40
40
|
#!/usr/bin/env python3
|
|
41
41
|
import json, re, sys
|
|
42
42
|
|
|
43
|
-
rows = []
|
|
44
43
|
for path in sys.argv[1:]:
|
|
45
44
|
text = open(path, encoding="utf-8").read()
|
|
46
45
|
m = re.match(r"^---\n(.*?)\n---", text, re.DOTALL)
|
|
@@ -48,8 +47,7 @@ for path in sys.argv[1:]:
|
|
|
48
47
|
(k.strip(), v.strip())
|
|
49
48
|
for k, _, v in (line.partition(":") for line in (m.group(1).splitlines() if m else []))
|
|
50
49
|
)
|
|
51
|
-
|
|
52
|
-
print(json.dumps(rows))
|
|
50
|
+
print(json.dumps({"title": fields.get("title"), "author": fields.get("author")}))
|
|
53
51
|
```
|
|
54
52
|
|
|
55
53
|
Point the path-table at it with `--on-file`:
|
|
@@ -75,7 +73,7 @@ for the full behavior.
|
|
|
75
73
|
|
|
76
74
|
One run, one process: on a thousand files the parser starts once, not a
|
|
77
75
|
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:]
|
|
76
|
+
That is the shape to write — loop over `sys.argv[1:]` and print each row as you go.
|
|
79
77
|
|
|
80
78
|
`--on-file` applies to every path-table in the query and may be given at most
|
|
81
79
|
once. It is a `query`-only flag: there is no config file involved yet, so it is
|
|
@@ -121,8 +119,8 @@ row that needs the file's `path` emits it (the parser has the path).
|
|
|
121
119
|
|
|
122
120
|
## The two shapes
|
|
123
121
|
|
|
124
|
-
Every parser is one of two shapes. The
|
|
125
|
-
way; what differs is how many files feed it.
|
|
122
|
+
Every parser is one of two shapes. The number of lines printed is the row
|
|
123
|
+
count either way; what differs is how many files feed it.
|
|
126
124
|
|
|
127
125
|
**One file, many rows.** A lab notebook is a single Markdown file of dated
|
|
128
126
|
`## YYYY-MM-DD` sections, and the question is about the sections. Name the one
|
|
@@ -132,12 +130,10 @@ section. `notebook.py`:
|
|
|
132
130
|
```python
|
|
133
131
|
import json, re, sys
|
|
134
132
|
|
|
135
|
-
rows = []
|
|
136
133
|
for path in sys.argv[1:]:
|
|
137
134
|
text = open(path, encoding="utf-8").read()
|
|
138
135
|
for date, body in re.findall(r"^## (\d{4}-\d\d-\d\d)\n(.*?)(?=^## |\Z)", text, re.S | re.M):
|
|
139
|
-
|
|
140
|
-
print(json.dumps(rows))
|
|
136
|
+
print(json.dumps({"date": date, "words": len(body.split())}))
|
|
141
137
|
```
|
|
142
138
|
|
|
143
139
|
```bash
|
|
@@ -158,14 +154,12 @@ attach it. `messages.py`:
|
|
|
158
154
|
```python
|
|
159
155
|
import json, os, sys
|
|
160
156
|
|
|
161
|
-
rows = []
|
|
162
157
|
for path in sys.argv[1:]:
|
|
163
158
|
project = os.path.relpath(path, "projects").split(os.sep)[0]
|
|
164
159
|
for line in open(path, encoding="utf-8"):
|
|
165
160
|
entry = json.loads(line)
|
|
166
161
|
if entry["type"] in ("user", "assistant"):
|
|
167
|
-
|
|
168
|
-
print(json.dumps(rows))
|
|
162
|
+
print(json.dumps({"project": project, "role": entry["type"], "text": entry["message"]["content"]}))
|
|
169
163
|
```
|
|
170
164
|
|
|
171
165
|
```bash
|
|
@@ -17,7 +17,7 @@ prints each file's basename:
|
|
|
17
17
|
name = "files"
|
|
18
18
|
ddl = "CREATE TABLE files (basename TEXT)"
|
|
19
19
|
glob = "**/*"
|
|
20
|
-
on-file = '''sh -c '
|
|
20
|
+
on-file = '''sh -c 'for p; do printf "{\"basename\":\"%s\"}\n" "${p##*/}"; done' sh'''
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## 1. Open the stream
|
package/docs/howto/skip-files.md
CHANGED
|
@@ -25,7 +25,7 @@ ignore = ["notes/drafts/**", "**/*.tmp"]
|
|
|
25
25
|
name = "notes"
|
|
26
26
|
ddl = "CREATE TABLE notes (basename TEXT)"
|
|
27
27
|
glob = "notes/**/*"
|
|
28
|
-
on-file = '''sh -c '
|
|
28
|
+
on-file = '''sh -c 'for p; do printf "{\"basename\":\"%s\"}\n" "${p##*/}"; done' sh'''
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Patterns match against root-relative paths, the same way table globs do. An
|
|
@@ -121,7 +121,7 @@ embedding. dirsql injects no columns, so the script emits the path itself,
|
|
|
121
121
|
from the `{root}` the hook passes in and the paths it appends:
|
|
122
122
|
|
|
123
123
|
```python
|
|
124
|
-
"""Embed each file's text; print one dirsql row
|
|
124
|
+
"""Embed each file's text; print one dirsql row per line on stdout."""
|
|
125
125
|
import json
|
|
126
126
|
import os
|
|
127
127
|
import sys
|
|
@@ -131,12 +131,9 @@ from model2vec import StaticModel
|
|
|
131
131
|
root, paths = sys.argv[1], sys.argv[2:]
|
|
132
132
|
texts = [open(path, encoding="utf-8").read() for path in paths]
|
|
133
133
|
model = StaticModel.from_pretrained("minishlab/potion-base-8M")
|
|
134
|
-
|
|
135
|
-
{"path": os.path.relpath(path, root), "text": text,
|
|
136
|
-
|
|
137
|
-
for path, text, vector in zip(paths, texts, model.encode(texts))
|
|
138
|
-
]
|
|
139
|
-
print(json.dumps(rows))
|
|
134
|
+
for path, text, vector in zip(paths, texts, model.encode(texts)):
|
|
135
|
+
print(json.dumps({"path": os.path.relpath(path, root), "text": text,
|
|
136
|
+
"embedding": json.dumps([round(float(x), 6) for x in vector])}))
|
|
140
137
|
```
|
|
141
138
|
|
|
142
139
|
The relative `embed.py` above resolves against the fragment directory, which
|
package/docs/reference/cli.md
CHANGED
|
@@ -383,8 +383,8 @@ uses**, so behavior is identical to `POST /query` by construction:
|
|
|
383
383
|
|
|
384
384
|
Attach a parser to every [path-table](./path-tables.md#parsing-rows-with-on-file)
|
|
385
385
|
in the query: the command runs once per path-table, with every matched file's
|
|
386
|
-
path as a trailing argument, and the JSON
|
|
387
|
-
|
|
386
|
+
path as a trailing argument, and the JSON row objects it prints, one per line,
|
|
387
|
+
replace the stat columns:
|
|
388
388
|
|
|
389
389
|
```sh
|
|
390
390
|
dirsql query "SELECT title, author FROM './posts/*.md'" \
|
package/docs/reference/config.md
CHANGED
|
@@ -226,7 +226,7 @@ what its required `on-file` command emits — dirsql injects nothing (see
|
|
|
226
226
|
| `name` | yes | The table's SQL name — the name you query it by. Declared, never derived from `ddl`: dirsql does not read the DDL text. The `ddl` must create a table by this name; if it doesn't, loading fails. |
|
|
227
227
|
| `ddl` | yes | A SQL batch, run verbatim — any number of statements. It must create a table called `name`; that table holds the file rows, and only the columns it declares are kept (keys the `on-file` command emits that are not declared are dropped). The rest of the batch is yours: indexes, virtual tables, triggers. See [Batch `ddl`](#batch-ddl). |
|
|
228
228
|
| `glob` | yes | Glob pattern, [anchored](#glob-anchor) at the config file's directory, under the [one glob rule](#glob-rule): `*` matches one level, `**` any depth. Every table whose glob matches a file receives that file's rows — a file can populate multiple tables. A `{name}` is a literal, as in bash and a path-table; the [glob page](./glob.md) has the syntax. |
|
|
229
|
-
| `on-file` | **yes** | A command run once per table, with every matched file's absolute path appended as a trailing argument; its stdout (one JSON
|
|
229
|
+
| `on-file` | **yes** | A command run once per table, with every matched file's absolute path appended as a trailing argument; its stdout (one JSON object per line) is the table's rows. Must be non-empty. A `[[table]]` with no `on-file` is a load error (see [parse errors](#parse-errors)). See [Command hooks](./hooks.md#on-file). |
|
|
230
230
|
| `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 `on-file` output. When `false`, extra keys are dropped and missing columns become `NULL`. |
|
|
231
231
|
|
|
232
232
|
`on-file` is required because a table's rows come from nowhere else. dirsql
|
|
@@ -339,13 +339,15 @@ Two consequences of `ddl` running **once, when the table is created**:
|
|
|
339
339
|
|
|
340
340
|
### `on-file` row mapping
|
|
341
341
|
|
|
342
|
-
The command prints
|
|
342
|
+
The command prints one JSON object per line (NDJSON); each line becomes one
|
|
343
|
+
row. Blank lines are skipped, and a command that prints nothing yields a table
|
|
344
|
+
with no rows. An array on a line is an error, not a list of rows.
|
|
343
345
|
JSON values map to SQLite as: `null` → `NULL`; `true`/`false` → `1`/`0`; an
|
|
344
346
|
integral number → `INTEGER`, any other number → `REAL`; a string → `TEXT`; a
|
|
345
347
|
nested array or object → its JSON text as `TEXT`.
|
|
346
348
|
|
|
347
349
|
A row's columns are exactly the keys the command emits, narrowed to the DDL;
|
|
348
|
-
dirsql merges nothing else in. Output that is not
|
|
350
|
+
dirsql merges nothing else in. Output that is not one JSON object per line fails
|
|
349
351
|
the table, and a failed table fails the build (see
|
|
350
352
|
[failure semantics](./hooks.md#failure-semantics)).
|
|
351
353
|
|
package/docs/reference/glob.md
CHANGED
|
@@ -19,7 +19,7 @@ divergence is a written decision.
|
|
|
19
19
|
## The fixture
|
|
20
20
|
|
|
21
21
|
Every example below runs against this tree. A name ending in `/` is an empty
|
|
22
|
-
directory, `a -> b` is a symlink, and `.gitignore` holds `*.log
|
|
22
|
+
directory, `a -> b` is a symlink, and `.gitignore` holds `*.log` and `dist/`.
|
|
23
23
|
|
|
24
24
|
<!-- conformance-tree -->
|
|
25
25
|
```text
|
|
@@ -48,6 +48,8 @@ docs/.draft.md
|
|
|
48
48
|
docs/nested/deep.md
|
|
49
49
|
docs/nested/.cache/x.md
|
|
50
50
|
node_modules/pkg/index.js
|
|
51
|
+
dist/bundle.js
|
|
52
|
+
dist/sub/o.js
|
|
51
53
|
real/r.md
|
|
52
54
|
uni/1.md
|
|
53
55
|
uni/a.md
|
|
@@ -126,6 +128,12 @@ A test runs every row against dirsql as a path-table and, with the leading
|
|
|
126
128
|
| `./*.log` | none | gitignore |
|
|
127
129
|
| `./ignored.log` | `ignored.log` | |
|
|
128
130
|
| `./{ignored,x}.log` | `ignored.log` | |
|
|
131
|
+
| `./dist/*.js` | none | gitignore |
|
|
132
|
+
| `./dist/**/*.js` | none | gitignore |
|
|
133
|
+
| `./dist/sub/*.js` | none | gitignore |
|
|
134
|
+
| `./d*/*.js` | none | gitignore |
|
|
135
|
+
| `./dist/bundle.js` | `dist/bundle.js` | |
|
|
136
|
+
| `./dist/sub/o.js` | `dist/sub/o.js` | |
|
|
129
137
|
| `./**/*.js` | `node_modules/pkg/index.js` | |
|
|
130
138
|
| `./node_modules/*/index.js` | `node_modules/pkg/index.js` | |
|
|
131
139
|
| `./docs/**/*.md` | `docs/api.md`, `docs/guide.md`, `docs/nested/deep.md` | |
|
package/docs/reference/hooks.md
CHANGED
|
@@ -48,18 +48,18 @@ tools like `uvx --with …` / `npx …` resolve their dependencies as usual.
|
|
|
48
48
|
|
|
49
49
|
### stdout protocol
|
|
50
50
|
|
|
51
|
-
The command's
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
The command's stdout is **NDJSON: one JSON object per line**, one row each.
|
|
52
|
+
Blank lines are skipped. A command that exits successfully and prints nothing
|
|
53
|
+
yields no rows. Every non-blank line must be a JSON object: a line that is
|
|
54
|
+
an array, a scalar, or log chatter fails the table, and the error names the
|
|
55
|
+
line number. There is no array form.
|
|
55
56
|
|
|
56
57
|
stderr is never data — it is captured only to enrich error messages (the
|
|
57
|
-
last 2 000 characters are attached to failures).
|
|
58
|
+
last 2 000 characters are attached to failures). Send logs there.
|
|
58
59
|
|
|
59
|
-
::: tip Print
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
compactly on one line.
|
|
60
|
+
::: tip Print one object per line
|
|
61
|
+
`jq` users: pass `-c` so each object is emitted compactly on one line. Do not
|
|
62
|
+
slurp into an array (`jq -s`, `jq -n '[inputs]'`).
|
|
63
63
|
:::
|
|
64
64
|
|
|
65
65
|
### Bounding a hook
|
|
@@ -95,8 +95,8 @@ A hook run fails when the command:
|
|
|
95
95
|
- exits non-zero (the exit code — or `signal`, if killed by one — and the
|
|
96
96
|
stderr tail are reported; a `timeout(1)` wrapper killing an overrun lands
|
|
97
97
|
here),
|
|
98
|
-
-
|
|
99
|
-
|
|
98
|
+
- or prints output where a non-blank line is not a JSON object (an array
|
|
99
|
+
line is rejected with a message saying to print one object per line).
|
|
100
100
|
|
|
101
101
|
What a failure *means*: the command ran once for the whole table, so its
|
|
102
102
|
failure is the table's, and a table that cannot be filled fails the build.
|
|
@@ -107,23 +107,22 @@ counts as the same kind of failure.
|
|
|
107
107
|
## `on-file` contract
|
|
108
108
|
|
|
109
109
|
Runs **once per table**, with every file matched by the table's `glob`
|
|
110
|
-
appended to the command as a trailing argument, and prints **one JSON
|
|
111
|
-
|
|
110
|
+
appended to the command as a trailing argument, and prints **one JSON object
|
|
111
|
+
per line** for the whole table, one row each. That is the whole contract. The command
|
|
112
112
|
reads the files itself; see [`[[table]]`](./config.md#table) for the
|
|
113
113
|
row-mapping rules.
|
|
114
114
|
|
|
115
115
|
One process over all the files is the shape to write: a loop over the
|
|
116
|
-
arguments that
|
|
116
|
+
arguments that prints each row as it is parsed. Parsing is the
|
|
117
117
|
command's cost, not dirsql's — dirsql spawns the command, waits, and reads
|
|
118
118
|
its stdout. A table over no matched files spawns nothing.
|
|
119
119
|
|
|
120
120
|
```python
|
|
121
121
|
import json, sys
|
|
122
122
|
|
|
123
|
-
rows = []
|
|
124
123
|
for path in sys.argv[1:]:
|
|
125
|
-
|
|
126
|
-
print(json.dumps(
|
|
124
|
+
for row in parse(path):
|
|
125
|
+
print(json.dumps(row))
|
|
127
126
|
```
|
|
128
127
|
|
|
129
128
|
There is no row-to-file attribution: dirsql does not know which file a row
|
|
@@ -154,7 +153,7 @@ path anywhere other than last gets a wrapper script. A command that spells
|
|
|
154
153
|
`{path}` is rejected at startup:
|
|
155
154
|
|
|
156
155
|
```
|
|
157
|
-
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
|
|
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 object per line. Remove `{path}` and read the paths from the command's arguments.
|
|
158
157
|
```
|
|
159
158
|
|
|
160
159
|
### Argument-list limits
|
|
@@ -162,7 +161,7 @@ on-file command `python3 extract.py {path}` uses `{path}`, but an on-file comman
|
|
|
162
161
|
Every platform caps the bytes one process may receive as arguments. When a
|
|
163
162
|
table's paths outgrow that cap, dirsql splits them into the fewest
|
|
164
163
|
consecutive runs that fit, runs the command once per run, and concatenates
|
|
165
|
-
the
|
|
164
|
+
the rows it prints, in order. The command cannot tell: each run is an
|
|
166
165
|
ordinary invocation with a subset of the paths. A command must therefore not
|
|
167
166
|
assume one invocation sees every path — a count, a cross-file join, or a
|
|
168
167
|
dedupe over `sys.argv[1:]` is per run, not per table. Do that work in SQL.
|
|
@@ -171,7 +171,7 @@ dirsql query "SELECT title, author FROM './posts/*.md'" \
|
|
|
171
171
|
```
|
|
172
172
|
|
|
173
173
|
The command runs once, with every matched file's absolute path appended as an
|
|
174
|
-
argument, and prints one JSON
|
|
174
|
+
argument, and prints one JSON object per line, exactly like a declared
|
|
175
175
|
table's [`on-file` hook](/reference/hooks) — same argv splitting, same `{root}`
|
|
176
176
|
placeholder, same [argument-list splitting](/reference/hooks#argument-list-limits)
|
|
177
177
|
over a very large table. Its output *is* the table:
|
|
@@ -185,11 +185,10 @@ over a very large table. Its output *is* the table:
|
|
|
185
185
|
run in — the same directory the `./` path is relative to — and `{root}`
|
|
186
186
|
names it. `python3 extract.py` finds `extract.py` in that directory,
|
|
187
187
|
whatever directory the path-table scans.
|
|
188
|
-
- **A parser failure is the query's failure.** A run that fails (spawn
|
|
189
|
-
non-zero exit
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
fails too, because there are no keys to infer columns from:
|
|
188
|
+
- **A parser failure is the query's failure.** A run that fails (spawn
|
|
189
|
+
or non-zero exit) or whose output is not one JSON object per line fails the
|
|
190
|
+
query with the command's stderr tail; there is no partial table, and the CLI
|
|
191
|
+
exits `1`. A run that succeeds but prints nothing fails too, because there are no keys to infer columns from:
|
|
193
192
|
|
|
194
193
|
```
|
|
195
194
|
dirsql_parsed: parser produced no rows for `posts/*.md`; cannot infer a schema
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dirsql",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.109",
|
|
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.
|
|
225
|
-
"@dirsql/lib-linux-arm64-gnu": "0.4.
|
|
226
|
-
"@dirsql/lib-darwin-x64": "0.4.
|
|
227
|
-
"@dirsql/lib-darwin-arm64": "0.4.
|
|
228
|
-
"@dirsql/lib-win32-x64-msvc": "0.4.
|
|
224
|
+
"@dirsql/lib-linux-x64-gnu": "0.4.109",
|
|
225
|
+
"@dirsql/lib-linux-arm64-gnu": "0.4.109",
|
|
226
|
+
"@dirsql/lib-darwin-x64": "0.4.109",
|
|
227
|
+
"@dirsql/lib-darwin-arm64": "0.4.109",
|
|
228
|
+
"@dirsql/lib-win32-x64-msvc": "0.4.109"
|
|
229
229
|
}
|
|
230
230
|
}
|