Skip to main content

Chronix

Struct Chronix 

Source
pub struct Chronix { /* private fields */ }
Expand description

The main Chronix database handle.

Provides a synchronous, thread-safe API for an embedded time-series database. Multiple threads may share a Chronix handle via Arc<Chronix>.

§Lock Ordering

To prevent deadlocks, locks must be acquired in the following order. Any code path that holds more than one lock simultaneously must obey this total order:

  1. catalog (RwLock<SegmentCatalog>)
  2. blooms (RwLock<BTreeMap<u64, SeriesBloomFilter>>)
  3. tombstones (RwLock<TombstoneSet>)
  4. rollup_registry (RwLock<RollupRegistry>)

known_series uses DashSet (sharded concurrent hash set) and does not participate in this ordering — its per-shard internal locks are fine-grained and never held across other lock acquisitions.

§Lifecycle

use chronix::Chronix;
use chronix_core::ChronixConfig;

let config = ChronixConfig::builder()
    .data_dir("/tmp/mydb")
    .build()
    .unwrap();

let db = Chronix::open(config).unwrap();
// … insert, query …
db.close().unwrap();

§One handle, cheap to clone

Chronix is a reference-counted handle: clone() is an Arc clone, every clone reads and writes the same database, and the database is closed when the last handle is dropped — or when any handle calls close. Hand clones to threads and tasks freely; there are no separate read and write handles to keep in step.

Implementations§

Source§

impl Chronix

Source

pub async fn archive_cold_segments( &self, config: &ArchiveConfig, ) -> Result<ArchiveOutcome>

Archive data older than config.cold_after to object storage.

Groups the catalog into (measurement, shard) units, reads each complete cold group through the read path, writes it as one Parquet object under measurement=<m>/shard=<n>/, verifies the upload, and only then drops the source segments — in that order, so a crash at any point leaves either hot data or archived data, never a catalog entry without a file.

§Errors

Returns an error if the object store cannot be reached. Per-group failures are counted in ArchiveOutcome::failed rather than aborting the pass: one unreadable group must not stop the rest.

Source§

impl Chronix

Source

pub fn register_udf(&self, udf: Arc<ScalarUDF>)

Register a custom scalar UDF that will be available in SQL queries.

Source

pub fn register_udaf(&self, udaf: Arc<AggregateUDF>)

Register a custom aggregate UDF that will be available in SQL queries.

Source

pub fn sql(&self, query: &str) -> Result<Vec<RecordBatch>>

Run a SQL query and collect the result.

The whole SQL surface — DataFusion with Chronix’s measurements as tables, time_bucket, rate, forecast and the rest of the analytics functions — from one call:

let batches = db.sql(
    "SELECT time_bucket('5m', _time) AS t, avg(usage) FROM cpu GROUP BY t ORDER BY t",
)?;

Queries are read-only, verified before they are planned: SET, DDL and COPY are rejected, exactly as on the server. Writes go through insert_batch.

This call blocks: on a private runtime from synchronous code, or by stepping the current worker aside on a multi-thread tokio runtime. On a current-thread runtime it is an error rather than a deadlock — use sql_async there.

§Errors

Returns DbError::Sql for a query that does not parse, plan or execute, and DbError::Internal when called from inside a current-thread async runtime.

Source

pub async fn sql_async(&self, query: &str) -> Result<Vec<RecordBatch>>

sql for async callers.

§Errors

As sql.

Source

pub fn session_context(&self) -> SessionContext

The DataFusion SessionContext behind sql: every measurement as a table, all analytics functions registered. For callers that want DataFusion’s own API — DataFrame, EXPLAIN, streaming execution.

Built on first use and shared; rebuilt after register_udf / register_udaf. Multi-tenant servers scope their own contexts with create_namespaced_session_context.

Source

pub fn promql(&self, query: &str, at_ns: i64) -> Result<PromQLValue>

Evaluate a PromQL instant query at at_ns.

Measurements are metrics and tags are labels, exactly as chronixd serves them to Grafana; the evaluator tracks Prometheus 3.x semantics.

let now = 1_700_000_000_000_000_000;
let value = db.promql(r#"rate(requests{host="a"}[5m])"#, now)?;
§Errors

Returns DbError::PromQl for a query that does not parse or evaluate.

Source

pub fn promql_range( &self, query: &str, start_ns: i64, end_ns: i64, step_ns: i64, ) -> Result<PromQLValue>

Evaluate a PromQL range query over [start_ns, end_ns] at step_ns.

The result is one series per label set, one sample per step; the window is read once, not once per step.

§Errors

As promql; a non-positive step is an error.

Source

pub fn custom_udfs(&self) -> Vec<Arc<ScalarUDF>>

Return a snapshot of all runtime-registered scalar UDFs.

Source

pub fn custom_udafs(&self) -> Vec<Arc<AggregateUDF>>

Return a snapshot of all runtime-registered aggregate UDFs.

Source

pub fn schema(&self, measurement: &str) -> Option<Arc<MeasurementSchema>>

Return the schema for a measurement, if it exists.

None for a measurement pending a soft-delete, exactly as for one that never existed: a “drop” that leaves the data fully readable for its whole grace period is not a drop. The segments and the schema registry entry are untouched, so a restore before the deadline is instant and lossless.

Source

pub fn schema_registry(&self) -> &SchemaRegistry

Return a snapshot of the schema registry.

Source

pub fn config(&self) -> &ChronixConfig

Return the database configuration.

Source

pub fn statistics(&self) -> DatabaseStatistics

Return a snapshot of current database statistics.

Provides on-demand database metrics including series cardinality, segment counts, memory usage, and WAL position. Also emits the values as gauge metrics via the metrics crate.

series_count is the exact number of live series — the same number the write-path cardinality limit is checked against, so the gauge and the admission decision cannot disagree.

Source

pub fn disk_usage_bytes(&self) -> u64

Bytes the database occupies on disk, cached for a minute.

Disk usage is the storage number an operator actually alerts on, and nothing exported it. It needs a directory walk, so the result is cached: statistics() runs on every metrics scrape, and a walk per scrape would make the exporter the most expensive thing in the process once a deployment holds many segments.

Source

pub fn measurement_names_in(&self, namespace: Option<&str>) -> Vec<String>

Measurement names this namespace holds series for.

None means unscoped — every measurement, which is what a single-tenant deployment wants and what the embedded API always wants.

The SQL catalog answers from this rather than from the process-wide schema registry, which listed every tenant’s measurement names: SELECT * FROM another_tenants_measurement returned zero rows where a name that does not exist errors, so a tenant could enumerate the others by probing. A measurement pending a whole-measurement drop is filtered out of both branches. It used to be filtered out of the unscoped one only, so a drop with a grace period configured was invisible on a single-tenant server and fully listed on a tenanted one — one question, two answers, in one function.

Source

pub fn has_measurement_in( &self, namespace: Option<&str>, measurement: &str, ) -> bool

Whether namespace holds any series of measurement.

None means unscoped, and then only the measurement’s existence matters. A measurement pending a drop exists for nobody, on either branch — the scoped one used to say yes, so under tenancy a dropped table still planned and answered zero rows where a name that does not exist is a planning error.

Source

pub fn data_dir(&self) -> &Path

Return the path to the data directory.

Source

pub fn segment_count(&self) -> usize

How many active segments the database holds.

Source

pub fn segments(&self) -> Vec<SegmentInfo>

A snapshot of every active segment.

Ordered by measurement, then by start time, so two calls on an unchanged database compare equal. The paths are absolute and resolved against this database’s directory.

Source

pub fn segments_of(&self, measurement: &str) -> Vec<SegmentInfo>

A snapshot of the active segments holding data for measurement.

Source

pub fn wal_replayed_records(&self) -> usize

Number of WAL records open() replayed into the memtable.

Zero after a graceful close(): everything acknowledged was already in a segment, and the catalog records the WAL floor so replay has nothing to do.

Source

pub fn wal_sequence(&self) -> u64

Return the current WAL sequence number.

Source

pub fn tag_keys(&self) -> Vec<String>

Every distinct tag key this database holds.

Both halves. The inverted index is built at flush, so it knows only the segments on disk; a series written a second ago is in no segment and therefore in no index. Answering from the index alone — which this did — meant a freshly started gateway reported no labels at all until its first flush, and a tag that only ever appears in recent data was permanently invisible. The server’s /labels never had the bug because it scans; this is the embedded answer to the same question, and the two must agree.

Sorted, and deduplicated across the two sources.

Source

pub fn tag_values(&self, key: &str) -> Vec<String>

Every distinct value of key this database holds.

Segments and memtables both, for the reason in tag_keys. Sorted.

Source

pub fn event_bus(&self) -> &EventBus

Access the underlying CDC event bus.

Use this to create raw chronix_streaming::cdc::Subscriptions or inspect bus statistics (published/lagged counts).

Source

pub fn subscribe(&self, filter: SubscriptionFilter) -> FilteredSubscription

Create a filtered CDC subscription.

Returns a FilteredSubscription that only delivers events matching the provided filter criteria (measurement, tags, event type).

§Example
use chronix_streaming::cdc::SubscriptionFilter;
use chronix::Chronix;

let filter = SubscriptionFilter::all()
    .measurement("cpu")
    .event_type("point_written");
let sub = db.subscribe(filter);
Source§

impl Chronix

Source

pub fn forecast( &self, measurement: &str, field: &str, tags: &[(&str, &str)], start_ns: i64, end_ns: i64, horizon: usize, config: Option<ForecastConfig>, ) -> Result<ForecastResult>

Forecast future values for a series.

Fetches historical data for the given measurement and field, fits a forecast model, and returns predicted values.

Source

pub fn auto_forecast( &self, measurement: &str, field: &str, tags: &[(&str, &str)], start_ns: i64, end_ns: i64, horizon: usize, options: Option<AutoForecastOptions>, ) -> Result<AutoForecast>

Forecast a series with the model chosen automatically.

Fetches the window, then hands it to select_model, which detects the seasonal period, chooses a differencing order by a KPSS unit-root test, searches an ARIMA order, and scores every eligible candidate by rolling-origin cross-validation at horizon — the only comparison that transfers across model families. The winner is refitted on the whole window.

The returned AutoForecast carries the forecast and the reasoning: which model won, what every candidate scored, and why anything that was not scored was left out.

§Errors

Returns DbError if the window cannot be read, if it is too short to hold a training window plus a horizon-long test window, or if no candidate could be cross-validated.

Source

pub fn detect_anomalies( &self, measurement: &str, field: &str, tags: &[(&str, &str)], start_ns: i64, end_ns: i64, config: Option<AnomalyConfig>, ) -> Result<Vec<AnomalyScore>>

Detect anomalies in a series.

Fetches historical data, fits a detector, and returns scores.

Source

pub fn preprocess( &self, measurement: &str, field: &str, tags: &[(&str, &str)], start_ns: i64, end_ns: i64, config: PreprocessConfig, ) -> Result<PreprocessResult>

Preprocess a series (interpolate, smooth, resample).

Source

pub fn fetch_series_data( &self, measurement: &str, field: &str, tags: &[(&str, &str)], start_ns: i64, end_ns: i64, ) -> Result<(Vec<i64>, Vec<f64>)>

Fetch raw (timestamps, values) for a single field+tags combination.

Uses memtable scan for in-memory data. For persisted data, build a query via Chronix::query() and Chronix::execute().

Source§

impl Chronix

Source

pub fn backup(&self, target_dir: &Path) -> Result<BackupManifest>

Take a checkpoint of this database into target_dir.

A checkpoint: the database as of the flush that starts it. Every write acknowledged before this call is in it; a write accepted while it runs is not. The directory it produces is a complete database plus a BackupManifest, driven by the catalog rather than by a directory walk and held against retirement by a segment lease, so it cannot be torn. Segment files are hard-linked where the target shares a filesystem, so a checkpoint of a large database is near instant and costs no space until those segments are compacted away. It carries no WAL, because the flush leaves nothing to replay.

restore copies it somewhere a server can open, and verifies it on the way; verify_backup runs that check on its own.

target_dir may already hold an earlier backup: files the new checkpoint does not name are removed, so backing up into one directory repeatedly is a rolling checkpoint rather than an accumulating pile.

§Errors

Returns an error if the database is closed, the flush fails, or file I/O fails.

Source

pub fn restore(backup_dir: &Path, target_dir: &Path) -> Result<BackupManifest>

Restore a checkpoint from backup_dir into target_dir.

backup_dir must contain a backup_manifest.json written by backup. target_dir must not already exist. The result can be opened with Chronix::open — on this machine or any other, because every path the catalog holds is relative to the data directory.

The copy is verified: every segment the restored catalog names has to be present at its recorded size, and the file and byte counts have to match the manifest. An incomplete backup is refused here rather than opening as a database that fails at its first query — which is what an unverified restore gives you, at the one moment nobody has a second copy.

§Errors

Returns an error if the manifest is missing or corrupt, the target exists, file I/O fails, or verification finds the backup incomplete.

Source

pub fn verify_backup(backup_dir: &Path) -> Result<BackupManifest>

Check that a backup is complete, without restoring it.

Returns its BackupManifest if every segment the backup’s own catalog names is present at its recorded size and the count matches the manifest — the same check restore runs, offered on its own because the standing advice for any backup scheme is to verify the ones you are keeping before you need them. A backup you can only check by restoring it is one nobody checks.

It reads; it writes nothing, and it needs no running database.

§Errors

DbError::InvalidRequest if the directory is not a backup, its manifest is corrupt, or a segment is missing or the wrong size — with the file named.

Source§

impl Chronix

Source

pub fn drop_measurement(&self, measurement: &str) -> Result<()>

Drop an entire measurement and all its data.

This removes:

  • All on-disk segments for the measurement
  • The measurement schema from the catalog and registry
  • Bloom filters for the dropped segments
  • Time-index entries for the dropped segments

In-flight memtable data is flushed first so that all data is in segments before removal. The cardinality tracker (known_series) is cleared to reflect the reduced series universe — it will be lazily rebuilt by subsequent inserts.

§Errors

Returns an error if the database is closed, flush fails, or segment deletion encounters an I/O error.

Source

pub fn delete_series( &self, measurement: &str, tags: &BTreeMap<String, String>, ) -> Result<()>

Delete every stored point of one series.

“Every stored point” is meant literally, and the distinction matters: the tombstone’s upper bound is resolved to the newest timestamp the series actually has, so a point written afterwards re-creates the series rather than disappearing into a standing delete. Re-provisioning a device under an identifier that had once been deleted is the ordinary case here.

This delegates to execute_delete rather than repeating it, so both paths flush first and both evict the last-value cache.

§Errors

Returns an error if the database is closed or the delete fails.

Source

pub fn gc_tombstones(&self) -> usize

Reclaim tombstones that can no longer mask anything.

A tombstone records the segments it was issued against. A segment leaves the catalog only by being rewritten — compaction applies tombstones as it merges — or by being deleted outright, so once none of a tombstone’s segments remains, no stored row can still match it and the tombstone is provably dead.

The question that matters is whether the delete has been materialised — not whether the series is still known, nor whether the measurement has active segments. Either weaker rule resurrects data.

Returns the number of tombstones reclaimed.

Source

pub fn delete_builder(&self) -> DeleteBuilder

Create a new delete request builder.

Returns a DeleteBuilder for ergonomic construction of predicate-based delete requests.

§Example
ⓘ
db.delete_builder()
    .measurement("cpu")
    .tag("host", "server-01")
    .before(cutoff_ts)
    .build()?;
Source

pub fn execute_delete(&self, req: &DeleteRequest) -> Result<DeleteOutcome>

Execute a predicate-based delete request.

Every series matching the measurement and tag filters is tombstoned over the request’s time range. When the request names no range, the upper bound is resolved per series to the newest timestamp that series actually has, so the delete covers exactly the data that exists and a later write re-creates the series.

The delete is durable before this returns: the resolved tombstones are appended to the catalog manifest and fsynced. They are logged to the data WAL as well, so a point-in-time restore replays them, but the catalog is what makes the delete survive a restart — the data WAL is truncated once the memtable it covers has been flushed.

§Errors

Returns an error if the database is closed, or if the tombstones cannot be persisted.

Source§

impl Chronix

Source

pub fn flush(&self) -> Result<Vec<FlushResult>>

Flush every memtable to segment files and advance the WAL floor.

Normally the maintenance thread does this when a memtable crosses its threshold; call it to make everything durable in segments now. One flush runs at a time; a concurrent caller waits for it.

§Errors

Returns DbError::Closed if the database is already closed, or the first flush error. A shard whose flush failed keeps its memtable — and its WAL records — for the next attempt; nothing is lost by a failed flush.

Source

pub fn close(&self) -> Result<()>

Close the database, flushing all pending data.

After closing, all subsequent operations will return DbError::Closed.

§Errors

Returns any error encountered while flushing shards or syncing the WAL during shutdown. A failed close can be retried.

Source

pub fn check_writable(&self) -> Result<()>

Whether the database can accept writes at all, and why not.

Persistent conditions only: closed, a WAL poisoned by a failed fsync, or a maintenance thread that has ended — each refuses every write until the database is reopened. Transient back-pressure — a full memtable waiting on a flush — is deliberately not unready: that is the moment to keep serving and let back-pressure work, not the moment to leave the load balancer.

The maintenance thread is here because a database that has lost it looks perfectly healthy right up to the moment it stops accepting writes for ever: nothing flushes, compacts, materialises a rollup or expires a shard again, and the memtable fills at whatever rate the workload writes. Reporting it while writes still succeed is what gives an orchestrator time to restart the process instead of discovering it through a wall of refusals.

chronixd’s /ready is the caller.

§Errors

DbError::Closed or DbError::PersistentOverload, with the reason.

Source

pub fn export_parquet( &self, plan: &QueryPlan, output_path: &Path, parquet_config: &ParquetExportConfig, ) -> Result<ParquetExportResult>

Export query results to a Parquet file.

For a Scan plan the export is genuinely streaming end to end: the query is driven by execute_iter, which reads one time-disjoint bucket of segments at a time, and each batch is written and dropped before the next is read. Peak memory is bounded by the busiest time bucket, not by the size of the export — a gateway can export a window far larger than its RAM.

Aggregate, downsample and window plans have to see their whole input, so those fall back to materialised execution.

§Errors

Returns an error if the database is closed, the query fails, or the Parquet file cannot be written.

Source

pub fn enforce_retention(&self, retention: Duration) -> Result<RetentionResult>

Enforce a retention policy, dropping every shard whose data falls entirely before now - retention.

Takes a Duration, as ChronixConfig::retention does. It used to take a bare i64 of nanoseconds, so the natural reading of the configuration — retention(Duration::from_secs(86_400)) — translated to enforce_retention(86_400), which is 86 microseconds of retention and deletes the database. A unit a caller has to remember is not a unit; this one cannot be got wrong.

Returns the number of segments deleted.

§Errors

Returns an error if the database is closed.

Source

pub fn enforce_configured_retention(&self) -> Result<RetentionResult>

Enforce every configured retention rule — the global one if there is one, plus every per-measurement and per-rollup rule.

This is what the maintenance thread calls. It used to call enforce_retention only when a global retention was configured, so a database that set a rule for one measurement, or a rollup that declared its own retention_ns, expired nothing at all.

§Errors

Returns an error if the database is closed.

Source

pub fn compact(&self) -> Result<usize>

Run compaction on all eligible shards.

Uses the CompactionPicker to identify shards with enough L0 segments to merit compaction, then runs the CompactionExecutor for each task. Input segments are removed from the catalog and disk after successful compaction.

§Returns

The number of compaction tasks executed.

§Errors

Returns an error if the database is closed or I/O fails.

Source

pub fn gc(&self) -> Result<usize>

Unlink the files of segments that were retired while a reader still held them.

Returns the number of segment files removed.

A retirement pass — retention, a drop, archiving, a compaction’s inputs — takes a segment out of the catalog and unlinks it in one step, unless a running scan has leased the path; then the entry is marked SegmentState::SoftDeleted, the rows are gone from every query, and the bytes wait here for the reader to finish. This is a step of every maintenance pass, so the wait is bounded by the maintenance interval rather than by anything the reader does.

§Errors

Returns an error if the database is closed.

Source

pub fn gc_pending_measurement_drops(&self) -> Result<usize>

Hard-delete measurements whose soft-delete TTL has elapsed.

Scans the catalog’s pending-drop set and hard-deletes any entry whose deadline is in the past. Each hard delete cancels its own pending-drop entry once it has actually removed the data (hard_delete_measurement), so a delete that fails part-way leaves the entry pending for the next pass to retry rather than forgetting it.

Returns the number of measurements hard-deleted.

The deadline is measured against the same reference retention uses, not the raw wall clock: see the comment in the body.

Source

pub fn restore_measurement(&self, measurement: &str) -> Result<bool>

Restore a measurement that was soft-deleted but whose TTL hasn’t elapsed yet.

Cancels the pending drop, durably, so it becomes visible in queries again. Returns true if the measurement was pending deletion and was restored, false if it was not pending.

§Errors

Returns an error if the database is closed, or the manifest cannot be written.

Source

pub fn is_measurement_pending_drop(&self, measurement: &str) -> bool

Check whether a measurement is pending soft-delete.

Source

pub fn flush_shard(&self, shard_id: ShardId) -> Result<Vec<FlushResult>>

Flush a single shard to per-measurement segment files.

Registration — catalog, indexes, sidecar — runs inside the engine’s flush window, so the memtable is released only once every segment it produced is in the catalog. A failure at any step keeps the memtable frozen for the next attempt and removes the files of this attempt.

Source§

impl Chronix

Source

pub fn query(&self) -> QueryBuilder

Start building a query using the fluent builder API.

§Example
let plan = db.query()
    .measurement("cpu")
    .tag("host", "server-01")
    .field("usage_idle")
    .range(0, i64::MAX)
    .build()
    .unwrap();
let batch = db.execute(&plan).unwrap();
Source

pub fn execute(&self, plan: &QueryPlan) -> Result<RecordBatch>

Execute a query plan and return the result as one Arrow RecordBatch.

Every plan runs on the same engine: the scan at its root is read by execute_iter — one time-disjoint bucket of segments at a time, deduplicated last-write-wins, tombstones and projection applied per source — and the nodes above it fold over that stream. Aggregates and downsamples fold incrementally; a limit stops the scan; a window function sees its whole input, so it is the one shape that materialises the scan first.

One read path, so nothing can disagree about last-write-wins, the time range or tombstones; execute is a convenience over it.

§Errors

Returns an error if the database is closed, the plan is invalid, a segment cannot be read, the query exceeds its timeout or memory budget, or the collected result exceeds max_query_result_bytes.

Source

pub fn execute_with_stats( &self, plan: &QueryPlan, ) -> Result<(RecordBatch, PruningStats)>

execute, returning the PruningStats that describe how many segments the pruning pipeline eliminated.

Source

pub fn execute_stream(&self, plan: &QueryPlan) -> Result<Vec<RecordBatch>>

Execute a query plan as a sequence of RecordBatches.

Scan, Limit(Scan), Aggregate(Scan) and Downsample(Scan) are streamed off execute_iter and collected here; the collection is bounded by max_query_result_bytes. Other plan shapes return a single batch, as execute would. Prefer execute_iter when the consumer can process batches as they arrive — it never accumulates.

§Errors

As execute.

Source

pub fn last_value( &self, measurement: &str, tags: &BTreeMap<String, String>, ) -> Result<Option<Point>>

Return the most recent Point for a given series.

This is a fast path: it scans the memtable first (newest data), then on-disk segments in reverse time order, and returns as soon as a matching point is found. No full query plan is needed.

tags must name every tag of the series, not a subset: this answers “the newest point of this series”, so a partial tag set identifies a different series and correctly yields None. Use query with tag filters to ask about a set of series.

Points masked by a tombstone are skipped at the timestamp level, so a ranged delete leaves the newest surviving point answerable rather than hiding the series.

§Errors

Returns an error if the database is closed or an I/O error occurs.

Source§

impl Chronix

Source

pub fn materialise_rollups(&self) -> Result<usize>

Materialise every rollup as far as its input is final.

For each rollup, the buckets between its watermark and the newest bucket whose input can no longer change are aggregated once, over every row that reaches them — segments and memtable, deduplicated, tombstones applied, one time-disjoint bucket of segments in memory at a time — written to the target measurement, and the watermark is advanced and persisted.

“Can no longer change” is exact rather than a grace period. A raw measurement accepts live writes only within the out-of-order window, so every bucket that ends before the window’s oldest shard is final. A rollup whose source is itself a rollup target is final exactly as far as that source has been materialised. Chains therefore stay consistent by construction, and a device that was offline for days materialises its backlog the moment it writes again, because the window is anchored on the newest write rather than on the clock.

This is the only place rollups are computed: a bucket aggregated from whatever segment happens to be in hand is a partial bucket that last-write-wins then resolves to a fraction of the truth. Writes go through backfill, not the live path, whose out-of-order window rejects anything older than two shards by design.

Called by compact and by enforce_retention before it drops a shard; callers that neither compact nor enforce retention call it themselves. Returns the number of rollup points written.

§Errors

Returns the first error from reading the source or writing the target. A rollup that fails keeps its watermark, so nothing is skipped; retention will not drop the raw data it needs.

Source

pub fn refresh_rollup(&self, name: &str, start: i64, end: i64) -> Result<usize>

Recompute [start, end] of a rollup from its source, now.

The escape hatch for a range the engine could not know had changed — an out-of-band restore, a corrected import — and the operation a backfill or a delete schedules automatically.

§Errors

Returns an error if name is not a rollup, or if the recomputation fails.

Source

pub fn rollup(&self, name: &str, start: i64, end: i64) -> Result<RecordBatch>

Read a rollup over [start, end] — the materialised buckets plus the buckets that are not yet final, aggregated on the fly from the source.

A materialised rollup lags the newest write by the out-of-order window, which is right for a three-year tier and wrong for a dashboard of the last hour. This is the real-time view: everything below the watermark comes from the target measurement, everything above it is computed from the raw data in the same pass, with the same accumulator, so the two halves cannot disagree. The live half is bounded by the window — at most a few shards of source data.

Returns one row per (bucket, tag group), ascending by bucket, with the target measurement’s columns (<field>_<agg>), or an empty batch with that schema.

§Errors

Returns an error if name is not a rollup, the database is closed, or the scan fails.

Source

pub fn rollup_where( &self, name: &str, start: i64, end: i64, tags: &[(&str, &str)], ) -> Result<RecordBatch>

rollup restricted to the series matching tags.

Every key must be one of the rollup’s group_by_tags, and anything else is refused by name. That is not a convenience check: a rollup’s target measurement carries only the tags it grouped by, so a filter on any other key would narrow the live half — read from the source, which still has every tag — and match nothing in the materialised half. The answer would be silently short on one side of the watermark and whole on the other, which is the worst shape a time-series answer can take.

Without this the caller reads every tag group and discards all but one. That is bounded for a handful of measurement points and is a whole-group-set materialisation for anyone with real cardinality.

§Errors

As rollup, plus DbError::InvalidRequest if a tag key is not one the rollup groups by.

Source

pub fn create_rollup(&self, config: RollupConfig) -> Result<()>

Register a rollup configuration.

§Errors

Returns an error if the database is closed or validation fails.

Source

pub fn rollup_state(&self, name: &str) -> Result<RollupState>

A rollup’s materialisation state: how far it has been aggregated, and which ranges below that are waiting to be recomputed because a later write changed their input.

§Errors

Returns an error if the database is closed.

Source

pub fn list_rollups(&self) -> Result<Vec<RollupConfig>>

List all registered rollup configurations.

§Errors

Returns an error if the database is closed.

Source

pub fn delete_rollup(&self, name: &str) -> Result<bool>

Remove a rollup configuration by name.

§Errors

Returns an error if the database is closed or the rollup doesn’t exist.

Source§

impl Chronix

Source

pub fn execute_iter(&self, plan: &QueryPlan) -> Result<BatchStream<'_>>

Execute a Scan plan as a lazily evaluated, bounded-memory iterator of RecordBatches.

Results are globally sorted by timestamp and deduplicated (last-write-wins), exactly as execute_stream returns them — but segments are read one time-disjoint bucket at a time, so peak memory is proportional to the busiest time bucket rather than to the whole result set. This is what makes export_parquet viable on a device whose RAM is smaller than its query window.

Scan and Limit(Scan) plans stream; a limit also terminates the scan early, so the buckets past it are never read. Aggregations, downsampling and window functions have to see their whole input, so pass those to execute or execute_stream.

§Errors

Returns an error if the database is closed or the plan is neither a Scan nor a Limit over one. I/O errors surface from the iterator, not from this call.

§Example
let plan = db.query().measurement("cpu").range(0, i64::MAX).build().unwrap();
let mut rows = 0;
for batch in db.execute_iter(&plan)? {
    rows += batch?.num_rows(); // each batch is dropped before the next is read
}
Source§

impl Chronix

Source

pub fn insert(&self, point: &Point) -> Result<()>

Insert a single data point.

Equivalent to insert_batch with one point, with a rejection reported as the error rather than as a partial result.

§Deduplication

Writes are not deduplicated. If the same (series, timestamp) is written twice, the later write wins at read time — on every read path — and compaction drops the older copy.

§Errors

Returns an error if the database is closed, the write is refused by admission control, the point’s timestamp is outside the out-of-order window, the series would exceed the cardinality budget, a field’s type conflicts with the schema, or the WAL append fails. In every case nothing was written.

Source

pub fn insert_batch(&self, points: &[Point]) -> Result<InsertResult>

Insert a batch of data points.

The accepted points are written to the WAL as one record and then inserted into the memtable; the rejected ones are reported in InsertResult::rejected with their reasons. This is significantly cheaper than calling insert in a loop — one fsync under FsyncPolicy::PerBatch instead of one per point.

§Errors

Returns an error — and writes nothing — if the database is closed, admission control refuses the write, the batch would exceed the cardinality budget, any field’s type conflicts with the schema, or the WAL append fails. Under FsyncPolicy::Periodic “written” means in the operating system’s buffers; the periodic sync makes it durable.

Source

pub fn backfill(&self, points: &[Point]) -> Result<InsertResult>

Insert points outside the out-of-order window.

Live ingestion is held to ±ooo_shard_tolerance shards of the newest write, which is what keeps segments time-partitioned and the number of open memtables bounded. Importing history — a migration, a device’s offline backlog, a rollup being materialised for a closed window — is the one legitimate reason to write far into the past, and it is a different operation with a different cost: the shards it touches are opened, flushed and later compacted like any other, and a query over that time range merges the new segments with the old.

Everything else — admission control, the cardinality budget, the schema, one WAL record per call — is exactly as for insert_batch, and so is the result: the only per-point rejection here is a timestamp beyond future_write_tolerance, and it is reported, not swallowed.

Backfilling a rollup’s source below its watermark marks those buckets for re-materialisation; see materialise_rollups.

§Errors

As insert_batch.

Source

pub fn declare_field( &self, measurement: &str, field: &str, column_type: ColumnType, ) -> Result<()>

Declare a field column before anything is written to it.

Chronix is schema-on-write: a measurement’s columns appear as the first point that carries them is written, and for a float or a counter that is all anyone needs. A decimal column is the exception, because its scale is part of its type and is fixed by whatever creates the column. Letting the first meter reading decide how many fractional digits a settlement register keeps is a coin toss: a device that happens to report 231.4 first pins the column at one digit, and 231.45 is then refused for ever.

Declaring it makes that a decision:

// A quarter-hour register under BK 618-25-02, in kWh to four places.
db.declare_field("meter", "z1nb_q", ColumnType::Decimal { scale: 4 })?;

The measurement is created if it does not exist. Declaring a column that already exists with exactly this type is a no-op; declaring one that exists with a different type — including a different decimal scale — is an error, because that is a type change and Chronix does not have those.

§Errors

Returns an error if the database is closed, the column exists with a different type, the name is invalid or reserved, or the catalog cannot be written.

Source

pub fn scan_memtable( &self, series_key: &SeriesKey, min_ts: Timestamp, max_ts: Timestamp, ) -> Vec<Point>

Scan the memtable for points in the given time range.

This is a low-level method that only reads from the in-memory buffers, not from persisted segments. For full queries, use execute.

Source§

impl Chronix

Source

pub fn open_small(data_dir: impl Into<PathBuf>) -> Result<Self>

Open (or create) a database with the small-footprint preset — see ChronixConfig::small for the exact budgets (≈ 48 MB memory, flash-friendly periodic WAL fsync, single compaction worker).

let db = Chronix::open_small("/var/lib/chronix").unwrap();
§Errors

Same failure modes as Chronix::open.

Source

pub fn open(config: ChronixConfig) -> Result<Self>

Open (or create) a Chronix database at the configured data_dir.

Acquires an exclusive file lock to prevent multiple processes from opening the same database directory.

§Errors

Returns an error if the lock cannot be acquired, the directory is unreadable, or WAL replay fails.

Trait Implementations§

Source§

impl Clone for Chronix

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Chronix

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Deref for Chronix

Source§

type Target = DbInner

The resulting type after dereferencing.
Source§

fn deref(&self) -> &DbInner

Dereferences the value.
Source§

impl Drop for Chronix

Source§

fn drop(&mut self)

Executes the destructor for this type. Read more
Source§

fn pin_drop(self: Pin<&mut Self>)

🔬This is a nightly-only experimental API. (pin_ergonomics)
Execute the destructor for this type, but different to Drop::drop, it requires self to be pinned. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<P, T> Receiver for P
where P: Deref<Target = T> + ?Sized, T: ?Sized,

Source§

type Target = T

🔬This is a nightly-only experimental API. (arbitrary_self_types)
The target type on which the method may be called.
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more