An example VGI worker: a DuckDB table function that projects when a
cron expression will fire, starting from a given timestamp. Cron math is provided by the
croner crate.
ATTACH 'crontimes' (TYPE vgi,
LOCATION '/path/to/vgi-crontimes/target/release/crontimes-worker');
-- the next 5 daily-09:00 fires after a start
SELECT * FROM crontimes.main.cron_fire_times('0 9 * * *', TIMESTAMP '2026-06-18 00:00:00', "end" := 5);┌───────┬─────────────────────┐
│ seq │ fire_time │
│ int64 │ timestamp │
├───────┼─────────────────────┤
│ 0 │ 2026-06-18 09:00:00 │
│ 1 │ 2026-06-19 09:00:00 │
│ 2 │ 2026-06-20 09:00:00 │
│ 3 │ 2026-06-21 09:00:00 │
│ 4 │ 2026-06-22 09:00:00 │
└───────┴─────────────────────┘
cron_fire_times(cron VARCHAR, start TIMESTAMP|TIMESTAMPTZ, [end := <int|timestamp>])
-> (seq BIGINT, fire_time TIMESTAMP|TIMESTAMPTZ)
-
cron— a cron expression. Standard 5-field (min hour dom month dow), plus optional leadingseconds(6-field) and/or trailingyear(7-field). -
start— the lower bound, and polymorphic: pass aTIMESTAMPandfire_timecomes back as a naiveTIMESTAMP(plain wall-clock, no DST); pass aTIMESTAMPTZandfire_timecomes back asTIMESTAMPTZ, fired in the session time zone (DST-aware — see below). Ifstartexactly matches the schedule it fires once (inclusive); otherwise the first row is the next fire strictly afterstart. -
end— optional, named, and polymorphic:- an integer ⇒ return that many occurrences (a count),
- a timestamp ⇒ return every fire time strictly before it,
- omitted ⇒ stream the (effectively infinite) series, hard-capped at year 4096 — pair it
with
LIMIT.
endis a SQL reserved word, so it must be quoted in named-argument position:"end" := ….
With a TIMESTAMPTZ start, fire times are computed in the session's TimeZone (forwarded from
DuckDB; requires the icu extension), so wall-clock schedules track daylight-saving:
LOAD icu;
SET TimeZone = 'America/New_York';
SELECT seq, fire_time, fire_time AT TIME ZONE 'UTC' AS utc_wall
FROM crontimes.main.cron_fire_times('0 12 * * *', TIMESTAMPTZ '2026-03-06', "end" := 4);┌─────┬────────────────────────┬─────────────────────┐
│ seq │ fire_time │ utc_wall │
├─────┼────────────────────────┼─────────────────────┤
│ 0 │ 2026-03-06 12:00:00-05 │ 2026-03-06 17:00:00 │ EST (UTC-5)
│ 1 │ 2026-03-07 12:00:00-05 │ 2026-03-07 17:00:00 │
│ 2 │ 2026-03-08 12:00:00-04 │ 2026-03-08 16:00:00 │ EDT after spring-forward
│ 3 │ 2026-03-09 12:00:00-04 │ 2026-03-09 16:00:00 │
└─────┴────────────────────────┴─────────────────────┘
"Noon" stays noon local while the underlying UTC instant shifts an hour. A naive TIMESTAMP
start ignores the zone entirely (plain wall-clock). Without icu/a session TimeZone, the
TIMESTAMPTZ path falls back to UTC.
-- every 15 minutes, first 10 (unbounded + LIMIT)
SELECT * FROM crontimes.main.cron_fire_times('*/15 * * * *', TIMESTAMP '2026-06-18 00:00:00') LIMIT 10;
-- weekday noon within a window (timestamp upper bound), tz-aware
SELECT * FROM crontimes.main.cron_fire_times(
'0 12 * * 1-5',
TIMESTAMPTZ '2026-06-18 00:00:00+00',
"end" := TIMESTAMPTZ '2026-07-01 00:00:00+00');
-- 6-field expression with seconds: every 30 seconds
SELECT * FROM crontimes.main.cron_fire_times('*/30 * * * * *', TIMESTAMP '2026-06-18 09:00:00', "end" := 4);cargo build --release # -> target/release/crontimes-worker
cargo test -p crontimes-core # cron-math unit testsRequires a DuckDB build with the vgi extension (e.g. the
haybarn distribution) to ATTACH the worker. See CLAUDE.md for the
end-to-end test harness.
crates/crontimes-core— pure cron→microseconds logic overcroner+chrono/chrono-tz(UTC and DST-aware), unit-tested with no Arrow/VGI dependency.crates/crontimes-worker— the VGI adapter: aTableFunctionwith a streamingTableProducer. It emits in growing batches (2048 → 65536 rows) with bounded memory, so an unbounded query streams to the year-4096 cap without materializing — e.g.'* * * * *'from 2026 yields 1,088,472,960 rows in ~8 min holding <10 MB. (Throughput is bound by cron computation, not IPC, so batch size mostly affects round-trip count rather than wall time.) Thestarttype (naive vs. tz) is dispatched at bind; the sessionTimeZoneis requested viaregister_setting.