1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
// ─── analytics.rs ─────────────────────────────────────────────────────────────
// This file implements the analytics query engine used by the WASM dashboard.
//
// What is an "analytics query"?
// A regular database query returns raw documents (rows of data).
// An analytics query returns a single computed number — a metric — derived
// from many documents. Examples:
// - "How many events happened in the last minute?" → COUNT
// - "What is the average order value?" → AVG
// - "What is the highest score?" → MAX
//
// How it's used:
// The analytics dashboard (analytics_dashboard.html) calls:
// client.startAutoRefresh(queries, 500)
// Every 500ms, the worker calls:
// db.analytics(JSON.stringify(query))
// which calls execute_query() here and returns a JSON string with the result.
//
// Data flow:
// analytics_dashboard.html
// → analytics-worker.js (db.analytics)
// → worker.rs (WorkerDb::analytics)
// → analytics::execute_query()
// → db.get_all() → filter → compute metric → return AnalyticsResult
// ─────────────────────────────────────────────────────────────────────────────
// Deserialize = automatically parse JSON into this struct.
// Serialize = automatically convert this struct to JSON.
use ;
// Value = a generic JSON value (can be string, number, object, array, null).
// json! = a macro that creates a Value from a JSON literal.
use ;
// Db = the main database handle (holds in-memory state + storage backend).
// query = the query module with helper functions like evaluate_where and get_nested_value.
use crate::;
// ─── AnalyticsQuery ───────────────────────────────────────────────────────────
/// The full analytics query sent from the dashboard.
///
/// Example JSON (what the dashboard sends):
/// ```json
/// {
/// "collection": "events",
/// "metric": { "type": "COUNT" },
/// "where": { "event_type": "button_click" }
/// }
/// ```
///
/// The `#[derive(Debug, Deserialize)]` annotation tells Rust to:
/// - `Debug`: allow printing this struct with `{:?}` for debugging.
/// - `Deserialize`: automatically parse it from JSON using serde.
// ─── MetricSpec ───────────────────────────────────────────────────────────────
/// Which metric to compute and (for aggregations) which field to aggregate over.
///
/// This is a "tagged enum" — serde uses the "type" JSON key to decide which
/// variant to deserialize into. For example:
/// `{ "type": "COUNT" }` → MetricSpec::Count
/// `{ "type": "SUM", "field": "price" }` → MetricSpec::Sum { field: "price" }
///
/// `#[serde(tag = "type")]` tells serde to use the "type" field as the discriminant.
// ─── AnalyticsResult ──────────────────────────────────────────────────────────
/// The result returned to the dashboard after executing an analytics query.
///
/// `result` is the computed metric value (a number, or null if no data).
/// `metadata` contains performance information shown in the dashboard UI.
///
/// `#[derive(Serialize)]` lets this struct be converted to JSON automatically.
/// Performance metadata attached to every analytics result.
///
/// The dashboard uses `execution_time_ms` to display the "Avg Query Latency" counter.
/// `rows_scanned` is useful for understanding query cost (before indexes kick in).
// ─── execute_query ────────────────────────────────────────────────────────────
/// Execute an analytics query against the database and return the result.
///
/// Steps:
/// 1. Record the start time (for execution_time_ms).
/// 2. Fetch all documents from the requested collection (O(n) copy).
/// 3. Filter documents using the WHERE clause (if provided).
/// 4. Compute the requested metric over the filtered documents.
/// 5. Return the result with timing metadata.
///
/// # Arguments
/// * `db` — The database handle. Used to call `db.get_all(collection)`.
/// * `query` — The parsed analytics query (collection + metric + optional filter).
// ─── extract_number ───────────────────────────────────────────────────────────
/// Extract a numeric value from a document, supporting nested dot-notation paths.
///
/// Examples:
/// `extract_number(&doc, "price")` → reads doc["price"]
/// `extract_number(&doc, "meta.discount")` → reads doc["meta"]["discount"]
///
/// Returns `None` if:
/// - The field doesn't exist in the document.
/// - The field exists but is not a number (e.g. it's a string or null).
///
/// Tries three numeric types in order:
/// 1. f64 (floating point) — covers most JSON numbers.
/// 2. i64 (signed integer) — for integers that don't fit in f64 exactly.
/// 3. u64 (unsigned integer) — for very large positive integers.
// ─── Tests ────────────────────────────────────────────────────────────────────