json-fs-query 3.1.4 → 3.3.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 (187) hide show
  1. package/README.md +257 -15
  2. package/browser/jsonFsQuery.js +3884 -5057
  3. package/browser/jsonFsQuery.min.js +1 -1
  4. package/dist/data/introspect/alter.d.ts +12 -0
  5. package/dist/data/introspect/alter.js +304 -0
  6. package/dist/data/introspect/alter.min.js +1 -0
  7. package/dist/data/introspect/create.d.ts +10 -0
  8. package/dist/data/introspect/create.js +9 -23
  9. package/dist/data/introspect/create.min.js +1 -1
  10. package/dist/data/introspect/drop.js +7 -15
  11. package/dist/data/introspect/drop.min.js +1 -1
  12. package/dist/data/introspect/formatDatabase.d.ts +17 -0
  13. package/dist/data/introspect/formatDatabase.js +56 -0
  14. package/dist/data/introspect/formatDatabase.min.js +1 -0
  15. package/dist/data/introspect/index.d.ts +9 -0
  16. package/dist/data/introspect/index.js +18 -5
  17. package/dist/data/introspect/index.min.js +1 -1
  18. package/dist/data/introspect/packDatabase.d.ts +27 -0
  19. package/dist/data/introspect/packDatabase.js +113 -0
  20. package/dist/data/introspect/packDatabase.min.js +1 -0
  21. package/dist/data/introspect/policies/foreignKeyExists.d.ts +2 -0
  22. package/dist/data/introspect/policies/foreignKeyExists.js +9 -3
  23. package/dist/data/introspect/policies/foreignKeyExists.min.js +1 -1
  24. package/dist/data/introspect/policies/hasDependents.d.ts +3 -3
  25. package/dist/data/introspect/policies/hasDependents.js +11 -3
  26. package/dist/data/introspect/policies/hasDependents.min.js +1 -1
  27. package/dist/data/introspect/policies/keyUnique.d.ts +3 -0
  28. package/dist/data/introspect/policies/keyUnique.js +10 -0
  29. package/dist/data/introspect/policies/keyUnique.min.js +1 -1
  30. package/dist/data/queries/databaseStatements.d.ts +31 -0
  31. package/dist/data/queries/databaseStatements.js +97 -0
  32. package/dist/data/queries/databaseStatements.min.js +1 -0
  33. package/dist/data/queries/deleteEntity.js +35 -19
  34. package/dist/data/queries/deleteEntity.min.js +1 -1
  35. package/dist/data/queries/insertEntity.d.ts +8 -0
  36. package/dist/data/queries/insertEntity.js +340 -43
  37. package/dist/data/queries/insertEntity.min.js +1 -1
  38. package/dist/data/queries/joinEntity.d.ts +15 -0
  39. package/dist/data/queries/joinEntity.js +91 -15
  40. package/dist/data/queries/joinEntity.min.js +1 -1
  41. package/dist/data/queries/query.d.ts +2 -1
  42. package/dist/data/queries/query.js +5 -2
  43. package/dist/data/queries/query.min.js +1 -1
  44. package/dist/data/queries/selectFields.d.ts +5 -4
  45. package/dist/data/queries/selectFields.js +25 -11
  46. package/dist/data/queries/selectFields.min.js +1 -1
  47. package/dist/data/queries/updateEntity.js +54 -15
  48. package/dist/data/queries/updateEntity.min.js +1 -1
  49. package/dist/data/storage/databases.d.ts +32 -0
  50. package/dist/data/storage/databases.js +93 -0
  51. package/dist/data/storage/databases.min.js +1 -0
  52. package/dist/data/storage/fileExists.d.ts +7 -0
  53. package/dist/data/storage/fileExists.js +25 -0
  54. package/dist/data/storage/fileExists.min.js +1 -0
  55. package/dist/data/storage/index.d.ts +19 -0
  56. package/dist/data/storage/index.js +124 -0
  57. package/dist/data/storage/index.min.js +1 -0
  58. package/dist/data/storage/listFiles.d.ts +8 -0
  59. package/dist/data/storage/listFiles.js +52 -0
  60. package/dist/data/storage/listFiles.min.js +1 -0
  61. package/dist/data/storage/makeDirectory.d.ts +7 -0
  62. package/dist/data/storage/makeDirectory.js +24 -0
  63. package/dist/data/storage/makeDirectory.min.js +1 -0
  64. package/dist/data/storage/nodeFiles.d.ts +6 -0
  65. package/dist/data/storage/nodeFiles.js +12 -0
  66. package/dist/data/storage/nodeFiles.min.js +1 -0
  67. package/dist/data/storage/readCache.d.ts +37 -0
  68. package/dist/data/storage/readCache.js +117 -0
  69. package/dist/data/storage/readCache.min.js +1 -0
  70. package/dist/data/storage/readJson.d.ts +16 -0
  71. package/dist/data/storage/readJson.js +38 -0
  72. package/dist/data/storage/readJson.min.js +1 -0
  73. package/dist/data/storage/removeDirectory.d.ts +8 -0
  74. package/dist/data/storage/removeDirectory.js +30 -0
  75. package/dist/data/storage/removeDirectory.min.js +1 -0
  76. package/dist/data/storage/removeFile.d.ts +8 -0
  77. package/dist/data/storage/removeFile.js +31 -0
  78. package/dist/data/storage/removeFile.min.js +1 -0
  79. package/dist/data/storage/storagePath.d.ts +11 -0
  80. package/dist/data/storage/storagePath.js +25 -0
  81. package/dist/data/storage/storagePath.min.js +1 -0
  82. package/dist/data/storage/writeJson.d.ts +17 -0
  83. package/dist/data/storage/writeJson.js +37 -0
  84. package/dist/data/storage/writeJson.min.js +1 -0
  85. package/dist/data/storage/writeSet.d.ts +64 -0
  86. package/dist/data/storage/writeSet.js +306 -0
  87. package/dist/data/storage/writeSet.min.js +1 -0
  88. package/dist/data/utilities/compareAs.d.ts +1 -0
  89. package/dist/data/utilities/compareAs.js +2 -2
  90. package/dist/data/utilities/compareAs.min.js +1 -1
  91. package/dist/data/utilities/config.d.ts +37 -0
  92. package/dist/data/utilities/config.js +77 -5
  93. package/dist/data/utilities/config.min.js +1 -1
  94. package/dist/data/utilities/databaseLock.d.ts +7 -0
  95. package/dist/data/utilities/databaseLock.js +19 -4
  96. package/dist/data/utilities/databaseLock.min.js +1 -1
  97. package/dist/data/utilities/deregisterRecord.js +4 -10
  98. package/dist/data/utilities/deregisterRecord.min.js +1 -1
  99. package/dist/data/utilities/getConditionIndexedList.js +3 -4
  100. package/dist/data/utilities/getConditionIndexedList.min.js +1 -1
  101. package/dist/data/utilities/indexBuffer.d.ts +35 -0
  102. package/dist/data/utilities/indexBuffer.js +139 -0
  103. package/dist/data/utilities/indexBuffer.min.js +1 -0
  104. package/dist/data/utilities/indexHas.d.ts +13 -0
  105. package/dist/data/utilities/indexHas.js +48 -0
  106. package/dist/data/utilities/indexHas.min.js +1 -0
  107. package/dist/data/utilities/joinBases.d.ts +29 -0
  108. package/dist/data/utilities/joinBases.js +128 -0
  109. package/dist/data/utilities/joinBases.min.js +1 -0
  110. package/dist/data/utilities/joinKey.d.ts +17 -0
  111. package/dist/data/utilities/joinKey.js +31 -0
  112. package/dist/data/utilities/joinKey.min.js +1 -0
  113. package/dist/data/utilities/keyIndexFiles.d.ts +16 -0
  114. package/dist/data/utilities/keyIndexFiles.js +60 -0
  115. package/dist/data/utilities/keyIndexFiles.min.js +1 -0
  116. package/dist/data/utilities/knownFields.d.ts +26 -0
  117. package/dist/data/utilities/knownFields.js +128 -0
  118. package/dist/data/utilities/knownFields.min.js +1 -0
  119. package/dist/data/utilities/packedTable.d.ts +34 -0
  120. package/dist/data/utilities/packedTable.js +60 -0
  121. package/dist/data/utilities/packedTable.min.js +1 -0
  122. package/dist/data/utilities/parsers/findInsertValues.d.ts +3 -3
  123. package/dist/data/utilities/parsers/findInsertValues.js +28 -12
  124. package/dist/data/utilities/parsers/findInsertValues.min.js +1 -1
  125. package/dist/data/utilities/parsers/maskStrings.js +11 -2
  126. package/dist/data/utilities/parsers/maskStrings.min.js +1 -1
  127. package/dist/data/utilities/parsers/parseAlter.d.ts +33 -0
  128. package/dist/data/utilities/parsers/parseAlter.js +84 -0
  129. package/dist/data/utilities/parsers/parseAlter.min.js +1 -0
  130. package/dist/data/utilities/parsers/parseCreate.d.ts +14 -0
  131. package/dist/data/utilities/parsers/parseCreate.js +13 -5
  132. package/dist/data/utilities/parsers/parseCreate.min.js +1 -1
  133. package/dist/data/utilities/parsers/parseDatabaseStatement.d.ts +17 -0
  134. package/dist/data/utilities/parsers/parseDatabaseStatement.js +82 -0
  135. package/dist/data/utilities/parsers/parseDatabaseStatement.min.js +1 -0
  136. package/dist/data/utilities/parsers/parseQuery.d.ts +3 -0
  137. package/dist/data/utilities/parsers/parseQuery.js +24 -0
  138. package/dist/data/utilities/parsers/parseQuery.min.js +1 -1
  139. package/dist/data/utilities/parsers/startsQuery.js +1 -1
  140. package/dist/data/utilities/parsers/startsQuery.min.js +1 -1
  141. package/dist/data/utilities/recordExists.js +2 -7
  142. package/dist/data/utilities/recordExists.min.js +1 -1
  143. package/dist/data/utilities/registerRecord.js +4 -10
  144. package/dist/data/utilities/registerRecord.min.js +1 -1
  145. package/dist/data/utilities/removeFromIndex.js +4 -10
  146. package/dist/data/utilities/removeFromIndex.min.js +1 -1
  147. package/dist/data/utilities/retrieveFile.d.ts +5 -0
  148. package/dist/data/utilities/retrieveFile.js +7 -16
  149. package/dist/data/utilities/retrieveFile.min.js +1 -1
  150. package/dist/data/utilities/retrieveFiles.d.ts +15 -0
  151. package/dist/data/utilities/retrieveFiles.js +48 -0
  152. package/dist/data/utilities/retrieveFiles.min.js +1 -0
  153. package/dist/data/utilities/retrieveRecord.d.ts +6 -0
  154. package/dist/data/utilities/retrieveRecord.js +24 -1
  155. package/dist/data/utilities/retrieveRecord.min.js +1 -1
  156. package/dist/data/utilities/retrieveRecords.d.ts +22 -0
  157. package/dist/data/utilities/retrieveRecords.js +60 -10
  158. package/dist/data/utilities/retrieveRecords.min.js +1 -1
  159. package/dist/data/utilities/rowLookup.d.ts +38 -0
  160. package/dist/data/utilities/rowLookup.js +99 -0
  161. package/dist/data/utilities/rowLookup.min.js +1 -0
  162. package/dist/data/utilities/runQuery.js +28 -4
  163. package/dist/data/utilities/runQuery.min.js +1 -1
  164. package/dist/data/utilities/semiJoin.d.ts +27 -0
  165. package/dist/data/utilities/semiJoin.js +97 -0
  166. package/dist/data/utilities/semiJoin.min.js +1 -0
  167. package/dist/data/utilities/updateIndex.js +5 -14
  168. package/dist/data/utilities/updateIndex.min.js +1 -1
  169. package/dist/index.d.ts +1 -1
  170. package/dist/index.js +24 -0
  171. package/dist/index.min.js +1 -1
  172. package/package.json +10 -4
  173. package/dist/data/utilities/getJoinIndexedList.d.ts +0 -3
  174. package/dist/data/utilities/getJoinIndexedList.js +0 -76
  175. package/dist/data/utilities/getJoinIndexedList.min.js +0 -1
  176. package/dist/data/utilities/getMatchingIndexes.d.ts +0 -2
  177. package/dist/data/utilities/getMatchingIndexes.js +0 -28
  178. package/dist/data/utilities/getMatchingIndexes.min.js +0 -1
  179. package/dist/data/utilities/matchIndexedJoins.d.ts +0 -2
  180. package/dist/data/utilities/matchIndexedJoins.js +0 -63
  181. package/dist/data/utilities/matchIndexedJoins.min.js +0 -1
  182. package/dist/data/utilities/restrictIndexedList.d.ts +0 -2
  183. package/dist/data/utilities/restrictIndexedList.js +0 -45
  184. package/dist/data/utilities/restrictIndexedList.min.js +0 -1
  185. package/dist/data/utilities/useJoinClause.d.ts +0 -6
  186. package/dist/data/utilities/useJoinClause.js +0 -57
  187. package/dist/data/utilities/useJoinClause.min.js +0 -1
package/README.md CHANGED
@@ -1,14 +1,93 @@
1
1
  # json-fs-query
2
2
 
3
- Read and manage data with JSON files - a small query engine that treats a folder of JSON files as a database, with
4
- a SQL-like query string (`read workouts where date = '2024-06-02'`) instead of a query builder API.
3
+ A git-native JSON data store for static sites. Your data lives in a folder of plain JSON files, committed with the
4
+ rest of the site: readable, diffable and reviewed in pull requests like code. json-fs-query adds what a folder of
5
+ files doesn't have on its own:
6
+
7
+ - **A schema**: field types, required and nullable fields, defaults.
8
+ - **Keys enforced**: primary, unique and foreign keys, checked on every write (a delete that would leave a dangling
9
+ reference is refused).
10
+ - **A SQL-like query language**: `read workouts where date = '2024-06-02' sort reps desc`, with joins, `group by`,
11
+ `alter`, multi-row inserts, and statements that change nothing if any row fails.
12
+ - **Reads straight from static hosting**: in a browser it reads the files over `fetch()` (GitHub Pages, Netlify, any
13
+ CDN), so a site can query its data with no database server. Writes run in Node: a script or a build step.
14
+
15
+ ## When to use it, and when not to
16
+
17
+ It suits data that's written by you (or your build) and read by your visitors: a portfolio's projects, a blog's
18
+ posts, a quiz bank, a workout log, a small catalogue. Small to medium size: hundreds to about ten thousand rows a
19
+ record. Data you want to see change in `git diff`, and ship without running anything server-side.
20
+
21
+ Use SQLite, or a real database server, instead when:
22
+
23
+ - **Visitors write data.** Writes need file system access, so a browser can only read. Something has to run a server
24
+ to accept writes.
25
+ - **Several programs write at once.** Calls are queued within one Node process; two processes writing one folder
26
+ aren't protected from each other.
27
+ - **The data is big.** Each row is a file, and a read loads the rows it needs into memory. The benchmarks go up to
28
+ 10,000 rows; once the files stop being something you'd read in a diff, SQLite will be faster and smaller.
29
+ - **You need what isn't here yet**: transactions across several statements, aggregates (`count`, `sum`), outer joins,
30
+ date and decimal field types, multi-field foreign keys, and `cascade` / `set null` on delete (a referenced row can't
31
+ be deleted while rows point at it, or checks can be turned off with `configure()`). These are planned, not built.
32
+
33
+ ## How fast
34
+
35
+ At 1,000 rows (a table of 100 parents and one of 1,000 children with a foreign key and an index), before the
36
+ performance work (3.1.4) and now. `BENCHMARKS.md` has every run and what each one measures.
37
+
38
+ | Operation, 1,000 rows | 3.1.4 | Now |
39
+ |---|---:|---:|
40
+ | Insert 1,000 rows, one `insert` each | 6.5 min | 5.7 s |
41
+ | Insert 1,000 rows in one `insert` | - | 0.43 s |
42
+ | Read every row | 376 ms | 151 ms |
43
+ | Indexed `where` | 7.8 ms | 4.2 ms |
44
+ | One join | 5.2 s | 176 ms |
45
+ | Two joins | 5.6 s | 177 ms |
46
+ | Update one row by key | 402 ms | 157 ms |
47
+ | Delete one row by key | 784 ms | 156 ms |
48
+ | Browser: read every row | 4.3 s | 689 ms, packed 11 ms |
49
+ | Browser: one join | 15.2 s | 732 ms, packed 20 ms |
50
+ | Browser: two joins | 26.1 s | 744 ms, packed 33 ms |
51
+ | Size of the data | 438 KB | 359 KB |
52
+
53
+ The browser rows are read from a local server; over a real network each request costs more, which is what packed
54
+ tables (see In the browser) are for: one request per record instead of one per row.
55
+
56
+ ## A static site, end to end
57
+
58
+ A Node script owns the data: it creates the records once and adds rows whenever there are new ones.
5
59
 
6
- The core engine (schema management, a full read/insert/update/delete query language, single- and multi-field key
7
- support, and referential integrity) is implemented and tested. Still open, tracked for whenever they're actually
8
- needed: multi-field *foreign* keys specifically (single-field ones work; composite primary/unique/index keys work
9
- too), and CASCADE/SET NULL modes for referential integrity (deleting/updating a referenced record is RESTRICTed -
10
- blocked outright - rather than cascading or nulling out the dependents; see `configure()` below to turn that
11
- enforcement off entirely if it doesn't fit your workflow).
60
+ ```js
61
+ // scripts/add-posts.js
62
+ const { configure, query, packDatabase } = require('json-fs-query')
63
+
64
+ configure({ databasePath: 'site/database/' })
65
+
66
+ await query(`create if not exists authors (_id string auto, name string, primary key (_id));
67
+ create if not exists posts (_id string auto, author_id string, title string, published string,
68
+ primary key (_id), index (published), foreign key (author_id) references authors (_id))`)
69
+
70
+ const [[author]] = await query("insert authors values name = 'Joshua'")
71
+ await query(`insert posts values
72
+ (author_id = '${author._id}', title = 'First post', published = '2026-11-01'),
73
+ (author_id = '${author._id}', title = 'Second post', published = '2026-11-08')`)
74
+
75
+ await packDatabase() // one file per record, for the browser
76
+ ```
77
+
78
+ Commit `site/database/` and deploy `site/` as usual. The page reads it with no server of its own:
79
+
80
+ ```html
81
+ <script src="https://unpkg.com/json-fs-query"></script>
82
+ <script>
83
+ jsonFsQuery.configure({ databasePath: 'database/' })
84
+ jsonFsQuery.query("read posts merge posts.author_id with authors._id sort published desc limit 10")
85
+ .then(([posts]) => console.log(posts)) // each post with its author's name as "authors.name"
86
+ </script>
87
+ ```
88
+
89
+ New posts go in the same way (an `insert`, or a JSON file edited by hand), then `packDatabase()`, commit and deploy. A
90
+ test that calls `checkPacked()` (see Packed tables) catches a forgotten `packDatabase()`.
12
91
 
13
92
  ## Install
14
93
 
@@ -45,10 +124,17 @@ JSON can leave a key out or store it as `null`:
45
124
  | Definition | Left out | `null` |
46
125
  |---|---|---|
47
126
  | `{ name: 'note', type: 'string' }` | error | error |
48
- | `{ name: 'note', type: 'string', optional: true }` | allowed | error |
127
+ | `{ name: 'note', type: 'string', optional: true }` | allowed | same as left out |
49
128
  | `{ name: 'note', type: 'string', nullable: true }` | error | stored as `null` |
50
129
  | `{ name: 'note', type: 'string', optional: true, nullable: true }` | allowed | stored as `null` |
51
130
 
131
+ An optional field that isn't nullable can never hold `null`, so a `null` given for it means "no value": `insert ...
132
+ note = null` stores the row without `note` (a default fills it in, as for a field left out), and `update ... set
133
+ note = null` removes `note` from the rows it changes, which is how an optional field is cleared (not a field of the
134
+ primary key: rows are found by it). As in SQL, a default only fills in a field when a row is inserted, so clearing a
135
+ field with a default leaves it out rather than setting the default. Nullable fields store `null`, and required fields
136
+ that aren't nullable reject it.
137
+
52
138
  `useDefault: true, defaultValue: …` fills in a field that is left out. `create()` checks the default against the
53
139
  field's type (a `null` default needs `nullable: true`) and throws if it doesn't fit. Primary key fields can't be
54
140
  nullable. Each key needs a non-empty `fields` list naming defined fields, and unknown key properties are rejected.
@@ -69,6 +155,29 @@ Keywords (`where`, `and`, `sort`, `limit`, `create`, `show records`...), command
69
155
  `delete`), sort directions (`asc`, `desc`) and comparators can be written in any case: `READ t` and `sort a ASC` work.
70
156
  Record and field names are case-sensitive.
71
157
 
158
+ Every record and field a query names must exist, as in SQL. A field means the queried record's own unless it's written
159
+ `<record>.<field>`, and it has to be one that record defines (it doesn't have to be stored in every row: an optional
160
+ field is fine). Otherwise the query fails before reading or writing anything, naming what's wrong and what's there:
161
+ `Record "workout" doesn't exist`, or `Unknown field "rep" in where: workouts has _id, exercise_id, date, reps`. In
162
+ `select`, the joined record's name stands for its rows, `<record>.<field>` picks the queried record's own field (named
163
+ as written: use `as` to rename it) or a merged record's, and a joined record's single field is an error, since its rows
164
+ are a list (merge it to put its fields on each row).
165
+
166
+ ### Inserting several rows
167
+
168
+ One `insert` can add many rows: put each row's values in brackets, separated by commas.
169
+
170
+ ```sql
171
+ insert workouts values (reps = 10, date = '2024-06-02'), (reps = 12, date = '2024-06-03'), (reps = 8, date = '2024-06-04')
172
+ ```
173
+
174
+ It resolves to the inserted rows, in order. Each row is checked against the rows already stored and the rows before it
175
+ in the statement (so two rows with the same primary or unique key fail, and a row's foreign key can point at a row
176
+ earlier in the same insert). The statement is all or nothing: if any row fails, none is written. Every index is read once
177
+ and the record file and each index are written once, so this is far faster than one `insert` per row: 1,000 rows take
178
+ about half a second, against about 7 seconds one at a time (see `BENCHMARKS.md`). One row without brackets
179
+ (`insert workouts values reps = 10, date = '2024-06-02'`) works as before.
180
+
72
181
  ### Strings
73
182
 
74
183
  Strings use single or double quotes. A string closes only with the quote it opened with, so the other quote is just
@@ -380,6 +489,32 @@ it, that is an error. A record that another record's foreign key references can'
380
489
  (unless `enforceForeignKeys` is turned off, see Configuration). `describe` resolves to `{ path, definition, keys }`
381
490
  (the `describe()` function gives only the fields), and `show records` to an array of names.
382
491
 
492
+ ### Changing a record's fields: `alter`
493
+
494
+ ```sql
495
+ alter workouts add note string optional -- a new field (written as in create)
496
+ alter workouts add status string default 'planned' -- existing rows get the default
497
+ alter workouts drop note -- removed from the definition and every row
498
+ alter workouts rename date to day -- in every row, its keys and indexes, and foreign keys to it
499
+ alter workouts modify status string optional default 'planned' -- the field's whole new definition
500
+ ```
501
+
502
+ Each `alter` makes one change, all or nothing (several, separated by `;`, run in turn), and resolves to the record's
503
+ new fields. The `alter()` function takes the same changes as objects, e.g.
504
+ `alter('workouts', { action: 'add', field: { name: 'note', type: 'string', optional: true } })`.
505
+
506
+ - **add**: existing rows get the field's default, or a new id for an `auto` field. A required field with neither is
507
+ refused while the record has rows (they'd be missing it): give it a default, or make it optional.
508
+ - **drop**: refused while a key of the record uses the field, or another record's foreign key references it.
509
+ - **rename**: a key's index file named after the field moves to the new name; foreign keys that reference the field
510
+ (in any record) are renamed with it.
511
+ - **modify**: gives the field's whole new definition, as MySQL's `MODIFY` does: anything it leaves out is cleared, so
512
+ repeat a default to keep it (`modify status string optional` removes `default 'planned'`, as leaving out `optional`
513
+ makes the field required). The type, and whether the field is `auto`, can't change (that's for a later version).
514
+ Making it required fills rows that are missing it from the new default, and is refused without one; making it not
515
+ nullable removes the `null`s when it stays optional, and otherwise fills them from the default. When a key uses the
516
+ field, filled rows go through the same checks as an `update` (a unique key refuses the same default twice).
517
+
383
518
  ### Reading a query over several lines: `queryStatus()`
384
519
 
385
520
  For a prompt which reads a query line by line (like the CLI), `queryStatus()` says whether the text so far is ready to
@@ -444,6 +579,18 @@ Queries that worked in 2.x can be errors now (each error names the problem), and
444
579
  `Missing a whole number after limit: x`. In 2.x these were `Unrecognized query value: ...`. Check any code that
445
580
  matches on that message.
446
581
 
582
+ ### Upgrading from 3.2
583
+
584
+ - A field a record doesn't define is an error everywhere a query names one. In 3.2 `insert` dropped it without a word,
585
+ `update ... set` stored it outside the record's definition, and `where`, `sort`, `group by`, `select`, `on` and
586
+ `merge` quietly matched nothing (or left rows unsorted). A field that's only missing from some rows is fine, as long
587
+ as the record defines it.
588
+ - `select <record>.<field>` for the queried record picks its field; in 3.2 it gave nothing. For a joined record
589
+ without `merge`, it's an error saying to select the record (its rows) or merge it.
590
+ - A record that doesn't exist is `Record "<name>" doesn't exist` (in a browser, `... doesn't exist, or <name>.json
591
+ can't be read`), as `drop` and `describe` already said, instead of `Could not read <name>.json`. A join written
592
+ without the record to join (`read workouts on ...`) says so, instead of `Could not read .json`.
593
+
447
594
  ## Configuration
448
595
 
449
596
  `configure()` sets where this package's data lives and how strictly it enforces foreign keys - call it once,
@@ -455,19 +602,78 @@ const { configure } = require('json-fs-query')
455
602
  configure({
456
603
  databasePath: 'database/', // where the JSON files live (default 'database/')
457
604
  relativePath: '', // URL prefix prepended to databasePath when reading via fetch() in a browser
458
- enforceForeignKeys: true // RESTRICT delete/update of a record other entities still reference (default true)
605
+ enforceForeignKeys: true, // RESTRICT delete/update of a record other entities still reference (default true)
606
+ readConcurrency: 16, // how many entry files a read has in flight at once (default 16)
607
+ format: 'compact', // how files are written: 'compact' or 'pretty' (default 'compact')
608
+ packedReads: undefined, // use packed files (see packDatabase); default true in a browser, false in Node
609
+ databasesPath: undefined, // a folder holding several databases (see Several databases)
610
+ database: undefined // the one of them in use
459
611
  })
460
612
  ```
461
613
 
462
614
  Turning `enforceForeignKeys` off restores the pre-1.0 behavior (no referential integrity checks at all) - useful if
463
615
  it doesn't fit a particular workflow, mirroring MySQL's own toggleable `foreign_key_checks`.
464
616
 
617
+ Reads fetch several entry files at once, `readConcurrency` at a time, and keep the record's order. In a browser each
618
+ file is one request: 16 suits HTTP/2 static hosts (such as GitHub Pages), while over HTTP/1.1 a browser only opens about
619
+ 6 connections per host anyway. `readConcurrency: 1` reads one file at a time, as every read did before this setting.
620
+
621
+ ### Several databases: `databasesPath`, `use`
622
+
623
+ One program can keep several databases (say, real data and a test copy) in one folder and switch between them:
624
+
625
+ ```js
626
+ configure({ databasesPath: 'databases/', database: 'real' }) // databases/real/ is the one in use
627
+ ```
628
+
629
+ ```sql
630
+ create database copy; -- a new, empty databases/copy/ (it doesn't switch to it)
631
+ use copy; -- the rest of this query, and the calls after it, use databases/copy/
632
+ show databases; -- ['real', 'copy'], in the order they were created
633
+ drop database if exists copy; -- deletes it and everything in it (not the one in use: use another first)
634
+ ```
635
+
636
+ `use` does what `configure({ database })` does, and both work in a browser (reading only), for any database listed in
637
+ `databases/__DATABASES.json`, the list `create database` and `drop database` keep (a browser can't list a folder).
638
+ With `databasesPath` set, calls on all its databases queue together (see Several calls at once), so a `use` part way
639
+ through a call is safe. A browser runs calls side by side instead, with one database in use for the page, so there
640
+ `use` refuses while another call is running: await it, or switch with `configure({ database })` between calls. A
641
+ database folder copied in by hand (to copy a database) isn't listed yet: `create database if not exists <name>` lists
642
+ it. Without `databasesPath`, `databasePath` names the one database, as before. A record can still be called
643
+ `database`: `create database (...)` and a bare `drop database` act on that record.
644
+
645
+ ### File format: `format` and `formatDatabase()`
646
+
647
+ Files are written as compact JSON by default: about a fifth smaller than indented JSON, and an editor shows them
648
+ formatted anyway. `format: 'pretty'` writes them indented by 2 spaces instead, for reading data in diffs (for example in
649
+ pull requests). Either format is always read, so a database can hold both: one written before this setting keeps its
650
+ indented files until they change.
651
+
652
+ `formatDatabase()` rewrites a whole database in one format (the configured one by default), leaving files already in
653
+ it alone, and resolves to how many JSON files it found and rewrote:
654
+
655
+ ```js
656
+ const { configure, formatDatabase } = require('json-fs-query')
657
+
658
+ configure({ databasePath: 'database/', format: 'pretty' })
659
+ await formatDatabase() // { files: 1119, rewritten: 1119 }
660
+ await formatDatabase('compact') // back to compact, whatever the setting
661
+ ```
662
+
663
+ It reads and checks every file before writing any: one that isn't valid JSON fails the call with its path and nothing
664
+ is changed. Node only, like every write.
665
+
666
+ **Upgrading a database kept in git:** before this setting every file was written indented, so after upgrading each
667
+ file turns compact the next time it changes, and diffs mix both. Either run `formatDatabase('compact')` once and commit
668
+ the result on its own, or set `format: 'pretty'` to keep writing indented files.
669
+
465
670
  ## Several calls at once
466
671
 
467
- Calls to `query()`, `create()`, `drop()` and `describe()` on one database (one `databasePath`) run one at a time, in
468
- the order they were made, so concurrent requests (for example in a web server) can't overwrite each other's
469
- changes. All the queries in one `query()` string run together, with no other call in between. This covers one Node
470
- process: two processes writing to the same folder (say a server and a CLI) are not protected from each other.
672
+ Calls to `query()`, `create()`, `drop()`, `describe()`, `formatDatabase()`, `packDatabase()` and `checkPacked()` on
673
+ one database (one `databasePath`) run one at a time, in the order they were made, so concurrent requests (for
674
+ example in a web server) can't overwrite each other's changes. All the queries in one `query()` string run together,
675
+ with no other call in between. This covers one Node process: two processes writing to the same folder (say a server
676
+ and a CLI) are not protected from each other.
471
677
 
472
678
  `configure()` can't point `databasePath` at another folder while calls are still running or queued (it throws:
473
679
  await them first), since each call reads the setting again at every step. The same folder written another way
@@ -491,6 +697,41 @@ Reads work in the browser via `fetch()` - `create`/`drop`, and any write through
491
697
  `delete`), need real filesystem access and only work in Node. They're still present in the browser build (same
492
698
  public API either way), but throw if actually called there, rather than being silently unavailable.
493
699
 
700
+ ### Packed tables: one request per record
701
+
702
+ Read over `fetch()`, every entry file is one request, so reading 1,000 rows is 1,000 requests. `packDatabase()` writes
703
+ each record's rows into one file, `__packed/<record>.json`, and a browser then reads the whole record in one request:
704
+ at 1,000 rows a full read takes about 25 ms instead of 1 s (see `BENCHMARKS.md`). Run it in Node as a build step
705
+ whenever the data changes, and serve (or commit) the `__packed/` folder with the rest of the database:
706
+
707
+ ```js
708
+ const { configure, packDatabase, checkPacked } = require('json-fs-query')
709
+
710
+ configure({ databasePath: 'database/' })
711
+ await packDatabase() // { records: 4, rows: 1452 }
712
+ await checkPacked() // { stale: [], orphaned: [] } - in a test, to catch a forgotten packDatabase()
713
+ ```
714
+
715
+ The entry files stay the copy that's edited and reviewed; a packed file is only ever a copy of them:
716
+
717
+ - Any `insert`, `update`, `delete` or `drop` through json-fs-query removes that record's packed file in the same
718
+ statement (a statement that changes no rows leaves it), so a browser never reads a packed file in place of a change.
719
+ - A packed file is only used while its entries match the record file, so rows added or removed without json-fs-query
720
+ fall back to the entry files.
721
+ - A row edited by hand keeps the same entries, so its packed file still looks current and a browser reads the old
722
+ row: run `packDatabase()` after hand edits, and run `checkPacked()` in CI (or a test) so a forgotten one fails the
723
+ build. It reports packed files that don't match their entry files (`stale`), and packed files of records that no
724
+ longer exist (`orphaned`, which `packDatabase()` removes).
725
+
726
+ A packed file is only read when a read needs more rows than one round of requests (`readConcurrency`, 16 by default).
727
+ A lookup of a few rows (an indexed `where` on a key, say) reads the index and those rows as before, so it never
728
+ downloads a whole record, and a record of 16 rows or fewer is never asked for a packed file. Indexes stay in their own
729
+ files. A larger record without a packed file costs one extra request that comes back as not found, before its entry
730
+ files are read as before.
731
+
732
+ Browsers use packed files by default and Node doesn't (it reads the entry files, which are always current). Change it
733
+ with `configure({ packedReads: true })` or `false`.
734
+
494
735
  ## Documentation
495
736
 
496
737
  The reference for every function is in [`docs/`](https://joshuaheagle.com/projects/json-fs-query/docs/index.html) (or open `docs/index.html` locally). It is generated from the TypeScript source, and each module is a folder of `src/data/`:
@@ -499,10 +740,11 @@ The reference for every function is in [`docs/`](https://joshuaheagle.com/projec
499
740
  | --- | --- |
500
741
  | `introspect` | Create, drop and describe a JSON-file "table" (and the policies that validate one) |
501
742
  | `queries` | Parse a query string and run it - reads, inserts, updates, deletes, joins, `where`, `sortBy`, `groupBy` |
743
+ | `storage` | Every file read, write, delete and listing (disk in Node, `fetch()` in a browser), the per-call cache and write set |
502
744
  | `utilities` | Shared helpers the queries use - retrieving records/files/indexes and merging join results |
503
745
 
504
746
  `src/index.ts` is the package's public entry point - it re-exports `query`, `quote`, `queryStatus`, `create`, `drop`,
505
- `describe`, `configure`, and the schema-definition types (`fieldProperties`, `keyProperties`, etc.) needed to build a `create()` call. Everything else
747
+ `alter`, `describe`, `formatDatabase`, `packDatabase`, `checkPacked`, `configure`, and the schema-definition types (`fieldProperties`, `keyProperties`, etc.) needed to build a `create()` call. Everything else
506
748
  under `src/data/` is an internal module, reachable but not part of the supported public API.
507
749
 
508
750
  ## Development