@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.
- package/dist/src/core/ids.d.ts +1 -1
- package/dist/src/core/read/document.d.ts +4 -1
- package/dist/src/core/read/document.js +18 -6
- package/dist/src/core/store/archive.d.ts +196 -0
- package/dist/src/core/store/archive.js +606 -0
- package/dist/src/core/store/gc.d.ts +25 -4
- package/dist/src/core/store/gc.js +43 -28
- package/dist/src/core/store/rebuild.d.ts +2 -0
- package/dist/src/core/store/rebuild.js +8 -2
- package/dist/src/core/store/retention.d.ts +138 -0
- package/dist/src/core/store/retention.js +409 -0
- package/dist/src/core/store/schema.d.ts +3 -2
- package/dist/src/core/store/schema.js +30 -4
- package/dist/src/core/store/size.d.ts +21 -2
- package/dist/src/core/store/size.js +24 -3
- package/dist/src/core/store/store.d.ts +1 -0
- package/dist/src/core/store/store.js +53 -1
- package/dist/src/graph/history.d.ts +2 -0
- package/dist/src/graph/history.js +8 -4
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -0
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/errors.js +4 -0
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +24 -6
- package/dist/src/mcp/server.js +2 -2
- package/dist/src/oqx-js/context.js +21 -5
- package/dist/src/oqx-js/dates.d.ts +105 -0
- package/dist/src/oqx-js/dates.js +427 -0
- package/dist/src/oqx-js/names.js +14 -3
- package/dist/src/oqx-js/planner.js +17 -2
- package/dist/src/oqx-js/run.d.ts +1 -0
- package/dist/src/oqx-js/run.js +66 -1
- package/dist/src/spec-versions.d.ts +4 -4
- package/dist/src/spec-versions.js +3 -3
- 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
|
package/dist/src/oqx-js/names.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
package/dist/src/oqx-js/run.d.ts
CHANGED
|
@@ -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";
|