@omgbase/core 0.21.0 → 0.23.0

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 (36) hide show
  1. package/dist/src/core/ids.d.ts +1 -1
  2. package/dist/src/core/read/document.d.ts +4 -1
  3. package/dist/src/core/read/document.js +18 -6
  4. package/dist/src/core/store/archive.d.ts +196 -0
  5. package/dist/src/core/store/archive.js +606 -0
  6. package/dist/src/core/store/gc.d.ts +25 -4
  7. package/dist/src/core/store/gc.js +43 -28
  8. package/dist/src/core/store/rebuild.d.ts +2 -0
  9. package/dist/src/core/store/rebuild.js +8 -2
  10. package/dist/src/core/store/retention.d.ts +138 -0
  11. package/dist/src/core/store/retention.js +409 -0
  12. package/dist/src/core/store/schema.d.ts +3 -2
  13. package/dist/src/core/store/schema.js +30 -4
  14. package/dist/src/core/store/size.d.ts +21 -2
  15. package/dist/src/core/store/size.js +24 -3
  16. package/dist/src/core/store/store.d.ts +1 -0
  17. package/dist/src/core/store/store.js +53 -1
  18. package/dist/src/graph/history.d.ts +2 -0
  19. package/dist/src/graph/history.js +8 -4
  20. package/dist/src/index.d.ts +3 -1
  21. package/dist/src/index.js +3 -0
  22. package/dist/src/mcp/errors.d.ts +1 -1
  23. package/dist/src/mcp/errors.js +4 -0
  24. package/dist/src/mcp/reference.d.ts +1 -1
  25. package/dist/src/mcp/reference.js +24 -6
  26. package/dist/src/mcp/server.js +2 -2
  27. package/dist/src/oqx-js/context.js +21 -5
  28. package/dist/src/oqx-js/dates.d.ts +105 -0
  29. package/dist/src/oqx-js/dates.js +427 -0
  30. package/dist/src/oqx-js/names.js +14 -3
  31. package/dist/src/oqx-js/planner.js +17 -2
  32. package/dist/src/oqx-js/run.d.ts +1 -0
  33. package/dist/src/oqx-js/run.js +66 -1
  34. package/dist/src/spec-versions.d.ts +4 -4
  35. package/dist/src/spec-versions.js +3 -3
  36. package/package.json +2 -2
@@ -0,0 +1,105 @@
1
+ import type { CallResult } from "@omgbase/oqx";
2
+ /** A parsed date: the instant, the offset it was WRITTEN in (minutes east of
3
+ * UTC; `0` for `Z` and for a date-only value), whether it is date-only, and
4
+ * whether it carried a fraction of a second as written (kept on render). */
5
+ export interface DateValue {
6
+ ms: number;
7
+ offset: number;
8
+ dateOnly: boolean;
9
+ fraction: boolean;
10
+ }
11
+ /** Parse an ISO-8601 date / datetime string; `null` for anything else (a
12
+ * non-string, a malformed string, an out-of-range field such as month 13 or
13
+ * hour 24). */
14
+ export declare function parseDate(x: unknown): DateValue | null;
15
+ interface Fields {
16
+ year: number;
17
+ month: number;
18
+ day: number;
19
+ hour: number;
20
+ minute: number;
21
+ second: number;
22
+ millis: number;
23
+ weekday: number;
24
+ }
25
+ /** The fields of the value in the offset it was written in (`weekday` 1 = Monday … 7 = Sunday). */
26
+ export declare function fieldsOf(d: DateValue): Fields;
27
+ /** The canonical string: `YYYY-MM-DD` for a date-only value; otherwise
28
+ * `YYYY-MM-DDThh:mm:ss` + `.fff` when the value carries a fraction (as written,
29
+ * or because arithmetic produced one) + `Z` for UTC or the written `±hh:mm`. */
30
+ export declare function renderDate(d: DateValue): string | null;
31
+ /** `date(x)` on a string that is not a named point: the canonical form, or null. */
32
+ export declare function canonicalDate(x: unknown): string | null;
33
+ /** The named points `date()` resolves against the clock. */
34
+ export declare const NAMED_POINTS: ReadonlySet<string>;
35
+ /** Resolve a named point at the instant `nowMs` (UTC): `now` is the instant
36
+ * with milliseconds (the form the store stamps commits in); the others are
37
+ * date-only — `sow`/`eow` the Monday/Sunday of the current week, `som`/`eom`
38
+ * the first/last day of the month, `soy`/`eoy` of the year. Null for a name
39
+ * that is not a point. */
40
+ export declare function namedPoint(name: string, nowMs: number): DateValue | null;
41
+ /** `date(x)`: a named point against the clock, else the canonical ISO form, else null. */
42
+ export declare function dateOf(x: unknown, nowMs?: number): string | null;
43
+ /** A parsed duration: the calendar counts `years` and `months` kept apart from
44
+ * the fixed-length `seconds`, so `plus` can be calendar-aware while `dur()`
45
+ * applies the 365-day / 30-day conventions. */
46
+ export interface Duration {
47
+ years: number;
48
+ months: number;
49
+ seconds: number;
50
+ }
51
+ /** Days per month / year under the fixed conventions `dur()` applies. */
52
+ export declare const MONTH_SECONDS: number;
53
+ export declare const YEAR_SECONDS: number;
54
+ /** Parse a duration string: Dataview's `<number> <unit>` pairs, each pair
55
+ * separated from the next by spaces, a comma, or nothing (`"1h30m"`, `"2 days,
56
+ * 3 hours"`; a trailing separator is malformed), units case-insensitive:
57
+ * y|yr|yrs|year|years, mo|mos|month|months, w|wk|wks|week|weeks, d|day|days,
58
+ * h|hr|hrs|hour|hours, m|min|mins|minute|minutes, s|sec|secs|second|seconds —
59
+ * or an ISO-8601 duration (`P1DT2H`, `PT90M`, `P2W`). Null when it is neither. */
60
+ export declare function parseDuration(x: unknown): Duration | null;
61
+ /** A duration's length in seconds under the conventions: a year is 365 days, a month 30. */
62
+ export declare function durationSeconds(d: Duration): number;
63
+ /** `dur(x)`: a number is that many seconds; a string is parsed; else null. */
64
+ export declare function durOf(x: unknown): number | null;
65
+ /** Shift a date by a whole number of calendar `months` (the month field moves
66
+ * and the day is clamped to the new month's length) and then by `seconds`. A
67
+ * date-only value stays date-only when the seconds are a whole number of days;
68
+ * otherwise it becomes a datetime in its offset (UTC for a date-only receiver). */
69
+ export declare function shiftDate(d: DateValue, months: number, seconds: number): DateValue;
70
+ /** A duration as `plus` applies it: the WHOLE years and months are calendar
71
+ * months; a fractional year or month count is not calendar — its fraction is
72
+ * 365-day / 30-day seconds — and joins the fixed-length seconds. */
73
+ export declare function calendarParts(dur: Duration): {
74
+ months: number;
75
+ seconds: number;
76
+ };
77
+ /** `d.plus(x)` (`sign` 1) / `d.minus(x)` (`sign` -1). */
78
+ export declare function plusMinus(recv: unknown, arg: unknown, sign: 1 | -1): string | null;
79
+ /** `a.until(b)`: seconds from `a` to `b` (negative when `b` is earlier), by instant. */
80
+ export declare function until(a: unknown, b: unknown): number | null;
81
+ /** `d.format(pattern)`: `yyyy` year, `MM` month, `dd` day, `HH` hour, `mm`
82
+ * minute, `ss` second (two digits each), `MMM` the English month abbreviation
83
+ * (`Jan`), `EEE` the English weekday abbreviation (`Mon`); every other
84
+ * character is literal. Fields are read in the written offset; a date-only
85
+ * value has `00` time fields. */
86
+ export declare function formatDate(recv: unknown, pattern: unknown): string | null;
87
+ /** `n.dur_text()`: `"2d 3h"`, `"1h 30m"`, `"45s"` — days, hours, minutes,
88
+ * seconds, zero parts omitted, `"0s"` for zero, a leading `-` for a negative
89
+ * duration, a fractional second kept (`"1.5s"`). Null for a non-number. */
90
+ export declare function durText(n: unknown): string | null;
91
+ /** `n.dur_iso()`: the ISO-8601 duration `P[nD][T[nH][nM][nS]]` — `PT1H30M`,
92
+ * `P2DT3H`, `PT45S`, `PT0S` for zero, a leading `-` for a negative duration.
93
+ * Null for a non-number. */
94
+ export declare function durIso(n: unknown): string | null;
95
+ /** The free functions and their arity. */
96
+ export declare const DATE_FUNCTIONS: Readonly<Record<string, number>>;
97
+ /** The methods and their arity: on a date string, and (`dur_text`/`dur_iso`) on a number. */
98
+ export declare const DATE_METHODS: Readonly<Record<string, number>>;
99
+ /** `callFunction` arm: `date(x)` / `dur(x)`; `{ handled: false }` for any other name. */
100
+ export declare function dateFunction(name: string, args: unknown[], nowMs?: number): CallResult;
101
+ /** `callMethod` arm: the date methods on a string receiver and the duration
102
+ * renderers on a number; a receiver of any other kind yields absent (the
103
+ * methods are total). `{ handled: false }` for any other name. */
104
+ export declare function dateMethod(name: string, recv: unknown, args: unknown[]): CallResult;
105
+ export {};
@@ -0,0 +1,427 @@
1
+ // Dates and durations for the query binding (spec/surface §1.3 "Dates and
2
+ // durations", 4.8): the Dataview-parity function set, served as omgbase HOST
3
+ // functions through `@omgbase/oqx`'s `callFunction` / `callMethod` seam.
4
+ //
5
+ // Two value conventions and no new value kinds:
6
+ // • a DATE is an ISO-8601 string — `YYYY-MM-DD` (date-only) or
7
+ // `YYYY-MM-DDThh:mm:ss[.fff](Z|±hh:mm)` — exactly what the store writes for
8
+ // `updated_at`/`ts` and what OQX already orders chronologically by code
9
+ // point (spec/oqx SEMANTICS §6);
10
+ // • a DURATION is a number of seconds.
11
+ // Arithmetic between them is METHODS (`d.plus(s)`, `a.until(b)`), never the
12
+ // `+`/`-` operators: `+` with a string operand concatenates (SEMANTICS §7), so
13
+ // `"2026-01-05" + 86400` is `"2026-01-0586400"` and must stay so.
14
+ //
15
+ // Everything is total: a value that is not a date (or a duration) yields
16
+ // absent, so a `where` over mixed data filters quietly. Only a wrong ARITY is
17
+ // an error (`filter_invalid`, the surface's eval error). The one impure input
18
+ // — the named points `today`, `now`, `sow`… — reads the HOST clock, UTC, through
19
+ // `Date.now()`, which the spec clock seam (`core/clock.ts`, `OMGBASE_SPEC_CLOCK`)
20
+ // pins; the language itself has no clock (the fixtures run under the corpus
21
+ // clock, so `date("today")` is `2026-09-27` there).
22
+ //
23
+ // The pure half (parse / render / shift / until / fields / format / the duration
24
+ // grammar) is exported for the unit tests and the Rust twin's reading; the two
25
+ // `CallResult` arms at the bottom are what context.ts and run.ts's constant fold
26
+ // call.
27
+ import { FilterInvalid } from "../search/cel/parser.js";
28
+ const DAY_MS = 86_400_000;
29
+ // `YYYY-MM-DD`, optionally `T`/`t`/space + `hh:mm[:ss[.fff…]]` + optional offset
30
+ // (`Z`, `±hh:mm`, `±hhmm`); a time without an offset is UTC.
31
+ const ISO_RE = /^(\d{4})-(\d{2})-(\d{2})(?:[Tt ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?(Z|z|[+-]\d{2}:?\d{2})?)?$/;
32
+ function daysInMonth(year, month) {
33
+ // month 1..12; day 0 of the next month is the last day of this one.
34
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
35
+ }
36
+ /** Parse an ISO-8601 date / datetime string; `null` for anything else (a
37
+ * non-string, a malformed string, an out-of-range field such as month 13 or
38
+ * hour 24). */
39
+ export function parseDate(x) {
40
+ if (typeof x !== "string")
41
+ return null;
42
+ const m = ISO_RE.exec(x.trim());
43
+ if (!m)
44
+ return null;
45
+ const year = Number(m[1]), month = Number(m[2]), day = Number(m[3]);
46
+ if (month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month))
47
+ return null;
48
+ if (m[4] === undefined) {
49
+ return { ms: Date.UTC(year, month - 1, day), offset: 0, dateOnly: true, fraction: false };
50
+ }
51
+ const hour = Number(m[4]), minute = Number(m[5]), second = m[6] === undefined ? 0 : Number(m[6]);
52
+ if (hour > 23 || minute > 59 || second > 59)
53
+ return null;
54
+ const fraction = m[7] !== undefined;
55
+ const millis = fraction ? Number(m[7].padEnd(3, "0").slice(0, 3)) : 0;
56
+ let offset = 0;
57
+ const z = m[8];
58
+ if (z !== undefined && z !== "Z" && z !== "z") {
59
+ const sign = z[0] === "-" ? -1 : 1;
60
+ const digits = z.slice(1).replace(":", "");
61
+ const oh = Number(digits.slice(0, 2)), om = Number(digits.slice(2, 4));
62
+ if (oh > 23 || om > 59)
63
+ return null;
64
+ offset = sign * (oh * 60 + om);
65
+ }
66
+ const local = Date.UTC(year, month - 1, day, hour, minute, second, millis);
67
+ return { ms: local - offset * 60_000, offset, dateOnly: false, fraction };
68
+ }
69
+ /** The fields of the value in the offset it was written in (`weekday` 1 = Monday … 7 = Sunday). */
70
+ export function fieldsOf(d) {
71
+ const t = new Date(d.ms + d.offset * 60_000);
72
+ return {
73
+ year: t.getUTCFullYear(), month: t.getUTCMonth() + 1, day: t.getUTCDate(),
74
+ hour: t.getUTCHours(), minute: t.getUTCMinutes(), second: t.getUTCSeconds(), millis: t.getUTCMilliseconds(),
75
+ weekday: ((t.getUTCDay() + 6) % 7) + 1,
76
+ };
77
+ }
78
+ const pad = (n, w) => String(n).padStart(w, "0");
79
+ /** The canonical string: `YYYY-MM-DD` for a date-only value; otherwise
80
+ * `YYYY-MM-DDThh:mm:ss` + `.fff` when the value carries a fraction (as written,
81
+ * or because arithmetic produced one) + `Z` for UTC or the written `±hh:mm`. */
82
+ export function renderDate(d) {
83
+ if (!Number.isFinite(d.ms))
84
+ return null;
85
+ const f = fieldsOf(d);
86
+ const ymd = `${pad(f.year, 4)}-${pad(f.month, 2)}-${pad(f.day, 2)}`;
87
+ if (d.dateOnly)
88
+ return ymd;
89
+ let s = `${ymd}T${pad(f.hour, 2)}:${pad(f.minute, 2)}:${pad(f.second, 2)}`;
90
+ if (d.fraction || f.millis !== 0)
91
+ s += `.${pad(f.millis, 3)}`;
92
+ if (d.offset === 0)
93
+ return `${s}Z`;
94
+ const a = Math.abs(d.offset);
95
+ return `${s}${d.offset < 0 ? "-" : "+"}${pad(Math.floor(a / 60), 2)}:${pad(a % 60, 2)}`;
96
+ }
97
+ /** `date(x)` on a string that is not a named point: the canonical form, or null. */
98
+ export function canonicalDate(x) {
99
+ const d = parseDate(x);
100
+ return d === null ? null : renderDate(d);
101
+ }
102
+ // ---- named points (the host clock, UTC) --------------------------------------------
103
+ /** The named points `date()` resolves against the clock. */
104
+ export const NAMED_POINTS = new Set(["now", "today", "yesterday", "tomorrow", "sow", "eow", "som", "eom", "soy", "eoy"]);
105
+ const dateOnly = (ms) => ({ ms, offset: 0, dateOnly: true, fraction: false });
106
+ /** Resolve a named point at the instant `nowMs` (UTC): `now` is the instant
107
+ * with milliseconds (the form the store stamps commits in); the others are
108
+ * date-only — `sow`/`eow` the Monday/Sunday of the current week, `som`/`eom`
109
+ * the first/last day of the month, `soy`/`eoy` of the year. Null for a name
110
+ * that is not a point. */
111
+ export function namedPoint(name, nowMs) {
112
+ const t = new Date(nowMs);
113
+ const y = t.getUTCFullYear(), mo = t.getUTCMonth(), d = t.getUTCDate();
114
+ const today = Date.UTC(y, mo, d);
115
+ switch (name) {
116
+ case "now": return { ms: nowMs, offset: 0, dateOnly: false, fraction: true };
117
+ case "today": return dateOnly(today);
118
+ case "yesterday": return dateOnly(today - DAY_MS);
119
+ case "tomorrow": return dateOnly(today + DAY_MS);
120
+ case "sow": return dateOnly(today - ((t.getUTCDay() + 6) % 7) * DAY_MS);
121
+ case "eow": return dateOnly(today + (6 - (t.getUTCDay() + 6) % 7) * DAY_MS);
122
+ case "som": return dateOnly(Date.UTC(y, mo, 1));
123
+ case "eom": return dateOnly(Date.UTC(y, mo + 1, 0));
124
+ case "soy": return dateOnly(Date.UTC(y, 0, 1));
125
+ case "eoy": return dateOnly(Date.UTC(y, 11, 31));
126
+ default: return null;
127
+ }
128
+ }
129
+ /** `date(x)`: a named point against the clock, else the canonical ISO form, else null. */
130
+ export function dateOf(x, nowMs = Date.now()) {
131
+ if (typeof x !== "string")
132
+ return null;
133
+ const key = x.trim().toLowerCase();
134
+ if (NAMED_POINTS.has(key))
135
+ return renderDate(namedPoint(key, nowMs));
136
+ return canonicalDate(x);
137
+ }
138
+ const UNIT_SECONDS = {
139
+ w: 604_800, wk: 604_800, wks: 604_800, week: 604_800, weeks: 604_800,
140
+ d: 86_400, day: 86_400, days: 86_400,
141
+ h: 3600, hr: 3600, hrs: 3600, hour: 3600, hours: 3600,
142
+ m: 60, min: 60, mins: 60, minute: 60, minutes: 60,
143
+ s: 1, sec: 1, secs: 1, second: 1, seconds: 1,
144
+ };
145
+ const UNIT_YEARS = new Set(["y", "yr", "yrs", "year", "years"]);
146
+ const UNIT_MONTHS = new Set(["mo", "mos", "month", "months"]);
147
+ /** Days per month / year under the fixed conventions `dur()` applies. */
148
+ export const MONTH_SECONDS = 30 * 86_400;
149
+ export const YEAR_SECONDS = 365 * 86_400;
150
+ const NUM = String.raw `(\d+(?:\.\d+)?)`;
151
+ const ISO_DUR_RE = new RegExp(`^([+-])?P(?:${NUM}Y)?(?:${NUM}M)?(?:${NUM}W)?(?:${NUM}D)?(?:T(?:${NUM}H)?(?:${NUM}M)?(?:${NUM}S)?)?$`, "i");
152
+ const PAIR_RE = /^(-?\d+(?:\.\d+)?)\s*([a-zA-Z]+)/;
153
+ /** Parse a duration string: Dataview's `<number> <unit>` pairs, each pair
154
+ * separated from the next by spaces, a comma, or nothing (`"1h30m"`, `"2 days,
155
+ * 3 hours"`; a trailing separator is malformed), units case-insensitive:
156
+ * y|yr|yrs|year|years, mo|mos|month|months, w|wk|wks|week|weeks, d|day|days,
157
+ * h|hr|hrs|hour|hours, m|min|mins|minute|minutes, s|sec|secs|second|seconds —
158
+ * or an ISO-8601 duration (`P1DT2H`, `PT90M`, `P2W`). Null when it is neither. */
159
+ export function parseDuration(x) {
160
+ if (typeof x !== "string")
161
+ return null;
162
+ const s = x.trim();
163
+ if (s === "")
164
+ return null;
165
+ const iso = ISO_DUR_RE.exec(s);
166
+ if (iso) {
167
+ const [, sign, y, mo, w, d, h, mi, sec] = iso;
168
+ if ([y, mo, w, d, h, mi, sec].every((v) => v === undefined))
169
+ return null;
170
+ const n = (v) => (v === undefined ? 0 : Number(v));
171
+ const k = sign === "-" ? -1 : 1;
172
+ return {
173
+ years: k * n(y),
174
+ months: k * n(mo),
175
+ seconds: k * (n(w) * 604_800 + n(d) * 86_400 + n(h) * 3600 + n(mi) * 60 + n(sec)),
176
+ };
177
+ }
178
+ let rest = s;
179
+ let years = 0, months = 0, seconds = 0;
180
+ for (;;) {
181
+ const m = PAIR_RE.exec(rest);
182
+ if (!m)
183
+ return null;
184
+ const n = Number(m[1]);
185
+ const unit = m[2].toLowerCase();
186
+ if (Object.hasOwn(UNIT_SECONDS, unit))
187
+ seconds += n * UNIT_SECONDS[unit];
188
+ else if (UNIT_MONTHS.has(unit))
189
+ months += n;
190
+ else if (UNIT_YEARS.has(unit))
191
+ years += n;
192
+ else
193
+ return null;
194
+ rest = rest.slice(m[0].length);
195
+ if (rest === "")
196
+ break;
197
+ // the separator: spaces, one comma among them, or none at all (`1h30m`)
198
+ rest = rest.replace(/^\s*,?\s*/, "");
199
+ if (rest === "")
200
+ return null;
201
+ }
202
+ return { years, months, seconds };
203
+ }
204
+ /** A duration's length in seconds under the conventions: a year is 365 days, a month 30. */
205
+ export function durationSeconds(d) {
206
+ return d.years * YEAR_SECONDS + d.months * MONTH_SECONDS + d.seconds;
207
+ }
208
+ /** `dur(x)`: a number is that many seconds; a string is parsed; else null. */
209
+ export function durOf(x) {
210
+ if (typeof x === "number")
211
+ return Number.isFinite(x) ? x : null;
212
+ const d = parseDuration(x);
213
+ return d === null ? null : durationSeconds(d);
214
+ }
215
+ // ---- arithmetic -------------------------------------------------------------------
216
+ /** Shift a date by a whole number of calendar `months` (the month field moves
217
+ * and the day is clamped to the new month's length) and then by `seconds`. A
218
+ * date-only value stays date-only when the seconds are a whole number of days;
219
+ * otherwise it becomes a datetime in its offset (UTC for a date-only receiver). */
220
+ export function shiftDate(d, months, seconds) {
221
+ let ms = d.ms;
222
+ if (months !== 0) {
223
+ const f = fieldsOf(d);
224
+ const total = f.year * 12 + (f.month - 1) + months;
225
+ const year = Math.floor(total / 12), month = ((total % 12) + 12) % 12 + 1;
226
+ const day = Math.min(f.day, daysInMonth(year, month));
227
+ ms = Date.UTC(year, month - 1, day, f.hour, f.minute, f.second, f.millis) - d.offset * 60_000;
228
+ }
229
+ const deltaMs = Math.round(seconds * 1000);
230
+ ms += deltaMs;
231
+ const dateOnly = d.dateOnly && deltaMs % DAY_MS === 0;
232
+ return { ms, offset: d.offset, dateOnly, fraction: d.fraction };
233
+ }
234
+ /** The argument of `plus`/`minus` as a duration: a number of seconds (never
235
+ * calendar) or a duration string (months/years calendar-aware). Null otherwise. */
236
+ function durationArg(x) {
237
+ if (typeof x === "number")
238
+ return Number.isFinite(x) ? { years: 0, months: 0, seconds: x } : null;
239
+ return parseDuration(x);
240
+ }
241
+ /** A duration as `plus` applies it: the WHOLE years and months are calendar
242
+ * months; a fractional year or month count is not calendar — its fraction is
243
+ * 365-day / 30-day seconds — and joins the fixed-length seconds. */
244
+ export function calendarParts(dur) {
245
+ const wy = Math.trunc(dur.years), wm = Math.trunc(dur.months);
246
+ return {
247
+ months: wy * 12 + wm,
248
+ seconds: dur.seconds + (dur.years - wy) * YEAR_SECONDS + (dur.months - wm) * MONTH_SECONDS,
249
+ };
250
+ }
251
+ /** `d.plus(x)` (`sign` 1) / `d.minus(x)` (`sign` -1). */
252
+ export function plusMinus(recv, arg, sign) {
253
+ const d = parseDate(recv);
254
+ const dur = durationArg(arg);
255
+ if (d === null || dur === null)
256
+ return null;
257
+ const { months, seconds } = calendarParts(dur);
258
+ return renderDate(shiftDate(d, sign * months, sign * seconds));
259
+ }
260
+ /** `a.until(b)`: seconds from `a` to `b` (negative when `b` is earlier), by instant. */
261
+ export function until(a, b) {
262
+ const da = parseDate(a), db = parseDate(b);
263
+ if (da === null || db === null)
264
+ return null;
265
+ return (db.ms - da.ms) / 1000;
266
+ }
267
+ // ---- format -------------------------------------------------------------------------
268
+ const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
269
+ const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
270
+ /** The `format` tokens, longest first (a scan takes the longest token at each position). */
271
+ const FORMAT_TOKENS = ["yyyy", "MMM", "EEE", "MM", "dd", "HH", "mm", "ss"];
272
+ /** `d.format(pattern)`: `yyyy` year, `MM` month, `dd` day, `HH` hour, `mm`
273
+ * minute, `ss` second (two digits each), `MMM` the English month abbreviation
274
+ * (`Jan`), `EEE` the English weekday abbreviation (`Mon`); every other
275
+ * character is literal. Fields are read in the written offset; a date-only
276
+ * value has `00` time fields. */
277
+ export function formatDate(recv, pattern) {
278
+ const d = parseDate(recv);
279
+ if (d === null || typeof pattern !== "string")
280
+ return null;
281
+ const f = fieldsOf(d);
282
+ let out = "";
283
+ let i = 0;
284
+ while (i < pattern.length) {
285
+ const tok = FORMAT_TOKENS.find((t) => pattern.startsWith(t, i));
286
+ if (tok === undefined) {
287
+ out += pattern[i];
288
+ i++;
289
+ continue;
290
+ }
291
+ switch (tok) {
292
+ case "yyyy":
293
+ out += pad(f.year, 4);
294
+ break;
295
+ case "MMM":
296
+ out += MONTHS[f.month - 1];
297
+ break;
298
+ case "EEE":
299
+ out += WEEKDAYS[f.weekday - 1];
300
+ break;
301
+ case "MM":
302
+ out += pad(f.month, 2);
303
+ break;
304
+ case "dd":
305
+ out += pad(f.day, 2);
306
+ break;
307
+ case "HH":
308
+ out += pad(f.hour, 2);
309
+ break;
310
+ case "mm":
311
+ out += pad(f.minute, 2);
312
+ break;
313
+ case "ss":
314
+ out += pad(f.second, 2);
315
+ break;
316
+ }
317
+ i += tok.length;
318
+ }
319
+ return out;
320
+ }
321
+ // ---- duration rendering -----------------------------------------------------------------
322
+ function splitSeconds(n) {
323
+ const neg = n < 0;
324
+ const a = Math.round(Math.abs(n) * 1000) / 1000;
325
+ const d = Math.floor(a / 86_400);
326
+ const h = Math.floor((a % 86_400) / 3600);
327
+ const m = Math.floor((a % 3600) / 60);
328
+ const s = Math.round((a % 60) * 1000) / 1000;
329
+ return { neg, d, h, m, s };
330
+ }
331
+ /** `n.dur_text()`: `"2d 3h"`, `"1h 30m"`, `"45s"` — days, hours, minutes,
332
+ * seconds, zero parts omitted, `"0s"` for zero, a leading `-` for a negative
333
+ * duration, a fractional second kept (`"1.5s"`). Null for a non-number. */
334
+ export function durText(n) {
335
+ if (typeof n !== "number" || !Number.isFinite(n))
336
+ return null;
337
+ const { neg, d, h, m, s } = splitSeconds(n);
338
+ const parts = [];
339
+ if (d)
340
+ parts.push(`${d}d`);
341
+ if (h)
342
+ parts.push(`${h}h`);
343
+ if (m)
344
+ parts.push(`${m}m`);
345
+ if (s)
346
+ parts.push(`${s}s`);
347
+ if (parts.length === 0)
348
+ return "0s";
349
+ return (neg ? "-" : "") + parts.join(" ");
350
+ }
351
+ /** `n.dur_iso()`: the ISO-8601 duration `P[nD][T[nH][nM][nS]]` — `PT1H30M`,
352
+ * `P2DT3H`, `PT45S`, `PT0S` for zero, a leading `-` for a negative duration.
353
+ * Null for a non-number. */
354
+ export function durIso(n) {
355
+ if (typeof n !== "number" || !Number.isFinite(n))
356
+ return null;
357
+ const { neg, d, h, m, s } = splitSeconds(n);
358
+ let out = "P";
359
+ if (d)
360
+ out += `${d}D`;
361
+ if (h || m || s) {
362
+ out += "T";
363
+ if (h)
364
+ out += `${h}H`;
365
+ if (m)
366
+ out += `${m}M`;
367
+ if (s)
368
+ out += `${s}S`;
369
+ }
370
+ if (out === "P")
371
+ out = "PT0S";
372
+ return (neg ? "-" : "") + out;
373
+ }
374
+ // ---- the OQX arms ---------------------------------------------------------------------
375
+ /** The free functions and their arity. */
376
+ export const DATE_FUNCTIONS = { date: 1, dur: 1 };
377
+ /** The methods and their arity: on a date string, and (`dur_text`/`dur_iso`) on a number. */
378
+ export const DATE_METHODS = {
379
+ plus: 1, minus: 1, until: 1, format: 1,
380
+ year: 0, month: 0, day: 0, weekday: 0, hour: 0, minute: 0, second: 0, date: 0,
381
+ dur_text: 0, dur_iso: 0,
382
+ };
383
+ function arity(spelling, want, got) {
384
+ if (got !== want)
385
+ throw new FilterInvalid(`${spelling} takes exactly ${want} argument${want === 1 ? "" : "s"}, got ${got}`, "OQX");
386
+ }
387
+ /** `callFunction` arm: `date(x)` / `dur(x)`; `{ handled: false }` for any other name. */
388
+ export function dateFunction(name, args, nowMs = Date.now()) {
389
+ if (!Object.hasOwn(DATE_FUNCTIONS, name))
390
+ return { handled: false };
391
+ arity(`${name}()`, DATE_FUNCTIONS[name], args.length);
392
+ return { handled: true, value: name === "date" ? dateOf(args[0], nowMs) : durOf(args[0]) };
393
+ }
394
+ /** `callMethod` arm: the date methods on a string receiver and the duration
395
+ * renderers on a number; a receiver of any other kind yields absent (the
396
+ * methods are total). `{ handled: false }` for any other name. */
397
+ export function dateMethod(name, recv, args) {
398
+ if (!Object.hasOwn(DATE_METHODS, name))
399
+ return { handled: false };
400
+ arity(`.${name}()`, DATE_METHODS[name], args.length);
401
+ const field = (k) => {
402
+ const d = parseDate(recv);
403
+ return d === null ? null : fieldsOf(d)[k];
404
+ };
405
+ switch (name) {
406
+ case "plus": return { handled: true, value: plusMinus(recv, args[0], 1) };
407
+ case "minus": return { handled: true, value: plusMinus(recv, args[0], -1) };
408
+ case "until": return { handled: true, value: until(recv, args[0]) };
409
+ case "format": return { handled: true, value: formatDate(recv, args[0]) };
410
+ case "year":
411
+ case "month":
412
+ case "day":
413
+ case "weekday":
414
+ case "hour":
415
+ case "minute":
416
+ case "second":
417
+ return { handled: true, value: field(name) };
418
+ case "date": {
419
+ const d = parseDate(recv);
420
+ return { handled: true, value: d === null ? null : renderDate({ ...d, dateOnly: true }) };
421
+ }
422
+ case "dur_text": return { handled: true, value: durText(recv) };
423
+ case "dur_iso": return { handled: true, value: durIso(recv) };
424
+ default: return { handled: false };
425
+ }
426
+ }
427
+ //# sourceMappingURL=dates.js.map
@@ -165,6 +165,14 @@ function kindOfPath(on, path) {
165
165
  cur = seg;
166
166
  continue;
167
167
  }
168
+ // At a group scope `rows` yields the grouped rows (their kind); a key is a
169
+ // value the language built, not a row of any kind.
170
+ if (typeof cur === "object") {
171
+ if (seg !== "rows" || i !== 0)
172
+ return null;
173
+ cur = cur.group;
174
+ continue;
175
+ }
168
176
  if (seg === "$it" && i === 0)
169
177
  continue;
170
178
  const next = HANDLES[cur][seg] ?? RELATIONS[cur][seg];
@@ -172,7 +180,7 @@ function kindOfPath(on, path) {
172
180
  return null;
173
181
  cur = next;
174
182
  }
175
- return cur === "root" ? null : cur;
183
+ return cur === "root" || typeof cur === "object" ? null : cur;
176
184
  }
177
185
  /** The kind of the frontier rows of a walk: the seeds' kind when every destination yields rows of that same kind, else unknown. */
178
186
  function kindOfFrontier(seed, f, readDepth, byDepth) {
@@ -194,13 +202,16 @@ export function kindOfScope(steps) {
194
202
  for (const step of steps) {
195
203
  const e = step.expr;
196
204
  if (e.kind === "follow")
197
- cur = kindOfFrontier(cur === "root" ? null : cur, e, readDepth, byDepth);
205
+ cur = kindOfFrontier(cur === "root" || typeof cur === "object" ? null : cur, e, readDepth, byDepth);
206
+ // A `group by` step (oqx 0.24): the rows at this depth become groups of them.
207
+ else if (e.kind === "groupBy")
208
+ cur = { group: cur === "root" || typeof cur === "object" ? null : cur };
198
209
  else
199
210
  cur = kindOfExpr(e, cur, readDepth, byDepth);
200
211
  byDepth.set(step.depth, cur);
201
212
  readDepth = step.depth;
202
213
  }
203
- return cur === "root" ? null : cur;
214
+ return cur === "root" || typeof cur === "object" ? null : cur;
204
215
  }
205
216
  /** Judge a dotted name read on rows of `target`; null = known. */
206
217
  function judge(store, repoId, target, path) {
@@ -91,6 +91,12 @@ function subMayRaise(s, target) {
91
91
  return true;
92
92
  if (s.follow && followMayRaise(s.follow, target))
93
93
  return true;
94
+ // A grouped block (oqx 0.24) evaluates its keys, `having` and its aggregates
95
+ // (`sum` over a string raises) per group.
96
+ if (s.groupBy?.keys.some((k) => exprMayRaise(k.expr, target, false)))
97
+ return true;
98
+ if (s.having && whereMayRaise(s.having, target, false))
99
+ return true;
94
100
  if (s.limit && exprMayRaise(s.limit, target, false))
95
101
  return true;
96
102
  if (s.offset && exprMayRaise(s.offset, target, false))
@@ -133,6 +139,9 @@ function exprMayRaise(e, target, root) {
133
139
  // a record is an eval error only the data decides, so with any interpolation
134
140
  // it may raise.
135
141
  case "template": return e.parts.some((p) => typeof p !== "string");
142
+ // An aggregate (oqx 0.24) raises on a non-number under `sum`/`avg`, which
143
+ // only the data decides; a grouped query is never planned anyway (below).
144
+ case "aggregate": return true;
136
145
  case "lit":
137
146
  case "binding": return false;
138
147
  }
@@ -148,8 +157,14 @@ export class SQLiteQueryPlanner {
148
157
  this.ctxOpts = ctxOpts;
149
158
  }
150
159
  plan(query, params) {
151
- // Only simple top-level scans: no follow, no `from E` re-projection.
152
- if (query.follow || query.from.length > 0)
160
+ // Only simple top-level scans: no follow, no `from E` re-projection, no
161
+ // `group by` (oqx 0.24). A grouped query is declined whole: its `where`
162
+ // conjuncts could be pushed (grouping happens after them, so the residual
163
+ // would group the produced rows correctly), but a pushed conjunct that
164
+ // empties the scan would hide an eval error an aggregate or `having` raises
165
+ // in memory (`sum` over a string) — decline (c) extended to the post-group
166
+ // clauses. SQL `GROUP BY` pushdown is a later round.
167
+ if (query.follow || query.from.length > 0 || query.groupBy)
153
168
  return null;
154
169
  const target = rootTarget(query.source);
155
170
  if (!target)
@@ -72,6 +72,7 @@ export interface OqxOptions {
72
72
  }
73
73
  export declare function rootPathLiterals(e: Expr): Expr;
74
74
  export declare function checkRootSpellings(q: Query): void;
75
+ export declare function foldDates(e: Expr): Expr;
75
76
  export declare function collectSemanticPhrases(source: string): string[];
76
77
  export declare function reservedAlias(items: readonly SelectItem[]): FilterInvalid | null;
77
78
  export declare const BARE_NEEDS_SELECT_MESSAGE = "`bare` needs a `select` \u2014 without a projection a hit is only its envelope";