@agentshouse/mdruntime 0.1.0-cb1f56e16291b → 0.1.0-cb3bb3b233fa1

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