crates.io ↗

iet-db-timescale

feature implementation official

SQLx implementation of database lifecycle, migrations, entities, and repositories.

CoreFeaturesIntegrationsStacksTooling
Used byiet-auth-acl-timescaleiet-auth-basic-timescaleiet-auth-identity-timescaleiet-auth-profiles-timescaleiet-auth-rbac-timescaleiet-auth-revocations-timescaleiet-auth-session-timescale
iet-db-timescaleimplementationTimescaleDB implementation
Depends oniet-appcontractiet-telemetrycontractiet-dbcontractiet-corecontractiet-configcontractiet-config-macroscontract

How to use it

iet-db-timescale is the TimescaleDB/Postgres persistence adapter. TimescaleDatabase owns a validated sqlx pool; entities registered in an EntityRegistry generate reviewed migration SQL; and the generated Repository<E, F> performs resource/scope-aware CRUD through a CallScope. It reads its settings from iet-config via DatabaseConfig.

rust
// crates/features/iet-db-timescale/tests/repository.rs
use iet_core::{CallContext, CallScope, Session};
use iet_db_timescale::{generation::EntityRegistry, persistence::Repository};

#[derive(iet_db_macros::Persistent, iet_db_macros::TimescaleEntity)]
#[persistent(table = "products")]
#[entity(fields = ProductField)]
struct Product {
    id: uuid::Uuid,
    #[field(operators = "Equal,Contains")]
    name: String,
    deleted_at: Option<chrono::DateTime<chrono::Utc>>,
}

// let config: iet_db_timescale::config::DatabaseConfig = ...;
let db = iet_db_timescale::TimescaleDatabase::connect(&config).await?;
let mut registry = EntityRegistry::default();
registry.register::<Product>()?;
sqlx::raw_sql(&registry.initial("public")?.up).execute(db.pool()).await?;

let mut session = db.session();
let repository = Repository::<Product, ProductField>::new(session.resource_id().clone());
let product = repository
    .insert_entity(
        &Product { id: uuid::Uuid::nil(), name: "One".into(), deleted_at: None },
        &mut CallScope::new(&CallContext::default(), &mut session),
    )
    .await?;
  • connect requires a reachable TimescaleDB extension, version 2.13+; connect_named labels pool metrics and connect_with_health_check controls per-checkout pings.
  • Business repositories must use their CallScope session rather than the pool directly; unrelated database tables are never dropped.
  • EntityRegistry::generate(...) writes versioned up/down migration files instead of applying schema at startup.

How it works

Registering a Persistent entity records its columns and indexes in a SchemaRegistry plus any hypertable and foreign-key metadata; EntityRegistry::generate calls initial for an empty database or diff against the live catalog and writes a MigrationPair of up/down SQL. At runtime, repositories perform scoped CRUD: TimescaleDatabase::connect opens a validated pool, db.session yields a session, and Repository operations verify the session's ResourceId through CallScope::require_resource before touching the pool.

flowchart LR
  A["EntityRegistry::register"] --> B["SchemaRegistry"]
  A --> C["hypertables and relations"]
  B --> D["EntityRegistry::generate"]
  C --> D
  D --> E["initial or diff"]
  E --> F["MigrationPair up and down"]
  F --> G["migration files"]
  H["TimescaleDatabase::connect"] --> I["db.session"]
  I --> J["Repository::new"]
  J --> K["insert_entity"]
  K --> L["CallScope::require_resource"]

What it implements

  • Implements iet_db::Database from iet-db.

What's inside

Modules

  • src/catalog.rs
  • src/config.rs
  • src/ddl.rs
  • src/filters.rs
  • src/generation.rs
  • src/health.rs
  • src/lib.rs
  • src/migration.rs
  • src/migration_files.rs
  • src/migration_observer.rs
  • src/persistence.rs
  • src/query.rs
  • src/schema.rs
  • src/session.rs
  • src/store_support.rs

Public API

(root)

  • struct TimescaleDatabase
  • pub fn version(&self) -> &semver::Version
  • pub fn session(&self) -> TimescaleSession
  • pub fn pool(&self) -> &PgPool — Infrastructure stores may share the selected pool; business repositories must use their CallScope session instead of this independently.
  • pub fn record_pool_metrics(&self) — Point-in-time pool pressure. No task or runtime is installed by the library.

catalog

  • struct CatalogColumn
  • struct CatalogIndex
  • struct CatalogTable
  • struct CatalogConstraint

config

  • struct DatabaseConfig

ddl

  • pub fn initial<E: Persistent>() -> AppResult<Vec<String>>

filters

  • enum Kind
  • enum Value
  • enum Operator
  • trait FilterField
rust
pub trait FilterField: Column {
    fn kind(&self) -> Kind;
    fn permits(&self, operator: Operator) -> bool;
}
  • enum Filter
  • pub fn append<F: FilterField>( builder: &mut QueryBuilder<'_, Postgres>, filter: &Filter<F>, ) -> AppResult<()>

generation

  • struct EntityRegistry
  • pub fn register<E: Persistent>(&mut self) -> AppResult<()>
  • pub fn initial(&self, schema: &str) -> AppResult<MigrationPair>

health

  • struct PostgresProbe

migration

  • struct Migrations

migration_files

  • struct MigrationPair
  • pub fn write_new( &self, directory: &Path, version: i64, name: &str, ) -> AppResult<(PathBuf, PathBuf)>

persistence

  • enum RecordId — Application-generated identifiers. UUID variants must be RFC UUID version 7.
  • trait Entity — Mapping is implementation-owned, never derived from caller-provided SQL names.
rust
pub trait Entity: Persistent {
    const ID_COLUMN: &'static str = "id";
}
  • trait EntityRow
rust
pub trait EntityRow: Sized + Send + Sync {
    const COLUMN_NAMES: &'static [&'static str];
    fn from_row(row: &sqlx::postgres::PgRow) -> AppResult<Self>;
}
  • trait EntityFields — Entity-defined field allowlists used by generated repository operations. Implementations are generated by PostgresEntity from #[field] metadata.
rust
pub trait EntityFields: Entity + EntityRow {
    type Field: WriteField + crate::filters::FilterField + Copy + 'static;

    const FIELDS: &'static [Self::Field];

    fn field_name(field: Self::Field) -> &'static str {
        crate::query::Column::identifier(&field)
    }

    fn value(field: Self::Field, value: &Self) -> AppResult<Option<crate::filters::Value>>;
}
  • struct Repository — Generic repository for implementation-owned entities. Automatically handles resource checks, resource checks, soft-deletion filtering, query building, row mapping, and unique constraint error translation.
  • pub fn new(resource: ResourceId) -> Self
  • pub fn resource_id(&self) -> &ResourceId
  • pub fn check_scope(&self, scope: &CallScope<'_, crate::TimescaleSession>) -> AppResult<()>
  • struct Assignment — None is a typed SQL NULL, permitted only by nullable field metadata.
  • trait MutableField — Mutable fields are an implementation-owned allowlist. System scope columns remain readable but cannot be written through generated entity operations.
rust
pub trait MutableField: crate::query::Column {
    fn writable(&self) -> bool {
        true
    }
}
  • trait WriteField
rust
pub trait WriteField: MutableField {
    fn kind(&self) -> crate::filters::Kind;
    fn nullable(&self) -> bool {
        false
    }
}
  • pub fn insert<E: Entity, F: WriteField>( assignments: &[Assignment<F>], _context: &CallContext, ) -> AppResult<QueryBuilder<'static, Postgres>>
  • pub fn update<E: Entity, F: WriteField>( id: impl Into<RecordId>, assignments: &[Assignment<F>], context: &CallContext, ) -> AppResult<QueryBuilder<'static, Postgres>>
  • pub fn update_text<E: Entity>( id: impl Into<RecordId>, field: &impl MutableField, value: String, context: &CallContext, ) -> AppResult<QueryBuilder<'static, Postgres>>
  • pub fn active_scope<E: Entity>( query: &mut QueryBuilder<'_, Postgres>, _context: &CallContext, ) -> AppResult<()> — Every ordinary operation gets tenancy and active-row predicates together.
  • pub fn select<E: Entity>( columns: &[&str], context: &CallContext, ) -> AppResult<QueryBuilder<'static, Postgres>>
  • pub fn count<E: Entity>(context: &CallContext) -> AppResult<QueryBuilder<'static, Postgres>>
  • pub fn delete<E: Entity>( id: impl Into<RecordId>, context: &CallContext, ) -> AppResult<QueryBuilder<'static, Postgres>>
  • pub fn prepare<E: Persistent>(schema: &str) -> AppResult<Vec<String>> — Regular PostgreSQL deliberately ignores optional Timescale metadata.
  • pub fn initial<E: Persistent>() -> AppResult<Vec<String>> — Initial reviewed DDL from entity metadata. Applying it remains deployment-owned.

query

  • trait Column — Implement only for trusted schema enums, never request strings.
rust
pub trait Column {
    fn identifier(&self) -> &'static str;
}
  • enum Comparison
  • pub fn text_filter( builder: &mut QueryBuilder<'_, Postgres>, column: &impl Column, comparison: Comparison, value: String, ) -> AppResult<()>
  • pub fn integer_filter( builder: &mut QueryBuilder<'_, Postgres>, column: &impl Column, comparison: Comparison, value: i64, ) -> AppResult<()>
  • pub fn seek_pair( builder: &mut QueryBuilder<'_, Postgres>, primary: &impl Column, primary_direction: Direction, position: i64, unique: &impl Column, unique_direction: Direction, id: i64, — Non-null integer tuple seek with a unique tie-breaker and independent directions. For before cursors invert directions for both seek and ORDER BY, then reverse results.
  • pub fn order( builder: &mut QueryBuilder<'_, Postgres>, column: &impl Column, direction: Direction, ) -> AppResult<()>
  • enum NullOrder
  • pub fn seek_text_pair( builder: &mut QueryBuilder<'_, Postgres>, primary: &impl Column, ordering: (Direction, NullOrder), position: Option<String>, unique: &impl Column, unique_direction: Direction, i — Nullable text seek under deterministic C collation, with non-null unique ID. ORDER BY must use the same collation, directions and explicit null ordering.
  • pub fn order_text( builder: &mut QueryBuilder<'_, Postgres>, column: &impl Column, direction: Direction, nulls: NullOrder, ) -> AppResult<()>

schema

  • struct ColumnMigrationHints
  • struct ReviewedSchemaChanges — A pair is structurally reversible, not a guarantee of data restoration.
  • enum SqlType
  • struct ColumnSpec
  • struct TableSpec
  • struct ReviewPlan
  • struct SchemaRegistry
  • pub fn with_table_rename(mut self, table: &str, from: &str) -> AppResult<Self> — Names are desired registered names. Hints may precede registration.
  • pub fn with_column_hints( mut self, table: &str, column: &str, hints: ColumnMigrationHints, ) -> AppResult<Self>
  • pub fn approve_destructive(mut self, approved: bool) -> Self — Applies only to diff_pair_with_hints; legacy diff behavior is unchanged.
  • pub fn register<E: iet_core::persistence::Persistent>(&mut self) -> AppResult<()>
  • pub fn review(&self, schema: &str, current: &[CatalogTable]) -> AppResult<ReviewPlan>
  • pub fn diff_pair_with_hints( &self, schema: &str, current: &[CatalogTable], ) -> AppResult<ReviewedSchemaChanges> — Explicit existing-column migrations. Unregistered tables are never dropped. Backfills retain changed data on rollback. Casts and relaxed nullability require approval and can still fail against the data present at execution. Missing DEFAULTS metadata means preserve, never infer DROP DEFAULT.
  • pub fn initial_pair(&self) -> AppResult<crate::migration_files::MigrationPair> — Fresh schema, generated entirely offline. Down intentionally destroys tables.
  • pub fn diff_pair( &self, schema: &str, current: &[CatalogTable], ) -> AppResult<Option<crate::migration_files::MigrationPair>> — Reversible additive diff. Existing type/nullability changes require authored steps. Only registered tables are managed; unrelated database tables are preserved.
  • pub fn diff( schema: &str, current: &[CatalogTable], desired: &[TableSpec], ) -> AppResult<ReviewPlan> — No implicit table/column drops, type casts or default/generated changes.

session

  • struct TimescaleSession

store_support

  • pub fn subject(value: &str) -> AppResult<uuid::Uuid>
  • pub fn valid(value: &str, max: usize) -> bool
  • pub fn invalid() -> AppError
  • pub fn backend(error: sqlx::Error) -> AppError
  • pub fn millis(time: SystemTime) -> AppResult<i64>

Impl map

  • iet_app::health::Probe for PostgresProbe
  • iet_core::SessionFactory for TimescaleDatabase
  • iet_db::Database for TimescaleDatabase
  • MigrationRunner for Migrations
  • Migrate for Observer<'_>
  • From<uuid::Uuid> for RecordId
  • From<i64> for RecordId
  • Session for TimescaleSession
  • TransactionSession for TimescaleSession
  • ConfigSource for Source

Depends on

External crates: chrono, rust_decimal, semver, serde, serde_json, sqlx, uuid

Used by

In this repository

  • examples/htmx-starter
  • examples/skills-only-app
  • examples/skills-only-app-task-tracker

Source: crates/features/iet-db-timescale