djogi 0.1.0-alpha.17

Model-first web framework for Rust — web-framework-agnostic core; Axum integration opt-in via the `axum` feature flag
Documentation

Djogi

A Postgres-native distributed data engine for Rust, built around model declarations. Define your models as structs, and Djogi derives the ORM, migrations, audit trail, and more from a single model definition.


Most Rust data libraries solve one slice — a query builder, migrations, or an audit log. Djogi treats the data tier as a single, model-first surface: typed queries, typed CTEs, schema evolution, relations, change tracking, row-level security, transport projections via visages, in-memory cache, cross-runtime predicate algebra, and spatial predicates all derive from your model declarations. You bring your own HTTP/application stack; Djogi owns the data layer end-to-end.

Why Postgres Only

Djogi targets PostgreSQL exclusively — permanently, not as a temporary limitation. Djogi depends on JSONB path operators for typed schema fields, advisory locks for migration safety, RETURNING for atomic round-trips, row-level locking, rich indexing, and transactional DDL. Abstracting over multiple databases would mean abandoning these capabilities or reimplementing them poorly. Every query Djogi generates targets Postgres directly; there is no lowest-common-denominator SQL layer.

Design Principles

  • Define once, derive everything#[derive(Model)] produces typed CRUD, migration diffs, audit hooks, and transport projections from a single struct declaration.
  • Explicit over implicit — No hidden middleware chains or convention-as-code. Behavior is visible in typed APIs and generated descriptors.
  • Postgres-first, not portable abstraction — Djogi maps core features directly to native Postgres capabilities instead of lowest-common-denominator SQL.
  • Your app stack, your handlers — Djogi integrates with Axum, Warp, Actix, Rocket, and others. Routing, middleware, and rendering remain yours.
  • Performance and safety defaults — Type-first APIs cover common production workloads, with explicit escape hatches available for exceptional requirements.

What You Get

Capability Guide
Models & schema derivation via #[derive(Model)] Models guide
Lazy typed queries with relations and eager loading Queries guide, Relations guide
Typed WITH / WITH RECURSIVE queries with cycle filtering Queries guide
Build-time migrations with advisory-locked runner Migrations guide
Typed JSONB columns preserving unknown fields JSONB guide
Spatial types and geographic predicates Spatial guide
Auth context, password hashing, multi-tenancy & RLS Auth guide, Tenancy guide
Audience-specific transport projections via visages, in-memory cache, and cross-runtime predicate algebra Visages guide
Transactions, outbox pattern, expressions & aggregates Transactions, Outbox, Expressions guides

Typed CTE Queries

Djogi now exposes typed Common Table Expressions directly from QuerySet: plain WITH terms through .with(...), recursive WITH RECURSIVE terms through .with_recursive(...), consumer selection through .from_cte(...), and Postgres cycle filtering through .cycle(...) plus .exclude_cycle_rows(). This covers the common “build a reusable subquery, then read from it” and “walk a hierarchy without dropping to raw SQL” cases inside the typed query surface.

use djogi::prelude::*;

let active_posts = Post::objects().filter(|f| f.published().eq(true));

let rows = Post::objects()
    .with("active_posts", active_posts)?
    .from_cte("active_posts")?
    .fetch_all(&mut ctx)
    .await?;

let ancestors = Category::objects()
    .with_recursive(
        "ancestors",
        Category::objects().filter(|f| f.id().eq(target_id)),
        RecursiveArm::<Category>::referencing("ancestors")
            .join_on("id", "parent_id")?,
    )?
    .from_cte("ancestors")?
    .fetch_all(&mut ctx)
    .await?;

When a query shape still falls outside the typed surface, the raw SQL escape hatches remain available. The point is that ordinary non-recursive and recursive CTE work no longer needs them.

Planned (not yet shipped): Rhai interactive shell for model inspection and query building; Maahi admin console — a Dioxus operations UI derived from your model descriptors.

Visage Subquery Predicates

Shipped in issues #309 / #446 / #447.

A visage is a typed projection of a model — a smaller struct that exposes only the fields relevant for a given audience or query. VisageQuerySet lets you build a subquery from a visage and embed it directly into a model filter without a round-trip.

Non-nullable fields

use djogi::prelude::*;

// Build a subquery projecting the `score` column from AuthorPublic visage rows
// where score >= 100.
let high_score_authors = AuthorPublic::filter(|a| a.score().gte(100))
    .selecting(AuthorPublic::score())
    .expect("no subquery-modifying calls made");

// Find all Posts whose author_id is in the high-score subquery.
let posts = Post::objects()
    .filter(|p| p.author_id().in_visage(high_score_authors))
    .fetch_all(&ctx)
    .await?;

Other predicates: not_in_visage, gt_any, gte_any, lt_any, lte_any, gt_all, gte_all, lt_all, lte_all.

Nullable fields

Nullable model fields (Option<V>) accept the same predicates. The subquery must project the inner value type V — build it from a non-nullable projected column:

// ScoreLog has `score: i32` (non-nullable).
// User has `score: Option<i32>` (nullable).
// Find all Users whose nullable score equals any score in the log:

let logged_scores = ScoreLogPublic::filter(|s| s.score().gte(0_i32))
    .selecting(ScoreLogPublic::score())
    .expect("no subquery-modifying calls");

// `logged_scores` projects `i32`, not `Option<i32>`.
// The nullable field `user.score: Option<i32>` accepts a VisageSubquery<_, i32>.
let users = User::objects()
    .filter(|u| u.score().in_visage(logged_scores))
    .fetch_all(&ctx)
    .await?;

The same nullable predicates also work inside a visage filter closure, not just a root-model one. A visage column accessor for a nullable field hands you the field handle bound to Option<V>, and it accepts the identical inner-type subquery:

// `UserPublic` is a visage of `User`; `score` is exposed and declared
// `Option<i32>` on the model. Inside a visage filter closure, the same
// `in_visage` (and `not_in_visage` / `*_any` / `*_all`) family is available
// on the nullable field, accepting a VisageSubquery<_, i32>.
let public_users = UserPublic::filter(|u| u.score().in_visage(logged_scores))
    .fetch_all(&ctx)
    .await?;

Design note: The subquery on the right-hand side of in_visage (and the rest of the family) is always built from a visage column via VisageQuerySet::selecting(...) — that is the only constructor of a VisageSubquery<_, U>. A visage column accessor preserves the field's declared type exactly: a field declared V yields a VisageColumn<_, V> (and a VisageSubquery<_, V>), while a field declared Option<V> yields a VisageColumn<_, Option<V>> (and a VisageSubquery<_, Option<V>>). Because the nullable predicates require the inner type U, the subquery source must be a non-nullable visage column — a field declared without Option<_> on the visage's backing struct. If a nullable visage column's inner values are needed as the subquery source, use a separate visage or visage field that declares that inner type without the Option wrapper. The nullable predicate family itself is available symmetrically on both the root-model filter surface (Model::objects().filter(...), where accessors return DjogiField) and the visage filter surface (Visage::filter(...), where accessors return FieldRef).

NULL semantics

Standard SQL NULL propagation applies:

  • col IN (subquery) — if the subquery is empty, returns FALSE for every row. If col is NULL, returns NULL (not FALSE).
  • col NOT IN (subquery)trap: if the subquery contains any NULL, the result is NULL for every row (no rows match). Use not_in_visage only with subqueries that project non-nullable columns, or apply an explicit IS NOT NULL filter on the subquery first.
  • col > ANY (subquery)TRUE if col is greater than at least one non-NULL row.
  • col > ALL (subquery) — vacuously TRUE when the subquery is empty (SQL standard).

Status

Current release: v0.1.0-alpha.16 (public alpha — API may change before v0.1.0 stable)

Project Layout

my-app/
  src/
    main.rs
    apps/
      vehicles/
        mod.rs              # djogi::apps! { ... }
        models.rs           # #[derive(Model)] structs + relations
        routes.rs           # handlers for your application stack
  build.rs                  # drift detection + migration generation
  Djogi.toml                # configuration
  migrations/               # managed by pipeline (git submodule)

djogi/                      # core data library crate
djogi-macros/               # proc macro crate
djogi-cli/                  # standalone `djogi` binary

Local Development

The .scripts/ directory is a git submodule pointing to TarunvirBains/scripts. Clone with submodules:

git clone --recurse-submodules https://github.com/TarunvirBains/djogi.git
# or, if you already cloned:
git submodule update --init --recursive

Reproduce CI locally:

./.scripts/ci-local.sh

Specification & Guides

The full specification lives in docs/spec/. Key docs:

Doc Covers
Models #[derive(Model)], field types, annotations
Query API QuerySet, conditions, filters
Relations ForeignKey, ManyToMany, through models
JSONB Schema Fields Jsonb<T>, unknown field preservation, subfield queries
Primary Keys HeerId, RanjId, custom PK integration
Migrations Drift detection, schema snapshots, differ
Logging CRUD audit trail, event tracing
Configuration & CLI Djogi.toml, CLI commands, app registration
Architecture Principles Framework boundaries and design decisions
Raw SQL Escape Hatches Bypass points for hand-rolled SQL and direct driver access
Scope & Boundaries What belongs in Djogi vs an app crate

Deep Links

  • Query aggregationrollup, cube, group_by_sets, and typed aggregate forms.
  • Enums — Postgres enum modeling via DjogiEnum.
  • Arrays — Array field operators and semantics.
  • Tracked fields — Dirty tracking and selective write behavior.
  • Optimistic locking — Version-based conflict control.
  • Apps — Multi-app registration and shared-cluster workflows.
  • Shell — Rhai shell trajectory and operational scripting context.
  • Maahi — Admin console and RBAC direction.
  • Design decisions — Long-form architecture rationale.
  • The hypothesis — Why model-first data tooling.