@agentshouse/mdruntime 0.1.0-cba6f2439bb25 → 0.1.0-cbbae1fff01c6

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