@agentshouse/mdruntime 0.1.0-cae97ad2e9142 → 0.1.0-caf080a9a9c60

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 (315) hide show
  1. package/README.md +14 -14
  2. package/dist/acceptance.d.ts +3 -0
  3. package/dist/acceptance.d.ts.map +1 -0
  4. package/dist/acceptance.js +318 -0
  5. package/dist/act-lanes.d.ts +29 -0
  6. package/dist/act-lanes.d.ts.map +1 -0
  7. package/dist/act-lanes.js +177 -0
  8. package/dist/actions.d.ts +25 -0
  9. package/dist/actions.d.ts.map +1 -0
  10. package/dist/actions.js +261 -0
  11. package/dist/adapter.d.ts +126 -107
  12. package/dist/adapter.d.ts.map +1 -1
  13. package/dist/attached-model.d.ts +3 -10
  14. package/dist/attached-model.d.ts.map +1 -1
  15. package/dist/attached-model.js +29 -47
  16. package/dist/bootstrap.d.ts +17 -19
  17. package/dist/bootstrap.d.ts.map +1 -1
  18. package/dist/bootstrap.js +266 -135
  19. package/dist/cell.d.ts +6 -0
  20. package/dist/cell.d.ts.map +1 -0
  21. package/dist/cell.js +9 -0
  22. package/dist/chain.d.ts +2 -1
  23. package/dist/chain.d.ts.map +1 -1
  24. package/dist/change-set.d.ts +4 -0
  25. package/dist/change-set.d.ts.map +1 -1
  26. package/dist/change-set.js +6 -0
  27. package/dist/change.d.ts +1 -0
  28. package/dist/change.d.ts.map +1 -1
  29. package/dist/change.js +4 -2
  30. package/dist/chat-types.d.ts +5 -0
  31. package/dist/chat-types.d.ts.map +1 -0
  32. package/dist/chat-types.js +13 -0
  33. package/dist/client.d.ts +28 -18
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +84 -20
  36. package/dist/collection.d.ts +28 -7
  37. package/dist/collection.d.ts.map +1 -1
  38. package/dist/collection.js +35 -19
  39. package/dist/compile.d.ts +8 -6
  40. package/dist/compile.d.ts.map +1 -1
  41. package/dist/compile.js +289 -332
  42. package/dist/connection.d.ts +6 -141
  43. package/dist/connection.d.ts.map +1 -1
  44. package/dist/connection.js +111 -5012
  45. package/dist/context.d.ts +2 -0
  46. package/dist/context.d.ts.map +1 -0
  47. package/dist/context.js +7 -0
  48. package/dist/conversation-live.d.ts +138 -0
  49. package/dist/conversation-live.d.ts.map +1 -0
  50. package/dist/conversation-live.js +249 -0
  51. package/dist/conversation-row.d.ts +33 -0
  52. package/dist/conversation-row.d.ts.map +1 -0
  53. package/dist/conversation-row.js +53 -0
  54. package/dist/conversation.d.ts +59 -50
  55. package/dist/conversation.d.ts.map +1 -1
  56. package/dist/conversation.js +938 -832
  57. package/dist/declaration.d.ts +10 -0
  58. package/dist/declaration.d.ts.map +1 -0
  59. package/dist/declaration.js +24 -0
  60. package/dist/discovery.d.ts +10 -0
  61. package/dist/discovery.d.ts.map +1 -0
  62. package/dist/discovery.js +153 -0
  63. package/dist/durability.d.ts +18 -1
  64. package/dist/durability.d.ts.map +1 -1
  65. package/dist/entry.d.ts +3 -2
  66. package/dist/entry.d.ts.map +1 -1
  67. package/dist/entry.js +19 -9
  68. package/dist/expectation.d.ts +26 -13
  69. package/dist/expectation.d.ts.map +1 -1
  70. package/dist/expectation.js +72 -19
  71. package/dist/failure.d.ts +3 -0
  72. package/dist/failure.d.ts.map +1 -1
  73. package/dist/failure.js +6 -0
  74. package/dist/figures.d.ts +49 -0
  75. package/dist/figures.d.ts.map +1 -0
  76. package/dist/figures.js +197 -0
  77. package/dist/file.d.ts +2 -11
  78. package/dist/file.d.ts.map +1 -1
  79. package/dist/file.js +6 -4
  80. package/dist/holdings.d.ts +29 -0
  81. package/dist/holdings.d.ts.map +1 -0
  82. package/dist/holdings.js +95 -0
  83. package/dist/host.d.ts +20 -8
  84. package/dist/host.d.ts.map +1 -1
  85. package/dist/host.js +70 -40
  86. package/dist/identity.d.ts +0 -1
  87. package/dist/identity.d.ts.map +1 -1
  88. package/dist/identity.js +0 -3
  89. package/dist/index.d.ts +33 -12
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +14 -3
  92. package/dist/install.d.ts +54 -0
  93. package/dist/install.d.ts.map +1 -0
  94. package/dist/install.js +539 -0
  95. package/dist/lifecycle.d.ts +7 -0
  96. package/dist/lifecycle.d.ts.map +1 -0
  97. package/dist/lifecycle.js +279 -0
  98. package/dist/listing.d.ts +48 -0
  99. package/dist/listing.d.ts.map +1 -0
  100. package/dist/listing.js +139 -0
  101. package/dist/live.d.ts +5 -4
  102. package/dist/live.d.ts.map +1 -1
  103. package/dist/live.js +64 -23
  104. package/dist/locks.d.ts +7 -0
  105. package/dist/locks.d.ts.map +1 -0
  106. package/dist/locks.js +33 -0
  107. package/dist/log.d.ts +1 -0
  108. package/dist/log.d.ts.map +1 -1
  109. package/dist/lost.d.ts +5 -0
  110. package/dist/lost.d.ts.map +1 -0
  111. package/dist/lost.js +50 -0
  112. package/dist/media.d.ts +8 -0
  113. package/dist/media.d.ts.map +1 -0
  114. package/dist/media.js +218 -0
  115. package/dist/memory.d.ts +15 -6
  116. package/dist/memory.d.ts.map +1 -1
  117. package/dist/memory.js +251 -98
  118. package/dist/messages.d.ts +24 -3
  119. package/dist/messages.d.ts.map +1 -1
  120. package/dist/messages.js +92 -32
  121. package/dist/model.d.ts +8 -11
  122. package/dist/model.d.ts.map +1 -1
  123. package/dist/model.js +5 -22
  124. package/dist/models.d.ts +10 -0
  125. package/dist/models.d.ts.map +1 -0
  126. package/dist/models.js +156 -0
  127. package/dist/observation.d.ts +3 -2
  128. package/dist/observation.d.ts.map +1 -1
  129. package/dist/observation.js +15 -15
  130. package/dist/opened.d.ts +1 -1
  131. package/dist/opened.d.ts.map +1 -1
  132. package/dist/opened.js +5 -5
  133. package/dist/opening.d.ts +4 -0
  134. package/dist/opening.d.ts.map +1 -0
  135. package/dist/opening.js +59 -0
  136. package/dist/outcome.d.ts +20 -6
  137. package/dist/outcome.d.ts.map +1 -1
  138. package/dist/outcome.js +34 -2
  139. package/dist/overlay.d.ts +6 -1
  140. package/dist/overlay.d.ts.map +1 -1
  141. package/dist/overlay.js +47 -21
  142. package/dist/page-metadata.d.ts +8 -0
  143. package/dist/page-metadata.d.ts.map +1 -0
  144. package/dist/page-metadata.js +19 -0
  145. package/dist/parts.d.ts +3 -0
  146. package/dist/parts.d.ts.map +1 -0
  147. package/dist/parts.js +10 -0
  148. package/dist/paths.d.ts +2 -0
  149. package/dist/paths.d.ts.map +1 -1
  150. package/dist/paths.js +26 -0
  151. package/dist/preparation.d.ts +20 -0
  152. package/dist/preparation.d.ts.map +1 -0
  153. package/dist/preparation.js +233 -0
  154. package/dist/presence.d.ts +52 -0
  155. package/dist/presence.d.ts.map +1 -0
  156. package/dist/presence.js +220 -0
  157. package/dist/queries.d.ts +16 -0
  158. package/dist/queries.d.ts.map +1 -0
  159. package/dist/queries.js +285 -0
  160. package/dist/query.d.ts +14 -210
  161. package/dist/query.d.ts.map +1 -1
  162. package/dist/query.js +3 -505
  163. package/dist/questions.d.ts +29 -0
  164. package/dist/questions.d.ts.map +1 -0
  165. package/dist/questions.js +115 -0
  166. package/dist/queue.d.ts +40 -0
  167. package/dist/queue.d.ts.map +1 -0
  168. package/dist/queue.js +465 -0
  169. package/dist/reach.d.ts +15 -12
  170. package/dist/reach.d.ts.map +1 -1
  171. package/dist/reach.js +228 -145
  172. package/dist/react.d.ts +15 -2
  173. package/dist/react.d.ts.map +1 -1
  174. package/dist/react.js +33 -1
  175. package/dist/read.d.ts +7 -0
  176. package/dist/read.d.ts.map +1 -0
  177. package/dist/read.js +17 -0
  178. package/dist/readiness.d.ts +3 -18
  179. package/dist/readiness.d.ts.map +1 -1
  180. package/dist/readiness.js +4 -10
  181. package/dist/reads.d.ts +38 -10
  182. package/dist/reads.d.ts.map +1 -1
  183. package/dist/reads.js +163 -85
  184. package/dist/recovery.d.ts +3 -0
  185. package/dist/recovery.d.ts.map +1 -1
  186. package/dist/recovery.js +57 -1
  187. package/dist/refresh.d.ts +7 -5
  188. package/dist/refresh.d.ts.map +1 -1
  189. package/dist/refresh.js +35 -7
  190. package/dist/refusal.d.ts +5 -0
  191. package/dist/refusal.d.ts.map +1 -0
  192. package/dist/refusal.js +57 -0
  193. package/dist/replica.d.ts +13 -19
  194. package/dist/replica.d.ts.map +1 -1
  195. package/dist/replica.js +66 -35
  196. package/dist/retried.d.ts +30 -0
  197. package/dist/retried.d.ts.map +1 -0
  198. package/dist/retried.js +72 -0
  199. package/dist/room-draft.d.ts +24 -10
  200. package/dist/room-draft.d.ts.map +1 -1
  201. package/dist/room-draft.js +142 -106
  202. package/dist/room-drafts.d.ts +24 -0
  203. package/dist/room-drafts.d.ts.map +1 -0
  204. package/dist/room-drafts.js +217 -0
  205. package/dist/rows.d.ts +16 -21
  206. package/dist/rows.d.ts.map +1 -1
  207. package/dist/rows.js +46 -174
  208. package/dist/runtime.d.ts +36 -0
  209. package/dist/runtime.d.ts.map +1 -0
  210. package/dist/runtime.js +1 -0
  211. package/dist/save.d.ts +42 -84
  212. package/dist/save.d.ts.map +1 -1
  213. package/dist/save.js +349 -170
  214. package/dist/saves.d.ts +32 -0
  215. package/dist/saves.d.ts.map +1 -0
  216. package/dist/saves.js +413 -0
  217. package/dist/search.d.ts +12 -63
  218. package/dist/search.d.ts.map +1 -1
  219. package/dist/search.js +9 -52
  220. package/dist/seed.d.ts +11 -2
  221. package/dist/seed.d.ts.map +1 -1
  222. package/dist/seed.js +71 -1
  223. package/dist/shared.d.ts +1 -7
  224. package/dist/shared.d.ts.map +1 -1
  225. package/dist/shared.js +13 -105
  226. package/dist/shown.d.ts +32 -0
  227. package/dist/shown.d.ts.map +1 -0
  228. package/dist/shown.js +290 -0
  229. package/dist/socket/admission.d.ts +7 -0
  230. package/dist/socket/admission.d.ts.map +1 -0
  231. package/dist/socket/admission.js +39 -0
  232. package/dist/socket/answers.d.ts +6 -0
  233. package/dist/socket/answers.d.ts.map +1 -0
  234. package/dist/socket/answers.js +20 -0
  235. package/dist/socket/lanes.d.ts +21 -0
  236. package/dist/socket/lanes.d.ts.map +1 -0
  237. package/dist/socket/lanes.js +80 -0
  238. package/dist/socket/reads.d.ts +10 -0
  239. package/dist/socket/reads.d.ts.map +1 -0
  240. package/dist/socket/reads.js +31 -0
  241. package/dist/socket/session.d.ts +38 -0
  242. package/dist/socket/session.d.ts.map +1 -0
  243. package/dist/socket/session.js +23 -0
  244. package/dist/socket.d.ts +68 -0
  245. package/dist/socket.d.ts.map +1 -0
  246. package/dist/socket.js +171 -0
  247. package/dist/source.d.ts +54 -0
  248. package/dist/source.d.ts.map +1 -0
  249. package/dist/source.js +82 -0
  250. package/dist/status.d.ts +4 -5
  251. package/dist/status.d.ts.map +1 -1
  252. package/dist/status.js +3 -7
  253. package/dist/store/installs.d.ts +30 -0
  254. package/dist/store/installs.d.ts.map +1 -0
  255. package/dist/store/installs.js +34 -0
  256. package/dist/store/readiness.d.ts +10 -0
  257. package/dist/store/readiness.d.ts.map +1 -0
  258. package/dist/store/readiness.js +52 -0
  259. package/dist/store/records.d.ts +212 -0
  260. package/dist/store/records.d.ts.map +1 -0
  261. package/dist/store/records.js +430 -0
  262. package/dist/store/room-held.d.ts +8 -0
  263. package/dist/store/room-held.d.ts.map +1 -0
  264. package/dist/store/room-held.js +17 -0
  265. package/dist/store/space.d.ts +11 -0
  266. package/dist/store/space.d.ts.map +1 -0
  267. package/dist/store/space.js +41 -0
  268. package/dist/store/watchers.d.ts +17 -0
  269. package/dist/store/watchers.d.ts.map +1 -0
  270. package/dist/store/watchers.js +56 -0
  271. package/dist/store/writes.d.ts +24 -0
  272. package/dist/store/writes.d.ts.map +1 -0
  273. package/dist/store/writes.js +125 -0
  274. package/dist/store.d.ts +110 -158
  275. package/dist/store.d.ts.map +1 -1
  276. package/dist/store.js +167 -403
  277. package/dist/stored.d.ts +65 -59
  278. package/dist/stored.d.ts.map +1 -1
  279. package/dist/stored.js +356 -271
  280. package/dist/surface.d.ts +88 -0
  281. package/dist/surface.d.ts.map +1 -0
  282. package/dist/surface.js +223 -0
  283. package/dist/sync.d.ts +19 -0
  284. package/dist/sync.d.ts.map +1 -0
  285. package/dist/sync.js +540 -0
  286. package/dist/tables.d.ts +127 -69
  287. package/dist/tables.d.ts.map +1 -1
  288. package/dist/tables.js +218 -105
  289. package/dist/tallies.d.ts +18 -0
  290. package/dist/tallies.d.ts.map +1 -0
  291. package/dist/tallies.js +129 -0
  292. package/dist/transfer.d.ts +0 -5
  293. package/dist/transfer.d.ts.map +1 -1
  294. package/dist/validation.d.ts +3 -4
  295. package/dist/validation.d.ts.map +1 -1
  296. package/dist/validation.js +42 -100
  297. package/dist/windows.d.ts +1 -12
  298. package/dist/windows.d.ts.map +1 -1
  299. package/dist/windows.js +2 -75
  300. package/dist/wire.d.ts +63 -11
  301. package/dist/wire.d.ts.map +1 -1
  302. package/dist/wire.js +21 -5
  303. package/package.json +3 -2
  304. package/dist/attempt.d.ts +0 -16
  305. package/dist/attempt.d.ts.map +0 -1
  306. package/dist/attempt.js +0 -70
  307. package/dist/fence.d.ts +0 -6
  308. package/dist/fence.d.ts.map +0 -1
  309. package/dist/fence.js +0 -10
  310. package/dist/fulltext.d.ts +0 -27
  311. package/dist/fulltext.d.ts.map +0 -1
  312. package/dist/fulltext.js +0 -368
  313. package/dist/words.d.ts +0 -19
  314. package/dist/words.d.ts.map +0 -1
  315. package/dist/words.js +0 -150
package/dist/compile.js CHANGED
@@ -1,119 +1,38 @@
1
- import { ARRAY_INDEX, parse, pointerSegments } from '@agentshouse/mdmodel';
1
+ import { ARRAY_INDEX, bodied, byCodePoint, compared, cursorOf, heldKind, parse, pointerSegments, sql, } from '@agentshouse/mdmodel';
2
2
  import { binaryRecord } from './binary.js';
3
- import { documentKey, kindOf, pathOfKey, placedOf, roomOfKey, settingsTextPath, } from './rows.js';
4
- import { byCodePoint, compared, cursorOf, FROM_START, MAXIMUM_WINDOW, } from './windows.js';
5
- const VISIBLE = (alias) => `${alias}.visible = 1 AND ${alias}.present = 1 AND ${alias}.queried = 1`;
6
- function fragment(sql, bind = []) {
7
- return { sql, bind };
8
- }
9
- function joined(parts, separator) {
10
- return {
11
- sql: parts.map((part) => part.sql).join(separator),
12
- bind: parts.flatMap((part) => part.bind),
13
- };
14
- }
15
- function all(parts) {
16
- return parts.length === 0 ? fragment('1') : joined(parts.map(grouped), ' AND ');
17
- }
18
- function any(parts) {
19
- return parts.length === 0 ? fragment('0') : joined(parts.map(grouped), ' OR ');
20
- }
21
- function grouped(part) {
22
- return { sql: `(${part.sql})`, bind: part.bind };
23
- }
24
- function list(values) {
25
- return fragment(`(${values.map(() => '?').join(', ')})`, values);
26
- }
27
- function sqlValue(value) {
28
- if (value === null)
29
- return { kind: 'null', value: null };
30
- if (typeof value === 'boolean')
31
- return { kind: 'boolean', value: value ? 1 : 0 };
32
- if (typeof value === 'number')
33
- return { kind: 'number', value };
34
- return { kind: 'string', value };
35
- }
36
- function same(alias, value) {
37
- const held = sqlValue(value);
38
- return held.kind === 'null'
39
- ? fragment(`${alias}.kind = 'null'`)
40
- : fragment(`${alias}.kind = ? AND ${alias}.value = ?`, [held.kind, held.value]);
41
- }
42
- const BOUNDS = { gt: '>', gte: '>=', lt: '<', lte: '<=' };
43
- function pointed(alias, field, reach) {
44
- const { keys, places, glob } = placedOf(field, reach);
45
- return fragment(`${alias}.keys = ? AND ${alias}.places ${glob ? 'GLOB' : '='} ?`, [keys, places]);
46
- }
47
- function heldValues(doc, field, alias, where) {
48
- return joined([
49
- fragment(`SELECT 1 FROM replica_values ${alias} WHERE ${alias}.doc = ${doc}.id AND `),
50
- grouped(pointed(alias, field, 'value')),
51
- fragment(' AND '),
52
- grouped(where),
53
- ], '');
54
- }
55
- function exists(part) {
56
- return { sql: `EXISTS (${part.sql})`, bind: part.bind };
57
- }
58
- function itemOf(held, where) {
59
- return exists(joined([
60
- fragment(`SELECT 1 FROM replica_values i WHERE i.doc = ${held}.doc AND i.keys = ${held}.keys AND i.places = ${held}.places AND i.item >= 0 AND `),
61
- grouped(where),
62
- ], ''));
63
- }
64
- function matching(held, values) {
65
- return any(values.map((value) => any([
66
- same(held, value),
67
- all([fragment(`${held}.kind = 'array'`), itemOf(held, same('i', value))]),
68
- ])));
69
- }
70
- function tested(doc, field, test) {
71
- const reached = exists(heldValues(doc, field, 'r', fragment('r.item = -1')));
72
- if (test.op === 'exists')
73
- return test.exists ? reached : fragment(`NOT ${reached.sql}`, reached.bind);
74
- if (test.op === 'eq' || test.op === 'in') {
75
- const values = test.op === 'eq' ? [test.value] : test.values;
76
- return exists(heldValues(doc, field, 'v', any(values.map((value) => same('v', value)))));
77
- }
78
- if (test.op === 'ne' || test.op === 'nin') {
79
- const values = test.op === 'ne' ? [test.value] : test.values;
80
- const unmatched = matching('h', values);
81
- return any([
82
- fragment(`NOT ${reached.sql}`, reached.bind),
83
- exists(heldValues(doc, field, 'h', all([fragment('h.item = -1'), fragment(`NOT (${unmatched.sql})`, unmatched.bind)]))),
84
- ]);
85
- }
86
- if (test.op === 'range') {
87
- return exists(heldValues(doc, field, 'v', all([
88
- fragment('v.item = -1'),
89
- ...test.bounds.map(([operator, bound]) => {
90
- const held = sqlValue(bound);
91
- return fragment(`v.kind = ? AND v.value ${BOUNDS[operator]} ?`, [
92
- held.kind,
93
- held.value,
94
- ]);
95
- }),
96
- ])));
97
- }
98
- if (test.op === 'all') {
99
- return exists(heldValues(doc, field, 'h', all([
100
- fragment("h.item = -1 AND h.kind = 'array'"),
101
- ...test.values.map((value) => itemOf('h', same('i', value))),
102
- ])));
103
- }
104
- return exists(heldValues(doc, field, 'v', fragment("v.item = -1 AND v.kind = 'array' AND v.size = ?", [test.size])));
3
+ import { definitionsAt } from './collection.js';
4
+ import { documentKey, pathOfKey, roomOfKey } from './rows.js';
5
+ import { FROM_START, MAXIMUM_WINDOW } from './windows.js';
6
+ const UNTYPED = 'any';
7
+ const { all, any, exists, fragment, grouped, joined, listed } = sql;
8
+ export const VISIBLE = (alias) => `${alias}.visible = 1 AND ${alias}.present = 1 AND ${alias}.queried = 1`;
9
+ function located(alias) {
10
+ return fragment(`(SELECT k.locator FROM replica_keys k WHERE k.key = ${alias}.key)`);
11
+ }
12
+ export function within(alias, path) {
13
+ return fragment(`(${alias}.path = ? OR (${alias}.path >= ? AND ${alias}.path < ?))`, [
14
+ path,
15
+ `${path}/`,
16
+ `${path}0`,
17
+ ]);
105
18
  }
106
- function conditioned(doc, condition) {
107
- if ('every' in condition)
108
- return all(condition.every.map((one) => conditioned(doc, one)));
109
- if ('some' in condition)
110
- return any(condition.some.map((one) => conditioned(doc, one)));
111
- return tested(doc, condition.field, condition.test);
19
+ function ancestors(area) {
20
+ const depth = area.path.split('/').length;
21
+ return area.open.flatMap((open) => {
22
+ const segments = open.split('/');
23
+ return segments.slice(depth - 1, -1).map((_, at) => segments.slice(0, depth + at).join('/'));
24
+ });
112
25
  }
113
- function located(bound, alias) {
114
- if (bound.rooms.length === 0)
115
- return fragment("''");
116
- return fragment(`CASE ${alias}.key ${bound.rooms.map(() => 'WHEN ? THEN ?').join(' ')} END`, bound.rooms.flatMap(({ key, locator }) => [key, locator]));
26
+ function fenced(alias, key, area, folders) {
27
+ const opened = any([
28
+ ...area.open.map((open) => within(alias, open)),
29
+ ...(folders ? ancestors(area).map((path) => fragment(`${alias}.path = ?`, [path])) : []),
30
+ ]);
31
+ return all([
32
+ fragment(`${alias}.key = ?`, [key]),
33
+ within(alias, area.path),
34
+ fragment(`NOT ${grouped(opened).sql}`, opened.bind),
35
+ ]);
117
36
  }
118
37
  function reached(bound, alias) {
119
38
  const reach = bound.reach;
@@ -127,42 +46,20 @@ function reached(bound, alias) {
127
46
  const key = bound.keys.get(roomOfKey(held));
128
47
  return key === undefined ? [] : [[key, pathOfKey(held)]];
129
48
  });
130
- const areas = reach.areas.flatMap((area) => {
49
+ const outside = any(reach.areas.flatMap((area) => {
131
50
  const key = bound.keys.get(area.room);
132
- return key === undefined ? [] : [{ key, path: area.path, types: area.types }];
133
- });
134
- const beneath = (area) => fragment(`${alias}.key = ? AND (${alias}.path = ? OR (${alias}.path >= ? AND ${alias}.path < ?))`, [area.key, area.path, `${area.path}/`, `${area.path}0`]);
135
- const typed = (types) => fragment(`${alias}.type IN ${list(types).sql}`, types);
136
- const outside = any(areas.map((area) => area.types.length === 0
137
- ? beneath(area)
138
- : all([beneath(area), fragment(`NOT ${grouped(typed(area.types)).sql}`, area.types)])));
51
+ return key === undefined ? [] : [fenced(alias, key, area, false)];
52
+ }));
139
53
  return all([
140
54
  any([
141
- ...(rooms.length === 0 ? [] : [fragment(`${alias}.key IN ${list(rooms).sql}`, rooms)]),
55
+ ...(rooms.length === 0 ? [] : [fragment(`${alias}.key IN ${listed(rooms).sql}`, rooms)]),
142
56
  ...(documents.length === 0
143
57
  ? []
144
58
  : [
145
59
  fragment(`EXISTS (SELECT 1 FROM json_each(?) j WHERE j.value ->> 0 = ${alias}.key AND j.value ->> 1 = ${alias}.path)`, [JSON.stringify(documents)]),
146
60
  ]),
147
- ...areas.flatMap((area) => area.types.length === 0 ? [] : [all([beneath(area), typed(area.types)])]),
148
61
  ]),
149
- ...(areas.length === 0 ? [] : [fragment(`NOT ${grouped(outside).sql}`, outside.bind)]),
150
- ]);
151
- }
152
- function scoped(alias, scope) {
153
- return all([
154
- ...(scope.types === null
155
- ? []
156
- : [fragment(`${alias}.type IN ${list(scope.types).sql}`, scope.types)]),
157
- ...(scope.pathPrefix === null || scope.pathPrefix === ''
158
- ? []
159
- : [
160
- fragment(`${alias}.path >= ? AND ${alias}.path < ?`, [
161
- `${scope.pathPrefix}/`,
162
- `${scope.pathPrefix}0`,
163
- ]),
164
- ]),
165
- ...(scope.where === null ? [] : [conditioned(alias, scope.where)]),
62
+ ...(reach.areas.length === 0 ? [] : [fragment(`NOT ${grouped(outside).sql}`, outside.bind)]),
166
63
  ]);
167
64
  }
168
65
  function related(bound, plan) {
@@ -170,17 +67,17 @@ function related(bound, plan) {
170
67
  if (plan.hop !== null) {
171
68
  parts.push(exists(joined([
172
69
  fragment(`SELECT 1 FROM replica_links l JOIN replica_entries m ON m.key = d.key AND m.path = l.target AND ${VISIBLE('m')} WHERE l.doc = d.id AND l.kind = 'field' AND`),
173
- grouped(pointed('l', plan.hop.field, 'link')),
70
+ grouped(sql.placed('l', plan.hop.field, 'link')),
174
71
  fragment('AND'),
175
- grouped(all([scoped('m', plan.hop), reached(bound, 'm')])),
72
+ grouped(all([sql.scoped('m', plan.hop), reached(bound, 'm')])),
176
73
  ], ' ')));
177
74
  }
178
75
  if (plan.backlinks !== null) {
179
76
  const field = plan.backlinks.field;
180
77
  parts.push(exists(joined([
181
78
  fragment(`SELECT 1 FROM replica_links l JOIN replica_entries s ON s.id = l.doc AND ${VISIBLE('s')} WHERE l.key = d.key AND l.target = d.path AND l.kind = 'field' AND`),
182
- ...(field === null ? [] : [grouped(pointed('l', field, 'link')), fragment('AND')]),
183
- grouped(all([scoped('s', plan.backlinks), reached(bound, 's')])),
79
+ ...(field === null ? [] : [grouped(sql.placed('l', field, 'link')), fragment('AND')]),
80
+ grouped(all([sql.scoped('s', plan.backlinks), reached(bound, 's')])),
184
81
  ], ' ')));
185
82
  }
186
83
  return parts;
@@ -188,11 +85,9 @@ function related(bound, plan) {
188
85
  function filtered(bound, plan) {
189
86
  const keys = bound.rooms.map(({ key }) => key);
190
87
  return all([
191
- fragment(`d.key IN ${list(keys).sql} AND ${VISIBLE('d')}`, keys),
192
- ...(plan.paths === null ? [] : [fragment(`d.path IN ${list(plan.paths).sql}`, plan.paths)]),
193
- ...(plan.folder === null ? [] : [fragment('d.folder = ?', [plan.folder])]),
88
+ fragment(`d.key IN ${listed(keys).sql} AND ${VISIBLE('d')}`, keys),
194
89
  reached(bound, 'd'),
195
- scoped('d', plan.root),
90
+ sql.tests('d', plan),
196
91
  ...related(bound, plan),
197
92
  ]);
198
93
  }
@@ -223,108 +118,65 @@ const GROUP_TIES = {
223
118
  locale: undefined,
224
119
  localeOptions: undefined,
225
120
  };
226
- function classOf(value, ordering) {
227
- if (value === null || value === undefined) {
228
- return { placement: ordering.nulls === 'first' ? 0 : 2, klass: 0, scalar: null };
229
- }
230
- if (typeof value === 'boolean')
231
- return { placement: 1, klass: 1, scalar: value ? 1 : 0 };
232
- if (typeof value === 'number')
233
- return { placement: 1, klass: 1, scalar: value };
234
- if (typeof value === 'string')
235
- return { placement: 1, klass: 2, scalar: value };
236
- return { placement: 1, klass: 3, scalar: JSON.stringify(value) };
237
- }
238
- function placement(term) {
239
- const nulls = term.ordering.nulls === 'first' ? 0 : 2;
240
- return fragment(`CASE WHEN ${term.kind.sql} IS NULL OR ${term.kind.sql} = 'null' THEN ${nulls} ELSE 1 END`, [...term.kind.bind, ...term.kind.bind]);
241
- }
242
- function klass(term) {
243
- return fragment(`CASE ${term.kind.sql} WHEN 'boolean' THEN 1 WHEN 'number' THEN 1 WHEN 'string' THEN 2 WHEN 'array' THEN 3 ELSE 4 END`, term.kind.bind);
244
- }
245
- function classed(term, which) {
246
- const kinds = which === 1 ? "('boolean', 'number')" : which === 2 ? "('string')" : "('array')";
247
- return fragment(`(CASE WHEN ${term.kind.sql} IN ${kinds} THEN ${term.value.sql} END)`, [
248
- ...term.kind.bind,
249
- ...term.value.bind,
250
- ]);
251
- }
252
- function collated(bound, term, which) {
253
- if (which === 1)
254
- return '';
255
- const name = bound.collate(term.ordering, which === 3, term.total === true);
256
- return name === null ? '' : ` COLLATE ${name}`;
257
- }
258
- function sorted(bound, term) {
259
- const direction = term.ordering.direction === 'desc' ? 'DESC' : 'ASC';
260
- return joined([
261
- placement(term),
262
- fragment(`${klass(term).sql} ${direction}`, klass(term).bind),
263
- ...[1, 2, 3].map((which) => {
264
- const value = classed(term, which);
265
- return fragment(`${value.sql}${collated(bound, term, which)} ${direction}`, value.bind);
266
- }),
267
- ], ', ');
268
- }
269
- function beyond(bound, term, value) {
270
- const held = classOf(value, term.ordering);
271
- const place = placement(term);
272
- if (held.placement !== 1) {
273
- return {
274
- beyond: fragment(`${place.sql} > ${held.placement}`, place.bind),
275
- equal: fragment(`${place.sql} = ${held.placement}`, place.bind),
276
- };
277
- }
278
- const after = term.ordering.direction === 'desc' ? '<' : '>';
279
- const kind = klass(term);
280
- const scalar = classed(term, held.klass);
281
- const collation = collated(bound, term, held.klass);
282
- return {
283
- beyond: any([
284
- fragment(`${place.sql} > 1`, place.bind),
285
- all([
286
- fragment(`${place.sql} = 1`, place.bind),
287
- any([
288
- fragment(`${kind.sql} ${after} ${held.klass}`, kind.bind),
289
- all([
290
- fragment(`${kind.sql} = ${held.klass}`, kind.bind),
291
- fragment(`${scalar.sql} ${after} ?${collation}`, [...scalar.bind, held.scalar]),
292
- ]),
293
- ]),
294
- ]),
295
- ]),
296
- equal: all([
297
- fragment(`${place.sql} = 1 AND ${kind.sql} = ${held.klass}`, [...place.bind, ...kind.bind]),
298
- fragment(`${scalar.sql} = ?${collation}`, [...scalar.bind, held.scalar]),
299
- ]),
300
- };
301
- }
302
- function following(parts, tie) {
303
- let condition = tie;
304
- for (const part of [...parts].reverse()) {
305
- condition = any([part.beyond, all([part.equal, condition])]);
306
- }
307
- return condition;
121
+ const GREATEST = { ...GROUP_ORDER, direction: 'desc', nulls: 'last' };
122
+ const TIMES = {
123
+ created: { value: 'd.created', instant: 'd.created_instant' },
124
+ updated: { value: "(d.change ->> '$.time')", instant: 'd.changed_instant' },
125
+ };
126
+ function subfolderOf(column, prefix) {
127
+ const root = prefix ?? '';
128
+ const base = root === '' ? '' : `${root}/`;
129
+ const rest = `substr(${column}, length(?) + 1)`;
130
+ return fragment(`(CASE WHEN ${column} = ? THEN NULL WHEN instr(${rest}, '/') = 0 THEN ${column} ELSE ? || substr(${rest}, 1, instr(${rest}, '/') - 1) END)`, [root, base, base, base, base]);
308
131
  }
309
- function keyTerm(bound, key, ordering, index) {
132
+ function keyTerm(key, ordering, index, prefix) {
310
133
  if (key.of === 'field') {
311
134
  const alias = `o${index}`;
312
135
  return {
313
- term: { ordering, kind: fragment(`${alias}.kind`), value: fragment(`${alias}.value`) },
136
+ term: {
137
+ ordering,
138
+ kind: fragment(`${alias}.kind`),
139
+ value: fragment(`${alias}.value`),
140
+ instant: fragment(`${alias}.instant`),
141
+ },
314
142
  join: joined([
315
143
  fragment(`LEFT JOIN replica_values ${alias} ON ${alias}.doc = d.id AND `),
316
- grouped(pointed(alias, key.field, 'exact')),
144
+ grouped(sql.placed(alias, key.field, 'exact')),
317
145
  fragment(` AND ${alias}.item = -1 AND ${alias}.kind NOT IN ('object', 'undefined')`),
318
146
  ], ''),
319
147
  };
320
148
  }
149
+ if (key.of === 'size') {
150
+ return {
151
+ term: {
152
+ ordering,
153
+ kind: fragment("(CASE WHEN d.bytes IS NULL THEN 'null' ELSE 'number' END)"),
154
+ value: fragment('d.bytes'),
155
+ },
156
+ join: null,
157
+ };
158
+ }
159
+ if (key.of === 'created' || key.of === 'updated') {
160
+ const { value, instant } = TIMES[key.of];
161
+ return {
162
+ term: {
163
+ ordering,
164
+ kind: fragment(`(CASE WHEN ${value} IS NULL THEN 'null' ELSE 'string' END)`),
165
+ value: fragment(value),
166
+ instant: fragment(instant),
167
+ },
168
+ join: null,
169
+ };
170
+ }
321
171
  const expression = key.of === 'room'
322
- ? located(bound, 'd')
172
+ ? located('d')
323
173
  : key.of === 'path'
324
174
  ? fragment('d.path')
325
175
  : key.of === 'type'
326
176
  ? fragment('d.type')
327
- : fragment('d.folder');
177
+ : key.of === 'subfolder'
178
+ ? subfolderOf('d.folder', prefix)
179
+ : fragment('d.folder');
328
180
  return {
329
181
  term: {
330
182
  ordering,
@@ -345,12 +197,20 @@ function decoded(kind, value) {
345
197
  return JSON.parse(value);
346
198
  return null;
347
199
  }
200
+ function sortedOf(kind, value, instant) {
201
+ return sql.sortValue(kind, decoded(kind, value), instant);
202
+ }
203
+ function sortedValue(row, index) {
204
+ return sortedOf(row[`k${index}`], row[`v${index}`], row[`i${index}`]);
205
+ }
348
206
  function decodedKey(key, row, index) {
349
- if (key.of === 'field') {
350
- const kind = row[`k${index}`];
351
- return kind === null || kind === undefined ? undefined : decoded(kind, row[`v${index}`]);
352
- }
353
- return row[`v${index}`] ?? null;
207
+ const kind = row[`k${index}`];
208
+ if (key.of === 'field' && (kind === null || kind === undefined))
209
+ return undefined;
210
+ return sortedValue(row, index);
211
+ }
212
+ function instantOf(term) {
213
+ return term.instant ?? fragment('NULL');
354
214
  }
355
215
  const registered = new WeakMap();
356
216
  function hashed(text) {
@@ -401,9 +261,6 @@ function collating(connection) {
401
261
  return name;
402
262
  };
403
263
  }
404
- function bodied(shape) {
405
- return shape.sections !== null || shape.preamble || shape.markdown;
406
- }
407
264
  function masked(pointers) {
408
265
  const root = { whole: false, members: new Map() };
409
266
  for (const pointer of pointers) {
@@ -455,11 +312,20 @@ function sectionsOf(sections, names) {
455
312
  return sections;
456
313
  return Object.fromEntries(names.flatMap((name) => (Object.hasOwn(sections, name) ? [[name, sections[name]]] : [])));
457
314
  }
458
- function documentOf(shape, room, row, linked) {
315
+ function freeParts(room, path, type, content) {
316
+ const definitions = room.composition === undefined ? undefined : definitionsAt(room.composition, path);
317
+ const definition = definitions?.(type === UNTYPED ? undefined : type);
318
+ if (definition === undefined && type !== UNTYPED)
319
+ return null;
320
+ const read = parse(content, path, definition);
321
+ return read.ok ? read.document : null;
322
+ }
323
+ function documentOf(shape, queried, row, linked) {
324
+ const room = queried.locator;
459
325
  const path = row.path;
460
326
  const type = row.type;
461
- const fileId = row.file_id ?? null;
462
- const revision = row.revision ?? null;
327
+ const fileId = row.file_id;
328
+ const revision = row.revision;
463
329
  if (type === null) {
464
330
  const binary = row.binary === null ? null : JSON.parse(row.binary);
465
331
  const content = row.content ?? null;
@@ -471,15 +337,16 @@ function documentOf(shape, room, row, linked) {
471
337
  : {
472
338
  binaryFile: binaryRecord(path, binary, revision ?? undefined),
473
339
  }),
474
- ...(shape.markdown && content !== null && settingsTextPath(path)
475
- ? { markdown: content }
476
- : {}),
340
+ ...(shape.markdown && content !== null ? { markdown: content } : {}),
341
+ ...(shape.change ? { change: changeOf(row) } : {}),
477
342
  ...(shape.fileId ? { fileId } : {}),
478
343
  ...(shape.revision ? { revision } : {}),
344
+ ...heldFacts(shape, row),
479
345
  };
480
346
  }
481
- const read = bodied(shape) ? parse(row.content, path) : null;
482
- const body = read?.ok === true ? read.document : null;
347
+ const body = shape.sections !== null || shape.preamble
348
+ ? freeParts(queried, path, type, row.content)
349
+ : null;
483
350
  const fields = JSON.parse(row.fields ?? '{}');
484
351
  return {
485
352
  room,
@@ -496,15 +363,21 @@ function documentOf(shape, room, row, linked) {
496
363
  ? {}
497
364
  : { sections: sectionsOf(body.sections, shape.sections) }),
498
365
  ...(shape.preamble && body !== null ? { preamble: body.preamble } : {}),
499
- ...(shape.markdown && body !== null ? { markdown: body.content } : {}),
366
+ ...(shape.markdown ? { markdown: row.content } : {}),
500
367
  ...(shape.backlinks ? { backlinks: linked.get(documentKey(room, path)) ?? [] } : {}),
501
- ...(shape.change
502
- ? {
503
- change: row.change === null ? null : JSON.parse(row.change),
504
- }
505
- : {}),
368
+ ...(shape.change ? { change: changeOf(row) } : {}),
506
369
  ...(shape.fileId ? { fileId } : {}),
507
370
  ...(shape.revision ? { revision } : {}),
371
+ ...heldFacts(shape, row),
372
+ };
373
+ }
374
+ function changeOf(row) {
375
+ return row.change === null ? null : JSON.parse(row.change);
376
+ }
377
+ function heldFacts(shape, row) {
378
+ return {
379
+ ...(shape.size ? { size: row.bytes === null ? null : Number(row.bytes) } : {}),
380
+ ...(shape.created ? { created: row.created } : {}),
508
381
  };
509
382
  }
510
383
  function backlinksOf(connection, bound, rows) {
@@ -516,8 +389,8 @@ function backlinksOf(connection, bound, rows) {
516
389
  held.push(row.path);
517
390
  byKey.set(key, held);
518
391
  }
392
+ const reach = reached(bound, 's');
519
393
  for (const [key, targets] of byKey) {
520
- const reach = reached(bound, 's');
521
394
  const room = bound.rooms.find((one) => one.key === key).locator;
522
395
  const found = connection.rows(`SELECT target, path, type, field FROM (SELECT l.target, s.path, s.type, l.field, ROW_NUMBER() OVER (PARTITION BY l.target ORDER BY s.path, l.field) AS rank FROM (SELECT DISTINCT doc, target, field FROM replica_links WHERE key = ? AND kind = 'field' AND target IN (SELECT value FROM json_each(?))) l JOIN replica_entries s ON s.id = l.doc AND ${VISIBLE('s')} WHERE ${reach.sql}) WHERE rank <= ? ORDER BY target, path, field`, [key, JSON.stringify(targets), ...reach.bind, MAXIMUM_WINDOW]);
523
396
  for (const row of found) {
@@ -530,38 +403,36 @@ function backlinksOf(connection, bound, rows) {
530
403
  return linked;
531
404
  }
532
405
  function documents(connection, bound, plan, shape, budget) {
533
- const terms = shape.orderBy.map(({ key, options }, index) => keyTerm(bound, key, orderingOf(options), index));
406
+ const terms = shape.orderBy.map(({ key, options }, index) => keyTerm(key, orderingOf(options), index, plan.root.pathPrefix));
534
407
  const filter = filtered(bound, plan);
535
408
  const cursor = plan.after?.values ?? null;
536
- const room = located(bound, 'd');
537
- const tie = cursor === null
409
+ const room = located('d');
410
+ const after = cursor === null
538
411
  ? null
539
412
  : (() => {
540
413
  const key = cursor[shape.orderBy.length];
541
- return any([
414
+ return sql.following(terms.map(({ term }, index) => sql.beyond(bound.collate, term, cursor[index])), any([
542
415
  fragment(`${room.sql} > ?`, [...room.bind, roomOfKey(key)]),
543
416
  fragment(`${room.sql} = ? AND d.path > ?`, [
544
417
  ...room.bind,
545
418
  roomOfKey(key),
546
419
  pathOfKey(key),
547
420
  ]),
548
- ]);
421
+ ]));
549
422
  })();
550
- const after = cursor === null || tie === null
551
- ? null
552
- : following(terms.map(({ term }, index) => beyond(bound, term, cursor[index])), tie);
553
423
  const selected = joined([
554
- fragment('SELECT d.id, d.key, d.path, d.type, d.binary, d.revision, d.fields, d.change, d.file_id'),
424
+ fragment('SELECT d.id, d.key, d.path, d.type, d.binary, d.revision, d.fields, d.change, d.file_id, d.bytes, d.created'),
555
425
  ...terms.map(({ term }, index) => joined([
556
426
  fragment(`, ${term.kind.sql} AS k${index}`, term.kind.bind),
557
427
  fragment(`, ${term.value.sql} AS v${index}`, term.value.bind),
428
+ fragment(`, ${instantOf(term).sql} AS i${index}`, instantOf(term).bind),
558
429
  ], '')),
559
430
  fragment(' FROM replica_entries d'),
560
431
  ...terms.flatMap(({ join }) => (join === null ? [] : [fragment(' '), join])),
561
432
  fragment(' WHERE '),
562
433
  after === null ? filter : all([filter, after]),
563
434
  fragment(' ORDER BY '),
564
- joined([...terms.map(({ term }) => sorted(bound, term)), room, fragment('d.path')], ', '),
435
+ joined([...terms.map(({ term }) => sql.sorted(bound.collate, term)), room, fragment('d.path')], ', '),
565
436
  ...(plan.limit === null ? [] : [fragment(' LIMIT ?', [plan.limit + 1])]),
566
437
  ], '');
567
438
  const rows = connection.rows(selected.sql, selected.bind);
@@ -569,8 +440,9 @@ function documents(connection, bound, plan, shape, budget) {
569
440
  const kept = bodied(shape) ? withContent(connection, windowed, budget) : windowed;
570
441
  const cut = kept.length;
571
442
  const roomOf = new Map(bound.rooms.map(({ key, locator }) => [key, locator]));
443
+ const queried = new Map(bound.rooms.map((one) => [one.key, one]));
572
444
  const linked = shape.backlinks ? backlinksOf(connection, bound, kept) : new Map();
573
- const found = kept.map((row) => documentOf(shape, roomOf.get(Number(row.key)), row, linked));
445
+ const found = kept.map((row) => documentOf(shape, queried.get(Number(row.key)), row, linked));
574
446
  const last = kept.at(-1);
575
447
  const cursorAt = (row) => cursorOf([
576
448
  ...shape.orderBy.map(({ key }, index) => decodedKey(key, row, index)),
@@ -600,7 +472,7 @@ function withContent(connection, rows, budget) {
600
472
  return rows;
601
473
  const ids = rows.map((row) => row.id);
602
474
  const sizes = new Map(connection
603
- .rows(`SELECT id, octet_length(content) AS size FROM replica_entries WHERE id IN ${list(ids).sql}`, ids)
475
+ .rows(`SELECT id, octet_length(content) AS size FROM replica_entries WHERE id IN ${listed(ids).sql}`, ids)
604
476
  .map((row) => [row.id, Number(row.size ?? 0)]));
605
477
  let cut = rows.length;
606
478
  let spent = 0;
@@ -615,125 +487,213 @@ function withContent(connection, rows, budget) {
615
487
  const kept = rows.slice(0, cut);
616
488
  const read = kept.map((row) => row.id);
617
489
  const held = new Map(connection
618
- .rows(`SELECT id, content FROM replica_entries WHERE id IN ${list(read).sql}`, read)
490
+ .rows(`SELECT id, content FROM replica_entries WHERE id IN ${listed(read).sql}`, read)
619
491
  .map((row) => [row.id, row.content]));
620
492
  return kept.map((row) => ({ ...row, content: held.get(row.id) ?? null }));
621
493
  }
622
- function storedFolders(bound, plan) {
623
- if (plan.paths !== null || plan.hop !== null || plan.backlinks !== null)
494
+ function storedFolders(bound, plan, subfolder) {
495
+ if (plan.paths !== null || plan.fileIds !== null)
496
+ return null;
497
+ if (plan.hop !== null || plan.backlinks !== null)
498
+ return null;
499
+ if (subfolder && plan.folder !== null)
624
500
  return null;
625
501
  const whole = bound.rooms.filter(({ locator }) => bound.reach === null || bound.reach.rooms.includes(locator));
626
502
  if (whole.length === 0)
627
503
  return null;
628
504
  const keys = whole.map(({ key }) => key);
629
- const prefix = plan.root.pathPrefix;
630
- const fenced = (bound.reach?.areas ?? []).flatMap((area) => {
505
+ const prefix = plan.root.pathPrefix ?? '';
506
+ const outside = (bound.reach?.areas ?? []).flatMap((area) => {
631
507
  const key = bound.keys.get(area.room);
632
- return key === undefined
633
- ? []
634
- : [
635
- fragment('NOT (f.key = ? AND (f.path = ? OR (f.path >= ? AND f.path < ?)))', [
636
- key,
637
- area.path,
638
- `${area.path}/`,
639
- `${area.path}0`,
640
- ]),
641
- ];
508
+ if (key === undefined)
509
+ return [];
510
+ const fence = fenced('f', key, area, true);
511
+ return [fragment(`NOT ${grouped(fence).sql}`, fence.bind)];
642
512
  });
513
+ const under = prefix === ''
514
+ ? subfolder
515
+ ? [fragment("f.path <> ''")]
516
+ : []
517
+ : [
518
+ subfolder
519
+ ? fragment('(f.path >= ? AND f.path < ?)', [`${prefix}/`, `${prefix}0`])
520
+ : within('f', prefix),
521
+ ];
643
522
  return all([
644
- fragment(`f.key IN ${list(keys).sql} AND f.layer = 0 AND f.present = 1`, keys),
645
- ...fenced,
646
- ...(prefix === null || prefix === ''
647
- ? []
648
- : [
649
- fragment('(f.path = ? OR (f.path >= ? AND f.path < ?))', [
650
- prefix,
651
- `${prefix}/`,
652
- `${prefix}0`,
653
- ]),
654
- ]),
523
+ fragment(`f.key IN ${listed(keys).sql} AND f.layer = 0 AND f.present = 1`, keys),
524
+ ...outside,
525
+ ...under,
526
+ ...(plan.outside ?? []).map((folder) => fragment(`NOT ${within('f', folder).sql}`, within('f', folder).bind)),
655
527
  ...(plan.folder === null ? [] : [fragment('f.path = ?', [plan.folder])]),
656
528
  ]);
657
529
  }
530
+ const SIZES = [
531
+ 'CASE WHEN d.binary IS NULL THEN 1 ELSE 0 END AS dn',
532
+ 'CASE WHEN d.binary IS NULL THEN d.bytes ELSE 0 END AS ds',
533
+ 'CASE WHEN d.binary IS NULL THEN 0 ELSE 1 END AS fn',
534
+ 'CASE WHEN d.binary IS NULL THEN 0 ELSE d.bytes END AS fs',
535
+ ];
536
+ const LATEST = "d.changed_instant || ' ' || (d.change ->> '$.time') AS u";
537
+ function greatestTerm(ordering) {
538
+ return { ordering, kind: fragment('gk'), value: fragment('gv'), instant: fragment('gi') };
539
+ }
540
+ function latestOf(held) {
541
+ if (typeof held !== 'string')
542
+ return null;
543
+ return held.slice(held.indexOf(' ') + 1);
544
+ }
658
545
  function groups(connection, bound, plan, shape) {
546
+ const prefix = plan.root.pathPrefix;
659
547
  const keyed = shape.groupBy.map((key, index) => {
660
- const { term, join } = keyTerm(bound, key, GROUP_ORDER, index);
548
+ const { term, join } = keyTerm(key, GROUP_ORDER, index, prefix);
661
549
  const kind = fragment(`COALESCE(${term.kind.sql}, 'null')`, term.kind.bind);
662
550
  return { term: { ...term, kind }, join };
663
551
  });
552
+ const ordering = shape.orderBy.map(({ by }) => by);
553
+ const sized = shape.size || ordering.includes('size');
554
+ const dated = shape.updated || ordering.includes('updated');
555
+ const greatest = shape.greatest === null
556
+ ? null
557
+ : keyTerm({ of: 'field', field: shape.greatest }, GREATEST, keyed.length, prefix);
664
558
  const filter = filtered(bound, plan);
665
559
  const inner = joined([
666
560
  fragment('SELECT '),
667
561
  joined(keyed.map(({ term }, index) => joined([
668
562
  fragment(`${term.kind.sql} AS k${index}`, term.kind.bind),
669
563
  fragment(`, ${term.value.sql} AS v${index}`, term.value.bind),
564
+ fragment(`, ${instantOf(term).sql} AS i${index}`, instantOf(term).bind),
670
565
  ], '')), ', '),
671
- fragment(', 1 AS n FROM replica_entries d'),
672
- ...keyed.flatMap(({ join }) => (join === null ? [] : [fragment(' '), join])),
566
+ fragment(['', '1 AS n', ...(sized ? SIZES : []), ...(dated ? [LATEST] : [])].join(', ')),
567
+ ...(greatest === null
568
+ ? []
569
+ : [
570
+ fragment(`, ${greatest.term.kind.sql} AS gk`, greatest.term.kind.bind),
571
+ fragment(`, ${greatest.term.value.sql} AS gv`, greatest.term.value.bind),
572
+ fragment(`, ${instantOf(greatest.term).sql} AS gi`, instantOf(greatest.term).bind),
573
+ ]),
574
+ fragment(' FROM replica_entries d'),
575
+ ...[...keyed, ...(greatest === null ? [] : [greatest])].flatMap(({ join }) => join === null ? [] : [fragment(' '), join]),
673
576
  fragment(' WHERE '),
674
577
  filter,
675
578
  ], '');
676
- const folders = shape.groupBy.length === 1 && shape.groupBy[0].of === 'folder'
677
- ? storedFolders(bound, plan)
678
- : null;
579
+ const only = shape.groupBy.length === 1 ? shape.groupBy[0].of : null;
580
+ const subfolder = only === 'subfolder';
581
+ const folders = only === 'folder' || subfolder ? storedFolders(bound, plan, subfolder) : null;
582
+ const folderKey = subfolder ? subfolderOf('f.path', prefix) : fragment('f.path');
583
+ const zeros = [
584
+ ...(sized ? ['0', '0', '0', '0'] : []),
585
+ ...(dated ? ['NULL'] : []),
586
+ ...(greatest === null ? [] : ['NULL', 'NULL', 'NULL']),
587
+ ];
679
588
  const source = folders === null
680
589
  ? inner
681
590
  : joined([
682
591
  inner,
683
- fragment(" UNION ALL SELECT 'string' AS k0, f.path AS v0, 0 AS n FROM replica_folders f WHERE "),
592
+ fragment(` UNION ALL SELECT 'string' AS k0, ${folderKey.sql} AS v0, NULL AS i0, 0 AS n${zeros.map((one) => `, ${one}`).join('')} FROM replica_folders f WHERE `, folderKey.bind),
684
593
  folders,
685
594
  ], '');
686
- const columns = keyed.map((_, index) => `k${index}, v${index}`).join(', ');
687
- const counted = {
688
- ordering: { ...GROUP_ORDER },
689
- kind: fragment("'number'"),
690
- value: fragment('n'),
691
- };
692
- const outer = (index, ordering) => ({
693
- ordering,
595
+ const held = keyed
596
+ .map((_, index) => `k${index}, i${index}, CASE WHEN i${index} IS NULL THEN v${index} END`)
597
+ .join(', ');
598
+ const ranked = greatest === null
599
+ ? source
600
+ : joined([
601
+ fragment(`SELECT *, ROW_NUMBER() OVER (PARTITION BY ${held} ORDER BY `),
602
+ sql.sorted(bound.collate, greatestTerm(GREATEST)),
603
+ fragment(', gv) AS gr FROM ('),
604
+ source,
605
+ fragment(')'),
606
+ ], '');
607
+ const columns = [
608
+ ...keyed.map((_, index) => `k${index}, min(v${index}) AS v${index}, i${index}`),
609
+ 'sum(n) AS n',
610
+ ...(sized ? ['sum(dn) AS dn', 'sum(ds) AS ds', 'sum(fn) AS fn', 'sum(fs) AS fs'] : []),
611
+ ...(dated ? ['max(u) AS u'] : []),
612
+ ...(greatest === null
613
+ ? []
614
+ : ['gk', 'gv', 'gi'].map((column) => `min(CASE WHEN gr = 1 THEN ${column} END) AS ${column}`)),
615
+ ].join(', ');
616
+ const outer = (index, order) => ({
617
+ ordering: order,
694
618
  kind: fragment(`k${index}`),
695
619
  value: fragment(`v${index}`),
696
620
  });
697
- const ordered = shape.orderBy.map(({ by, direction }) => by === 'count'
698
- ? { ...counted, ordering: { ...GROUP_ORDER, direction } }
699
- : outer(by, { ...GROUP_ORDER, direction }));
621
+ const termOf = (by, order) => {
622
+ if (by === 'count')
623
+ return { ordering: order, kind: fragment("'number'"), value: fragment('n') };
624
+ if (by === 'size') {
625
+ return { ordering: order, kind: fragment("'number'"), value: fragment('(ds + fs)') };
626
+ }
627
+ if (by === 'updated') {
628
+ return {
629
+ ordering: order,
630
+ kind: fragment("(CASE WHEN u IS NULL THEN 'null' ELSE 'string' END)"),
631
+ value: fragment("substr(u, instr(u, ' ') + 1)"),
632
+ instant: fragment("substr(u, 1, instr(u, ' ') - 1)"),
633
+ };
634
+ }
635
+ if (by === 'greatest')
636
+ return greatestTerm(order);
637
+ return { ...outer(by, order), instant: fragment(`i${by}`) };
638
+ };
639
+ const valueOf = (row, by) => {
640
+ if (by === 'count')
641
+ return Number(row.n);
642
+ if (by === 'size')
643
+ return Number(row.ds) + Number(row.fs);
644
+ if (by === 'updated') {
645
+ const text = latestOf(row.u);
646
+ return text === null ? null : sql.sortValue('string', text, row.u.split(' ')[0]);
647
+ }
648
+ if (by === 'greatest')
649
+ return sortedOf(row.gk, row.gv, row.gi);
650
+ return sortedValue(row, by);
651
+ };
652
+ const ordered = shape.orderBy.map(({ by, direction, nulls }) => termOf(by, { ...GROUP_ORDER, direction, nulls: nulls ?? GROUP_ORDER.nulls }));
700
653
  const ties = keyed.flatMap((_, index) => [
701
654
  { ...outer(index, GROUP_TIES), total: true },
702
655
  { ordering: GROUP_TIES, kind: fragment("'string'"), value: fragment(`k${index}`) },
703
656
  ]);
704
657
  const cursor = plan.after?.values ?? null;
705
- const tieValues = cursor === null
706
- ? null
707
- : cursor[shape.orderBy.length].flatMap((value) => [
708
- value,
709
- kindOf(value ?? null),
710
- ]);
711
- const after = cursor === null || tieValues === null
658
+ const after = cursor === null
712
659
  ? null
713
- : following(ordered.map((term, index) => beyond(bound, term, cursor[index])), following(ties.slice(0, -1).map((term, index) => beyond(bound, term, tieValues[index])), beyond(bound, ties.at(-1), tieValues.at(-1)).beyond));
714
- const grouping = joined([fragment(`SELECT ${columns}, sum(n) AS n FROM (`), source, fragment(`) GROUP BY ${columns}`)], '');
660
+ : (() => {
661
+ const tieValues = cursor[shape.orderBy.length].flatMap((value) => [value, heldKind(value ?? null)]);
662
+ return sql.following(ordered.map((term, index) => sql.beyond(bound.collate, term, cursor[index])), sql.following(ties
663
+ .slice(0, -1)
664
+ .map((term, index) => sql.beyond(bound.collate, term, tieValues[index])), sql.beyond(bound.collate, ties.at(-1), tieValues.at(-1)).beyond));
665
+ })();
666
+ const grouping = joined([fragment(`SELECT ${columns} FROM (`), ranked, fragment(`) GROUP BY ${held}`)], '');
715
667
  const windowed = joined([
716
668
  fragment('SELECT * FROM ('),
717
669
  grouping,
718
670
  fragment(')'),
719
671
  ...(after === null ? [] : [fragment(' WHERE '), after]),
720
672
  fragment(' ORDER BY '),
721
- joined([...ordered, ...ties].map((term) => sorted(bound, term)), ', '),
673
+ joined([...ordered, ...ties].map((term) => sql.sorted(bound.collate, term)), ', '),
722
674
  ...(plan.limit === null ? [] : [fragment(' LIMIT ?', [plan.limit + 1])]),
723
675
  ], '');
724
676
  const rows = connection.rows(windowed.sql, windowed.bind);
725
677
  const kept = plan.limit === null ? rows : rows.slice(0, plan.limit);
726
678
  const keyOf = (row) => keyed.map((_, index) => decoded(row[`k${index}`], row[`v${index}`]));
727
- const found = kept.map((row) => ({ key: keyOf(row), count: Number(row.n) }));
679
+ const found = kept.map((row) => ({
680
+ key: keyOf(row),
681
+ count: Number(row.n),
682
+ ...(shape.size
683
+ ? {
684
+ documents: { count: Number(row.dn), size: Number(row.ds) },
685
+ files: { count: Number(row.fn), size: Number(row.fs) },
686
+ }
687
+ : {}),
688
+ ...(shape.updated ? { updated: latestOf(row.u) } : {}),
689
+ ...(shape.greatest === null ? {} : { greatest: decoded(row.gk, row.gv) }),
690
+ }));
728
691
  const last = kept.at(-1);
729
692
  const next = plan.limit === null || rows.length <= plan.limit
730
693
  ? undefined
731
694
  : last === undefined
732
695
  ? (plan.after?.text ?? FROM_START)
733
- : cursorOf([
734
- ...shape.orderBy.map(({ by }) => by === 'count' ? Number(last.n) : decoded(last[`k${by}`], last[`v${by}`])),
735
- keyOf(last),
736
- ]);
696
+ : cursorOf([...shape.orderBy.map(({ by }) => valueOf(last, by)), keyOf(last)]);
737
697
  const total = plan.total
738
698
  ? Number(connection.rows(joined([fragment('SELECT count(*) AS held FROM ('), grouping, fragment(')')], '').sql, grouping.bind)[0].held)
739
699
  : undefined;
@@ -751,9 +711,6 @@ function boundOf(connection, plan, rooms) {
751
711
  collate: collating(connection),
752
712
  };
753
713
  }
754
- export function selected(connection, plan, rooms) {
755
- return filtered(boundOf(connection, plan, rooms), plan);
756
- }
757
714
  export function answerQuery(connection, plan, rooms, budget = null) {
758
715
  const bound = boundOf(connection, plan, rooms);
759
716
  if (rooms.length === 0) {