C ABI
infrastore-ffi compiles a cdylib (libinfrastore_ffi.{dylib,so,dll}) exposing a C-compatible
API over Store. The header crates/infrastore-ffi/include/infrastore.h is generated by cbindgen —
do not hand-edit it. The Julia binding is the primary consumer.
Conventions
- Return codes. Every function returns
int32_t.INFRASTORE_OK(0) is success; any other value is an error. Retrieve the human-readable detail withinfrastore_last_error_message. - Opaque handles.
InfraStore,InfraStoreBulkRead, the readers and the batch are incomplete struct types; you only ever hold pointers. Free them withinfrastore_store_free. - Out-parameters. Results are written through caller-provided pointers (
**out,*out_len, …). - Caller-owned buffers. Returned arrays come back through an out-pointer + length and must be
freed:
f64percentile buffers (double **) withinfrastore_buffer_free_f64, raw element-byte buffers (uint8_t **) withinfrastore_buffer_free_u8, timestamp and shape buffers (int64_t **) withinfrastore_buffer_free_i64, and theu64dims buffer frominfrastore_bulk_result_get_forecast(uint64_t **) withinfrastore_buffer_free_u64. - Typed arrays. Add functions take an
element_typestring (a dtype spelling such as"f64", or a composite kind such as"tuple(3,f64)"/"piecewise_linear"— see Element types),ndimsplus adims_ptrshape array ([length, k1, …]), and the raw little-endiandata_ptrofdata_byte_lenbytes. The physical dtype the bytes are encoded in is derived from the element type. Reads return the dtype code (0=f64, 1=f32, 2=i64, 3=i32, 4=u64, 5=bool, 6=i16, 7=i8, 8=u32, 9=u16, 10=u8) and a raw byte buffer for the caller to decode, plus an optionalout_element_typestring saying what those bytes mean;infrastore_bulk_result_get_singlefollows the same dtype-generic convention. owner_categoryis anint32_t(0 = Component,1 = SupplementalAttribute) passed to the add functions. The owner identity is the pair(owner_id, owner_category)— a component and a supplemental attribute may share a numericowner_idand stay distinct — and the category is recorded with the association at add time.application_datais an optional opaque, package-owned payload (typically JSON) passed verbatim to the add functions and stored uninterpreted. Element typing does not go here — that iselement_type.- Strings are null-terminated UTF-8. Optional string arguments (
application_data,features_json,units,quantity_kind,unit_system,time_reference,component_field,name_glob) acceptNULL. time_referencerecords how a series' timestamps were spelled:"utc","zoneless", a fixed offset ("-07:00"), or an IANA zone name ("America/Denver"). An unparseable value fails withINFRASTORE_ERR_INVALID_PARAMETERrather than degrading to unset. Zone existence is not checked here — the caller's own tz database is the one that should warn. Timestamps stay Unix milliseconds either way, sotime_range_zonelessis what tells a wall-clock bound from an instant one, and the store refuses a bound whose spelling the series cannot answer rather than coercing it. Thezonelessfilter argument on the list/filter exports is a tri-stateint32_t: negative means no filter,0selects the instant-bearing rows (including those with no reference), and1selects the wall-clock rows. See Time references.- Features are passed as a JSON object string whose values are int / float / bool / string. An
add call whose feature names shadow a time-series or key field (
name,resolution,owner_id, …) fails withINFRASTORE_ERR_INVALID_PARAMETER; see reserved feature names. - Timestamps are
int64_tUnix milliseconds. Resolutions/horizons/intervals are ISO-8601 duration strings (e.g."PT1H","P1M","P1Y"); aNULL(or empty) string means unset. On output they are ownedchar *strings — free each withinfrastore_string_free.
Status Codes
| Macro | Value | Meaning |
|---|---|---|
INFRASTORE_OK | 0 | Success |
INFRASTORE_ERR_NULL_POINTER | 1 | A required pointer was NULL |
INFRASTORE_ERR_INVALID_UTF8 | 2 | A string argument was not UTF-8 |
INFRASTORE_ERR_INVALID_PARAMETER | 3 | A bad argument value |
INFRASTORE_ERR_NOT_FOUND | 4 | No matching series / array |
INFRASTORE_ERR_DUPLICATE | 5 | Key already exists |
INFRASTORE_ERR_INTEGRITY | 6 | On-disk inconsistency |
INFRASTORE_ERR_READ_ONLY | 7 | Write on a read-only store |
INFRASTORE_ERR_IO | 8 | I/O failure |
INFRASTORE_ERR_INCOMPATIBLE_FORMAT | 9 | The store on disk was written in a different, incompatible on-disk format than this build reads. There is no in-place upgrade. |
INFRASTORE_ERR_DUPLICATE_ASSOCIATION | 10 | An attachment or parent/child edge with the same identity already exists. Distinct from INFRASTORE_ERR_DUPLICATE, which is about time-series identity. |
INFRASTORE_ERR_STORE_EXISTS | 11 | A store already exists where one was about to be created. Creating there would discard its arrays while keeping its catalog. Use infrastore_store_create_replacing to discard it on purpose. |
INFRASTORE_ERR_MISMATCHED_ARTIFACT | 12 | The HDF5 file and its .sqlite catalog do not carry the same generation stamp: they are halves of two different saves. |
INFRASTORE_ERR_DUPLICATE_ASSOCIATION_ID | 13 | An import supplied an association_id a live row already holds. Distinct from INFRASTORE_ERR_DUPLICATE_ASSOCIATION (the endpoint pair collides) and INFRASTORE_ERR_DUPLICATE (a series' identity collides). |
INFRASTORE_ERR_OWNER_MISMATCH | 14 | An id-addressed call carrying an expected owner named a row that belongs to a different one. Distinct from INFRASTORE_ERR_NOT_FOUND: the row is there, and it is the caller's belief about who owns it that is stale. |
INFRASTORE_ERR_CATALOG_MIGRATION_REQUIRED | 15 | The .sqlite catalog is at an older schema revision than this build and the store was opened read-only, so nothing could migrate it. Open it once for writing (or run infrastore upgrade) and the ladder runs. |
INFRASTORE_ERR_CATALOG_TOO_NEW | 16 | The .sqlite catalog was written by a newer infrastore than this build understands. The mirror of INFRASTORE_ERR_CATALOG_MIGRATION_REQUIRED, with the other remedy: upgrade the software, not the store. |
INFRASTORE_ERR_INTERNAL | 99 | Unexpected internal error |
Lifecycle
/* compression_kind: 0 = none, 1 = DEFLATE (deflate_level 0-9 + shuffle); 1, 3, true is the
default policy. catalog_mode: 0 = attached (<path>.sqlite), 1 = in memory (written only by
infrastore_store_persist). in_memory=true admits only catalog_mode=1. */
int32_t infrastore_store_create_with_catalog(const char *path, bool in_memory, uint8_t compression_kind,
uint8_t deflate_level, bool shuffle, uint8_t catalog_mode,
struct InfraStore **out);
int32_t infrastore_store_open_with_catalog(const char *path, bool read_only, uint8_t catalog_mode,
struct InfraStore **out);
/* Open the array half of an artifact whose catalog is ABSENT, minting an empty one stamped to
match the arrays, and hand back a writable store holding every array and no rows — the way in
to a store shipped as arrays plus an OpenAPI document. Replay the rows with
infrastore_store_import_time_series_associations_openapi and its supplemental-attribute
counterpart. INFRASTORE_ERR_STORE_EXISTS when <path>.sqlite is already there; that store wants
infrastore_store_open_with_catalog. Never read-only. */
int32_t infrastore_store_open_without_catalog(const char *path, uint8_t catalog_mode,
struct InfraStore **out);
/* The create entry points above fail with INFRASTORE_ERR_STORE_EXISTS when either half of a
store is already at `path`. This one discards it first — both halves plus the catalog's
-wal/-shm sidecars — and is destructive and not atomic. */
int32_t infrastore_store_create_replacing(const char *path, uint8_t compression_kind,
uint8_t deflate_level, bool shuffle, uint8_t catalog_mode,
struct InfraStore **out);
/* Copy both halves of the store at `src` to `dest` and open the copy read-write, leaving `src`
untouched. The safe way to load a store you intend to change: mutating an artifact in place is
unrecoverable if interrupted, since HDF5 has no journal. INFRASTORE_ERR_STORE_EXISTS if `dest`
already holds a store. */
int32_t infrastore_store_open_copy(const char *src, const char *dest, uint8_t catalog_mode,
struct InfraStore **out);
int32_t infrastore_store_catalog_mode(const struct InfraStore *handle, uint8_t *out);
void infrastore_store_free(struct InfraStore *handle);
/* Frees any owned `char *` this library returns (resolutions, horizons, intervals, …). */
void infrastore_string_free(char *s);
void infrastore_buffer_free_f64(double *ptr, uint64_t len);
void infrastore_buffer_free_u8(uint8_t *ptr, uint64_t len);
void infrastore_buffer_free_i64(int64_t *ptr, uint64_t len);
void infrastore_buffer_free_u64(uint64_t *ptr, uint64_t len);
SingleTimeSeries
There is no key on this ABI. A series is addressed by its catalog association id, an int64_t
that every write reports through infrastore_store_add_batch's out_ids and that
infrastore_store_list_metadata carries on every row. Reads take that id and come back through the
shared bulk-result handle, so one decoding path serves the single and bulk reads alike; see
Bulk Reads.
Writes go through a batch. There is one write path on this ABI: build a batch with
infrastore_batch_new, append to it, then commit it with infrastore_store_add_batch, which hands
back one id per item in batch order. A single add is a one-item batch, which costs one extra call
and keeps a second, near-identical set of per-type entry points off the surface. See
Batch Writes.
int32_t infrastore_batch_add_single(struct InfraStoreBatch *batch,
int64_t owner_id, const char *owner_type,
int32_t owner_category, /* 0=Component, 1=SupplementalAttribute */
const char *name,
int64_t initial_ts_unix_ms, const char *resolution, /* ISO-8601 */
const char *element_type, /* e.g. "f64", "piecewise_linear" */
uint64_t ndims, const uint64_t *dims_ptr,
const uint8_t *data_ptr, uint64_t data_byte_len,
const char *application_data, /* optional */
const char *features_json, /* optional */
const char *units, /* optional */
const char *quantity_kind, /* optional */
const char *unit_system, /* optional: "natural_units" | "component_base" */
const char *time_reference, /* optional: "utc" | "zoneless" | "-07:00" | IANA name */
const char *component_field); /* optional: e.g. "max_active_power" */
/* All-or-nothing removal by catalog association id: an id naming no row returns
INFRASTORE_ERR_NOT_FOUND and removes nothing. A repeated id is removed, and
counted, once. `ids` may be null only when `n` is 0.
Set has_owner to hold every id to (owner_id, owner_category): the owner is read
and the row deleted by the same transaction, and a row belonging to anyone else
is INFRASTORE_ERR_OWNER_MISMATCH with the whole batch rolled back. */
int32_t infrastore_store_remove_by_ids(struct InfraStore *handle, const int64_t *ids,
uint64_t n,
bool has_owner, int64_t owner_id, int32_t owner_category,
uint64_t *out_removed);
/* Whether an association is filed under `id` -- a primary-key probe that fetches
no row, so a consumer can validate every reference in its own model on load
rather than discovering a dangling one mid-run. Never NOT_FOUND: a stale
reference is the answer, not an error. */
int32_t infrastore_store_association_exists(const struct InfraStore *handle,
int64_t association_id, bool *out_present);
NonSequentialTimeSeries
infrastore_batch_add_non_sequential takes an explicit int64_t Unix-millisecond timestamp array
alongside the typed data buffer. It is read like every other type — by id, through
infrastore_store_read_by_id and then infrastore_bulk_result_get_irregular, which returns owned
timestamp, shape, and raw-byte buffers (free with infrastore_buffer_free_i64,
infrastore_buffer_free_i64, and infrastore_buffer_free_u8) plus the dtype code and, in
out_element_type, the canonical element-type string. The shape is the full
[length, *element_shape] array shape (the first dim is time, so callers can recover an
N-dimensional per-step element shape). out_application_data is the optional opaque package-owned
payload, returned as an owned C string of its full length (NULL when unset; free with
infrastore_string_free). Earlier revisions copied it into a caller-sized buffer, which invited
silent truncation.
int32_t infrastore_batch_add_non_sequential(struct InfraStoreBatch *batch,
int64_t owner_id, const char *owner_type,
int32_t owner_category, const char *name,
const int64_t *timestamps_unix_ms, uint64_t timestamps_len,
const char *element_type,
uint64_t ndims, const uint64_t *dims_ptr,
const uint8_t *data_ptr, uint64_t data_byte_len,
const char *application_data, const char *features_json,
const char *units,
const char *quantity_kind, const char *unit_system,
const char *time_reference,
const char *component_field);
/* Reads a NonSequentialTimeSeries or PersistentTimeSeries slot; any other type is
INFRASTORE_ERR_INVALID_PARAMETER. */
int32_t infrastore_bulk_result_get_irregular(const struct InfraStoreBulkReadHandle *result, uint64_t index,
int64_t **out_timestamps, uint64_t *out_timestamps_len,
int32_t *out_dtype,
int64_t **out_shape, uint64_t *out_shape_len,
uint8_t **out_data, uint64_t *out_data_byte_len,
char **out_application_data, char **out_element_type,
char **out_units, char **out_quantity_kind,
char **out_unit_system, char **out_time_reference,
char **out_component_field);
PersistentTimeSeries
infrastore_batch_add_persistent takes exactly the argument list of
infrastore_batch_add_non_sequential above — the same int64_t Unix-millisecond vector, the same
owned buffers, the same ownership and free rules. The two types carry the same payload; timestamps
is the breakpoint vector, and what differs is what a read between those instants means.
int32_t infrastore_batch_add_persistent(struct InfraStoreBatch *batch, /* ...as add_non_sequential... */);
It is read like every other type — by id, through infrastore_store_read_by_id /
infrastore_store_read_by_ids, then infrastore_bulk_result_get_irregular on the slot whose
infrastore_bulk_result_item_type is 6.
The value at out_timestamps[i] is in force from that instant until the next breakpoint, and past
the last one forever. There is no value before the first breakpoint: a range read whose start
precedes it is refused with INFRASTORE_ERR_INVALID_PARAMETER rather than clamped, and a range that
starts mid-step begins at the breakpoint in force there, so the returned slice always defines a
value at the caller's start.
The ABI discriminant is 6, kept numerically equal to the storage code. See the
time-series types.
Attribute-Based Existence
An existence probe stays attribute-addressed: it is answered off the catalog indexes without hydrating a row, so routing it through an id lookup would cost more than the question. There is one of them, taking the filter record every other filter-taking export takes.
Set features_exact to compare features_json as the row's whole feature set — that is a single
content-hash comparison. The default subset match adds an indexed probe per requested feature.
Neither hydrates a row, but a hot loop testing a complete feature set wants the exact form.
/* True iff any association matches the filter. A NULL filter matches everything. */
int32_t infrastore_store_has_any_by_filter(const struct InfraStore *handle,
const struct InfraStoreFilter *filter,
bool *out_present);
int32_t infrastore_store_get_array_by_hash(const struct InfraStore *handle, const uint8_t *data_hash,
int32_t *out_dtype,
uint8_t **out_data, uint64_t *out_byte_len); /* infrastore_buffer_free_u8 */
/* Count SingleTimeSeries (*out_sts) and DeterministicSingleTimeSeries (*out_dst)
associations that reference the 32-byte content hash data_hash, across all
owners — one catalog query to decide whether removing a SingleTimeSeries would
orphan a DST that shares its backing array. */
int32_t infrastore_store_count_array_references(const struct InfraStore *handle, const uint8_t *data_hash,
uint64_t *out_sts, uint64_t *out_dst);
/* The whole metadata record for one association id, as a JSON object with the
shape of one infrastore_store_list_metadata element: owner_id, owner_type,
owner_category, time_series_type, name, data_hash (64-char hex),
initial_timestamp_ms, resolution, horizon, interval, count, length,
percentiles, element_type, element_shape, features, units, quantity_kind,
unit_system, time_reference, component_field, application_data, id -- fields
that do not apply to the row's type are null. One export covers every time
series type, static and forecast alike. Probe-then-fetch: call with
buf = NULL, cap = 0 to learn *out_len, then again with an out_len+1-byte
buffer. *out_present is false when the id names no row -- a stale reference is
an answer, not an error. */
int32_t infrastore_store_get_metadata_by_id(const struct InfraStore *handle,
int64_t association_id,
char *buf, uint64_t cap, uint64_t *out_len,
bool *out_present);
/* The same listing addressed by a set of ids, in the order given, as an owned
JSON array (free with infrastore_string_free). INFRASTORE_ERR_NOT_FOUND if any
id names no row: a caller naming ids is asserting they exist, and a silently
short array would let a stale reference pass as an absent match. */
int32_t infrastore_store_list_metadata_by_ids(const struct InfraStore *handle,
const int64_t *ids, uint64_t n,
char **out_json, uint64_t *out_len);
infrastore_store_list_metadata is the identify half: it answers which series exist and carries the
id that addresses each. infrastore_store_get_metadata_by_id +
infrastore_store_get_array_by_hash is the direct-array read path for a binding that maintains its
own object model, and infrastore_store_association_exists answers whether a reference it recorded
earlier still resolves without fetching the row. There is deliberately no attribute-to-id resolver:
a caller that wants exactly one row poses the filter and checks that it got one.
Forecasts
The forecast types are created and read through the C ABI. ts_type is the TimeSeriesType
discriminant — 0 = SingleTimeSeries, 1 = NonSequentialTimeSeries, 2 = Deterministic,
3 = DeterministicSingleTimeSeries, 4 = Probabilistic, 5 = Scenarios,
6 = PersistentTimeSeries. The ABI keeps its own mapping, deliberately identical to the storage
codes. As a filter it is read per Type filters below. Forecast values are
dtype-generic raw little-endian byte buffers with explicit dimensions — the same element_type,
ndims, dims_ptr, data_ptr, data_byte_len convention as the static batch adds (see the
data model for the conventional shapes); the store
records the windowing parameters in metadata and does not interpret the layout. A
DeterministicSingleTimeSeries (3) is read like any other forecast but cannot be written through
infrastore_batch_add_forecast — it is derived via infrastore_store_transform_single_time_series.
Forecast writes go through a batch like every other write. infrastore_batch_add_forecast accepts
only ts_type 2 = Deterministic or 5 = Scenarios; infrastore_batch_add_probabilistic adds the
percentile vector for Probabilistic. DeterministicSingleTimeSeries (3) is not addable
through infrastore_batch_add_forecast — it errors and directs you to
infrastore_store_transform_single_time_series, which derives a DeterministicSingleTimeSeries
from every stored SingleTimeSeries (sharing the backing array) and writes the number transformed
to *out_count:
int32_t infrastore_batch_add_forecast(struct InfraStoreBatch *batch,
int64_t owner_id, const char *owner_type, int32_t owner_category,
const char *name, int32_t ts_type,
int64_t initial_ts_unix_ms,
const char *resolution, const char *horizon, const char *interval, /* ISO-8601 */
uint64_t count,
const char *element_type,
uint64_t ndims, const uint64_t *dims_ptr,
const uint8_t *data_ptr, uint64_t data_byte_len,
const char *application_data, /* optional */
const char *features_json, const char *units,
const char *quantity_kind, /* optional */
const char *unit_system, /* optional: "natural_units" | "component_base" */
const char *time_reference, /* optional: "utc" | "zoneless" | "-07:00" | IANA name */
const char *component_field); /* optional: e.g. "max_active_power" */
int32_t infrastore_batch_add_probabilistic(struct InfraStoreBatch *batch,
int64_t owner_id, const char *owner_type,
int32_t owner_category, const char *name,
int64_t initial_ts_unix_ms,
const char *resolution, const char *horizon, const char *interval, /* ISO-8601 */
uint64_t count,
const double *percentiles_ptr, uint64_t percentiles_len,
const char *element_type,
uint64_t ndims, const uint64_t *dims_ptr,
const uint8_t *data_ptr, uint64_t data_byte_len,
const char *application_data, /* optional */
const char *features_json, const char *units,
const char *quantity_kind, /* optional */
const char *unit_system, /* optional: "natural_units" | "component_base" */
const char *time_reference, /* optional: "utc" | "zoneless" | "-07:00" | IANA name */
const char *component_field); /* optional: e.g. "max_active_power" */
int32_t infrastore_store_transform_single_time_series(struct InfraStore *handle,
const char *horizon, const char *interval, /* ISO-8601 */
int32_t owner_category, /* <0 = all categories; else 0=Component, 1=SupplementalAttribute */
const char *resolution, /* NULL = all resolutions */
bool normalize_single_window, /* store the zero interval for a single window */
bool require_uniform_forecast_grid, /* every resolution must agree on count + initial_timestamp */
bool dry_run, /* check only; legal on a read-only store */
uint64_t *out_count,
uint64_t *out_sources, /* optional (NULL skips): matched before idempotent skips */
char *out_interval, /* optional (NULL skips): ISO-8601, INTERVAL_BUF_LEN bytes */
bool *out_interval_normalized, /* optional (NULL skips) */
int64_t **out_ids); /* optional (NULL skips): infrastore_buffer_free_i64 */
Every writer takes an optional out_id, which receives the catalog id the row was filed under; pass
NULL to skip it. No writer takes an id as input: the catalog always assigns. The one entry point
that files rows under ids a caller supplies is
infrastore_store_import_time_series_associations_openapi, replaying a document that already
recorded them — see Association ids.
infrastore_store_transform_single_time_series reports the rest of its outcome through four
optional out-parameters, each skippable with NULL: out_sources is how many SingleTimeSeries
matched before idempotent skips, so zero distinguishes "nothing to transform" from "everything was
already derived"; out_interval receives the ISO-8601 interval actually stored, NUL-terminated (an
ISO period is bounded, so this takes a fixed INTERVAL_BUF_LEN-byte buffer rather than a probe
pass); out_interval_normalized is non-zero when the request described a single window; and
out_ids receives the id of each view written, *out_count of them in write order, to free with
infrastore_buffer_free_i64. When nothing was written — a dry run, or a re-run that finds every
view already derived — *out_ids is set to NULL. On a dry run *out_count still reports the
count a committing run would produce, so a caller must check the pointer rather than the count
before indexing it.
Its two policy flags are the TransformPolicy of the core API. Both false is the permissive
default; InfrastructureSystems.jl passes both true. normalize_single_window selects the
single-window encoding — non-zero stores the zero interval, which is what InfrastructureSystems.jl
looks up by, and zero stores the requested interval verbatim. require_uniform_forecast_grid
requires every resolution in scope, plus any forecast already stored at the same
(resolution, interval), to agree on the derived window count and initial_timestamp. dry_run
runs every check and reports what a committing run would produce without writing, and is legal
against a read-only store.
Forecasts are read like everything else: by id, through infrastore_store_read_by_id (or
infrastore_store_read_by_ids / ..._range) and then infrastore_bulk_result_get_forecast, which
returns the decoded data buffer, its out-dimensions, the window parameters, and — for
Probabilistic — the percentile vector. A stored DeterministicSingleTimeSeries is synthesized
into Deterministic values (its dense windows are materialized from the backing
SingleTimeSeries), but it remains a distinct stored type in the catalog.
Type filters
A read names only an id, so there is no requested type to disagree with what is stored — the
family-resolution rules that a ts_type argument used to carry now live entirely in the identify
half. infrastore_store_list_metadata's type filter reads one the same way:
2 = Deterministicmatches a storedDeterministicor a storedDeterministicSingleTimeSeries. A DST is a synthetic view that reads back as aDeterministic, so a caller selects a deterministic forecast without knowing which form the store holds. Each row still reports its own concretetime_series_type.3 = DeterministicSingleTimeSeriesnarrows to the derived form alone — for callers auditing which forecasts are synthetic rather than reading values.4 = Probabilisticand5 = Scenariosmatch only themselves.
The caller owns the returned buffers: free *out_data with infrastore_buffer_free_u8, *out_dims
with infrastore_buffer_free_u64, *out_percentiles (non-NULL only for Probabilistic) with
infrastore_buffer_free_f64, and each of the *out_resolution / *out_horizon / *out_interval
ISO-8601 strings with infrastore_string_free.
Forecast metadata is read with the same infrastore_store_get_metadata_by_id as everything else.
The returned row carries the windowing parameters (horizon, interval, count), the content
hash, and the percentiles of a Probabilistic, without decoding the array.
infrastore_store_copy_time_series copies one association onto another owner (optionally under a
new name). Arrays are content-addressed, so only a new association row is written — no array data is
duplicated, and the stored type is preserved (a DeterministicSingleTimeSeries stays one rather
than being materialized into a dense Deterministic). The copy keeps the source's owner category.
The leading owner_id / owner_category / name / ts_type / resolution / interval /
features_json arguments identify the source series; a NULL new_name keeps the source name.
int32_t infrastore_store_copy_time_series(struct InfraStore *handle,
/* source series: */
int64_t owner_id, int32_t owner_category, const char *name,
int32_t ts_type,
const char *resolution, const char *interval, /* ISO-8601; NULL = unset */
const char *features_json,
/* destination: */
int64_t dst_owner_id, const char *dst_owner_type,
const char *new_name); /* NULL = keep the source name */
Grid Arithmetic
Stateless entry points into the core's own period arithmetic, for a binding that holds a series as a native struct and would otherwise reimplement the grid.
int32_t infrastore_grid_timestamps(int64_t initial_unix_ms,
const char *resolution_iso,
uint64_t length,
int64_t *buf,
uint64_t cap,
uint64_t *out_len);
int32_t infrastore_infer_period(const int64_t *timestamps_unix_ms,
uint64_t len,
char **out_iso);
infrastore_grid_timestamps materializes initial + k · resolution for k in [0, length),
probe-then-fetch like infrastore_static_reader_timestamps (call with buf null and cap 0 to
learn the length). It is calendar-aware for a P1M/P1Y resolution, which steps the UTC
calendar — the reference a series records is a spelling, not a grid.
infrastore_infer_period is the inverse: the ISO-8601 period that reproduces a timeline exactly, or
INFRASTORE_ERR_INVALID_PARAMETER with a message naming the entry that breaks the pattern and
NonSequentialTimeSeries as the remedy. out_iso is an owned string the caller frees with
infrastore_string_free. A local-clock daily or monthly grid in a DST zone is refused here, which
is what lets a caller hand over the timeline they have instead of asserting a resolution the store
cannot check.
There is exactly one implementation of "which instants does this series contain" in the project, and
it is the core's Period::add_to. These two functions are how a binding reaches it.
Readers
The per-timestamp read path is exposed as two opaque reader handles — InfraStoreStaticReaderHandle
for SingleTimeSeries and InfraStoreForecastReaderHandle for forecasts. A reader is built once
over a filter (the same has_owner / owner_id / has_owner_category / owner_category / name
/ resolution / features_json convention as the attribute-based access, with a forecast reader
also taking a ts_type), then driven per timestamp. The lifecycle is: build → read the layout
once → *_read in a loop → fetch values per group/entry → free. Each reader pins one resolution
and owns reusable buffers that each read overwrites in place.
Ownership rules: the *_grid / *_timeline resolution/interval out-strings (char **) are owned —
free each with infrastore_string_free. *_group_id / *_entry_id write a plain int64_t catalog
id, so nothing is allocated and nothing needs freeing — resolve one with
infrastore_store_get_metadata_by_id to recover the series a column or entry came from. The
*_values buffers (const uint8_t **) are borrowed: they point into reader memory, stay valid
only until the next read or *_free, and must not be freed. Group/entry shapes follow the
probe-then-fetch convention — call *_info with shape_buf = NULL / shape_cap = 0 to learn
*out_shape_len, then again with a buffer of that length. *_read errors (never clamps) if
at_unix_ms is off the reader's grid/timeline.
StaticReader
Reads every matching static series at one timestamp, partitioned into (dtype, element_shape)
groups; each group's values are one dense [num_columns, *element_shape] little-endian buffer whose
column j is the key from infrastore_static_reader_group_key(reader, group_idx, j, …).
int32_t infrastore_store_build_static_reader(const struct InfraStore *handle,
int32_t time_series_type,
const struct InfraStoreFilter *filter,
bool has_window_start, int64_t window_start_ms,
bool window_start_zoneless,
bool has_window_length, uint64_t window_length,
struct InfraStoreStaticReaderHandle **out_reader);
int32_t infrastore_static_reader_grid(const struct InfraStoreStaticReaderHandle *reader,
int64_t *out_initial_ms, char **out_resolution, /* null, or free with infrastore_string_free */
uint64_t *out_length);
int32_t infrastore_static_reader_time_reference(const struct InfraStoreStaticReaderHandle *reader,
char **out_time_reference); /* null, or free with infrastore_string_free */
int32_t infrastore_static_reader_timestamps(const struct InfraStoreStaticReaderHandle *reader,
int64_t *buf, uint64_t cap, uint64_t *out_len); /* probe with buf=NULL, cap=0 */
int32_t infrastore_static_reader_num_groups(const struct InfraStoreStaticReaderHandle *reader, uint64_t *out_n);
int32_t infrastore_static_reader_group_info(const struct InfraStoreStaticReaderHandle *reader, uint64_t group_idx,
int32_t *out_dtype, uint64_t *out_num_columns,
int64_t *shape_buf, uint64_t shape_cap, uint64_t *out_shape_len);
int32_t infrastore_static_reader_group_id(const struct InfraStoreStaticReaderHandle *reader,
uint64_t group_idx, uint64_t col_idx,
int64_t *out_id); /* the column's catalog id */
int32_t infrastore_static_reader_read(struct InfraStoreStaticReaderHandle *reader,
const struct InfraStore *store, int64_t at_unix_ms);
int32_t infrastore_static_reader_invalidate(struct InfraStoreStaticReaderHandle *reader);
/* empty every group; for a caller refusing a read of its own */
int32_t infrastore_static_reader_group_values(const struct InfraStoreStaticReaderHandle *reader, uint64_t group_idx,
const uint8_t **out_ptr, /* borrowed; valid until next read/free */
uint64_t *out_byte_len);
void infrastore_static_reader_free(struct InfraStoreStaticReaderHandle *reader);
has_window_start lifts the shared-grid requirement for SingleTimeSeries: the reader then sweeps
the window window_start_ms (plus window_length steps, or as far as every matched series
reaches when has_window_length is false) rather than the grid the series happen to share, and each
column reads at an offset of its own. It is checked, never clamped — a matched series that does not
cover the window is INFRASTORE_ERR_INVALID_PARAMETER with a message naming it, the anchor must
fall on each series' own step boundaries, and a calendar resolution is refused where re-anchoring
would move the dates. window_start_zoneless carries how the caller spelled the anchor, the same
convention as the ranged reads' bounds: the wire form is Unix milliseconds either way, and the flag
is what tells a wall clock from an instant. The window belongs to SingleTimeSeries alone, and
has_window_length without has_window_start is an error.
The filter record's has_initial_timestamp / has_length are something else: the grid filter,
which every filter-taking export carries. They match only the series already on that grid, so the
ones that are not never become columns — where a window sweeps a named span across whatever matched.
Use the window when the ragged series should all take part, the filter when they should not; they
compose. The filter carries no spelling flag because it selects rather than reads: a grid no row is
on is an empty result, not an error.
time_series_type is the reader's own argument rather than the filter's — a reader is built for one
type, so a record that also sets has_time_series_type must name the same type or the build is
INFRASTORE_ERR_INVALID_PARAMETER — and picks the three shapes a reader can take. The rest of the
record is infrastore_store_list_metadata's in full; a static series stores no interval, so a
filter that sets one matches nothing on a static reader. For SingleTimeSeries (0), the filter's
resolution must be a non-empty ISO-8601 period — one resolution per reader — and all matched
series must share one grid (initial_timestamp + length). For NonSequentialTimeSeries (1),
resolution must be null (an irregular series has none) and all matched series must instead
share one timestamp vector; infrastore_static_reader_grid then reports *out_resolution as null,
and infrastore_static_reader_timestamps is how the timeline is read. For PersistentTimeSeries
(6), resolution must likewise be null — but this is the one case whose columns need not
share a timeline: a step function has a value at every instant from its first breakpoint on, so each
column carries its values forward on breakpoints of its own, and the reader's timeline is the union
of them all. Reading at an instant before some column's first breakpoint is an error naming that
column. Any other discriminant is rejected.
Uniformity — where it is required — is validated at build and errors on divergence, so in every case each column has a value at every valid timestamp (no presence mask).
infrastore_static_reader_time_reference reports the one spelling the axis carries — "utc",
"zoneless", a fixed offset such as "-07:00", or an IANA zone name — as an owned string the
caller frees with infrastore_string_free. It is null when the cohort records no spelling,
which is not the same claim as "zoneless": the second says the timestamps are wall clocks. A
cohort whose columns agree reports their reference; one whose columns merely agree on naming
instants reports "utc"; one mixing zoneless with the rest never builds at all. Reading the axis
without it leaves a C consumer unable to tell a wall-clock timeline from an unspecified or a UTC
one. infrastore_forecast_reader_time_reference is the window-timeline counterpart, on the same
terms.
ForecastReader
Reads the forecast window at one timestamp for every matching forecast of one type. The build
ts_type names the forecast type; a Deterministic reader (2) also includes
DeterministicSingleTimeSeries (3), read into identical [H, *E] windows — the same rule
infrastore_store_list_metadata's type filter applies. All matched forecasts must share one window
timeline (initial_timestamp + interval + count). Each entry's window is a little-endian buffer
of its *_entry_info shape.
int32_t infrastore_store_build_forecast_reader(const struct InfraStore *handle,
int32_t time_series_type,
const struct InfraStoreFilter *filter,
struct InfraStoreForecastReaderHandle **out_reader);
int32_t infrastore_forecast_reader_timeline(const struct InfraStoreForecastReaderHandle *reader,
int64_t *out_initial_ms,
char **out_resolution, char **out_interval, /* free each with infrastore_string_free */
uint64_t *out_count);
int32_t infrastore_forecast_reader_time_reference(const struct InfraStoreForecastReaderHandle *reader,
char **out_time_reference); /* null, or free with infrastore_string_free */
int32_t infrastore_forecast_reader_num_entries(const struct InfraStoreForecastReaderHandle *reader, uint64_t *out_n);
int32_t infrastore_forecast_reader_num_slots(const struct InfraStoreForecastReaderHandle *reader, uint64_t *out_n);
int32_t infrastore_forecast_reader_entry_slot(const struct InfraStoreForecastReaderHandle *reader,
uint64_t entry_idx, uint64_t *out_slot);
int32_t infrastore_forecast_reader_entry_info(const struct InfraStoreForecastReaderHandle *reader, uint64_t entry_idx,
int32_t *out_dtype,
int64_t *shape_buf, uint64_t shape_cap, uint64_t *out_shape_len);
int32_t infrastore_forecast_reader_entry_id(const struct InfraStoreForecastReaderHandle *reader,
uint64_t entry_idx, int64_t *out_id); /* the entry's catalog id */
int32_t infrastore_forecast_reader_read(struct InfraStoreForecastReaderHandle *reader,
const struct InfraStore *store, int64_t at_unix_ms);
int32_t infrastore_forecast_reader_invalidate(struct InfraStoreForecastReaderHandle *reader);
/* empty every entry's window */
int32_t infrastore_forecast_reader_entry_values(const struct InfraStoreForecastReaderHandle *reader, uint64_t entry_idx,
const uint8_t **out_ptr, /* borrowed; valid until next read/free */
uint64_t *out_byte_len);
void infrastore_forecast_reader_free(struct InfraStoreForecastReaderHandle *reader);
A read is all-or-nothing. A read spans every group (or entry), so a failing
infrastore_static_reader_read / infrastore_forecast_reader_read leaves the reader wholly empty —
..._group_values / ..._entry_values hand back a zero-length buffer — rather than some groups
holding the new timestamp's values and the rest the previous read's. A binding that refuses a read
before calling in (the Julia wrapper checks the bound's spelling against the reader's timeline,
which at_unix_ms cannot carry) calls ..._invalidate to leave the same empty reader behind.
Window-read deduplication. Forecasts that reference the same backing array and read plan
(deduplicated identical data, or several DeterministicSingleTimeSeries over one
SingleTimeSeries) collapse to a single window slot. infrastore_forecast_reader_read performs
one backend read per slot, not per entry, so a forecast shared by N owners is read once per
timestamp. infrastore_forecast_reader_num_slots is that physical read count, and
infrastore_forecast_reader_entry_slot gives the 0-based slot backing each entry (entries that
share data report the same slot) — group entries by slot to also decode each unique window only
once.
Batch Writes
A batch is the only write path on this ABI. There are no per-type store-level add functions: a single add is a one-item batch, which costs one extra call and keeps a second, near-identical set of entry points off the surface.
A batch accumulates add requests client-side (no store I/O); infrastore_store_add_batch commits
them all in one metadata transaction, which is much faster than per-item adds when ingesting
many series. It is also the fast HDF5 write path: same-shaped SingleTimeSeries are packed into
batch-sized datasets so the timestamp-major chunks are filled whole rather than a column at a time.
Data buffers are copied into the batch, so they only need to stay valid for the
infrastore_batch_add_* call. The submit is all-or-nothing and drains the batch in either case (on
error nothing was committed and the batch is left empty). On success *out_ids holds one id per
item, in batch order; the caller owns that array and frees it with
infrastore_buffer_free_i64(*out_ids, *out_len). The batch handle itself is reusable after submit
and must eventually be released with infrastore_batch_free.
struct InfraStoreBatch *infrastore_batch_new(void);
void infrastore_batch_free(struct InfraStoreBatch *batch);
int32_t infrastore_batch_add_single(struct InfraStoreBatch *batch, ...);
int32_t infrastore_batch_add_non_sequential(struct InfraStoreBatch *batch, ...);
int32_t infrastore_batch_add_persistent(struct InfraStoreBatch *batch, ...);
int32_t infrastore_batch_add_forecast(struct InfraStoreBatch *batch, ...); /* 2=Deterministic, 5=Scenarios */
int32_t infrastore_batch_add_probabilistic(struct InfraStoreBatch *batch, ...);
int32_t infrastore_store_add_batch(struct InfraStore *handle, struct InfraStoreBatch *batch,
uint64_t *out_len, int64_t **out_ids); /* infrastore_buffer_free_i64 */
Bulk Reads
infrastore_store_read_by_ids reads many series in one call, reading each packed dataset's column
span once instead of re-reading every chunk per series — the efficient way to load many whole series
(e.g. for exploration or plotting), where a single full-series read otherwise touches every chunk
under the timestamp-major layout. infrastore_store_read_by_id is the single-id form, which also
takes a window; infrastore_store_read_by_ids_range is the bounds form, which clips.
Every read returns its results in an InfraStoreBulkRead handle — a single read holds exactly one
item — in the order the ids were given, repeats included. Elements are read out with
infrastore_bulk_result_get_single / ..._get_irregular / ..._get_forecast, chosen by the type
infrastore_bulk_result_item_type reports. The caller owns the returned strings and buffers and
frees them with infrastore_string_free, infrastore_buffer_free_i64, and
infrastore_buffer_free_u8. The handle is not consumed by a read (elements may be read more than
once) and must be released with infrastore_bulk_result_free.
int32_t infrastore_store_read_by_ids(const struct InfraStore *handle,
const int64_t *ids, uint64_t n,
struct InfraStoreBulkRead **out_result);
int32_t infrastore_store_read_by_ids_range(const struct InfraStore *handle,
const int64_t *ids, uint64_t n,
bool zoneless, int64_t start_ms, int64_t end_ms,
struct InfraStoreBulkRead **out_result);
int32_t infrastore_bulk_result_item_type(const struct InfraStoreBulkRead *result, uint64_t index,
int32_t *out_type);
int32_t infrastore_bulk_result_get_single(const struct InfraStoreBulkRead *result, uint64_t index, ...);
void infrastore_bulk_result_free(struct InfraStoreBulkRead *result);
An id naming no row fails the whole call with INFRASTORE_ERR_NOT_FOUND rather than being skipped.
The handle carries each item's name, and infrastore_bulk_result_item_name hands item index's
back as an owned C string, freed with infrastore_string_free — the companion to
infrastore_bulk_result_item_type, and how a caller labels what it decodes.
infrastore_store_remove_by_ids is the removal direction of the same reference, listed with the
other removals above: one all-or-nothing transaction, the count through out_removed, and
INFRASTORE_ERR_NOT_FOUND if any id names no row.
Both id-addressed calls carry an optional owner guard — has_owner beside owner_id and
owner_category, ignored when the flag is false — that holds the addressed row to one owner and
returns INFRASTORE_ERR_OWNER_MISMATCH when it belongs to another. A consumer that addresses a
series by id but means "this owner's series" has to use it rather than confirming the owner in a
call of its own: an id survives a reassignment, so a separate check has a window after it in which
the row can move, and a removal issued after that window retires the new owner's series. The guard
puts the check and the act in one transaction, and costs the read nothing at all — the owner comes
off the same row the values are materialized from.
infrastore_store_read_by_id also takes a window. Each optional argument is a *_present flag
beside its value; with none present it reads the whole series. start_ms is Unix milliseconds,
spelled zoned or zoneless by start_zoneless like every other bound; len counts timesteps (the
static types) and count counts windows (the forecasts), and supplying the one that does not apply
is INFRASTORE_ERR_INVALID_PARAMETER rather than an argument the store drops. So is a start off the
series' own grid or an extent running past its end — a window is checked where
infrastore_store_read_by_ids_range clips.
int32_t infrastore_store_read_by_ids(const struct InfraStore *handle, const int64_t *ids,
uint64_t n, struct InfraStoreBulkRead **out_result);
int32_t infrastore_store_read_by_id(const struct InfraStore *handle, int64_t id,
bool start_present, bool start_zoneless, int64_t start_ms,
bool len_present, uint64_t len,
bool count_present, uint64_t count,
bool has_owner, int64_t owner_id, int32_t owner_category,
struct InfraStoreBulkRead **out_result);
int32_t infrastore_bulk_result_item_name(const struct InfraStoreBulkRead *result,
uint64_t index, char **out_name);
Store-Wide Operations
/* True iff the store holds nothing at all — no time series, and no associations
in any catalog. One short-circuited existence probe per catalog table, so it is
O(1) in store size and stays correct as the catalog gains tables; prefer it to
a caller-side conjunction over the counting entry points below, which is
neither. */
int32_t infrastore_store_is_empty(const struct InfraStore *handle, bool *out);
int32_t infrastore_store_counts(const struct InfraStore *handle, int64_t *out_components_with_time_series,
int64_t *out_static_time_series, int64_t *out_forecasts);
/* Association count per type as a JSON array of {time_series_type, count};
probe-then-fetch. */
int32_t infrastore_store_counts_by_type(const struct InfraStore *handle,
char *buf, uint64_t cap, uint64_t *out_len);
/* Distinct stored arrays (content hashes); shared arrays count once. */
int32_t infrastore_store_num_distinct_arrays(const struct InfraStore *handle, int64_t *out_count);
/* Grouped static / forecast summaries as JSON arrays (one object per group with a
`count` field); probe-then-fetch. */
int32_t infrastore_store_static_summary(const struct InfraStore *handle,
char *buf, uint64_t cap, uint64_t *out_len);
int32_t infrastore_store_forecast_summary(const struct InfraStore *handle,
char *buf, uint64_t cap, uint64_t *out_len);
/* Distinct owners per category + distinct arrays per kind (static/forecast). */
int32_t infrastore_store_counts_detailed(const struct InfraStore *handle, int64_t *out_components,
int64_t *out_supplemental_attributes,
int64_t *out_static_time_series, int64_t *out_forecasts);
/* Distinct owner ids of owner_category as a JSON array; optional type/resolution
filters (NULL resolution = none). Probe-then-fetch. */
int32_t infrastore_store_list_owner_ids(const struct InfraStore *handle, int32_t owner_category,
bool has_time_series_type, int32_t time_series_type,
const char *resolution, char *buf, uint64_t cap, uint64_t *out_len);
/* out_present = false when no matching forecast. The out_horizon/out_interval/
out_resolution ISO-8601 strings are NULL when absent (free with infrastore_string_free);
out_count and out_initial_ms are -1 when absent. filter_* NULL = no filter. */
int32_t infrastore_store_get_forecast_parameters(const struct InfraStore *handle,
const char *filter_resolution, const char *filter_interval,
bool *out_present, char **out_horizon,
char **out_interval, int64_t *out_count,
char **out_resolution, int64_t *out_initial_ms);
/* Per-resolution static grids as a JSON array of {"resolution","initial_timestamp_ms",
"length"} objects, ordered by resolution (empty array when no SingleTimeSeries);
error when the series at one resolution disagree. filter_resolution NULL = every
resolution, else scope to that ISO-8601 grid. Probe-then-fetch (buf=NULL, cap=0 to size). */
int32_t infrastore_store_check_static_consistency(const struct InfraStore *handle,
const char *filter_resolution, char *buf,
uint64_t cap, uint64_t *out_len);
/* Distinct resolutions as a JSON array of ISO-8601 duration strings, ascending;
optional type filter. Probe-then-fetch (buf=NULL, cap=0 to size). */
int32_t infrastore_store_get_resolutions(const struct InfraStore *handle,
bool has_time_series_type, int32_t time_series_type,
char *buf, uint64_t cap, uint64_t *out_len);
/* out_kind: 0 = none, 1 = DEFLATE (out_level 0-9 + out_shuffle). */
int32_t infrastore_store_get_compression(const struct InfraStore *handle, uint8_t *out_kind,
uint8_t *out_level, bool *out_shuffle);
int32_t infrastore_store_verify(const struct InfraStore *handle, uint64_t *out_error_count);
/* Compacts and returns the report as a JSON object {slots_reclaimed, datasets_dropped,
feature_sets_reclaimed, timestamp_sets_reclaimed, bytes_reclaimed}. Owned allocation:
free with infrastore_string_free. On an on-disk store this rewrites the .h5 file and
replaces it, so the call must run exactly once (no probe-then-fetch). */
int32_t infrastore_store_compact(struct InfraStore *handle, char **out_json, uint64_t *out_len);
int32_t infrastore_store_flush(struct InfraStore *handle);
/* Cross-operation transactions. Adds, removals, and transforms between a begin and
its matching commit either all take effect or none do; removals are reversible
only inside one. Calls nest -- only the outermost commit is durable. This is
store state, not a borrowed guard, so nothing has to survive the ABI boundary.
Holds the SQLite write lock until the outermost commit/rollback. */
int32_t infrastore_store_begin_transaction(struct InfraStore *handle);
int32_t infrastore_store_commit_transaction(struct InfraStore *handle);
int32_t infrastore_store_rollback_transaction(struct InfraStore *handle);
int32_t infrastore_store_in_transaction(struct InfraStore *handle, bool *out);
/* The byte budget an open transaction's buffered adds are held to, and through it how wide a
dataset a run of single adds writes. Raised, that run produces the dataset the equivalent
bulk add produces -- a batch handed over as a list applies no budget, the caller already
holding it. The figure belongs to the handle, not the artifact: nothing is persisted.
`bytes` must be non-zero; lowering it under an open transaction writes out whatever the
buffer already holds beyond the new figure, so that call can do file I/O and fail. */
int32_t infrastore_store_write_buffer_bytes(const struct InfraStore *handle, uint64_t *out);
int32_t infrastore_store_set_write_buffer_bytes(struct InfraStore *handle, uint64_t bytes);
/* Persist the store's data to `path` (HDF5) and `<path>.sqlite` (metadata),
materializing an in-memory store to disk. Existing target files are overwritten. */
int32_t infrastore_store_persist(struct InfraStore *handle, const char *path);
/* Write an in-memory catalog to this store's own <path>.sqlite, stamped to match the HDF5 file
already beside it. Unlike infrastore_store_persist, writes no arrays: they are already in
place. A checkpoint, not a mode switch. For catalog_mode=0 this is infrastore_store_flush. */
int32_t infrastore_store_persist_catalog(struct InfraStore *handle);
/* has_owner=false clears all; when true, the owner is the pair (owner_id, owner_category). */
int32_t infrastore_store_clear(struct InfraStore *handle, bool has_owner, int64_t owner_id,
int32_t owner_category); /* owner_category: 0=Component, 1=SupplementalAttribute */
/* Reassign every time series owned by old_owner_id (in owner_category) to
new_owner_id; *out_updated (when non-NULL) receives the number of associations
changed. */
int32_t infrastore_store_replace_owner(struct InfraStore *handle,
int64_t old_owner_id, int64_t new_owner_id,
int32_t owner_category, uint64_t *out_updated);
/* List catalog metadata rows as a JSON array (identity, the per-type descriptive
snapshot, the physical detail, and the association `id` the write handed back
— the address every read and removal takes). A row carries no
timestamp vector: an irregular series' time axis is the one part of a row that
costs a read per row, so a listing omits it and a caller that needs it reads
the series. See the filter record below; a NULL filter lists the whole store.
The JSON comes back as an *owned* allocation through out_json (free with
infrastore_string_free), with out_len its byte length — NOT probe-then-fetch,
because a listing's size scales with the catalog and probing would run the
query and serialize the rows twice. */
int32_t infrastore_store_list_metadata(const struct InfraStore *handle,
const struct InfraStoreFilter *filter,
char **out_json, uint64_t *out_len);
The filter record
Every filter-taking export — infrastore_store_list_metadata, list_names, list_owner_types,
has_any_by_filter, remove_by_filter, export_time_series_associations_openapi, and both reader
builders — takes the catalog filter as one const struct InfraStoreFilter * rather than as
positional arguments.
A NULL pointer, and equally an all-zero record, is the empty filter: it matches everything. So a
caller writes InfraStoreFilter f = {0}; and sets only the fields it cares about. The predicates
are independent and ANDed. The record borrows its strings — they must outlive the call, and nothing
in it is freed.
The layout is part of the ABI. The record buys one filter parser and no churn across the eight
call sites when a predicate is added; it does not make the ABI forward-compatible. A caller compiled
against an older header allocates the older sizeof(InfraStoreFilter), so appending a field is a
breaking change like any other — rebuild against the regenerated header.
typedef struct InfraStoreFilter {
bool has_owner_id; int64_t owner_id;
bool has_owner_category; int32_t owner_category; /* 0=Component, 1=SupplementalAttribute */
bool has_time_series_type; int32_t time_series_type;
const char *name; /* NULL = unset */
const char *name_glob; /* SQLite GLOB over the name (`*`/`?`, case-sensitive), ANDed
with `name` rather than replacing it */
const char *resolution; /* ISO-8601 */
const char *interval; /* ISO-8601; matches forecasts only — static rows carry none */
const char *features_json; /* JSON object; int, float, bool or string values */
bool features_exact; /* compare `features_json` as the row's whole feature set
(content hash) rather than a subset it must contain;
with `features_json` NULL, only feature-less rows match */
const char *component_field; /* exact, case-sensitive; a row declaring none matches no value */
bool has_zoneless; bool zoneless; /* which timestamp-spelling group */
bool has_initial_timestamp; int64_t initial_timestamp_ms;
bool has_length; uint64_t length;
} InfraStoreFilter;
Every row carries the hex data_hash of the array it resolves to, and rows that share a stored
array share it — so grouping a listing by data_hash discovers which time series share their
underlying data, in one query.
Associations
Two catalogs of relationships between entities the store does not otherwise model. Both are independent of time series: there are no foreign keys and no cascade (both endpoints live in the caller's object graph, so a cascade could never fire), so removing a time series never removes an association and vice versa; a caller that wants both makes both calls.
Each family's query predicate crosses the boundary as one JSON object string rather than a set of positional arguments, because half its fields are string lists. Every field is optional and the set ones are ANDed:
{ "component_id": 1, "component_types": ["Generator", "Load"],
"attribute_id": 100, "attribute_types": ["GeographicInfo"] }
{ "parent_id": 1, "parent_types": ["Generator"],
"child_id": 7, "child_types": ["Bus"] }
A NULL (or empty) filter_json is the empty filter and matches every row — which is what
makes a bulk export/import round trip one call each way. The *_types lists hold concrete type
names, rendered as SQL IN (…); expanding an abstract type into its subtypes stays in the calling
language, and an empty list matches nothing. An unknown field or malformed JSON is
INFRASTORE_ERR_INVALID_PARAMETER. Every remove function reports its count through out_removed
and treats removing nothing as success, not an error.
The list functions use the probe-then-fetch convention: call with buf = NULL, cap = 0 to learn the
length via out_len, then again with a len + 1-byte buffer. (infrastore_store_list_metadata,
infrastore_store_list_metadata_by_ids, and
infrastore_store_list_supplemental_attribute_associations are the exceptions in this header — they
return an owned string instead, because a no-filter call exports the whole catalog or table.)
Supplemental-attribute associations
Which supplemental attributes are attached to which components. Identity is the
(component_id, attribute_id) pair; the type names are denormalized labels, so re-attaching the
same pair under different type names is still a duplicate. One attribute may be attached to many
components.
/* Attach supplemental attribute (attribute_id, attribute_type) to component
(component_id, component_type). INFRASTORE_ERR_DUPLICATE_ASSOCIATION if that component
already carries that attribute, whatever type names are supplied. */
int32_t infrastore_store_add_supplemental_attribute_association(struct InfraStore *handle,
int64_t component_id,
const char *component_type,
int64_t attribute_id,
const char *attribute_type);
/* Attach many in one all-or-nothing transaction, from a JSON array of objects with
component_id, component_type, attribute_id, and attribute_type. The import half
of the round trip whose export is infrastore_store_list_supplemental_attribute_associations
with a NULL filter. *out_added (when non-NULL) receives the number inserted. */
int32_t infrastore_store_add_supplemental_attribute_associations(struct InfraStore *handle,
const char *associations_json,
uint64_t *out_added, /* optional */
int64_t **out_ids); /* optional; owned,
infrastore_buffer_free_i64 */
/* Whether any attachment matches filter_json (NULL = any). */
int32_t infrastore_store_has_supplemental_attribute_association(const struct InfraStore *handle,
const char *filter_json,
bool *out_found);
/* Matching attachments as a JSON array, in insertion order; each object carries
component_id, component_type, attribute_id, attribute_type. Returns the JSON
through out_json as an OWNED allocation, freed with infrastore_string_free
(a no-filter call exports the whole table, so this follows the owned-string
convention rather than probe-then-fetch). */
int32_t infrastore_store_list_supplemental_attribute_associations(const struct InfraStore *handle,
const char *filter_json,
char **out_json, uint64_t *out_len);
/* Distinct attribute ids of the matching rows, ascending, as a JSON array — the
attributes attached to a component when component_id is set. Probe-then-fetch. */
int32_t infrastore_store_list_supplemental_attribute_ids(const struct InfraStore *handle,
const char *filter_json,
char *buf, uint64_t cap,
uint64_t *out_len);
/* Distinct component ids of the matching rows, ascending, as a JSON array — the
components carrying an attribute when attribute_id is set. Probe-then-fetch. */
int32_t infrastore_store_list_components_with_attributes(const struct InfraStore *handle,
const char *filter_json,
char *buf, uint64_t cap,
uint64_t *out_len);
/* Remove every matching attachment; *out_removed (when non-NULL) receives the
count. Removing nothing is success, not an error. */
int32_t infrastore_store_remove_supplemental_attribute_associations(struct InfraStore *handle,
const char *filter_json,
uint64_t *out_removed);
/* Move every attachment from component old_id to new_id; *out_updated (when
non-NULL) receives the rows changed. INFRASTORE_ERR_DUPLICATE_ASSOCIATION if new_id
already carries one of the attributes being moved. */
int32_t infrastore_store_replace_supplemental_attribute_component_id(struct InfraStore *handle,
int64_t old_id, int64_t new_id,
uint64_t *out_updated);
/* Attachment counts through out_count. kind selects what is counted:
0 = rows matching the filter, 1 = distinct attributes among them,
2 = distinct components among them. */
int32_t infrastore_store_count_supplemental_attribute_associations(const struct InfraStore *handle,
const char *filter_json,
int32_t kind, int64_t *out_count);
/* Grouped counts as JSON arrays: by attribute type, as {"type": …, "count": …}
ordered by type; and by both type names, as
{"component_type": …, "attribute_type": …, "count": …} ordered by attribute type
then component type. Probe-then-fetch. */
int32_t infrastore_store_supplemental_attribute_counts_by_type(const struct InfraStore *handle,
char *buf, uint64_t cap,
uint64_t *out_len);
int32_t infrastore_store_supplemental_attribute_summary(const struct InfraStore *handle,
char *buf, uint64_t cap,
uint64_t *out_len);
/* The attribute ids attached to component 1. */
const char *filter = "{\"component_id\":1}";
uint64_t len = 0;
infrastore_store_list_supplemental_attribute_ids(store, filter, NULL, 0, &len);
char *json = malloc(len + 1);
infrastore_store_list_supplemental_attribute_ids(store, filter, json, len + 1, &len); /* e.g. "[100]" */
free(json);
Parent/child associations
Directed edges between components — a generator (parent) connected to a bus (child), say. Both
endpoints are always components. Identity is the ordered (parent_id, child_id) pair, so the
reversed pair is a different edge, and with no relationship-kind column one ordered pair may be
related at most once.
This family is deliberately narrower than the supplemental one — no counts-by-type and no grouped summary — because there is no consumer for them yet; both are additive if one appears.
/* Record a directed edge from component (parent_id, parent_type) to component
(child_id, child_type). INFRASTORE_ERR_DUPLICATE_ASSOCIATION if that ordered pair is
already related; the reversed pair is a different edge. */
int32_t infrastore_store_add_parent_child_association(struct InfraStore *handle,
int64_t parent_id, const char *parent_type,
int64_t child_id, const char *child_type);
/* Record many edges in one all-or-nothing transaction, from a JSON array of objects
with parent_id, parent_type, child_id, and child_type. *out_added (when non-NULL)
receives the number inserted. */
int32_t infrastore_store_add_parent_child_associations(struct InfraStore *handle,
const char *associations_json,
uint64_t *out_added, /* optional */
int64_t **out_ids); /* optional; owned,
infrastore_buffer_free_i64 */
/* Whether any edge matches filter_json (NULL = any). */
int32_t infrastore_store_has_parent_child_association(const struct InfraStore *handle,
const char *filter_json,
bool *out_found);
/* Matching edges as a JSON array, in insertion order; each object carries
parent_id, parent_type, child_id, child_type. Probe-then-fetch. */
int32_t infrastore_store_list_parent_child_associations(const struct InfraStore *handle,
const char *filter_json,
char *buf, uint64_t cap,
uint64_t *out_len);
/* Distinct ids on one end of the matching edges, ascending, as a JSON array.
endpoint is 0 for parents and 1 for children — so endpoint = 1 with parent_id
set is "the children of this component". Probe-then-fetch. */
int32_t infrastore_store_list_parent_child_ids(const struct InfraStore *handle,
const char *filter_json, int32_t endpoint,
char *buf, uint64_t cap, uint64_t *out_len);
/* Remove every matching edge; *out_removed (when non-NULL) receives the count.
Removing nothing is success, not an error. */
int32_t infrastore_store_remove_parent_child_associations(struct InfraStore *handle,
const char *filter_json,
uint64_t *out_removed);
/* Rewrite component old_id to new_id on BOTH ends of every edge; *out_updated
(when non-NULL) receives the rows changed. INFRASTORE_ERR_DUPLICATE_ASSOCIATION if the
rewrite would duplicate an edge new_id already has. */
int32_t infrastore_store_replace_parent_child_component_id(struct InfraStore *handle,
int64_t old_id, int64_t new_id,
uint64_t *out_updated);
/* Number of edges matching filter_json, through out_count. */
int32_t infrastore_store_count_parent_child_associations(const struct InfraStore *handle,
const char *filter_json,
int64_t *out_count);
/* The children of component 1. */
const char *filter = "{\"parent_id\":1}";
uint64_t len = 0;
infrastore_store_list_parent_child_ids(store, filter, 1 /* children */, NULL, 0, &len);
char *json = malloc(len + 1);
infrastore_store_list_parent_child_ids(store, filter, 1, json, len + 1, &len); /* e.g. "[7]" */
free(json);
Neither association catalog is exposed over the gRPC server or the
infrastore CLI.
Store Attributes
Key/value provenance about the artifact as a whole. See Store attributes for the model.
int32_t infrastore_store_set_store_attribute(InfraStore *store,
const char *key,
const char *value);
int32_t infrastore_store_get_store_attribute(const InfraStore *store,
const char *key,
char **out_value,
uint64_t *out_len);
int32_t infrastore_store_list_store_attributes(const InfraStore *store,
char **out_json,
uint64_t *out_len);
int32_t infrastore_store_remove_store_attribute(InfraStore *store,
const char *key,
bool *out_removed);
infrastore_store_set_store_attribute(store, "creator", "sienna-build");
char *value = NULL;
uint64_t len = 0;
infrastore_store_get_store_attribute(store, "creator", &value, &len);
if (value != NULL) { /* NULL means the key is unset */
puts(value);
infrastore_string_free(value);
}
char *json = NULL;
infrastore_store_list_store_attributes(store, &json, &len); /* {"creator":"sienna-build"} */
infrastore_string_free(json);
bool removed = false;
infrastore_store_remove_store_attribute(store, "creator", &removed);
Two conventions to note:
getspells "unset" as a null*out_value, not as a zero*out_len. A key set to the empty string is a legitimate value, and the two must stay distinguishable. An unset key isINFRASTORE_OK, notINFRASTORE_ERR_NOT_FOUND.listreturns a JSON object, following the owned-string convention, rather than an array-of-pairs buffer — the ABI has no pair type and this needs no new one.
Unlike the JSON payloads every other owned-string export produces, a value is the caller's own text
and can contain an interior NUL; get reports one as INFRASTORE_ERR_INTEGRITY rather than
returning a value truncated at it. An empty key or one beginning with infrastore. (reserved, on
removal as well as on write) is INFRASTORE_ERR_INVALID_PARAMETER; a write to a read-only store is
INFRASTORE_ERR_READ_ONLY.
The read half is available over the gRPC server; the writes are not, like every other write.
OpenAPI-row Association Serde
Direct JSON serde of the two association catalogs, in the wire spelling
SiennaSchemas defines (TimeSeries/*.json,
Core/Associations/SupplementalAttributeAssociation.json). The two exports use the owned-string
convention.
/* Export time_series_associations matching the filter as a sorted OpenAPI-row
JSON array. Each row's uri and data_hash are the hex-encoded content hash
the store already has for that row -- never a caller-supplied locator.
Takes the same filter record as infrastore_store_list_metadata. Returns the
JSON through out_json as an OWNED allocation, freed with infrastore_string_free.
PersistentTimeSeries rows are omitted -- the wire contract has no schema for
the type -- and a filter naming it is INFRASTORE_ERR_INVALID_PARAMETER. */
int32_t infrastore_store_export_time_series_associations_openapi(const struct InfraStore *handle,
const struct InfraStoreFilter *filter,
char **out_json, uint64_t *out_len);
/* Bulk-ingest a JSON array of time-series association OpenAPI rows in one
all-or-nothing transaction -- the import half of the round trip whose export
is infrastore_store_export_time_series_associations_openapi. Rows only: every
row must name an array this store already holds, an irregular row must name
its time axis with timestamps_uri, and each keeps the association_id it
carries. A PersistentTimeSeries row is refused: no export writes one, and the
wire contract has no schema to check one against. *out_added (when non-NULL)
receives the number inserted. */
int32_t infrastore_store_import_time_series_associations_openapi(
struct InfraStore *handle, const char *json, uint64_t *out_added);
/* Export the whole supplemental_attribute_associations table as an OpenAPI-row
JSON array, sorted by (component_id, attribute_id). Owned-string return. */
int32_t infrastore_store_export_supplemental_attribute_associations_openapi(
const struct InfraStore *handle, char **out_json, uint64_t *out_len);
/* Bulk-ingest a JSON array of supplemental-attribute association OpenAPI rows in
one all-or-nothing transaction -- the import half of the round trip whose
export is infrastore_store_export_supplemental_attribute_associations_openapi.
*out_added (when non-NULL) receives the number inserted. */
int32_t infrastore_store_import_supplemental_attribute_associations_openapi(
struct InfraStore *handle, const char *json, uint64_t *out_added);
A catalog row's data_hash is NOT NULL, and infrastore fills a row's uri/data_hash wire
fields with that same content hash, hex-encoded, on export. The time-series import writes rows
only: infrastore never modifies the data to make an incoming document agree with what it already
holds, so a row naming an array this store does not hold is refused
(INFRASTORE_ERR_INVALID_PARAMETER) rather than written as a dangling reference. A
NonSequentialTimeSeries row locates its time axis the same way, with timestamps_uri: the values
cannot imply it, since two irregular series with identical values on different axes share one
content-addressed array, so a row missing the locator or naming an axis this store does not hold is
refused alike; a PersistentTimeSeries row is refused earlier still, as a type outside the six the
wire contract defines. A geometry disagreement between an added series and its own association row
is likewise rejected at the add boundary, loudly and without writing anything.
Error Messages
int32_t infrastore_last_error_message(char *buf, uint64_t buf_len, uint64_t *needed);
Copies the thread-local error message (UTF-8, null-terminated) into buf. *needed receives the
length excluding the NUL. If buf_len is too small, the message is truncated but INFRASTORE_OK is
still returned — call once with buf = NULL, buf_len = 0 to learn the needed size, then again with
a buffer of *needed + 1. This is the pattern TimeSeries.jl uses.
Building
cargo build -p infrastore-ffi --release
# Library at: target/release/libinfrastore_ffi.{dylib,so,dll}
Point consumers at it via the INFRASTORE_LIB environment variable (see the
Julia guide).
Tracing
int32_t infrastore_store_init_logging(const char *filter);
Initialize the Rust tracing subscriber. filter is a null-terminated UTF-8
EnvFilter
directive string (e.g. "debug" or "infrastore_core=debug"). Pass NULL to read the RUST_LOG
environment variable; if that variable is also unset, no output is produced.
The subscriber is initialized at most once per process — subsequent calls are no-ops. Returns
INFRASTORE_OK on success or INFRASTORE_ERR_INVALID_UTF8 if filter is not valid UTF-8.
The Julia binding calls this automatically from its __init__ hook when
RUST_LOG is set, and exposes init_logging for explicit control.