@agentshouse/mdruntime 0.1.0-cefbc8bb8a1c0 → 0.1.0-cf08ada271a7f

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 +309 -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 -104
  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 +11 -10
  14. package/dist/bootstrap.d.ts.map +1 -1
  15. package/dist/bootstrap.js +205 -88
  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 -334
  39. package/dist/connection.d.ts +5 -143
  40. package/dist/connection.d.ts.map +1 -1
  41. package/dist/connection.js +101 -5310
  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 +873 -912
  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 +177 -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 +38 -33
  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 +27 -8
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +9 -1
  86. package/dist/install.d.ts +55 -0
  87. package/dist/install.d.ts.map +1 -0
  88. package/dist/install.js +543 -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 +271 -0
  95. package/dist/listing.d.ts +53 -0
  96. package/dist/listing.d.ts.map +1 -0
  97. package/dist/listing.js +169 -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 +9 -0
  102. package/dist/locks.d.ts.map +1 -0
  103. package/dist/locks.js +62 -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 +61 -0
  109. package/dist/media.d.ts +1 -1
  110. package/dist/media.d.ts.map +1 -1
  111. package/dist/media.js +3 -3
  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 +170 -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.js +3 -3
  128. package/dist/opening.d.ts +4 -0
  129. package/dist/opening.d.ts.map +1 -0
  130. package/dist/opening.js +59 -0
  131. package/dist/outcome.d.ts +4 -4
  132. package/dist/outcome.d.ts.map +1 -1
  133. package/dist/outcome.js +3 -0
  134. package/dist/overlay.d.ts +3 -1
  135. package/dist/overlay.d.ts.map +1 -1
  136. package/dist/overlay.js +43 -21
  137. package/dist/parts.d.ts +11 -0
  138. package/dist/parts.d.ts.map +1 -0
  139. package/dist/parts.js +21 -0
  140. package/dist/paths.d.ts +1 -0
  141. package/dist/paths.d.ts.map +1 -1
  142. package/dist/paths.js +22 -0
  143. package/dist/preparation.d.ts +20 -0
  144. package/dist/preparation.d.ts.map +1 -0
  145. package/dist/preparation.js +233 -0
  146. package/dist/queries.d.ts +16 -0
  147. package/dist/queries.d.ts.map +1 -0
  148. package/dist/queries.js +272 -0
  149. package/dist/query.d.ts +14 -210
  150. package/dist/query.d.ts.map +1 -1
  151. package/dist/query.js +3 -505
  152. package/dist/questions.d.ts +34 -0
  153. package/dist/questions.d.ts.map +1 -0
  154. package/dist/questions.js +122 -0
  155. package/dist/queue.d.ts +39 -0
  156. package/dist/queue.d.ts.map +1 -0
  157. package/dist/queue.js +455 -0
  158. package/dist/reach.d.ts +15 -13
  159. package/dist/reach.d.ts.map +1 -1
  160. package/dist/reach.js +187 -140
  161. package/dist/react.d.ts +14 -2
  162. package/dist/react.d.ts.map +1 -1
  163. package/dist/react.js +10 -1
  164. package/dist/read.d.ts +7 -0
  165. package/dist/read.d.ts.map +1 -0
  166. package/dist/read.js +17 -0
  167. package/dist/readiness.d.ts +2 -16
  168. package/dist/readiness.d.ts.map +1 -1
  169. package/dist/readiness.js +4 -10
  170. package/dist/reads.d.ts +36 -10
  171. package/dist/reads.d.ts.map +1 -1
  172. package/dist/reads.js +160 -77
  173. package/dist/recovery.d.ts.map +1 -1
  174. package/dist/recovery.js +9 -1
  175. package/dist/refresh.d.ts +7 -5
  176. package/dist/refresh.d.ts.map +1 -1
  177. package/dist/refresh.js +35 -7
  178. package/dist/refusal.d.ts +5 -0
  179. package/dist/refusal.d.ts.map +1 -0
  180. package/dist/refusal.js +57 -0
  181. package/dist/replica.d.ts +11 -17
  182. package/dist/replica.d.ts.map +1 -1
  183. package/dist/replica.js +64 -34
  184. package/dist/room-draft.d.ts +7 -8
  185. package/dist/room-draft.d.ts.map +1 -1
  186. package/dist/room-draft.js +100 -103
  187. package/dist/room-drafts.d.ts +24 -0
  188. package/dist/room-drafts.d.ts.map +1 -0
  189. package/dist/room-drafts.js +217 -0
  190. package/dist/rows.d.ts +14 -21
  191. package/dist/rows.d.ts.map +1 -1
  192. package/dist/rows.js +49 -186
  193. package/dist/runtime.d.ts +32 -0
  194. package/dist/runtime.d.ts.map +1 -0
  195. package/dist/runtime.js +1 -0
  196. package/dist/save.d.ts +41 -86
  197. package/dist/save.d.ts.map +1 -1
  198. package/dist/save.js +239 -116
  199. package/dist/saves.d.ts +32 -0
  200. package/dist/saves.d.ts.map +1 -0
  201. package/dist/saves.js +413 -0
  202. package/dist/search.d.ts +12 -63
  203. package/dist/search.d.ts.map +1 -1
  204. package/dist/search.js +9 -52
  205. package/dist/seed.d.ts +11 -2
  206. package/dist/seed.d.ts.map +1 -1
  207. package/dist/seed.js +71 -1
  208. package/dist/shared.d.ts +1 -7
  209. package/dist/shared.d.ts.map +1 -1
  210. package/dist/shared.js +13 -105
  211. package/dist/shown.d.ts +32 -0
  212. package/dist/shown.d.ts.map +1 -0
  213. package/dist/shown.js +299 -0
  214. package/dist/socket/admission.d.ts +7 -0
  215. package/dist/socket/admission.d.ts.map +1 -0
  216. package/dist/socket/admission.js +39 -0
  217. package/dist/socket/answers.d.ts +6 -0
  218. package/dist/socket/answers.d.ts.map +1 -0
  219. package/dist/socket/answers.js +20 -0
  220. package/dist/socket/lanes.d.ts +20 -0
  221. package/dist/socket/lanes.d.ts.map +1 -0
  222. package/dist/socket/lanes.js +77 -0
  223. package/dist/socket/reads.d.ts +10 -0
  224. package/dist/socket/reads.d.ts.map +1 -0
  225. package/dist/socket/reads.js +31 -0
  226. package/dist/socket/rooms.d.ts +41 -0
  227. package/dist/socket/rooms.d.ts.map +1 -0
  228. package/dist/socket/rooms.js +90 -0
  229. package/dist/socket/session.d.ts +38 -0
  230. package/dist/socket/session.d.ts.map +1 -0
  231. package/dist/socket/session.js +23 -0
  232. package/dist/socket.d.ts +69 -0
  233. package/dist/socket.d.ts.map +1 -0
  234. package/dist/socket.js +175 -0
  235. package/dist/source.d.ts +61 -0
  236. package/dist/source.d.ts.map +1 -0
  237. package/dist/source.js +82 -0
  238. package/dist/status.d.ts +0 -6
  239. package/dist/status.d.ts.map +1 -1
  240. package/dist/status.js +0 -6
  241. package/dist/store/fences.d.ts +16 -0
  242. package/dist/store/fences.d.ts.map +1 -0
  243. package/dist/store/fences.js +26 -0
  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 +107 -157
  266. package/dist/store.d.ts.map +1 -1
  267. package/dist/store.js +143 -395
  268. package/dist/stored.d.ts +59 -38
  269. package/dist/stored.d.ts.map +1 -1
  270. package/dist/stored.js +279 -210
  271. package/dist/surface.d.ts +88 -0
  272. package/dist/surface.d.ts.map +1 -0
  273. package/dist/surface.js +224 -0
  274. package/dist/sync.d.ts +19 -0
  275. package/dist/sync.d.ts.map +1 -0
  276. package/dist/sync.js +530 -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,44 +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 unindexed(column) {
159
- return `+${column}`;
160
- }
161
- function scoped(alias, scope, byPaths = false) {
162
- const type = byPaths ? unindexed(`${alias}.type`) : `${alias}.type`;
163
- return all([
164
- ...(scope.types === null ? [] : [fragment(`${type} IN ${list(scope.types).sql}`, scope.types)]),
165
- ...(scope.pathPrefix === null || scope.pathPrefix === ''
166
- ? []
167
- : [
168
- fragment(`${alias}.path >= ? AND ${alias}.path < ?`, [
169
- `${scope.pathPrefix}/`,
170
- `${scope.pathPrefix}0`,
171
- ]),
172
- ]),
173
- ...(scope.where === null ? [] : [conditioned(alias, scope.where)]),
60
+ ...(reach.areas.length === 0 ? [] : [fragment(`NOT ${grouped(outside).sql}`, outside.bind)]),
174
61
  ]);
175
62
  }
176
63
  function related(bound, plan) {
@@ -178,17 +65,17 @@ function related(bound, plan) {
178
65
  if (plan.hop !== null) {
179
66
  parts.push(exists(joined([
180
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`),
181
- grouped(pointed('l', plan.hop.field, 'link')),
68
+ grouped(sql.placed('l', plan.hop.field, 'link')),
182
69
  fragment('AND'),
183
- grouped(all([scoped('m', plan.hop), reached(bound, 'm')])),
70
+ grouped(all([sql.scoped('m', plan.hop), reached(bound, 'm')])),
184
71
  ], ' ')));
185
72
  }
186
73
  if (plan.backlinks !== null) {
187
74
  const field = plan.backlinks.field;
188
75
  parts.push(exists(joined([
189
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`),
190
- ...(field === null ? [] : [grouped(pointed('l', field, 'link')), fragment('AND')]),
191
- 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')])),
192
79
  ], ' ')));
193
80
  }
194
81
  return parts;
@@ -196,11 +83,9 @@ function related(bound, plan) {
196
83
  function filtered(bound, plan) {
197
84
  const keys = bound.rooms.map(({ key }) => key);
198
85
  return all([
199
- fragment(`d.key IN ${list(keys).sql} AND ${VISIBLE('d')}`, keys),
200
- ...(plan.paths === null ? [] : [fragment(`d.path IN ${list(plan.paths).sql}`, plan.paths)]),
201
- ...(plan.folder === null ? [] : [fragment('d.folder = ?', [plan.folder])]),
86
+ fragment(`d.key IN ${listed(keys).sql} AND ${VISIBLE('d')}`, keys),
202
87
  reached(bound, 'd'),
203
- scoped('d', plan.root, plan.paths !== null),
88
+ sql.tests('d', plan),
204
89
  ...related(bound, plan),
205
90
  ]);
206
91
  }
@@ -231,108 +116,65 @@ const GROUP_TIES = {
231
116
  locale: undefined,
232
117
  localeOptions: undefined,
233
118
  };
234
- function classOf(value, ordering) {
235
- if (value === null || value === undefined) {
236
- return { placement: ordering.nulls === 'first' ? 0 : 2, klass: 0, scalar: null };
237
- }
238
- if (typeof value === 'boolean')
239
- return { placement: 1, klass: 1, scalar: value ? 1 : 0 };
240
- if (typeof value === 'number')
241
- return { placement: 1, klass: 1, scalar: value };
242
- if (typeof value === 'string')
243
- return { placement: 1, klass: 2, scalar: value };
244
- return { placement: 1, klass: 3, scalar: JSON.stringify(value) };
245
- }
246
- function placement(term) {
247
- const nulls = term.ordering.nulls === 'first' ? 0 : 2;
248
- 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]);
249
- }
250
- function klass(term) {
251
- 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);
252
- }
253
- function classed(term, which) {
254
- const kinds = which === 1 ? "('boolean', 'number')" : which === 2 ? "('string')" : "('array')";
255
- return fragment(`(CASE WHEN ${term.kind.sql} IN ${kinds} THEN ${term.value.sql} END)`, [
256
- ...term.kind.bind,
257
- ...term.value.bind,
258
- ]);
259
- }
260
- function collated(bound, term, which) {
261
- if (which === 1)
262
- return '';
263
- const name = bound.collate(term.ordering, which === 3, term.total === true);
264
- return name === null ? '' : ` COLLATE ${name}`;
265
- }
266
- function sorted(bound, term) {
267
- const direction = term.ordering.direction === 'desc' ? 'DESC' : 'ASC';
268
- return joined([
269
- placement(term),
270
- fragment(`${klass(term).sql} ${direction}`, klass(term).bind),
271
- ...[1, 2, 3].map((which) => {
272
- const value = classed(term, which);
273
- return fragment(`${value.sql}${collated(bound, term, which)} ${direction}`, value.bind);
274
- }),
275
- ], ', ');
276
- }
277
- function beyond(bound, term, value) {
278
- const held = classOf(value, term.ordering);
279
- const place = placement(term);
280
- if (held.placement !== 1) {
281
- return {
282
- beyond: fragment(`${place.sql} > ${held.placement}`, place.bind),
283
- equal: fragment(`${place.sql} = ${held.placement}`, place.bind),
284
- };
285
- }
286
- const after = term.ordering.direction === 'desc' ? '<' : '>';
287
- const kind = klass(term);
288
- const scalar = classed(term, held.klass);
289
- const collation = collated(bound, term, held.klass);
290
- return {
291
- beyond: any([
292
- fragment(`${place.sql} > 1`, place.bind),
293
- all([
294
- fragment(`${place.sql} = 1`, place.bind),
295
- any([
296
- fragment(`${kind.sql} ${after} ${held.klass}`, kind.bind),
297
- all([
298
- fragment(`${kind.sql} = ${held.klass}`, kind.bind),
299
- fragment(`${scalar.sql} ${after} ?${collation}`, [...scalar.bind, held.scalar]),
300
- ]),
301
- ]),
302
- ]),
303
- ]),
304
- equal: all([
305
- fragment(`${place.sql} = 1 AND ${kind.sql} = ${held.klass}`, [...place.bind, ...kind.bind]),
306
- fragment(`${scalar.sql} = ?${collation}`, [...scalar.bind, held.scalar]),
307
- ]),
308
- };
309
- }
310
- function following(parts, tie) {
311
- let condition = tie;
312
- for (const part of [...parts].reverse()) {
313
- condition = any([part.beyond, all([part.equal, condition])]);
314
- }
315
- 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]);
316
129
  }
317
- function keyTerm(bound, key, ordering, index) {
130
+ function keyTerm(key, ordering, index, prefix) {
318
131
  if (key.of === 'field') {
319
132
  const alias = `o${index}`;
320
133
  return {
321
- 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
+ },
322
140
  join: joined([
323
141
  fragment(`LEFT JOIN replica_values ${alias} ON ${alias}.doc = d.id AND `),
324
- grouped(pointed(alias, key.field, 'exact')),
142
+ grouped(sql.placed(alias, key.field, 'exact')),
325
143
  fragment(` AND ${alias}.item = -1 AND ${alias}.kind NOT IN ('object', 'undefined')`),
326
144
  ], ''),
327
145
  };
328
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
+ }
329
169
  const expression = key.of === 'room'
330
- ? located(bound, 'd')
170
+ ? located('d')
331
171
  : key.of === 'path'
332
172
  ? fragment('d.path')
333
173
  : key.of === 'type'
334
174
  ? fragment('d.type')
335
- : fragment('d.folder');
175
+ : key.of === 'subfolder'
176
+ ? subfolderOf('d.folder', prefix)
177
+ : fragment('d.folder');
336
178
  return {
337
179
  term: {
338
180
  ordering,
@@ -353,12 +195,20 @@ function decoded(kind, value) {
353
195
  return JSON.parse(value);
354
196
  return null;
355
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
+ }
356
204
  function decodedKey(key, row, index) {
357
- if (key.of === 'field') {
358
- const kind = row[`k${index}`];
359
- return kind === null || kind === undefined ? undefined : decoded(kind, row[`v${index}`]);
360
- }
361
- 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');
362
212
  }
363
213
  const registered = new WeakMap();
364
214
  function hashed(text) {
@@ -409,9 +259,6 @@ function collating(connection) {
409
259
  return name;
410
260
  };
411
261
  }
412
- function bodied(shape) {
413
- return shape.sections !== null || shape.preamble || shape.markdown;
414
- }
415
262
  function masked(pointers) {
416
263
  const root = { whole: false, members: new Map() };
417
264
  for (const pointer of pointers) {
@@ -466,8 +313,8 @@ function sectionsOf(sections, names) {
466
313
  function documentOf(shape, room, row, linked) {
467
314
  const path = row.path;
468
315
  const type = row.type;
469
- const fileId = row.file_id ?? null;
470
- const revision = row.revision ?? null;
316
+ const fileId = row.file_id;
317
+ const revision = row.revision;
471
318
  if (type === null) {
472
319
  const binary = row.binary === null ? null : JSON.parse(row.binary);
473
320
  const content = row.content ?? null;
@@ -480,11 +327,13 @@ function documentOf(shape, room, row, linked) {
480
327
  binaryFile: binaryRecord(path, binary, revision ?? undefined),
481
328
  }),
482
329
  ...(shape.markdown && content !== null ? { markdown: content } : {}),
330
+ ...(shape.change ? { change: changeOf(row) } : {}),
483
331
  ...(shape.fileId ? { fileId } : {}),
484
332
  ...(shape.revision ? { revision } : {}),
333
+ ...heldFacts(shape, row),
485
334
  };
486
335
  }
487
- const read = bodied(shape) ? parse(row.content, path) : null;
336
+ const read = shape.sections !== null || shape.preamble ? parse(row.content, path) : null;
488
337
  const body = read?.ok === true ? read.document : null;
489
338
  const fields = JSON.parse(row.fields ?? '{}');
490
339
  return {
@@ -502,15 +351,21 @@ function documentOf(shape, room, row, linked) {
502
351
  ? {}
503
352
  : { sections: sectionsOf(body.sections, shape.sections) }),
504
353
  ...(shape.preamble && body !== null ? { preamble: body.preamble } : {}),
505
- ...(shape.markdown && body !== null ? { markdown: body.content } : {}),
354
+ ...(shape.markdown ? { markdown: row.content } : {}),
506
355
  ...(shape.backlinks ? { backlinks: linked.get(documentKey(room, path)) ?? [] } : {}),
507
- ...(shape.change
508
- ? {
509
- change: row.change === null ? null : JSON.parse(row.change),
510
- }
511
- : {}),
356
+ ...(shape.change ? { change: changeOf(row) } : {}),
512
357
  ...(shape.fileId ? { fileId } : {}),
513
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 } : {}),
514
369
  };
515
370
  }
516
371
  function backlinksOf(connection, bound, rows) {
@@ -522,8 +377,8 @@ function backlinksOf(connection, bound, rows) {
522
377
  held.push(row.path);
523
378
  byKey.set(key, held);
524
379
  }
380
+ const reach = reached(bound, 's');
525
381
  for (const [key, targets] of byKey) {
526
- const reach = reached(bound, 's');
527
382
  const room = bound.rooms.find((one) => one.key === key).locator;
528
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]);
529
384
  for (const row of found) {
@@ -536,38 +391,36 @@ function backlinksOf(connection, bound, rows) {
536
391
  return linked;
537
392
  }
538
393
  function documents(connection, bound, plan, shape, budget) {
539
- 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));
540
395
  const filter = filtered(bound, plan);
541
396
  const cursor = plan.after?.values ?? null;
542
- const room = located(bound, 'd');
543
- const tie = cursor === null
397
+ const room = located('d');
398
+ const after = cursor === null
544
399
  ? null
545
400
  : (() => {
546
401
  const key = cursor[shape.orderBy.length];
547
- return any([
402
+ return sql.following(terms.map(({ term }, index) => sql.beyond(bound.collate, term, cursor[index])), any([
548
403
  fragment(`${room.sql} > ?`, [...room.bind, roomOfKey(key)]),
549
404
  fragment(`${room.sql} = ? AND d.path > ?`, [
550
405
  ...room.bind,
551
406
  roomOfKey(key),
552
407
  pathOfKey(key),
553
408
  ]),
554
- ]);
409
+ ]));
555
410
  })();
556
- const after = cursor === null || tie === null
557
- ? null
558
- : following(terms.map(({ term }, index) => beyond(bound, term, cursor[index])), tie);
559
411
  const selected = joined([
560
- 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'),
561
413
  ...terms.map(({ term }, index) => joined([
562
414
  fragment(`, ${term.kind.sql} AS k${index}`, term.kind.bind),
563
415
  fragment(`, ${term.value.sql} AS v${index}`, term.value.bind),
416
+ fragment(`, ${instantOf(term).sql} AS i${index}`, instantOf(term).bind),
564
417
  ], '')),
565
418
  fragment(' FROM replica_entries d'),
566
419
  ...terms.flatMap(({ join }) => (join === null ? [] : [fragment(' '), join])),
567
420
  fragment(' WHERE '),
568
421
  after === null ? filter : all([filter, after]),
569
422
  fragment(' ORDER BY '),
570
- 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')], ', '),
571
424
  ...(plan.limit === null ? [] : [fragment(' LIMIT ?', [plan.limit + 1])]),
572
425
  ], '');
573
426
  const rows = connection.rows(selected.sql, selected.bind);
@@ -606,7 +459,7 @@ function withContent(connection, rows, budget) {
606
459
  return rows;
607
460
  const ids = rows.map((row) => row.id);
608
461
  const sizes = new Map(connection
609
- .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)
610
463
  .map((row) => [row.id, Number(row.size ?? 0)]));
611
464
  let cut = rows.length;
612
465
  let spent = 0;
@@ -621,125 +474,213 @@ function withContent(connection, rows, budget) {
621
474
  const kept = rows.slice(0, cut);
622
475
  const read = kept.map((row) => row.id);
623
476
  const held = new Map(connection
624
- .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)
625
478
  .map((row) => [row.id, row.content]));
626
479
  return kept.map((row) => ({ ...row, content: held.get(row.id) ?? null }));
627
480
  }
628
- function storedFolders(bound, plan) {
629
- 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)
630
487
  return null;
631
488
  const whole = bound.rooms.filter(({ locator }) => bound.reach === null || bound.reach.rooms.includes(locator));
632
489
  if (whole.length === 0)
633
490
  return null;
634
491
  const keys = whole.map(({ key }) => key);
635
- const prefix = plan.root.pathPrefix;
636
- const fenced = (bound.reach?.areas ?? []).flatMap((area) => {
492
+ const prefix = plan.root.pathPrefix ?? '';
493
+ const outside = (bound.reach?.areas ?? []).flatMap((area) => {
637
494
  const key = bound.keys.get(area.room);
638
- return key === undefined
639
- ? []
640
- : [
641
- fragment('NOT (f.key = ? AND (f.path = ? OR (f.path >= ? AND f.path < ?)))', [
642
- key,
643
- area.path,
644
- `${area.path}/`,
645
- `${area.path}0`,
646
- ]),
647
- ];
495
+ if (key === undefined)
496
+ return [];
497
+ const fence = fenced('f', key, area, true);
498
+ return [fragment(`NOT ${grouped(fence).sql}`, fence.bind)];
648
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
+ ];
649
509
  return all([
650
- fragment(`f.key IN ${list(keys).sql} AND f.layer = 0 AND f.present = 1`, keys),
651
- ...fenced,
652
- ...(prefix === null || prefix === ''
653
- ? []
654
- : [
655
- fragment('(f.path = ? OR (f.path >= ? AND f.path < ?))', [
656
- prefix,
657
- `${prefix}/`,
658
- `${prefix}0`,
659
- ]),
660
- ]),
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)),
661
514
  ...(plan.folder === null ? [] : [fragment('f.path = ?', [plan.folder])]),
662
515
  ]);
663
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
+ }
664
532
  function groups(connection, bound, plan, shape) {
533
+ const prefix = plan.root.pathPrefix;
665
534
  const keyed = shape.groupBy.map((key, index) => {
666
- const { term, join } = keyTerm(bound, key, GROUP_ORDER, index);
535
+ const { term, join } = keyTerm(key, GROUP_ORDER, index, prefix);
667
536
  const kind = fragment(`COALESCE(${term.kind.sql}, 'null')`, term.kind.bind);
668
537
  return { term: { ...term, kind }, join };
669
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);
670
545
  const filter = filtered(bound, plan);
671
546
  const inner = joined([
672
547
  fragment('SELECT '),
673
548
  joined(keyed.map(({ term }, index) => joined([
674
549
  fragment(`${term.kind.sql} AS k${index}`, term.kind.bind),
675
550
  fragment(`, ${term.value.sql} AS v${index}`, term.value.bind),
551
+ fragment(`, ${instantOf(term).sql} AS i${index}`, instantOf(term).bind),
676
552
  ], '')), ', '),
677
- fragment(', 1 AS n FROM replica_entries d'),
678
- ...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]),
679
563
  fragment(' WHERE '),
680
564
  filter,
681
565
  ], '');
682
- const folders = shape.groupBy.length === 1 && shape.groupBy[0].of === 'folder'
683
- ? storedFolders(bound, plan)
684
- : 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
+ ];
685
575
  const source = folders === null
686
576
  ? inner
687
577
  : joined([
688
578
  inner,
689
- 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),
690
580
  folders,
691
581
  ], '');
692
- const columns = keyed.map((_, index) => `k${index}, v${index}`).join(', ');
693
- const counted = {
694
- ordering: { ...GROUP_ORDER },
695
- kind: fragment("'number'"),
696
- value: fragment('n'),
697
- };
698
- const outer = (index, ordering) => ({
699
- 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,
700
605
  kind: fragment(`k${index}`),
701
606
  value: fragment(`v${index}`),
702
607
  });
703
- const ordered = shape.orderBy.map(({ by, direction }) => by === 'count'
704
- ? { ...counted, ordering: { ...GROUP_ORDER, direction } }
705
- : 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 }));
706
640
  const ties = keyed.flatMap((_, index) => [
707
641
  { ...outer(index, GROUP_TIES), total: true },
708
642
  { ordering: GROUP_TIES, kind: fragment("'string'"), value: fragment(`k${index}`) },
709
643
  ]);
710
644
  const cursor = plan.after?.values ?? null;
711
- const tieValues = cursor === null
712
- ? null
713
- : cursor[shape.orderBy.length].flatMap((value) => [
714
- value,
715
- kindOf(value ?? null),
716
- ]);
717
- const after = cursor === null || tieValues === null
645
+ const after = cursor === null
718
646
  ? null
719
- : 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));
720
- 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}`)], '');
721
654
  const windowed = joined([
722
655
  fragment('SELECT * FROM ('),
723
656
  grouping,
724
657
  fragment(')'),
725
658
  ...(after === null ? [] : [fragment(' WHERE '), after]),
726
659
  fragment(' ORDER BY '),
727
- joined([...ordered, ...ties].map((term) => sorted(bound, term)), ', '),
660
+ joined([...ordered, ...ties].map((term) => sql.sorted(bound.collate, term)), ', '),
728
661
  ...(plan.limit === null ? [] : [fragment(' LIMIT ?', [plan.limit + 1])]),
729
662
  ], '');
730
663
  const rows = connection.rows(windowed.sql, windowed.bind);
731
664
  const kept = plan.limit === null ? rows : rows.slice(0, plan.limit);
732
665
  const keyOf = (row) => keyed.map((_, index) => decoded(row[`k${index}`], row[`v${index}`]));
733
- 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
+ }));
734
678
  const last = kept.at(-1);
735
679
  const next = plan.limit === null || rows.length <= plan.limit
736
680
  ? undefined
737
681
  : last === undefined
738
682
  ? (plan.after?.text ?? FROM_START)
739
- : cursorOf([
740
- ...shape.orderBy.map(({ by }) => by === 'count' ? Number(last.n) : decoded(last[`k${by}`], last[`v${by}`])),
741
- keyOf(last),
742
- ]);
683
+ : cursorOf([...shape.orderBy.map(({ by }) => valueOf(last, by)), keyOf(last)]);
743
684
  const total = plan.total
744
685
  ? Number(connection.rows(joined([fragment('SELECT count(*) AS held FROM ('), grouping, fragment(')')], '').sql, grouping.bind)[0].held)
745
686
  : undefined;
@@ -757,9 +698,6 @@ function boundOf(connection, plan, rooms) {
757
698
  collate: collating(connection),
758
699
  };
759
700
  }
760
- export function selected(connection, plan, rooms) {
761
- return filtered(boundOf(connection, plan, rooms), plan);
762
- }
763
701
  export function answerQuery(connection, plan, rooms, budget = null) {
764
702
  const bound = boundOf(connection, plan, rooms);
765
703
  if (rooms.length === 0) {