The first production schema is now a single canonical file: schema/telemetry_schema.sql.
Legacy compatibility migrations were removed because this database has no prior production deployments. The schema file represents the current design directly and runs on an empty PostgreSQL database.
Source worker ingestion is documented in docs/mqtt_ingestion_api.md, with MQTT used there as a concrete example.
A draft note for future counter-style telemetry, including energy and traffic counters, is in docs/energy_counter_draft.md.
Segment tables are now created through:
telemetry.create_segment_table(p_table_name, p_value_type)Supported value types are:
double precisionbooleansmallintThe generated tables keep the existing historian shape and protections:
device_pk foreign key for future numeric-key joins while keeping device_id for compatibilitypolicy_id foreign key to telemetry.metric_policies(policy_id) so every stored interval remembers which ingestion policy produced ittelemetry.devices(device_id) with ON UPDATE/DELETE CASCADEtstzrange periods enforced with EXCLUDE USING gist(device_id, start_time DESC) for point/range lookups(device_id, segment_period) for range sampling/overlap queriesfillfactor and aggressive autovacuum/analyze thresholdsRuntime ingestion now flows through a single function:
telemetry.ingest_segment(...)telemetry.metrics.table_name is now stored as plain text, not regclass. Runtime code normalizes it, validates that it resolves under the telemetry schema, and only then interpolates the quoted identifier into dynamic SQL.
Dynamic SQL is used only to parameterize the target table name. All values stay bound through USING, so the only dynamic part of the hot path is table access.
The generic engine preserves the existing historian semantics:
(metric, device)telemetry.deviceslast_seen updates on successful ingestionNULL segmentsNULL tailstelemetry.devices is the canonical device catalog. Ingestion still accepts unseen devices without a separate provisioning step, but it inserts them into the registry before segment writes so foreign keys remain valid.
Deleting or renaming a device is now referentially safe because segment tables and watermark rows cascade from the device registry.
telemetry.devices also has an internal device_pk surrogate primary key. Segment tables keep device_id for API compatibility, but now store device_pk as the internal foreign key.
telemetry.metrics keeps metric_name as the external API identifier, but now uses metric_pk as the internal primary key. Text identifiers remain unique and stable for backward compatibility.
telemetry.metrics now carries both:
metric_typecomparison_modemetric_type and comparison_mode are PostgreSQL enums, so invalid values fail with type-level errors and show up clearly in schema introspection.
Current combinations are:
comparison_mode = 'epsilon'comparison_mode = 'exact'Runtime ingestion policy is now versioned in telemetry.metric_policies. The active row is selected by (metric_name, valid_from <= observed_at) with the greatest valid_from, and new segments store that policy_id. Existing metrics are seeded with an initial valid_from = '-infinity' policy row so historical segments stay compatible.
This keeps historical semantics stable when policy parameters such as epsilon, rounding_precision, allow_null, or max_sampling_interval change over time. Query wrappers do not need to re-resolve policy history; they use the policy_id already attached to each segment when they need policy-specific behavior such as open-segment tail cutoff.
New policies are added through telemetry.add_metric_policy(...). telemetry.register_numeric_metric(...) and telemetry.register_boolean_metric(...) still provision the metric and ensure the initial -infinity policy exists.
If a policy changes while a segment is still open, ingestion now splits that segment at policy.valid_from before handling the incoming sample. The synthetic continuation segment starts exactly at the boundary with samples_count = 0, so no interval can silently extend past the policy that generated it.
The schema also allows a future state metric type backed by smallint with comparison_mode = 'exact', without changing the segment engine again.
The internal table readers are generic:
telemetry.value_at_from_table(...)telemetry.segments_between_from_table(...)telemetry.samples_from_table(...)Public wrappers stay typed because PostgreSQL function return types are static:
telemetry.value_at(...), telemetry.metric_segments(...), telemetry.sample_metric(...)telemetry.boolean_value_at(...), telemetry.boolean_segments_between(...), telemetry.boolean_samples(...)The schema now also includes:
telemetry.verify_segments(...) for integrity checkstelemetry.compact_segments(...) for manual zero-gap identical-value compactiontelemetry.metric_retention_policies as a retention/compaction policy registrytelemetry.inactive_devices(...) for offline-device detection