datanotes 3.4.2 → 3.5.0

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.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/dist/api/extensions.cjs +1088 -5
  3. package/dist/api/extensions.js +1088 -5
  4. package/dist/api/index.cjs +1088 -5
  5. package/dist/api/index.js +1088 -5
  6. package/dist/api/mcp.cjs +1088 -5
  7. package/dist/api/mcp.js +1088 -5
  8. package/dist/datanotes.cjs +4408 -1618
  9. package/dist/docs/administration/git-sync.md +17 -0
  10. package/dist/docs/administration/limits.md +29 -0
  11. package/dist/docs/administration/use-with-obsidian.md +12 -0
  12. package/dist/docs/api/operations/alter_table.md +6 -1
  13. package/dist/docs/api/operations/create_table.md +5 -0
  14. package/dist/docs/api/operations/describe_table.md +1 -1
  15. package/dist/docs/api/operations/pack_rows.md +45 -0
  16. package/dist/docs/api/operations/tags/auxiliary.md +2 -0
  17. package/dist/docs/api/operations/unpack_rows.md +40 -0
  18. package/dist/docs/api.md +3 -1
  19. package/dist/docs/guides/data-quality/check-constraints.md +10 -10
  20. package/dist/docs/guides/data-quality/hand-edits.md +8 -8
  21. package/dist/docs/guides/database-objects/bases.md +11 -11
  22. package/dist/docs/guides/database-objects/catalogs-and-schemas.md +10 -10
  23. package/dist/docs/guides/database-objects/columns.md +41 -41
  24. package/dist/docs/guides/database-objects/packs.md +563 -0
  25. package/dist/docs/guides/database-objects/relations.md +28 -28
  26. package/dist/docs/guides/database-objects/tables.md +28 -28
  27. package/dist/docs/guides/database-objects/views.md +44 -44
  28. package/dist/docs/guides/history/change-data-feed.md +34 -34
  29. package/dist/docs/guides/history/journal-and-undo.md +12 -12
  30. package/dist/docs/guides/history/restore.md +20 -20
  31. package/dist/docs/guides/history/table-history.md +31 -31
  32. package/dist/docs/guides/read-and-write/automatic-links.md +15 -15
  33. package/dist/docs/guides/read-and-write/import-notes.md +21 -21
  34. package/dist/docs/guides/read-and-write/joins-and-aggregates.md +7 -7
  35. package/dist/docs/guides/read-and-write/read-and-filter.md +10 -10
  36. package/dist/docs/guides/read-and-write/write-rows.md +10 -10
  37. package/dist/docs/guides/search/text.md +1 -1
  38. package/dist/docs/reference/cli.md +3 -1
  39. package/dist/docs/reference/operations/auxiliary.md +30 -1
  40. package/dist/docs/reference/operations/queries.md +1 -1
  41. package/dist/docs/reference/operations/rows.md +3 -3
  42. package/dist/docs/reference/operations/schema.md +8 -4
  43. package/dist/docs/reference/operations.md +55 -53
  44. package/dist/docs/reference/settings.md +1 -0
  45. package/dist/docs/release-notes.md +29 -0
  46. package/package.json +1 -1
@@ -0,0 +1,563 @@
1
+ # Packs of rows
2
+
3
+ > Keep the rows of a closed period in one note, a section per row: fewer files on disk and in git, the same rows for the engine.
4
+
5
+ A table can hold thousands of small rows a year, such as a reading log or a sleep log. Each row is a note, so the table folder holds thousands of files. A pack is one note that holds the rows of one period, such as a month, one `##` section per row.
6
+
7
+ The engine still sees one row at a time. `select_rows`, links, the link graph, the change feed and the Model Context Protocol (MCP) tools give each row at its own path, with its values, text and modification time. Only the files on disk change.
8
+
9
+ The examples on this page keep a log of reading sessions.
10
+
11
+ ## How a pack reads
12
+
13
+ A pack is a Markdown note in the table’s folder, named `<table> <key>.md`, such as `reading 2025-03.md`. Its front matter names the table and the period. Each row is a `##` section:
14
+
15
+ ```markdown
16
+ ---
17
+ pack: main.public.reading
18
+ key: 2025-03
19
+ description: "Rows of main.public.reading for 2025-03. ..."
20
+ ---
21
+
22
+
23
+ ## 2025-03-02T2100
24
+ id:: 019a3c1e-7f3b-7c2a-9d41-6c0f1e2a3b4c
25
+ started_at:: 2025-03-02T21:00
26
+ book:: Moby-Dick
27
+ minutes:: 40
28
+
29
+
30
+ ## 2025-03-16T2200
31
+ id:: 019a3c1e-8a10-7d55-b2e3-1f4a5b6c7d8e
32
+ started_at:: 2025-03-16T22:00
33
+ book:: Persuasion
34
+ minutes:: 30
35
+
36
+
37
+ Chapter 4, the walk to Winthrop.
38
+ ```
39
+
40
+ * The heading is the row’s file name.
41
+ * A field is `key:: value`, its value YAML on one line, as in front matter.
42
+ * The row’s own text starts after the first empty line, with headings from `###` down.
43
+
44
+ The engine lists each row at the path it would have as a note of its own, such as `datanotes/main/public/reading/2025/2025-03-02T2100.md`. That path doesn’t exist on disk, but every operation finds the row there.
45
+
46
+ ## Know which rows stay notes of their own
47
+
48
+ `pack_rows` leaves a row as a note of its own when:
49
+
50
+ * its period isn’t over yet, or ended less than `pack_after_days` days ago
51
+ * it changed in the last 24 hours
52
+ * its text has a `#` or `##` heading, which would end its section
53
+ * it isn’t at the path its values give, such as a name with a `-2` suffix
54
+ * its note isn’t in the form the engine writes, such as another YAML style or a comment
55
+
56
+ A dry run lists these rows in `excluded`, with the reason. A row the engine can’t pack now waits for the next run.
57
+
58
+ ## Link to a packed row
59
+
60
+ A link to a packed row names the pack and the row’s heading: `[[reading 2025-03#2025-03-02T2100]]`. Obsidian follows it to the section. `pack_rows` rewrites the links other notes have to the rows in this form, and `unpack_rows` writes them back.
61
+
62
+ A link by the row’s name, such as `[[2025-03-02T2100]]`, still leads to the row for the engine. A link column takes both forms.
63
+
64
+ Links between rows of the same pack stay as they are written.
65
+
66
+ ## Know who reads the files
67
+
68
+ Every reader that goes through the engine sees the rows: the HTTP API, the MCP tools, `datanotes/client` and the change feed. Programs that read the vault’s files directly see the packs instead:
69
+
70
+ * Obsidian, which shows each pack as one note
71
+ * scripts and services that count or open the files of a table folder, with `find`, `grep` or fixed paths
72
+ * agents with a shell, and git hosts such as GitHub
73
+
74
+ To count the rows of a table, call `select_rows`. On disk, count the headings of its packs with `grep -c '^## '`.
75
+
76
+ Before you set `pack` on a table, check what reads its files without the engine.
77
+
78
+ ## Packs need the engine service
79
+
80
+ The engine service, `datanotes serve`, keeps the packs. Obsidian’s plugin doesn’t, so it refuses the `pack` option and `pack_rows`.
81
+
82
+ The service also packs on its own. Once a day, after 10 minutes without writes, it runs `pack_rows` on every table with a `pack` option. The setting `autoPack: false` turns this off.
83
+
84
+ ## Merge packs changed on two devices
85
+
86
+ Two devices that add or change different rows of one pack shouldn’t get a git conflict. The engine has a git merge driver that merges a pack row by row, by `id`:
87
+
88
+ 1. A row changed on one side only takes that side.
89
+ 2. A row added on one side is added.
90
+ 3. A row removed on one side, and unchanged on the other, is removed.
91
+ 4. A row changed differently on both sides is a conflict in its section only.
92
+
93
+ [Git sync](https://datanotes.dev/administration/git-sync/) registers the driver in the vault’s repository. To register it by hand, see that page.
94
+
95
+ ## Choose the period of a pack
96
+
97
+ A table’s `pack` option names a date or date-time column and a period, such as `{started_at:YYYY-MM}`. Once a month is over, one note holds all its rows. `pack_after_days` sets how many days after the end of a period its rows wait, 7 by default.
98
+
99
+ **`create_table`**
100
+
101
+ ```json
102
+ {
103
+ "name": "reading",
104
+ "description": "Reading sessions: when I read, which book and for how long",
105
+ "time_column": "started_at",
106
+ "partition": "{started_at:YYYY}",
107
+ "pack": "{started_at:YYYY-MM}",
108
+ "pack_after_days": 3,
109
+ "columns": [
110
+ {
111
+ "name": "started_at",
112
+ "type": "datetime",
113
+ "required": true,
114
+ "description": "When the session started"
115
+ },
116
+ {
117
+ "name": "book",
118
+ "type": "string",
119
+ "required": true,
120
+ "description": "The book I read"
121
+ },
122
+ {
123
+ "name": "minutes",
124
+ "type": "integer",
125
+ "min": 1,
126
+ "description": "How long I read, in minutes"
127
+ }
128
+ ]
129
+ }
130
+ ```
131
+
132
+ Answer:
133
+
134
+ ```json
135
+ {
136
+ "ok": true,
137
+ "migration_id": "20261026T080043-create_table-create-table-main-public-readi"
138
+ }
139
+ ```
140
+
141
+ `datanotes/metastore.yaml` now reads:
142
+
143
+ ```yaml
144
+ # Metastore (Datanotes): catalogs, schemas, tables, columns, views.
145
+ id: 01a17739-dd7c-7689-914e-27b8f67b394c
146
+ config:
147
+ history: true
148
+
149
+
150
+ catalogs:
151
+ main:
152
+ schemas:
153
+ public:
154
+ tables:
155
+ reading:
156
+ description: "Reading sessions: when I read, which book and for how long"
157
+ options:
158
+ partition: "{started_at:YYYY}"
159
+ filename: "{started_at:YYYY-MM-DD[T]HHmm}"
160
+ archive: _archive
161
+ pack: "{started_at:YYYY-MM}"
162
+ pack_after_days: 3
163
+ columns:
164
+ - name: id
165
+ type: string
166
+ required: true
167
+ description: "Unique id of the row (UUIDv7): set when the row is created, never changes"
168
+ unique: true
169
+ identity: uuid
170
+ dense: true
171
+ - name: started_at
172
+ type: datetime
173
+ required: true
174
+ description: When the session started
175
+ - name: book
176
+ type: string
177
+ required: true
178
+ description: The book I read
179
+ - name: minutes
180
+ type: integer
181
+ min: 1
182
+ description: How long I read, in minutes
183
+ ```
184
+
185
+ `alter_table` changes them while no row is in a pack. Here the rows wait one day:
186
+
187
+ **`alter_table`**
188
+
189
+ ```json
190
+ { "table": "reading", "pack": "{started_at:YYYY-MM}", "pack_after_days": 1 }
191
+ ```
192
+
193
+ Answer:
194
+
195
+ ```json
196
+ { "ok": true, "planned_texts": ["datanotes/metastore.yaml"] }
197
+ ```
198
+
199
+ The template takes one date or date-time column and a format made of `YYYY`, `MM` and `DD`. Anything else is refused, with the table’s date columns in the hint:
200
+
201
+ **`alter_table`**
202
+
203
+ ```json
204
+ { "table": "reading", "pack": "{minutes:YYYY-MM}" }
205
+ ```
206
+
207
+ Refused:
208
+
209
+ ```text
210
+ main.public.reading: "pack": "minutes" is a integer column, not a date
211
+ Hint: A pack holds the rows of a period: name a date or date-time column. The date columns of the table: started_at. For example: "{started_at:YYYY-MM}".
212
+ Docs: https://datanotes.dev/guides/database-objects/packs.md#choose-the-period-of-a-pack (offline: read_docs("guides/database-objects/packs#choose-the-period-of-a-pack"))
213
+ ```
214
+
215
+ ## Pack and unpack rows
216
+
217
+ `pack_rows` puts the rows of every closed period into their pack. A row changed in the last 24 hours waits for the next run. A dry run shows the packs first, and the rows that stay notes of their own, with the reason.
218
+
219
+ Four sessions of March and April 2025. The last one has a heading of its own:
220
+
221
+ **`batch`**
222
+
223
+ ```json
224
+ {
225
+ "operations": [
226
+ {
227
+ "op": "insert_row",
228
+ "table": "reading",
229
+ "values": { "started_at": "2025-03-02T21:00", "book": "Moby-Dick", "minutes": 40 }
230
+ },
231
+ {
232
+ "op": "insert_row",
233
+ "table": "reading",
234
+ "values": { "started_at": "2025-03-09T21:30", "book": "Moby-Dick", "minutes": 25 }
235
+ },
236
+ {
237
+ "op": "insert_row",
238
+ "table": "reading",
239
+ "values": { "started_at": "2025-03-16T22:00", "book": "Persuasion", "minutes": 30 },
240
+ "body": "Chapter 4, the walk to Winthrop.\n"
241
+ },
242
+ {
243
+ "op": "insert_row",
244
+ "table": "reading",
245
+ "values": { "started_at": "2025-04-06T21:00", "book": "Persuasion", "minutes": 35 },
246
+ "body": "# Notes\n\nAnne and Wentworth meet again.\n"
247
+ }
248
+ ]
249
+ }
250
+ ```
251
+
252
+ The notes in `datanotes/main/public/reading` haven’t changed since 2025-05-01.
253
+
254
+ **`pack_rows`**
255
+
256
+ ```json
257
+ { "table": "reading", "dry_run": true }
258
+ ```
259
+
260
+ Answer:
261
+
262
+ ```json
263
+ {
264
+ "ok": true,
265
+ "dry_run": true,
266
+ "packs": [
267
+ {
268
+ "pack": "datanotes/main/public/reading/reading 2025-03.md",
269
+ "key": "2025-03",
270
+ "rows": [
271
+ "datanotes/main/public/reading/2025/2025-03-02T2100.md",
272
+ "datanotes/main/public/reading/2025/2025-03-09T2130.md",
273
+ "datanotes/main/public/reading/2025/2025-03-16T2200.md"
274
+ ]
275
+ }
276
+ ],
277
+ "excluded": [
278
+ {
279
+ "path": "datanotes/main/public/reading/2025/2025-04-06T2100.md",
280
+ "reason": "its text has a # or ## heading: \"# Notes\""
281
+ }
282
+ ],
283
+ "open_rows": 0
284
+ }
285
+ ```
286
+
287
+ The same call without `dry_run` writes the pack. The three notes of March go, and one note holds them:
288
+
289
+ **`pack_rows`**
290
+
291
+ ```json
292
+ { "table": "reading" }
293
+ ```
294
+
295
+ Answer:
296
+
297
+ ```json
298
+ {
299
+ "ok": true,
300
+ "migration_id": "20261026T080044-pack_rows-pack-3-rows-of-reading",
301
+ "packs": [
302
+ {
303
+ "pack": "datanotes/main/public/reading/reading 2025-03.md",
304
+ "key": "2025-03",
305
+ "rows": [
306
+ "datanotes/main/public/reading/2025/2025-03-02T2100.md",
307
+ "datanotes/main/public/reading/2025/2025-03-09T2130.md",
308
+ "datanotes/main/public/reading/2025/2025-03-16T2200.md"
309
+ ]
310
+ }
311
+ ]
312
+ }
313
+ ```
314
+
315
+ `datanotes/main/public/reading/reading 2025-03.md` now reads:
316
+
317
+ ```markdown
318
+ ---
319
+ pack: main.public.reading
320
+ key: 2025-03
321
+ description: "Rows of main.public.reading for 2025-03. Each ## section is a row, its fields are key:: value. Read them with select_rows."
322
+ ---
323
+
324
+
325
+ ## 2025-03-02T2100
326
+ id:: 01a17739-dd92-7321-9568-708a1a72dffe
327
+ started_at:: 2025-03-02T21:00
328
+ book:: Moby-Dick
329
+ minutes:: 40
330
+
331
+
332
+
333
+
334
+
335
+
336
+ ## 2025-03-09T2130
337
+ id:: 01a17739-dd97-79de-b866-231408f32289
338
+ started_at:: 2025-03-09T21:30
339
+ book:: Moby-Dick
340
+ minutes:: 25
341
+
342
+
343
+
344
+
345
+
346
+
347
+ ## 2025-03-16T2200
348
+ id:: 01a17739-dd9b-780a-9860-0eb45495b7d9
349
+ started_at:: 2025-03-16T22:00
350
+ book:: Persuasion
351
+ minutes:: 30
352
+
353
+
354
+ Chapter 4, the walk to Winthrop.
355
+ ```
356
+
357
+ Nothing changes for the engine and its readers. `select_rows` gives each row at its own path, with its values and text:
358
+
359
+ **`select_rows`**
360
+
361
+ ```json
362
+ {
363
+ "table": "reading",
364
+ "columns": ["started_at", "book", "minutes", "file_modified"],
365
+ "order_by": "started_at"
366
+ }
367
+ ```
368
+
369
+ Answer:
370
+
371
+ ```json
372
+ {
373
+ "table": "main.public.reading",
374
+ "total": 4,
375
+ "rows": [
376
+ {
377
+ "path": "datanotes/main/public/reading/2025/2025-03-02T2100.md",
378
+ "started_at": "2025-03-02T21:00",
379
+ "book": "Moby-Dick",
380
+ "minutes": 40,
381
+ "file_modified": "2025-05-01T00:00:00Z"
382
+ },
383
+ {
384
+ "path": "datanotes/main/public/reading/2025/2025-03-09T2130.md",
385
+ "started_at": "2025-03-09T21:30",
386
+ "book": "Moby-Dick",
387
+ "minutes": 25,
388
+ "file_modified": "2025-05-01T00:00:00Z"
389
+ },
390
+ {
391
+ "path": "datanotes/main/public/reading/2025/2025-03-16T2200.md",
392
+ "started_at": "2025-03-16T22:00",
393
+ "book": "Persuasion",
394
+ "minutes": 30,
395
+ "file_modified": "2025-05-01T00:00:00Z"
396
+ },
397
+ {
398
+ "path": "datanotes/main/public/reading/2025/2025-04-06T2100.md",
399
+ "started_at": "2025-04-06T21:00",
400
+ "book": "Persuasion",
401
+ "minutes": 35,
402
+ "file_modified": "2025-05-01T00:00:00Z"
403
+ }
404
+ ]
405
+ }
406
+ ```
407
+
408
+ `unpack_rows` takes rows out again, as notes of their own with the text and modification time they had. `where` picks some rows, `key` one pack:
409
+
410
+ **`unpack_rows`**
411
+
412
+ ```json
413
+ { "table": "reading", "where": { "book": "Persuasion" } }
414
+ ```
415
+
416
+ Answer:
417
+
418
+ ```json
419
+ {
420
+ "ok": true,
421
+ "packs": [
422
+ {
423
+ "pack": "datanotes/main/public/reading/reading 2025-03.md",
424
+ "key": "2025-03",
425
+ "rows": ["datanotes/main/public/reading/2025/2025-03-16T2200.md"]
426
+ }
427
+ ]
428
+ }
429
+ ```
430
+
431
+ Both operations are journaled: `undo_migration` takes a pack apart, or packs the rows again. The change feed and the webhooks see no change of the rows, and no new version of the table is written.
432
+
433
+ ## Read and write a packed row
434
+
435
+ A packed row is still a row. A write changes its section of the pack. When the new values move it to another period or path, the row comes out as a note of its own.
436
+
437
+ **`update_row`**
438
+
439
+ ```json
440
+ {
441
+ "table": "reading",
442
+ "path": "datanotes/main/public/reading/2025/2025-03-09T2130.md",
443
+ "set": { "minutes": 45 }
444
+ }
445
+ ```
446
+
447
+ Answer:
448
+
449
+ ```json
450
+ {
451
+ "ok": true,
452
+ "migration_id": "20261026T080044-update_row-update-reading",
453
+ "planned_edits": [
454
+ {
455
+ "path": "datanotes/main/public/reading/2025/2025-03-09T2130.md",
456
+ "set": { "minutes": 45 },
457
+ "remove": []
458
+ }
459
+ ]
460
+ }
461
+ ```
462
+
463
+ `datanotes/main/public/reading/reading 2025-03.md` now reads:
464
+
465
+ ```markdown
466
+ ---
467
+ pack: main.public.reading
468
+ key: 2025-03
469
+ description: "Rows of main.public.reading for 2025-03. Each ## section is a row, its fields are key:: value. Read them with select_rows."
470
+ ---
471
+
472
+
473
+ ## 2025-03-02T2100
474
+ id:: 01a17739-dd92-7321-9568-708a1a72dffe
475
+ started_at:: 2025-03-02T21:00
476
+ book:: Moby-Dick
477
+ minutes:: 40
478
+
479
+
480
+
481
+
482
+
483
+
484
+ ## 2025-03-09T2130
485
+ id:: 01a17739-dd97-79de-b866-231408f32289
486
+ started_at:: 2025-03-09T21:30
487
+ book:: Moby-Dick
488
+ minutes:: 45
489
+ ```
490
+
491
+ ## Unpack before you change the table’s layout
492
+
493
+ A packed row keeps the path its values give it. So the rows must be notes of their own before the table moves, before its rows change folder or name, and before its `pack` option changes:
494
+
495
+ **`alter_table`**
496
+
497
+ ```json
498
+ { "table": "reading", "partition": "{started_at:YYYY/MM}" }
499
+ ```
500
+
501
+ Refused:
502
+
503
+ ```text
504
+ main.public.reading has 2 rows in 1 pack: the table moves or its rows change folder or name only while every row is a note of its own
505
+ Hint: Run unpack_rows on main.public.reading first, then alter the table, then pack_rows again.
506
+ Docs: https://datanotes.dev/guides/database-objects/packs.md#unpack-before-you-change-the-tables-layout (offline: read_docs("guides/database-objects/packs#unpack-before-you-change-the-tables-layout"))
507
+ ```
508
+
509
+ **`unpack_rows`**
510
+
511
+ ```json
512
+ { "table": "reading" }
513
+ ```
514
+
515
+ Answer:
516
+
517
+ ```json
518
+ {
519
+ "ok": true,
520
+ "packs": [
521
+ {
522
+ "pack": "datanotes/main/public/reading/reading 2025-03.md",
523
+ "key": "2025-03",
524
+ "rows": [
525
+ "datanotes/main/public/reading/2025/2025-03-02T2100.md",
526
+ "datanotes/main/public/reading/2025/2025-03-09T2130.md"
527
+ ]
528
+ }
529
+ ]
530
+ }
531
+ ```
532
+
533
+ **`alter_table`**
534
+
535
+ ```json
536
+ { "table": "reading", "partition": "{started_at:YYYY/MM}" }
537
+ ```
538
+
539
+ Answer:
540
+
541
+ ```json
542
+ {
543
+ "ok": true,
544
+ "planned_moves": [
545
+ {
546
+ "from": "datanotes/main/public/reading/2025/2025-03-02T2100.md",
547
+ "to": "datanotes/main/public/reading/2025/03/2025-03-02T2100.md"
548
+ },
549
+ {
550
+ "from": "datanotes/main/public/reading/2025/2025-03-09T2130.md",
551
+ "to": "datanotes/main/public/reading/2025/03/2025-03-09T2130.md"
552
+ },
553
+ {
554
+ "from": "datanotes/main/public/reading/2025/2025-03-16T2200.md",
555
+ "to": "datanotes/main/public/reading/2025/03/2025-03-16T2200.md"
556
+ },
557
+ {
558
+ "from": "datanotes/main/public/reading/2025/2025-04-06T2100.md",
559
+ "to": "datanotes/main/public/reading/2025/04/2025-04-06T2100.md"
560
+ }
561
+ ]
562
+ }
563
+ ```