@agentshouse/mdruntime 0.1.0-c44866799613e → 0.1.0-c44d5cc00aee5

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 (314) hide show
  1. package/README.md +18 -17
  2. package/dist/acceptance.d.ts +3 -0
  3. package/dist/acceptance.d.ts.map +1 -0
  4. package/dist/acceptance.js +299 -0
  5. package/dist/act-lanes.d.ts +30 -0
  6. package/dist/act-lanes.d.ts.map +1 -0
  7. package/dist/act-lanes.js +225 -0
  8. package/dist/actions.d.ts +26 -0
  9. package/dist/actions.d.ts.map +1 -0
  10. package/dist/actions.js +302 -0
  11. package/dist/adapter.d.ts +110 -112
  12. package/dist/adapter.d.ts.map +1 -1
  13. package/dist/attached-model.d.ts +3 -11
  14. package/dist/attached-model.d.ts.map +1 -1
  15. package/dist/attached-model.js +28 -51
  16. package/dist/bootstrap.d.ts +15 -18
  17. package/dist/bootstrap.d.ts.map +1 -1
  18. package/dist/bootstrap.js +252 -158
  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 +3 -0
  31. package/dist/chat-types.d.ts.map +1 -0
  32. package/dist/chat-types.js +11 -0
  33. package/dist/client.d.ts +27 -21
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +36 -17
  36. package/dist/collection.d.ts +23 -7
  37. package/dist/collection.d.ts.map +1 -1
  38. package/dist/collection.js +13 -19
  39. package/dist/compile.d.ts +8 -6
  40. package/dist/compile.d.ts.map +1 -1
  41. package/dist/compile.js +288 -329
  42. package/dist/connection.d.ts +5 -143
  43. package/dist/connection.d.ts.map +1 -1
  44. package/dist/connection.js +109 -5104
  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 +143 -0
  49. package/dist/conversation-live.d.ts.map +1 -0
  50. package/dist/conversation-live.js +254 -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 +65 -48
  55. package/dist/conversation.d.ts.map +1 -1
  56. package/dist/conversation.js +987 -838
  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 +150 -0
  63. package/dist/durability.d.ts +19 -1
  64. package/dist/durability.d.ts.map +1 -1
  65. package/dist/durability.js +4 -0
  66. package/dist/entry.d.ts +3 -2
  67. package/dist/entry.d.ts.map +1 -1
  68. package/dist/entry.js +19 -9
  69. package/dist/expectation.d.ts +26 -13
  70. package/dist/expectation.d.ts.map +1 -1
  71. package/dist/expectation.js +72 -19
  72. package/dist/failure.d.ts +3 -0
  73. package/dist/failure.d.ts.map +1 -1
  74. package/dist/failure.js +6 -0
  75. package/dist/figures.d.ts +49 -0
  76. package/dist/figures.d.ts.map +1 -0
  77. package/dist/figures.js +197 -0
  78. package/dist/holdings.d.ts +29 -0
  79. package/dist/holdings.d.ts.map +1 -0
  80. package/dist/holdings.js +95 -0
  81. package/dist/host.d.ts +21 -8
  82. package/dist/host.d.ts.map +1 -1
  83. package/dist/host.js +73 -40
  84. package/dist/identity.d.ts +0 -1
  85. package/dist/identity.d.ts.map +1 -1
  86. package/dist/identity.js +0 -3
  87. package/dist/index.d.ts +34 -12
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +16 -5
  90. package/dist/install.d.ts +54 -0
  91. package/dist/install.d.ts.map +1 -0
  92. package/dist/install.js +530 -0
  93. package/dist/lifecycle.d.ts +7 -0
  94. package/dist/lifecycle.d.ts.map +1 -0
  95. package/dist/lifecycle.js +277 -0
  96. package/dist/listing.d.ts +48 -0
  97. package/dist/listing.d.ts.map +1 -0
  98. package/dist/listing.js +139 -0
  99. package/dist/live.d.ts +5 -4
  100. package/dist/live.d.ts.map +1 -1
  101. package/dist/live.js +64 -23
  102. package/dist/locks.d.ts +7 -0
  103. package/dist/locks.d.ts.map +1 -0
  104. package/dist/locks.js +32 -0
  105. package/dist/lost.d.ts +5 -0
  106. package/dist/lost.d.ts.map +1 -0
  107. package/dist/lost.js +49 -0
  108. package/dist/media.d.ts +2 -2
  109. package/dist/media.d.ts.map +1 -1
  110. package/dist/media.js +23 -13
  111. package/dist/memory.d.ts +17 -7
  112. package/dist/memory.d.ts.map +1 -1
  113. package/dist/memory.js +210 -97
  114. package/dist/messages.d.ts +33 -5
  115. package/dist/messages.d.ts.map +1 -1
  116. package/dist/messages.js +104 -36
  117. package/dist/model.d.ts +2 -11
  118. package/dist/model.d.ts.map +1 -1
  119. package/dist/model.js +0 -23
  120. package/dist/models.d.ts +10 -0
  121. package/dist/models.d.ts.map +1 -0
  122. package/dist/models.js +153 -0
  123. package/dist/observation.d.ts +4 -3
  124. package/dist/observation.d.ts.map +1 -1
  125. package/dist/observation.js +23 -17
  126. package/dist/opened.d.ts +1 -1
  127. package/dist/opened.d.ts.map +1 -1
  128. package/dist/opened.js +5 -5
  129. package/dist/opening.d.ts +4 -0
  130. package/dist/opening.d.ts.map +1 -0
  131. package/dist/opening.js +53 -0
  132. package/dist/outcome.d.ts +7 -4
  133. package/dist/outcome.d.ts.map +1 -1
  134. package/dist/outcome.js +18 -0
  135. package/dist/overlay.d.ts +6 -1
  136. package/dist/overlay.d.ts.map +1 -1
  137. package/dist/overlay.js +47 -21
  138. package/dist/page-metadata.d.ts +8 -0
  139. package/dist/page-metadata.d.ts.map +1 -0
  140. package/dist/page-metadata.js +19 -0
  141. package/dist/parts.d.ts +4 -0
  142. package/dist/parts.d.ts.map +1 -0
  143. package/dist/parts.js +13 -0
  144. package/dist/paths.d.ts +2 -0
  145. package/dist/paths.d.ts.map +1 -1
  146. package/dist/paths.js +26 -0
  147. package/dist/preparation.d.ts +20 -0
  148. package/dist/preparation.d.ts.map +1 -0
  149. package/dist/preparation.js +227 -0
  150. package/dist/presence.d.ts +52 -0
  151. package/dist/presence.d.ts.map +1 -0
  152. package/dist/presence.js +220 -0
  153. package/dist/queries.d.ts +16 -0
  154. package/dist/queries.d.ts.map +1 -0
  155. package/dist/queries.js +282 -0
  156. package/dist/query.d.ts +14 -210
  157. package/dist/query.d.ts.map +1 -1
  158. package/dist/query.js +3 -505
  159. package/dist/questions.d.ts +29 -0
  160. package/dist/questions.d.ts.map +1 -0
  161. package/dist/questions.js +115 -0
  162. package/dist/queue.d.ts +40 -0
  163. package/dist/queue.d.ts.map +1 -0
  164. package/dist/queue.js +450 -0
  165. package/dist/reach.d.ts +15 -12
  166. package/dist/reach.d.ts.map +1 -1
  167. package/dist/reach.js +240 -149
  168. package/dist/react.d.ts +15 -2
  169. package/dist/react.d.ts.map +1 -1
  170. package/dist/react.js +33 -1
  171. package/dist/read.d.ts +7 -0
  172. package/dist/read.d.ts.map +1 -0
  173. package/dist/read.js +17 -0
  174. package/dist/readiness.d.ts +4 -19
  175. package/dist/readiness.d.ts.map +1 -1
  176. package/dist/readiness.js +4 -10
  177. package/dist/reads.d.ts +38 -10
  178. package/dist/reads.d.ts.map +1 -1
  179. package/dist/reads.js +163 -85
  180. package/dist/recovery.d.ts +3 -0
  181. package/dist/recovery.d.ts.map +1 -1
  182. package/dist/recovery.js +57 -1
  183. package/dist/refresh.d.ts +7 -5
  184. package/dist/refresh.d.ts.map +1 -1
  185. package/dist/refresh.js +35 -7
  186. package/dist/refusal.d.ts +5 -0
  187. package/dist/refusal.d.ts.map +1 -0
  188. package/dist/refusal.js +55 -0
  189. package/dist/replica.d.ts +12 -18
  190. package/dist/replica.d.ts.map +1 -1
  191. package/dist/replica.js +64 -34
  192. package/dist/retried.d.ts +30 -0
  193. package/dist/retried.d.ts.map +1 -0
  194. package/dist/retried.js +72 -0
  195. package/dist/room-draft.d.ts +24 -10
  196. package/dist/room-draft.d.ts.map +1 -1
  197. package/dist/room-draft.js +142 -106
  198. package/dist/room-drafts.d.ts +24 -0
  199. package/dist/room-drafts.d.ts.map +1 -0
  200. package/dist/room-drafts.js +217 -0
  201. package/dist/rows.d.ts +14 -21
  202. package/dist/rows.d.ts.map +1 -1
  203. package/dist/rows.js +49 -186
  204. package/dist/runtime.d.ts +34 -0
  205. package/dist/runtime.d.ts.map +1 -0
  206. package/dist/runtime.js +1 -0
  207. package/dist/save.d.ts +42 -84
  208. package/dist/save.d.ts.map +1 -1
  209. package/dist/save.js +349 -170
  210. package/dist/saves.d.ts +32 -0
  211. package/dist/saves.d.ts.map +1 -0
  212. package/dist/saves.js +410 -0
  213. package/dist/search.d.ts +12 -63
  214. package/dist/search.d.ts.map +1 -1
  215. package/dist/search.js +9 -52
  216. package/dist/seed.d.ts +11 -2
  217. package/dist/seed.d.ts.map +1 -1
  218. package/dist/seed.js +71 -1
  219. package/dist/shared.d.ts +1 -7
  220. package/dist/shared.d.ts.map +1 -1
  221. package/dist/shared.js +13 -105
  222. package/dist/shown.d.ts +33 -0
  223. package/dist/shown.d.ts.map +1 -0
  224. package/dist/shown.js +302 -0
  225. package/dist/socket/admission.d.ts +7 -0
  226. package/dist/socket/admission.d.ts.map +1 -0
  227. package/dist/socket/admission.js +39 -0
  228. package/dist/socket/answers.d.ts +6 -0
  229. package/dist/socket/answers.d.ts.map +1 -0
  230. package/dist/socket/answers.js +20 -0
  231. package/dist/socket/lanes.d.ts +28 -0
  232. package/dist/socket/lanes.d.ts.map +1 -0
  233. package/dist/socket/lanes.js +103 -0
  234. package/dist/socket/reads.d.ts +10 -0
  235. package/dist/socket/reads.d.ts.map +1 -0
  236. package/dist/socket/reads.js +31 -0
  237. package/dist/socket/session.d.ts +38 -0
  238. package/dist/socket/session.d.ts.map +1 -0
  239. package/dist/socket/session.js +23 -0
  240. package/dist/socket.d.ts +67 -0
  241. package/dist/socket.d.ts.map +1 -0
  242. package/dist/socket.js +164 -0
  243. package/dist/source.d.ts +54 -0
  244. package/dist/source.d.ts.map +1 -0
  245. package/dist/source.js +82 -0
  246. package/dist/status.d.ts +4 -5
  247. package/dist/status.d.ts.map +1 -1
  248. package/dist/status.js +3 -7
  249. package/dist/store/installs.d.ts +30 -0
  250. package/dist/store/installs.d.ts.map +1 -0
  251. package/dist/store/installs.js +34 -0
  252. package/dist/store/readiness.d.ts +10 -0
  253. package/dist/store/readiness.d.ts.map +1 -0
  254. package/dist/store/readiness.js +52 -0
  255. package/dist/store/records.d.ts +214 -0
  256. package/dist/store/records.d.ts.map +1 -0
  257. package/dist/store/records.js +438 -0
  258. package/dist/store/room-held.d.ts +8 -0
  259. package/dist/store/room-held.d.ts.map +1 -0
  260. package/dist/store/room-held.js +17 -0
  261. package/dist/store/space.d.ts +11 -0
  262. package/dist/store/space.d.ts.map +1 -0
  263. package/dist/store/space.js +41 -0
  264. package/dist/store/watchers.d.ts +17 -0
  265. package/dist/store/watchers.d.ts.map +1 -0
  266. package/dist/store/watchers.js +56 -0
  267. package/dist/store/writes.d.ts +24 -0
  268. package/dist/store/writes.d.ts.map +1 -0
  269. package/dist/store/writes.js +125 -0
  270. package/dist/store.d.ts +120 -171
  271. package/dist/store.d.ts.map +1 -1
  272. package/dist/store.js +166 -414
  273. package/dist/stored.d.ts +62 -57
  274. package/dist/stored.d.ts.map +1 -1
  275. package/dist/stored.js +317 -239
  276. package/dist/surface.d.ts +88 -0
  277. package/dist/surface.d.ts.map +1 -0
  278. package/dist/surface.js +220 -0
  279. package/dist/sync.d.ts +19 -0
  280. package/dist/sync.d.ts.map +1 -0
  281. package/dist/sync.js +535 -0
  282. package/dist/tables.d.ts +126 -65
  283. package/dist/tables.d.ts.map +1 -1
  284. package/dist/tables.js +250 -113
  285. package/dist/tallies.d.ts +18 -0
  286. package/dist/tallies.d.ts.map +1 -0
  287. package/dist/tallies.js +128 -0
  288. package/dist/transfer.d.ts +0 -5
  289. package/dist/transfer.d.ts.map +1 -1
  290. package/dist/validation.d.ts +3 -4
  291. package/dist/validation.d.ts.map +1 -1
  292. package/dist/validation.js +42 -100
  293. package/dist/windows.d.ts +1 -12
  294. package/dist/windows.d.ts.map +1 -1
  295. package/dist/windows.js +2 -75
  296. package/dist/wire.d.ts +65 -9
  297. package/dist/wire.d.ts.map +1 -1
  298. package/dist/wire.js +22 -5
  299. package/package.json +3 -2
  300. package/dist/attempt.d.ts +0 -16
  301. package/dist/attempt.d.ts.map +0 -1
  302. package/dist/attempt.js +0 -70
  303. package/dist/fence.d.ts +0 -6
  304. package/dist/fence.d.ts.map +0 -1
  305. package/dist/fence.js +0 -10
  306. package/dist/fulltext.d.ts +0 -27
  307. package/dist/fulltext.d.ts.map +0 -1
  308. package/dist/fulltext.js +0 -368
  309. package/dist/log.d.ts +0 -5
  310. package/dist/log.d.ts.map +0 -1
  311. package/dist/log.js +0 -14
  312. package/dist/words.d.ts +0 -19
  313. package/dist/words.d.ts.map +0 -1
  314. 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 } 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;
@@ -472,12 +338,15 @@ function documentOf(shape, room, row, linked) {
472
338
  binaryFile: binaryRecord(path, binary, revision ?? undefined),
473
339
  }),
474
340
  ...(shape.markdown && content !== null ? { markdown: content } : {}),
341
+ ...(shape.change ? { change: changeOf(row) } : {}),
475
342
  ...(shape.fileId ? { fileId } : {}),
476
343
  ...(shape.revision ? { revision } : {}),
344
+ ...heldFacts(shape, row),
477
345
  };
478
346
  }
479
- const read = bodied(shape) ? parse(row.content, path) : null;
480
- 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;
481
350
  const fields = JSON.parse(row.fields ?? '{}');
482
351
  return {
483
352
  room,
@@ -494,15 +363,21 @@ function documentOf(shape, room, row, linked) {
494
363
  ? {}
495
364
  : { sections: sectionsOf(body.sections, shape.sections) }),
496
365
  ...(shape.preamble && body !== null ? { preamble: body.preamble } : {}),
497
- ...(shape.markdown && body !== null ? { markdown: body.content } : {}),
366
+ ...(shape.markdown ? { markdown: row.content } : {}),
498
367
  ...(shape.backlinks ? { backlinks: linked.get(documentKey(room, path)) ?? [] } : {}),
499
- ...(shape.change
500
- ? {
501
- change: row.change === null ? null : JSON.parse(row.change),
502
- }
503
- : {}),
368
+ ...(shape.change ? { change: changeOf(row) } : {}),
504
369
  ...(shape.fileId ? { fileId } : {}),
505
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 } : {}),
506
381
  };
507
382
  }
508
383
  function backlinksOf(connection, bound, rows) {
@@ -514,8 +389,8 @@ function backlinksOf(connection, bound, rows) {
514
389
  held.push(row.path);
515
390
  byKey.set(key, held);
516
391
  }
392
+ const reach = reached(bound, 's');
517
393
  for (const [key, targets] of byKey) {
518
- const reach = reached(bound, 's');
519
394
  const room = bound.rooms.find((one) => one.key === key).locator;
520
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]);
521
396
  for (const row of found) {
@@ -528,38 +403,36 @@ function backlinksOf(connection, bound, rows) {
528
403
  return linked;
529
404
  }
530
405
  function documents(connection, bound, plan, shape, budget) {
531
- 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));
532
407
  const filter = filtered(bound, plan);
533
408
  const cursor = plan.after?.values ?? null;
534
- const room = located(bound, 'd');
535
- const tie = cursor === null
409
+ const room = located('d');
410
+ const after = cursor === null
536
411
  ? null
537
412
  : (() => {
538
413
  const key = cursor[shape.orderBy.length];
539
- return any([
414
+ return sql.following(terms.map(({ term }, index) => sql.beyond(bound.collate, term, cursor[index])), any([
540
415
  fragment(`${room.sql} > ?`, [...room.bind, roomOfKey(key)]),
541
416
  fragment(`${room.sql} = ? AND d.path > ?`, [
542
417
  ...room.bind,
543
418
  roomOfKey(key),
544
419
  pathOfKey(key),
545
420
  ]),
546
- ]);
421
+ ]));
547
422
  })();
548
- const after = cursor === null || tie === null
549
- ? null
550
- : following(terms.map(({ term }, index) => beyond(bound, term, cursor[index])), tie);
551
423
  const selected = joined([
552
- 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'),
553
425
  ...terms.map(({ term }, index) => joined([
554
426
  fragment(`, ${term.kind.sql} AS k${index}`, term.kind.bind),
555
427
  fragment(`, ${term.value.sql} AS v${index}`, term.value.bind),
428
+ fragment(`, ${instantOf(term).sql} AS i${index}`, instantOf(term).bind),
556
429
  ], '')),
557
430
  fragment(' FROM replica_entries d'),
558
431
  ...terms.flatMap(({ join }) => (join === null ? [] : [fragment(' '), join])),
559
432
  fragment(' WHERE '),
560
433
  after === null ? filter : all([filter, after]),
561
434
  fragment(' ORDER BY '),
562
- 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')], ', '),
563
436
  ...(plan.limit === null ? [] : [fragment(' LIMIT ?', [plan.limit + 1])]),
564
437
  ], '');
565
438
  const rows = connection.rows(selected.sql, selected.bind);
@@ -567,8 +440,9 @@ function documents(connection, bound, plan, shape, budget) {
567
440
  const kept = bodied(shape) ? withContent(connection, windowed, budget) : windowed;
568
441
  const cut = kept.length;
569
442
  const roomOf = new Map(bound.rooms.map(({ key, locator }) => [key, locator]));
443
+ const queried = new Map(bound.rooms.map((one) => [one.key, one]));
570
444
  const linked = shape.backlinks ? backlinksOf(connection, bound, kept) : new Map();
571
- 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));
572
446
  const last = kept.at(-1);
573
447
  const cursorAt = (row) => cursorOf([
574
448
  ...shape.orderBy.map(({ key }, index) => decodedKey(key, row, index)),
@@ -598,7 +472,7 @@ function withContent(connection, rows, budget) {
598
472
  return rows;
599
473
  const ids = rows.map((row) => row.id);
600
474
  const sizes = new Map(connection
601
- .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)
602
476
  .map((row) => [row.id, Number(row.size ?? 0)]));
603
477
  let cut = rows.length;
604
478
  let spent = 0;
@@ -613,125 +487,213 @@ function withContent(connection, rows, budget) {
613
487
  const kept = rows.slice(0, cut);
614
488
  const read = kept.map((row) => row.id);
615
489
  const held = new Map(connection
616
- .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)
617
491
  .map((row) => [row.id, row.content]));
618
492
  return kept.map((row) => ({ ...row, content: held.get(row.id) ?? null }));
619
493
  }
620
- function storedFolders(bound, plan) {
621
- 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)
622
500
  return null;
623
501
  const whole = bound.rooms.filter(({ locator }) => bound.reach === null || bound.reach.rooms.includes(locator));
624
502
  if (whole.length === 0)
625
503
  return null;
626
504
  const keys = whole.map(({ key }) => key);
627
- const prefix = plan.root.pathPrefix;
628
- const fenced = (bound.reach?.areas ?? []).flatMap((area) => {
505
+ const prefix = plan.root.pathPrefix ?? '';
506
+ const outside = (bound.reach?.areas ?? []).flatMap((area) => {
629
507
  const key = bound.keys.get(area.room);
630
- return key === undefined
631
- ? []
632
- : [
633
- fragment('NOT (f.key = ? AND (f.path = ? OR (f.path >= ? AND f.path < ?)))', [
634
- key,
635
- area.path,
636
- `${area.path}/`,
637
- `${area.path}0`,
638
- ]),
639
- ];
508
+ if (key === undefined)
509
+ return [];
510
+ const fence = fenced('f', key, area, true);
511
+ return [fragment(`NOT ${grouped(fence).sql}`, fence.bind)];
640
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
+ ];
641
522
  return all([
642
- fragment(`f.key IN ${list(keys).sql} AND f.layer = 0 AND f.present = 1`, keys),
643
- ...fenced,
644
- ...(prefix === null || prefix === ''
645
- ? []
646
- : [
647
- fragment('(f.path = ? OR (f.path >= ? AND f.path < ?))', [
648
- prefix,
649
- `${prefix}/`,
650
- `${prefix}0`,
651
- ]),
652
- ]),
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)),
653
527
  ...(plan.folder === null ? [] : [fragment('f.path = ?', [plan.folder])]),
654
528
  ]);
655
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
+ }
656
545
  function groups(connection, bound, plan, shape) {
546
+ const prefix = plan.root.pathPrefix;
657
547
  const keyed = shape.groupBy.map((key, index) => {
658
- const { term, join } = keyTerm(bound, key, GROUP_ORDER, index);
548
+ const { term, join } = keyTerm(key, GROUP_ORDER, index, prefix);
659
549
  const kind = fragment(`COALESCE(${term.kind.sql}, 'null')`, term.kind.bind);
660
550
  return { term: { ...term, kind }, join };
661
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);
662
558
  const filter = filtered(bound, plan);
663
559
  const inner = joined([
664
560
  fragment('SELECT '),
665
561
  joined(keyed.map(({ term }, index) => joined([
666
562
  fragment(`${term.kind.sql} AS k${index}`, term.kind.bind),
667
563
  fragment(`, ${term.value.sql} AS v${index}`, term.value.bind),
564
+ fragment(`, ${instantOf(term).sql} AS i${index}`, instantOf(term).bind),
668
565
  ], '')), ', '),
669
- fragment(', 1 AS n FROM replica_entries d'),
670
- ...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]),
671
576
  fragment(' WHERE '),
672
577
  filter,
673
578
  ], '');
674
- const folders = shape.groupBy.length === 1 && shape.groupBy[0].of === 'folder'
675
- ? storedFolders(bound, plan)
676
- : 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
+ ];
677
588
  const source = folders === null
678
589
  ? inner
679
590
  : joined([
680
591
  inner,
681
- 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),
682
593
  folders,
683
594
  ], '');
684
- const columns = keyed.map((_, index) => `k${index}, v${index}`).join(', ');
685
- const counted = {
686
- ordering: { ...GROUP_ORDER },
687
- kind: fragment("'number'"),
688
- value: fragment('n'),
689
- };
690
- const outer = (index, ordering) => ({
691
- 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,
692
618
  kind: fragment(`k${index}`),
693
619
  value: fragment(`v${index}`),
694
620
  });
695
- const ordered = shape.orderBy.map(({ by, direction }) => by === 'count'
696
- ? { ...counted, ordering: { ...GROUP_ORDER, direction } }
697
- : 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 }));
698
653
  const ties = keyed.flatMap((_, index) => [
699
654
  { ...outer(index, GROUP_TIES), total: true },
700
655
  { ordering: GROUP_TIES, kind: fragment("'string'"), value: fragment(`k${index}`) },
701
656
  ]);
702
657
  const cursor = plan.after?.values ?? null;
703
- const tieValues = cursor === null
704
- ? null
705
- : cursor[shape.orderBy.length].flatMap((value) => [
706
- value,
707
- kindOf(value ?? null),
708
- ]);
709
- const after = cursor === null || tieValues === null
658
+ const after = cursor === null
710
659
  ? null
711
- : 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));
712
- 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}`)], '');
713
667
  const windowed = joined([
714
668
  fragment('SELECT * FROM ('),
715
669
  grouping,
716
670
  fragment(')'),
717
671
  ...(after === null ? [] : [fragment(' WHERE '), after]),
718
672
  fragment(' ORDER BY '),
719
- joined([...ordered, ...ties].map((term) => sorted(bound, term)), ', '),
673
+ joined([...ordered, ...ties].map((term) => sql.sorted(bound.collate, term)), ', '),
720
674
  ...(plan.limit === null ? [] : [fragment(' LIMIT ?', [plan.limit + 1])]),
721
675
  ], '');
722
676
  const rows = connection.rows(windowed.sql, windowed.bind);
723
677
  const kept = plan.limit === null ? rows : rows.slice(0, plan.limit);
724
678
  const keyOf = (row) => keyed.map((_, index) => decoded(row[`k${index}`], row[`v${index}`]));
725
- 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
+ }));
726
691
  const last = kept.at(-1);
727
692
  const next = plan.limit === null || rows.length <= plan.limit
728
693
  ? undefined
729
694
  : last === undefined
730
695
  ? (plan.after?.text ?? FROM_START)
731
- : cursorOf([
732
- ...shape.orderBy.map(({ by }) => by === 'count' ? Number(last.n) : decoded(last[`k${by}`], last[`v${by}`])),
733
- keyOf(last),
734
- ]);
696
+ : cursorOf([...shape.orderBy.map(({ by }) => valueOf(last, by)), keyOf(last)]);
735
697
  const total = plan.total
736
698
  ? Number(connection.rows(joined([fragment('SELECT count(*) AS held FROM ('), grouping, fragment(')')], '').sql, grouping.bind)[0].held)
737
699
  : undefined;
@@ -749,9 +711,6 @@ function boundOf(connection, plan, rooms) {
749
711
  collate: collating(connection),
750
712
  };
751
713
  }
752
- export function selected(connection, plan, rooms) {
753
- return filtered(boundOf(connection, plan, rooms), plan);
754
- }
755
714
  export function answerQuery(connection, plan, rooms, budget = null) {
756
715
  const bound = boundOf(connection, plan, rooms);
757
716
  if (rooms.length === 0) {