@agentshouse/mdruntime 0.1.0-c3c7796382e28 → 0.1.0-c3faae279d4d2

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