1use std::borrow::Cow;
16use std::cmp::Ordering;
17use std::collections::{BTreeMap, HashMap};
18
19use rustc_hash::FxHashMap;
20use serde::{Deserialize, Serialize};
21use serde_json::{json, Map, Value};
22
23use super::signature::canonical_json;
24use super::sources::{parse_source, source_with_params, ParsedSource};
25use super::{js_trim, utf16_cmp};
26use crate::dates::{date_bucket, days_in_month, weekday_of, DateBucket, DATE_BUCKETS};
27use crate::json::{number, number_to_string};
28
29pub const RECORDS_SCHEME: &str = "records";
31pub const RECORDS_SORT_DEFAULT: &str = "createdAt";
33pub const RECORDS_ORDER_DEFAULT: &str = "desc";
34pub const RECORDS_LIMIT_DEFAULT: u32 = 100;
35pub const RECORDS_LIMIT_MAX: u32 = 500;
36pub const RECORDS_RESERVED_PARAMS: &[&str] = &["app", "before", "by", "expand", "limit", "mine", "month", "order", "q", "query", "sort", "sum", "user"];
38pub const RECORDS_ROW_KEYS: &[&str] = &["id", "createdAt", "updatedAt", "createdBy", "authorId", "author"];
40pub const RECORDS_FILTER_OPS: &[&str] = &["eq", "gt", "gte", "lt", "lte", "in", "between"];
43pub const RECORDS_OP_ALIASES: &[(&str, &str)] = &[("=", "eq"), ("==", "eq"), (">", "gt"), (">=", "gte"), ("<", "lt"), ("<=", "lte")];
45pub const RECORDS_AGGREGATE_OPS: &[&str] = &["sum", "avg", "min", "max", "count", "countDistinct", "first", "last"];
46pub const RECORDS_RUNNING_OPS: &[&str] = &["sum", "count", "avg", "min", "max"];
47pub const RECORDS_AGGREGATE_RESERVED: &[&str] = &["count", "sum", "running", "rows"];
49pub const RECORDS_WEEKDAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
51pub const RECORDS_MONTHS: [&str; 12] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "lowercase")]
55pub enum RecordsFilterOp {
56 Eq,
57 Gt,
58 Gte,
59 Lt,
60 Lte,
61 In,
62 Between,
63}
64
65impl RecordsFilterOp {
66 pub fn as_str(self) -> &'static str {
67 match self {
68 Self::Eq => "eq",
69 Self::Gt => "gt",
70 Self::Gte => "gte",
71 Self::Lt => "lt",
72 Self::Lte => "lte",
73 Self::In => "in",
74 Self::Between => "between",
75 }
76 }
77
78 pub fn parse(text: &str) -> Option<Self> {
80 let name = RECORDS_OP_ALIASES.iter().find(|(alias, _)| *alias == text).map_or(text, |(_, op)| op);
81 match name {
82 "eq" => Some(Self::Eq),
83 "gt" => Some(Self::Gt),
84 "gte" => Some(Self::Gte),
85 "lt" => Some(Self::Lt),
86 "lte" => Some(Self::Lte),
87 "in" => Some(Self::In),
88 "between" => Some(Self::Between),
89 _ => None,
90 }
91 }
92
93 fn suffix(text: &str) -> Option<Self> {
95 match text {
96 "gt" => Some(Self::Gt),
97 "gte" => Some(Self::Gte),
98 "lt" => Some(Self::Lt),
99 "lte" => Some(Self::Lte),
100 "in" => Some(Self::In),
101 "between" => Some(Self::Between),
102 _ => None,
103 }
104 }
105
106 fn is_list(self) -> bool {
107 matches!(self, Self::In | Self::Between)
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
113#[serde(untagged)]
114pub enum RecordsFilterValue {
115 One(String),
116 Many(Vec<String>),
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
122pub struct RecordsFilter {
123 pub field: String,
124 pub op: RecordsFilterOp,
125 pub value: RecordsFilterValue,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
129#[serde(rename_all = "lowercase")]
130pub enum RecordsOrder {
131 Asc,
132 Desc,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
136pub struct RecordsSortKey {
137 pub field: String,
138 pub order: RecordsOrder,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
143pub struct RecordsMonthView {
144 pub field: String,
145 pub key: Option<String>,
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
152pub struct RecordsGroupKey {
153 pub field: String,
154 pub bucket: Option<DateBucket>,
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub enum RecordsAggregateOp {
160 Sum,
161 Avg,
162 Min,
163 Max,
164 Count,
165 CountDistinct,
166 First,
167 Last,
168}
169
170impl RecordsAggregateOp {
171 pub fn parse(text: &str) -> Option<Self> {
172 match text {
173 "sum" => Some(Self::Sum),
174 "avg" => Some(Self::Avg),
175 "min" => Some(Self::Min),
176 "max" => Some(Self::Max),
177 "count" => Some(Self::Count),
178 "countDistinct" => Some(Self::CountDistinct),
179 "first" => Some(Self::First),
180 "last" => Some(Self::Last),
181 _ => None,
182 }
183 }
184}
185
186#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
188pub struct RecordsAggregate {
189 pub name: String,
190 pub op: RecordsAggregateOp,
191 pub field: Option<String>,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
196#[serde(rename_all = "lowercase")]
197pub enum RecordsRunningOp {
198 Sum,
199 Count,
200 Avg,
201 Min,
202 Max,
203}
204
205impl RecordsRunningOp {
206 pub fn parse(text: &str) -> Option<Self> {
207 match text {
208 "sum" => Some(Self::Sum),
209 "count" => Some(Self::Count),
210 "avg" => Some(Self::Avg),
211 "min" => Some(Self::Min),
212 "max" => Some(Self::Max),
213 _ => None,
214 }
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
220pub struct RecordsRunning {
221 pub field: String,
222 pub op: RecordsRunningOp,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
228pub struct RecordsBefore {
229 pub field: String,
230 pub date: String,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
235pub struct RecordsQuery {
236 pub from: Option<String>,
238 pub app: Option<String>,
240 pub mine: bool,
242 pub user: Option<String>,
244 #[serde(rename = "where")]
246 pub where_: Vec<RecordsFilter>,
247 #[serde(default)]
249 pub or: Vec<Vec<RecordsFilter>>,
250 pub sort: Vec<RecordsSortKey>,
252 pub limit: u32,
253 pub sum: Option<String>,
255 pub month: Option<RecordsMonthView>,
256 #[serde(default)]
258 pub group: Vec<RecordsGroupKey>,
259 #[serde(default)]
261 pub aggregate: Vec<RecordsAggregate>,
262 #[serde(default)]
264 pub running: Vec<RecordsRunning>,
265 #[serde(default)]
266 pub before: Option<RecordsBefore>,
267 #[serde(default)]
270 pub expand: Vec<String>,
271}
272
273impl Default for RecordsQuery {
274 fn default() -> Self {
275 RecordsQuery {
276 from: None,
277 app: None,
278 mine: false,
279 user: None,
280 where_: Vec::new(),
281 or: Vec::new(),
282 sort: vec![RecordsSortKey { field: RECORDS_SORT_DEFAULT.to_string(), order: RecordsOrder::Desc }],
283 limit: RECORDS_LIMIT_DEFAULT,
284 sum: None,
285 month: None,
286 group: Vec::new(),
287 aggregate: Vec::new(),
288 running: Vec::new(),
289 before: None,
290 expand: Vec::new(),
291 }
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
297pub struct RecordsQueryIssue {
298 pub path: String,
299 pub message: String,
300}
301
302pub(crate) fn js_string_number(s: &str) -> f64 {
305 let t = js_trim(s);
306 if t.is_empty() {
307 return 0.0;
308 }
309 for (prefix, radix) in [("0x", 16), ("0X", 16), ("0o", 8), ("0O", 8), ("0b", 2), ("0B", 2)] {
310 if let Some(digits) = t.strip_prefix(prefix) {
311 if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
312 return f64::NAN;
313 }
314 return digits.chars().fold(0.0, |n, c| n * f64::from(radix) + f64::from(c.to_digit(radix).unwrap_or(0)));
315 }
316 }
317 let (sign, body) = match t.as_bytes()[0] {
318 b'-' => (-1.0, &t[1..]),
319 b'+' => (1.0, &t[1..]),
320 _ => (1.0, t),
321 };
322 if body == "Infinity" {
323 return sign * f64::INFINITY;
324 }
325 let b = body.as_bytes();
326 let mut i = 0;
327 let digits = |i: &mut usize| {
328 let start = *i;
329 while b.get(*i).is_some_and(u8::is_ascii_digit) {
330 *i += 1;
331 }
332 *i - start
333 };
334 let int = digits(&mut i);
335 let frac = if b.get(i) == Some(&b'.') {
336 i += 1;
337 digits(&mut i)
338 } else {
339 0
340 };
341 if int == 0 && frac == 0 {
342 return f64::NAN;
343 }
344 if matches!(b.get(i), Some(b'e' | b'E')) {
345 i += 1;
346 if matches!(b.get(i), Some(b'+' | b'-')) {
347 i += 1;
348 }
349 if digits(&mut i) == 0 {
350 return f64::NAN;
351 }
352 }
353 if i != b.len() {
354 return f64::NAN;
355 }
356 body.parse::<f64>().map(|n| sign * n).unwrap_or(f64::NAN)
357}
358
359fn is_month_key(text: &str) -> bool {
361 let b = text.as_bytes();
362 b.len() == 7 && b[..4].iter().all(u8::is_ascii_digit) && b[4] == b'-' && matches!(&text[5..7], "01" | "02" | "03" | "04" | "05" | "06" | "07" | "08" | "09" | "10" | "11" | "12")
363}
364
365fn trimmed(v: Option<&str>) -> Option<String> {
366 v.map(js_trim).filter(|s| !s.is_empty()).map(str::to_string)
367}
368
369pub fn parse_records_query(params: &BTreeMap<String, String>, kind: Option<&str>) -> RecordsQuery {
374 let from = trimmed(kind);
375 if let Some(q) = params.get("q").filter(|q| !js_trim(q).is_empty()) {
376 let json = serde_json::from_str::<Value>(q).ok().filter(Value::is_object).unwrap_or_else(|| json!({}));
377 return parse_records_json(&json, from.as_deref()).0;
378 }
379 let limit_raw = params.get("limit").map_or(f64::NAN, |l| js_string_number(l));
380 let mut keys: Vec<&String> = params.keys().collect();
381 keys.sort_by(|a, b| utf16_cmp(a, b));
382 let mut where_ = Vec::new();
383 for key in keys {
384 if key.is_empty() || RECORDS_RESERVED_PARAMS.contains(&key.as_str()) {
385 continue;
386 }
387 let value = params[key].clone();
388 match key.rfind('.').and_then(|dot| RecordsFilterOp::suffix(&key[dot + 1..]).map(|op| (dot, op))) {
389 Some((dot, op)) if dot > 0 => {
390 let value = if op.is_list() { RecordsFilterValue::Many(value.split(',').map(str::to_string).collect()) } else { RecordsFilterValue::One(value) };
391 where_.push(RecordsFilter { field: key[..dot].to_string(), op, value });
392 }
393 _ => where_.push(RecordsFilter { field: key.clone(), op: RecordsFilterOp::Eq, value: RecordsFilterValue::One(value) }),
394 }
395 }
396 where_.sort_by(|a, b| utf16_cmp(&a.field, &b.field).then_with(|| utf16_cmp(a.op.as_str(), b.op.as_str())));
397 let order = match params.get("order").map(String::as_str) {
398 Some("asc") => RecordsOrder::Asc,
399 Some("desc") => RecordsOrder::Desc,
400 _ if RECORDS_ORDER_DEFAULT == "asc" => RecordsOrder::Asc,
401 _ => RecordsOrder::Desc,
402 };
403 let limit = if limit_raw.is_finite() && limit_raw.fract() == 0.0 && limit_raw > 0.0 { limit_raw.min(f64::from(RECORDS_LIMIT_MAX)) as u32 } else { RECORDS_LIMIT_DEFAULT };
404 let by = trimmed(params.get("by").map(String::as_str));
405 let before = trimmed(params.get("before").map(String::as_str));
406 RecordsQuery {
407 from,
408 app: trimmed(params.get("app").map(String::as_str)),
409 mine: matches!(params.get("mine").map(String::as_str), Some("1" | "true")),
410 user: trimmed(params.get("user").map(String::as_str)),
411 where_,
412 sort: vec![RecordsSortKey { field: trimmed(params.get("sort").map(String::as_str)).unwrap_or_else(|| RECORDS_SORT_DEFAULT.to_string()), order }],
413 limit,
414 sum: trimmed(params.get("sum").map(String::as_str)),
415 before: before.map(|date| RecordsBefore { field: by.clone().unwrap_or_else(|| RECORDS_SORT_DEFAULT.to_string()), date }),
416 month: by.map(|field| RecordsMonthView { field, key: params.get("month").filter(|m| is_month_key(m)).cloned() }),
417 expand: {
418 let mut out: Vec<String> = Vec::new();
419 for f in params.get("expand").map(String::as_str).unwrap_or("").split(',').map(js_trim).filter(|f| !f.is_empty()) {
420 if !out.iter().any(|o| o == f) {
421 out.push(f.to_string());
422 }
423 }
424 out
425 },
426 ..RecordsQuery::default()
427 }
428}
429
430pub fn parse_records_source(source: &ParsedSource) -> Option<RecordsQuery> {
432 (source.scheme == RECORDS_SCHEME).then(|| parse_records_query(&source.params, Some(&source.name)))
433}
434
435fn scalar_text(v: &Value) -> Option<String> {
437 match v {
438 Value::Null => Some(String::new()),
439 Value::String(s) => Some(s.clone()),
440 Value::Bool(b) => Some(b.to_string()),
441 Value::Number(n) => Some(number_to_string(n.as_f64().unwrap_or(0.0))),
442 _ => None,
443 }
444}
445
446fn issue(issues: &mut Vec<RecordsQueryIssue>, path: impl Into<String>, message: impl Into<String>) {
447 issues.push(RecordsQueryIssue { path: path.into(), message: message.into() });
448}
449
450fn parse_condition(entry: &Value, at: &str, issues: &mut Vec<RecordsQueryIssue>) -> Option<RecordsFilter> {
452 let Some(list) = entry.as_array().filter(|l| l.len() >= 3) else {
453 issue(issues, at, "a condition is [field, op, value]");
454 return None;
455 };
456 let Some(field) = list[0].as_str().map(js_trim).filter(|f| !f.is_empty()) else {
457 issue(issues, format!("{at}/0"), "a condition's field is a non-empty string");
458 return None;
459 };
460 let Some(op) = list[1].as_str().and_then(RecordsFilterOp::parse) else {
461 issue(issues, format!("{at}/1"), format!("unknown operator {}", crate::json::to_js_string(&list[1])));
462 return None;
463 };
464 let rest = &list[2..];
465 if op == RecordsFilterOp::In {
466 let values: Option<Vec<String>> = match rest {
467 [Value::Array(items)] => items.iter().map(scalar_text).collect(),
468 _ => None,
469 };
470 let Some(values) = values else {
471 issue(issues, format!("{at}/2"), "in takes one list of values");
472 return None;
473 };
474 return Some(RecordsFilter { field: field.to_string(), op, value: RecordsFilterValue::Many(values) });
475 }
476 let values: Option<Vec<String>> = rest.iter().map(scalar_text).collect();
477 let want = if op == RecordsFilterOp::Between { 2 } else { 1 };
478 let Some(mut values) = values.filter(|v| v.len() == want) else {
479 issue(issues, format!("{at}/2"), if op == RecordsFilterOp::Between { "between takes a low and a high value".to_string() } else { format!("{} takes one value", op.as_str()) });
480 return None;
481 };
482 let value = if op == RecordsFilterOp::Between { RecordsFilterValue::Many(values) } else { RecordsFilterValue::One(values.remove(0)) };
483 Some(RecordsFilter { field: field.to_string(), op, value })
484}
485
486const QUERY_KEYS: &[&str] = &["from", "app", "mine", "user", "where", "sort", "limit", "sum", "month", "group", "aggregate", "running", "before", "expand"];
487
488fn sorted_keys(map: &Map<String, Value>) -> Vec<&String> {
489 let mut keys: Vec<&String> = map.keys().collect();
490 keys.sort_by(|a, b| utf16_cmp(a, b));
491 keys
492}
493
494pub fn parse_records_json(json: &Value, kind: Option<&str>) -> (RecordsQuery, Vec<RecordsQueryIssue>) {
498 let mut issues = Vec::new();
499 let mut query = RecordsQuery::default();
500 let Some(obj) = json.as_object() else {
501 issue(&mut issues, "", "a query is an object");
502 query.from = trimmed(kind);
503 return (query, issues);
504 };
505 for key in sorted_keys(obj) {
506 if !QUERY_KEYS.contains(&key.as_str()) && !key.starts_with('$') {
507 issue(&mut issues, format!("/{key}"), format!("unknown key {key}"));
508 }
509 }
510 let str_of = |key: &str, issues: &mut Vec<RecordsQueryIssue>| -> Option<String> {
511 match obj.get(key) {
512 None | Some(Value::Null) => None,
513 Some(Value::String(s)) => trimmed(Some(s)),
514 Some(_) => {
515 issue(issues, format!("/{key}"), format!("{key} is a string"));
516 None
517 }
518 }
519 };
520 query.from = match trimmed(kind) {
521 Some(k) => Some(k),
522 None => str_of("from", &mut issues),
523 };
524 query.app = str_of("app", &mut issues);
525 query.user = str_of("user", &mut issues);
526 query.sum = str_of("sum", &mut issues);
527 match obj.get("mine") {
528 None => {}
529 Some(Value::Bool(b)) => query.mine = *b,
530 Some(_) => issue(&mut issues, "/mine", "mine is a boolean"),
531 }
532 match obj.get("where") {
533 None => {}
534 Some(Value::Array(list)) => {
535 for (i, entry) in list.iter().enumerate() {
536 let at = format!("/where/{i}");
537 if let Some(alt) = entry.as_object() {
538 let Some(alternatives) = alt.get("or").and_then(Value::as_array).filter(|_| alt.len() == 1) else {
539 issue(&mut issues, at, "an alternative is {or: [[condition…]…]}");
540 continue;
541 };
542 if !query.or.is_empty() {
543 issue(&mut issues, at, "one or per query");
544 continue;
545 }
546 for (j, alt) in alternatives.iter().enumerate() {
547 let Some(conds) = alt.as_array() else {
548 issue(&mut issues, format!("{at}/or/{j}"), "an alternative is a list of conditions");
549 continue;
550 };
551 let parsed: Vec<RecordsFilter> = conds.iter().enumerate().filter_map(|(k, c)| parse_condition(c, &format!("{at}/or/{j}/{k}"), &mut issues)).collect();
552 query.or.push(parsed);
553 }
554 continue;
555 }
556 if let Some(parsed) = parse_condition(entry, &at, &mut issues) {
557 query.where_.push(parsed);
558 }
559 }
560 }
561 Some(_) => issue(&mut issues, "/where", "where is a list of conditions"),
562 }
563 if let Some(sort) = obj.get("sort") {
564 let single = sort.is_string();
565 let list: Option<Vec<Value>> = if single { Some(vec![sort.clone()]) } else { sort.as_array().cloned() };
566 match list {
567 None => issue(&mut issues, "/sort", "sort is a field or a list of [field, order]"),
568 Some(list) => {
569 let mut keys = Vec::new();
570 for (i, entry) in list.iter().enumerate() {
571 let asc = Value::String("asc".into());
572 let (name, order): (Option<&Value>, Option<&Value>) = match entry {
573 Value::String(_) => (Some(entry), Some(&asc)),
574 Value::Array(pair) => (pair.first(), if pair.len() > 1 { pair.get(1) } else { Some(&asc) }),
575 _ => (None, None),
576 };
577 let name = name.and_then(Value::as_str).map(js_trim).filter(|n| !n.is_empty());
578 let order = match order.and_then(Value::as_str) {
579 Some("asc") => Some(RecordsOrder::Asc),
580 Some("desc") => Some(RecordsOrder::Desc),
581 _ => None,
582 };
583 match (name, order) {
584 (Some(name), Some(order)) => keys.push(RecordsSortKey { field: name.to_string(), order }),
585 _ => issue(&mut issues, if single { "/sort".to_string() } else { format!("/sort/{i}") }, "a sort key is field or [field, asc|desc]"),
586 }
587 }
588 if !keys.is_empty() {
589 query.sort = keys;
590 }
591 }
592 }
593 }
594 if let Some(limit) = obj.get("limit") {
595 let n = match limit {
596 Value::Number(n) => n.as_f64().unwrap_or(f64::NAN),
597 Value::String(s) if !js_trim(s).is_empty() => js_string_number(s),
598 _ => f64::NAN,
599 };
600 if n.is_finite() && n.fract() == 0.0 && n >= 1.0 {
601 query.limit = n.min(f64::from(RECORDS_LIMIT_MAX)) as u32;
602 } else {
603 issue(&mut issues, "/limit", "limit is a positive integer");
604 }
605 }
606 match obj.get("month") {
607 None | Some(Value::Null) => {}
608 Some(m) => {
609 let field = m.get("field").and_then(Value::as_str).map(js_trim).filter(|f| !f.is_empty());
610 let key = m.get("key");
611 match (m.as_object(), field) {
612 (Some(_), Some(field)) if matches!(key, None | Some(Value::Null) | Some(Value::String(_))) => {
613 query.month = Some(RecordsMonthView { field: field.to_string(), key: key.and_then(Value::as_str).filter(|k| is_month_key(k)).map(str::to_string) });
614 }
615 _ => issue(&mut issues, "/month", "month is {field, key?}"),
616 }
617 }
618 }
619 match obj.get("group") {
620 None => {}
621 Some(Value::Array(list)) => {
622 for (i, entry) in list.iter().enumerate() {
623 let at = format!("/group/{i}");
624 let key = match entry {
625 Value::String(s) if !js_trim(s).is_empty() => Some(RecordsGroupKey { field: js_trim(s).to_string(), bucket: None }),
626 Value::Object(o) if o.len() == 2 => {
627 let field = o.get("bucket").and_then(Value::as_str).map(js_trim).filter(|f| !f.is_empty());
628 let bucket = o.get("by").and_then(Value::as_str).and_then(DateBucket::parse);
629 field.zip(bucket).map(|(field, bucket)| RecordsGroupKey { field: field.to_string(), bucket: Some(bucket) })
630 }
631 _ => None,
632 };
633 let Some(key) = key else {
634 issue(&mut issues, at, format!("a group key is a field or {{bucket: field, by: {}}}", DATE_BUCKETS.join("|")));
635 continue;
636 };
637 if query.group.iter().any(|g| g.field == key.field || (key.field == "author" && g.field == "authorId") || (key.field == "authorId" && g.field == "author")) {
638 issue(&mut issues, at, format!("duplicate group {}", key.field));
639 continue;
640 }
641 query.group.push(key);
642 }
643 }
644 Some(_) => issue(&mut issues, "/group", "group is a list of keys"),
645 }
646 match obj.get("aggregate") {
647 None => {}
648 Some(Value::Object(aggs)) => {
649 for name in sorted_keys(aggs) {
650 let def = &aggs[name];
651 let at = format!("/aggregate/{name}");
652 if js_trim(name).is_empty() || RECORDS_AGGREGATE_RESERVED.contains(&name.as_str()) || query.group.iter().any(|g| g.field == *name || (g.field == "author" && name == "authorId")) {
653 issue(&mut issues, at, format!("the name {name} is taken"));
654 continue;
655 }
656 let list = def.as_array();
657 let op = list.and_then(|l| l.first()).and_then(Value::as_str).and_then(RecordsAggregateOp::parse);
658 let len = list.map_or(0, Vec::len);
659 let field = list.filter(|l| l.len() > 1).and_then(|l| l[1].as_str()).map(js_trim).filter(|f| !f.is_empty());
660 match op {
661 Some(op) if len <= 2 && !(len > 1 && field.is_none()) && (op == RecordsAggregateOp::Count || field.is_some()) => {
662 query.aggregate.push(RecordsAggregate { name: name.clone(), op, field: field.map(str::to_string) });
663 }
664 _ => issue(&mut issues, at, format!("an aggregate is [{}, field] (count: the field is optional)", RECORDS_AGGREGATE_OPS.join("|"))),
665 }
666 }
667 }
668 Some(_) => issue(&mut issues, "/aggregate", "aggregate is {name: [op, field?]}"),
669 }
670 match obj.get("running") {
671 None => {}
672 Some(Value::Object(running)) => {
673 for name in sorted_keys(running) {
674 match running[name].as_str().and_then(RecordsRunningOp::parse) {
675 Some(op) if !js_trim(name).is_empty() => query.running.push(RecordsRunning { field: name.clone(), op }),
676 _ => issue(&mut issues, format!("/running/{name}"), format!("a running value is {{field: {}}}", RECORDS_RUNNING_OPS.join("|"))),
677 }
678 }
679 }
680 Some(_) => issue(&mut issues, "/running", "running is {field: op}"),
681 }
682 match obj.get("before") {
683 None | Some(Value::Null) => {}
684 Some(Value::String(date)) => {
685 let implied = query.group.iter().find(|g| g.bucket.is_some()).map(|g| g.field.clone()).or_else(|| query.month.as_ref().map(|m| m.field.clone())).unwrap_or_else(|| RECORDS_SORT_DEFAULT.to_string());
686 query.before = trimmed(Some(date)).map(|date| RecordsBefore { field: implied, date });
687 }
688 Some(b) => {
689 let field = b.as_object().and_then(|o| o.get("field")).and_then(Value::as_str).map(js_trim).filter(|f| !f.is_empty());
690 match (field, b.get("date").and_then(Value::as_str)) {
691 (Some(field), Some(date)) => query.before = trimmed(Some(date)).map(|date| RecordsBefore { field: field.to_string(), date }),
692 _ => issue(&mut issues, "/before", "before is a date or {field, date}"),
693 }
694 }
695 }
696 match obj.get("expand") {
698 None => {}
699 Some(Value::Array(list)) => {
700 for (i, entry) in list.iter().enumerate() {
701 match entry.as_str().map(js_trim).filter(|f| !f.is_empty()) {
702 None => issue(&mut issues, format!("/expand/{i}"), "an expanded field is a non-empty string"),
703 Some(f) if query.expand.iter().any(|e| e == f) => issue(&mut issues, format!("/expand/{i}"), format!("duplicate expand {f}")),
704 Some(f) => query.expand.push(f.to_string()),
705 }
706 }
707 }
708 Some(_) => issue(&mut issues, "/expand", "expand is a list of ref fields"),
709 }
710 (query, issues)
711}
712
713pub fn fill_query_templates(value: &Value, params: &BTreeMap<String, String>) -> Value {
715 match value {
716 Value::String(s) => {
717 let mut out = String::new();
718 let mut rest = s.as_str();
719 while let Some(start) = rest.find("${") {
720 let Some(len) = rest[start + 2..].find('}') else { break };
721 out.push_str(&rest[..start]);
722 let name = js_trim(&rest[start + 2..start + 2 + len]);
723 out.push_str(params.get(name).map_or("", String::as_str));
724 rest = &rest[start + 2 + len + 1..];
725 }
726 out.push_str(rest);
727 Value::String(out)
728 }
729 Value::Array(items) => Value::Array(items.iter().map(|v| fill_query_templates(v, params)).collect()),
730 Value::Object(map) => Value::Object(map.iter().map(|(k, v)| (k.clone(), fill_query_templates(v, params))).collect()),
731 other => other.clone(),
732 }
733}
734
735pub fn expand_query_source(source: &str, queries: Option<&Value>) -> String {
740 let Some(parsed) = parse_source(source) else { return source.to_string() };
741 let Some(id) = parsed.params.get("query") else { return source.to_string() };
742 let Some(query) = queries.and_then(Value::as_object).and_then(|q| q.get(id)) else { return source.to_string() };
743 let mut params = parsed.params.clone();
744 params.remove("query");
745 let filled = fill_query_templates(query, ¶ms);
746 let base = js_trim(source);
747 let head = base.split_once('?').map_or(base, |(h, _)| h);
748 source_with_params(head, Some(&json!({ "q": canonical_json(&filled) })))
749}
750
751#[derive(Debug, Clone, Copy, PartialEq)]
753pub struct RecordRowRef<'a> {
754 pub id: &'a str,
755 pub kind: &'a str,
756 pub data: &'a Value,
757 pub created_by: Option<&'a str>,
758 pub author_id: Option<&'a str>,
760 pub created_at: &'a str,
761 pub updated_at: &'a str,
762}
763
764impl<'a> RecordRowRef<'a> {
765 pub fn author(&self) -> Option<&'a str> {
767 self.author_id.or(self.created_by)
768 }
769}
770
771#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
773#[serde(rename_all = "camelCase")]
774pub struct RecordRow {
775 pub id: String,
776 pub kind: String,
777 #[serde(default)]
778 pub data: Value,
779 #[serde(default)]
780 pub created_by: Option<String>,
781 #[serde(default)]
782 pub author_id: Option<String>,
783 pub created_at: String,
784 pub updated_at: String,
785}
786
787impl RecordRow {
788 pub fn as_ref(&self) -> RecordRowRef<'_> {
789 RecordRowRef {
790 id: &self.id,
791 kind: &self.kind,
792 data: &self.data,
793 created_by: self.created_by.as_deref(),
794 author_id: self.author_id.as_deref(),
795 created_at: &self.created_at,
796 updated_at: &self.updated_at,
797 }
798 }
799}
800
801#[derive(Debug, Clone, Copy)]
803pub struct RecordsContext<'a> {
804 pub me: Option<&'a str>,
806 pub now: &'a str,
808 pub today: Option<&'a str>,
811 pub names: &'a HashMap<String, String>,
813 pub computed: Option<&'a BTreeMap<String, String>>,
816 pub refs: Option<&'a Value>,
819 pub issues: Option<&'a Map<String, Value>>,
821 pub transitions: Option<&'a super::record_transitions::RecordTransitions>,
824 pub role: Option<&'a str>,
826}
827
828pub fn kind_refs(records: Option<&Value>, kind: Option<&str>) -> Option<Value> {
831 let def = records?.as_object()?.get(kind?)?.as_object()?;
832 let mut out = Map::new();
833 for (name, target) in def.get("refs")?.as_object()? {
834 match target {
835 Value::String(t) if t == "issue" || t == "member" => {
836 out.insert(name.clone(), target.clone());
837 }
838 Value::Object(o) => {
839 if let Some(k) = o.get("kind").and_then(Value::as_str).filter(|k| !k.is_empty()) {
840 out.insert(name.clone(), json!({ "kind": k }));
841 }
842 }
843 _ => {}
844 }
845 }
846 (!out.is_empty()).then_some(Value::Object(out))
847}
848
849fn value_row(row: &RecordRowRef, names: &HashMap<String, String>) -> Map<String, Value> {
851 let mut obj: Map<String, Value> = row.data.as_object().cloned().unwrap_or_default();
852 obj.insert("id".into(), json!(row.id));
853 obj.insert("createdAt".into(), json!(row.created_at));
854 obj.insert("updatedAt".into(), json!(row.updated_at));
855 obj.insert("createdBy".into(), json!(row.created_by));
856 obj.insert("authorId".into(), json!(row.author()));
857 let author = row.author().and_then(|a| names.get(a)).cloned().unwrap_or_default();
858 obj.insert("author".into(), json!(author));
859 obj
860}
861
862fn expanded_ref(target: &Value, value: Option<&Value>, by_id: &HashMap<&str, &RecordRowRef>, ctx: &RecordsContext) -> Value {
867 let id = match value {
868 Some(Value::String(s)) => s.clone(),
869 Some(Value::Number(n)) => number_to_string(n.as_f64().unwrap_or(0.0)),
870 _ => String::new(),
871 };
872 if id.is_empty() {
873 return Value::Null;
874 }
875 match target {
876 Value::String(t) if t == "issue" => ctx.issues.and_then(|m| m.get(&id)).cloned().unwrap_or(Value::Null),
877 Value::String(t) if t == "member" => ctx.names.get(&id).map_or(Value::Null, |name| json!({ "id": id, "name": name })),
878 _ => {
879 let kind = target.get("kind").and_then(Value::as_str).unwrap_or("");
880 by_id.get(id.as_str()).filter(|r| r.kind == kind).map_or(Value::Null, |r| Value::Object(value_row(r, ctx.names)))
881 }
882 }
883}
884
885pub const RECORDS_COMPUTED_MAX: usize = 16;
887pub const RECORDS_COMPUTED_ROW_KEYS: [&str; 5] = ["id", "createdAt", "updatedAt", "createdBy", "authorId"];
889
890pub fn kind_computed(records: Option<&Value>, kind: Option<&str>) -> Option<BTreeMap<String, String>> {
893 let def = records?.as_object()?.get(kind?)?.as_object()?;
894 let out: BTreeMap<String, String> = def.get("computed")?.as_object()?.iter().filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))).collect();
895 (!out.is_empty()).then_some(out)
896}
897
898pub fn computed_data(rows: &[RecordRowRef], kind: &str, computed: &BTreeMap<String, String>, now: &str) -> Vec<Option<Value>> {
905 let fields: Vec<(&String, Option<crate::cel::Node>, Vec<String>)> = computed
906 .iter()
907 .map(|(name, source)| {
908 let ast = crate::cel::parse_expression(source).ok();
909 let reads = ast.as_ref().map(crate::cel::expression_names).unwrap_or_default();
910 (name, ast, reads)
911 })
912 .collect();
913 let env = crate::cel::ExpressionEnv { now: Some(crate::cel::parse_timestamp(now).unwrap_or(0.0)), offset: None, max_steps: None };
914 rows.iter()
915 .map(|row| {
916 if row.kind != kind {
917 return None;
918 }
919 let mut vars: Map<String, Value> = row.data.as_object().cloned().unwrap_or_default();
920 vars.insert("id".into(), Value::String(row.id.to_string()));
921 vars.insert("createdAt".into(), Value::String(row.created_at.to_string()));
922 vars.insert("updatedAt".into(), Value::String(row.updated_at.to_string()));
923 vars.insert("createdBy".into(), row.created_by.map_or(Value::Null, |s| Value::String(s.to_string())));
924 vars.insert("authorId".into(), row.author().map_or(Value::Null, |s| Value::String(s.to_string())));
925 let mut data: Map<String, Value> = row.data.as_object().cloned().unwrap_or_default();
926 for (name, ast, reads) in &fields {
927 for r in reads {
928 if !vars.contains_key(r) {
929 vars.insert(r.clone(), Value::Null);
930 }
931 }
932 let value = ast.as_ref().and_then(|ast| crate::cel::evaluate_node(ast, &vars, &env).ok()).unwrap_or(Value::Null);
933 data.insert((*name).clone(), value);
934 }
935 Some(Value::Object(data))
936 })
937 .collect()
938}
939
940#[derive(Clone, Copy)]
942enum Field<'a> {
943 Text(&'a str),
944 Json(&'a Value),
945 Missing,
946}
947
948fn num_text(s: &str) -> Option<f64> {
949 if js_trim(s).is_empty() {
950 return None;
951 }
952 Some(js_string_number(s)).filter(|f| f.is_finite())
953}
954
955fn num(v: &Field) -> Option<f64> {
956 match v {
957 Field::Json(Value::Number(n)) => n.as_f64().filter(|f| f.is_finite()),
958 Field::Json(Value::String(s)) => num_text(s),
959 Field::Text(s) => num_text(s),
960 Field::Json(_) | Field::Missing => None,
961 }
962}
963
964fn num_value(v: Option<&Value>) -> Option<f64> {
965 v.and_then(|v| num(&Field::Json(v)))
966}
967
968fn text<'a>(v: &Field<'a>) -> Cow<'a, str> {
971 match *v {
972 Field::Missing | Field::Json(Value::Null) => Cow::Borrowed(""),
973 Field::Text(s) => Cow::Borrowed(s),
974 Field::Json(Value::String(s)) => Cow::Borrowed(s.as_str()),
975 Field::Json(Value::Bool(b)) => Cow::Borrowed(if *b { "true" } else { "false" }),
976 Field::Json(Value::Number(n)) => Cow::Owned(number_to_string(n.as_f64().unwrap_or(0.0))),
977 Field::Json(other) => Cow::Owned(canonical_json(other)),
978 }
979}
980
981fn json_of(v: &Field) -> Value {
983 match v {
984 Field::Missing => Value::Null,
985 Field::Text(s) => Value::String((*s).to_string()),
986 Field::Json(v) => (*v).clone(),
987 }
988}
989
990enum Key<'a> {
994 Num(f64, Field<'a>),
996 Text(Cow<'a, str>),
997}
998
999impl<'a> Key<'a> {
1000 fn of(v: Field<'a>) -> Self {
1001 match num(&v) {
1002 Some(n) => Key::Num(n, v),
1003 None => Key::Text(text(&v)),
1004 }
1005 }
1006
1007 fn text_ref(&self) -> &str {
1009 match self {
1010 Key::Num(..) => "",
1011 Key::Text(t) => t,
1012 }
1013 }
1014
1015 fn text(&self) -> Cow<'_, str> {
1016 match self {
1017 Key::Num(_, f) => text(f),
1018 Key::Text(t) => Cow::Borrowed(t),
1019 }
1020 }
1021}
1022
1023fn cmp_keys(a: &Key, b: &Key) -> Ordering {
1025 if let (Key::Num(na, _), Key::Num(nb, _)) = (a, b) {
1026 return na.partial_cmp(nb).unwrap_or(Ordering::Equal);
1027 }
1028 utf16_cmp(&a.text(), &b.text())
1029}
1030
1031fn compare(a: &Field, b: &Field) -> Ordering {
1032 cmp_keys(&Key::of(*a), &Key::of(*b))
1033}
1034
1035struct Cond<'q> {
1037 filter: &'q RecordsFilter,
1038 bounds: Vec<Key<'q>>,
1039}
1040
1041impl<'q> Cond<'q> {
1042 fn new(filter: &'q RecordsFilter) -> Self {
1043 let bounds = match (&filter.value, filter.op) {
1044 (_, RecordsFilterOp::Eq | RecordsFilterOp::In) => Vec::new(),
1045 (RecordsFilterValue::One(v), _) => vec![Key::of(Field::Text(v))],
1046 (RecordsFilterValue::Many(vs), _) => vs.iter().map(|v| Key::of(Field::Text(v))).collect(),
1047 };
1048 Cond { filter, bounds }
1049 }
1050
1051 fn holds(&self, row: &RecordRowRef) -> bool {
1052 let value = field(row, &self.filter.field);
1053 match self.filter.op {
1054 RecordsFilterOp::Eq => match &self.filter.value {
1055 RecordsFilterValue::One(v) => text(&value) == v.as_str(),
1056 RecordsFilterValue::Many(_) => false,
1057 },
1058 RecordsFilterOp::In => {
1059 let t = text(&value);
1060 match &self.filter.value {
1061 RecordsFilterValue::One(v) => t == v.as_str(),
1062 RecordsFilterValue::Many(vs) => vs.iter().any(|v| t == v.as_str()),
1063 }
1064 }
1065 RecordsFilterOp::Between => self.bounds.len() == 2 && {
1066 let k = Key::of(value);
1067 cmp_keys(&k, &self.bounds[0]).is_ge() && cmp_keys(&k, &self.bounds[1]).is_le()
1068 },
1069 op => {
1070 let [bound] = self.bounds.as_slice() else { return false };
1071 let c = cmp_keys(&Key::of(value), bound);
1072 match op {
1073 RecordsFilterOp::Gt => c.is_gt(),
1074 RecordsFilterOp::Gte => c.is_ge(),
1075 RecordsFilterOp::Lt => c.is_lt(),
1076 _ => c.is_le(),
1077 }
1078 }
1079 }
1080 }
1081}
1082
1083fn data_field<'a>(row: &RecordRowRef<'a>, name: &str) -> Field<'a> {
1084 match row.data.as_object().and_then(|o| o.get(name)) {
1085 Some(v) => Field::Json(v),
1086 None => Field::Missing,
1087 }
1088}
1089
1090static NULL: Value = Value::Null;
1091
1092fn field<'a>(row: &RecordRowRef<'a>, name: &str) -> Field<'a> {
1094 match name {
1095 "createdAt" => Field::Text(row.created_at),
1096 "updatedAt" => Field::Text(row.updated_at),
1097 "id" => Field::Text(row.id),
1098 "createdBy" => row.created_by.map_or(Field::Json(&NULL), Field::Text),
1099 "authorId" => row.author().map_or(Field::Json(&NULL), Field::Text),
1100 _ => data_field(row, name),
1101 }
1102}
1103
1104fn parse_month(key: &str) -> (i64, i64) {
1105 let (y, m) = key.split_once('-').unwrap_or(("1970", "1"));
1106 (y.parse().unwrap_or(1970), m.parse::<i64>().unwrap_or(1).clamp(1, 12))
1107}
1108
1109pub fn shift_month(key: &str, by: i64) -> String {
1111 let (y, m) = parse_month(key);
1112 let index = y * 12 + (m - 1) + by;
1113 format!("{}-{:02}", index.div_euclid(12), index.rem_euclid(12) + 1)
1114}
1115
1116pub fn month_label(key: &str) -> String {
1118 let (y, m) = parse_month(key);
1119 format!("{} {y}", RECORDS_MONTHS[(m - 1) as usize])
1120}
1121
1122#[derive(Debug, Clone, PartialEq)]
1124pub struct RecordsDay {
1125 pub date: String,
1126 pub day: i64,
1127 pub weekday: &'static str,
1128 pub count: u64,
1129 pub sum: f64,
1130}
1131
1132pub fn month_days(key: &str) -> Vec<RecordsDay> {
1134 let (y, m) = parse_month(key);
1135 (1..=days_in_month(y, m)).map(|day| RecordsDay { date: format!("{key}-{day:02}"), day, weekday: RECORDS_WEEKDAYS[weekday_of(y, m, day)], count: 0, sum: 0.0 }).collect()
1136}
1137
1138fn round(n: f64) -> f64 {
1140 (n * 1_000_000.0 + 0.5).floor() / 1_000_000.0
1141}
1142
1143fn prefix(s: &str, n: usize) -> String {
1145 s.chars().take(n).collect()
1146}
1147
1148fn aggregate_of(agg: &RecordsAggregate, rows: &[&RecordRowRef]) -> Value {
1150 let values: Vec<Field> = match &agg.field {
1151 Some(f) => rows.iter().map(|r| field(r, f)).collect(),
1152 None => Vec::new(),
1153 };
1154 match agg.op {
1155 RecordsAggregateOp::Count => json!(if agg.field.is_none() { rows.len() } else { values.iter().filter(|v| !text(v).is_empty()).count() }),
1156 RecordsAggregateOp::Sum => number(round(values.iter().fold(0.0, |acc, v| acc + num(v).unwrap_or(0.0)))),
1157 RecordsAggregateOp::Avg => {
1158 let nums: Vec<f64> = values.iter().filter_map(num).collect();
1159 if nums.is_empty() {
1160 Value::Null
1161 } else {
1162 number(round(nums.iter().fold(0.0, |a, b| a + b) / nums.len() as f64))
1163 }
1164 }
1165 RecordsAggregateOp::Min | RecordsAggregateOp::Max => {
1166 let mut best: Option<&Field> = None;
1167 for v in &values {
1168 if text(v).is_empty() {
1169 continue;
1170 }
1171 let better = match best {
1172 None => true,
1173 Some(b) if agg.op == RecordsAggregateOp::Min => compare(v, b).is_lt(),
1174 Some(b) => compare(v, b).is_gt(),
1175 };
1176 if better {
1177 best = Some(v);
1178 }
1179 }
1180 best.map_or(Value::Null, |b| num(b).map_or_else(|| Value::String(text(b).into_owned()), number))
1181 }
1182 RecordsAggregateOp::CountDistinct => {
1183 let mut seen: Vec<Cow<str>> = values.iter().map(text).filter(|t| !t.is_empty()).collect();
1184 seen.sort();
1185 seen.dedup();
1186 json!(seen.len())
1187 }
1188 RecordsAggregateOp::First => values.first().map_or(Value::Null, json_of),
1189 RecordsAggregateOp::Last => values.last().map_or(Value::Null, json_of),
1190 }
1191}
1192
1193fn sum_of(row: &RecordRowRef, f: &str) -> f64 {
1194 num_value(row.data.as_object().and_then(|o| o.get(f))).unwrap_or(0.0)
1195}
1196
1197fn summary(query: &RecordsQuery, rows: &[&RecordRowRef], out: &mut Map<String, Value>) {
1199 out.insert("count".into(), json!(rows.len()));
1200 if let Some(sum) = &query.sum {
1201 out.insert("sum".into(), number(round(rows.iter().fold(0.0, |acc, row| acc + sum_of(row, sum)))));
1202 }
1203 for agg in &query.aggregate {
1204 out.insert(agg.name.clone(), aggregate_of(agg, rows));
1205 }
1206}
1207
1208#[derive(Default, Clone, Copy)]
1209struct RunningAcc {
1210 sum: f64,
1211 count: u64,
1212 nums: u64,
1213 min: Option<f64>,
1214 max: Option<f64>,
1215}
1216
1217struct Running<'q> {
1219 list: &'q [RecordsRunning],
1220 acc: Vec<RunningAcc>,
1221}
1222
1223impl<'q> Running<'q> {
1224 fn new(list: &'q [RecordsRunning]) -> Self {
1225 Running { list, acc: vec![RunningAcc::default(); list.len()] }
1226 }
1227
1228 fn add(&mut self, row: &RecordRowRef) {
1229 for (r, a) in self.list.iter().zip(self.acc.iter_mut()) {
1230 a.count += 1;
1231 let Some(n) = num(&field(row, &r.field)) else { continue };
1232 a.sum += n;
1233 a.nums += 1;
1234 a.min = Some(a.min.map_or(n, |m| if n < m { n } else { m }));
1235 a.max = Some(a.max.map_or(n, |m| if n > m { n } else { m }));
1236 }
1237 }
1238
1239 fn read(&self) -> Value {
1240 let mut out = Map::new();
1241 for (r, a) in self.list.iter().zip(&self.acc) {
1242 let v = match r.op {
1243 RecordsRunningOp::Sum => number(round(a.sum)),
1244 RecordsRunningOp::Count => json!(a.count),
1245 RecordsRunningOp::Avg if a.nums > 0 => number(round(a.sum / a.nums as f64)),
1246 RecordsRunningOp::Avg => Value::Null,
1247 RecordsRunningOp::Min => a.min.map_or(Value::Null, number),
1248 RecordsRunningOp::Max => a.max.map_or(Value::Null, number),
1249 };
1250 out.insert(r.field.clone(), v);
1251 }
1252 Value::Object(out)
1253 }
1254}
1255
1256fn group_part<'a>(key: &RecordsGroupKey, row: &RecordRowRef<'a>) -> Cow<'a, str> {
1259 if key.field == "author" {
1260 return Cow::Borrowed(row.author().unwrap_or(""));
1261 }
1262 let v = field(row, &key.field);
1263 match key.bucket {
1264 Some(bucket) => Cow::Owned(date_bucket(&text(&v), bucket).unwrap_or_default()),
1265 None => text(&v),
1266 }
1267}
1268
1269fn group_columns(key: &RecordsGroupKey, row: &RecordRowRef, names: &HashMap<String, String>, cols: &mut Map<String, Value>) {
1272 if key.field == "author" {
1273 let author = row.author();
1274 cols.insert("author".into(), json!(author.and_then(|a| names.get(a)).cloned().unwrap_or_default()));
1275 cols.insert("authorId".into(), json!(author));
1276 return;
1277 }
1278 let v = field(row, &key.field);
1279 if let Some(bucket) = key.bucket {
1280 cols.insert(key.field.clone(), json!(date_bucket(&text(&v), bucket)));
1281 return;
1282 }
1283 cols.insert(key.field.clone(), json_of(&v));
1284}
1285
1286fn prefix_str(s: &str, n: usize) -> &str {
1288 s.char_indices().nth(n).map_or(s, |(i, _)| &s[..i])
1289}
1290
1291enum Col<'a> {
1295 Num(Vec<f64>),
1296 Rank(Vec<u32>),
1297 Mixed(Vec<Key<'a>>),
1298}
1299
1300impl<'a> Col<'a> {
1301 fn of(rows: &[&RecordRowRef<'a>], name: &str) -> Self {
1302 let keys: Vec<Key<'a>> = rows.iter().map(|row| Key::of(field(row, name))).collect();
1303 if keys.iter().all(|k| matches!(k, Key::Num(..))) {
1304 return Col::Num(keys.iter().map(|k| if let Key::Num(n, _) = k { *n } else { 0.0 }).collect());
1305 }
1306 if !keys.iter().all(|k| matches!(k, Key::Text(_))) {
1307 return Col::Mixed(keys);
1308 }
1309 let mut ranks: FxHashMap<&str, u32> = FxHashMap::default();
1310 for k in &keys {
1311 ranks.entry(k.text_ref()).or_insert(0);
1312 }
1313 let mut distinct: Vec<&str> = ranks.keys().copied().collect();
1314 distinct.sort_unstable_by(|a, b| utf16_cmp(a, b));
1315 for (i, t) in distinct.iter().enumerate() {
1316 ranks.insert(t, i as u32);
1317 }
1318 Col::Rank(keys.iter().map(|k| ranks[k.text_ref()]).collect())
1319 }
1320
1321 fn cmp(&self, a: usize, b: usize) -> Ordering {
1322 match self {
1323 Col::Num(v) => v[a].partial_cmp(&v[b]).unwrap_or(Ordering::Equal),
1324 Col::Rank(v) => v[a].cmp(&v[b]),
1325 Col::Mixed(v) => cmp_keys(&v[a], &v[b]),
1326 }
1327 }
1328}
1329
1330fn sorted<'r, 'a>(rows: Vec<&'r RecordRowRef<'a>>, sort: &[RecordsSortKey]) -> Vec<&'r RecordRowRef<'a>> {
1336 let cols: Vec<(Col<'a>, bool)> = sort.iter().map(|s| (Col::of(&rows, &s.field), s.order == RecordsOrder::Asc)).collect();
1337 let ascii: Vec<bool> = rows.iter().map(|row| row.id.is_ascii()).collect();
1338 let order = |a: usize, b: usize| {
1339 for (col, asc) in &cols {
1340 let c = col.cmp(a, b);
1341 if c.is_ne() {
1342 return if *asc { c } else { c.reverse() };
1343 }
1344 }
1345 let (ia, ib) = (rows[a].id, rows[b].id);
1346 if ascii[a] || ascii[b] {
1347 ia.as_bytes().cmp(ib.as_bytes())
1348 } else {
1349 utf16_cmp(ia, ib)
1350 }
1351 };
1352 let mut idx: Vec<usize> = (0..rows.len()).collect();
1353 if cols.iter().any(|(c, _)| matches!(c, Col::Mixed(_))) {
1354 idx.sort_by(|&a, &b| order(a, b));
1355 } else {
1356 idx.sort_unstable_by(|&a, &b| order(a, b).then(a.cmp(&b)));
1357 }
1358 idx.into_iter().map(|i| rows[i]).collect()
1359}
1360
1361pub fn records_value(rows: &[RecordRowRef], query: &RecordsQuery, ctx: RecordsContext) -> Option<Value> {
1366 records_value_in(rows, rows, query, ctx)
1367}
1368
1369fn records_value_in(all: &[RecordRowRef], rows: &[RecordRowRef], query: &RecordsQuery, ctx: RecordsContext) -> Option<Value> {
1372 let kind = query.from.as_deref()?;
1373 if let Some(computed) = ctx.computed.filter(|c| !c.is_empty()) {
1375 let data = computed_data(rows, kind, computed, ctx.now);
1376 let rows: Vec<RecordRowRef> = rows.iter().zip(&data).map(|(row, d)| RecordRowRef { data: d.as_ref().unwrap_or(row.data), ..*row }).collect();
1377 return records_value_in(all, &rows, query, RecordsContext { computed: None, ..ctx });
1378 }
1379 let expand: Vec<(&str, &Value)> = query.expand.iter().filter_map(|name| ctx.refs.and_then(|r| r.get(name)).map(|t| (name.as_str(), t))).collect();
1381 let by_id: HashMap<&str, &RecordRowRef> = if expand.is_empty() { HashMap::new() } else { all.iter().map(|r| (r.id, r)).collect() };
1382 let view = query.month.as_ref();
1383 let today: &str = ctx.today.filter(|t| is_day_text(t)).unwrap_or_else(|| prefix_str(ctx.now, 10));
1384 let month_key: Option<String> = view.map(|v| v.key.clone().unwrap_or_else(|| prefix(today, 7)));
1385 let where_: Vec<Cond> = query.where_.iter().map(Cond::new).collect();
1386 let or: Vec<Vec<Cond>> = query.or.iter().map(|alt| alt.iter().map(Cond::new).collect()).collect();
1387 let before_date = query.before.as_ref().map(|b| Key::of(Field::Text(&b.date)));
1388 let mut before: Vec<&RecordRowRef> = Vec::new();
1389 let mut matched: Vec<&RecordRowRef> = Vec::new();
1390 for row in rows {
1391 if row.kind != kind {
1392 continue;
1393 }
1394 let author = row.author();
1395 if query.mine && (ctx.me.is_none() || author != ctx.me) {
1396 continue;
1397 }
1398 if let Some(user) = &query.user {
1399 if author != Some(user.as_str()) {
1400 continue;
1401 }
1402 }
1403 if !where_.iter().all(|c| c.holds(row)) {
1404 continue;
1405 }
1406 if !or.is_empty() && !or.iter().any(|alt| alt.iter().all(|c| c.holds(row))) {
1407 continue;
1408 }
1409 if let (Some(b), Some(date)) = (&query.before, &before_date) {
1410 let t = text(&field(row, &b.field));
1411 if !t.is_empty() && cmp_keys(&Key::of(Field::Text(&t)), date).is_lt() {
1412 before.push(row);
1413 continue;
1414 }
1415 }
1416 if let (Some(view), Some(key)) = (view, &month_key) {
1417 if !text(&data_field(row, &view.field)).starts_with(key.as_str()) {
1418 continue;
1419 }
1420 }
1421 matched.push(row);
1422 }
1423 let matched = sorted(matched, &query.sort);
1424 let before = sorted(before, &query.sort);
1425 let seeded = || {
1426 let mut r = Running::new(&query.running);
1427 for row in &before {
1428 r.add(row);
1429 }
1430 r
1431 };
1432 let mut row_running = (!query.running.is_empty() && query.group.is_empty()).then(seeded);
1433 let out_rows: Vec<Value> = matched
1434 .iter()
1435 .take(query.limit as usize)
1436 .map(|row| {
1437 let mut obj = value_row(row, ctx.names);
1438 for (name, target) in &expand {
1439 obj.insert((*name).to_string(), expanded_ref(target, row.data.get(*name), &by_id, &ctx));
1440 }
1441 if let Some(t) = ctx.transitions {
1442 let viewer = super::record_transitions::TransitionViewer { id: ctx.me, role: ctx.role };
1443 let ownership = json!({"authorId": row.author_id, "createdBy": row.created_by});
1444 let next = super::record_transitions::transitions_from(Some(t), Some(row.data), Some(&viewer), Some(&ownership));
1445 obj.insert(super::record_transitions::RECORD_TRANSITIONS_ROW_KEY.into(), json!(next));
1446 }
1447 if let Some(r) = row_running.as_mut() {
1448 r.add(row);
1449 obj.insert("running".into(), r.read());
1450 }
1451 Value::Object(obj)
1452 })
1453 .collect();
1454 let mut out = Map::new();
1455 out.insert("rows".into(), Value::Array(out_rows));
1456 out.insert("count".into(), json!(matched.len()));
1457 if let Some(sum) = &query.sum {
1458 let total = matched.iter().fold(0.0, |acc, row| acc + sum_of(row, sum));
1459 out.insert("sum".into(), number(round(total)));
1460 }
1461 if let (Some(view), Some(key)) = (view, &month_key) {
1462 let mut days = month_days(key);
1463 let mut per_day: Vec<Vec<&RecordRowRef>> = vec![Vec::new(); days.len()];
1464 for row in &matched {
1465 let t = text(&data_field(row, &view.field));
1466 let date = prefix_str(&t, 10);
1467 let Some(i) = days.iter().position(|d| d.date == date) else {
1468 continue;
1469 };
1470 let day = &mut days[i];
1471 day.count += 1;
1472 if let Some(sum) = &query.sum {
1473 day.sum = round(day.sum + sum_of(row, sum));
1474 }
1475 per_day[i].push(row);
1476 }
1477 let mut runnings: Vec<Option<Value>> = vec![None; days.len()];
1478 if !query.running.is_empty() {
1479 let mut r = seeded();
1480 for (i, rows) in per_day.iter().enumerate() {
1481 for row in rows {
1482 r.add(row);
1483 }
1484 runnings[i] = Some(r.read());
1485 }
1486 }
1487 let labels: Vec<String> = days.iter().map(|d| d.day.to_string()).collect();
1488 let counts: Vec<u64> = days.iter().map(|d| d.count).collect();
1489 let sums: Vec<Value> = days.iter().map(|d| number(d.sum)).collect();
1490 let days_json: Vec<Value> = days
1491 .iter()
1492 .zip(runnings)
1493 .map(|(d, running)| {
1494 let mut day = json!({"date": d.date, "day": d.day, "weekday": d.weekday, "count": d.count, "sum": number(d.sum)});
1495 if let (Some(running), Some(obj)) = (running, day.as_object_mut()) {
1496 obj.insert("running".into(), running);
1497 }
1498 day
1499 })
1500 .collect();
1501 out.insert(
1502 "month".into(),
1503 json!({
1504 "key": key,
1505 "label": month_label(key),
1506 "prev": shift_month(key, -1),
1507 "next": shift_month(key, 1),
1508 "today": today,
1509 "days": days_json,
1510 "labels": labels,
1511 "counts": counts,
1512 "sums": sums,
1513 }),
1514 );
1515 }
1516 if !query.group.is_empty() {
1517 struct Group<'r, 'a> {
1518 parts: Vec<Cow<'a, str>>,
1519 rows: Vec<&'r RecordRowRef<'a>>,
1520 out: Map<String, Value>,
1521 }
1522 let mut groups: Vec<Group> = Vec::new();
1523 let mut index: FxHashMap<Vec<Cow<str>>, usize> = FxHashMap::default();
1524 let mut parts: Vec<Cow<str>> = Vec::with_capacity(query.group.len());
1525 for row in &matched {
1526 parts.clear();
1527 parts.extend(query.group.iter().map(|key| group_part(key, row)));
1528 match index.get(parts.as_slice()) {
1529 Some(&i) => groups[i].rows.push(row),
1530 None => {
1531 let mut cols = Map::new();
1532 for key in &query.group {
1533 group_columns(key, row, ctx.names, &mut cols);
1534 }
1535 index.insert(parts.clone(), groups.len());
1536 groups.push(Group { parts: parts.clone(), rows: vec![row], out: cols });
1537 }
1538 }
1539 }
1540 for g in &mut groups {
1541 summary(query, &g.rows, &mut g.out);
1542 }
1543 let mut columns: Vec<&str> = Vec::new();
1544 for g in &query.group {
1545 if g.field == "author" {
1546 columns.extend(["author", "authorId"]);
1547 } else {
1548 columns.push(&g.field);
1549 }
1550 }
1551 columns.push("count");
1552 if query.sum.is_some() {
1553 columns.push("sum");
1554 }
1555 columns.extend(query.aggregate.iter().map(|a| a.name.as_str()));
1556 let sort_keys: Vec<&RecordsSortKey> = query.sort.iter().filter(|k| columns.contains(&k.field.as_str())).collect();
1557 let col = |g: &Group, name: &str| -> Value { g.out.get(name).cloned().unwrap_or(Value::Null) };
1558 groups.sort_by(|a, b| {
1559 for key in &sort_keys {
1560 let (va, vb) = (col(a, &key.field), col(b, &key.field));
1561 let c = compare(&Field::Json(&va), &Field::Json(&vb));
1562 if c.is_ne() {
1563 return if key.order == RecordsOrder::Asc { c } else { c.reverse() };
1564 }
1565 }
1566 for key in &query.group {
1567 let (va, vb) = (col(a, &key.field), col(b, &key.field));
1568 let c = compare(&Field::Json(&va), &Field::Json(&vb));
1569 if c.is_ne() {
1570 return c;
1571 }
1572 }
1573 for (pa, pb) in a.parts.iter().zip(&b.parts) {
1574 let c = utf16_cmp(pa, pb);
1575 if c.is_ne() {
1576 return c;
1577 }
1578 }
1579 Ordering::Equal
1580 });
1581 if !query.running.is_empty() {
1582 let mut r = seeded();
1583 for g in &mut groups {
1584 for row in &g.rows {
1585 r.add(row);
1586 }
1587 g.out.insert("running".into(), r.read());
1588 }
1589 }
1590 out.insert("groups".into(), Value::Array(groups.into_iter().map(|g| Value::Object(g.out)).collect()));
1591 }
1592 if !query.aggregate.is_empty() {
1593 let mut totals = Map::new();
1594 summary(query, &matched, &mut totals);
1595 out.insert("totals".into(), Value::Object(totals));
1596 }
1597 if query.before.is_some() {
1598 let mut b = Map::new();
1599 summary(query, &before, &mut b);
1600 out.insert("before".into(), Value::Object(b));
1601 }
1602 Some(Value::Object(out))
1603}
1604
1605pub fn records_value_owned(rows: &[RecordRow], query: &RecordsQuery, ctx: RecordsContext) -> Option<Value> {
1607 let refs: Vec<RecordRowRef> = rows.iter().map(RecordRow::as_ref).collect();
1608 records_value(&refs, query, ctx)
1609}
1610
1611#[cfg(test)]
1612mod tests {
1613 use super::*;
1614
1615 #[test]
1616 fn number_parses_like_js() {
1617 assert_eq!(js_string_number(" 12 "), 12.0);
1618 assert_eq!(js_string_number("0x10"), 16.0);
1619 assert_eq!(js_string_number("1e2"), 100.0);
1620 assert_eq!(js_string_number(""), 0.0);
1621 assert!(js_string_number("12px").is_nan());
1622 assert!(js_string_number("inf").is_nan());
1623 assert!(js_string_number("-0x1").is_nan());
1624 assert_eq!(js_string_number("-Infinity"), f64::NEG_INFINITY);
1625 }
1626
1627 #[test]
1628 fn months_shift_across_years() {
1629 assert_eq!(shift_month("2026-01", -1), "2025-12");
1630 assert_eq!(shift_month("2026-12", 1), "2027-01");
1631 assert_eq!(shift_month("2026-03", -15), "2024-12");
1632 assert_eq!(month_label("2026-10"), "October 2026");
1633 assert_eq!(month_days("2028-02").len(), 29);
1634 }
1635}
1636
1637fn is_day_text(t: &str) -> bool {
1639 let b = t.as_bytes();
1640 b.len() == 10 && b.iter().enumerate().all(|(i, c)| if i == 4 || i == 7 { *c == b'-' } else { c.is_ascii_digit() })
1641}