datanotes 2.0.1 → 3.0.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 (181) hide show
  1. package/NOTICE +9 -0
  2. package/README.md +207 -28
  3. package/SECURITY.md +92 -45
  4. package/dist/THIRD-PARTY-NOTICES.txt +682 -0
  5. package/dist/api/catalog.d.cts +69 -0
  6. package/dist/api/catalog.d.ts +50 -2
  7. package/dist/api/cli-options.d.cts +39 -0
  8. package/dist/api/cli-options.d.ts +39 -0
  9. package/dist/api/client.d.cts +89 -0
  10. package/dist/api/client.d.ts +19 -30
  11. package/dist/api/docs.d.cts +211 -0
  12. package/dist/api/docs.d.ts +211 -0
  13. package/dist/api/guidance.d.cts +81 -0
  14. package/dist/api/guidance.d.ts +81 -0
  15. package/dist/api/index.cjs +6610 -2247
  16. package/dist/api/index.d.cts +13 -0
  17. package/dist/api/index.d.ts +10 -7
  18. package/dist/api/index.js +6610 -2247
  19. package/dist/api/local.cjs +118 -6
  20. package/dist/api/local.d.cts +95 -0
  21. package/dist/api/local.d.ts +70 -6
  22. package/dist/api/local.js +104 -6
  23. package/dist/api/mcp.cjs +17384 -0
  24. package/dist/api/mcp.d.cts +108 -0
  25. package/dist/api/mcp.d.ts +108 -0
  26. package/dist/api/mcp.js +17334 -0
  27. package/dist/api/otlp.d.cts +65 -0
  28. package/dist/api/redact.d.cts +17 -0
  29. package/dist/api/redact.d.ts +17 -0
  30. package/dist/api/requests.d.cts +26 -0
  31. package/dist/api/requests.d.ts +14 -16
  32. package/dist/api/results.d.cts +16 -0
  33. package/dist/api/results.d.ts +2 -1
  34. package/dist/api/tokens.d.cts +7 -0
  35. package/dist/datanotes.cjs +62511 -28069
  36. package/dist/docs/administration/configuration.md +59 -0
  37. package/dist/docs/administration/docker.md +80 -0
  38. package/dist/docs/administration/git-sync.md +71 -0
  39. package/dist/docs/administration/limits.md +69 -0
  40. package/dist/docs/administration/run-the-service.md +219 -0
  41. package/dist/docs/administration/status.md +61 -0
  42. package/dist/docs/administration/use-with-obsidian.md +75 -0
  43. package/dist/docs/api/operations/add_column.md +49 -0
  44. package/dist/docs/api/operations/add_constraint.md +55 -0
  45. package/dist/docs/api/operations/adopt_rows.md +50 -0
  46. package/dist/docs/api/operations/align_relations.md +41 -0
  47. package/dist/docs/api/operations/alter_column.md +46 -0
  48. package/dist/docs/api/operations/alter_table.md +67 -0
  49. package/dist/docs/api/operations/batch.md +48 -0
  50. package/dist/docs/api/operations/changes_stream.md +28 -0
  51. package/dist/docs/api/operations/check_table.md +29 -0
  52. package/dist/docs/api/operations/clone_table.md +48 -0
  53. package/dist/docs/api/operations/comment_on.md +42 -0
  54. package/dist/docs/api/operations/configure_database.md +52 -0
  55. package/dist/docs/api/operations/create_table.md +78 -0
  56. package/dist/docs/api/operations/create_view.md +54 -0
  57. package/dist/docs/api/operations/create_webhook.md +37 -0
  58. package/dist/docs/api/operations/define_base.md +52 -0
  59. package/dist/docs/api/operations/delete_rows.md +43 -0
  60. package/dist/docs/api/operations/delete_webhook.md +31 -0
  61. package/dist/docs/api/operations/describe_database.md +30 -0
  62. package/dist/docs/api/operations/describe_history.md +40 -0
  63. package/dist/docs/api/operations/describe_semantic_index.md +24 -0
  64. package/dist/docs/api/operations/describe_table.md +38 -0
  65. package/dist/docs/api/operations/docs.md +13 -0
  66. package/dist/docs/api/operations/drop_column.md +41 -0
  67. package/dist/docs/api/operations/drop_constraint.md +39 -0
  68. package/dist/docs/api/operations/drop_table.md +43 -0
  69. package/dist/docs/api/operations/drop_view.md +38 -0
  70. package/dist/docs/api/operations/engine_status.md +23 -0
  71. package/dist/docs/api/operations/graph_neighbors.md +32 -0
  72. package/dist/docs/api/operations/graph_path.md +32 -0
  73. package/dist/docs/api/operations/graph_traverse.md +43 -0
  74. package/dist/docs/api/operations/identify_session.md +24 -0
  75. package/dist/docs/api/operations/infer_table.md +43 -0
  76. package/dist/docs/api/operations/insert_row.md +48 -0
  77. package/dist/docs/api/operations/list_changes.md +35 -0
  78. package/dist/docs/api/operations/list_folder.md +28 -0
  79. package/dist/docs/api/operations/list_link_targets.md +44 -0
  80. package/dist/docs/api/operations/list_migrations.md +35 -0
  81. package/dist/docs/api/operations/list_tables.md +32 -0
  82. package/dist/docs/api/operations/list_webhooks.md +23 -0
  83. package/dist/docs/api/operations/manual_index.md +14 -0
  84. package/dist/docs/api/operations/manual_page.md +22 -0
  85. package/dist/docs/api/operations/merge_rows.md +44 -0
  86. package/dist/docs/api/operations/move_rows.md +52 -0
  87. package/dist/docs/api/operations/openapi.md +13 -0
  88. package/dist/docs/api/operations/openapi_post.md +20 -0
  89. package/dist/docs/api/operations/read_note.md +29 -0
  90. package/dist/docs/api/operations/recompute_columns.md +44 -0
  91. package/dist/docs/api/operations/record_tool_call.md +36 -0
  92. package/dist/docs/api/operations/rename_column.md +44 -0
  93. package/dist/docs/api/operations/rename_value.md +45 -0
  94. package/dist/docs/api/operations/restore_database.md +51 -0
  95. package/dist/docs/api/operations/restore_table.md +46 -0
  96. package/dist/docs/api/operations/search_notes.md +28 -0
  97. package/dist/docs/api/operations/select_rows.md +61 -0
  98. package/dist/docs/api/operations/semantic_search.md +34 -0
  99. package/dist/docs/api/operations/semantic_search_docs.md +26 -0
  100. package/dist/docs/api/operations/set_table_bases.md +44 -0
  101. package/dist/docs/api/operations/status.md +13 -0
  102. package/dist/docs/api/operations/stop_engine.md +23 -0
  103. package/dist/docs/api/operations/sync_semantic_index.md +33 -0
  104. package/dist/docs/api/operations/table_changes.md +31 -0
  105. package/dist/docs/api/operations/tags/auxiliary.md +14 -0
  106. package/dist/docs/api/operations/tags/change-feed.md +8 -0
  107. package/dist/docs/api/operations/tags/history.md +17 -0
  108. package/dist/docs/api/operations/tags/queries.md +15 -0
  109. package/dist/docs/api/operations/tags/rows-dml.md +16 -0
  110. package/dist/docs/api/operations/tags/schema-ddl.md +22 -0
  111. package/dist/docs/api/operations/tags/service.md +15 -0
  112. package/dist/docs/api/operations/tags/tokens.md +13 -0
  113. package/dist/docs/api/operations/tokens_create.md +34 -0
  114. package/dist/docs/api/operations/tokens_delete.md +29 -0
  115. package/dist/docs/api/operations/tokens_list.md +21 -0
  116. package/dist/docs/api/operations/tokens_me.md +20 -0
  117. package/dist/docs/api/operations/tokens_rotate.md +29 -0
  118. package/dist/docs/api/operations/tokens_update.md +34 -0
  119. package/dist/docs/api/operations/undo_migration.md +46 -0
  120. package/dist/docs/api/operations/update_row.md +60 -0
  121. package/dist/docs/api/operations/update_rows.md +46 -0
  122. package/dist/docs/api/operations/vacuum_table.md +37 -0
  123. package/dist/docs/api.md +135 -0
  124. package/dist/docs/develop/build-an-app.md +320 -0
  125. package/dist/docs/develop/build-on-datanotes.md +138 -0
  126. package/dist/docs/develop/change-feed-and-webhooks.md +70 -0
  127. package/dist/docs/develop/client.md +116 -0
  128. package/dist/docs/develop/concurrency.md +153 -0
  129. package/dist/docs/develop/http-api.md +79 -0
  130. package/dist/docs/develop/telemetry.md +86 -0
  131. package/dist/docs/guides/agents/docs-offline.md +99 -0
  132. package/dist/docs/guides/agents/how-agents-use-datanotes.md +124 -0
  133. package/dist/docs/guides/agents/prompts.md +495 -0
  134. package/dist/docs/guides/agents/reading-errors.md +112 -0
  135. package/dist/docs/guides/data-quality/check-constraints.md +379 -0
  136. package/dist/docs/guides/data-quality/check-table.md +63 -0
  137. package/dist/docs/guides/data-quality/hand-edits.md +507 -0
  138. package/dist/docs/guides/data-quality/sync-between-devices.md +51 -0
  139. package/dist/docs/guides/data-quality/validation.md +90 -0
  140. package/dist/docs/guides/database-objects/bases.md +480 -0
  141. package/dist/docs/guides/database-objects/catalogs-and-schemas.md +897 -0
  142. package/dist/docs/guides/database-objects/columns.md +1332 -0
  143. package/dist/docs/guides/database-objects/relations.md +1454 -0
  144. package/dist/docs/guides/database-objects/tables.md +1404 -0
  145. package/dist/docs/guides/database-objects/views.md +1003 -0
  146. package/dist/docs/guides/history/back-up.md +195 -0
  147. package/dist/docs/guides/history/change-data-feed.md +247 -0
  148. package/dist/docs/guides/history/journal-and-undo.md +262 -0
  149. package/dist/docs/guides/history/restore.md +341 -0
  150. package/dist/docs/guides/history/table-history.md +657 -0
  151. package/dist/docs/guides/history/vacuum.md +35 -0
  152. package/dist/docs/guides/read-and-write/automatic-links.md +768 -0
  153. package/dist/docs/guides/read-and-write/batch.md +139 -0
  154. package/dist/docs/guides/read-and-write/import-notes.md +896 -0
  155. package/dist/docs/guides/read-and-write/joins-and-aggregates.md +251 -0
  156. package/dist/docs/guides/read-and-write/read-and-filter.md +536 -0
  157. package/dist/docs/guides/read-and-write/write-rows.md +573 -0
  158. package/dist/docs/guides/search/link-graph.md +146 -0
  159. package/dist/docs/guides/search/semantic.md +133 -0
  160. package/dist/docs/guides/search/text.md +142 -0
  161. package/dist/docs/guides/security/security-model.md +110 -0
  162. package/dist/docs/guides/security/tokens.md +60 -0
  163. package/dist/docs/guides/security/who-did-what.md +42 -0
  164. package/dist/docs/index.md +113 -0
  165. package/dist/docs/llms.txt +33 -0
  166. package/dist/docs/reference/cli.md +136 -0
  167. package/dist/docs/reference/compatibility.md +63 -0
  168. package/dist/docs/reference/errors.md +72 -0
  169. package/dist/docs/reference/glossary.md +161 -0
  170. package/dist/docs/reference/operations/auxiliary.md +116 -0
  171. package/dist/docs/reference/operations/history.md +168 -0
  172. package/dist/docs/reference/operations/queries.md +169 -0
  173. package/dist/docs/reference/operations/rows.md +185 -0
  174. package/dist/docs/reference/operations/schema.md +316 -0
  175. package/dist/docs/reference/operations.md +96 -0
  176. package/dist/docs/reference/settings.md +31 -0
  177. package/dist/docs/release-notes.md +67 -0
  178. package/dist/docs/start/connect-agents.md +204 -0
  179. package/dist/docs/start/how-it-works.md +317 -0
  180. package/dist/docs/start/quick-start.md +126 -0
  181. package/package.json +58 -14
@@ -0,0 +1,1454 @@
1
+ # Relations and hierarchies
2
+
3
+ > Link rows to rows of other tables, keep two-way links in step, take allowed values from a table, build trees and limit link targets.
4
+
5
+ A link column holds a wikilink to a note, such as `[[Charade]]`, and `ref` names the table that note must be a row of. The link points to a row of the other table, and you reach that row’s columns through the link.
6
+
7
+ The examples on this page share one diary of classic films, with actors, films, viewings, tags and genres.
8
+
9
+ ## Point a column to a row of another table
10
+
11
+ A viewing is of one film, so its `film` column is a `link` whose `ref` is the film table. The value must link to a note that is a row of `film`.
12
+
13
+ **`create_table`**
14
+
15
+ ```json
16
+ {
17
+ "name": "watch",
18
+ "description": "Each time I watch a film",
19
+ "columns": [
20
+ {
21
+ "name": "film",
22
+ "type": "link",
23
+ "ref": "main.public.film",
24
+ "required": true,
25
+ "description": "The film I watched"
26
+ },
27
+ {
28
+ "name": "where",
29
+ "type": "enum",
30
+ "values": ["cinema", "home", "outdoor screening"],
31
+ "required": true,
32
+ "description": "The kind of place I watched it in"
33
+ },
34
+ {
35
+ "name": "place",
36
+ "type": "string",
37
+ "description": "The cinema, park or room, in my words"
38
+ },
39
+ { "name": "with", "type": "string", "description": "Who watched it with me" },
40
+ {
41
+ "name": "occurred_at",
42
+ "type": "datetime",
43
+ "required": true,
44
+ "description": "When the film started"
45
+ }
46
+ ],
47
+ "time_column": "occurred_at"
48
+ }
49
+ ```
50
+
51
+ Answer:
52
+
53
+ ```json
54
+ {
55
+ "ok": true,
56
+ "op": "create_table",
57
+ "planned_texts": ["datanotes/metastore.yaml"],
58
+ "commits": [{ "table": "main.public.watch", "version": 0, "operation": "CREATE TABLE" }],
59
+ "migration_id": "20261012T071949-create_table-create-table-main-public-watch"
60
+ }
61
+ ```
62
+
63
+ **`insert_row`**
64
+
65
+ ```json
66
+ {
67
+ "table": "watch",
68
+ "values": {
69
+ "film": "[[His Girl Friday]]",
70
+ "where": "cinema",
71
+ "place": "the film club downtown",
72
+ "with": "my sister",
73
+ "occurred_at": "2026-03-14T20:30"
74
+ },
75
+ "body": "A full hall, and everyone laughed at the same lines.\n"
76
+ }
77
+ ```
78
+
79
+ Answer:
80
+
81
+ ```json
82
+ {
83
+ "ok": true,
84
+ "op": "insert_row",
85
+ "files_created": 1,
86
+ "planned_creates": ["datanotes/main/public/watch/2026-03-14T2030.md"],
87
+ "commits": [{ "table": "main.public.watch", "version": 1, "operation": "WRITE" }],
88
+ "migration_id": "20261012T071949-insert_row-insert-into-watch"
89
+ }
90
+ ```
91
+
92
+ `datanotes/main/public/watch/2026-03-14T2030.md` now reads:
93
+
94
+ ```markdown
95
+ ---
96
+ table: main.public.watch
97
+ id: 01a12efb-6265-7094-88e9-b7fc57c84b37
98
+ film: "[[His Girl Friday]]"
99
+ where: cinema
100
+ place: the film club downtown
101
+ with: my sister
102
+ occurred_at: 2026-03-14T20:30
103
+ ---
104
+ A full hall, and everyone laughed at the same lines.
105
+ ```
106
+
107
+ **`insert_row`**
108
+
109
+ ```json
110
+ {
111
+ "table": "watch",
112
+ "values": {
113
+ "film": "[[Charade]]",
114
+ "where": "home",
115
+ "place": "my living room",
116
+ "occurred_at": "2026-09-26T21:30"
117
+ }
118
+ }
119
+ ```
120
+
121
+ The engine refuses a link to a note that doesn’t exist (`broken_link`):
122
+
123
+ **`insert_row`**
124
+
125
+ ```json
126
+ {
127
+ "table": "watch",
128
+ "values": { "film": "[[Charde]]", "where": "home", "occurred_at": "2026-09-27T21:00" }
129
+ }
130
+ ```
131
+
132
+ Refused:
133
+
134
+ ```text
135
+ insert_row refused — nothing was written:
136
+ invalid datanotes/main/public/watch/2026-09-27T2100.md: film link does not resolve: Charde
137
+ Hint: Link to a note that exists, as "[[Note name]]": list_link_targets(table, column) lists the rows the column may point to. Then call insert_row again (dry_run: true checks without writing).
138
+ Docs: https://datanotes.dev/guides/database-objects/relations.md#point-a-column-to-a-row-of-another-table (offline: read_docs("guides/database-objects/relations#point-a-column-to-a-row-of-another-table"))
139
+ ```
140
+
141
+ It also refuses a link to a note of another table (`ref`), here an actor:
142
+
143
+ **`insert_row`**
144
+
145
+ ```json
146
+ {
147
+ "table": "watch",
148
+ "values": {
149
+ "film": "[[Cary Grant]]",
150
+ "where": "home",
151
+ "occurred_at": "2026-09-27T21:00"
152
+ }
153
+ }
154
+ ```
155
+
156
+ Refused:
157
+
158
+ ```text
159
+ insert_row refused — nothing was written:
160
+ invalid datanotes/main/public/watch/2026-09-27T2100.md: film link must point to a row of table "main.public.film"
161
+ Hint: Link to a row of the table the column points to (its ref) that its ref_where / ref_by allow: list_link_targets(table, column) lists them. Then call insert_row again (dry_run: true checks without writing).
162
+ Docs: https://datanotes.dev/guides/database-objects/relations.md#limit-link-targets-with-ref_by-and-ref_where (offline: read_docs("guides/database-objects/relations#limit-link-targets-with-ref_by-and-ref_where"))
163
+ ```
164
+
165
+ ## Link to several rows with a list of links
166
+
167
+ A film has several actors, so `cast` is a `list` of `link` items, and the engine checks each one against `ref`. `inverse: films` names the actor column that links back, and from then on the engine keeps both sides in step. Both columns must exist, and you declare `inverse` on one side only.
168
+
169
+ **`add_column`**
170
+
171
+ ```json
172
+ {
173
+ "table": "actor",
174
+ "column": {
175
+ "name": "films",
176
+ "type": "list",
177
+ "items": "link",
178
+ "ref": "main.public.film",
179
+ "description": "The films the actor plays in"
180
+ }
181
+ }
182
+ ```
183
+
184
+ Answer:
185
+
186
+ ```json
187
+ {
188
+ "ok": true,
189
+ "op": "add_column",
190
+ "planned_texts": ["datanotes/metastore.yaml"],
191
+ "commits": [{ "table": "main.public.actor", "version": 4, "operation": "ADD COLUMNS" }],
192
+ "migration_id": "20261012T071949-add_column-add-column-films-to-actor"
193
+ }
194
+ ```
195
+
196
+ **`add_column`**
197
+
198
+ ```json
199
+ {
200
+ "table": "film",
201
+ "column": {
202
+ "name": "cast",
203
+ "type": "list",
204
+ "items": "link",
205
+ "ref": "main.public.actor",
206
+ "inverse": "films",
207
+ "description": "The actors in the film"
208
+ }
209
+ }
210
+ ```
211
+
212
+ Answer:
213
+
214
+ ```json
215
+ {
216
+ "ok": true,
217
+ "op": "add_column",
218
+ "planned_texts": ["datanotes/metastore.yaml"],
219
+ "commits": [{ "table": "main.public.film", "version": 4, "operation": "ADD COLUMNS" }],
220
+ "migration_id": "20261012T071949-add_column-add-column-cast-to-film"
221
+ }
222
+ ```
223
+
224
+ **`update_row`**
225
+
226
+ ```json
227
+ {
228
+ "table": "film",
229
+ "path": "datanotes/main/public/film/Charade.md",
230
+ "set": { "cast": ["[[Cary Grant]]", "[[Audrey Hepburn]]"] }
231
+ }
232
+ ```
233
+
234
+ Answer:
235
+
236
+ ```json
237
+ {
238
+ "ok": true,
239
+ "op": "update_row",
240
+ "notes_changed": 3,
241
+ "planned_edits": [
242
+ {
243
+ "path": "datanotes/main/public/film/Charade.md",
244
+ "set": { "cast": ["[[Cary Grant]]", "[[Audrey Hepburn]]"] },
245
+ "remove": []
246
+ },
247
+ {
248
+ "path": "datanotes/main/public/actor/Cary Grant.md",
249
+ "set": { "films": ["[[Charade]]"] },
250
+ "remove": []
251
+ },
252
+ {
253
+ "path": "datanotes/main/public/actor/Audrey Hepburn.md",
254
+ "set": { "films": ["[[Charade]]"] },
255
+ "remove": []
256
+ }
257
+ ],
258
+ "commits": [
259
+ { "table": "main.public.actor", "version": 5, "operation": "UPDATE" },
260
+ { "table": "main.public.film", "version": 5, "operation": "UPDATE" }
261
+ ],
262
+ "migration_id": "20261012T071949-update_row-update-film"
263
+ }
264
+ ```
265
+
266
+ `datanotes/main/public/film/Charade.md` now reads:
267
+
268
+ ```markdown
269
+ ---
270
+ table: main.public.film
271
+ id: 01a12efb-6245-7d26-b6fb-0820654b699a
272
+ title: Charade
273
+ year: 1963
274
+ minutes: 113
275
+ cast:
276
+ - "[[Cary Grant]]"
277
+ - "[[Audrey Hepburn]]"
278
+ ---
279
+ ```
280
+
281
+ The same write added the film to both actors, in one operation with one undo:
282
+
283
+ **`read_note`**
284
+
285
+ ```json
286
+ { "path": "datanotes/main/public/actor/Cary Grant.md" }
287
+ ```
288
+
289
+ Answer:
290
+
291
+ ```markdown
292
+ ---
293
+ table: main.public.actor
294
+ id: 01a12efb-621d-71b4-a0ec-578ef615f6b0
295
+ name: Cary Grant
296
+ born: 1904-01-18
297
+ films:
298
+ - "[[Charade]]"
299
+ ---
300
+ ```
301
+
302
+ One wrong item makes the engine refuse the whole list:
303
+
304
+ **`update_row`**
305
+
306
+ ```json
307
+ {
308
+ "table": "film",
309
+ "path": "datanotes/main/public/film/His Girl Friday.md",
310
+ "set": { "cast": ["[[Cary Grant]]", "[[Charade]]"] }
311
+ }
312
+ ```
313
+
314
+ Refused:
315
+
316
+ ```text
317
+ update_row refused — nothing was written:
318
+ invalid datanotes/main/public/film/His Girl Friday.md: cast[1] link must point to a row of table "main.public.actor"
319
+ Hint: Link to a row of the table the column points to (its ref) that its ref_where / ref_by allow: list_link_targets(table, column) lists them. Then call update_row again (dry_run: true checks without writing).
320
+ Docs: https://datanotes.dev/guides/database-objects/relations.md#limit-link-targets-with-ref_by-and-ref_where (offline: read_docs("guides/database-objects/relations#limit-link-targets-with-ref_by-and-ref_where"))
321
+ ```
322
+
323
+ **`update_row`**
324
+
325
+ ```json
326
+ {
327
+ "table": "film",
328
+ "path": "datanotes/main/public/film/His Girl Friday.md",
329
+ "set": { "cast": ["[[Cary Grant]]", "[[Rosalind Russell]]"] }
330
+ }
331
+ ```
332
+
333
+ ## Read linked rows in the same call
334
+
335
+ `expand` replaces each link with the linked row’s columns, so one call returns the films with their actors.
336
+
337
+ **`select_rows`**
338
+
339
+ ```json
340
+ {
341
+ "table": "film",
342
+ "columns": ["title", "cast"],
343
+ "expand": ["cast"],
344
+ "where": { "title": "Charade" }
345
+ }
346
+ ```
347
+
348
+ Answer:
349
+
350
+ ```json
351
+ {
352
+ "table": "main.public.film",
353
+ "total": 1,
354
+ "rows": [
355
+ {
356
+ "path": "datanotes/main/public/film/Charade.md",
357
+ "title": "Charade",
358
+ "cast": [
359
+ {
360
+ "path": "datanotes/main/public/actor/Cary Grant.md",
361
+ "table": "main.public.actor",
362
+ "id": "01a12efb-621d-71b4-a0ec-578ef615f6b0",
363
+ "name": "Cary Grant",
364
+ "born": "1904-01-18",
365
+ "films": ["[[Charade]]", "[[His Girl Friday]]"]
366
+ },
367
+ {
368
+ "path": "datanotes/main/public/actor/Audrey Hepburn.md",
369
+ "table": "main.public.actor",
370
+ "id": "01a12efb-622a-7ee5-b08f-81dd7aa17fb1",
371
+ "name": "Audrey Hepburn",
372
+ "born": "1929-05-04",
373
+ "films": ["[[Charade]]"]
374
+ }
375
+ ]
376
+ }
377
+ ]
378
+ }
379
+ ```
380
+
381
+ ## Repair two-way links with `align_relations`
382
+
383
+ Writes through the engine keep both sides of an `inverse` link in step, and the engine mirrors links you edit by hand about 2s later. When you declare `inverse` on data that already exists, `align_relations` adds the missing reciprocal links. `check_table` reports the missing ones first, with code `inverse`.
384
+
385
+ **`add_column`**
386
+
387
+ ```json
388
+ {
389
+ "table": "film",
390
+ "column": {
391
+ "name": "see_also",
392
+ "type": "list",
393
+ "items": "link",
394
+ "ref": "self",
395
+ "description": "Films to watch next if I liked this one"
396
+ }
397
+ }
398
+ ```
399
+
400
+ **`update_row`**
401
+
402
+ ```json
403
+ {
404
+ "table": "film",
405
+ "path": "datanotes/main/public/film/Charade.md",
406
+ "set": { "see_also": ["[[His Girl Friday]]"] }
407
+ }
408
+ ```
409
+
410
+ Charade points to His Girl Friday, but His Girl Friday doesn’t point back. Declare `see_also` symmetric:
411
+
412
+ **`alter_column`**
413
+
414
+ ```json
415
+ { "table": "film", "column": "see_also", "changes": { "inverse": "see_also" } }
416
+ ```
417
+
418
+ Answer:
419
+
420
+ ```json
421
+ {
422
+ "ok": true,
423
+ "op": "alter_column",
424
+ "planned_texts": ["datanotes/metastore.yaml"],
425
+ "commits": [{ "table": "main.public.film", "version": 9, "operation": "CHANGE COLUMN" }],
426
+ "migration_id": "20261012T071949-alter_column-alter-column-see_also-in-film"
427
+ }
428
+ ```
429
+
430
+ **`check_table`**
431
+
432
+ ```json
433
+ { "table": "film" }
434
+ ```
435
+
436
+ Answer:
437
+
438
+ ```json
439
+ {
440
+ "table": "film",
441
+ "rows_checked": 3,
442
+ "total": 1,
443
+ "violations": [
444
+ {
445
+ "path": "datanotes/main/public/film/Charade.md",
446
+ "violations": [
447
+ {
448
+ "column": "see_also",
449
+ "code": "inverse",
450
+ "message": "datanotes/main/public/film/His Girl Friday.md does not link back in \"see_also\" (align_relations adds it)",
451
+ "value": "datanotes/main/public/film/His Girl Friday.md"
452
+ }
453
+ ]
454
+ }
455
+ ]
456
+ }
457
+ ```
458
+
459
+ **`align_relations`**
460
+
461
+ ```json
462
+ { "table": "film" }
463
+ ```
464
+
465
+ Answer:
466
+
467
+ ```json
468
+ {
469
+ "ok": true,
470
+ "op": "align_relations",
471
+ "notes_changed": 1,
472
+ "planned_edits": [
473
+ {
474
+ "path": "datanotes/main/public/film/His Girl Friday.md",
475
+ "set": { "see_also": ["[[Charade]]"] },
476
+ "remove": []
477
+ }
478
+ ],
479
+ "commits": [
480
+ { "table": "main.public.film", "version": 10, "operation": "ALIGN RELATIONS" }
481
+ ],
482
+ "migration_id": "20261012T071949-align_relations-align-inverse-links-of-main-pu"
483
+ }
484
+ ```
485
+
486
+ **`select_rows`**
487
+
488
+ ```json
489
+ { "table": "film", "columns": ["title", "see_also"] }
490
+ ```
491
+
492
+ Answer:
493
+
494
+ ```json
495
+ {
496
+ "table": "main.public.film",
497
+ "total": 3,
498
+ "rows": [
499
+ {
500
+ "path": "datanotes/main/public/film/Charade.md",
501
+ "title": "Charade",
502
+ "see_also": ["[[His Girl Friday]]"]
503
+ },
504
+ {
505
+ "path": "datanotes/main/public/film/His Girl Friday.md",
506
+ "title": "His Girl Friday",
507
+ "see_also": ["[[Charade]]"]
508
+ },
509
+ {
510
+ "path": "datanotes/main/public/film/Night of the Living Dead.md",
511
+ "title": "Night of the Living Dead",
512
+ "see_also": null
513
+ }
514
+ ]
515
+ }
516
+ ```
517
+
518
+ ## Take allowed values from another table
519
+
520
+ `vocabulary` limits a text column, or a list of text, to the names of another table’s rows, without links. Each value must be the `name` of a row of that table, so adding a row to the vocabulary table adds an allowed value.
521
+
522
+ **`create_table`**
523
+
524
+ ```json
525
+ {
526
+ "name": "tag",
527
+ "description": "Words I tag films with",
528
+ "columns": [
529
+ {
530
+ "name": "name",
531
+ "type": "string",
532
+ "required": true,
533
+ "unique": true,
534
+ "description": "The tag, as I write it"
535
+ }
536
+ ],
537
+ "filename": "{name}"
538
+ }
539
+ ```
540
+
541
+ **`insert_row`**
542
+
543
+ ```json
544
+ { "table": "tag", "values": { "name": "funny" } }
545
+ ```
546
+
547
+ **`insert_row`**
548
+
549
+ ```json
550
+ { "table": "tag", "values": { "name": "rewatch" } }
551
+ ```
552
+
553
+ **`add_column`**
554
+
555
+ ```json
556
+ {
557
+ "table": "film",
558
+ "column": {
559
+ "name": "tags",
560
+ "type": "list",
561
+ "items": "string",
562
+ "vocabulary": "main.public.tag",
563
+ "description": "My tags for the film, from the tag table"
564
+ }
565
+ }
566
+ ```
567
+
568
+ Answer:
569
+
570
+ ```json
571
+ {
572
+ "ok": true,
573
+ "op": "add_column",
574
+ "planned_texts": ["datanotes/metastore.yaml"],
575
+ "commits": [{ "table": "main.public.film", "version": 11, "operation": "ADD COLUMNS" }],
576
+ "migration_id": "20261012T071949-add_column-add-column-tags-to-film"
577
+ }
578
+ ```
579
+
580
+ **`update_row`**
581
+
582
+ ```json
583
+ {
584
+ "table": "film",
585
+ "path": "datanotes/main/public/film/His Girl Friday.md",
586
+ "set": { "tags": ["funny", "fast talk"] }
587
+ }
588
+ ```
589
+
590
+ Refused:
591
+
592
+ ```text
593
+ update_row refused — nothing was written:
594
+ invalid datanotes/main/public/film/His Girl Friday.md: tags[1] "fast talk" is not in main.public.tag (add it there, or use one of: funny, rewatch)
595
+ Hint: Use one of the values listed in the message, or first insert the new value as a row of the vocabulary table. Then call update_row again (dry_run: true checks without writing).
596
+ Docs: https://datanotes.dev/guides/database-objects/relations.md#take-allowed-values-from-another-table (offline: read_docs("guides/database-objects/relations#take-allowed-values-from-another-table"))
597
+ ```
598
+
599
+ **`insert_row`**
600
+
601
+ ```json
602
+ { "table": "tag", "values": { "name": "fast talk" } }
603
+ ```
604
+
605
+ Once you add `fast talk` to the tag table, the same write goes through:
606
+
607
+ **`update_row`**
608
+
609
+ ```json
610
+ {
611
+ "table": "film",
612
+ "path": "datanotes/main/public/film/His Girl Friday.md",
613
+ "set": { "tags": ["funny", "fast talk"] }
614
+ }
615
+ ```
616
+
617
+ Answer:
618
+
619
+ ```json
620
+ {
621
+ "ok": true,
622
+ "op": "update_row",
623
+ "notes_changed": 1,
624
+ "planned_edits": [
625
+ {
626
+ "path": "datanotes/main/public/film/His Girl Friday.md",
627
+ "set": { "tags": ["funny", "fast talk"] },
628
+ "remove": []
629
+ }
630
+ ],
631
+ "commits": [{ "table": "main.public.film", "version": 12, "operation": "UPDATE" }],
632
+ "migration_id": "20261012T071949-update_row-update-film-4"
633
+ }
634
+ ```
635
+
636
+ ## Build a tree of rows with a parent link
637
+
638
+ `hierarchy: true` makes a link column a parent link to the same table (`ref: self`). A row can never be its own ancestor. The `within` filter on the parent column returns every row below a given one, at any depth. Here the genres form a tree: Cinema holds the sound era, which holds screwball comedy.
639
+
640
+ **`create_table`**
641
+
642
+ ```json
643
+ {
644
+ "name": "genre",
645
+ "description": "Eras and genres of cinema, as a tree",
646
+ "columns": [
647
+ {
648
+ "name": "name",
649
+ "type": "string",
650
+ "required": true,
651
+ "description": "The name of the era or genre"
652
+ },
653
+ {
654
+ "name": "kind",
655
+ "type": "enum",
656
+ "values": ["cinema", "era", "genre"],
657
+ "required": true,
658
+ "description": "The level in the tree: cinema, an era or a genre"
659
+ },
660
+ {
661
+ "name": "parent",
662
+ "type": "link",
663
+ "ref": "self",
664
+ "hierarchy": true,
665
+ "description": "The era or cinema one level up"
666
+ }
667
+ ],
668
+ "filename": "{name}"
669
+ }
670
+ ```
671
+
672
+ Answer:
673
+
674
+ ```json
675
+ {
676
+ "ok": true,
677
+ "op": "create_table",
678
+ "planned_texts": ["datanotes/metastore.yaml"],
679
+ "commits": [{ "table": "main.public.genre", "version": 0, "operation": "CREATE TABLE" }],
680
+ "migration_id": "20261012T071949-create_table-create-table-main-public-genre"
681
+ }
682
+ ```
683
+
684
+ **`insert_row`**
685
+
686
+ ```json
687
+ { "table": "genre", "values": { "name": "Cinema", "kind": "cinema" } }
688
+ ```
689
+
690
+ **`insert_row`**
691
+
692
+ ```json
693
+ {
694
+ "table": "genre",
695
+ "values": { "name": "Sound film", "kind": "era", "parent": "[[Cinema]]" }
696
+ }
697
+ ```
698
+
699
+ **`insert_row`**
700
+
701
+ ```json
702
+ {
703
+ "table": "genre",
704
+ "values": { "name": "Screwball comedy", "kind": "genre", "parent": "[[Sound film]]" }
705
+ }
706
+ ```
707
+
708
+ **`select_rows`**
709
+
710
+ ```json
711
+ {
712
+ "table": "genre",
713
+ "columns": ["name", "parent"],
714
+ "where": { "parent": { "within": "[[Cinema]]" } }
715
+ }
716
+ ```
717
+
718
+ Answer:
719
+
720
+ ```json
721
+ {
722
+ "table": "main.public.genre",
723
+ "total": 2,
724
+ "rows": [
725
+ {
726
+ "path": "datanotes/main/public/genre/Screwball comedy.md",
727
+ "name": "Screwball comedy",
728
+ "parent": "[[Sound film]]"
729
+ },
730
+ {
731
+ "path": "datanotes/main/public/genre/Sound film.md",
732
+ "name": "Sound film",
733
+ "parent": "[[Cinema]]"
734
+ }
735
+ ]
736
+ }
737
+ ```
738
+
739
+ The engine refuses a loop:
740
+
741
+ **`update_row`**
742
+
743
+ ```json
744
+ {
745
+ "table": "genre",
746
+ "path": "datanotes/main/public/genre/Cinema.md",
747
+ "set": { "parent": "[[Screwball comedy]]" }
748
+ }
749
+ ```
750
+
751
+ Refused:
752
+
753
+ ```text
754
+ update_row refused — nothing was written:
755
+ invalid datanotes/main/public/genre/Cinema.md: parent would make "Cinema" its own ancestor (Cinema → Screwball comedy → Sound film → Cinema)
756
+ Hint: A row cannot be its own ancestor: point the hierarchy column to a row that is not below this one. Then call update_row again (dry_run: true checks without writing).
757
+ Docs: https://datanotes.dev/guides/database-objects/relations.md#build-a-tree-of-rows-with-a-parent-link (offline: read_docs("guides/database-objects/relations#build-a-tree-of-rows-with-a-parent-link"))
758
+ ```
759
+
760
+ ## Limit link targets with `ref_by` and `ref_where`
761
+
762
+ A link column can limit which rows it points to, without a CHECK. `ref_by` reads a column of the row itself. With `levels: [cinema, era, genre]`, Cinema has no parent, an era’s parent must be Cinema, and a genre’s parent must be an era.
763
+
764
+ These are the levels of the CHECK above, which stays. Each refusal below names both rules, and the `ref_by` one also lists the valid parents.
765
+
766
+ `describe_table` shows the expanded `map`. You can also write `map` directly, with `null` for a top level.
767
+
768
+ `ref_where` is a `where` filter that every linked row must match. The engine checks writes as it does for CHECKs that follow links, including the rows that link to a changed row.
769
+
770
+ **`alter_column`**
771
+
772
+ ```json
773
+ {
774
+ "table": "genre",
775
+ "column": "parent",
776
+ "changes": { "ref_by": { "column": "kind", "levels": ["cinema", "era", "genre"] } }
777
+ }
778
+ ```
779
+
780
+ Answer:
781
+
782
+ ```json
783
+ {
784
+ "ok": true,
785
+ "op": "alter_column",
786
+ "planned_texts": ["datanotes/metastore.yaml"],
787
+ "commits": [
788
+ { "table": "main.public.genre", "version": 10, "operation": "CHANGE COLUMN" }
789
+ ],
790
+ "migration_id": "20261012T071951-alter_column-alter-column-parent-in-genre"
791
+ }
792
+ ```
793
+
794
+ **`describe_table`**
795
+
796
+ ```json
797
+ { "table": "genre" }
798
+ ```
799
+
800
+ Answer:
801
+
802
+ ```json
803
+ {
804
+ "name": "parent",
805
+ "type": "link",
806
+ "required": false,
807
+ "ref": "main.public.genre",
808
+ "aliases": [],
809
+ "description": "The era or cinema one level up",
810
+ "hierarchy": true,
811
+ "ref_by": {
812
+ "column": "kind",
813
+ "levels": ["cinema", "era", "genre"],
814
+ "map": { "cinema": null, "era": ["cinema"], "genre": ["era"] }
815
+ },
816
+ "dense": false
817
+ }
818
+ ```
819
+
820
+ A genre needs a parent, and the refusal (`required`) says why:
821
+
822
+ **`update_row`**
823
+
824
+ ```json
825
+ {
826
+ "table": "genre",
827
+ "path": "datanotes/main/public/genre/Slapstick comedy.md",
828
+ "unset": ["parent"]
829
+ }
830
+ ```
831
+
832
+ Refused:
833
+
834
+ ```text
835
+ update_row refused — nothing was written:
836
+ invalid datanotes/main/public/genre/Slapstick comedy.md: parent is required for kind "genre": it needs a parent with kind era
837
+ invalid datanotes/main/public/genre/Slapstick comedy.md: parent_by_level violates CHECK parent_by_level: {"or":[{"kind":"cinema","parent":{"exists":false}},{"kind":"era","parent.kind":"cinema"},{"kind":"genre","parent.kind":"era"}]}
838
+ Hint: Give a value to every required column; describe_table marks them (required: true) and lists an enum's values. The row breaks a CHECK constraint of the table (named in the message, with its rule): change the values so the rule holds. describe_table lists the table's constraints. Then call update_row again (dry_run: true checks without writing).
839
+ Docs: https://datanotes.dev/guides/database-objects/tables.md#describe-a-table-before-writing-to-it (offline: read_docs("guides/database-objects/tables#describe-a-table-before-writing-to-it"))
840
+ ```
841
+
842
+ The engine refuses a parent of the wrong level and lists the valid ones:
843
+
844
+ **`update_row`**
845
+
846
+ ```json
847
+ {
848
+ "table": "genre",
849
+ "path": "datanotes/main/public/genre/Slapstick comedy.md",
850
+ "set": { "parent": "[[Cinema]]" }
851
+ }
852
+ ```
853
+
854
+ Refused:
855
+
856
+ ```text
857
+ update_row refused — nothing was written:
858
+ invalid datanotes/main/public/genre/Slapstick comedy.md: parent [[Cinema]] is not a valid parent for kind "genre" (needs kind era); valid: Silent film, Sound film
859
+ invalid datanotes/main/public/genre/Slapstick comedy.md: parent_by_level violates CHECK parent_by_level: {"or":[{"kind":"cinema","parent":{"exists":false}},{"kind":"era","parent.kind":"cinema"},{"kind":"genre","parent.kind":"era"}]}
860
+ Hint: Link to a row of the table the column points to (its ref) that its ref_where / ref_by allow: list_link_targets(table, column) lists them. The row breaks a CHECK constraint of the table (named in the message, with its rule): change the values so the rule holds. describe_table lists the table's constraints. Then call update_row again (dry_run: true checks without writing).
861
+ Docs: https://datanotes.dev/guides/database-objects/relations.md#limit-link-targets-with-ref_by-and-ref_where (offline: read_docs("guides/database-objects/relations#limit-link-targets-with-ref_by-and-ref_where"))
862
+ ```
863
+
864
+ `list_link_targets` returns the valid targets for a row before you write it, so an agent or a picker offers only those:
865
+
866
+ **`list_link_targets`**
867
+
868
+ ```json
869
+ { "table": "genre", "column": "parent", "values": { "kind": "genre" } }
870
+ ```
871
+
872
+ Answer:
873
+
874
+ ```json
875
+ {
876
+ "table": "main.public.genre",
877
+ "column": "parent",
878
+ "ref": "main.public.genre",
879
+ "required": true,
880
+ "empty": false,
881
+ "reason": "kind \"genre\" needs kind era",
882
+ "rows": [
883
+ {
884
+ "name": "Silent film",
885
+ "link": "[[datanotes/main/public/genre/Silent film|Silent film]]",
886
+ "path": "datanotes/main/public/genre/Silent film.md",
887
+ "kind": "era"
888
+ },
889
+ {
890
+ "name": "Sound film",
891
+ "link": "[[datanotes/main/public/genre/Sound film|Sound film]]",
892
+ "path": "datanotes/main/public/genre/Sound film.md",
893
+ "kind": "era"
894
+ }
895
+ ],
896
+ "total": 2
897
+ }
898
+ ```
899
+
900
+ Silent film can’t become a top level while a genre hangs from it:
901
+
902
+ **`update_row`**
903
+
904
+ ```json
905
+ {
906
+ "table": "genre",
907
+ "path": "datanotes/main/public/genre/Silent film.md",
908
+ "set": { "kind": "cinema" },
909
+ "unset": ["parent"]
910
+ }
911
+ ```
912
+
913
+ Refused:
914
+
915
+ ```text
916
+ update_row refused — nothing was written:
917
+ invalid datanotes/main/public/genre/Slapstick comedy.md: parent [[Silent film]] is not a valid parent for kind "genre" (needs kind era); valid: Sound film (after this change to [[Silent film]], a row it links to)
918
+ invalid datanotes/main/public/genre/Slapstick comedy.md: parent_by_level violates CHECK parent_by_level: {"or":[{"kind":"cinema","parent":{"exists":false}},{"kind":"era","parent.kind":"cinema"},{"kind":"genre","parent.kind":"era"}]} (after this change to [[Silent film]], a row it links to)
919
+ Hint: Link to a row of the table the column points to (its ref) that its ref_where / ref_by allow: list_link_targets(table, column) lists them. The row breaks a CHECK constraint of the table (named in the message, with its rule): change the values so the rule holds. describe_table lists the table's constraints. Then call update_row again (dry_run: true checks without writing).
920
+ Docs: https://datanotes.dev/guides/database-objects/relations.md#limit-link-targets-with-ref_by-and-ref_where (offline: read_docs("guides/database-objects/relations#limit-link-targets-with-ref_by-and-ref_where"))
921
+ ```
922
+
923
+ A new `genre` column on `film` uses `ref_where`, so a film links to a genre, never to an era or to Cinema:
924
+
925
+ **`add_column`**
926
+
927
+ ```json
928
+ {
929
+ "table": "film",
930
+ "column": {
931
+ "name": "genre",
932
+ "type": "link",
933
+ "ref": "main.public.genre",
934
+ "ref_where": { "kind": "genre" },
935
+ "description": "The film's genre"
936
+ }
937
+ }
938
+ ```
939
+
940
+ Answer:
941
+
942
+ ```json
943
+ {
944
+ "ok": true,
945
+ "op": "add_column",
946
+ "planned_texts": ["datanotes/metastore.yaml"],
947
+ "commits": [{ "table": "main.public.film", "version": 13, "operation": "ADD COLUMNS" }],
948
+ "migration_id": "20261012T071951-add_column-add-column-genre-to-film"
949
+ }
950
+ ```
951
+
952
+ **`list_link_targets`**
953
+
954
+ ```json
955
+ { "table": "film", "column": "genre" }
956
+ ```
957
+
958
+ Answer:
959
+
960
+ ```json
961
+ {
962
+ "table": "main.public.film",
963
+ "column": "genre",
964
+ "ref": "main.public.genre",
965
+ "required": false,
966
+ "empty": false,
967
+ "reason": "ref_where {\"kind\":\"genre\"}",
968
+ "rows": [
969
+ {
970
+ "name": "Screwball comedy",
971
+ "link": "[[datanotes/main/public/genre/Screwball comedy|Screwball comedy]]",
972
+ "path": "datanotes/main/public/genre/Screwball comedy.md"
973
+ },
974
+ {
975
+ "name": "Slapstick comedy",
976
+ "link": "[[datanotes/main/public/genre/Slapstick comedy|Slapstick comedy]]",
977
+ "path": "datanotes/main/public/genre/Slapstick comedy.md"
978
+ }
979
+ ],
980
+ "total": 2
981
+ }
982
+ ```
983
+
984
+ ## Compute a link from other columns
985
+
986
+ A generated link column builds its link from a template. `link({occurred_at:YYYY-MM-DD})`, with `ref` set to a table of days, links each viewing to its day’s note. That note may not exist yet; the link resolves once it does.
987
+
988
+ **`create_table`**
989
+
990
+ ```json
991
+ {
992
+ "name": "day",
993
+ "description": "One note per day of my diary",
994
+ "columns": [
995
+ {
996
+ "name": "date",
997
+ "type": "date",
998
+ "required": true,
999
+ "description": "The day the note is about"
1000
+ }
1001
+ ],
1002
+ "time_column": "date",
1003
+ "filename": "{date:YYYY-MM-DD}"
1004
+ }
1005
+ ```
1006
+
1007
+ **`add_column`**
1008
+
1009
+ ```json
1010
+ {
1011
+ "table": "watch",
1012
+ "column": {
1013
+ "name": "on_day",
1014
+ "type": "link",
1015
+ "ref": "main.public.day",
1016
+ "generated": "link({occurred_at:YYYY-MM-DD})",
1017
+ "description": "The diary note of the day I watched the film"
1018
+ }
1019
+ }
1020
+ ```
1021
+
1022
+ Answer:
1023
+
1024
+ ```json
1025
+ {
1026
+ "ok": true,
1027
+ "op": "add_column",
1028
+ "planned_texts": ["datanotes/metastore.yaml"],
1029
+ "commits": [{ "table": "main.public.watch", "version": 3, "operation": "ADD COLUMNS" }],
1030
+ "migration_id": "20261012T071951-add_column-add-column-on_day-to-watch"
1031
+ }
1032
+ ```
1033
+
1034
+ **`insert_row`**
1035
+
1036
+ ```json
1037
+ {
1038
+ "table": "watch",
1039
+ "values": {
1040
+ "film": "[[Night of the Living Dead]]",
1041
+ "where": "cinema",
1042
+ "place": "the film club downtown",
1043
+ "with": "my sister",
1044
+ "occurred_at": "2026-10-31T23:00"
1045
+ },
1046
+ "body": "A late show. Half the hall jumped at the same moment.\n"
1047
+ }
1048
+ ```
1049
+
1050
+ Answer:
1051
+
1052
+ ```json
1053
+ {
1054
+ "ok": true,
1055
+ "op": "insert_row",
1056
+ "files_created": 1,
1057
+ "planned_creates": ["datanotes/main/public/watch/2026-10-31T2300.md"],
1058
+ "commits": [{ "table": "main.public.watch", "version": 4, "operation": "WRITE" }],
1059
+ "migration_id": "20261012T071951-insert_row-insert-into-watch"
1060
+ }
1061
+ ```
1062
+
1063
+ `datanotes/main/public/watch/2026-10-31T2300.md` now reads:
1064
+
1065
+ ```markdown
1066
+ ---
1067
+ table: main.public.watch
1068
+ id: 01a12efb-6be3-7ac0-b188-7489a3e62a71
1069
+ film: "[[Night of the Living Dead]]"
1070
+ where: cinema
1071
+ place: the film club downtown
1072
+ with: my sister
1073
+ occurred_at: 2026-10-31T23:00
1074
+ on_day: "[[datanotes/main/public/day/2026-10-31|2026-10-31]]"
1075
+ ---
1076
+ A late show. Half the hall jumped at the same moment.
1077
+ ```
1078
+
1079
+ ## Delete a row that other notes link to
1080
+
1081
+ `on_referenced` tells `delete_rows` what to do with links to the rows it deletes:
1082
+
1083
+ * **`"restrict"`**: the default; refuses rows that other notes link to
1084
+ * **`"set_null"`**: clears those links, but refuses a required one, like `ON DELETE SET NULL` on a `NOT NULL` column in Structured Query Language (SQL)
1085
+ * **`"ignore"`**: leaves the links dangling
1086
+
1087
+ **`delete_rows`**
1088
+
1089
+ ```json
1090
+ { "table": "film", "where": { "title": "Charade" } }
1091
+ ```
1092
+
1093
+ Refused:
1094
+
1095
+ ```text
1096
+ delete_rows refused — nothing was written:
1097
+ conflict datanotes/main/public/film/Charade.md: referenced by datanotes/main/public/actor/Audrey Hepburn.md, datanotes/main/public/actor/Cary Grant.md, datanotes/main/public/film/His Girl Friday.md, datanotes/main/public/watch/2026-09-26T2130.md
1098
+ Hint: Other rows link to it: pass on_referenced ("set_null" or "ignore"), or change those rows first.
1099
+ Docs: https://datanotes.dev/guides/database-objects/relations.md#delete-a-row-that-other-notes-link-to (offline: read_docs("guides/database-objects/relations#delete-a-row-that-other-notes-link-to"))
1100
+ ```
1101
+
1102
+ Charade’s viewing needs its `film`, so the engine also refuses to clear the link:
1103
+
1104
+ **`delete_rows`**
1105
+
1106
+ ```json
1107
+ { "table": "film", "where": { "title": "Charade" }, "on_referenced": "set_null" }
1108
+ ```
1109
+
1110
+ Refused:
1111
+
1112
+ ```text
1113
+ delete_rows refused — nothing was written:
1114
+ conflict datanotes/main/public/watch/2026-09-26T2130.md: "film" is required and would be left empty: change or delete this row first, or use on_referenced "ignore"
1115
+ Hint: Other rows link to it: pass on_referenced ("set_null" or "ignore"), or change those rows first.
1116
+ Docs: https://datanotes.dev/guides/database-objects/relations.md#delete-a-row-that-other-notes-link-to (offline: read_docs("guides/database-objects/relations#delete-a-row-that-other-notes-link-to"))
1117
+ ```
1118
+
1119
+ Delete the viewing and the film together with `batch`. The viewing goes first, then `set_null` clears Charade from Cary Grant’s `films`. If the engine refused any step, it would apply none of them.
1120
+
1121
+ **`batch`**
1122
+
1123
+ ```json
1124
+ {
1125
+ "summary": "remove Charade and its viewing",
1126
+ "operations": [
1127
+ { "op": "delete_rows", "table": "watch", "where": { "film": "[[Charade]]" } },
1128
+ {
1129
+ "op": "delete_rows",
1130
+ "table": "film",
1131
+ "where": { "title": "Charade" },
1132
+ "on_referenced": "set_null"
1133
+ }
1134
+ ]
1135
+ }
1136
+ ```
1137
+
1138
+ Answer (72 lines)
1139
+
1140
+ ```json
1141
+ {
1142
+ "ok": true,
1143
+ "op": "batch",
1144
+ "dry_run": false,
1145
+ "migration_id": "20261012T071951-batch-remove-charade-and-its-viewing",
1146
+ "steps": [
1147
+ {
1148
+ "index": 0,
1149
+ "op": "delete_rows",
1150
+ "ok": true,
1151
+ "migration_id": "20261012T071951-delete_rows-archive-1-rows-of-watch",
1152
+ "result": {
1153
+ "ok": true,
1154
+ "op": "delete_rows",
1155
+ "files_moved": 1,
1156
+ "planned_moves": [
1157
+ {
1158
+ "from": "datanotes/main/public/watch/2026-09-26T2130.md",
1159
+ "to": "datanotes/main/public/watch/_archive/2026-09-26T2130.md"
1160
+ }
1161
+ ],
1162
+ "commits": [{ "table": "main.public.watch", "version": 5, "operation": "DELETE" }],
1163
+ "migration_id": "20261012T071951-delete_rows-archive-1-rows-of-watch"
1164
+ }
1165
+ },
1166
+ {
1167
+ "index": 1,
1168
+ "op": "delete_rows",
1169
+ "ok": true,
1170
+ "migration_id": "20261012T071951-delete_rows-archive-1-rows-of-film",
1171
+ "result": {
1172
+ "ok": true,
1173
+ "op": "delete_rows",
1174
+ "notes_changed": 4,
1175
+ "files_moved": 1,
1176
+ "planned_edits": [
1177
+ {
1178
+ "path": "datanotes/main/public/actor/Audrey Hepburn.md",
1179
+ "set": { "films": [] },
1180
+ "remove": []
1181
+ },
1182
+ {
1183
+ "path": "datanotes/main/public/actor/Cary Grant.md",
1184
+ "set": { "films": ["[[His Girl Friday]]"] },
1185
+ "remove": []
1186
+ },
1187
+ {
1188
+ "path": "datanotes/main/public/film/His Girl Friday.md",
1189
+ "set": { "see_also": [] },
1190
+ "remove": []
1191
+ },
1192
+ {
1193
+ "path": "datanotes/main/public/watch/_archive/2026-09-26T2130.md",
1194
+ "set": { "film": null },
1195
+ "remove": []
1196
+ }
1197
+ ],
1198
+ "planned_moves": [
1199
+ {
1200
+ "from": "datanotes/main/public/film/Charade.md",
1201
+ "to": "datanotes/main/public/film/_archive/Charade.md"
1202
+ }
1203
+ ],
1204
+ "commits": [
1205
+ { "table": "main.public.actor", "version": 7, "operation": "DELETE" },
1206
+ { "table": "main.public.film", "version": 14, "operation": "DELETE" }
1207
+ ],
1208
+ "migration_id": "20261012T071951-delete_rows-archive-1-rows-of-film"
1209
+ }
1210
+ }
1211
+ ]
1212
+ }
1213
+ ```
1214
+
1215
+ **`read_note`**
1216
+
1217
+ ```json
1218
+ { "path": "datanotes/main/public/actor/Cary Grant.md" }
1219
+ ```
1220
+
1221
+ Answer:
1222
+
1223
+ ```markdown
1224
+ ---
1225
+ table: main.public.actor
1226
+ id: 01a12efb-621d-71b4-a0ec-578ef615f6b0
1227
+ name: Cary Grant
1228
+ born: 1904-01-18
1229
+ films:
1230
+ - "[[His Girl Friday]]"
1231
+ ---
1232
+ ```
1233
+
1234
+ ## See the relations in `metastore.yaml`
1235
+
1236
+ The engine wrote this file with the relations from this page:
1237
+
1238
+ * **Link columns**: each with its `ref`, and `self` kept as written
1239
+ * **`inverse`**: on one side only
1240
+ * **`vocabulary`, `hierarchy` and `ref_where`**: as declared
1241
+ * **`ref_by`**: with the `levels` shortcut kept as written
1242
+ * **Generated link**: the `on_day` column with its template
1243
+
1244
+ `datanotes/metastore.yaml` now reads:
1245
+
1246
+ ```yaml
1247
+ # Metastore (Datanotes): catalogs, schemas, tables, columns, views.
1248
+ id: 01a12efb-6211-77a6-a9b2-1c2a593b92c9
1249
+ config:
1250
+ history: true
1251
+
1252
+
1253
+ catalogs:
1254
+ main:
1255
+ schemas:
1256
+ public:
1257
+ tables:
1258
+ actor:
1259
+ description: Actors of the films I watch
1260
+ options:
1261
+ filename: "{name}"
1262
+ archive: _archive
1263
+ columns:
1264
+ - name: id
1265
+ type: string
1266
+ required: true
1267
+ description: "Unique id of the row (UUIDv7): set when the row is created, never changes"
1268
+ unique: true
1269
+ identity: uuid
1270
+ dense: true
1271
+ - name: name
1272
+ type: string
1273
+ required: true
1274
+ description: The actor's name, as credited
1275
+ unique: true
1276
+ - name: born
1277
+ type: date
1278
+ description: The actor's date of birth
1279
+ - name: films
1280
+ type: list
1281
+ items: link
1282
+ ref: main.public.film
1283
+ description: The films the actor plays in
1284
+ film:
1285
+ description: Classic films I watched or want to watch
1286
+ options:
1287
+ filename: "{title}"
1288
+ archive: _archive
1289
+ columns:
1290
+ - name: id
1291
+ type: string
1292
+ required: true
1293
+ description: "Unique id of the row (UUIDv7): set when the row is created, never changes"
1294
+ unique: true
1295
+ identity: uuid
1296
+ dense: true
1297
+ - name: title
1298
+ type: string
1299
+ required: true
1300
+ description: The film's title, as on its poster
1301
+ unique: true
1302
+ - name: year
1303
+ type: integer
1304
+ description: Year the film first came out
1305
+ - name: minutes
1306
+ type: integer
1307
+ min: 1
1308
+ description: Running time in minutes
1309
+ - name: cast
1310
+ type: list
1311
+ items: link
1312
+ ref: main.public.actor
1313
+ description: The actors in the film
1314
+ inverse: films
1315
+ - name: see_also
1316
+ type: list
1317
+ items: link
1318
+ ref: self
1319
+ description: Films to watch next if I liked this one
1320
+ inverse: see_also
1321
+ - name: tags
1322
+ type: list
1323
+ items: string
1324
+ description: My tags for the film, from the tag table
1325
+ vocabulary: main.public.tag
1326
+ - name: genre
1327
+ type: link
1328
+ ref: main.public.genre
1329
+ description: The film's genre
1330
+ ref_where:
1331
+ kind: genre
1332
+ watch:
1333
+ description: Each time I watch a film
1334
+ options:
1335
+ filename: "{occurred_at:YYYY-MM-DD[T]HHmm}"
1336
+ archive: _archive
1337
+ columns:
1338
+ - name: id
1339
+ type: string
1340
+ required: true
1341
+ description: "Unique id of the row (UUIDv7): set when the row is created, never changes"
1342
+ unique: true
1343
+ identity: uuid
1344
+ dense: true
1345
+ - name: film
1346
+ type: link
1347
+ required: true
1348
+ ref: main.public.film
1349
+ description: The film I watched
1350
+ - name: where
1351
+ type: enum
1352
+ required: true
1353
+ values:
1354
+ - cinema
1355
+ - home
1356
+ - outdoor screening
1357
+ description: The kind of place I watched it in
1358
+ - name: place
1359
+ type: string
1360
+ description: The cinema, park or room, in my words
1361
+ - name: with
1362
+ type: string
1363
+ description: Who watched it with me
1364
+ - name: occurred_at
1365
+ type: datetime
1366
+ required: true
1367
+ description: When the film started
1368
+ - name: on_day
1369
+ type: link
1370
+ ref: main.public.day
1371
+ description: The diary note of the day I watched the film
1372
+ generated: link({occurred_at:YYYY-MM-DD})
1373
+ tag:
1374
+ description: Words I tag films with
1375
+ options:
1376
+ filename: "{name}"
1377
+ archive: _archive
1378
+ columns:
1379
+ - name: id
1380
+ type: string
1381
+ required: true
1382
+ description: "Unique id of the row (UUIDv7): set when the row is created, never changes"
1383
+ unique: true
1384
+ identity: uuid
1385
+ dense: true
1386
+ - name: name
1387
+ type: string
1388
+ required: true
1389
+ description: The tag, as I write it
1390
+ unique: true
1391
+ genre:
1392
+ description: Eras and genres of cinema, as a tree
1393
+ constraints:
1394
+ parent_by_level:
1395
+ or:
1396
+ - kind: cinema
1397
+ parent:
1398
+ exists: false
1399
+ - kind: era
1400
+ parent.kind: cinema
1401
+ - kind: genre
1402
+ parent.kind: era
1403
+ options:
1404
+ filename: "{name}"
1405
+ archive: _archive
1406
+ columns:
1407
+ - name: id
1408
+ type: string
1409
+ required: true
1410
+ description: "Unique id of the row (UUIDv7): set when the row is created, never changes"
1411
+ unique: true
1412
+ identity: uuid
1413
+ dense: true
1414
+ - name: name
1415
+ type: string
1416
+ required: true
1417
+ description: The name of the era or genre
1418
+ - name: kind
1419
+ type: enum
1420
+ required: true
1421
+ values:
1422
+ - cinema
1423
+ - era
1424
+ - genre
1425
+ description: "The level in the tree: cinema, an era or a genre"
1426
+ - name: parent
1427
+ type: link
1428
+ ref: self
1429
+ description: The era or cinema one level up
1430
+ hierarchy: true
1431
+ ref_by:
1432
+ column: kind
1433
+ levels:
1434
+ - cinema
1435
+ - era
1436
+ - genre
1437
+ day:
1438
+ description: One note per day of my diary
1439
+ options:
1440
+ filename: "{date:YYYY-MM-DD}"
1441
+ archive: _archive
1442
+ columns:
1443
+ - name: id
1444
+ type: string
1445
+ required: true
1446
+ description: "Unique id of the row (UUIDv7): set when the row is created, never changes"
1447
+ unique: true
1448
+ identity: uuid
1449
+ dense: true
1450
+ - name: date
1451
+ type: date
1452
+ required: true
1453
+ description: The day the note is about
1454
+ ```